mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 11:56:23 +08:00
feat(daemon): 实现 catalog.* 命名空间与 resource.manifest(issue #1)
补齐 issue #1 方法分层中可立即实现的剩余命名空间: - catalog.status:当前已发布版本的 catalog 概览(读取其 snapshot: app/bundle 版本、connection group、addressables 根、端点与 marker 计数、launcher 元数据、GameMainConfig 摘要)。 - catalog.versions:版本历史(current / in_progress / previous / failed,来自 official-version-state.json)。 - catalog.diff:当前 snapshot 相对上一个可用版本的差异(复用 YostarJpSyncSnapshot::diff 与 diff_extended_snapshot;含 changed_endpoint_urls 与 previous_snapshot_missing 标志;无上一版本 时按首次观察处理 is_initial=true)。 - catalog.refresh:经任务执行器的 catalog 更新检查任务(dry-run + plan,不下载不审计;支持 force),返回 task_id 可轮询。 - resource.manifest:当前版本下载 manifest 的分页只读查询(offset 默认 0、limit 默认 100/上限 1000,BTreeMap 按 URL 有序保证分页稳定; 非法 limit → 700002)。infrastructure 公开 OfficialDownloadManifest(Entry) 与 read_download_manifest_at。 约定:只读查询在尚无已发布版本或文件缺失时返回 ok=true 且 data.available=false(正常状态而非错误,便于 Go 层直接分支)。 is_pending_rpc_method 缩减为 task.create / resource.repair / patch.* / unityfs.*(后两者待引擎;repair 待引擎独立修复模式)。 新增 read_daemon_resource_state 供 resource/catalog 只读查询共用。 测试:新增 10 个 dispatch 单测(含 fixture:状态文件 + 版本状态 + snapshot + manifest)与 read_download_manifest_at 单测;调整 pending 断言。真实 daemon 端到端验证(fixture 资源根 + 死代理快速失败): raw socket 依次断言 catalog.status/versions/diff、resource.manifest 分页与参数错误、catalog.refresh 入队与 task.status 轮询、 resource.repair→700003、未知方法→700001,全部通过,daemon.stop 干净 退出。全量 fmt / clippy --workspace --all-targets -D warnings / test --workspace 全绿。 已知后续(另行推进):任务配置校验失败(如缺少 app-version)尚未接入 输入域错误码(应为 100001,现落 900001)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1500,12 +1500,15 @@ fn strong_seed_hash_kind(
|
||||
}
|
||||
}
|
||||
|
||||
/// 官方下载 manifest:按完整 URL 为键记录每个已下载资源的本地校验信息。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct OfficialDownloadManifest {
|
||||
pub struct OfficialDownloadManifest {
|
||||
/// Manifest 结构版本。
|
||||
#[serde(default = "default_download_manifest_version")]
|
||||
version: u32,
|
||||
pub version: u32,
|
||||
/// 按 URL 为键的资源条目。
|
||||
#[serde(default)]
|
||||
entries: BTreeMap<String, OfficialDownloadManifestEntry>,
|
||||
pub entries: BTreeMap<String, OfficialDownloadManifestEntry>,
|
||||
}
|
||||
|
||||
impl Default for OfficialDownloadManifest {
|
||||
@@ -1517,12 +1520,41 @@ impl Default for OfficialDownloadManifest {
|
||||
}
|
||||
}
|
||||
|
||||
/// 官方下载 manifest 中的单个资源条目。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct OfficialDownloadManifestEntry {
|
||||
url: String,
|
||||
destination: String,
|
||||
bytes: u64,
|
||||
blake3: String,
|
||||
pub struct OfficialDownloadManifestEntry {
|
||||
/// 官方资源 URL。
|
||||
pub url: String,
|
||||
/// 相对资源根目录的落盘路径。
|
||||
pub destination: String,
|
||||
/// 文件字节数。
|
||||
pub bytes: u64,
|
||||
/// 文件 BLAKE3 摘要(hex)。
|
||||
pub blake3: String,
|
||||
}
|
||||
|
||||
/// 读取指定资源根目录下的官方下载 manifest(daemon RPC 只读查询用)。
|
||||
///
|
||||
/// 文件缺失返回 `Ok(None)`;符号链接、解析失败或版本不支持返回 `Err`。
|
||||
pub fn read_download_manifest_at(
|
||||
resource_root: &Path,
|
||||
) -> Result<Option<OfficialDownloadManifest>, String> {
|
||||
let path = resource_root.join(DOWNLOAD_MANIFEST_FILE);
|
||||
let Some(bytes) = read_file_no_symlink(&path, "下载 manifest")? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let manifest: OfficialDownloadManifest = serde_json::from_slice(&bytes)
|
||||
.map_err(|error| format!("解析下载 manifest 失败 {}:{error}", path.display()))?;
|
||||
if manifest.version != DOWNLOAD_MANIFEST_VERSION {
|
||||
return Err(format!(
|
||||
"不支持的下载 manifest 版本 {},文件 {}",
|
||||
manifest.version,
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(manifest))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -1797,6 +1829,27 @@ mod tests {
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn read_download_manifest_at_handles_missing_and_bad_version() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
// 文件缺失:Ok(None)。
|
||||
assert!(read_download_manifest_at(temp.path()).unwrap().is_none());
|
||||
|
||||
// 正常 manifest:读取条目。
|
||||
let path = temp.path().join(DOWNLOAD_MANIFEST_FILE);
|
||||
fs::write(
|
||||
&path,
|
||||
br#"{"version":1,"entries":{"https://a":{"url":"https://a","destination":"a","bytes":1,"blake3":"aa"}}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let manifest = read_download_manifest_at(temp.path()).unwrap().unwrap();
|
||||
assert_eq!(manifest.entries.len(), 1);
|
||||
|
||||
// 不支持的版本:Err。
|
||||
fs::write(&path, br#"{"version":999,"entries":{}}"#).unwrap();
|
||||
assert!(read_download_manifest_at(temp.path()).is_err());
|
||||
}
|
||||
|
||||
fn discovery_plan() -> YostarJpResourceDiscoveryPlan {
|
||||
YostarJpResourceDiscoveryPlan {
|
||||
connection_group_name: "Prod-Audit".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user