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
+54 -19
View File
@@ -42,6 +42,13 @@ pub struct LocalizedPatchConfig {
pub localized_output_root: PathBuf,
/// Version identifier shared with the official release.
pub release_id: String,
/// Optional distinct localized release ID. When omitted, `release_id` is
/// used for backward-compatible publication paths.
pub localized_release_id: Option<String>,
/// Allow publishing a new localized release even when the source release
/// already has a localized current release. The caller should normally
/// provide a distinct localized release ID.
pub force: bool,
/// Patch operations to apply.
pub patches: Vec<LocalizedTextAssetPatch>,
}
@@ -58,9 +65,29 @@ impl LocalizedPatchConfig {
official_release_root: official_release_root.into(),
localized_output_root: localized_output_root.into(),
release_id: release_id.into(),
localized_release_id: None,
force: false,
patches,
}
}
/// Sets a distinct localized release ID.
pub fn with_localized_release_id(mut self, release_id: impl Into<String>) -> Self {
self.localized_release_id = Some(release_id.into());
self
}
/// Enables or disables forced publication.
pub fn with_force(mut self, force: bool) -> Self {
self.force = force;
self
}
fn published_release_id(&self) -> &str {
self.localized_release_id
.as_deref()
.unwrap_or(&self.release_id)
}
}
/// Persisted localized release state.
@@ -187,7 +214,7 @@ impl LocalizedPatchManifest {
}
/// Result of a successful localized release publication.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct LocalizedPatchReport {
/// Published version directory.
pub version_path: PathBuf,
@@ -217,14 +244,15 @@ impl LocalizedPatchService {
/// Copies the official release, applies patches in staging and publishes it.
pub fn publish(&self, config: &LocalizedPatchConfig) -> anyhow::Result<LocalizedPatchReport> {
let published_release_id = config.published_release_id().to_string();
let staging = config
.localized_output_root
.join(LOCALIZED_STAGING_DIR)
.join(&config.release_id);
.join(&published_release_id);
let version_path = config
.localized_output_root
.join(LOCALIZED_VERSIONS_DIR)
.join(&config.release_id);
.join(&published_release_id);
let current_path = config.localized_output_root.join(LOCALIZED_CURRENT_LINK);
let previous_current_target = current_symlink_target(&current_path).ok().flatten();
let version_existed_before = version_path.exists();
@@ -257,11 +285,11 @@ impl LocalizedPatchService {
let staging = config
.localized_output_root
.join(LOCALIZED_STAGING_DIR)
.join(&config.release_id);
.join(config.published_release_id());
let version_path = config
.localized_output_root
.join(LOCALIZED_VERSIONS_DIR)
.join(&config.release_id);
.join(config.published_release_id());
let current_path = config.localized_output_root.join(LOCALIZED_CURRENT_LINK);
let state_path = config
.localized_output_root
@@ -269,10 +297,12 @@ impl LocalizedPatchService {
let patch_manifest_path = version_path.join(LOCALIZED_PATCH_MANIFEST_FILE);
if version_path.exists() {
return Err(anyhow::anyhow!(
"localized release already exists: {}",
version_path.display()
));
let message = if config.force {
"localized release target already exists; forced publication requires a distinct localized release id"
} else {
"localized release already exists"
};
return Err(anyhow::anyhow!("{message}: {}", version_path.display()));
}
remove_owned_staging(&staging)?;
fs::create_dir_all(&staging)?;
@@ -313,7 +343,7 @@ impl LocalizedPatchService {
let manifest = LocalizedPatchManifest {
manifest_version: LOCALIZED_PATCH_MANIFEST_VERSION,
official_release_id: config.release_id.clone(),
localized_release_id: config.release_id.clone(),
localized_release_id: config.published_release_id().to_string(),
generated_unix_seconds: unix_seconds_now(),
file_count: changed_files.len(),
text_asset_operation_count: changed_files
@@ -339,13 +369,13 @@ impl LocalizedPatchService {
switch_current_symlink(
&config.localized_output_root,
&current_path,
&config.release_id,
config.published_release_id(),
)?;
let state = LocalizedVersionState {
state_version: 1,
official_release_id: config.release_id.clone(),
current_release_id: Some(config.release_id.clone()),
current_release_id: Some(config.published_release_id().to_string()),
status: "localized".to_string(),
updated_unix_seconds: unix_seconds_now(),
};
@@ -623,13 +653,18 @@ fn validate_config(config: &LocalizedPatchConfig) -> Result<(), String> {
localized.display()
));
}
if config.release_id.is_empty()
|| config.release_id.contains('/')
|| config.release_id.contains('\\')
|| config.release_id == "."
|| config.release_id == ".."
{
return Err(format!("非法汉化 release id{}", config.release_id));
for (label, release_id) in [
("官方", config.release_id.as_str()),
("汉化", config.published_release_id()),
] {
if release_id.is_empty()
|| release_id.contains('/')
|| release_id.contains('\\')
|| release_id == "."
|| release_id == ".."
{
return Err(format!("非法{label} release id{release_id}"));
}
}
ensure_safe_directory_path(&config.official_release_root, "官方 release")?;
ensure_safe_directory_path(&config.localized_output_root, "汉化输出目录")?;