feat(bat-api): 实现内嵌 dashboard
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

Closes #46
This commit is contained in:
2026-08-31 23:17:23 +08:00
parent ab21344773
commit 4ed81f0030
30 changed files with 3527 additions and 91 deletions
+129 -4
View File
@@ -1,5 +1,12 @@
use super::*;
#[derive(Debug, Deserialize)]
struct TranslationTaskResultUpdateParam {
unit_id: String,
source_text: String,
translated_text: String,
}
pub(super) fn build_translation_tasks_report(
state_dir: &Path,
query: OfficialTextUnitTaskQuery,
@@ -170,6 +177,13 @@ pub(super) fn update_translation_task_status_report(
.or_else(|| rpc_string_param(params, "reason"))
.map(str::to_string);
let provider_run_id = rpc_string_param(params, "provider_run_id").map(str::to_string);
let provider = rpc_string_param(params, "provider").map(str::to_string);
let result_params = translation_task_result_params(params)?;
if !result_params.is_empty() && status != TranslationTaskStatus::Completed {
return Err(anyhow::anyhow!(
"translation_results 只能随 completed 状态写入"
));
}
let (_, version_state) = read_daemon_resource_state(state_dir)?;
let current = version_state
.as_ref()
@@ -182,6 +196,22 @@ pub(super) fn update_translation_task_status_report(
repository_path.display()
));
}
let textunit_index = if result_params.is_empty() {
None
} else {
Some(
read_textunit_index_at(&current.resource_root)
.map_err(anyhow::Error::msg)?
.ok_or_else(|| {
anyhow::anyhow!("当前 release 缺少 TextUnit 明细索引,无法校验人工校对结果")
})?,
)
};
let result_provider = provider.clone().unwrap_or_else(|| "manual".to_string());
let result_timestamp = unix_seconds_now();
let result_provider_run_id = provider_run_id
.clone()
.unwrap_or_else(|| format!("manual-{result_timestamp}"));
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
@@ -190,10 +220,36 @@ pub(super) fn update_translation_task_status_report(
let repository = SqliteTranslationTaskRepository::open(&repository_path)
.await
.map_err(|error| anyhow::anyhow!("{error}"))?;
repository
.update_status(task_id, status, failure_reason, provider_run_id)
.await
.map_err(|error| anyhow::anyhow!("{error}"))
if let Some(index) = textunit_index.as_ref() {
let current_task = repository
.find(task_id)
.await
.map_err(|error| anyhow::anyhow!("{error}"))?;
let results = build_manual_translation_results(
&current_task,
index,
&result_params,
&result_provider,
&result_provider_run_id,
result_timestamp,
)?;
repository
.update_status_with_results(
task_id,
status,
failure_reason,
Some(result_provider_run_id),
Some(result_provider),
Some(&results),
)
.await
.map_err(|error| anyhow::anyhow!("{error}"))
} else {
repository
.update_status(task_id, status, failure_reason, provider_run_id)
.await
.map_err(|error| anyhow::anyhow!("{error}"))
}
})?;
Ok(serde_json::json!({
"available": true,
@@ -215,6 +271,75 @@ pub(super) fn textunit_query_json(query: &OfficialTextUnitQuery) -> serde_json::
})
}
fn translation_task_result_params(
params: Option<&serde_json::Value>,
) -> anyhow::Result<Vec<TranslationTaskResultUpdateParam>> {
let Some(value) = params
.and_then(|params| params.get("translation_results"))
.or_else(|| params.and_then(|params| params.get("results")))
else {
return Ok(Vec::new());
};
if value.is_null() {
return Ok(Vec::new());
}
serde_json::from_value(value.clone())
.map_err(|error| anyhow::anyhow!("translation_results 必须是结果数组:{error}"))
}
fn build_manual_translation_results(
task: &bat_infrastructure::PersistedTranslationTask,
index: &bat_infrastructure::OfficialTextUnitIndex,
params: &[TranslationTaskResultUpdateParam],
provider: &str,
provider_run_id: &str,
translated_unix_seconds: u64,
) -> anyhow::Result<Vec<bat_infrastructure::TranslationTaskUnitResult>> {
let index_by_id = index
.units
.iter()
.map(|unit| (unit.id.as_str(), unit))
.collect::<std::collections::BTreeMap<_, _>>();
let mut seen = std::collections::BTreeSet::new();
let mut results = Vec::with_capacity(params.len());
for param in params {
let unit_id = param.unit_id.trim();
if unit_id.is_empty() {
return Err(anyhow::anyhow!("translation_results[].unit_id 不能为空"));
}
if !seen.insert(unit_id.to_string()) {
return Err(anyhow::anyhow!(
"translation_results 包含重复 TextUnit{unit_id}"
));
}
let unit = index_by_id
.get(unit_id)
.ok_or_else(|| anyhow::anyhow!("translation_results 引用了未知 TextUnit{unit_id}"))?;
if unit.destination != task.task.destination
|| unit.archive_entry != task.task.archive_entry
{
return Err(anyhow::anyhow!(
"TextUnit {unit_id} 不属于翻译任务 {}",
task.task.task_id
));
}
if param.source_text != unit.source_text {
return Err(anyhow::anyhow!(
"TextUnit {unit_id} 的 source_text 与当前索引不一致"
));
}
results.push(bat_infrastructure::TranslationTaskUnitResult {
unit_id: unit_id.to_string(),
source_text: param.source_text.clone(),
translated_text: param.translated_text.clone(),
provider: provider.to_string(),
provider_run_id: provider_run_id.to_string(),
translated_unix_seconds,
});
}
Ok(results)
}
pub(super) fn translation_task_query_json(query: &OfficialTextUnitTaskQuery) -> serde_json::Value {
serde_json::json!({
"task_id": query.task_id.clone(),