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
+578 -16
View File
@@ -8,22 +8,26 @@ use bat_infrastructure::DEFAULT_DOWNLOAD_CONCURRENCY;
use bat_infrastructure::{
apply_patch_file, apply_unityfs_field_patch_file, apply_unityfs_string_field_patch_file,
apply_unityfs_text_asset_patch_file, changed_endpoint_urls, diff_extended_snapshot,
gc_orphan_staging, lexical_absolute, open_append_file, read_download_manifest_at,
export_translation_workbench, gc_orphan_staging, lexical_absolute,
localized_text_asset_patches, open_append_file, read_download_manifest_at,
read_file_no_symlink, read_localized_patch_manifest_at, read_localized_version_state,
read_parse_cache_at, read_snapshot, read_textunit_index_at, read_version_state,
redact_proxy_url, resolve_curl_proxy, validate_output_root, validate_runtime_state_dir,
write_file_atomic, CurlProxyConfig, CurlProxyMode, OfficialEndpointMarkerRole,
OfficialFailedVersionRecord, OfficialResourceHashVerification, OfficialResourceVerification,
OfficialServerInfoSource, OfficialTextUnitQuery, OfficialTextUnitTaskQuery,
OfficialUpdateConfig, OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService,
OfficialUpdateSnapshot, OfficialUpdateStatus, OfficialVerificationSummary,
OfficialVersionRecord, OfficialVersionState, PatchApplyKind, PatchApplyParams,
PatchApplyReport, ReleaseFlowStatusCode, SqliteResourceRepository,
SqliteTranslationTaskRepository, TranslationTaskStatus, UnityFsFieldPatchParams,
UnityFsPatchReport, UnityFsStringFieldPatchParams, UnityFsTextAssetPatchParams,
LOCALIZED_CURRENT_LINK, LOCALIZED_PATCH_MANIFEST_FILE, LOCALIZED_VERSIONS_DIR,
LOCALIZED_VERSION_STATE_FILE, MAX_DOWNLOAD_CONCURRENCY, MIN_DOWNLOAD_CONCURRENCY,
OFFICIAL_PARSE_CACHE_FILE, OFFICIAL_TEXTUNIT_INDEX_FILE, PRIVATE_FILE_MODE,
read_parse_cache_at, read_snapshot, read_textunit_index_at, read_translation_workbench,
read_version_state, redact_proxy_url, repack_bundle, resolve_curl_proxy, set_translation,
validate_output_root, validate_runtime_state_dir, write_file_atomic,
write_official_textunit_queues, CurlProxyConfig, CurlProxyMode, LocalizedPatchConfig,
LocalizedPatchReport, LocalizedPatchService, OfficialEndpointMarkerRole,
OfficialFailedVersionRecord, OfficialParseCacheService, OfficialParseConfig,
OfficialResourceHashVerification, OfficialResourceVerification, OfficialServerInfoSource,
OfficialTextUnitQuery, OfficialTextUnitTaskQuery, OfficialUpdateConfig, OfficialUpdateProgress,
OfficialUpdateReport, OfficialUpdateService, OfficialUpdateSnapshot, OfficialUpdateStatus,
OfficialVerificationSummary, OfficialVersionRecord, OfficialVersionState, PatchApplyKind,
PatchApplyParams, PatchApplyReport, ReleaseFlowStatusCode, RepackReport,
SqliteResourceRepository, SqliteTranslationTaskRepository, TranslationTaskStatus,
UnityFsFieldPatchParams, UnityFsPatchReport, UnityFsStringFieldPatchParams,
UnityFsTextAssetPatchParams, LOCALIZED_CURRENT_LINK, LOCALIZED_PATCH_MANIFEST_FILE,
LOCALIZED_VERSIONS_DIR, LOCALIZED_VERSION_STATE_FILE, MAX_DOWNLOAD_CONCURRENCY,
MIN_DOWNLOAD_CONCURRENCY, OFFICIAL_PARSE_CACHE_FILE, OFFICIAL_TEXTUNIT_INDEX_FILE,
PRIVATE_FILE_MODE,
};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
@@ -46,16 +50,23 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
mod patch_commands;
#[path = "readonly_query.rs"]
mod readonly_query;
#[path = "schedule_commands.rs"]
mod schedule_commands;
#[path = "task_registry.rs"]
mod task_registry;
#[path = "translation_query.rs"]
mod translation_query;
#[path = "workflow_commands.rs"]
mod workflow_commands;
use patch_commands::{
is_write_patch_command, run_write_patch_command, validate_write_patch_options,
};
#[cfg(test)]
use readonly_query::run_readonly_query_command_with_rpc;
use readonly_query::{run_readonly_query_command, validate_readonly_query_options};
use schedule_commands::{
run_schedule_add, run_schedule_list, run_schedule_remove, run_schedule_run, run_schedule_update,
};
use task_registry::{
run_task_worker, CancelOutcome, DaemonTaskContext, TaskJob, TaskKind, TaskRegistry,
};
@@ -67,6 +78,9 @@ use translation_query::{
build_translation_handoff_report, build_translation_tasks_report, textunit_query_json,
update_translation_task_status_report,
};
use workflow_commands::{
run_parse_once, run_publish_localized, run_repack, run_translate_once, run_translation_set,
};
const EXIT_ERROR: i32 = 1;
const EXIT_LOCKED: i32 = 75;
@@ -168,6 +182,52 @@ fn run() -> anyhow::Result<i32> {
}
Ok(0)
}
CliCommand::Pull => {
run_repeated_workflow(&options, "pull", |options| {
run_sync_command_foreground(options, "pull")
})?;
Ok(0)
}
CliCommand::Parse => {
run_repeated_workflow(&options, "parse", run_parse_once)?;
Ok(0)
}
CliCommand::Translate => {
run_repeated_workflow(&options, "translate", run_translate_once)?;
Ok(0)
}
CliCommand::TranslationSet => {
run_translation_set(&options)?;
Ok(0)
}
CliCommand::Repack => {
run_repack(&options)?;
Ok(0)
}
CliCommand::PublishLocalized => {
run_repeated_workflow(&options, "publish-localized", run_publish_localized)?;
Ok(0)
}
CliCommand::ScheduleList => {
run_schedule_list(&options)?;
Ok(0)
}
CliCommand::ScheduleAdd => {
run_schedule_add(&options)?;
Ok(0)
}
CliCommand::ScheduleUpdate => {
run_schedule_update(&options)?;
Ok(0)
}
CliCommand::ScheduleRemove => {
run_schedule_remove(&options)?;
Ok(0)
}
CliCommand::ScheduleRun => {
run_schedule_run(&options)?;
Ok(0)
}
CliCommand::Status => {
let _control_lock = DaemonControlLock::acquire(&options.state_dir)?;
print_daemon_status(&options.state_dir, options.output_format)?;
@@ -233,6 +293,34 @@ fn run() -> anyhow::Result<i32> {
}
}
fn run_repeated_workflow(
options: &CliOptions,
command_name: &'static str,
mut operation: impl FnMut(&CliOptions) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
let max_runs = if options.watch {
None
} else {
Some(options.run_count.unwrap_or(1))
};
let mut completed_runs = 0usize;
loop {
operation(options)?;
completed_runs += 1;
if max_runs.is_some_and(|limit| completed_runs >= limit) {
return Ok(());
}
if options.progress {
eprintln!(
"{command_name} 下一轮将在 {} 后执行(已完成 {} 轮)",
format_duration(options.interval),
completed_runs
);
}
thread::sleep(options.interval);
}
}
#[derive(Debug, Serialize)]
struct ErrorReport<'a> {
status: &'a str,
@@ -248,10 +336,31 @@ struct CliOptions {
command: CliCommand,
output_format: OutputFormat,
watch: bool,
run_count: Option<usize>,
interval_explicit: bool,
daemon: bool,
daemon_child: bool,
state_dir: PathBuf,
output_explicit: bool,
resource_root: Option<PathBuf>,
translation_file: Option<PathBuf>,
translation_id: Option<String>,
translation_text: Option<String>,
translation_text_file: Option<PathBuf>,
localized_release_id: Option<String>,
repack_spec: Option<PathBuf>,
schedule_group: Option<String>,
schedule_action: Option<String>,
schedule_id: Option<String>,
schedule_at_unix: Option<u64>,
schedule_delay: Option<Duration>,
schedule_every: Option<Duration>,
schedule_count: Option<usize>,
schedule_args: Vec<String>,
schedule_clear_args: bool,
schedule_clear_every: bool,
schedule_enabled: Option<bool>,
schedule_option_explicit: bool,
sync_option_explicit: bool,
proxy_option_explicit: bool,
interval: Duration,
@@ -309,10 +418,31 @@ impl Default for CliOptions {
command: CliCommand::Run,
output_format: OutputFormat::Human,
watch: false,
run_count: None,
interval_explicit: false,
daemon: false,
daemon_child: false,
state_dir: PathBuf::from(DEFAULT_DAEMON_STATE_DIR),
output_explicit: false,
resource_root: None,
translation_file: None,
translation_id: None,
translation_text: None,
translation_text_file: None,
localized_release_id: None,
repack_spec: None,
schedule_group: None,
schedule_action: None,
schedule_id: None,
schedule_at_unix: None,
schedule_delay: None,
schedule_every: None,
schedule_count: None,
schedule_args: Vec::new(),
schedule_clear_args: false,
schedule_clear_every: false,
schedule_enabled: None,
schedule_option_explicit: false,
sync_option_explicit: false,
proxy_option_explicit: false,
interval: Duration::from_secs(DEFAULT_WATCH_INTERVAL_SECONDS),
@@ -372,6 +502,17 @@ enum OutputFormat {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CliCommand {
Run,
Pull,
Parse,
Translate,
TranslationSet,
Repack,
PublishLocalized,
ScheduleList,
ScheduleAdd,
ScheduleUpdate,
ScheduleRemove,
ScheduleRun,
Status,
Stop,
Restart,
@@ -831,6 +972,11 @@ const RPC_METHOD_RESOURCE_REPAIR: &str = "resource.repair";
const RPC_METHOD_RESOURCE_MANIFEST: &str = "resource.manifest";
const RPC_METHOD_RESOURCE_INDEX: &str = "resource.index";
const RPC_METHOD_RESOURCE_LIST: &str = "resource.list";
const RPC_METHOD_SCHEDULE_LIST: &str = "schedule.list";
const RPC_METHOD_SCHEDULE_ADD: &str = "schedule.add";
const RPC_METHOD_SCHEDULE_UPDATE: &str = "schedule.update";
const RPC_METHOD_SCHEDULE_REMOVE: &str = "schedule.remove";
const RPC_METHOD_SCHEDULE_RUN: &str = "schedule.run";
const RPC_METHOD_PARSE_STATUS: &str = "parse.status";
const RPC_METHOD_PARSE_TEXT_UNITS: &str = "parse.text_units";
const RPC_METHOD_PARSE_ERRORS: &str = "parse.errors";
@@ -1616,6 +1762,77 @@ fn dispatch_rpc_method(
"resource.state",
build_resource_state_report(state_dir),
),
RPC_METHOD_SCHEDULE_LIST => rpc_envelope_from_result(
request_id,
RPC_METHOD_SCHEDULE_LIST,
schedule_commands::schedule_list_report(state_dir),
),
RPC_METHOD_SCHEDULE_ADD => {
let params = match rpc_struct_params::<schedule_commands::ScheduleMutationRequest>(
request.params.as_ref(),
RPC_METHOD_SCHEDULE_ADD,
) {
Ok(params) => params,
Err(error) => return rpc_envelope_error(request_id, error),
};
rpc_envelope_from_result(
request_id,
RPC_METHOD_SCHEDULE_ADD,
schedule_commands::schedule_add_report(state_dir, params),
)
}
RPC_METHOD_SCHEDULE_UPDATE => {
let params = match rpc_struct_params::<schedule_commands::ScheduleMutationRequest>(
request.params.as_ref(),
RPC_METHOD_SCHEDULE_UPDATE,
) {
Ok(params) => params,
Err(error) => return rpc_envelope_error(request_id, error),
};
rpc_envelope_from_result(
request_id,
RPC_METHOD_SCHEDULE_UPDATE,
schedule_commands::schedule_update_report(state_dir, params),
)
}
RPC_METHOD_SCHEDULE_REMOVE => {
let params = match rpc_struct_params::<schedule_commands::ScheduleMutationRequest>(
request.params.as_ref(),
RPC_METHOD_SCHEDULE_REMOVE,
) {
Ok(params) => params,
Err(error) => return rpc_envelope_error(request_id, error),
};
rpc_envelope_from_result(
request_id,
RPC_METHOD_SCHEDULE_REMOVE,
schedule_commands::schedule_remove_report(state_dir, params),
)
}
RPC_METHOD_SCHEDULE_RUN => {
let params = request
.params
.as_ref()
.map(|value| serde_json::from_value(value.clone()))
.transpose()
.map_err(|error| {
ApiError::new(
ErrorCode::RPC_INVALID_PARAMS,
RPC_METHOD_SCHEDULE_RUN,
format!("params 无效:{error}"),
)
});
let params = match params {
Ok(Some(params)) => params,
Ok(None) => schedule_commands::ScheduleRunRequest::default(),
Err(error) => return rpc_envelope_error(request_id, error),
};
rpc_envelope_from_result(
request_id,
RPC_METHOD_SCHEDULE_RUN,
schedule_commands::schedule_run_report(state_dir, params),
)
}
RPC_METHOD_RESOURCE_SYNC => {
let force = rpc_bool_param(request.params.as_ref(), "force").unwrap_or(false);
enqueue_task_envelope(tasks, TaskKind::Sync, force, request_id)
@@ -3837,6 +4054,42 @@ trait HumanReport {
fn print_human(&self) -> anyhow::Result<()>;
}
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(())
}
}
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("后台状态");
@@ -6180,6 +6433,11 @@ fn parse_args_with_env(
while let Some(flag) = args.next() {
match flag.as_str() {
"res" | "resource" | "resources" => parse_resource_command(&mut args, &mut options)?,
"parse" => parse_parse_command(&mut args, &mut options)?,
"i18n" | "tr" | "translation" | "translate" => {
parse_translation_command(&mut args, &mut options)?
}
"status" => {
ensure_command_not_set(options.command, "status")?;
options.command = CliCommand::Status;
@@ -6313,6 +6571,29 @@ fn parse_args_with_env(
PathBuf::from(next_option_value(&mut args, &flag)?);
options.output_explicit = true;
}
"--resource-root" => {
options.resource_root = Some(PathBuf::from(next_option_value(&mut args, &flag)?));
}
"--translation-file" | "--workbench" => {
options.translation_file =
Some(PathBuf::from(next_option_value(&mut args, &flag)?));
}
"--translation-id" => {
options.translation_id = Some(next_option_value(&mut args, &flag)?);
}
"--translated-text" => {
options.translation_text = Some(next_option_value(&mut args, &flag)?);
}
"--translated-file" => {
options.translation_text_file =
Some(PathBuf::from(next_option_value(&mut args, &flag)?));
}
"--localized-release-id" => {
options.localized_release_id = Some(next_option_value(&mut args, &flag)?);
}
"--repack-spec" => {
options.repack_spec = Some(PathBuf::from(next_option_value(&mut args, &flag)?));
}
"--import-repository" => {
options.config.import_repository = true;
options.sync_option_explicit = true;
@@ -6334,6 +6615,18 @@ fn parse_args_with_env(
"--state-dir" | "--pid-dir" => {
options.state_dir = PathBuf::from(next_option_value(&mut args, &flag)?);
}
"--run-count" | "--repeat" => {
let value = next_option_value(&mut args, &flag)?
.parse::<usize>()
.map_err(|error| anyhow::anyhow!("{flag} 无效:{error}"))?;
if value == 0 {
return Err(anyhow::anyhow!("{flag} 必须大于 0"));
}
options.run_count = Some(value);
}
"--once" => {
options.run_count = Some(1);
}
"--snapshot" => {
options.config.snapshot_path =
Some(PathBuf::from(next_option_value(&mut args, &flag)?));
@@ -6415,6 +6708,7 @@ fn parse_args_with_env(
}
"--interval" => {
options.interval = parse_duration(&next_option_value(&mut args, &flag)?)?;
options.interval_explicit = true;
options.sync_option_explicit = true;
}
"--interval-seconds" => {
@@ -6422,8 +6716,66 @@ fn parse_args_with_env(
.parse::<u64>()
.map_err(|error| anyhow::anyhow!("{flag} 的秒数无效:{error}"))?;
options.interval = Duration::from_secs(seconds);
options.interval_explicit = true;
options.sync_option_explicit = true;
}
"--schedule-id" | "--id" => {
options.schedule_id = Some(next_option_value(&mut args, &flag)?);
options.schedule_option_explicit = true;
}
"--schedule-action" | "--action" => {
options.schedule_action = Some(next_option_value(&mut args, &flag)?);
options.schedule_option_explicit = true;
}
"--schedule-at-unix" => {
let value = next_option_value(&mut args, &flag)?
.parse::<u64>()
.map_err(|error| anyhow::anyhow!("{flag} 无效:{error}"))?;
options.schedule_at_unix = Some(value);
options.schedule_option_explicit = true;
}
"--schedule-delay" => {
options.schedule_delay =
Some(parse_duration(&next_option_value(&mut args, &flag)?)?);
options.schedule_option_explicit = true;
}
"--schedule-every" => {
options.schedule_every =
Some(parse_duration(&next_option_value(&mut args, &flag)?)?);
options.schedule_option_explicit = true;
}
"--schedule-count" => {
let value = next_option_value(&mut args, &flag)?
.parse::<usize>()
.map_err(|error| anyhow::anyhow!("{flag} 无效:{error}"))?;
if value == 0 {
return Err(anyhow::anyhow!("--schedule-count 必须大于 0"));
}
options.schedule_count = Some(value);
options.schedule_option_explicit = true;
}
"--schedule-arg" => {
options
.schedule_args
.push(next_option_value(&mut args, &flag)?);
options.schedule_option_explicit = true;
}
"--schedule-clear-args" => {
options.schedule_clear_args = true;
options.schedule_option_explicit = true;
}
"--schedule-clear-every" => {
options.schedule_clear_every = true;
options.schedule_option_explicit = true;
}
"--schedule-enabled" => {
options.schedule_enabled = Some(true);
options.schedule_option_explicit = true;
}
"--schedule-disabled" => {
options.schedule_enabled = Some(false);
options.schedule_option_explicit = true;
}
"--error-retry" => {
options.error_retry_interval =
parse_duration(&next_option_value(&mut args, &flag)?)?;
@@ -6741,7 +7093,7 @@ fn parse_args_with_env(
options.progress = false;
options.banner = false;
}
CliCommand::Refresh | CliCommand::Verify | CliCommand::Repair => {
CliCommand::Pull | CliCommand::Refresh | CliCommand::Verify | CliCommand::Repair => {
if !options.config.auto_discover
&& options.config.server_info_source.is_none()
&& options.config.connection_group.is_none()
@@ -6756,6 +7108,52 @@ fn parse_args_with_env(
options.config.repair = false;
}
}
CliCommand::Parse | CliCommand::Translate | CliCommand::PublishLocalized => {
if options.daemon || options.daemon_child {
return Err(anyhow::anyhow!(
"parse/translate/publish-localized 使用 --watch 或 schedule,不支持 daemon"
));
}
if options.config.dry_run {
return Err(anyhow::anyhow!(
"parse/translate/publish-localized 不能使用 --dry-run"
));
}
}
CliCommand::TranslationSet | CliCommand::Repack => {
if options.watch || options.daemon || options.daemon_child {
return Err(anyhow::anyhow!("translation set/repack 只支持单次执行"));
}
if options.config.force || options.sync_option_explicit {
return Err(anyhow::anyhow!("translation set/repack 不接受资源同步选项"));
}
options.progress = false;
options.banner = false;
}
CliCommand::ScheduleList
| CliCommand::ScheduleAdd
| CliCommand::ScheduleUpdate
| CliCommand::ScheduleRemove
| CliCommand::ScheduleRun => {
if options.daemon || options.daemon_child {
return Err(anyhow::anyhow!("schedule 命令不能使用 daemon"));
}
if options.output_explicit
|| options.proxy_option_explicit
|| (options.config.force && !matches!(options.command, CliCommand::ScheduleRun))
|| options.config.dry_run
|| tools_are_non_default(&options.config, &options.env_baseline_config)
{
return Err(anyhow::anyhow!(
"schedule 命令只接受 --state-dir、--json/--human、调度选项和 schedule run 的 --watch/--interval/--force"
));
}
if options.interval_explicit && !matches!(options.command, CliCommand::ScheduleRun) {
return Err(anyhow::anyhow!("--interval 只适用于 schedule run 的轮询"));
}
options.progress = false;
options.banner = false;
}
CliCommand::Restart | CliCommand::Reload => {
if (options.sync_option_explicit
|| options.output_explicit
@@ -6787,6 +7185,35 @@ fn parse_args_with_env(
if options.error_retry_interval.is_zero() {
return Err(anyhow::anyhow!("watch 失败重试间隔必须大于 0"));
}
if options.run_count.is_some_and(|count| count > 1)
&& !options.interval_explicit
&& matches!(
options.command,
CliCommand::Pull
| CliCommand::Parse
| CliCommand::Translate
| CliCommand::PublishLocalized
)
{
return Err(anyhow::anyhow!(
"--run-count 大于 1 时必须显式指定 --interval"
));
}
if matches!(
options.command,
CliCommand::Pull | CliCommand::Parse | CliCommand::Translate | CliCommand::PublishLocalized
) {
if options.watch && options.run_count.is_some() {
return Err(anyhow::anyhow!(
"--watch 与 --run-count 不能同时指定;周期执行请使用 --watch"
));
}
if options.interval_explicit && !options.watch && options.run_count.unwrap_or(1) <= 1 {
return Err(anyhow::anyhow!(
"--interval 需要配合 --watch 或 --run-count 大于 1"
));
}
}
if (options.watch || options.daemon || options.daemon_child)
&& !options.quiet_up_to_date_explicit
{
@@ -6818,6 +7245,109 @@ fn parse_args_with_env(
Ok(options)
}
fn parse_resource_command(
args: &mut impl Iterator<Item = String>,
options: &mut CliOptions,
) -> anyhow::Result<()> {
let action = next_option_value(args, "resource")?;
match action.as_str() {
"pull" => {
ensure_command_not_set(options.command, "resource pull")?;
options.command = CliCommand::Pull;
}
"refresh" => {
ensure_command_not_set(options.command, "resource refresh")?;
options.command = CliCommand::Refresh;
}
"verify" => {
ensure_command_not_set(options.command, "resource verify")?;
options.command = CliCommand::Verify;
}
"repair" => {
ensure_command_not_set(options.command, "resource repair")?;
options.command = CliCommand::Repair;
}
"status" => {
ensure_command_not_set(options.command, "resource status")?;
options.command = CliCommand::ResourceIndex;
}
"index" => {
ensure_command_not_set(options.command, "resource index")?;
options.command = CliCommand::ResourceIndex;
}
"schedule" => {
options.schedule_group = Some("res".to_string());
parse_schedule_command(args, options)?
}
other => return Err(anyhow::anyhow!("未知 resource 二级命令:{other}")),
}
Ok(())
}
fn parse_parse_command(
args: &mut impl Iterator<Item = String>,
options: &mut CliOptions,
) -> anyhow::Result<()> {
let action = next_option_value(args, "parse")?;
let command = match action.as_str() {
"run" => CliCommand::Parse,
"status" => CliCommand::ParseStatus,
"text-units" => CliCommand::ParseTextUnits,
"errors" => CliCommand::ParseErrors,
"schedule" => {
options.schedule_group = Some("parse".to_string());
return parse_schedule_command(args, options);
}
"repack" => CliCommand::Repack,
other => return Err(anyhow::anyhow!("未知 parse 二级命令:{other}")),
};
ensure_command_not_set(options.command, &format!("parse {action}"))?;
options.command = command;
Ok(())
}
fn parse_translation_command(
args: &mut impl Iterator<Item = String>,
options: &mut CliOptions,
) -> anyhow::Result<()> {
let action = next_option_value(args, "translation")?;
let command = match action.as_str() {
"run" => CliCommand::Translate,
"export" => CliCommand::Translate,
"set" => CliCommand::TranslationSet,
"publish" => CliCommand::PublishLocalized,
"tasks" => CliCommand::TranslationTasks,
"handoff" => CliCommand::TranslationHandoff,
"status" => CliCommand::LocalizedStatus,
"schedule" => {
options.schedule_group = Some("i18n".to_string());
return parse_schedule_command(args, options);
}
other => return Err(anyhow::anyhow!("未知 translation 二级命令:{other}")),
};
ensure_command_not_set(options.command, &format!("translation {action}"))?;
options.command = command;
Ok(())
}
fn parse_schedule_command(
args: &mut impl Iterator<Item = String>,
options: &mut CliOptions,
) -> anyhow::Result<()> {
let action = next_option_value(args, "schedule")?;
let command = match action.as_str() {
"list" => CliCommand::ScheduleList,
"add" => CliCommand::ScheduleAdd,
"update" => CliCommand::ScheduleUpdate,
"remove" | "delete" => CliCommand::ScheduleRemove,
"run" => CliCommand::ScheduleRun,
other => return Err(anyhow::anyhow!("未知 schedule 二级命令:{other}")),
};
ensure_command_not_set(options.command, &format!("schedule {action}"))?;
options.command = command;
Ok(())
}
fn ensure_command_not_set(command: CliCommand, next: &str) -> anyhow::Result<()> {
if command == CliCommand::Run {
Ok(())
@@ -6902,6 +7432,15 @@ fn print_usage(binary: &str) {
eprintln!(" {binary} <COMMAND> [OPTIONS]");
eprintln!();
eprintln!("Commands:");
eprintln!(" res pull Pull official resources once or repeatedly");
eprintln!(" res schedule Manage resource pull schedules (CLI/RPC/dashboard)");
eprintln!(" parse run Parse current official release");
eprintln!(" parse repack Repack a UnityFS bundle from a JSON spec");
eprintln!(" i18n run Refresh offline translation work");
eprintln!(" i18n export Export an editable translation workbench");
eprintln!(" i18n set Update one translation workbench entry");
eprintln!(" i18n publish Publish a localized release");
eprintln!(" i18n schedule Manage translation schedules");
eprintln!(" refresh Run one update check, or ask a live daemon to refresh");
eprintln!(" verify Verify remote plan, local manifest, and official seed hashes");
eprintln!(" repair Redownload resources that fail local verification");
@@ -6930,6 +7469,10 @@ fn print_usage(binary: &str) {
eprintln!(" {binary} --auto-discover --dry-run");
eprintln!(" {binary} --auto-discover --watch");
eprintln!(" {binary} --auto-discover --daemon");
eprintln!(" {binary} res pull --auto-discover --run-count 3 --interval 1h");
eprintln!(" {binary} parse run --force --resource-root /tmp/bat-release");
eprintln!(" {binary} i18n export --translation-file /tmp/bat-workbench.json");
eprintln!(" {binary} i18n publish --translation-file /tmp/bat-workbench.json --force");
eprintln!(" {binary} status");
eprintln!(" {binary} refresh --force --json");
eprintln!();
@@ -6971,6 +7514,15 @@ fn print_usage(binary: &str) {
eprintln!(" --force Force download/refresh");
eprintln!(" --audit-local | --no-audit-local Enable/disable local manifest audit");
eprintln!(" --repair | --no-repair Enable/disable automatic repair");
eprintln!(" --run-count <N> Run pull/parse/translate/publish N times");
eprintln!(" --once Explicitly select one run");
eprintln!(" --resource-root <DIR> Use an explicit published official release root");
eprintln!(" --translation-file <PATH> Translation workbench JSON file");
eprintln!(" --translation-id <ID> TextUnit ID for i18n set");
eprintln!(" --translated-text <TEXT> Inline translation for i18n set");
eprintln!(" --translated-file <PATH> UTF-8 translation file for i18n set");
eprintln!(" --localized-release-id <ID> Explicit localized publication ID");
eprintln!(" --repack-spec <PATH> UnityFS batch repack JSON spec");
eprintln!();
eprintln!("Read-only queries:");
eprintln!(" --offset <N> Query offset for resource-index/parse-text-units/parse-errors/translation-tasks");
@@ -7031,6 +7583,16 @@ fn print_usage(binary: &str) {
eprintln!(" --quiet-up-to-date Suppress clean up-to-date reports");
eprintln!(" --no-quiet-up-to-date Always print reports");
eprintln!(" --tail <N> Log lines for logs command (default: 200)");
eprintln!(" --schedule-id <ID> Schedule identifier");
eprintln!(" --schedule-action <ACTION> Schedule action (pull/run/repack/publish)");
eprintln!(" --schedule-at-unix <SECONDS> First execution time");
eprintln!(" --schedule-delay <DURATION> Delay first execution from now");
eprintln!(" --schedule-every <DURATION> Period between executions");
eprintln!(" --schedule-count <N> Bounded execution count");
eprintln!(" --schedule-arg <ARG> Argument passed to scheduled child command");
eprintln!(" --schedule-clear-args Clear args during schedule update");
eprintln!(" --schedule-clear-every Convert a periodic plan to one-shot");
eprintln!(" --schedule-enabled/--schedule-disabled Enable/disable a schedule");
eprintln!();
eprintln!("Output:");
eprintln!(" --human Human-readable output (default)");