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
+78
View File
@@ -1217,6 +1217,27 @@ impl SqliteTranslationTaskRepository {
status: TranslationTaskStatus,
failure_reason: Option<String>,
provider_run_id: Option<String>,
) -> Result<PersistedTranslationTask> {
self.update_status_with_results(
task_id,
status,
failure_reason,
provider_run_id,
None,
None,
)
.await
}
/// Updates provider state and optionally replaces durable TextUnit results.
pub async fn update_status_with_results(
&self,
task_id: &str,
status: TranslationTaskStatus,
failure_reason: Option<String>,
provider_run_id: Option<String>,
provider: Option<String>,
translation_results: Option<&[TranslationTaskUnitResult]>,
) -> Result<PersistedTranslationTask> {
let current = self.find(task_id).await?;
let now = unix_seconds_now_i64();
@@ -1228,6 +1249,13 @@ impl SqliteTranslationTaskRepository {
current.attempt_count
};
let normalized_reason = failure_reason.filter(|reason| !reason.trim().is_empty());
let provider_run_id = provider_run_id.filter(|value| !value.trim().is_empty());
let provider = provider.filter(|value| !value.trim().is_empty());
let translation_results_json =
translation_results
.map(serde_json::to_string)
.transpose()
.map_err(|error| bat_core::Error::Serialization(error.to_string()))?;
let completed = (status == TranslationTaskStatus::Completed).then_some(now);
sqlx::query(
r#"
@@ -1235,6 +1263,8 @@ impl SqliteTranslationTaskRepository {
SET worker_status = ?2, failure_reason = ?3, attempt_count = ?4,
updated_unix_seconds = ?5, completed_unix_seconds = ?6,
provider_run_id = COALESCE(?7, provider_run_id),
provider = COALESCE(?8, provider),
translation_results_json = COALESCE(?9, translation_results_json),
lease_owner = NULL, lease_expires_unix_seconds = NULL,
failure_class = NULL, failure_retryable = 0,
next_attempt_unix_seconds = NULL
@@ -1248,6 +1278,8 @@ impl SqliteTranslationTaskRepository {
.bind(now)
.bind(completed)
.bind(provider_run_id)
.bind(provider)
.bind(translation_results_json)
.execute(&self.pool)
.await
.map_err(db_error)?;
@@ -1696,6 +1728,52 @@ mod tests {
assert_eq!(retrievable[0].attempt_count, 1);
}
#[tokio::test]
async fn sqlite_translation_tasks_persist_manual_results_without_worker_lease() {
let temp = tempfile::TempDir::new().unwrap();
let repository =
SqliteTranslationTaskRepository::new(temp.path().join("translation-tasks.sqlite"))
.await
.unwrap();
let queue = queue(vec![task(
"task-a",
"Bundles/a.bundle",
OfficialTextUnitTaskStatus::QueuedOffline,
Some(OfficialParseStatus::Parsed),
None,
)]);
repository.sync_queue(&queue).await.unwrap();
let result = TranslationTaskUnitResult {
unit_id: "unit-a".to_string(),
source_text: "source".to_string(),
translated_text: "manual translation".to_string(),
provider: "manual".to_string(),
provider_run_id: "manual-run-1".to_string(),
translated_unix_seconds: 321,
};
let updated = repository
.update_status_with_results(
"task-a",
TranslationTaskStatus::Completed,
None,
Some("manual-run-1".to_string()),
Some("manual".to_string()),
Some(std::slice::from_ref(&result)),
)
.await
.unwrap();
assert_eq!(updated.task_status, TranslationTaskStatus::Completed);
assert_eq!(updated.provider.as_deref(), Some("manual"));
assert_eq!(updated.provider_run_id.as_deref(), Some("manual-run-1"));
assert_eq!(updated.translation_results, vec![result.clone()]);
assert_eq!(
repository.find("task-a").await.unwrap().translation_results,
vec![result]
);
}
#[tokio::test]
async fn sqlite_translation_tasks_recover_expired_leases_for_retry() {
let temp = tempfile::TempDir::new().unwrap();