mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
feat(patch):统一清单驱动汉化发布
This commit is contained in:
@@ -395,6 +395,7 @@ struct CliOptions {
|
||||
output_explicit: bool,
|
||||
resource_root: Option<PathBuf>,
|
||||
translation_file: Option<PathBuf>,
|
||||
patch_manifest: Option<PathBuf>,
|
||||
translation_id: Option<String>,
|
||||
translation_text: Option<String>,
|
||||
translation_text_file: Option<PathBuf>,
|
||||
@@ -521,6 +522,7 @@ impl Default for CliOptions {
|
||||
output_explicit: false,
|
||||
resource_root: None,
|
||||
translation_file: None,
|
||||
patch_manifest: None,
|
||||
translation_id: None,
|
||||
translation_text: None,
|
||||
translation_text_file: None,
|
||||
@@ -1130,6 +1132,8 @@ struct LocalizedPublishRpcParams {
|
||||
alias = "from_worker_results"
|
||||
)]
|
||||
from_worker: bool,
|
||||
#[serde(default, alias = "patch_manifest_path")]
|
||||
patch_manifest: Option<PathBuf>,
|
||||
#[serde(default, alias = "release_id")]
|
||||
localized_release_id: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -1226,8 +1230,8 @@ fn is_pending_rpc_method(method: &str) -> bool {
|
||||
// 等语义方法创建,通用创建接口暂不开放。
|
||||
// daemon.clean-stable:CLI 侧按进程生命周期处理;
|
||||
// live RPC 内不做在线清理。
|
||||
// patch.* / unityfs.*:文件级写入入口已开放;发布级 patch 构建、复杂
|
||||
// UnityFS 语义编辑和 inspect 等子命令仍未开放。
|
||||
// patch.* / unityfs.*:文件级写入入口已开放;发布级 patch 构建和 inspect
|
||||
// 等子命令仍未开放,发布级 generic manifest 通过 localized.publish 进入。
|
||||
matches!(method, "task.create" | RPC_METHOD_CLEAN_STABLE)
|
||||
|| (method.starts_with("patch.") && method != RPC_METHOD_PATCH_APPLY)
|
||||
|| (method.starts_with("unityfs.")
|
||||
@@ -3277,8 +3281,13 @@ fn build_localized_status_report(
|
||||
let mut patch_manifest_path = None;
|
||||
let mut patch_manifest_available = false;
|
||||
let mut patch_manifest_matches_release = false;
|
||||
let mut patch_manifest_integrity_status = None;
|
||||
let mut patch_manifest_source_version = None;
|
||||
let mut patch_manifest_target_version = None;
|
||||
let mut patch_file_count = None;
|
||||
let mut patch_text_asset_operation_count = None;
|
||||
let mut patch_operation_count = None;
|
||||
let mut patch_kind_counts = serde_json::Map::new();
|
||||
let mut rollback_previous_current_target = None;
|
||||
let mut flow_status_code = if official_version_id.is_some() {
|
||||
ReleaseFlowStatusCode::LocalizedPending
|
||||
@@ -3326,6 +3335,47 @@ fn build_localized_status_report(
|
||||
patch_file_count = Some(manifest.file_count);
|
||||
patch_text_asset_operation_count = Some(manifest.text_asset_operation_count);
|
||||
rollback_previous_current_target = manifest.rollback.previous_current_target;
|
||||
if let Some(generic_manifest) = manifest.patch_manifest.as_ref() {
|
||||
patch_manifest_source_version =
|
||||
Some(generic_manifest.source_version.clone());
|
||||
patch_manifest_target_version =
|
||||
Some(generic_manifest.target_version.clone());
|
||||
let mut operation_count = 0usize;
|
||||
for file in &generic_manifest.files {
|
||||
for operation in &file.operations {
|
||||
operation_count += 1;
|
||||
let key = match operation.patch_kind() {
|
||||
bat_patch::PatchKind::Binary => "binary",
|
||||
bat_patch::PatchKind::Json => "json",
|
||||
bat_patch::PatchKind::Text => "text",
|
||||
bat_patch::PatchKind::UnityFsTextAsset => "unityfs_text_asset",
|
||||
bat_patch::PatchKind::UnityFsStringField => {
|
||||
"unityfs_string_field"
|
||||
}
|
||||
bat_patch::PatchKind::UnityFsField => "unityfs_field",
|
||||
bat_patch::PatchKind::Mixed => "mixed",
|
||||
}
|
||||
.to_string();
|
||||
let count = patch_kind_counts
|
||||
.entry(key)
|
||||
.or_insert_with(|| serde_json::json!(0));
|
||||
*count = serde_json::json!(count.as_u64().unwrap_or_default() + 1);
|
||||
}
|
||||
}
|
||||
patch_operation_count = Some(operation_count);
|
||||
patch_manifest_integrity_status = Some(
|
||||
if bat_patch::validate_patch_manifest(generic_manifest).is_ok() {
|
||||
"valid"
|
||||
} else {
|
||||
"invalid"
|
||||
},
|
||||
);
|
||||
patch_manifest_matches_release &= generic_manifest.source_version
|
||||
== manifest.official_release_id
|
||||
&& generic_manifest.target_version == manifest.localized_release_id;
|
||||
} else {
|
||||
patch_manifest_integrity_status = Some("legacy");
|
||||
}
|
||||
}
|
||||
if candidate.is_dir()
|
||||
&& current_points_to_published_version
|
||||
@@ -3363,8 +3413,13 @@ fn build_localized_status_report(
|
||||
"patch_manifest_path": patch_manifest_path,
|
||||
"patch_manifest_available": patch_manifest_available,
|
||||
"patch_manifest_matches_release": patch_manifest_matches_release,
|
||||
"patch_manifest_integrity_status": patch_manifest_integrity_status,
|
||||
"patch_manifest_source_version": patch_manifest_source_version,
|
||||
"patch_manifest_target_version": patch_manifest_target_version,
|
||||
"patch_file_count": patch_file_count,
|
||||
"patch_text_asset_operation_count": patch_text_asset_operation_count,
|
||||
"patch_operation_count": patch_operation_count,
|
||||
"patch_kind_counts": patch_kind_counts,
|
||||
"rollback_previous_current_target": rollback_previous_current_target,
|
||||
"matches_current_official_release": matches_current_official_release,
|
||||
"current_points_to_published_version": current_points_to_published_version,
|
||||
@@ -3487,11 +3542,17 @@ fn localized_publish_rpc_report(
|
||||
let translation_file = params
|
||||
.translation_file
|
||||
.filter(|path| !path.as_os_str().is_empty());
|
||||
if translation_file.is_some() == params.from_worker {
|
||||
let patch_manifest = params
|
||||
.patch_manifest
|
||||
.filter(|path| !path.as_os_str().is_empty());
|
||||
let input_count = usize::from(translation_file.is_some())
|
||||
+ usize::from(params.from_worker)
|
||||
+ usize::from(patch_manifest.is_some());
|
||||
if input_count != 1 {
|
||||
return Err(ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_LOCALIZED_PUBLISH,
|
||||
"localized.publish 必须且只能指定 translation_file 或 from_worker",
|
||||
"localized.publish 必须且只能指定 translation_file、from_worker 或 patch_manifest",
|
||||
));
|
||||
}
|
||||
let mut config = base_config.clone();
|
||||
@@ -3501,6 +3562,7 @@ fn localized_publish_rpc_report(
|
||||
config,
|
||||
state_dir: state_dir.to_path_buf(),
|
||||
translation_file,
|
||||
patch_manifest,
|
||||
translation_from_worker: params.from_worker,
|
||||
localized_release_id: normalize_optional_rpc_string(params.localized_release_id),
|
||||
..CliOptions::default()
|
||||
@@ -6844,6 +6906,9 @@ fn parse_args_with_env(
|
||||
options.translation_file =
|
||||
Some(PathBuf::from(next_option_value(&mut args, &flag)?));
|
||||
}
|
||||
"--patch-manifest" | "--patch-manifest-file" => {
|
||||
options.patch_manifest = Some(PathBuf::from(next_option_value(&mut args, &flag)?));
|
||||
}
|
||||
"--translation-id" => {
|
||||
options.translation_id = Some(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
@@ -7554,6 +7619,9 @@ fn parse_args_with_env(
|
||||
if options.command != CliCommand::PublishLocalized && options.translation_from_worker {
|
||||
return Err(anyhow::anyhow!("--from-worker 只适用于 i18n publish"));
|
||||
}
|
||||
if options.command != CliCommand::PublishLocalized && options.patch_manifest.is_some() {
|
||||
return Err(anyhow::anyhow!("--patch-manifest 只适用于 i18n publish"));
|
||||
}
|
||||
|
||||
match options.command {
|
||||
CliCommand::Status | CliCommand::Stop | CliCommand::Logs => {
|
||||
@@ -7805,10 +7873,13 @@ fn parse_args_with_env(
|
||||
));
|
||||
}
|
||||
if matches!(options.command, CliCommand::PublishLocalized)
|
||||
&& options.translation_file.is_some() == options.translation_from_worker
|
||||
&& (usize::from(options.translation_file.is_some())
|
||||
+ usize::from(options.translation_from_worker)
|
||||
+ usize::from(options.patch_manifest.is_some())
|
||||
!= 1)
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"i18n publish 必须且只能指定 --translation-file 或 --from-worker"
|
||||
"i18n publish 必须且只能指定 --translation-file、--from-worker 或 --patch-manifest"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,6 +659,19 @@ fn grouped_workflow_commands_use_short_top_level_aliases() {
|
||||
assert_eq!(options.command, CliCommand::PublishLocalized);
|
||||
assert!(options.translation_from_worker);
|
||||
assert_eq!(options.localized_release_id.as_deref(), Some("localized-1"));
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"publish",
|
||||
"--patch-manifest",
|
||||
"/tmp/patch-manifest.json",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::PublishLocalized);
|
||||
assert_eq!(
|
||||
options.patch_manifest,
|
||||
Some(PathBuf::from("/tmp/patch-manifest.json"))
|
||||
);
|
||||
assert!(parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
@@ -5107,6 +5120,7 @@ fn dispatch_localized_status_verifies_current_release_pointer() {
|
||||
file_count: 0,
|
||||
text_asset_operation_count: 0,
|
||||
files: Vec::new(),
|
||||
patch_manifest: None,
|
||||
rollback: bat_infrastructure::LocalizedPatchRollbackInfo {
|
||||
previous_current_target: None,
|
||||
remove_version_path: localized_version.clone(),
|
||||
@@ -5243,6 +5257,7 @@ fn dispatch_localized_rollback_restores_manifest_previous_release() {
|
||||
file_count: 0,
|
||||
text_asset_operation_count: 0,
|
||||
files: Vec::new(),
|
||||
patch_manifest: None,
|
||||
rollback: bat_infrastructure::LocalizedPatchRollbackInfo {
|
||||
previous_current_target: None,
|
||||
remove_version_path: previous.clone(),
|
||||
@@ -5261,6 +5276,7 @@ fn dispatch_localized_rollback_restores_manifest_previous_release() {
|
||||
file_count: 0,
|
||||
text_asset_operation_count: 0,
|
||||
files: Vec::new(),
|
||||
patch_manifest: None,
|
||||
rollback: bat_infrastructure::LocalizedPatchRollbackInfo {
|
||||
previous_current_target: Some(PathBuf::from("versions/release-1")),
|
||||
remove_version_path: current.clone(),
|
||||
@@ -5362,6 +5378,7 @@ fn localized_status_keeps_published_release_during_manual_proofreading() {
|
||||
file_count: 0,
|
||||
text_asset_operation_count: 0,
|
||||
files: Vec::new(),
|
||||
patch_manifest: None,
|
||||
rollback: bat_infrastructure::LocalizedPatchRollbackInfo {
|
||||
previous_current_target: None,
|
||||
remove_version_path: localized_version.clone(),
|
||||
|
||||
@@ -172,6 +172,7 @@ fn reject_patch_apply_options(options: &CliOptions, command: &str) -> anyhow::Re
|
||||
if options.patch_kind.is_some()
|
||||
|| options.patch_source_path.is_some()
|
||||
|| options.patch_patch_path.is_some()
|
||||
|| options.patch_manifest.is_some()
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"{command} 不接受 --patch-kind、--source-file 或 --patch-file"
|
||||
@@ -191,6 +192,7 @@ fn reject_unityfs_write_options(options: &CliOptions, command: &str) -> anyhow::
|
||||
|| options.unityfs_expected_value.is_some()
|
||||
|| options.unityfs_replacement_value.is_some()
|
||||
|| options.unityfs_expected_semantic_value.is_some()
|
||||
|| options.patch_manifest.is_some()
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"{command} 不接受 UnityFS 写入参数;请改用 unityfs-patch-* 命令"
|
||||
|
||||
@@ -339,6 +339,49 @@ pub(super) fn publish_localized_report(
|
||||
options: &CliOptions,
|
||||
) -> anyhow::Result<LocalizedPatchReport> {
|
||||
let (resource_root, official_release_id) = current_official_release(options)?;
|
||||
if let Some(manifest_path) = options.patch_manifest.as_ref() {
|
||||
let manifest_bytes = read_file_no_symlink(manifest_path, "generic patch manifest")
|
||||
.map_err(anyhow::Error::msg)?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("generic patch manifest 不存在:{}", manifest_path.display())
|
||||
})?;
|
||||
let manifest: bat_patch::PatchManifest = serde_json::from_slice(&manifest_bytes)
|
||||
.map_err(|error| anyhow::anyhow!("generic patch manifest 无效:{error}"))?;
|
||||
bat_patch::validate_patch_manifest(&manifest)
|
||||
.map_err(|error| anyhow::anyhow!("generic patch manifest 无效:{error}"))?;
|
||||
if manifest.source_version != official_release_id {
|
||||
return Err(anyhow::anyhow!(
|
||||
"generic patch source version={} 与当前官方 release={} 不一致",
|
||||
manifest.source_version,
|
||||
official_release_id
|
||||
));
|
||||
}
|
||||
let localized_release_id = options
|
||||
.localized_release_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| manifest.target_version.clone());
|
||||
if localized_release_id != manifest.target_version {
|
||||
return Err(anyhow::anyhow!(
|
||||
"generic patch target version={} 必须与 localized release id={} 一致",
|
||||
manifest.target_version,
|
||||
localized_release_id
|
||||
));
|
||||
}
|
||||
let config = LocalizedPatchConfig::new(
|
||||
resource_root,
|
||||
options.config.localized_output_root.clone(),
|
||||
official_release_id,
|
||||
Vec::new(),
|
||||
)
|
||||
.with_archive_commands(
|
||||
options.config.unzip_command.clone(),
|
||||
options.config.zip_command.clone(),
|
||||
)
|
||||
.with_manifest(manifest)
|
||||
.with_localized_release_id(localized_release_id)
|
||||
.with_force(options.config.force);
|
||||
return LocalizedPatchService::new().publish(&config);
|
||||
}
|
||||
let workbench = if options.translation_from_worker {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user