mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 11:34:59 +08:00
fix(sync): 固化 release flow 状态码
This commit is contained in:
@@ -15,11 +15,11 @@ use bat_infrastructure::{
|
||||
OfficialServerInfoSource, OfficialTextUnitQuery, OfficialUpdateConfig, OfficialUpdateProgress,
|
||||
OfficialUpdateReport, OfficialUpdateService, OfficialUpdateSnapshot, OfficialUpdateStatus,
|
||||
OfficialVerificationSummary, OfficialVersionRecord, OfficialVersionState, PatchApplyKind,
|
||||
PatchApplyParams, PatchApplyReport, SqliteResourceRepository, UnityFsFieldPatchParams,
|
||||
UnityFsPatchReport, UnityFsStringFieldPatchParams, UnityFsTextAssetPatchParams,
|
||||
LOCALIZED_CURRENT_LINK, LOCALIZED_PATCH_MANIFEST_FILE, LOCALIZED_VERSIONS_DIR,
|
||||
LOCALIZED_VERSION_STATE_FILE, OFFICIAL_PARSE_CACHE_FILE, OFFICIAL_TEXTUNIT_INDEX_FILE,
|
||||
PRIVATE_FILE_MODE,
|
||||
PatchApplyParams, PatchApplyReport, ReleaseFlowStatusCode, SqliteResourceRepository,
|
||||
UnityFsFieldPatchParams, UnityFsPatchReport, UnityFsStringFieldPatchParams,
|
||||
UnityFsTextAssetPatchParams, LOCALIZED_CURRENT_LINK, LOCALIZED_PATCH_MANIFEST_FILE,
|
||||
LOCALIZED_VERSIONS_DIR, LOCALIZED_VERSION_STATE_FILE, OFFICIAL_PARSE_CACHE_FILE,
|
||||
OFFICIAL_TEXTUNIT_INDEX_FILE, PRIVATE_FILE_MODE,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -649,6 +649,8 @@ struct DaemonStatusFile {
|
||||
#[serde(default)]
|
||||
current_stage: Option<String>,
|
||||
#[serde(default)]
|
||||
status_code: Option<String>,
|
||||
#[serde(default)]
|
||||
current_message: Option<String>,
|
||||
#[serde(default)]
|
||||
download_progress: Option<DaemonDownloadProgress>,
|
||||
@@ -2450,9 +2452,55 @@ fn read_daemon_resource_state(
|
||||
Ok((status_file, version_state))
|
||||
}
|
||||
|
||||
fn flow_status_fields(
|
||||
code: ReleaseFlowStatusCode,
|
||||
) -> (&'static str, &'static str, &'static str, bool, bool) {
|
||||
(
|
||||
code.status(),
|
||||
code.as_str(),
|
||||
code.phase(),
|
||||
code.terminal(),
|
||||
code.retryable(),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_resource_state_report(state_dir: &Path) -> anyhow::Result<serde_json::Value> {
|
||||
let (status_file, version_state) = read_daemon_resource_state(state_dir)?;
|
||||
let flow_status_code = status_file
|
||||
.as_ref()
|
||||
.and_then(|status| status.status_code.as_deref())
|
||||
.and_then(ReleaseFlowStatusCode::from_str)
|
||||
.or_else(|| {
|
||||
if version_state
|
||||
.as_ref()
|
||||
.and_then(|state| state.in_progress_version.as_ref())
|
||||
.is_some()
|
||||
{
|
||||
Some(ReleaseFlowStatusCode::OfficialDownloading)
|
||||
} else if version_state
|
||||
.as_ref()
|
||||
.is_some_and(|state| !state.failed_versions.is_empty())
|
||||
{
|
||||
Some(ReleaseFlowStatusCode::OfficialFailed)
|
||||
} else if version_state
|
||||
.as_ref()
|
||||
.and_then(|state| state.current_completed_version.as_ref())
|
||||
.is_some()
|
||||
{
|
||||
Some(ReleaseFlowStatusCode::OfficialPublished)
|
||||
} else {
|
||||
Some(ReleaseFlowStatusCode::OfficialUnavailable)
|
||||
}
|
||||
})
|
||||
.unwrap_or(ReleaseFlowStatusCode::OfficialUnavailable);
|
||||
let (status, status_code, status_phase, status_terminal, status_retryable) =
|
||||
flow_status_fields(flow_status_code);
|
||||
Ok(serde_json::json!({
|
||||
"status": status,
|
||||
"status_code": status_code,
|
||||
"status_phase": status_phase,
|
||||
"status_terminal": status_terminal,
|
||||
"status_retryable": status_retryable,
|
||||
"resource_output_root": status_file
|
||||
.as_ref()
|
||||
.map(|status| status.resource_output_root.clone()),
|
||||
@@ -2477,15 +2525,39 @@ fn build_catalog_status_report(state_dir: &Path) -> anyhow::Result<serde_json::V
|
||||
.and_then(|state| state.current_completed_version.as_ref());
|
||||
let snapshot = current.and_then(|record| read_snapshot(&record.snapshot_path).ok().flatten());
|
||||
let (Some(record), Some(snapshot)) = (current, snapshot) else {
|
||||
return Ok(serde_json::json!({ "available": false }));
|
||||
let (status, status_code, status_phase, status_terminal, status_retryable) =
|
||||
flow_status_fields(ReleaseFlowStatusCode::OfficialUnavailable);
|
||||
let (distribution_status, distribution_status_code, _, _, _) =
|
||||
flow_status_fields(ReleaseFlowStatusCode::DistributionBlocked);
|
||||
return Ok(serde_json::json!({
|
||||
"available": false,
|
||||
"status": status,
|
||||
"status_code": status_code,
|
||||
"status_phase": status_phase,
|
||||
"status_terminal": status_terminal,
|
||||
"status_retryable": status_retryable,
|
||||
"distribution_status": distribution_status,
|
||||
"distribution_status_code": distribution_status_code,
|
||||
}));
|
||||
};
|
||||
let official_seed_hash_marker_count = snapshot
|
||||
.endpoint_markers
|
||||
.iter()
|
||||
.filter(|marker| marker.role == OfficialEndpointMarkerRole::OfficialSeedHash)
|
||||
.count();
|
||||
let (status, status_code, status_phase, status_terminal, status_retryable) =
|
||||
flow_status_fields(ReleaseFlowStatusCode::OfficialPublished);
|
||||
let (distribution_status, distribution_status_code, _, _, _) =
|
||||
flow_status_fields(ReleaseFlowStatusCode::DistributionReady);
|
||||
Ok(serde_json::json!({
|
||||
"available": true,
|
||||
"status": status,
|
||||
"status_code": status_code,
|
||||
"status_phase": status_phase,
|
||||
"status_terminal": status_terminal,
|
||||
"status_retryable": status_retryable,
|
||||
"distribution_status": distribution_status,
|
||||
"distribution_status_code": distribution_status_code,
|
||||
"version": {
|
||||
"id": record.id,
|
||||
"completed_unix_seconds": record.completed_unix_seconds,
|
||||
@@ -2712,13 +2784,37 @@ fn build_parse_status_report(state_dir: &Path) -> anyhow::Result<serde_json::Val
|
||||
.as_ref()
|
||||
.and_then(|state| state.current_completed_version.as_ref());
|
||||
let Some(record) = current else {
|
||||
return Ok(serde_json::json!({ "available": false }));
|
||||
let (status, status_code, status_phase, status_terminal, status_retryable) =
|
||||
flow_status_fields(ReleaseFlowStatusCode::ParseBlockedOfficial);
|
||||
let (translation_status, translation_status_code, _, _, _) =
|
||||
flow_status_fields(ReleaseFlowStatusCode::TranslationUnavailable);
|
||||
return Ok(serde_json::json!({
|
||||
"available": false,
|
||||
"status": status,
|
||||
"status_code": status_code,
|
||||
"status_phase": status_phase,
|
||||
"status_terminal": status_terminal,
|
||||
"status_retryable": status_retryable,
|
||||
"translation_status": translation_status,
|
||||
"translation_status_code": translation_status_code,
|
||||
}));
|
||||
};
|
||||
let cache_path = record.resource_root.join(OFFICIAL_PARSE_CACHE_FILE);
|
||||
let Some(cache) = read_parse_cache_at(&record.resource_root).map_err(anyhow::Error::msg)?
|
||||
else {
|
||||
let (status, status_code, status_phase, status_terminal, status_retryable) =
|
||||
flow_status_fields(ReleaseFlowStatusCode::ParsePending);
|
||||
let (translation_status, translation_status_code, _, _, _) =
|
||||
flow_status_fields(ReleaseFlowStatusCode::TranslationUnavailable);
|
||||
return Ok(serde_json::json!({
|
||||
"available": false,
|
||||
"status": status,
|
||||
"status_code": status_code,
|
||||
"status_phase": status_phase,
|
||||
"status_terminal": status_terminal,
|
||||
"status_retryable": status_retryable,
|
||||
"translation_status": translation_status,
|
||||
"translation_status_code": translation_status_code,
|
||||
"current_version_id": record.id,
|
||||
"resource_root": record.resource_root,
|
||||
"cache_path": cache_path,
|
||||
@@ -2732,8 +2828,31 @@ fn build_parse_status_report(state_dir: &Path) -> anyhow::Result<serde_json::Val
|
||||
let textunit_index_path = record.resource_root.join(OFFICIAL_TEXTUNIT_INDEX_FILE);
|
||||
let textunit_index =
|
||||
read_textunit_index_at(&record.resource_root).map_err(anyhow::Error::msg)?;
|
||||
let parse_status_code = if cache.summary.failed_count > 0 {
|
||||
ReleaseFlowStatusCode::ParseCompletedWithErrors
|
||||
} else {
|
||||
ReleaseFlowStatusCode::ParseCompleted
|
||||
};
|
||||
let queue_summary = textunit_queue.as_ref().map(|queue| &queue.summary);
|
||||
let translation_status_code =
|
||||
if queue_summary.is_some_and(|summary| summary.queued_task_count > 0) {
|
||||
ReleaseFlowStatusCode::TranslationQueuedOffline
|
||||
} else {
|
||||
ReleaseFlowStatusCode::TranslationUnavailable
|
||||
};
|
||||
let (status, status_code, status_phase, status_terminal, status_retryable) =
|
||||
flow_status_fields(parse_status_code);
|
||||
let (translation_status, translation_status_code, _, _, _) =
|
||||
flow_status_fields(translation_status_code);
|
||||
Ok(serde_json::json!({
|
||||
"available": true,
|
||||
"status": status,
|
||||
"status_code": status_code,
|
||||
"status_phase": status_phase,
|
||||
"status_terminal": status_terminal,
|
||||
"status_retryable": status_retryable,
|
||||
"translation_status": translation_status,
|
||||
"translation_status_code": translation_status_code,
|
||||
"current_version_id": record.id,
|
||||
"resource_root": record.resource_root,
|
||||
"cache_path": cache_path,
|
||||
@@ -2880,11 +2999,19 @@ fn build_localized_status_report(
|
||||
let mut patch_file_count = None;
|
||||
let mut patch_text_asset_operation_count = None;
|
||||
let mut rollback_previous_current_target = None;
|
||||
let mut flow_status_code = if official_version_id.is_some() {
|
||||
ReleaseFlowStatusCode::LocalizedPending
|
||||
} else {
|
||||
ReleaseFlowStatusCode::LocalizedBlockedOfficial
|
||||
};
|
||||
|
||||
if let Some(localized_state) = state.as_ref() {
|
||||
matches_current_official_release = official_version_id
|
||||
.as_deref()
|
||||
.is_some_and(|id| localized_state.official_release_id == id);
|
||||
if official_version_id.is_some() && !matches_current_official_release {
|
||||
flow_status_code = ReleaseFlowStatusCode::LocalizedStale;
|
||||
}
|
||||
if localized_state.status == "localized" && matches_current_official_release {
|
||||
if let Some(release_id) = localized_state.current_release_id.as_deref() {
|
||||
let candidate = localized_root.join(LOCALIZED_VERSIONS_DIR).join(release_id);
|
||||
@@ -2907,15 +3034,22 @@ fn build_localized_status_report(
|
||||
&& patch_manifest_matches_release
|
||||
{
|
||||
status = "localized";
|
||||
flow_status_code = ReleaseFlowStatusCode::LocalizedPublished;
|
||||
published_version_path = Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let (_, status_code, status_phase, status_terminal, status_retryable) =
|
||||
flow_status_fields(flow_status_code);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"available": state.is_some(),
|
||||
"status": status,
|
||||
"status_code": status_code,
|
||||
"status_phase": status_phase,
|
||||
"status_terminal": status_terminal,
|
||||
"status_retryable": status_retryable,
|
||||
"official_current_version_id": official_version_id,
|
||||
"localized_output_root": localized_root,
|
||||
"state_path": state_path,
|
||||
@@ -3314,6 +3448,7 @@ struct DaemonStatusReport {
|
||||
last_error: Option<String>,
|
||||
next_retry_seconds: Option<u64>,
|
||||
current_stage: Option<String>,
|
||||
status_code: Option<String>,
|
||||
current_message: Option<String>,
|
||||
download_progress: Option<DaemonDownloadProgress>,
|
||||
version_state_path: Option<PathBuf>,
|
||||
@@ -3462,6 +3597,7 @@ fn start_daemon_with_args(
|
||||
last_error: None,
|
||||
next_retry_seconds: None,
|
||||
current_stage: None,
|
||||
status_code: None,
|
||||
current_message: None,
|
||||
download_progress: None,
|
||||
pending_scheduled_force: false,
|
||||
@@ -3691,6 +3827,9 @@ fn build_daemon_status_report(state_dir: &Path) -> anyhow::Result<DaemonStatusRe
|
||||
current_stage: status_file
|
||||
.as_ref()
|
||||
.and_then(|status| status.current_stage.clone()),
|
||||
status_code: status_file
|
||||
.as_ref()
|
||||
.and_then(|status| status.status_code.clone()),
|
||||
current_message: status_file
|
||||
.as_ref()
|
||||
.and_then(|status| status.current_message.clone()),
|
||||
@@ -4702,6 +4841,7 @@ impl HumanReport for OfficialUpdateReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title("官方资源同步");
|
||||
print_field("状态", self.update_status.as_str());
|
||||
print_field("状态码", self.status_code.as_str());
|
||||
print_field("应用版本", &self.app_version);
|
||||
print_optional_field("Bundle 版本", self.bundle_version.as_deref());
|
||||
print_field("连接组", &self.connection_group);
|
||||
@@ -5692,6 +5832,7 @@ fn update_daemon_status(state_dir: &Path, update: DaemonStatusUpdate<'_>) -> any
|
||||
last_error: None,
|
||||
next_retry_seconds: None,
|
||||
current_stage: None,
|
||||
status_code: None,
|
||||
current_message: None,
|
||||
download_progress: None,
|
||||
pending_scheduled_force: false,
|
||||
@@ -5701,6 +5842,21 @@ fn update_daemon_status(state_dir: &Path, update: DaemonStatusUpdate<'_>) -> any
|
||||
status.pid = std::process::id();
|
||||
status.state = update.state.to_string();
|
||||
status.updated_unix_seconds = unix_seconds_now();
|
||||
status.status_code = match update.state {
|
||||
"running" => Some(ReleaseFlowStatusCode::OfficialChecking.as_str().to_string()),
|
||||
"waiting" => Some(
|
||||
ReleaseFlowStatusCode::OfficialWaitingForResources
|
||||
.as_str()
|
||||
.to_string(),
|
||||
),
|
||||
"error" => Some(ReleaseFlowStatusCode::OfficialFailed.as_str().to_string()),
|
||||
"sleeping" => update
|
||||
.last_update_status
|
||||
.as_deref()
|
||||
.map(ReleaseFlowStatusCode::from_update_status)
|
||||
.map(|code| code.as_str().to_string()),
|
||||
_ => None,
|
||||
};
|
||||
status.last_update_status = update.last_update_status;
|
||||
status.last_error = update.last_error;
|
||||
status.next_retry_seconds = update.next_retry_seconds;
|
||||
@@ -5728,6 +5884,7 @@ fn update_daemon_progress(state_dir: &Path, event: &OfficialUpdateProgress) -> a
|
||||
status.pid = std::process::id();
|
||||
status.updated_unix_seconds = unix_seconds_now();
|
||||
status.current_stage = Some(event.stage.to_string());
|
||||
status.status_code = Some(event.status_code.as_str().to_string());
|
||||
status.current_message = Some(event.message.clone());
|
||||
status.download_progress = match (event.download_index, event.download_total) {
|
||||
(Some(index), Some(total)) => Some(DaemonDownloadProgress {
|
||||
@@ -6269,6 +6426,8 @@ impl RotatingStructuredLogger {
|
||||
"level": "info",
|
||||
"stage": event.stage,
|
||||
"stage_label": localized_stage(event.stage),
|
||||
"status_code": event.status_code.as_str(),
|
||||
"status_phase": event.status_code.phase(),
|
||||
"message": event.message.as_str(),
|
||||
"download": event.download_index.map(|index| serde_json::json!({
|
||||
"index": index,
|
||||
@@ -9788,6 +9947,7 @@ mod tests {
|
||||
last_error: None,
|
||||
next_retry_seconds: Some(60),
|
||||
current_stage: None,
|
||||
status_code: Some(ReleaseFlowStatusCode::OfficialUpToDate.as_str().to_string()),
|
||||
current_message: None,
|
||||
download_progress: None,
|
||||
pending_scheduled_force: false,
|
||||
@@ -9860,6 +10020,29 @@ mod tests {
|
||||
current_dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_resource_state_reports_status_code_for_bat_api() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let state_dir = temp.path().join("state");
|
||||
let output_root = temp.path().join("output");
|
||||
write_catalog_fixture(&state_dir, &output_root, "bundle-b2", None);
|
||||
|
||||
let envelope = dispatch_rpc_method(
|
||||
&rpc_request("resource.state", None),
|
||||
&state_dir,
|
||||
&new_daemon_control(),
|
||||
&test_task_context(),
|
||||
"req-state-1".to_string(),
|
||||
);
|
||||
let value = serde_json::to_value(&envelope).unwrap();
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["status"], "up_to_date");
|
||||
assert_eq!(value["data"]["status_code"], "official.up_to_date");
|
||||
assert_eq!(value["data"]["status_phase"], "official_sync");
|
||||
assert_eq!(value["data"]["status_terminal"], true);
|
||||
assert_eq!(value["data"]["last_update_status"], "up_to_date");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_catalog_status_unavailable_without_state() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
@@ -9873,6 +10056,12 @@ mod tests {
|
||||
let value = serde_json::to_value(&envelope).unwrap();
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["available"], false);
|
||||
assert_eq!(value["data"]["status"], "unavailable");
|
||||
assert_eq!(value["data"]["status_code"], "official.unavailable");
|
||||
assert_eq!(
|
||||
value["data"]["distribution_status_code"],
|
||||
"distribution.blocked"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -9893,6 +10082,12 @@ mod tests {
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["available"], true);
|
||||
assert_eq!(value["data"]["bundle_version"], "bundle-b2");
|
||||
assert_eq!(value["data"]["status"], "published");
|
||||
assert_eq!(value["data"]["status_code"], "official.published");
|
||||
assert_eq!(
|
||||
value["data"]["distribution_status_code"],
|
||||
"distribution.ready"
|
||||
);
|
||||
assert_eq!(value["data"]["version"]["id"], "v-current");
|
||||
assert_eq!(value["data"]["endpoint_count"], 0);
|
||||
assert_eq!(value["data"]["connection_group_name"], "Prod-Audit");
|
||||
@@ -9938,6 +10133,12 @@ mod tests {
|
||||
let value = serde_json::to_value(&envelope).unwrap();
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["available"], true);
|
||||
assert_eq!(value["data"]["status"], "completed");
|
||||
assert_eq!(value["data"]["status_code"], "parse.completed");
|
||||
assert_eq!(
|
||||
value["data"]["translation_status_code"],
|
||||
"translation.queued_offline"
|
||||
);
|
||||
assert_eq!(value["data"]["current_version_id"], "v-current");
|
||||
assert_eq!(value["data"]["previous_version_id"], "v-previous");
|
||||
assert_eq!(value["data"]["previous_snapshot_missing"], false);
|
||||
@@ -10475,6 +10676,12 @@ mod tests {
|
||||
let value = serde_json::to_value(&envelope).unwrap();
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["available"], false);
|
||||
assert_eq!(value["data"]["status"], "pending");
|
||||
assert_eq!(value["data"]["status_code"], "parse.pending");
|
||||
assert_eq!(
|
||||
value["data"]["translation_status_code"],
|
||||
"translation.unavailable"
|
||||
);
|
||||
assert_eq!(value["data"]["current_version_id"], "v-current");
|
||||
}
|
||||
|
||||
@@ -10543,6 +10750,7 @@ mod tests {
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["available"], true);
|
||||
assert_eq!(value["data"]["status"], "localized");
|
||||
assert_eq!(value["data"]["status_code"], "localized.published");
|
||||
assert_eq!(value["data"]["official_current_version_id"], "v-current");
|
||||
assert_eq!(value["data"]["matches_current_official_release"], true);
|
||||
assert_eq!(value["data"]["current_points_to_published_version"], true);
|
||||
@@ -10601,6 +10809,7 @@ mod tests {
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["available"], true);
|
||||
assert_eq!(value["data"]["status"], "not_localized");
|
||||
assert_eq!(value["data"]["status_code"], "localized.stale");
|
||||
assert_eq!(value["data"]["matches_current_official_release"], false);
|
||||
assert_eq!(
|
||||
value["data"]["published_version_path"],
|
||||
|
||||
Reference in New Issue
Block a user