feat(bat): 完善工作流调度与 dashboard RPC
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

补全资源拉取、解析、翻译、重打包和本地化发布命令,支持单次、限定次数与周期调度。移除 TUI 计划并通过 schedule.* RPC 暴露给 bat-api dashboard。

Closes #43
This commit is contained in:
2026-08-03 22:18:52 +08:00
parent 3b103be8a9
commit 0784d5b532
27 changed files with 2931 additions and 58 deletions
@@ -0,0 +1,193 @@
use super::*;
pub(super) fn run_parse_once(options: &CliOptions) -> anyhow::Result<()> {
let (resource_root, release_id) = current_official_release(options)?;
let parse_config =
OfficialParseConfig::new(&resource_root, options.config.unzip_command.clone())
.with_force(options.config.force);
let parse_report = OfficialParseCacheService::new()
.run(&parse_config)
.map_err(anyhow::Error::msg)?;
let queue_report =
write_official_textunit_queues(&resource_root).map_err(anyhow::Error::msg)?;
let data = serde_json::json!({
"official_release_id": release_id,
"resource_root": resource_root,
"forced": options.config.force,
"parse": parse_report,
"translation_queue": queue_report,
});
print_report(
options.output_format,
&CommandReport {
command: "parse",
status: "completed",
message: "官方资源解析已执行",
data,
},
)
}
pub(super) fn run_translate_once(options: &CliOptions) -> anyhow::Result<()> {
let (resource_root, release_id) = current_official_release(options)?;
let queue = write_official_textunit_queues(&resource_root).map_err(anyhow::Error::msg)?;
let exported = options
.translation_file
.as_ref()
.map(|path| {
export_translation_workbench(&resource_root, release_id.clone(), path).map(
|workbench| {
serde_json::json!({
"path": path,
"entry_count": workbench.entries.len(),
})
},
)
})
.transpose()?;
let data = serde_json::json!({
"official_release_id": release_id,
"resource_root": resource_root,
"queue": queue,
"workbench": exported,
"provider": "offline",
"note": "当前 translate 只生成/刷新离线队列和可编辑工作台,不调用外部翻译 provider",
});
print_report(
options.output_format,
&CommandReport {
command: "translate",
status: "queued",
message: "翻译离线队列已刷新",
data,
},
)
}
pub(super) fn run_translation_set(options: &CliOptions) -> anyhow::Result<()> {
let path = options
.translation_file
.as_ref()
.ok_or_else(|| anyhow::anyhow!("translation-set 必须指定 --translation-file"))?;
let text = match (&options.translation_text, &options.translation_text_file) {
(Some(_), Some(_)) => {
return Err(anyhow::anyhow!(
"--translated-text 与 --translated-file 只能指定一个"
))
}
(Some(text), None) => text.clone(),
(None, Some(path)) => String::from_utf8(
read_file_no_symlink(path, "翻译文本文件")
.map_err(anyhow::Error::msg)?
.ok_or_else(|| anyhow::anyhow!("翻译文本文件不存在:{}", path.display()))?,
)?,
(None, None) => {
return Err(anyhow::anyhow!(
"translation-set 必须指定 --translated-text 或 --translated-file"
))
}
};
let entry_id = options
.translation_id
.as_deref()
.ok_or_else(|| anyhow::anyhow!("translation-set 必须指定 --translation-id"))?;
let entry = set_translation(path, entry_id, text)?;
let data = serde_json::json!({
"translation_file": path,
"entry": entry,
});
print_report(
options.output_format,
&CommandReport {
command: "translation-set",
status: "updated",
message: "翻译工作台条目已更新",
data,
},
)
}
pub(super) fn run_repack(options: &CliOptions) -> anyhow::Result<()> {
let spec = options
.repack_spec
.as_ref()
.ok_or_else(|| anyhow::anyhow!("repack 必须指定 --repack-spec"))?;
let report = repack_bundle(spec)?;
print_report(options.output_format, &report)
}
pub(super) fn run_publish_localized(options: &CliOptions) -> anyhow::Result<()> {
let translation_file = options
.translation_file
.as_ref()
.ok_or_else(|| anyhow::anyhow!("publish-localized 必须指定 --translation-file"))?;
let (resource_root, official_release_id) = current_official_release(options)?;
let workbench = read_translation_workbench(translation_file)?;
if workbench.official_release_id != official_release_id {
return Err(anyhow::anyhow!(
"翻译工作台 release={} 与当前官方 release={} 不一致;请重新导出",
workbench.official_release_id,
official_release_id
));
}
let expected_root = lexical_absolute(&resource_root).map_err(anyhow::Error::msg)?;
if workbench.official_resource_root != expected_root {
return Err(anyhow::anyhow!(
"翻译工作台资源根目录与当前 release 不一致;请重新导出"
));
}
let patches = localized_text_asset_patches(&resource_root, &workbench)?;
let localized_release_id = options.localized_release_id.clone().or_else(|| {
options
.config
.force
.then(|| format!("{}-manual-{}", official_release_id, unix_seconds_now()))
});
let mut config = LocalizedPatchConfig::new(
resource_root,
options.config.localized_output_root.clone(),
official_release_id,
patches,
)
.with_force(options.config.force);
if let Some(release_id) = localized_release_id {
config = config.with_localized_release_id(release_id);
}
let report = LocalizedPatchService::new().publish(&config)?;
print_report(options.output_format, &report)
}
fn current_official_release(options: &CliOptions) -> anyhow::Result<(PathBuf, String)> {
let state = read_version_state(&options.config.version_state_path())?;
let resource_root = if let Some(resource_root) = options.resource_root.clone() {
lexical_absolute(&resource_root).map_err(anyhow::Error::msg)?
} else {
state
.as_ref()
.and_then(|state| state.current_completed_version.as_ref())
.map(|record| record.resource_root.clone())
.unwrap_or(active_official_resource_root(&options.config.output_root)?)
};
let release_id = state
.as_ref()
.and_then(|state| state.current_completed_version.as_ref())
.filter(|_| options.resource_root.is_none())
.map(|record| record.id.clone())
.or_else(|| {
resource_root
.file_name()
.and_then(|name| name.to_str())
.map(str::to_string)
})
.ok_or_else(|| anyhow::anyhow!("无法从当前官方资源根目录确定 release id"))?;
if read_download_manifest_at(&resource_root)
.map_err(anyhow::Error::msg)?
.is_none()
{
return Err(anyhow::anyhow!(
"当前官方 release 缺少官方下载 manifest{}",
resource_root.display()
));
}
Ok((resource_root, release_id))
}