mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 13:54:53 +08:00
@@ -23,14 +23,16 @@ use bat_infrastructure::{
|
||||
OfficialUpdateService, OfficialUpdateSnapshot, OfficialUpdateStatus,
|
||||
OfficialVerificationSummary, OfficialVersionRecord, OfficialVersionState, PatchApplyKind,
|
||||
PatchApplyParams, PatchApplyReport, ReleaseFlowStatusCode, RepackReport,
|
||||
SqliteResourceRepository, SqliteTranslationTaskRepository, TranslationTaskStatus,
|
||||
UnityFsFieldPatchParams, UnityFsPatchReport, UnityFsStringFieldPatchParams,
|
||||
UnityFsTextAssetPatchParams, CROWDIN_TEXTUNIT_QUEUE_FILE, LOCALIZED_CURRENT_LINK,
|
||||
SqliteResourceRepository, SqliteTranslationTaskRepository, TranslationProviderKind,
|
||||
TranslationTaskStatus, TranslationWorkerConfig, UnityFsFieldPatchParams, UnityFsPatchReport,
|
||||
UnityFsStringFieldPatchParams, UnityFsTextAssetPatchParams, CROWDIN_TEXTUNIT_QUEUE_FILE,
|
||||
DEFAULT_TRANSLATION_CONCURRENCY, DEFAULT_TRANSLATION_LEASE_SECONDS,
|
||||
DEFAULT_TRANSLATION_MAX_ATTEMPTS, DEFAULT_TRANSLATION_RETRY_BACKOFF, LOCALIZED_CURRENT_LINK,
|
||||
LOCALIZED_PATCH_MANIFEST_FILE, LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING,
|
||||
LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING_LABEL, LOCALIZED_VERSIONS_DIR,
|
||||
LOCALIZED_VERSION_STATE_FILE, MAX_DOWNLOAD_CONCURRENCY, MIN_DOWNLOAD_CONCURRENCY,
|
||||
OFFICIAL_PARSE_CACHE_FILE, OFFICIAL_TEXTUNIT_INDEX_FILE, OFFICIAL_TEXTUNIT_TASK_QUEUE_FILE,
|
||||
PRIVATE_FILE_MODE,
|
||||
LOCALIZED_VERSION_STATE_FILE, MAX_DOWNLOAD_CONCURRENCY, MAX_TRANSLATION_CONCURRENCY,
|
||||
MIN_DOWNLOAD_CONCURRENCY, MIN_TRANSLATION_CONCURRENCY, OFFICIAL_PARSE_CACHE_FILE,
|
||||
OFFICIAL_TEXTUNIT_INDEX_FILE, OFFICIAL_TEXTUNIT_TASK_QUEUE_FILE, PRIVATE_FILE_MODE,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -85,6 +87,7 @@ use workflow_commands::{
|
||||
run_parse_clear_cache, run_parse_once, run_publish_localized, run_repack, run_translate_once,
|
||||
run_translation_get, run_translation_proofread, run_translation_set,
|
||||
run_translation_task_update, run_translation_unset, run_translation_validate,
|
||||
run_translation_worker,
|
||||
};
|
||||
|
||||
const EXIT_ERROR: i32 = 1;
|
||||
@@ -229,6 +232,10 @@ fn run() -> anyhow::Result<i32> {
|
||||
run_translation_proofread(&options)?;
|
||||
Ok(0)
|
||||
}
|
||||
CliCommand::TranslationWorker => {
|
||||
run_repeated_workflow(&options, "translation-worker", run_translation_worker)?;
|
||||
Ok(0)
|
||||
}
|
||||
CliCommand::Repack => {
|
||||
run_repack(&options)?;
|
||||
Ok(0)
|
||||
@@ -378,6 +385,15 @@ struct CliOptions {
|
||||
translation_text_file: Option<PathBuf>,
|
||||
translation_failure_reason: Option<String>,
|
||||
translation_provider_run_id: Option<String>,
|
||||
translation_provider: Option<String>,
|
||||
translation_fixture: Option<PathBuf>,
|
||||
worker_concurrency: usize,
|
||||
worker_max_attempts: u32,
|
||||
worker_lease_seconds: u64,
|
||||
worker_retry_backoff: Duration,
|
||||
worker_max_tasks: Option<usize>,
|
||||
worker_id: Option<String>,
|
||||
translation_worker_option_explicit: bool,
|
||||
localized_release_id: Option<String>,
|
||||
repack_spec: Option<PathBuf>,
|
||||
schedule_group: Option<String>,
|
||||
@@ -463,6 +479,15 @@ impl Default for CliOptions {
|
||||
translation_text_file: None,
|
||||
translation_failure_reason: None,
|
||||
translation_provider_run_id: None,
|
||||
translation_provider: None,
|
||||
translation_fixture: None,
|
||||
worker_concurrency: DEFAULT_TRANSLATION_CONCURRENCY,
|
||||
worker_max_attempts: DEFAULT_TRANSLATION_MAX_ATTEMPTS,
|
||||
worker_lease_seconds: DEFAULT_TRANSLATION_LEASE_SECONDS,
|
||||
worker_retry_backoff: DEFAULT_TRANSLATION_RETRY_BACKOFF,
|
||||
worker_max_tasks: None,
|
||||
worker_id: None,
|
||||
translation_worker_option_explicit: false,
|
||||
localized_release_id: None,
|
||||
repack_spec: None,
|
||||
schedule_group: None,
|
||||
@@ -546,6 +571,7 @@ enum CliCommand {
|
||||
TranslationGet,
|
||||
TranslationUnset,
|
||||
TranslationTaskUpdate,
|
||||
TranslationWorker,
|
||||
TranslationProofread,
|
||||
Repack,
|
||||
PublishLocalized,
|
||||
@@ -1025,6 +1051,7 @@ const RPC_METHOD_TRANSLATION_TASKS: &str = "translation.tasks";
|
||||
const RPC_METHOD_TRANSLATION_HANDOFF: &str = "translation.handoff";
|
||||
const RPC_METHOD_TRANSLATION_TASK_UPDATE: &str = "translation.task.update";
|
||||
const RPC_METHOD_TRANSLATION_PROOFREAD: &str = "translation.proofread";
|
||||
const RPC_METHOD_TRANSLATION_WORKER_RUN: &str = "translation.worker.run";
|
||||
const RPC_METHOD_LOCALIZED_STATUS: &str = "localized.status";
|
||||
const RPC_METHOD_CATALOG_STATUS: &str = "catalog.status";
|
||||
const RPC_METHOD_CATALOG_VERSIONS: &str = "catalog.versions";
|
||||
@@ -2027,6 +2054,13 @@ fn dispatch_rpc_method(
|
||||
mark_localized_manual_proofreading_report(state_dir, &tasks.base_config)
|
||||
.and_then(|report| serde_json::to_value(report).map_err(anyhow::Error::from)),
|
||||
),
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN => {
|
||||
let config = match rpc_translation_worker_config(request.params.as_ref()) {
|
||||
Ok(config) => config,
|
||||
Err(error) => return rpc_envelope_error(request_id, error),
|
||||
};
|
||||
enqueue_translation_worker_envelope(tasks, config, request_id)
|
||||
}
|
||||
RPC_METHOD_LOCALIZED_STATUS => rpc_envelope_from_result(
|
||||
request_id,
|
||||
"localized.status",
|
||||
@@ -2211,7 +2245,9 @@ fn enqueue_task_envelope(
|
||||
.unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
|
||||
let job = TaskJob {
|
||||
id: task_id.clone(),
|
||||
kind,
|
||||
config,
|
||||
translation_worker_config: None,
|
||||
cancel,
|
||||
};
|
||||
if tasks.queue.send(job).is_err() {
|
||||
@@ -2237,6 +2273,50 @@ fn enqueue_task_envelope(
|
||||
)
|
||||
}
|
||||
|
||||
fn enqueue_translation_worker_envelope(
|
||||
tasks: &DaemonTaskContext,
|
||||
worker_config: TranslationWorkerConfig,
|
||||
request_id: String,
|
||||
) -> RpcEnvelope {
|
||||
let config = TaskKind::TranslationWorker.build_config(&tasks.base_config, false);
|
||||
let task_id = tasks.registry.create(TaskKind::TranslationWorker);
|
||||
let cancel = tasks
|
||||
.registry
|
||||
.cancel_flag(&task_id)
|
||||
.unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
|
||||
let job = TaskJob {
|
||||
id: task_id.clone(),
|
||||
kind: TaskKind::TranslationWorker,
|
||||
config,
|
||||
translation_worker_config: Some(worker_config.clone()),
|
||||
cancel,
|
||||
};
|
||||
if tasks.queue.send(job).is_err() {
|
||||
tasks.registry.update(&task_id, |record| {
|
||||
record.status = "failed";
|
||||
record.finished_at = Some(unix_seconds_now());
|
||||
record.error = Some(ApiError::new(
|
||||
ErrorCode::INTERNAL,
|
||||
"task.enqueue",
|
||||
"任务执行器不可用",
|
||||
));
|
||||
});
|
||||
return rpc_envelope_error(
|
||||
request_id,
|
||||
ApiError::new(ErrorCode::INTERNAL, "task.enqueue", "任务执行器不可用"),
|
||||
);
|
||||
}
|
||||
rpc_envelope_ok(
|
||||
request_id,
|
||||
"accepted",
|
||||
serde_json::json!({
|
||||
"task_id": task_id,
|
||||
"kind": RPC_METHOD_TRANSLATION_WORKER_RUN,
|
||||
"worker": worker_config,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn rpc_ack_value(
|
||||
command: &'static str,
|
||||
message: &'static str,
|
||||
@@ -3070,6 +3150,207 @@ fn rpc_struct_params<T: DeserializeOwned>(
|
||||
})
|
||||
}
|
||||
|
||||
fn rpc_translation_worker_config(
|
||||
params: Option<&serde_json::Value>,
|
||||
) -> Result<TranslationWorkerConfig, ApiError> {
|
||||
let empty = serde_json::json!({});
|
||||
let params = params.unwrap_or(&empty);
|
||||
if !params.is_object() {
|
||||
return Err(ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN,
|
||||
"params 必须是 JSON object",
|
||||
));
|
||||
}
|
||||
let provider = rpc_translation_worker_string_param(
|
||||
params,
|
||||
&["provider", "translation_provider"],
|
||||
"provider",
|
||||
)?
|
||||
.unwrap_or_else(|| TranslationProviderKind::Mock.as_str().to_string());
|
||||
let provider = TranslationProviderKind::parse(&provider).ok_or_else(|| {
|
||||
ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN,
|
||||
format!("不支持的 translation provider:{provider}"),
|
||||
)
|
||||
})?;
|
||||
let concurrency = rpc_translation_worker_usize_param(
|
||||
params,
|
||||
&[
|
||||
"concurrency",
|
||||
"worker_concurrency",
|
||||
"translation_concurrency",
|
||||
],
|
||||
"concurrency",
|
||||
)?
|
||||
.unwrap_or(DEFAULT_TRANSLATION_CONCURRENCY);
|
||||
let max_attempts = rpc_translation_worker_u32_param(
|
||||
params,
|
||||
&[
|
||||
"max_attempts",
|
||||
"worker_max_attempts",
|
||||
"translation_max_attempts",
|
||||
],
|
||||
"max_attempts",
|
||||
)?
|
||||
.unwrap_or(DEFAULT_TRANSLATION_MAX_ATTEMPTS);
|
||||
let lease_seconds = rpc_translation_worker_u64_param(
|
||||
params,
|
||||
&[
|
||||
"lease_seconds",
|
||||
"worker_lease_seconds",
|
||||
"translation_lease_seconds",
|
||||
],
|
||||
"lease_seconds",
|
||||
)?
|
||||
.unwrap_or(DEFAULT_TRANSLATION_LEASE_SECONDS);
|
||||
let retry_backoff_seconds = rpc_translation_worker_u64_param(
|
||||
params,
|
||||
&[
|
||||
"retry_backoff_seconds",
|
||||
"worker_retry_backoff_seconds",
|
||||
"translation_retry_backoff_seconds",
|
||||
],
|
||||
"retry_backoff_seconds",
|
||||
)?
|
||||
.unwrap_or(DEFAULT_TRANSLATION_RETRY_BACKOFF.as_secs());
|
||||
let max_tasks = rpc_translation_worker_usize_param(
|
||||
params,
|
||||
&["max_tasks", "worker_max_tasks", "translation_max_tasks"],
|
||||
"max_tasks",
|
||||
)?;
|
||||
let config = TranslationWorkerConfig {
|
||||
provider,
|
||||
fixture_path: rpc_translation_worker_string_param(
|
||||
params,
|
||||
&[
|
||||
"fixture_path",
|
||||
"translation_fixture",
|
||||
"provider_fixture",
|
||||
"mock_fixture",
|
||||
"fixture",
|
||||
],
|
||||
"fixture_path",
|
||||
)?
|
||||
.map(PathBuf::from),
|
||||
concurrency,
|
||||
max_attempts,
|
||||
lease_seconds,
|
||||
retry_backoff: Duration::from_secs(retry_backoff_seconds),
|
||||
max_tasks,
|
||||
worker_id: rpc_translation_worker_string_param(
|
||||
params,
|
||||
&["worker_id", "translation_worker_id"],
|
||||
"worker_id",
|
||||
)?
|
||||
.unwrap_or_else(|| "bat-rpc-worker".to_string()),
|
||||
};
|
||||
config.validate().map_err(|error| {
|
||||
ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN,
|
||||
error.to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn rpc_translation_worker_string_param(
|
||||
params: &serde_json::Value,
|
||||
aliases: &[&str],
|
||||
label: &str,
|
||||
) -> Result<Option<String>, ApiError> {
|
||||
for key in aliases {
|
||||
let Some(value) = params.get(*key) else {
|
||||
continue;
|
||||
};
|
||||
if value.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(text) = value.as_str() else {
|
||||
return Err(ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN,
|
||||
format!("{label} 必须是字符串"),
|
||||
));
|
||||
};
|
||||
let text = text.trim();
|
||||
if text.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
return Ok(Some(text.to_string()));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn rpc_translation_worker_u64_param(
|
||||
params: &serde_json::Value,
|
||||
aliases: &[&str],
|
||||
label: &str,
|
||||
) -> Result<Option<u64>, ApiError> {
|
||||
for key in aliases {
|
||||
let Some(value) = params.get(*key) else {
|
||||
continue;
|
||||
};
|
||||
if value.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
if value.as_i64().is_some_and(|number| number < 0) {
|
||||
return Err(ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN,
|
||||
format!("{label} 不能为负数"),
|
||||
));
|
||||
}
|
||||
let Some(number) = value.as_u64() else {
|
||||
return Err(ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN,
|
||||
format!("{label} 必须是非负整数 JSON number"),
|
||||
));
|
||||
};
|
||||
return Ok(Some(number));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn rpc_translation_worker_usize_param(
|
||||
params: &serde_json::Value,
|
||||
aliases: &[&str],
|
||||
label: &str,
|
||||
) -> Result<Option<usize>, ApiError> {
|
||||
rpc_translation_worker_u64_param(params, aliases, label)?
|
||||
.map(|number| {
|
||||
usize::try_from(number).map_err(|error| {
|
||||
ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN,
|
||||
format!("{label} 无效:{error}"),
|
||||
)
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn rpc_translation_worker_u32_param(
|
||||
params: &serde_json::Value,
|
||||
aliases: &[&str],
|
||||
label: &str,
|
||||
) -> Result<Option<u32>, ApiError> {
|
||||
rpc_translation_worker_u64_param(params, aliases, label)?
|
||||
.map(|number| {
|
||||
u32::try_from(number).map_err(|error| {
|
||||
ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN,
|
||||
format!("{label} 无效:{error}"),
|
||||
)
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// 分页参数:`offset` 默认 0;`limit` 默认 100,范围 1..=1000。
|
||||
fn rpc_page_params(params: Option<&serde_json::Value>) -> anyhow::Result<(usize, usize)> {
|
||||
let offset = params
|
||||
@@ -4221,6 +4502,32 @@ impl HumanReport for bat_infrastructure::LocalizedTranslationWorkflowReport {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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("后台状态");
|
||||
@@ -6321,6 +6628,17 @@ BAT_AUTO_DISCOVER=1
|
||||
#BAT_DOWNLOAD_CONCURRENCY=8
|
||||
#BAT_UNZIP=unzip
|
||||
|
||||
# ---- 翻译 provider worker ----
|
||||
# provider:mock(本地 fixture)或 crowdin(读取 CROWDIN_* 环境变量)
|
||||
#BAT_TRANSLATION_PROVIDER=mock
|
||||
#BAT_TRANSLATION_FIXTURE=
|
||||
#BAT_TRANSLATION_CONCURRENCY=8
|
||||
#BAT_TRANSLATION_MAX_ATTEMPTS=3
|
||||
#BAT_TRANSLATION_LEASE_SECONDS=300
|
||||
#BAT_TRANSLATION_RETRY_BACKOFF_SECONDS=5
|
||||
#BAT_TRANSLATION_MAX_TASKS=
|
||||
#BAT_TRANSLATION_WORKER_ID=
|
||||
|
||||
# ---- 输出 ----
|
||||
# 设为 1 时输出机器可读 JSON(默认人类可读)
|
||||
#BAT_JSON=0
|
||||
@@ -6504,6 +6822,38 @@ fn apply_bat_env_overrides(
|
||||
if let Some(v) = value("BAT_UNZIP") {
|
||||
options.config.unzip_command = PathBuf::from(v);
|
||||
}
|
||||
if let Some(v) = value("BAT_TRANSLATION_PROVIDER") {
|
||||
options.translation_provider = Some(v);
|
||||
}
|
||||
if let Some(v) = value("BAT_TRANSLATION_FIXTURE") {
|
||||
options.translation_fixture = Some(PathBuf::from(v));
|
||||
}
|
||||
if let Some(v) = value("BAT_TRANSLATION_CONCURRENCY") {
|
||||
options.worker_concurrency =
|
||||
parse_translation_worker_concurrency(&v, "环境变量 BAT_TRANSLATION_CONCURRENCY")?;
|
||||
}
|
||||
if let Some(v) = value("BAT_TRANSLATION_MAX_ATTEMPTS") {
|
||||
options.worker_max_attempts =
|
||||
parse_positive_u32(&v, "环境变量 BAT_TRANSLATION_MAX_ATTEMPTS")?;
|
||||
}
|
||||
if let Some(v) = value("BAT_TRANSLATION_LEASE_SECONDS") {
|
||||
options.worker_lease_seconds =
|
||||
parse_positive_u64(&v, "环境变量 BAT_TRANSLATION_LEASE_SECONDS")?;
|
||||
}
|
||||
if let Some(v) = value("BAT_TRANSLATION_RETRY_BACKOFF_SECONDS") {
|
||||
options.worker_retry_backoff = Duration::from_secs(v.parse::<u64>().map_err(|error| {
|
||||
anyhow::anyhow!("环境变量 BAT_TRANSLATION_RETRY_BACKOFF_SECONDS 的秒数无效:{error}")
|
||||
})?);
|
||||
}
|
||||
if let Some(v) = value("BAT_TRANSLATION_MAX_TASKS") {
|
||||
options.worker_max_tasks = Some(parse_positive_usize(
|
||||
&v,
|
||||
"环境变量 BAT_TRANSLATION_MAX_TASKS",
|
||||
)?);
|
||||
}
|
||||
if let Some(v) = value("BAT_TRANSLATION_WORKER_ID") {
|
||||
options.worker_id = Some(v);
|
||||
}
|
||||
if let Some(v) = value("BAT_PROXY") {
|
||||
options.config.curl_proxy = parse_proxy_config(&v)?;
|
||||
}
|
||||
@@ -6725,6 +7075,55 @@ fn parse_args_with_env(
|
||||
"--provider-run-id" => {
|
||||
options.translation_provider_run_id = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--translation-provider" | "--provider" => {
|
||||
options.translation_provider = Some(next_option_value(&mut args, &flag)?);
|
||||
options.translation_worker_option_explicit = true;
|
||||
}
|
||||
"--translation-fixture" | "--provider-fixture" | "--mock-fixture" | "--fixture" => {
|
||||
options.translation_fixture =
|
||||
Some(PathBuf::from(next_option_value(&mut args, &flag)?));
|
||||
options.translation_worker_option_explicit = true;
|
||||
}
|
||||
"--worker-concurrency" | "--translation-concurrency" => {
|
||||
options.worker_concurrency = parse_translation_worker_concurrency(
|
||||
&next_option_value(&mut args, &flag)?,
|
||||
&flag,
|
||||
)?;
|
||||
options.translation_worker_option_explicit = true;
|
||||
}
|
||||
"--worker-max-attempts" | "--translation-max-attempts" => {
|
||||
options.worker_max_attempts =
|
||||
parse_positive_u32(&next_option_value(&mut args, &flag)?, &flag)?;
|
||||
options.translation_worker_option_explicit = true;
|
||||
}
|
||||
"--worker-lease-seconds" | "--translation-lease-seconds" => {
|
||||
options.worker_lease_seconds =
|
||||
parse_positive_u64(&next_option_value(&mut args, &flag)?, &flag)?;
|
||||
options.translation_worker_option_explicit = true;
|
||||
}
|
||||
"--worker-retry-backoff" | "--translation-retry-backoff" => {
|
||||
options.worker_retry_backoff =
|
||||
parse_duration(&next_option_value(&mut args, &flag)?)?;
|
||||
options.translation_worker_option_explicit = true;
|
||||
}
|
||||
"--worker-retry-backoff-seconds" | "--translation-retry-backoff-seconds" => {
|
||||
let seconds = next_option_value(&mut args, &flag)?
|
||||
.parse::<u64>()
|
||||
.map_err(|error| anyhow::anyhow!("{flag} 的秒数无效:{error}"))?;
|
||||
options.worker_retry_backoff = Duration::from_secs(seconds);
|
||||
options.translation_worker_option_explicit = true;
|
||||
}
|
||||
"--worker-max-tasks" | "--translation-max-tasks" => {
|
||||
options.worker_max_tasks = Some(parse_positive_usize(
|
||||
&next_option_value(&mut args, &flag)?,
|
||||
&flag,
|
||||
)?);
|
||||
options.translation_worker_option_explicit = true;
|
||||
}
|
||||
"--worker-id" | "--translation-worker-id" => {
|
||||
options.worker_id = Some(next_option_value(&mut args, &flag)?);
|
||||
options.translation_worker_option_explicit = true;
|
||||
}
|
||||
"--localized-release-id" => {
|
||||
options.localized_release_id = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
@@ -7185,6 +7584,12 @@ fn parse_args_with_env(
|
||||
));
|
||||
}
|
||||
|
||||
if options.command != CliCommand::TranslationWorker
|
||||
&& options.translation_worker_option_explicit
|
||||
{
|
||||
return Err(anyhow::anyhow!("翻译 worker 参数只适用于 i18n worker run"));
|
||||
}
|
||||
|
||||
match options.command {
|
||||
CliCommand::Status | CliCommand::Stop | CliCommand::Logs => {
|
||||
if options.sync_option_explicit
|
||||
@@ -7406,6 +7811,56 @@ fn parse_args_with_env(
|
||||
options.progress = false;
|
||||
options.banner = false;
|
||||
}
|
||||
CliCommand::TranslationWorker => {
|
||||
if options.daemon || options.daemon_child {
|
||||
return Err(anyhow::anyhow!(
|
||||
"i18n worker run 使用 --watch 或 --run-count,不支持 daemon"
|
||||
));
|
||||
}
|
||||
if options.config.force
|
||||
|| options.config.dry_run
|
||||
|| options.schedule_option_explicit
|
||||
|| options.query_option_explicit
|
||||
|| options.write_patch_option_explicit
|
||||
|| options.translation_file.is_some()
|
||||
|| options.translation_id.is_some()
|
||||
|| options.translation_text.is_some()
|
||||
|| options.translation_text_file.is_some()
|
||||
|| options.translation_failure_reason.is_some()
|
||||
|| options.translation_provider_run_id.is_some()
|
||||
|| options.localized_release_id.is_some()
|
||||
|| options.repack_spec.is_some()
|
||||
|| options.proxy_option_explicit
|
||||
|| tools_are_non_default(&options.config, &options.env_baseline_config)
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"i18n worker run 只接受 --output、--resource-root、--state-dir、worker 参数和轮询参数"
|
||||
));
|
||||
}
|
||||
let provider = options
|
||||
.translation_provider
|
||||
.as_deref()
|
||||
.unwrap_or(TranslationProviderKind::Mock.as_str());
|
||||
if TranslationProviderKind::parse(provider).is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"i18n worker run 的 provider 无效:{provider}"
|
||||
));
|
||||
}
|
||||
TranslationWorkerConfig {
|
||||
provider: TranslationProviderKind::parse(provider).expect("provider 已在上方校验"),
|
||||
fixture_path: options.translation_fixture.clone(),
|
||||
concurrency: options.worker_concurrency,
|
||||
max_attempts: options.worker_max_attempts,
|
||||
lease_seconds: options.worker_lease_seconds,
|
||||
retry_backoff: options.worker_retry_backoff,
|
||||
max_tasks: options.worker_max_tasks,
|
||||
worker_id: options
|
||||
.worker_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "bat-worker-validation".to_string()),
|
||||
}
|
||||
.validate()?;
|
||||
}
|
||||
CliCommand::TranslationProofread => {
|
||||
if options.watch || options.daemon || options.daemon_child {
|
||||
return Err(anyhow::anyhow!("i18n proofread 只支持单次执行或 RPC 调用"));
|
||||
@@ -7506,6 +7961,7 @@ fn parse_args_with_env(
|
||||
CliCommand::Pull
|
||||
| CliCommand::Parse
|
||||
| CliCommand::Translate
|
||||
| CliCommand::TranslationWorker
|
||||
| CliCommand::PublishLocalized
|
||||
)
|
||||
{
|
||||
@@ -7515,7 +7971,11 @@ fn parse_args_with_env(
|
||||
}
|
||||
if matches!(
|
||||
options.command,
|
||||
CliCommand::Pull | CliCommand::Parse | CliCommand::Translate | CliCommand::PublishLocalized
|
||||
CliCommand::Pull
|
||||
| CliCommand::Parse
|
||||
| CliCommand::Translate
|
||||
| CliCommand::PublishLocalized
|
||||
| CliCommand::TranslationWorker
|
||||
) {
|
||||
if options.watch && options.run_count.is_some() {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -7632,6 +8092,9 @@ fn parse_translation_command(
|
||||
if action == "workbench" || action == "wb" {
|
||||
return parse_translation_workbench_command(args, options);
|
||||
}
|
||||
if action == "worker" {
|
||||
return parse_translation_worker_command(args, options);
|
||||
}
|
||||
let command = match action.as_str() {
|
||||
"run" => CliCommand::Translate,
|
||||
"export" => CliCommand::Translate,
|
||||
@@ -7655,6 +8118,20 @@ fn parse_translation_command(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_translation_worker_command(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
options: &mut CliOptions,
|
||||
) -> anyhow::Result<()> {
|
||||
let action = next_option_value(args, "translation worker")?;
|
||||
let command = match action.as_str() {
|
||||
"run" => CliCommand::TranslationWorker,
|
||||
other => return Err(anyhow::anyhow!("未知 translation worker 二级命令:{other}")),
|
||||
};
|
||||
ensure_command_not_set(options.command, &format!("translation worker {action}"))?;
|
||||
options.command = command;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_translation_workbench_command(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
options: &mut CliOptions,
|
||||
@@ -7738,6 +8215,48 @@ fn parse_download_concurrency(value: &str, source: &str) -> anyhow::Result<usize
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn parse_translation_worker_concurrency(value: &str, source: &str) -> anyhow::Result<usize> {
|
||||
let parsed = value
|
||||
.parse::<usize>()
|
||||
.map_err(|error| anyhow::anyhow!("{source} 无效:{error}"))?;
|
||||
if !(MIN_TRANSLATION_CONCURRENCY..=MAX_TRANSLATION_CONCURRENCY).contains(&parsed) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"{source} 必须在 {MIN_TRANSLATION_CONCURRENCY}..={MAX_TRANSLATION_CONCURRENCY} 范围内"
|
||||
));
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn parse_positive_usize(value: &str, source: &str) -> anyhow::Result<usize> {
|
||||
let parsed = value
|
||||
.parse::<usize>()
|
||||
.map_err(|error| anyhow::anyhow!("{source} 无效:{error}"))?;
|
||||
if parsed == 0 {
|
||||
return Err(anyhow::anyhow!("{source} 必须大于 0"));
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn parse_positive_u32(value: &str, source: &str) -> anyhow::Result<u32> {
|
||||
let parsed = value
|
||||
.parse::<u32>()
|
||||
.map_err(|error| anyhow::anyhow!("{source} 无效:{error}"))?;
|
||||
if parsed == 0 {
|
||||
return Err(anyhow::anyhow!("{source} 必须大于 0"));
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn parse_positive_u64(value: &str, source: &str) -> anyhow::Result<u64> {
|
||||
let parsed = value
|
||||
.parse::<u64>()
|
||||
.map_err(|error| anyhow::anyhow!("{source} 无效:{error}"))?;
|
||||
if parsed == 0 {
|
||||
return Err(anyhow::anyhow!("{source} 必须大于 0"));
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn next_option_value(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
flag: &str,
|
||||
@@ -7807,6 +8326,7 @@ fn print_usage(binary: &str) {
|
||||
eprintln!(" i18n unset Clear one translated workbench entry");
|
||||
eprintln!(" i18n validate Validate workbench against the current official release");
|
||||
eprintln!(" i18n proofread Mark localized workflow as manual proofreading");
|
||||
eprintln!(" i18n worker run Run translation provider worker once or repeatedly");
|
||||
eprintln!(
|
||||
" i18n tasks / i18n task list / i18n task status Query current offline TextUnit translation task status"
|
||||
);
|
||||
@@ -7854,6 +8374,9 @@ fn print_usage(binary: &str) {
|
||||
" {binary} i18n unset --translation-file /tmp/bat-workbench.json --translation-id unit-1"
|
||||
);
|
||||
eprintln!(" {binary} i18n proofread --json");
|
||||
eprintln!(
|
||||
" {binary} i18n worker run --provider mock --worker-concurrency 8 --run-count 2 --interval 30s"
|
||||
);
|
||||
eprintln!(" {binary} i18n tasks --json");
|
||||
eprintln!(" {binary} i18n handoff --json");
|
||||
eprintln!(" {binary} i18n status --json");
|
||||
@@ -7908,6 +8431,17 @@ fn print_usage(binary: &str) {
|
||||
eprintln!(" --translated-file <PATH> UTF-8 translation file for i18n set");
|
||||
eprintln!(" --failure-reason <TEXT> Provider failure reason for i18n task update");
|
||||
eprintln!(" --provider-run-id <ID> Provider run ID for i18n task update");
|
||||
eprintln!(" --translation-provider <NAME> / --provider <NAME> Provider for i18n worker run (mock/crowdin)");
|
||||
eprintln!(" --translation-fixture <PATH> Mock/provider fixture for i18n worker run");
|
||||
eprintln!(
|
||||
" --worker-concurrency <N> Translation worker concurrency (default: 8, range 1..=256)"
|
||||
);
|
||||
eprintln!(" --worker-max-attempts <N> Maximum claims per translation task");
|
||||
eprintln!(" --worker-lease-seconds <N> Lease seconds for one claimed task");
|
||||
eprintln!(" --worker-retry-backoff <DURATION> Retry backoff after retryable failure");
|
||||
eprintln!(" --worker-retry-backoff-seconds <N> Retry backoff seconds");
|
||||
eprintln!(" --worker-max-tasks <N> Maximum tasks claimed in one worker run");
|
||||
eprintln!(" --worker-id <ID> Worker ID prefix for lease diagnostics");
|
||||
eprintln!(" --localized-release-id <ID> Explicit localized publication ID");
|
||||
eprintln!(" --repack-spec <PATH> UnityFS batch repack JSON spec");
|
||||
eprintln!();
|
||||
|
||||
@@ -140,6 +140,165 @@ fn env_invalid_values_error() {
|
||||
assert!(parse(&["bat", "--download-concurrency", "257"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translation_worker_command_options_are_validated() {
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"worker",
|
||||
"run",
|
||||
"--provider",
|
||||
"mock",
|
||||
"--translation-fixture",
|
||||
"/tmp/mock-translation.json",
|
||||
"--worker-concurrency",
|
||||
"8",
|
||||
"--worker-max-attempts",
|
||||
"4",
|
||||
"--worker-lease-seconds",
|
||||
"60",
|
||||
"--worker-retry-backoff",
|
||||
"0s",
|
||||
"--worker-max-tasks",
|
||||
"2",
|
||||
"--worker-id",
|
||||
"manual-run",
|
||||
"--run-count",
|
||||
"2",
|
||||
"--interval",
|
||||
"30s",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationWorker);
|
||||
assert_eq!(options.translation_provider.as_deref(), Some("mock"));
|
||||
assert_eq!(
|
||||
options.translation_fixture,
|
||||
Some(PathBuf::from("/tmp/mock-translation.json"))
|
||||
);
|
||||
assert_eq!(options.worker_concurrency, 8);
|
||||
assert_eq!(options.worker_max_attempts, 4);
|
||||
assert_eq!(options.worker_lease_seconds, 60);
|
||||
assert_eq!(options.worker_retry_backoff, Duration::ZERO);
|
||||
assert_eq!(options.worker_max_tasks, Some(2));
|
||||
assert_eq!(options.worker_id.as_deref(), Some("manual-run"));
|
||||
assert_eq!(options.run_count, Some(2));
|
||||
|
||||
assert!(parse(&["bat", "i18n", "worker", "run"]).is_ok());
|
||||
assert!(parse(&[
|
||||
"bat",
|
||||
"translation",
|
||||
"worker",
|
||||
"run",
|
||||
"--translation-concurrency",
|
||||
"256"
|
||||
])
|
||||
.is_ok());
|
||||
assert!(parse(&["bat", "i18n", "worker", "run", "--worker-concurrency", "0"]).is_err());
|
||||
assert!(parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"worker",
|
||||
"run",
|
||||
"--worker-concurrency",
|
||||
"257"
|
||||
])
|
||||
.is_err());
|
||||
assert!(parse(&["bat", "i18n", "worker", "run", "--worker-max-attempts", "0"]).is_err());
|
||||
assert!(parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"worker",
|
||||
"run",
|
||||
"--worker-lease-seconds",
|
||||
"0"
|
||||
])
|
||||
.is_err());
|
||||
assert!(parse(&["bat", "i18n", "worker", "run", "--worker-max-tasks", "0"]).is_err());
|
||||
assert!(parse(&["bat", "i18n", "run", "--worker-concurrency", "8"]).is_err());
|
||||
assert!(parse(&["bat", "i18n", "worker", "run", "--provider", "unknown"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translation_worker_env_defaults_apply() {
|
||||
let options = parse_with_env(
|
||||
&["bat", "i18n", "worker", "run"],
|
||||
&[
|
||||
("BAT_TRANSLATION_PROVIDER", "mock"),
|
||||
("BAT_TRANSLATION_FIXTURE", "/tmp/fixture.json"),
|
||||
("BAT_TRANSLATION_CONCURRENCY", "16"),
|
||||
("BAT_TRANSLATION_MAX_ATTEMPTS", "5"),
|
||||
("BAT_TRANSLATION_LEASE_SECONDS", "120"),
|
||||
("BAT_TRANSLATION_RETRY_BACKOFF_SECONDS", "0"),
|
||||
("BAT_TRANSLATION_MAX_TASKS", "7"),
|
||||
("BAT_TRANSLATION_WORKER_ID", "env-worker"),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationWorker);
|
||||
assert_eq!(options.translation_provider.as_deref(), Some("mock"));
|
||||
assert_eq!(
|
||||
options.translation_fixture,
|
||||
Some(PathBuf::from("/tmp/fixture.json"))
|
||||
);
|
||||
assert_eq!(options.worker_concurrency, 16);
|
||||
assert_eq!(options.worker_max_attempts, 5);
|
||||
assert_eq!(options.worker_lease_seconds, 120);
|
||||
assert_eq!(options.worker_retry_backoff, Duration::ZERO);
|
||||
assert_eq!(options.worker_max_tasks, Some(7));
|
||||
assert_eq!(options.worker_id.as_deref(), Some("env-worker"));
|
||||
|
||||
assert!(parse_with_env(
|
||||
&["bat", "i18n", "worker", "run"],
|
||||
&[("BAT_TRANSLATION_CONCURRENCY", "257")]
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_with_env(
|
||||
&["bat", "i18n", "worker", "run"],
|
||||
&[("BAT_TRANSLATION_MAX_ATTEMPTS", "0")]
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translation_worker_rpc_params_are_strict_and_accept_aliases() {
|
||||
let default_config = rpc_translation_worker_config(None).unwrap();
|
||||
assert_eq!(default_config.provider, TranslationProviderKind::Mock);
|
||||
assert_eq!(default_config.concurrency, DEFAULT_TRANSLATION_CONCURRENCY);
|
||||
|
||||
let config = rpc_translation_worker_config(Some(&serde_json::json!({
|
||||
"translation_provider": "mock",
|
||||
"provider_fixture": "/tmp/mock-provider.json",
|
||||
"worker_concurrency": 8,
|
||||
"worker_max_attempts": 4,
|
||||
"worker_lease_seconds": 60,
|
||||
"worker_retry_backoff_seconds": 0,
|
||||
"worker_max_tasks": 2,
|
||||
"translation_worker_id": "rpc-worker"
|
||||
})))
|
||||
.unwrap();
|
||||
assert_eq!(config.provider, TranslationProviderKind::Mock);
|
||||
assert_eq!(
|
||||
config.fixture_path,
|
||||
Some(PathBuf::from("/tmp/mock-provider.json"))
|
||||
);
|
||||
assert_eq!(config.concurrency, 8);
|
||||
assert_eq!(config.max_attempts, 4);
|
||||
assert_eq!(config.lease_seconds, 60);
|
||||
assert_eq!(config.retry_backoff, Duration::ZERO);
|
||||
assert_eq!(config.max_tasks, Some(2));
|
||||
assert_eq!(config.worker_id, "rpc-worker");
|
||||
|
||||
assert!(rpc_translation_worker_config(Some(&serde_json::json!([]))).is_err());
|
||||
assert!(rpc_translation_worker_config(Some(&serde_json::json!({"provider": "bad"}))).is_err());
|
||||
assert!(rpc_translation_worker_config(Some(&serde_json::json!({"concurrency": "8"}))).is_err());
|
||||
assert!(rpc_translation_worker_config(Some(&serde_json::json!({"concurrency": -1}))).is_err());
|
||||
assert!(rpc_translation_worker_config(Some(&serde_json::json!({"concurrency": 0}))).is_err());
|
||||
assert!(rpc_translation_worker_config(Some(&serde_json::json!({"concurrency": 257}))).is_err());
|
||||
assert!(rpc_translation_worker_config(Some(&serde_json::json!({"max_attempts": 0}))).is_err());
|
||||
assert!(rpc_translation_worker_config(Some(&serde_json::json!({"lease_seconds": 0}))).is_err());
|
||||
assert!(rpc_translation_worker_config(Some(&serde_json::json!({"max_tasks": 0}))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grouped_workflow_commands_use_short_top_level_aliases() {
|
||||
let options = parse(&[
|
||||
|
||||
@@ -4,12 +4,14 @@ pub(super) const MAX_RETAINED_TASKS: usize = 64;
|
||||
/// 每个任务保留的进度日志行数上限。
|
||||
pub(super) const MAX_TASK_LOG_LINES: usize = 200;
|
||||
|
||||
/// 任务类型:目前覆盖官方同步、校验与 catalog 更新检查。
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 任务类型:覆盖资源同步、校验、修复、翻译 worker 与 catalog 更新检查。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum TaskKind {
|
||||
Sync,
|
||||
Verify,
|
||||
Repair,
|
||||
/// 翻译 provider worker 轮次。
|
||||
TranslationWorker,
|
||||
/// catalog 更新检查:只做发现 + 拉取计划(dry-run),不下载不审计。
|
||||
Refresh,
|
||||
}
|
||||
@@ -20,6 +22,7 @@ impl TaskKind {
|
||||
Self::Sync => RPC_METHOD_RESOURCE_SYNC,
|
||||
Self::Verify => RPC_METHOD_RESOURCE_VERIFY,
|
||||
Self::Repair => RPC_METHOD_RESOURCE_REPAIR,
|
||||
Self::TranslationWorker => RPC_METHOD_TRANSLATION_WORKER_RUN,
|
||||
Self::Refresh => RPC_METHOD_CATALOG_REFRESH,
|
||||
}
|
||||
}
|
||||
@@ -56,6 +59,10 @@ impl TaskKind {
|
||||
config.repair = false;
|
||||
config.force = force;
|
||||
}
|
||||
Self::TranslationWorker => {
|
||||
config.dry_run = false;
|
||||
config.force = false;
|
||||
}
|
||||
}
|
||||
config
|
||||
}
|
||||
@@ -155,6 +162,7 @@ fn task_kind_static(kind: &str) -> Option<&'static str> {
|
||||
RPC_METHOD_RESOURCE_SYNC => Some(RPC_METHOD_RESOURCE_SYNC),
|
||||
RPC_METHOD_RESOURCE_VERIFY => Some(RPC_METHOD_RESOURCE_VERIFY),
|
||||
RPC_METHOD_RESOURCE_REPAIR => Some(RPC_METHOD_RESOURCE_REPAIR),
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN => Some(RPC_METHOD_TRANSLATION_WORKER_RUN),
|
||||
RPC_METHOD_CATALOG_REFRESH => Some(RPC_METHOD_CATALOG_REFRESH),
|
||||
_ => None,
|
||||
}
|
||||
@@ -494,7 +502,9 @@ impl TaskStore {
|
||||
/// 提交给任务 worker 的作业(配置已按任务类型派生完毕)。
|
||||
pub(super) struct TaskJob {
|
||||
pub(super) id: String,
|
||||
pub(super) kind: TaskKind,
|
||||
pub(super) config: OfficialUpdateConfig,
|
||||
pub(super) translation_worker_config: Option<TranslationWorkerConfig>,
|
||||
/// 与任务记录共享的取消标志。
|
||||
pub(super) cancel: Arc<AtomicBool>,
|
||||
}
|
||||
@@ -528,36 +538,68 @@ pub(super) fn run_task_worker(
|
||||
});
|
||||
|
||||
let cancel = Arc::clone(&job.cancel);
|
||||
let run_result = {
|
||||
let _sync_guard = sync_lock
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
let progress_registry = registry.clone();
|
||||
let progress_id = job.id.clone();
|
||||
let cancel_check = Arc::clone(&cancel);
|
||||
let stop_control = Arc::clone(&control);
|
||||
service.run_with_progress_and_cancellation(
|
||||
&job.config,
|
||||
|event| {
|
||||
progress_registry
|
||||
.append_log(&progress_id, format!("[{}] {}", event.stage, event.message));
|
||||
progress_registry.update(&progress_id, |record| {
|
||||
record.stage = Some(event.stage.to_string());
|
||||
record.message = Some(event.message.clone());
|
||||
});
|
||||
},
|
||||
|| {
|
||||
cancel_check.load(Ordering::Relaxed)
|
||||
|| daemon_control_stop_requested(Some(&stop_control))
|
||||
},
|
||||
)
|
||||
let run_result = if job.kind == TaskKind::TranslationWorker {
|
||||
let worker_config = job
|
||||
.translation_worker_config
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("translation worker 任务缺少运行配置"));
|
||||
worker_config.and_then(|worker_config| {
|
||||
let _sync_guard = sync_lock
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
registry.append_log(&job.id, "[translation-worker] 开始执行".to_string());
|
||||
registry.update(&job.id, |record| {
|
||||
record.stage = Some("translation-worker".to_string());
|
||||
record.message = Some("翻译 provider worker 正在执行".to_string());
|
||||
});
|
||||
let resource_root = active_official_resource_root(&job.config.output_root)?;
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
runtime
|
||||
.block_on(bat_infrastructure::run_translation_worker_at(
|
||||
&resource_root,
|
||||
worker_config,
|
||||
))
|
||||
.and_then(|report| serde_json::to_value(report).map_err(anyhow::Error::from))
|
||||
})
|
||||
} else {
|
||||
let run_result = {
|
||||
let _sync_guard = sync_lock
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
let progress_registry = registry.clone();
|
||||
let progress_id = job.id.clone();
|
||||
let cancel_check = Arc::clone(&cancel);
|
||||
let stop_control = Arc::clone(&control);
|
||||
service.run_with_progress_and_cancellation(
|
||||
&job.config,
|
||||
|event| {
|
||||
progress_registry.append_log(
|
||||
&progress_id,
|
||||
format!("[{}] {}", event.stage, event.message),
|
||||
);
|
||||
progress_registry.update(&progress_id, |record| {
|
||||
record.stage = Some(event.stage.to_string());
|
||||
record.message = Some(event.message.clone());
|
||||
});
|
||||
},
|
||||
|| {
|
||||
cancel_check.load(Ordering::Relaxed)
|
||||
|| daemon_control_stop_requested(Some(&stop_control))
|
||||
},
|
||||
)
|
||||
};
|
||||
run_result
|
||||
.map(|report| serde_json::to_value(&report).map_err(anyhow::Error::from))
|
||||
.and_then(|result| result)
|
||||
};
|
||||
|
||||
match run_result {
|
||||
Ok(report) => registry.update(&job.id, |record| {
|
||||
record.status = "succeeded";
|
||||
record.finished_at = Some(unix_seconds_now());
|
||||
record.result = serde_json::to_value(&report).ok();
|
||||
record.result = Some(report);
|
||||
}),
|
||||
Err(error) => {
|
||||
let cancelled = cancel.load(Ordering::Relaxed);
|
||||
|
||||
@@ -253,6 +253,43 @@ pub(super) fn run_translation_proofread(options: &CliOptions) -> anyhow::Result<
|
||||
print_report(options.output_format, &report)
|
||||
}
|
||||
|
||||
pub(super) fn run_translation_worker(options: &CliOptions) -> anyhow::Result<()> {
|
||||
let resource_root = options
|
||||
.resource_root
|
||||
.clone()
|
||||
.map(|path| lexical_absolute(&path).map_err(anyhow::Error::msg))
|
||||
.transpose()?
|
||||
.unwrap_or(active_official_resource_root(&options.config.output_root)?);
|
||||
let provider = options
|
||||
.translation_provider
|
||||
.as_deref()
|
||||
.unwrap_or(TranslationProviderKind::Mock.as_str());
|
||||
let provider = TranslationProviderKind::parse(provider)
|
||||
.ok_or_else(|| anyhow::anyhow!("i18n worker run 的 provider 无效:{provider}"))?;
|
||||
let config = TranslationWorkerConfig {
|
||||
provider,
|
||||
fixture_path: options.translation_fixture.clone(),
|
||||
concurrency: options.worker_concurrency,
|
||||
max_attempts: options.worker_max_attempts,
|
||||
lease_seconds: options.worker_lease_seconds,
|
||||
retry_backoff: options.worker_retry_backoff,
|
||||
max_tasks: options.worker_max_tasks,
|
||||
worker_id: options
|
||||
.worker_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("bat-worker-{}", std::process::id())),
|
||||
};
|
||||
config.validate()?;
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let report = runtime.block_on(bat_infrastructure::run_translation_worker_at(
|
||||
&resource_root,
|
||||
&config,
|
||||
))?;
|
||||
print_report(options.output_format, &report)
|
||||
}
|
||||
|
||||
pub(super) fn run_repack(options: &CliOptions) -> anyhow::Result<()> {
|
||||
let spec = options
|
||||
.repack_spec
|
||||
|
||||
Reference in New Issue
Block a user