Merge branch 'master' into feat/agc-run-preview-tip
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m24s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m53s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 20s
Project CI / Backend tests (pull_request) Failing after 21s
Project CI / Native shell tests (pull_request) Failing after 21s
Project CI / Frontend tests (pull_request) Failing after 21s
Project CI / Repository checks (pull_request) Failing after 21s
Project CI / AI game creator shell web tests (pull_request) Failing after 21s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled

This commit is contained in:
2026-09-22 17:31:32 +08:00
3 changed files with 35 additions and 2 deletions
@@ -1,5 +1,13 @@
# 踩坑与排障记录
## release 安装包文件名中的编码空格不能按控制字符拒绝
- **现象**:release 环境点击“下载客户端”只显示无法获取最新版本,`GET /api/client-downloads` 返回 `502 UPSTREAM_ERROR`dev 环境正常。
- **原因**:release 渠道产品名为“陶泥儿 Release”,NSIS 首装包名因此包含空格,OSS `latest.json` 中的 URL 使用 `%20``validate_download_url` 的百分号解码校验把 `0x20` 当成控制字符拒绝,Windows 清单被判非法;release macOS 清单当时又未发布,聚合后两端都无下载项并返回 502。
- **处理(现行口径)**:清单 URL 校验允许百分号编码的空格,继续拒绝其它控制字符、`/``\`、DEL、非法编码和跨目录文件名;不得通过改写真实产物名绕过校验。
- **验证**`platform-oss``陶泥儿%20Release_0.1.110_x64-setup.exe` 做清单解析回归;线上修复后接口应返回 release Windows 下载项,macOS 未发布时进入 `unavailablePlatforms`
- **关联**`server-rs/crates/platform-oss/src/client_downloads.rs``apps/ai-game-creator-shell/scripts/build-release.mjs``docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md`
## 客户端图标素材描述不能先包装再截断
- 现象:资源画布填写了具体图标需求,平台实际收到的 `iconDescriptions` 却只包含泛化的小游戏美术指令,生成结果不遵循输入。
@@ -1,6 +1,6 @@
# AGC 客户端更新检查与下载
更新时间:`2026-09-21`
更新时间:`2026-09-22`
本文件是 AGC 客户端自动更新的主规范:更新能力由 Tauri 官方插件 `tauri-plugin-updater` 承担,并按下文渠道分发。
@@ -27,6 +27,7 @@
- 数据不跨渠道共享:本地项目、工程快照、模板、登录态、诊断日志与 Runner/项目锁按渠道身份分目录。切渠道等于换一个客户端,不迁移、不合并本地数据;渠道内的 origin 隔离规则不变。
- 主窗口与工作区/启动器窗口标题取构建期产品名,让同机并存的渠道客户端在任务栏与 Alt-Tab 中可区分;默认渠道标题仍是「陶泥儿」。
- 首装包与更新包的对象名包含产品名(如 `陶泥儿 Release_0.1.96_x64-setup.exe``陶泥儿 Release_0.1.96_aarch64.dmg`)。清单 `downloads` 地址由发布脚本按本次真实产物派生,禁止写死产品名;首装包选择按 `<版本>_<架构>.dmg` 唯一匹配,不依赖产品名字面量。
- 产品名中的空格在清单 URL 中必须使用标准百分号编码(如 `%20`);网站服务端允许这种合法文件名编码,但仍拒绝控制字符、路径分隔符、DEL 和非法百分号编码,不得通过改写真实产物名绕过校验。
- Windows 提权 ACL 修复助手按目录名识别安装身份:`<基线>``<基线>.<渠道>` 都在 AGC 自有的 managed 范围内;相似前缀(例如 `world.genarrative.ai-game-creator-backup`)不在范围内,落回 user-selected 范围或直接拒绝。
## 非目标
@@ -304,7 +304,8 @@ fn has_safe_percent_encoding(file_name: &str) -> bool {
let Ok(byte) = u8::from_str_radix(hex, 16) else {
return false;
};
if byte <= b' ' || matches!(byte, b'/' | b'\\' | 127) {
// 渠道安装身份可以包含空格,OSS 清单中的文件名会将空格编码为 `%20`。
if byte < b' ' || matches!(byte, b'/' | b'\\' | 127) {
return false;
}
remaining = &encoded[2..];
@@ -541,6 +542,29 @@ mod tests {
}
}
#[test]
fn percent_encoded_spaces_in_release_installer_names_are_allowed() {
let version = "0.1.110";
let installer = download_url(
DownloadPlatform::Windows,
version,
"%E9%99%B6%E6%B3%A5%E5%84%BF%20Release_0.1.110_x64-setup.exe",
)
.replace("/dev-win/", "/release-win/");
let manifest = manifest_with_downloads(
version,
json!({
"windows-x86_64": { "url": installer }
}),
);
let parsed =
super::parse_manifest(&manifest, DownloadPlatform::Windows, "release").unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].download_url, installer);
}
#[test]
fn macos_requires_explicit_dmg_metadata_and_preserves_available_architectures() {
let updater_only = json!({