合并远端主分支

同步外部 MCP 项目选择与 API 契约修复
同步 Gitea CI 网络稳定性与构建镜像配置
This commit is contained in:
2026-08-07 19:50:29 +08:00
16 changed files with 962 additions and 53 deletions
+17 -3
View File
@@ -4016,11 +4016,18 @@
- 现象:`Repository checks``Frontend tests``Backend tests``Native shell tests` 都从全新 job 容器开始,apt、setup-node、rustup 和原生系统库在不同 job 里重复安装;后端与原生壳的安装时间可达数分钟,并把软件源和代理瞬时失败放大为四份。
- 原因:Gitea Actions job 彼此隔离,上一个 job 在容器内安装的包不会自动进入下一个 job;把同一套不随 PR 变化的工具链写在 workflow step 中,必然每次重做。
- 处理:用 `deploy/container/gitea-ci-job.Dockerfile` 预装 Node 22、Rust 1.96、`rustfmt`、Chrome、`bwrap``rg``ffmpeg``clang/lld` 和 Tauri / 后端系统依赖,并按锁预热根 npm、server-rs 与桌面壳 Cargo 下载缓存。四个 job 统一 `runs-on: genarrative-ci`,先用镜像内脚本直接从 Gitea checkout,再以 runtime 模式运行 `scripts/check-gitea-ci-job-image.sh`,同时检查缓存锁、工具链、完整 bwrap 与 Chrome headless。`RUSTUP_AUTO_INSTALL=0``rust-toolchain.toml` 变更时先重建镜像,不把下载 fallback 放回 job。
- 处理:用 `deploy/container/gitea-ci-job.Dockerfile` 预装 Node 22、Rust 1.96、`rustfmt`、Chrome、`bwrap``rg``ffmpeg``clang/lld` 和 Tauri / 后端系统依赖,并按锁预热根与 AI 游戏创作壳 npm、server-rs、桌面壳与 AI 游戏创作壳 Cargo 下载缓存。四个 job 统一 `runs-on: genarrative-ci`,先用镜像内脚本直接从 Gitea checkout,再以 runtime 模式运行 `scripts/check-gitea-ci-job-image.sh`,同时检查五份缓存锁、工具链、完整 bwrap 与 Chrome headless。`RUSTUP_AUTO_INSTALL=0``rust-toolchain.toml` 变更时先重建镜像,不把下载 fallback 放回 job。
- 依赖边界:每个 job 仍必须各自执行 `npm ci`,让当前 lockfile 和 PR 依赖在干净环境中验证;区别是命中镜像 cache 时只做本地解包,锁新增依赖时才走受控网络。不要把 `node_modules` 或 Cargo `target` 烘进镜像,也不要向不受信任 PR 挂载跨 job 可写 cache。
- 锁漂移边界:runtime 校验输出 `server_rust_cache_lock=partial` 说明镜像内 Cargo lock 与当前 checkout 不同,不代表新增 crate 已经缓存。必须在新镜像中以 `--network none`当前 lock 执行真实 `cargo fetch/build --offline``cargo metadata --no-deps` 不会证明依赖 archive 可用,不能作为替代。
- 锁漂移边界:runtime 校验输出任一 `*_cache_lock=partial` 说明镜像内 lock 与当前 checkout 不同,不代表新增依赖已经缓存;必须同时输出 Actions warning,提示可信分支落地后刷新镜像。必须在新镜像中对 server-rs、桌面壳和 AI 游戏创作壳当前 lock 执行真实 `cargo fetch --locked --offline``cargo metadata --no-deps` 不会证明依赖 archive 可用,不能作为替代。
- 构建网络边界:`CARGO_NET_RETRY` 只覆盖部分 crate 下载,registry `config.json` / index TLS 握手仍可能直接终止整次 fetch。Dockerfile 对每个 `cargo fetch --locked` 再做最多 5 次整命令级有界重试,最终仍执行断网 fetch,不能降低为无锁重试或省略离线闭合验证。
- 验证:workflow 不再出现 GitHub checkout action、apt、setup-node 或 rustup 安装 step;镜像`--network none` 下能按当前 npm / Cargo lock 完成依赖准备,四个 job 的环境校验、干净 `npm ci` 和原有测试门禁仍全部执行。
- 验证:workflow 不再出现 GitHub checkout action、apt、setup-node 或 rustup 安装 step;镜像能按五份当前 lock 完成缓存闭合,四个 job 的环境校验、经 3 次整命令级有界重试保护的干净 `npm ci` 和原有测试门禁仍全部执行。
## Gitea Actions HTTPS CONNECT 隧道必须双向收束 socket2026-08-07
- 现象:CI 的 `npm ci` 高频出现 `ECONNRESET / network aborted`Cargo 则出现 crates.io TLS EOF、连接超时或下载失败;同一出口 gateway 容器看似健康,却累计自动重启数百次,日志反复出现 `Socket.ondata -> Writable.write -> write EPIPE -> Unhandled 'error' event`
- 原因:HTTPS CONNECT 建立后使用 `upstreamSocket.pipe(clientSocket)` 与反向 pipe,但只监听 upstream `error`;客户端在 DNS 等待、下载或 job 清理期间关闭连接时,pipe 继续向已断开的 client socket 写入,未处理的 EPIPE 会让 Node 进程退出。`unless-stopped` 自动拉起和浅层 healthcheck 会掩盖崩溃,所有并发 npm / Cargo 隧道同时被 reset。
- 处理:CONNECT 一开始就为 client socket 注册 `error / close`,解析完成后为 upstream socket注册同样的双向销毁处理;DNS 返回、写 200 和开始 pipe 前都检查 client 是否已销毁。任一端 error、close 或 timeout 都幂等 destroy 两端,不把普通客户端 reset 写成错误日志。不要用进程级 `uncaughtException` 吞掉问题,也不要只增加 npm/Cargo 重试掩盖 gateway 崩溃。
- 验证:在独立 canary 和正式 gateway 上分别并发制造至少 500 次“CONNECT 后立即断开”,随后确认容器仍运行、restart count 不增加、日志无 EPIPE;再通过同一 proxy 对 npm registry 与 crates index 建立完整 TLS 隧道。切换前仍须确认 Gitea 无活跃 run 且 Runner 内层无 job 容器。
## Gitea CI 预构建镜像不能只靠 tag 判断内容
@@ -4208,6 +4215,13 @@
- 验证:覆盖“服务端已入队但提交响应丢失”后两次 POST 的 endpoint、正文 bytes 与 `Idempotency-Key` 完全相同,原键重试仍返回同一 operation,最终只出现一份 completed result 和一次计费 / 写回;恢复再次 transport 失败或临时鉴权失败仍保留同一账本;换 owner 不可见;MCP 与 REST 对同一 owner、同一请求和同一键必须命中同一 operation。
- 关联:`server-rs/crates/api-server/src/external_generation.rs``server-rs/crates/api-server/src/external_mcp.rs``docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md`
## MCP 列表不能透传完整项目快照(2026-08-07)
- 现象:账号项目数量增长后,`list_editor_projects` 把每个项目的 `canvas / layers / resources` 全量透传,REST 响应超过 MCP 4 MiB 上限,Agent 因整批失败而无法展示、查重或安全选择项目;缺少必填请求体时,内部 Axum JSON extractor 的文本 `415` 又会被泛化成“非 JSON 响应”。
- 处理:项目列表 REST 保持默认 `view=full` 兼容,并提供 `view=summary`MCP 固定使用 summary 且不向 Agent 暴露或接受 `view=full`。摘要只返回 `projectId / title / updatedAt / cover`,封面取最新且存在稳定 `objectKey``project-cover-snapshot`,展示时再调用 `/assets/read-url`,不在列表内嵌图片或签名 URL。MCP 在构造内部 REST 请求前按 OpenAPI schema 校验 required body;缺正文和缺字段分别返回结构化错误,不进入写入、上传票据或计费路径。
- 验证:用 19 个完整序列化后超过 4 MiB 的项目 fixture 证明摘要仍低于上限且不含大型布局;覆盖四个历史 `415` 工具的缺正文、空对象和非对象输入,并断言项目列表工具固定 summary、调用方不能通过 query 覆盖。
- 关联:`server-rs/crates/api-server/src/external_mcp.rs``server-rs/crates/api-server/src/external_editor_api.rs``docs/openapi/genarrative-external-v1.openapi.json``docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md`
## api-server 嵌入仓库外资源时必须同步容器构建上下文(2026-07-31)
- 现象:本地 `cargo test` 可以编译 MCP 与 Skill 下载模块,但 api-server 镜像在 Rust 编译阶段报 `include_str!` 找不到 OpenAPI 或 Skill 文件。