mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 01:46:44 +08:00
feat(daemon): 任务历史文件态持久化与 .env 无参启动(issue #1)
任务持久化: - 任务历史落 <state-dir>/bat-tasks.json(版本化、0600 原子写、不跟随 symlink),生命周期转换时 write-through;seq 持久化避免 pid 复用撞 ID - daemon 重启恢复历史,中断时仍 queued/running 的任务标记 failed, 新增错误码 TASK_INTERRUPTED(BAT-ERR-700005) - 损坏文件改名 .corrupt 留证后从空历史开始;无法识别的记录计数跳过 - core 新增 ErrorCode::ALL 公开码表与 from_id 反查(持久化错误码往返) .env 配置: - 首次启动在二进制所在目录释放 .env 模板(0600、create_new 防竞态), 之后每次启动加载为进程环境变量(不覆盖已存在变量) - 优先级:CLI > 进程环境变量 > .env > 内置默认;BAT_* 键映射到 CliOptions 默认值,BAT_WATCH/BAT_DAEMON 仅对无子命令 Run 生效且 CLI 显式模式/dry-run 时让位;Redis 键预留(未接入) - 工具/代理"非默认"判断改按 env 应用后基线,status/stop/logs 在 .env 存在时不误判;BAT_SKIP_ENV_FILE=1 整体禁用 验证:新增 10 个单测(env 解析/优先级/守卫回归/持久化往返/中断标记/ 损坏恢复/未知类型跳过);真机 e2e:.env 释放加载、daemon 纯 .env 启动、 catalog.refresh 任务带类型化错误码落盘并跨 daemon 重启恢复(含日志); fmt / clippy --workspace --all-targets -D warnings / test --workspace 全绿 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+78
-50
@@ -221,10 +221,76 @@ impl ErrorCode {
|
||||
pub const RPC_NOT_IMPLEMENTED: Self = Self::new(700_003, "rpc_not_implemented", false);
|
||||
/// 任务不存在。
|
||||
pub const TASK_NOT_FOUND: Self = Self::new(700_004, "task_not_found", false);
|
||||
/// 任务因 daemon 停止/重启而中断(未跑完即失去执行器)。
|
||||
pub const TASK_INTERRUPTED: Self = Self::new(700_005, "task_interrupted", false);
|
||||
|
||||
// ---- 900xxx 内部/未知 ----
|
||||
/// 未归类的内部错误。
|
||||
pub const INTERNAL: Self = Self::new(900_001, "internal", false);
|
||||
|
||||
/// 全部命名错误码的码表:查表、唯一性校验和从持久化形态反查共用。
|
||||
/// 新增错误码必须同步登记到这里。
|
||||
pub const ALL: &'static [Self] = &[
|
||||
Self::MISSING_APP_VERSION,
|
||||
Self::MISSING_CONNECTION_GROUP,
|
||||
Self::MISSING_SERVER_INFO_SOURCE,
|
||||
Self::INVALID_PROXY_SCHEME,
|
||||
Self::INVALID_ARGUMENT,
|
||||
Self::DANGEROUS_OUTPUT_ROOT,
|
||||
Self::PATH_ESCAPE,
|
||||
Self::SYMLINK_REJECTED,
|
||||
Self::FILE_PERMISSION,
|
||||
Self::HTTP_FORBIDDEN,
|
||||
Self::HTTP_NOT_FOUND,
|
||||
Self::HTTP_CLIENT_ERROR,
|
||||
Self::HTTP_TOO_MANY_REQUESTS,
|
||||
Self::HTTP_SERVER_ERROR,
|
||||
Self::NETWORK_DNS,
|
||||
Self::NETWORK_CONNECT,
|
||||
Self::NETWORK_TIMEOUT,
|
||||
Self::NETWORK_TLS,
|
||||
Self::NETWORK_INTERRUPTED,
|
||||
Self::NETWORK_OTHER,
|
||||
Self::RETRY_EXHAUSTED,
|
||||
Self::QUARANTINED,
|
||||
Self::NON_OFFICIAL_URL,
|
||||
Self::LAUNCHER_API_REJECTED,
|
||||
Self::PROXY_FAILURE,
|
||||
Self::BLAKE3_MISMATCH,
|
||||
Self::OFFICIAL_HASH_MISMATCH,
|
||||
Self::SIZE_MISMATCH,
|
||||
Self::ZIP_STRUCTURE_INVALID,
|
||||
Self::RESOURCE_LOCKED,
|
||||
Self::STAGING_PREPARE_FAILED,
|
||||
Self::PUBLISH_FAILED,
|
||||
Self::VERSION_STATE_WRITE_FAILED,
|
||||
Self::CAS_HASH_MISMATCH,
|
||||
Self::CAS_OBJECT_NOT_FOUND,
|
||||
Self::CAS_REFERENCE_UNDERFLOW,
|
||||
Self::CAS_DATABASE,
|
||||
Self::MANIFEST_PARSE_FAILED,
|
||||
Self::UNITYFS_PARSE_FAILED,
|
||||
Self::GAME_MAIN_CONFIG_FAILED,
|
||||
Self::LAUNCHER_RESPONSE_INVALID,
|
||||
Self::RPC_UNKNOWN_METHOD,
|
||||
Self::RPC_INVALID_PARAMS,
|
||||
Self::RPC_NOT_IMPLEMENTED,
|
||||
Self::TASK_NOT_FOUND,
|
||||
Self::TASK_INTERRUPTED,
|
||||
Self::INTERNAL,
|
||||
];
|
||||
|
||||
/// 按错误码数字反查命名常量;未登记的数字返回 `None`。
|
||||
pub fn from_number_lookup(number: u32) -> Option<Self> {
|
||||
Self::ALL.iter().copied().find(|code| code.number == number)
|
||||
}
|
||||
|
||||
/// 按展示 ID(`BAT-ERR-NNNNNN`)反查命名常量;格式非法或未登记返回 `None`。
|
||||
/// 用于从持久化/传输形态恢复错误码。
|
||||
pub fn from_id(id: &str) -> Option<Self> {
|
||||
let number = id.strip_prefix("BAT-ERR-")?.parse::<u32>().ok()?;
|
||||
Self::from_number_lookup(number)
|
||||
}
|
||||
}
|
||||
|
||||
/// 结构化 API 错误:进入 RPC envelope、CLI `--json` 输出和结构化日志的统一形态。
|
||||
@@ -348,59 +414,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn error_codes_are_unique() {
|
||||
// 防止新增码值撞号:列举全部命名常量,断言 number 唯一。
|
||||
let codes = [
|
||||
ErrorCode::MISSING_APP_VERSION,
|
||||
ErrorCode::MISSING_CONNECTION_GROUP,
|
||||
ErrorCode::MISSING_SERVER_INFO_SOURCE,
|
||||
ErrorCode::INVALID_PROXY_SCHEME,
|
||||
ErrorCode::INVALID_ARGUMENT,
|
||||
ErrorCode::DANGEROUS_OUTPUT_ROOT,
|
||||
ErrorCode::PATH_ESCAPE,
|
||||
ErrorCode::SYMLINK_REJECTED,
|
||||
ErrorCode::FILE_PERMISSION,
|
||||
ErrorCode::HTTP_FORBIDDEN,
|
||||
ErrorCode::HTTP_NOT_FOUND,
|
||||
ErrorCode::HTTP_CLIENT_ERROR,
|
||||
ErrorCode::HTTP_TOO_MANY_REQUESTS,
|
||||
ErrorCode::HTTP_SERVER_ERROR,
|
||||
ErrorCode::NETWORK_DNS,
|
||||
ErrorCode::NETWORK_CONNECT,
|
||||
ErrorCode::NETWORK_TIMEOUT,
|
||||
ErrorCode::NETWORK_TLS,
|
||||
ErrorCode::NETWORK_INTERRUPTED,
|
||||
ErrorCode::NETWORK_OTHER,
|
||||
ErrorCode::RETRY_EXHAUSTED,
|
||||
ErrorCode::QUARANTINED,
|
||||
ErrorCode::NON_OFFICIAL_URL,
|
||||
ErrorCode::LAUNCHER_API_REJECTED,
|
||||
ErrorCode::PROXY_FAILURE,
|
||||
ErrorCode::BLAKE3_MISMATCH,
|
||||
ErrorCode::OFFICIAL_HASH_MISMATCH,
|
||||
ErrorCode::SIZE_MISMATCH,
|
||||
ErrorCode::ZIP_STRUCTURE_INVALID,
|
||||
ErrorCode::RESOURCE_LOCKED,
|
||||
ErrorCode::STAGING_PREPARE_FAILED,
|
||||
ErrorCode::PUBLISH_FAILED,
|
||||
ErrorCode::VERSION_STATE_WRITE_FAILED,
|
||||
ErrorCode::CAS_HASH_MISMATCH,
|
||||
ErrorCode::CAS_OBJECT_NOT_FOUND,
|
||||
ErrorCode::CAS_REFERENCE_UNDERFLOW,
|
||||
ErrorCode::CAS_DATABASE,
|
||||
ErrorCode::MANIFEST_PARSE_FAILED,
|
||||
ErrorCode::UNITYFS_PARSE_FAILED,
|
||||
ErrorCode::GAME_MAIN_CONFIG_FAILED,
|
||||
ErrorCode::LAUNCHER_RESPONSE_INVALID,
|
||||
ErrorCode::RPC_UNKNOWN_METHOD,
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
ErrorCode::RPC_NOT_IMPLEMENTED,
|
||||
ErrorCode::TASK_NOT_FOUND,
|
||||
ErrorCode::INTERNAL,
|
||||
];
|
||||
let mut numbers: Vec<u32> = codes.iter().map(ErrorCode::number).collect();
|
||||
// 防止新增码值撞号:公开码表 `ALL` 即全部命名常量,断言 number 唯一。
|
||||
let mut numbers: Vec<u32> = ErrorCode::ALL.iter().map(ErrorCode::number).collect();
|
||||
let total = numbers.len();
|
||||
numbers.sort_unstable();
|
||||
numbers.dedup();
|
||||
assert_eq!(numbers.len(), total, "存在重复的错误码 number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_id_round_trips_registered_codes() {
|
||||
// 持久化恢复路径依赖 from_id 反查:全部登记码必须可经展示 ID 往返。
|
||||
for code in ErrorCode::ALL {
|
||||
assert_eq!(ErrorCode::from_id(&code.id()), Some(*code));
|
||||
}
|
||||
assert_eq!(ErrorCode::from_id("BAT-ERR-999999"), None);
|
||||
assert_eq!(ErrorCode::from_id("not-an-id"), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user