mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
864 lines
35 KiB
Rust
864 lines
35 KiB
Rust
use super::*;
|
|
|
|
pub(super) trait HumanReport {
|
|
fn print_human(&self) -> anyhow::Result<()>;
|
|
}
|
|
|
|
pub(super) fn print_report<T>(format: OutputFormat, report: &T) -> anyhow::Result<()>
|
|
where
|
|
T: Serialize + HumanReport,
|
|
{
|
|
match format {
|
|
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(report)?),
|
|
OutputFormat::Human => report.print_human()?,
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn print_json_value(
|
|
format: OutputFormat,
|
|
value: &serde_json::Value,
|
|
) -> anyhow::Result<()> {
|
|
match format {
|
|
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(value)?),
|
|
OutputFormat::Human => print_human_json_value(value)?,
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
impl HumanReport for serde_json::Value {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_human_json_value(self)
|
|
}
|
|
}
|
|
|
|
impl HumanReport for RepackReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title("UnityFS 重打包");
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_path_field("源 bundle", &self.source_bundle);
|
|
print_path_field("目标 bundle", &self.target_bundle);
|
|
print_field("操作数", self.operation_count);
|
|
print_field("源字节", self.source_bytes);
|
|
print_field("目标字节", self.target_bytes);
|
|
print_field("源 BLAKE3", &self.source_blake3);
|
|
print_field("目标 BLAKE3", &self.target_blake3);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for LocalizedPatchReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title("汉化 release 发布");
|
|
print_path_field("版本目录", &self.version_path);
|
|
print_path_field("current", &self.current_path);
|
|
print_path_field("状态文件", &self.state_path);
|
|
print_path_field("patch manifest", &self.patch_manifest_path);
|
|
print_field("变更文件数", self.files.len());
|
|
print_field("TextAsset 操作数", self.manifest.text_asset_operation_count);
|
|
print_field("校验文件数", self.integrity.verified_changed_file_count);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for LocalizedRollbackReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title("汉化 release 回滚");
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_field("回滚 release", &self.rolled_back_release_id);
|
|
print_optional_field("恢复 release", self.restored_release_id.as_deref());
|
|
print_path_field("汉化输出目录", &self.localized_output_root);
|
|
print_path_field("current", &self.current_path);
|
|
print_path_field("状态文件", &self.state_path);
|
|
print_path_field("删除版本目录", &self.removed_version_path);
|
|
print_optional_path_field("恢复 current 目标", self.restored_current_target.as_ref());
|
|
print_field("新状态", &self.state.status);
|
|
print_optional_field("当前 release", self.state.current_release_id.as_deref());
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for bat_infrastructure::LocalizedTranslationWorkflowReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title("汉化工作流状态");
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_field("官方 release", &self.official_release_id);
|
|
print_optional_field("汉化 release", self.current_release_id.as_deref());
|
|
print_field("汉化发布状态", &self.localized_release_status);
|
|
print_field("工作流状态", &self.translation_workflow_status);
|
|
print_field("工作流状态码", self.translation_workflow_status_code);
|
|
print_field("工作流标签", self.translation_workflow_label);
|
|
print_field("允许发布", format_bool(self.publish_allowed));
|
|
print_path_field("汉化输出目录", &self.localized_output_root);
|
|
print_path_field("状态文件", &self.state_path);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for bat_infrastructure::TranslationWorkerReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title("翻译 provider worker");
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_field("官方 release", &self.official_release_id);
|
|
print_field("provider", &self.provider);
|
|
print_field("回收 lease", self.recovered_lease_count);
|
|
print_field("领取任务", self.claimed_count);
|
|
print_field("完成任务", self.completed_count);
|
|
print_field("失败任务", self.failed_count);
|
|
print_field("已安排重试", self.retry_scheduled_count);
|
|
print_field("剩余任务", self.remaining_count);
|
|
print_path_field("Translation Memory", &self.translation_memory_path);
|
|
print_field("TM 可用", format_bool(self.translation_memory_available));
|
|
print_field("TM 命中 TextUnit", self.translation_memory_hit_count);
|
|
print_field("Provider TextUnit", self.provider_unit_count);
|
|
print_path_field("Glossary", &self.glossary_path);
|
|
print_field("Glossary 可用", format_bool(self.glossary_available));
|
|
print_field("Glossary blocking TextUnit", self.glossary_blocked_count);
|
|
for failure in &self.translation_memory_failures {
|
|
println!(" - TM: {failure}");
|
|
}
|
|
for failure in &self.glossary_failures {
|
|
println!(" - Glossary: {failure}");
|
|
}
|
|
for failure in &self.failures {
|
|
println!(
|
|
" - {} [{}] retryable={} {}",
|
|
failure.task_id,
|
|
failure.failure_class,
|
|
format_bool(failure.retryable),
|
|
failure.failure_reason
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn print_human_json_value(value: &serde_json::Value) -> anyhow::Result<()> {
|
|
if value.get("running").is_some() && value.get("state_dir").is_some() {
|
|
print_title("后台状态");
|
|
print_json_field(value, "status", "状态");
|
|
print_json_field(value, "message", "消息");
|
|
print_json_field(value, "running", "运行中");
|
|
print_json_field(value, "pid", "PID");
|
|
print_json_field(value, "daemon_state", "后台状态");
|
|
print_json_field(value, "rpc_available", "RPC 可用");
|
|
print_json_field(value, "stale_pid_file", "失效 PID");
|
|
print_json_field(value, "stale_socket", "失效 socket");
|
|
print_json_field(value, "last_update_status", "上次同步");
|
|
print_json_field(value, "last_success_unix_seconds", "最后成功时间");
|
|
print_json_field(value, "last_error", "上次错误");
|
|
print_json_field(value, "next_retry_seconds", "下次重试秒数");
|
|
print_json_field(value, "next_check_unix_seconds", "下次检查时间");
|
|
print_json_field(value, "current_stage", "当前阶段");
|
|
print_json_field(value, "current_message", "当前消息");
|
|
print_daemon_download_progress_json_summary(value.get("download_progress"));
|
|
print_json_field(value, "version_state_path", "版本状态路径");
|
|
print_daemon_version_state_json_summary(value.get("version_state"));
|
|
print_json_field(value, "resource_output_root", "资源目录");
|
|
print_json_field(value, "state_dir", "状态目录");
|
|
print_json_field(value, "socket_path", "socket");
|
|
print_json_field(value, "log_path", "日志");
|
|
print_json_field(value, "structured_log_path", "结构化日志");
|
|
print_json_field(value, "rotated_structured_log_paths", "轮转日志");
|
|
return Ok(());
|
|
}
|
|
if value.get("command").and_then(serde_json::Value::as_str) == Some("logs") {
|
|
print_title("后台日志");
|
|
print_json_field(value, "status", "状态");
|
|
print_json_field(value, "message", "消息");
|
|
print_json_field(value, "log_path", "日志");
|
|
print_json_field(value, "bytes", "字节");
|
|
print_json_field(value, "total_lines", "总行数");
|
|
print_json_field(value, "returned_lines", "返回行数");
|
|
if let Some(content) = value.get("content").and_then(serde_json::Value::as_str) {
|
|
if !content.is_empty() {
|
|
println!();
|
|
println!("{content}");
|
|
}
|
|
}
|
|
return Ok(());
|
|
}
|
|
if value.get("command").is_some() && value.get("status").is_some() {
|
|
print_title("后台命令");
|
|
print_json_field(value, "command", "命令");
|
|
print_json_field(value, "status", "状态");
|
|
print_json_field(value, "message", "消息");
|
|
print_json_field(value, "force", "force");
|
|
print_json_field(value, "state_dir", "状态目录");
|
|
print_json_field(value, "socket_path", "socket");
|
|
return Ok(());
|
|
}
|
|
println!("{}", serde_json::to_string_pretty(value)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn print_daemon_download_progress_json_summary(value: Option<&serde_json::Value>) {
|
|
let Some(value) = value else {
|
|
return;
|
|
};
|
|
if value.is_null() {
|
|
return;
|
|
}
|
|
print_field(
|
|
"下载进度",
|
|
format_daemon_download_progress_json(value)
|
|
.unwrap_or_else(|error| format!("无法解析:{error}")),
|
|
);
|
|
}
|
|
|
|
pub(super) fn format_daemon_download_progress_json(
|
|
value: &serde_json::Value,
|
|
) -> Result<String, String> {
|
|
let progress = serde_json::from_value::<DaemonDownloadProgress>(value.clone())
|
|
.map_err(|error| error.to_string())?;
|
|
Ok(format_daemon_download_progress(&progress))
|
|
}
|
|
|
|
fn print_daemon_version_state_json_summary(value: Option<&serde_json::Value>) {
|
|
let Some(value) = value else {
|
|
return;
|
|
};
|
|
if value.is_null() {
|
|
return;
|
|
}
|
|
match serde_json::from_value::<OfficialVersionState>(value.clone()) {
|
|
Ok(version_state) => print_daemon_version_state_summary(&version_state),
|
|
Err(error) => print_field("版本状态", format!("无法解析:{error}")),
|
|
}
|
|
}
|
|
|
|
fn print_daemon_version_state_summary(version_state: &OfficialVersionState) {
|
|
print_optional_field(
|
|
"当前完成版本",
|
|
version_state
|
|
.current_completed_version
|
|
.as_ref()
|
|
.map(|version| version.id.as_str()),
|
|
);
|
|
print_optional_field(
|
|
"正在拉取版本",
|
|
version_state
|
|
.in_progress_version
|
|
.as_ref()
|
|
.map(|version| version.id.as_str()),
|
|
);
|
|
print_optional_field(
|
|
"上一个可用版本",
|
|
version_state
|
|
.previous_available_version
|
|
.as_ref()
|
|
.map(|version| version.id.as_str()),
|
|
);
|
|
let historical_failures = visible_historical_failed_versions(version_state);
|
|
print_field("历史失败版本数", historical_failures.len());
|
|
if let Some(failed) = historical_failures.last() {
|
|
print_field("最近历史失败版本", &failed.version.id);
|
|
print_field("最近历史失败时间", failed.failed_unix_seconds);
|
|
print_field("最近历史失败原因", &failed.error);
|
|
}
|
|
}
|
|
|
|
pub(super) fn visible_historical_failed_versions(
|
|
version_state: &OfficialVersionState,
|
|
) -> Vec<&OfficialFailedVersionRecord> {
|
|
let in_progress = version_state.in_progress_version.as_ref();
|
|
version_state
|
|
.failed_versions
|
|
.iter()
|
|
.filter(|failed| {
|
|
!in_progress.is_some_and(|version| version_matches_for_status(&failed.version, version))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn version_matches_for_status(left: &OfficialVersionRecord, right: &OfficialVersionRecord) -> bool {
|
|
left.app_version == right.app_version
|
|
&& left.bundle_version == right.bundle_version
|
|
&& left.addressables_root == right.addressables_root
|
|
}
|
|
|
|
fn print_json_field(value: &serde_json::Value, key: &str, label: &str) {
|
|
let Some(value) = value.get(key) else {
|
|
return;
|
|
};
|
|
if value.is_null() {
|
|
return;
|
|
}
|
|
if let Some(value) = value.as_str() {
|
|
print_field(label, value);
|
|
} else {
|
|
print_field(label, value);
|
|
}
|
|
}
|
|
|
|
fn print_title(title: &str) {
|
|
println!("{title}");
|
|
}
|
|
|
|
fn print_field(label: &str, value: impl std::fmt::Display) {
|
|
println!(" {label:<18} {value}");
|
|
}
|
|
|
|
fn print_optional_field<T>(label: &str, value: Option<T>)
|
|
where
|
|
T: std::fmt::Display,
|
|
{
|
|
if let Some(value) = value {
|
|
print_field(label, value);
|
|
}
|
|
}
|
|
|
|
fn print_path_field(label: &str, value: &Path) {
|
|
print_field(label, value.display());
|
|
}
|
|
|
|
fn print_optional_path_field(label: &str, value: Option<&PathBuf>) {
|
|
if let Some(value) = value {
|
|
print_path_field(label, value);
|
|
}
|
|
}
|
|
|
|
fn print_list(label: &str, values: &[String], limit: usize) {
|
|
if values.is_empty() {
|
|
return;
|
|
}
|
|
println!(" {label}:");
|
|
for value in values.iter().take(limit) {
|
|
println!(" - {value}");
|
|
}
|
|
if values.len() > limit {
|
|
println!(" ... 还有 {} 项", values.len() - limit);
|
|
}
|
|
}
|
|
|
|
fn format_daemon_download_progress(progress: &DaemonDownloadProgress) -> String {
|
|
let status = progress.status.as_deref().unwrap_or("running");
|
|
if let Some(hash) = progress.official_hash.as_ref() {
|
|
return format!(
|
|
"{}/{} official_hash algorithm={} expected={} actual={} data={} hash={}",
|
|
progress.index,
|
|
progress.total,
|
|
hash.algorithm.as_str(),
|
|
hash.expected,
|
|
hash.actual,
|
|
hash.data_url,
|
|
hash.hash_url
|
|
);
|
|
}
|
|
if status == "failed" {
|
|
return format!(
|
|
"{}/{} failed kind={} http={} retryable={} attempts={} quarantined={} {}",
|
|
progress.index,
|
|
progress.total,
|
|
progress.failure_kind.as_deref().unwrap_or("unknown"),
|
|
progress
|
|
.failure_http_status
|
|
.map(|status| status.to_string())
|
|
.unwrap_or_else(|| "none".to_string()),
|
|
progress
|
|
.failure_retryable
|
|
.map(|retryable| retryable.to_string())
|
|
.unwrap_or_else(|| "unknown".to_string()),
|
|
progress
|
|
.failure_attempts
|
|
.map(|attempts| attempts.to_string())
|
|
.unwrap_or_else(|| "0".to_string()),
|
|
progress
|
|
.quarantined
|
|
.map(|quarantined| quarantined.to_string())
|
|
.unwrap_or_else(|| "false".to_string()),
|
|
progress.url
|
|
);
|
|
}
|
|
if let Some(verification) = progress.verification.as_ref() {
|
|
return format!(
|
|
"{}/{} {} bytes={} blake3={} zip_checked={} zip_verified={} {}",
|
|
progress.index,
|
|
progress.total,
|
|
status,
|
|
verification.actual_bytes,
|
|
verification.actual_blake3,
|
|
verification.zip_checked,
|
|
verification.zip_structure_verified,
|
|
progress.url
|
|
);
|
|
}
|
|
format!(
|
|
"{}/{} {} {}",
|
|
progress.index, progress.total, status, progress.url
|
|
)
|
|
}
|
|
|
|
fn print_verification_summary(summary: &OfficialVerificationSummary) {
|
|
println!(" 校验摘要:");
|
|
for line in verification_summary_lines(summary) {
|
|
println!(" - {line}");
|
|
}
|
|
}
|
|
|
|
pub(super) fn verification_summary_lines(summary: &OfficialVerificationSummary) -> Vec<String> {
|
|
vec![
|
|
format!(
|
|
"官方 .hash 强校验: {} 对 ({})",
|
|
summary.official_hash_verified_count, summary.official_hash_scope
|
|
),
|
|
format!(
|
|
"本地 BLAKE3 复用校验: {} 项通过, {} 项需修复 ({})",
|
|
summary.local_manifest_blake3_verified_count,
|
|
summary.local_manifest_repair_needed_count,
|
|
summary.local_manifest_blake3_scope
|
|
),
|
|
format!(
|
|
"ZIP 结构校验: {} 个 ZIP 通过 ({})",
|
|
summary.zip_structure_verified_count, summary.zip_structure_scope
|
|
),
|
|
]
|
|
}
|
|
|
|
fn format_bool(value: bool) -> &'static str {
|
|
if value {
|
|
"yes"
|
|
} else {
|
|
"no"
|
|
}
|
|
}
|
|
|
|
fn format_bytes(value: u64) -> String {
|
|
const KIB: f64 = 1024.0;
|
|
const MIB: f64 = 1024.0 * 1024.0;
|
|
const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
|
|
let value_f = value as f64;
|
|
if value_f >= GIB {
|
|
format!("{value_f:.2} GiB", value_f = value_f / GIB)
|
|
} else if value_f >= MIB {
|
|
format!("{value_f:.2} MiB", value_f = value_f / MIB)
|
|
} else if value_f >= KIB {
|
|
format!("{value_f:.2} KiB", value_f = value_f / KIB)
|
|
} else {
|
|
format!("{value} B")
|
|
}
|
|
}
|
|
|
|
fn platform_label(platform: PatchPlatform) -> &'static str {
|
|
match platform {
|
|
PatchPlatform::Windows => "Windows",
|
|
PatchPlatform::Android => "Android",
|
|
}
|
|
}
|
|
|
|
fn endpoint_kind_label_for_human(kind: YostarJpResourceEndpointKind) -> &'static str {
|
|
match kind {
|
|
YostarJpResourceEndpointKind::TableCatalog => "table_catalog",
|
|
YostarJpResourceEndpointKind::TableCatalogHash => "table_catalog_hash",
|
|
YostarJpResourceEndpointKind::AddressablesCatalog => "addressables_catalog",
|
|
YostarJpResourceEndpointKind::AddressablesCatalogHash => "addressables_catalog_hash",
|
|
YostarJpResourceEndpointKind::BundlePackingInfo => "bundle_packing_info",
|
|
YostarJpResourceEndpointKind::BundlePackingInfoHash => "bundle_packing_info_hash",
|
|
YostarJpResourceEndpointKind::MediaCatalog => "media_catalog",
|
|
YostarJpResourceEndpointKind::MediaCatalogHash => "media_catalog_hash",
|
|
}
|
|
}
|
|
|
|
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);
|
|
print_field(
|
|
"平台",
|
|
self.platforms
|
|
.iter()
|
|
.map(|platform| platform_label(*platform))
|
|
.collect::<Vec<_>>()
|
|
.join(", "),
|
|
);
|
|
print_field("需要下载", format_bool(self.should_download));
|
|
print_field(
|
|
"等待官方资源",
|
|
format_bool(self.waiting_for_official_resources),
|
|
);
|
|
print_field("首次同步", format_bool(self.is_initial));
|
|
print_field("强制刷新", format_bool(self.force));
|
|
print_field("本地审计", format_bool(self.audit_local));
|
|
print_field("自动修复", format_bool(self.repair));
|
|
print_field("dry-run", format_bool(self.dry_run));
|
|
print_path_field("官方资源目录", &self.output_root);
|
|
print_path_field("汉化输出目录", &self.localized_output_root);
|
|
print_field("汉化发布状态", self.localized_release_status.as_str());
|
|
print_path_field("汉化 current", &self.localized_current_path);
|
|
print_optional_path_field(
|
|
"汉化 published",
|
|
self.localized_published_version_path.as_ref(),
|
|
);
|
|
print_path_field("active release", &self.active_resource_root);
|
|
print_path_field("current", &self.current_path);
|
|
print_path_field("version state", &self.version_state_path);
|
|
print_optional_path_field("staging", self.staging_path.as_ref());
|
|
print_optional_path_field("published", self.published_version_path.as_ref());
|
|
print_path_field("snapshot", &self.snapshot_path);
|
|
print_path_field("manifest", &self.download_manifest);
|
|
print_optional_path_field("资源变更集", self.resource_change_set_path.as_ref());
|
|
print_optional_path_field("Crowdin handoff", self.crowdin_handoff_path.as_ref());
|
|
print_optional_path_field("解析缓存", self.parse_cache_path.as_ref());
|
|
print_optional_path_field("TextUnit 任务队列", self.textunit_task_queue_path.as_ref());
|
|
print_optional_path_field(
|
|
"Crowdin TextUnit 队列",
|
|
self.crowdin_textunit_queue_path.as_ref(),
|
|
);
|
|
print_optional_path_field("写入 snapshot", self.snapshot_written.as_ref());
|
|
print_optional_path_field(
|
|
"启动器引导产物",
|
|
self.launcher_bootstrap_artifact_path.as_ref(),
|
|
);
|
|
print_optional_path_field("bootstrap cache", self.bootstrap_cache_path.as_ref());
|
|
print_optional_field("bootstrap 命中", self.bootstrap_cache_hit.map(format_bool));
|
|
print_optional_field("计划 URL 数", self.download_url_count);
|
|
print_optional_field("资源数", self.resource_count);
|
|
print_field("已下载", self.downloaded_count);
|
|
print_field("已续传", self.resumed_count);
|
|
print_field("当前 manifest 复用", self.skipped_count);
|
|
print_field("历史 release 复用", self.release_reused_count);
|
|
print_field("CAS 复用", self.cas_reused_count);
|
|
print_field("复用量", format_bytes(self.reused_bytes));
|
|
print_field("传输量", format_bytes(self.transferred_bytes));
|
|
print_field("复用诊断", self.reuse_warnings.len());
|
|
for warning in &self.reuse_warnings {
|
|
println!(
|
|
" - 复用回退 [{}] {} {}",
|
|
warning.source, warning.url, warning.message
|
|
);
|
|
}
|
|
print_field("最终大小", format_bytes(self.final_bytes));
|
|
print_field("本地校验通过", self.local_manifest_verified_count);
|
|
print_field("需修复", self.local_manifest_repair_needed_count);
|
|
print_field("官方 hash 校验", self.official_seed_hash_verified_count);
|
|
print_verification_summary(&self.verification_summary);
|
|
if let Some(summary) = self.resource_change_summary.as_ref() {
|
|
print_field("新增资源", summary.added_count);
|
|
print_field("变更资源", summary.modified_count);
|
|
print_field("删除资源", summary.removed_count);
|
|
print_field("解析候选", summary.parse_candidate_count);
|
|
print_field("Crowdin 候选", summary.translation_candidate_count);
|
|
}
|
|
if let Some(summary) = self.parse_summary.as_ref() {
|
|
print_field("解析缓存条目", summary.cache_entry_count);
|
|
print_field("解析成功 bundle", summary.parsed_bundle_count);
|
|
print_field("解析复用", summary.skipped_unchanged_count);
|
|
print_field("解析不支持", summary.unsupported_count);
|
|
print_field("解析失败", summary.failed_count);
|
|
print_field("TextAsset", summary.text_asset_count);
|
|
print_field("TextUnit", summary.text_unit_count);
|
|
print_field("二进制 TextAsset", summary.skipped_binary_text_asset_count);
|
|
print_field("TextUnit 诊断", summary.text_unit_error_count);
|
|
}
|
|
if let Some(summary) = self.textunit_task_summary.as_ref() {
|
|
print_field("TextUnit 资源候选", summary.resource_candidate_count);
|
|
print_field("TextUnit 解析条目", summary.parse_entry_count);
|
|
print_field("TextUnit 任务", summary.queued_task_count);
|
|
print_field("增量 TextUnit", summary.text_unit_count);
|
|
print_field("TextUnit 无解析", summary.skipped_no_parse_entry_count);
|
|
print_field("TextUnit 无文本", summary.skipped_no_text_unit_count);
|
|
print_field("TextUnit 解析失败", summary.skipped_parse_failed_count);
|
|
print_field("TextUnit 不支持", summary.skipped_unsupported_count);
|
|
}
|
|
print_field("catalog marker", self.addressables_marker_checked_count);
|
|
if !self.unavailable_endpoints.is_empty() {
|
|
let unavailable = self
|
|
.unavailable_endpoints
|
|
.iter()
|
|
.map(|endpoint| {
|
|
format!(
|
|
"{}{} kind={} http={} {}",
|
|
endpoint_kind_label_for_human(endpoint.kind),
|
|
endpoint
|
|
.platform
|
|
.map(|platform| format!(" ({})", platform_label(platform)))
|
|
.unwrap_or_default(),
|
|
endpoint.error_kind,
|
|
endpoint
|
|
.http_status
|
|
.map(|status| status.to_string())
|
|
.unwrap_or_else(|| "none".to_string()),
|
|
endpoint.url
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
print_list("不可用官方 endpoint", &unavailable, 8);
|
|
}
|
|
print_list("变更 endpoint", &self.changed_endpoint_urls, 8);
|
|
print_list("计划 URL", &self.download_urls, 8);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<T> HumanReport for CommandReport<T>
|
|
where
|
|
T: Serialize + HumanReport,
|
|
{
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
self.data.print_human()
|
|
}
|
|
}
|
|
|
|
impl HumanReport for PatchApplyReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_field("Patch 类型", self.kind.as_str());
|
|
print_path_field("源文件", &self.source_path);
|
|
print_path_field("Patch 文件", &self.patch_path);
|
|
print_path_field("目标文件", &self.target_path);
|
|
print_field("源字节", self.source_size);
|
|
print_field("Patch 字节", self.patch_size);
|
|
print_field("目标字节", self.target_size);
|
|
print_field("源 BLAKE3", &self.source_blake3);
|
|
print_field("Patch BLAKE3", &self.patch_blake3);
|
|
print_field("目标 BLAKE3", &self.target_blake3);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for UnityFsPatchReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_path_field("源 bundle", &self.bundle_path);
|
|
print_field("Serialized 文件", &self.serialized_file_path);
|
|
print_field("Path ID", self.path_id);
|
|
print_optional_field("字段路径", self.field_path.as_deref());
|
|
print_path_field("目标 bundle", &self.target_path);
|
|
print_field("源字节", self.source_size);
|
|
print_field("替换字节", self.replacement_size);
|
|
print_field("目标字节", self.target_size);
|
|
print_field("源 BLAKE3", &self.source_blake3);
|
|
print_field("替换 BLAKE3", &self.replacement_blake3);
|
|
print_field("目标 BLAKE3", &self.target_blake3);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for DaemonStartReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("状态", self.status);
|
|
print_field("PID", self.pid);
|
|
print_path_field("资源目录", &self.resource_output_root);
|
|
print_path_field("汉化目录", &self.localized_output_root);
|
|
print_path_field("状态目录", &self.state_dir);
|
|
print_path_field("socket", &self.socket_path);
|
|
print_path_field("日志", &self.log_path);
|
|
print_path_field("结构化日志", &self.structured_log_path);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for DaemonStatusReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title("后台状态");
|
|
print_field("状态", self.status);
|
|
print_field("消息", self.message);
|
|
print_field("运行中", format_bool(self.running));
|
|
print_optional_field("PID", self.pid);
|
|
print_optional_field("后台状态", self.daemon_state.as_deref());
|
|
print_field("RPC 可用", format_bool(self.rpc_available));
|
|
print_field("失效 PID", format_bool(self.stale_pid_file));
|
|
print_field("失效 socket", format_bool(self.stale_socket));
|
|
print_optional_field("上次同步", self.last_update_status.as_deref());
|
|
print_optional_field("最后成功时间", self.last_success_unix_seconds);
|
|
print_optional_field("上次错误", self.last_error.as_deref());
|
|
print_optional_field("下次重试秒数", self.next_retry_seconds);
|
|
print_optional_field("下次检查时间", self.next_check_unix_seconds);
|
|
print_optional_field("当前阶段", self.current_stage.as_deref());
|
|
print_optional_field("当前消息", self.current_message.as_deref());
|
|
if let Some(progress) = self.download_progress.as_ref() {
|
|
print_field("下载进度", format_daemon_download_progress(progress));
|
|
}
|
|
if let Some(version_state) = self.version_state.as_ref() {
|
|
print_daemon_version_state_summary(version_state);
|
|
}
|
|
print_optional_path_field("资源目录", self.resource_output_root.as_ref());
|
|
print_optional_path_field("汉化目录", self.localized_output_root.as_ref());
|
|
print_optional_path_field("版本状态", self.version_state_path.as_ref());
|
|
print_path_field("状态目录", &self.state_dir);
|
|
print_path_field("socket", &self.socket_path);
|
|
print_optional_path_field("日志", self.log_path.as_ref());
|
|
print_optional_path_field("结构化日志", self.structured_log_path.as_ref());
|
|
let rotated = self
|
|
.rotated_structured_log_paths
|
|
.iter()
|
|
.map(|path| path.display().to_string())
|
|
.collect::<Vec<_>>();
|
|
print_list("轮转日志", &rotated, 5);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for DaemonStopReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("状态", self.status);
|
|
print_field("已停止", format_bool(self.stopped));
|
|
print_optional_field("PID", self.pid);
|
|
print_path_field("状态目录", &self.state_dir);
|
|
print_path_field("socket", &self.socket_path);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for DaemonControlReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_field("策略", self.strategy);
|
|
print_optional_field("旧 PID", self.previous_pid);
|
|
print_field("PID", self.pid);
|
|
print_path_field("资源目录", &self.resource_output_root);
|
|
print_path_field("状态目录", &self.state_dir);
|
|
print_path_field("socket", &self.socket_path);
|
|
print_path_field("日志", &self.log_path);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for VerifyCommandReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_field("健康", format_bool(self.healthy));
|
|
print_field("远端状态", &self.remote_update_status);
|
|
print_path_field("校验资源目录", &self.verified_resource_root);
|
|
print_optional_field("计划 URL 数", self.planned_url_count);
|
|
print_field("计划异常数", self.expected_plan_failure_count);
|
|
print_field("本地 manifest 项", self.local_manifest_entry_count);
|
|
print_field("本地校验通过", self.local_manifest_verified_count);
|
|
print_field("本地失败数", self.local_manifest_failure_count);
|
|
print_field("官方 hash 对", self.official_hash_pair_count);
|
|
print_field("官方 hash 通过", self.official_hash_verified_count);
|
|
print_verification_summary(&self.verification_summary);
|
|
if !self.official_hash_errors.is_empty() {
|
|
print_list("官方 hash 错误", &self.official_hash_errors, 8);
|
|
}
|
|
if !self.failures.is_empty() {
|
|
println!(" 失败项:");
|
|
for item in self.failures.iter().take(12) {
|
|
println!(" - {} -> {}", item.status, item.destination.display());
|
|
}
|
|
if self.failures.len() > 12 {
|
|
println!(" ... 还有 {} 项", self.failures.len() - 12);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for LogsReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("状态", self.status);
|
|
print_path_field("日志", &self.log_path);
|
|
print_field("存在", format_bool(self.exists));
|
|
print_field("为空", format_bool(self.empty));
|
|
print_field("字节", self.bytes);
|
|
print_field("总行数", self.total_lines);
|
|
print_field("返回行数", self.returned_lines);
|
|
if !self.content.is_empty() {
|
|
println!();
|
|
println!("{}", self.content);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for DoctorReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_field("健康", format_bool(self.healthy));
|
|
println!(" 检查:");
|
|
for check in &self.checks {
|
|
println!(
|
|
" [{}] {} - {}",
|
|
if check.ok { "OK" } else { "FAIL" },
|
|
check.name,
|
|
check.message
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for DoctorCasReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_field("健康", format_bool(self.healthy));
|
|
print_path_field("CAS 目录", &self.cas_root);
|
|
print_path_field("对象目录", &self.objects_dir);
|
|
print_path_field("元数据库", &self.metadata_db_path);
|
|
print_field("对象数", self.object_count);
|
|
print_field("总字节", self.total_size);
|
|
print_field("无效对象文件", self.invalid_object_count);
|
|
println!(" 检查:");
|
|
for check in &self.checks {
|
|
println!(
|
|
" [{}] {} - {}",
|
|
if check.ok { "OK" } else { "FAIL" },
|
|
check.name,
|
|
check.message
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for CleanStableReport {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_path_field("资源目录", &self.output_root);
|
|
print_path_field("状态目录", &self.state_dir);
|
|
if !self.removed_paths.is_empty() {
|
|
println!(" 已清理:");
|
|
for path in &self.removed_paths {
|
|
println!(" - {}", path.display());
|
|
}
|
|
}
|
|
if !self.skipped_paths.is_empty() {
|
|
println!(" 已跳过:");
|
|
for path in &self.skipped_paths {
|
|
println!(" - {}", path.display());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl HumanReport for DaemonRpcAck {
|
|
fn print_human(&self) -> anyhow::Result<()> {
|
|
print_title(self.message);
|
|
print_field("命令", self.command);
|
|
print_field("状态", self.status);
|
|
print_optional_field("force", self.force.map(format_bool));
|
|
print_path_field("状态目录", &self.state_dir);
|
|
print_path_field("socket", &self.socket_path);
|
|
Ok(())
|
|
}
|
|
}
|