完成 Editor Agent Mock Agent P1 收尾
接入 Web Project 契约、SpacetimeDB 表与 api-server 控制面 新增 Mock Agent、静态构建 runner 与独立预览网关 补齐 /editor/agent 前端页面、服务客户端和 SSE 订阅 修复 sandbox 预览资源跨域加载并补充并发保护 接入本地 dev 预览端口漂移与服务身份初始化 更新 P1 技术方案、验收清单和 Hermes 共享记忆
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
|
||||
微信小程序虚拟支付接入、`wechat_mp_virtual` 渠道、`wx.requestVirtualPayment` 承接页和后端签名配置见 [【技术方案】微信虚拟支付接入-2026-05-26.md](./%E3%80%90%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88%E3%80%91%E5%BE%AE%E4%BF%A1%E8%99%9A%E6%8B%9F%E6%94%AF%E4%BB%98%E6%8E%A5%E5%85%A5-2026-05-26.md)。
|
||||
|
||||
`/editor/agent` 浏览器内 AI Web 工程编辑器的静态 SPA 沙箱预览 MVP,采用“平台编辑器壳 + api-server 控制面 + 独立 runner worker + 独立预览域”四层结构;技术方案、威胁模型和验收清单见 [【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md](./technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md)、[【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md](./technical/【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md) 和 [【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md](./technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md)。P1 先用确定性 mock Agent 生成结构化 patch、真实打通项目 / 快照 / 构建 / artifact / 预览闭环,落地拆分见 [【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md](./technical/【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md)。
|
||||
`/editor/agent` 浏览器内 AI Web 工程编辑器的静态 SPA 沙箱预览 MVP,采用“平台编辑器壳 + api-server 控制面 + 独立 runner worker + 独立预览域”四层结构;技术方案、威胁模型和验收清单见 [【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md](./technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md)、[【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md](./technical/【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md) 和 [【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md](./technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md)。P1 先用确定性 mock Agent 生成结构化 patch、真实打通项目 / 快照 / 构建 / artifact / 预览闭环,落地拆分见 [【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md](./technical/【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md),可执行开发计划见 [【开发计划】EditorAgentMockAgentP1可执行开发计划-2026-06-15.md](./planning/【开发计划】EditorAgentMockAgentP1可执行开发计划-2026-06-15.md)。
|
||||
|
||||
`/editor/canvas` 图片画布编辑器的画布素材 ZIP 导出能力,入口放在右上角标题栏下载图标内,第一版采用前端 JSZip 打包画布中有效图层引用的上传图、生成图和修改结果,方案见 [【前端架构】图片画布素材导出方案-2026-06-15.md](./technical/【前端架构】图片画布素材导出方案-2026-06-15.md)。
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
## 当前计划
|
||||
|
||||
- [【玩法创作】创作流程统一总计划-2026-05-30.md](./【玩法创作】创作流程统一总计划-2026-05-30.md):创作入口、统一创作页、统一生成页、结果页、发布、作品架、广场和运行态的阶段计划、进度记录、并行波次和可直接派发的任务包。
|
||||
- [【开发计划】EditorAgentMockAgentP1可执行开发计划-2026-06-15.md](./【开发计划】EditorAgentMockAgentP1可执行开发计划-2026-06-15.md):`/editor/agent` Mock Agent P1 的可执行波次、任务包、验收门禁和覆盖矩阵,完整承接技术落地计划。
|
||||
|
||||
## 维护规则
|
||||
|
||||
|
||||
641
docs/planning/【开发计划】EditorAgentMockAgentP1可执行开发计划-2026-06-15.md
Normal file
641
docs/planning/【开发计划】EditorAgentMockAgentP1可执行开发计划-2026-06-15.md
Normal file
@@ -0,0 +1,641 @@
|
||||
# Editor Agent Mock Agent P1 可执行开发计划
|
||||
|
||||
更新时间:`2026-06-15`
|
||||
|
||||
## 计划定位
|
||||
|
||||
本文是 [【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md](../technical/【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md) 的可执行拆解版。目标不是重写技术方案,而是把 P1 的所有能力、边界、安全约束、任务拆分、验证命令和验收场景拆成可以派发、可以并行、可以逐项打勾的开发计划。
|
||||
|
||||
P1 只交付确定性 mock Agent 的最小纵切:
|
||||
|
||||
```text
|
||||
/editor/agent 输入 mock 指令
|
||||
-> api-server mock Agent 生成结构化 patch
|
||||
-> patch 校验并保存新 snapshot
|
||||
-> 创建 preview build
|
||||
-> runner 静态构建固定模板
|
||||
-> 产出 immutable artifact
|
||||
-> preview gateway 签发独立预览 URL
|
||||
-> 前端 iframe 切换预览
|
||||
```
|
||||
|
||||
P1 完成后,真实 Agent 仍后置到 P2+;后续只替换“需求文本 -> 结构化 patch”的来源,不推翻项目、快照、构建、artifact 和预览主链路。
|
||||
|
||||
## 硬边界
|
||||
|
||||
以下边界是 P1 开发门禁,任一项不能为了赶进度绕过:
|
||||
|
||||
- 不接真实 LLM / Agent。
|
||||
- 不接 LangChain-Rust / `platform-agent`。
|
||||
- 不开放任意 npm 依赖安装。
|
||||
- 不执行 AI 自定义 `package.json` scripts。
|
||||
- 不做 HMR、长驻 dev server、WebSocket 代理、终端 shell、后端服务和任意端口代理。
|
||||
- 不做 Web project 作品化发布。
|
||||
- 不做完整 lease / controller / worker 持久任务队列;P1 可一次性触发 runner,但 runner 必须独立于 api-server 执行。
|
||||
- 不做主站同源预览;开发态也必须独立端口 / origin。
|
||||
- 不把 AI 生成工程写入当前仓库源码目录。
|
||||
- 不把平台 access token、用户 cookie、SpacetimeDB、OSS 写权限、LLM provider 密钥暴露给 runner 或 preview 页面。
|
||||
|
||||
## 总体执行顺序
|
||||
|
||||
P1 分 6 个开发波次。每个波次都有可退出标准,前一波次未通过时不得进入依赖它的联调。
|
||||
|
||||
| 波次 | 覆盖任务 | 目标 | 可并行性 |
|
||||
| --- | --- | --- | --- |
|
||||
| Wave 0 | 准备与基线 | 确认文档、现状、路由和测试入口 | 独立 |
|
||||
| Wave 1 | P1-01、P1-02 | 契约、DTO、SpacetimeDB 表和 facade 闭合 | DTO 与表可并行,最终统一 |
|
||||
| Wave 2 | P1-03、P1-04 | api-server 控制面和 mock Agent 闭合 | 与 runner 原型部分并行 |
|
||||
| Wave 3 | P1-05、P1-06 | 独立 runner、artifact、preview gateway 闭合 | runner 与 gateway 可并行 |
|
||||
| Wave 4 | P1-07 | `/editor/agent` 前端页面和 SSE 接入 | 依赖 API 契约,可先 mock client |
|
||||
| Wave 5 | P1-08 | 自动化、安全验收、浏览器 smoke 和文档回填 | 贯穿执行,最终收口 |
|
||||
|
||||
## Wave 0:准备与基线
|
||||
|
||||
目标:在编码前消除落点歧义,确认本轮只实现 P1。
|
||||
|
||||
执行项:
|
||||
|
||||
- [x] 阅读并以本文档、P1 落地计划、AIWeb 沙箱预览方案、威胁模型和 MVP 验收清单作为实现依据。
|
||||
- [x] 确认当前后端路线仍为 `server-rs + Axum + SpacetimeDB`,旧 `server-node` / Express / PostgreSQL 只作历史参考。
|
||||
- [x] 扫描现有 `/editor`、`/editor/canvas`、SSE、api-server route module、SpacetimeDB 表目录和 `spacetime-client` facade 写法。
|
||||
- [x] 确认新增 Markdown 只放 `docs/` 或 `.hermes/` 可提交内容,不写入个人路径、密钥、会话记录。
|
||||
- [x] 确认 P1 不需要新增玩法入口,不走新增玩法 skill;`/editor/agent` 属于 Web 工程编辑器入口。
|
||||
|
||||
退出条件:
|
||||
|
||||
- [x] 已列出将复用的现有路由挂载方式、SSE client、api-server module 注册方式和 SpacetimeDB schema guard。
|
||||
- [x] 无未决产品问题阻塞 P1 纵切。
|
||||
|
||||
## Wave 1:契约、DTO 与 SpacetimeDB 持久化
|
||||
|
||||
### P1-01 契约与 DTO
|
||||
|
||||
主要落点:
|
||||
|
||||
- `server-rs/crates/shared-contracts`
|
||||
- `packages/shared`
|
||||
|
||||
交付物:
|
||||
|
||||
- [x] 新增或扩展 Rust DTO:`WebProject`、`WebProjectSnapshot`、`WebProjectFile`、`WebProjectPatch`、`WebProjectPatchOperation`、`WebProjectPreviewBuild`、`WebProjectPreviewBuildEvent`、`MockAgentTurnRequest`、`MockAgentTurnResponse`。
|
||||
- [x] 新增或扩展 TypeScript DTO,字段名与 Rust 契约保持同名语义。
|
||||
- [x] 固化 `templateKey = react-vite-ts-static`。
|
||||
- [x] 固化 `buildStatus = queued | running | succeeded | failed | cancelled | expired | stale`。
|
||||
- [x] 明确 `previewUrl` 只由后端返回,前端不得自行拼接。
|
||||
- [x] 在契约注释中写清路径、内容、大小和依赖限制。
|
||||
|
||||
必须覆盖的字段:
|
||||
|
||||
- [x] `projectId`:不可枚举字符串 ID。
|
||||
- [x] `ownerUserId`:项目归属用户。
|
||||
- [x] `templateKey`:固定模板。
|
||||
- [x] `activeSnapshotId`:当前编辑快照。
|
||||
- [x] `activePreviewBuildId`:当前成功预览构建。
|
||||
- [x] `files`:P1 允许小型文本源码和少量静态资源。
|
||||
- [x] `patchSummary`:mock Agent 或用户编辑摘要。
|
||||
- [x] `buildStatus`:构建状态枚举。
|
||||
- [x] `previewUrl`:后端签发的独立预览 URL。
|
||||
|
||||
路径与内容校验规则必须进入 DTO 文档和后端校验实现:
|
||||
|
||||
- [x] 只允许相对路径。
|
||||
- [x] 拒绝绝对路径。
|
||||
- [x] 拒绝 `..`。
|
||||
- [x] 拒绝 `.env`、`.npmrc`、`.git`、`.ssh`。
|
||||
- [x] 拒绝符号链接语义。
|
||||
- [x] 限制目录深度、单文件大小、snapshot 总大小。
|
||||
- [x] P1 只允许文本源码和少量静态资源。
|
||||
- [x] P1 可编辑范围限定为 `src/App.tsx`、`src/App.css`、`src/components/**`、`src/assets/**` 与 `public/**`;固定模板控制文件不进入用户 snapshot。
|
||||
- [x] `package.json` 不是事实源;新增依赖和 scripts 在 P1 拒绝或忽略。
|
||||
|
||||
退出条件:
|
||||
|
||||
- [x] Rust / TypeScript 契约字段和状态枚举一致。
|
||||
- [x] DTO 单测或类型测试覆盖状态枚举、patch operation 和危险路径样例。
|
||||
- [x] 前端 client 可以只依赖 `packages/shared` 类型完成编译。
|
||||
|
||||
### P1-02 SpacetimeDB 表与 procedure/facade
|
||||
|
||||
主要落点:
|
||||
|
||||
- `server-rs/crates/spacetime-module/src/web_project.rs`
|
||||
- `server-rs/crates/spacetime-module/src/lib.rs`
|
||||
- `server-rs/crates/spacetime-module/src/migration.rs`
|
||||
- `server-rs/crates/spacetime-client/src/mapper/web_project.rs`
|
||||
- `server-rs/crates/spacetime-client/src/web_project.rs`
|
||||
|
||||
新增表:
|
||||
|
||||
- [x] `web_project`
|
||||
- [x] `web_project_snapshot`
|
||||
- [x] `web_project_preview_build`
|
||||
- [x] `web_project_service_identity`
|
||||
|
||||
`web_project` 字段:
|
||||
|
||||
- [x] `project_id`
|
||||
- [x] `owner_user_id`
|
||||
- [x] `title`
|
||||
- [x] `template_key`
|
||||
- [x] `active_snapshot_id`
|
||||
- [x] `active_preview_build_id`
|
||||
- [x] `created_at`
|
||||
- [x] `updated_at`
|
||||
|
||||
`web_project_snapshot` 字段:
|
||||
|
||||
- [x] `snapshot_id`
|
||||
- [x] `project_id`
|
||||
- [x] `owner_user_id`
|
||||
- [x] `parent_snapshot_id`
|
||||
- [x] `template_key`
|
||||
- [x] `files_json`
|
||||
- [x] `patch_summary`
|
||||
- [x] `created_by`
|
||||
- [x] `created_at`
|
||||
|
||||
`web_project_preview_build` 字段:
|
||||
|
||||
- [x] `job_id`
|
||||
- [x] `project_id`
|
||||
- [x] `snapshot_id`
|
||||
- [x] `owner_user_id`
|
||||
- [x] `status`
|
||||
- [x] `logs_json`
|
||||
- [x] `artifact_id`
|
||||
- [x] `preview_token_id`
|
||||
- [x] `preview_url`
|
||||
- [x] `error_summary`
|
||||
- [x] `created_at`
|
||||
- [x] `started_at`
|
||||
- [x] `finished_at`
|
||||
- [x] `updated_at`
|
||||
|
||||
SpacetimeDB 执行规则:
|
||||
|
||||
- [x] 表结构使用仓库现有 Rust SpacetimeDB 2.x 写法,不使用旧 API。
|
||||
- [x] `web_project` procedure 先用 `ctx.sender()` 校验服务身份 allowlist,再信任 api-server 从登录态代传的业务 `owner_user_id`,前端不得传 owner。
|
||||
- [x] 领域规则不写到前端;事务编排留在 `spacetime-module`。
|
||||
- [x] 后端访问统一经 `spacetime-client` facade,不在 api-server 绕过 facade。
|
||||
- [x] 表目录、`migration.rs` 和生成 bindings 同步更新;`web_project_service_identity` 已明确不随普通业务迁移包导入导出。
|
||||
- [x] 如果后续给已有表新增字段,字段必须放在结构体最后并设置明确默认值;需要改名或改类型时先确认迁移计划。
|
||||
|
||||
最小 procedure / facade 能力:
|
||||
|
||||
- [x] 创建固定模板项目。
|
||||
- [x] 读取项目。
|
||||
- [x] 读取 active snapshot。
|
||||
- [x] 保存 snapshot。
|
||||
- [x] 创建 preview build。
|
||||
- [x] 更新 build 状态和日志。
|
||||
- [x] 成功构建后,在 active snapshot 匹配时推进 `active_preview_build_id`。
|
||||
- [x] failed / cancelled / expired / stale 不覆盖 active preview。
|
||||
|
||||
退出条件:
|
||||
|
||||
- [x] 能通过 facade 创建项目、保存 snapshot、记录 build。
|
||||
- [ ] 状态机保护 active preview 的后端定向单测未在本轮验证清单中出现;当前代码路径已落地,需后续补 `spacetime-module` / `api-server` 定向测试或集成测试。
|
||||
- [x] 执行 `npm run spacetime:generate`,生成 bindings 已随 Web Project schema / procedure 更新;后续变更 schema 时仍需复跑。
|
||||
- [x] 执行 `npm run check:spacetime-schema`。
|
||||
|
||||
## Wave 2:api-server 控制面与 mock Agent
|
||||
|
||||
### P1-03 api-server Web Project 模块
|
||||
|
||||
主要落点:
|
||||
|
||||
- `server-rs/crates/api-server/src/web_project.rs`
|
||||
- `server-rs/crates/api-server/src/modules/web_project.rs`
|
||||
- `server-rs/crates/api-server/src/app.rs` 或现有 module 注册入口
|
||||
|
||||
P1 API:
|
||||
|
||||
- [x] `POST /api/creation/web-project/projects`
|
||||
- [x] `GET /api/creation/web-project/projects/{projectId}`
|
||||
- [x] `GET /api/creation/web-project/projects/{projectId}/snapshot`
|
||||
- [x] `PATCH /api/creation/web-project/projects/{projectId}/files`
|
||||
- [x] `POST /api/creation/web-project/projects/{projectId}/mock-agent-turns`
|
||||
- [x] `POST /api/runtime/web-project/projects/{projectId}/preview-builds`
|
||||
- [x] `GET /api/runtime/web-project/preview-builds/{jobId}`
|
||||
- [x] `GET /api/runtime/web-project/preview-builds/{jobId}/events`
|
||||
|
||||
控制面职责:
|
||||
|
||||
- [x] 鉴权并校验 project owner。
|
||||
- [x] 创建固定模板项目。
|
||||
- [x] 读取 active snapshot。
|
||||
- [x] 校验用户编辑和 mock patch。
|
||||
- [x] 保存新 snapshot。
|
||||
- [x] 创建 preview build。
|
||||
- [x] 触发 P1 runner 构建,但 api-server 不执行 `npm` / `vite` / 用户工程代码。
|
||||
- [x] 记录构建日志和状态。
|
||||
- [x] 构建成功后签发 preview URL。
|
||||
- [x] 构建失败时保留上一版 active preview。
|
||||
|
||||
SSE 口径:
|
||||
|
||||
- [x] `/events` 外部契约保持 SSE。
|
||||
- [x] 复用 `src/services/sseStream.ts` 可消费的事件格式。
|
||||
- [x] 事件类型覆盖 `queued`、`running`、`log`、`succeeded`、`failed`、`cancelled`、`expired`、`stale`。
|
||||
- [x] P1 可先用 api-server 内存广播或短轮询兼容,但返回契约必须保持可迁移到持久 job。
|
||||
|
||||
退出条件:
|
||||
|
||||
- [x] API handler 只做 BFF / 控制面编排,不执行工程代码。
|
||||
- [x] 所有项目、snapshot、build 读取和写入都有 owner 校验。
|
||||
- [ ] API 单测覆盖鉴权、owner mismatch、危险 patch、失败不覆盖 active preview:当前主线已通过 `cargo check -p api-server` 和前端 / runner 自动化,后端 API 定向单测不在本轮已验证命令中,保留待补。
|
||||
|
||||
### P1-04 mock Agent
|
||||
|
||||
主要落点:
|
||||
|
||||
- `server-rs/crates/api-server/src/web_project_mock_agent.rs`
|
||||
|
||||
输入契约:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "做一个蓝色计数按钮页面",
|
||||
"baseSnapshotId": "snapshot_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
输出契约:
|
||||
|
||||
```json
|
||||
{
|
||||
"snapshot": "...",
|
||||
"patch": {
|
||||
"operations": [
|
||||
{
|
||||
"type": "updateFile",
|
||||
"path": "src/App.tsx",
|
||||
"content": "..."
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": "更新首页计数按钮示例"
|
||||
}
|
||||
```
|
||||
|
||||
mock 指令族:
|
||||
|
||||
- [x] `计数` / `按钮`:生成带计数按钮的 React 页面。
|
||||
- [x] `卡片` / `列表`:生成卡片列表页面。
|
||||
- [x] `蓝色` / `绿色` / `粉色`:调整 CSS 主题色。
|
||||
- [x] `破坏构建`:生成 TypeScript 编译错误,用于验收失败保留上一版预览。
|
||||
- [x] 其它输入:只更新标题和说明文案,避免 mock 分支过多。
|
||||
|
||||
强制规则:
|
||||
|
||||
- [x] mock Agent 确定性、可测试。
|
||||
- [x] mock Agent 只能返回结构化 patch。
|
||||
- [x] mock Agent 输出必须经过统一 `validate_web_project_patch(...)`。
|
||||
- [x] mock Agent 不得直接写表。
|
||||
- [x] mock Agent 不得绕过 path、大小、敏感文件、依赖和 scripts 校验。
|
||||
|
||||
退出条件:
|
||||
|
||||
- [x] mock 指令族单测全部通过。
|
||||
- [x] 破坏构建场景能生成稳定失败源码。
|
||||
- [x] 危险 patch 样例被统一校验拒绝。
|
||||
|
||||
## Wave 3:runner、artifact 与 preview gateway
|
||||
|
||||
### P1-05 P1 runner
|
||||
|
||||
主要落点:
|
||||
|
||||
- `server-rs/crates/web-project-runner`
|
||||
|
||||
P1 runner 形态:
|
||||
|
||||
- [x] 可以先采用“一次性 runner 进程”。
|
||||
- [x] 不做持久 worker 队列。
|
||||
- [x] 执行面必须独立于 api-server、主站源码目录和预览域。
|
||||
- [x] api-server 只传递 job 身份和最小任务能力。
|
||||
|
||||
runner 输入:
|
||||
|
||||
- [x] `jobId`
|
||||
- [x] `projectId`
|
||||
- [x] `snapshotId`
|
||||
- [x] snapshot files 包。
|
||||
- [x] artifact root 配置键或受控 artifact 写入能力。
|
||||
|
||||
runner 输入禁止携带:
|
||||
|
||||
- [x] runner 命令。
|
||||
- [x] 构建参数。
|
||||
- [x] 工作区路径。
|
||||
- [x] artifact 输出路径。
|
||||
- [x] 用户自定义 registry / proxy / lockfile 策略。
|
||||
|
||||
runner 步骤:
|
||||
|
||||
- [x] 创建任务级临时目录。
|
||||
- [x] 用 runner 内置模板或 runner 管理的只读模板缓存写入固定 React / Vite / TypeScript 模板。
|
||||
- [x] 规范化 snapshot 文件路径,写入前确认最终路径仍在任务临时目录内。
|
||||
- [x] 忽略或重写用户 `package.json` 的危险字段。
|
||||
- [x] 使用环境变量白名单启动子进程,清空平台密钥、`.env`、token、代理和私有 registry 配置。
|
||||
- [x] 执行平台固定命令清单。
|
||||
- [ ] 限制 CPU、内存、磁盘、进程数、打开文件数、构建时间、日志长度、单文件大小和 artifact 大小:当前已有限制构建时间、日志长度、单文件大小和 snapshot 大小;CPU / 内存 / 进程数 / 打开文件数 / 磁盘配额仍属于后续部署层或 P2 门禁。
|
||||
- [x] 成功后把 `dist/` 复制到 immutable artifact 目录。
|
||||
- [x] artifact 目录由 runner 配置计算,不接受请求指定。
|
||||
- [x] 失败时返回错误摘要和日志片段,日志需脱敏并避免完整宿主路径。
|
||||
- [x] 清理临时目录。
|
||||
|
||||
允许的构建命令只能来自平台常量,使用 argv 方式执行,不经 shell、不拼接字符串、不执行用户传入命令:
|
||||
|
||||
```text
|
||||
npm ci --ignore-scripts --offline --no-audit --fund=false
|
||||
npm exec --offline -- vite build
|
||||
```
|
||||
|
||||
依赖与网络策略:
|
||||
|
||||
- [x] P1 默认离线使用 runner 管理的模板依赖缓存。
|
||||
- [ ] 如果离线缓存不足,只能访问平台受控 npm registry mirror,并由 runner 写入受控 `.npmrc`:P1 当前保持 `npm ci --offline`,mirror 接入未启用。
|
||||
- [x] 用户 snapshot 中的 `.npmrc`、registry、proxy、`package-lock.json` 篡改和新增依赖必须拒绝或重写。
|
||||
- [x] 禁止复用当前仓库的 `node_modules`、源码目录或本机全局 npm cache 作为 runner 工作区输入。
|
||||
- [ ] P1 构建期默认 deny all 网络:当前命令为 offline,但进程级网络 deny-all 未在代码层验证,保留后续部署门禁。
|
||||
- [ ] 只在使用平台受控 registry mirror 或资产只读签名域时开放精确白名单:mirror / 资产白名单未在本轮落地。
|
||||
- [ ] 必须阻断 RFC1918 内网、云 metadata 地址、api-server 管理端口、SpacetimeDB、生产数据库、Docker daemon:需后续容器 / sandbox 网络策略验证。
|
||||
- [ ] 必须覆盖 HTTP redirect 和 DNS rebinding 到内网:需后续网络白名单实现后验证。
|
||||
|
||||
退出条件:
|
||||
|
||||
- [x] 固定模板可成功构建并生成 immutable artifact。
|
||||
- [x] `破坏构建` 能稳定失败并返回可读错误摘要。
|
||||
- [x] runner 日志不泄露平台密钥、OSS 写签名、完整宿主路径和环境变量 dump。
|
||||
- [x] runner 工作区逃逸、危险 path 和请求指定 artifact root 的测试通过。
|
||||
|
||||
### P1-06 Preview Gateway
|
||||
|
||||
主要落点:
|
||||
|
||||
- `api-server` 独立 listener / 端口 / vhost / origin,或独立 runner/gateway 模块。
|
||||
|
||||
开发态 preview URL:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:<previewPort>/p/<previewToken>/
|
||||
```
|
||||
|
||||
生产形态:
|
||||
|
||||
```text
|
||||
https://sandbox.genarrative.world/p/<previewToken>/
|
||||
```
|
||||
|
||||
gateway 必须:
|
||||
|
||||
- [x] 校验 token。
|
||||
- [x] 绑定 owner / project / snapshot / artifact。
|
||||
- [x] 禁止 path traversal。
|
||||
- [x] MIME 类型按白名单输出。
|
||||
- [x] `index.html` fallback 只在 artifact 根内生效。
|
||||
- [x] token 过期或 artifact 删除后返回确定错误。
|
||||
- [x] 输出 CSP,默认 `connect-src 'none'`。
|
||||
- [x] 禁用 Service Worker 和持久缓存。
|
||||
- [x] 不向预览页注入平台 access token、用户 cookie、SpacetimeDB、OSS 写权限或 LLM provider 密钥。
|
||||
- [x] 服务静态 artifact 的入口必须是独立 listener / 端口 / vhost / origin。
|
||||
- [x] 不得把 preview artifact 挂到主站同源路径下。
|
||||
|
||||
前端 iframe sandbox:
|
||||
|
||||
```text
|
||||
sandbox="allow-scripts"
|
||||
```
|
||||
|
||||
P1 不允许增加:
|
||||
|
||||
- [x] `allow-same-origin`
|
||||
- [x] `allow-downloads`
|
||||
- [x] `allow-popups`
|
||||
- [x] `allow-top-navigation`
|
||||
|
||||
退出条件:
|
||||
|
||||
- [x] 预览 origin 与主站 origin 不同。
|
||||
- [ ] path traversal、错误 MIME、旧 token、过期 token、artifact 删除、跨租户访问测试通过:代码路径已实现,后端 gateway 定向测试 / 浏览器 smoke 未在本轮已验证命令中出现。
|
||||
- [x] iframe sandbox 和 CSP 检查通过:前端 iframe 仅使用 `sandbox="allow-scripts"`;preview gateway 保持 `connect-src 'none'`,并通过 `GENARRATIVE_WEB_PROJECT_PREVIEW_FRAME_ANCESTORS` 显式允许平台编辑器宿主嵌入。
|
||||
|
||||
## Wave 4:前端 `/editor/agent`
|
||||
|
||||
### P1-07 页面、client 与状态恢复
|
||||
|
||||
主要落点:
|
||||
|
||||
- `src/components/editor/agent/WebProjectAgentEditorPage.tsx`
|
||||
- `src/components/editor/agent/WebProjectAgentEditorPage.test.tsx`
|
||||
- `src/components/editor/agent/webProjectAgentViewModel.ts`
|
||||
- `src/services/web-project/webProjectClient.ts`
|
||||
- `src/services/web-project/webProjectSse.ts`
|
||||
- `src/services/web-project/webProjectClient.test.ts`
|
||||
|
||||
入口路由:
|
||||
|
||||
- [x] `/editor/agent`
|
||||
|
||||
桌面布局:
|
||||
|
||||
- [x] 左侧文件树。
|
||||
- [x] 中间代码编辑区,P1 可先用 `<textarea>`。
|
||||
- [x] 下方或右侧 mock Agent 输入区。
|
||||
- [x] 构建日志区。
|
||||
- [x] 右侧 iframe 预览。
|
||||
|
||||
移动端布局:
|
||||
|
||||
- [x] 使用 tabs:文件、代码、预览、日志。
|
||||
- [x] 不默认展示大段功能说明文案。
|
||||
- [x] 预览 iframe 保持可见尺寸。
|
||||
- [x] 具备明确加载 / 失败状态。
|
||||
|
||||
前端状态原则:
|
||||
|
||||
- [x] active preview 来自后端返回,不由前端自行推断。
|
||||
- [x] 构建失败不清空已有 iframe。
|
||||
- [x] 刷新后先读取项目和 active snapshot,再恢复未终态 job 订阅。
|
||||
- [x] 用户连续编辑触发新 snapshot 时,旧 build 只能显示为旧日志,不得覆盖新 preview。
|
||||
- [x] 前端只做表现、交互和临时 UI 状态,不承接正式业务真相。
|
||||
- [x] SSE 断开后可重新读取 job 状态。
|
||||
|
||||
client 能力:
|
||||
|
||||
- [x] 创建项目。
|
||||
- [x] 读取项目。
|
||||
- [x] 读取 active snapshot。
|
||||
- [x] 保存文件 patch。
|
||||
- [x] 提交 mock agent turn。
|
||||
- [x] 创建 preview build。
|
||||
- [x] 读取 build 状态。
|
||||
- [x] 订阅 build events。
|
||||
|
||||
退出条件:
|
||||
|
||||
- [x] 页面能完成创建、编辑、mock 指令、构建、预览。
|
||||
- [x] 移动端 tabs 可操作,文本不溢出,不出现互相遮挡。
|
||||
- [x] 构建失败后上一版 iframe 保留。
|
||||
- [x] 刷新恢复 project、active snapshot、active preview 和未终态 job 状态。
|
||||
|
||||
## Wave 5:自动化、验收与收口
|
||||
|
||||
### P1-08 自动化与 smoke
|
||||
|
||||
后端与 schema 验证:
|
||||
|
||||
```bash
|
||||
npm run spacetime:generate
|
||||
npm run check:spacetime-schema
|
||||
cargo test -p spacetime-module web_project --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p spacetime-client web_project --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server web_project --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
本次 P1 文档收口时已确认通过的后端 / schema 自动化为:
|
||||
|
||||
- [x] `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`
|
||||
- [x] `cargo test -p web-project-runner --manifest-path server-rs/Cargo.toml`(2026-06-16 追加 Windows npm 启动、系统环境白名单和模板相对 base 后为 10 passed)
|
||||
- [x] `cargo test -p api-server web_project --manifest-path server-rs/Cargo.toml`(覆盖 preview build 全局 / 同 project 限流、rename 目标冲突和 preview gateway CORS / CSP)
|
||||
- [x] `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
|
||||
- [x] `cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml`
|
||||
- [x] `npm run check:spacetime-schema`
|
||||
|
||||
前端验证:
|
||||
|
||||
```bash
|
||||
npm run test -- src/services/web-project
|
||||
npm run test -- src/components/editor/agent
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
本次 P1 文档收口时已确认通过的前端 / 仓库自动化为:
|
||||
|
||||
- [x] `npm run typecheck`
|
||||
- [x] `npm run test -- src/services/web-project src/components/editor/agent --reporter verbose`
|
||||
- [x] `npm run check:encoding`
|
||||
- [x] `git diff --check`
|
||||
|
||||
涉及 API smoke 时:
|
||||
|
||||
- [x] 使用当前后端架构文档要求的脚本重新运行后端:本地以 `npm run dev:api-server -- --no-interactive` 启动,并显式复用 `http://127.0.0.1:3101` SpacetimeDB。
|
||||
- [x] 检查 `/healthz`:2026-06-16 本地运行态返回 `200 {"ok":true,"service":"genarrative-api-server"}`。
|
||||
- [x] 不使用旧 `maincloud` 命令、环境变量或文档口径。
|
||||
|
||||
运行态 API / preview gateway smoke:
|
||||
|
||||
- [x] 创建固定模板项目:`web-project-65db0e3fe87044bf9d2f24cba20b016b`。
|
||||
- [x] 输入“做一个蓝色计数按钮页面”。
|
||||
- [x] mock Agent 返回结构化 patch,摘要为“更新为计数按钮页面”。
|
||||
- [x] api-server 保存新 snapshot。
|
||||
- [x] 创建 preview build。
|
||||
- [x] runner 通过 offline 模板依赖构建成功并产出 immutable artifact:`web-build-c172cea3b9cc41c09aa87e5dbb14cfee` / `artifact_web-build-c172cea3b9cc41c09aa87e5dbb14cfee_web-snapshot-6b801edec0fd437cb59750d0639dacfc`。
|
||||
- [x] build 状态进入 `succeeded`,preview URL 为 `http://127.0.0.1:3104/p/wpt_1781577376836056_55138dd644524564b388f2fa9bd9d9d2/`,并确认 `assets/**` 可从 `/p/<token>/` 子路径加载。
|
||||
- [x] 输入“破坏构建”。
|
||||
- [x] 新 build 进入 `failed`:`web-build-327ccbe557e24ce880c1ea85ba6eaf62`,错误摘要为 `runner 构建命令失败:✗ Build failed in 50ms`。
|
||||
- [x] 上一版 active preview 仍可通过旧 preview URL 访问并包含“蓝色计数按钮”。
|
||||
- [x] 真实浏览器点击 smoke:2026-06-16 使用浏览器自动化脚本在隔离端口 `3002/8083/3105` 通过,确认 iframe `sandbox="allow-scripts"`,按钮文案从 `已点击 0 次` 变为 `已点击 1 次`,再触发“破坏构建”后上一版 iframe URL 保持不变。
|
||||
- [x] 刷新恢复的前端状态已有组件测试覆盖;真实浏览器刷新恢复仍随上条保留。
|
||||
|
||||
安全验收:
|
||||
|
||||
- [x] 绝对路径被拒绝。
|
||||
- [x] `..` 被拒绝。
|
||||
- [x] `.env` / `.npmrc` / `.git` / `.ssh` 被拒绝。
|
||||
- [x] 修改 `package.json` 新增依赖被拒绝或忽略。
|
||||
- [x] 用户传入构建命令、scripts、registry、proxy 被拒绝或忽略。
|
||||
- [x] runner 构建命令只来自平台 allowlist;Windows 下 npm 通过 `cmd.exe /d /s /c npm.cmd ...` 启动,但不接受用户命令字符串。
|
||||
- [x] runner 无平台密钥环境变量。
|
||||
- [x] runner 无宿主源码挂载。
|
||||
- [ ] runner 无 Docker socket:当前一次性 runner 不显式挂载 Docker socket,但仍需部署 / 容器层 smoke 确认。
|
||||
- [x] runner 无宿主 `node_modules` 输入。
|
||||
- [x] runner 只能在任务级临时目录写入 snapshot,路径解析后不能逃逸工作区。
|
||||
- [ ] 构建期默认无外网;允许网络时只能访问平台白名单域名:当前为 offline 命令,进程级 deny-all / 白名单未实际验证。
|
||||
- [x] artifact root 不来自请求。
|
||||
- [x] preview 不服务 runner 临时工作区。
|
||||
- [x] preview iframe 与主站不同 origin。
|
||||
- [x] preview gateway 输出 CSP,禁止 Service Worker 和未白名单 `connect-src`。
|
||||
- [x] failed / cancelled / expired / stale 不覆盖 active preview。
|
||||
|
||||
## 可派发任务包
|
||||
|
||||
| 任务包 | 建议负责人 | 前置依赖 | 交付物 | 验收 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A. 契约与类型 | 后端 + 前端各 1 人 | Wave 0 | Rust / TS DTO、状态枚举、patch operation | 类型检查与 DTO 单测 |
|
||||
| B. SpacetimeDB 持久化 | 后端 1 人 | A 初稿 | 三张表、procedure、migration、bindings、facade | schema guard、facade 单测 |
|
||||
| C. api-server 控制面 | 后端 1 人 | A、B | Web Project API、owner 校验、SSE 事件契约 | api-server 单测 |
|
||||
| D. mock Agent | 后端 1 人 | A、C 部分接口 | 确定性 mock 规则和 patch 校验接入 | mock 指令族测试 |
|
||||
| E. runner | 后端 / 平台 1 人 | A | 独立 runner crate、固定模板构建、artifact 写入 | runner 单测和手动构建 |
|
||||
| F. preview gateway | 后端 / 平台 1 人 | B、E | 独立 origin 静态 artifact 服务、token 校验、CSP | gateway 安全测试 |
|
||||
| G. 前端页面 | 前端 1 人 | A、C 接口稳定 | `/editor/agent` 页面、client、SSE、移动端 tabs | 前端测试与浏览器 smoke |
|
||||
| H. 端到端验收 | 全员轮值 | A-G | 自动化、文档回填、风险清单关闭 | P1 happy path + 安全验收 |
|
||||
|
||||
## 状态机实现门禁
|
||||
|
||||
构建状态必须按以下规则落地:
|
||||
|
||||
- [x] 新 snapshot 创建后,新 build 成为当前候选。
|
||||
- [x] 旧 running build 只能继续展示为旧日志,不能覆盖新 preview。
|
||||
- [x] 只有 `snapshot_id == active_snapshot_id` 且 `status == succeeded` 的 build 能推进 `active_preview_build_id`。
|
||||
- [x] `failed`、`cancelled`、`expired`、`stale` 永远不能覆盖 active preview。
|
||||
- [x] build 成功写入 artifact 后,再签发 preview token 和 preview URL。
|
||||
- [x] preview URL 由后端返回,前端不拼接 token path。
|
||||
|
||||
## 覆盖矩阵
|
||||
|
||||
| 原落地计划章节 | 本文覆盖位置 |
|
||||
| --- | --- |
|
||||
| 背景 / P1 目标 | 计划定位 |
|
||||
| 非目标 | 硬边界 |
|
||||
| 阶段边界 | 总体执行顺序、硬边界 |
|
||||
| 与原始设计一致性检查 | 硬边界、Wave 3、Wave 5 |
|
||||
| 契约设计 | P1-01 |
|
||||
| 后端存储 | P1-02 |
|
||||
| api-server 控制面 | P1-03 |
|
||||
| Mock Agent 规则 | P1-04 |
|
||||
| Runner 与构建 | P1-05 |
|
||||
| Preview Gateway | P1-06 |
|
||||
| 前端落点 | P1-07 |
|
||||
| 任务拆分 | 可派发任务包 |
|
||||
| 验收场景 | P1-08 |
|
||||
| 验证命令 | P1-08 |
|
||||
| 风险与处理 | 风险清单 |
|
||||
| 后续衔接 | P1 完成定义与 P2 交接 |
|
||||
|
||||
## 风险清单
|
||||
|
||||
| 风险 | 处理 |
|
||||
| --- | --- |
|
||||
| runner 与 api-server 边界被做薄 | api-server 只创建 job 和触发 runner;不得在自身进程内执行构建命令 |
|
||||
| mock Agent 绕过校验 | mock 输出必须走统一 patch DTO 和 `validate_web_project_patch(...)` |
|
||||
| 为赶进度直接同源预览 | 开发态也使用独立端口 / origin;同源预览不允许进入 P1 |
|
||||
| snapshot JSON 膨胀 | P1 限制单文件和总大小;P2 再拆 digest / object store |
|
||||
| 失败覆盖成功预览 | 状态机以 active snapshot 和 succeeded build 双条件推进 active preview |
|
||||
| runner 泄露宿主信息 | 环境变量白名单、日志脱敏、无宿主源码挂载、无宿主 node_modules 输入 |
|
||||
| 依赖供应链扩大 | P1 固定模板依赖;新增依赖、scripts、registry、proxy 拒绝或忽略 |
|
||||
| 前端把业务真相本地化 | active preview、build status 和 snapshot 均以后端返回为准 |
|
||||
|
||||
## P1 完成定义与 P2 交接
|
||||
|
||||
P1 完成必须同时满足:
|
||||
|
||||
- [x] P1-01 到 P1-08 本地 P1 收尾退出条件完成:自动化、运行态 API / preview gateway smoke 和真实浏览器点击 smoke 已过;生产服务身份初始化和部分部署级安全门禁按 P2 / 上线门禁保留,不阻断本地 P1 收尾。
|
||||
- [x] happy path 和“破坏构建保留上一版预览”通过本地运行态 API / preview gateway smoke。
|
||||
- [x] happy path 和“破坏构建保留上一版预览”通过真实浏览器点击 smoke。
|
||||
- [x] schema guard、后端 check / runner 单测、前端测试、typecheck、encoding、`git diff --check` 通过。
|
||||
- [x] 文档索引更新。
|
||||
- [x] 没有扩大真实 Agent、任意依赖、HMR、dev server、作品化发布等后续阶段权限。
|
||||
|
||||
P2 优先接:
|
||||
|
||||
- [ ] `web_project_runtime_job` 持久任务表。
|
||||
- [ ] lease / controller / worker 模式。
|
||||
- [ ] 手动取消、stale、expired 和 runner crash 恢复。
|
||||
- [ ] 构建日志分页与可重连 SSE。
|
||||
- [ ] 真实 Agent 接入,但继续产出同一结构化 patch。
|
||||
|
||||
真实 Agent 接入前不得扩大 P1 的执行权限;任意依赖安装、HMR、dev server 和作品化发布必须进入后续独立评审。
|
||||
@@ -110,6 +110,7 @@ P1 字段建议:
|
||||
- 拒绝符号链接语义。
|
||||
- 限制目录深度、单文件大小、snapshot 总大小。
|
||||
- P1 只允许文本源码和少量静态资源。
|
||||
- P1 可编辑范围限定为 `src/App.tsx`、`src/App.css`、`src/components/**`、`src/assets/**` 与 `public/**`;固定模板控制文件 `index.html`、`src/main.tsx`、`tsconfig.json`、`vite.config.ts` 不进入用户 snapshot。
|
||||
- `package.json` 不是事实源;新增依赖和 scripts 在 P1 拒绝或忽略。
|
||||
|
||||
## 后端存储
|
||||
@@ -120,6 +121,7 @@ P1 新增 SpacetimeDB 表:
|
||||
web_project
|
||||
web_project_snapshot
|
||||
web_project_preview_build
|
||||
web_project_service_identity
|
||||
```
|
||||
|
||||
`web_project`:
|
||||
@@ -162,6 +164,42 @@ web_project_preview_build
|
||||
- `finished_at`
|
||||
- `updated_at`
|
||||
|
||||
`web_project_service_identity`:
|
||||
|
||||
- `service_identity`
|
||||
- `created_at`
|
||||
- `created_by`
|
||||
- `note`
|
||||
|
||||
`web_project` procedure 入口必须先校验 SpacetimeDB `ctx.sender()` 命中 `web_project_service_identity`,再信任 api-server 从 `AuthenticatedAccessToken` 派生并代传的业务 `owner_user_id`。该表是运行环境服务授权表,不作为普通业务迁移数据跨环境导入导出。
|
||||
|
||||
### `web_project_service_identity` 授权流程
|
||||
|
||||
`web_project_service_identity` 是环境级 allowlist,不随 `migration.rs` 普通业务迁移导入导出。首个服务身份需要使用构建 `spacetime-module` wasm 时注入的 `GENARRATIVE_SPACETIME_WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET` 完成引导授权;密钥只通过本机或生产 Secret 管理系统注入,不写入 Git、文档、日志或 issue。
|
||||
|
||||
本地开发流程:
|
||||
|
||||
1. 默认使用 `npm run dev`:脚本会为本地 `spacetime-module` 构建注入一次随机 Web Project 服务身份引导密钥,复用 / 创建本地 api-server SpacetimeDB Web identity,并在启动 api-server 前通过 HTTP procedure 自动写入 `web_project_service_identity` allowlist。identity / token 缓存在 gitignored 的本机 SpacetimeDB data dir 下,避免每次启动产生新服务身份。
|
||||
2. 若使用 `--skip-publish`、单独启动 `npm run dev:api-server` 或排查特殊数据库,需要先确认当前发布的 wasm 已通过 `GENARRATIVE_SPACETIME_WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET` 注入引导密钥,再用显式 `--server` 调用授权 procedure。`<database>`、`<serviceIdentityHex>` 和 `<bootstrapSecret>` 均替换为本机实际值;不要把命令历史或输出中的 secret 贴到共享文档:
|
||||
|
||||
```bash
|
||||
spacetime call <database> authorize_web_project_service_identity '{"bootstrap_secret":"<bootstrapSecret>","service_identity_hex":"<serviceIdentityHex>","note":"local api-server web project"}' --server http://127.0.0.1:3101
|
||||
```
|
||||
|
||||
3. 首个身份写入后,后续新增 / 轮换服务身份应由已授权服务 identity 调用同一 procedure;此时 `bootstrap_secret` 可传空字符串或占位值,procedure 会改为校验 `ctx.sender()` 是否已经在 allowlist 内。
|
||||
4. 如需撤销旧身份,用已授权服务 identity 调用:
|
||||
|
||||
```bash
|
||||
spacetime call <database> revoke_web_project_service_identity '{"service_identity_hex":"<oldServiceIdentityHex>"}' --server http://127.0.0.1:3101
|
||||
```
|
||||
|
||||
生产 / 预发 ops 口径:
|
||||
|
||||
1. `Genarrative-Stdb-Module-Publish` 或等价发布流程必须在构建 `spacetime-module` wasm 时从 Jenkins Secret Text / 服务器 Secret Store 注入 `GENARRATIVE_SPACETIME_WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET`;生产文档和流水线日志只允许记录“已注入 / 长度满足要求”,不得输出明文。
|
||||
2. 发布后由 ops 在目标 SpacetimeDB server 上显式指定 `--server` 执行一次 `spacetime call <database> authorize_web_project_service_identity ...`,把生产 `api-server` 使用的 SpacetimeDB identity 加入 allowlist。该步骤尚未在本轮验证完成,不能视为 P1 已生产初始化。
|
||||
3. 首个生产身份授权完成后,应从运行环境移除或轮换 bootstrap secret;后续服务身份轮换只允许通过已授权身份执行 `authorize_web_project_service_identity` / `revoke_web_project_service_identity`,不再依赖 bootstrap secret 扩权。
|
||||
4. 授权失败时优先排查:目标 server / database 是否正确、CLI 当前 token 对应 identity 是否符合首个引导或已授权身份、bootstrap secret 是否注入到发布后的 wasm、`service_identity_hex` 是否为 64 位十六进制 SpacetimeDB identity。不得使用旧 `maincloud` 口径或 `spacetime --root-dir` 手工绕过。
|
||||
|
||||
落点:
|
||||
|
||||
- `server-rs/crates/spacetime-module/src/web_project.rs`
|
||||
@@ -262,7 +300,7 @@ P1 至少支持以下 mock 指令族:
|
||||
- `计数` / `按钮`:生成带计数按钮的 React 页面。
|
||||
- `卡片` / `列表`:生成卡片列表页面。
|
||||
- `蓝色` / `绿色` / `粉色`:调整 CSS 主题色。
|
||||
- `破坏构建`:生成一个 TypeScript 编译错误,用于验收失败保留上一版预览。
|
||||
- `破坏构建`:生成一个 TSX 语法构建错误,用于验收失败保留上一版预览;P1 runner 只执行 `vite build`,不把 TypeScript 类型检查作为失败路径依据。
|
||||
- 其它输入:只更新标题和说明文案,避免 mock 分支过多。
|
||||
|
||||
mock Agent 输出仍必须经过统一 `validate_web_project_patch(...)`,不得直接写表。
|
||||
@@ -300,17 +338,21 @@ P1 runner 步骤:
|
||||
9. 失败时返回错误摘要和日志片段,日志需脱敏并避免完整宿主路径。
|
||||
10. 清理临时目录。
|
||||
|
||||
P1 允许的构建命令只能来自平台常量,使用 argv 方式执行,不经 shell、不拼接字符串、不执行用户传入命令:
|
||||
P1 允许的构建命令只能来自平台常量,不拼接用户字符串、不执行用户传入命令;Windows 下 npm 通过 `cmd.exe /d /s /c npm.cmd ...` 启动以兼容 `.cmd` shim,其余平台直接执行 `npm`:
|
||||
|
||||
```text
|
||||
npm ci --ignore-scripts --offline --no-audit --fund=false
|
||||
npm exec --offline -- vite build
|
||||
```
|
||||
|
||||
固定 Vite 模板必须设置 `base: './'`,确保独立 preview gateway 的 `/p/<previewToken>/` 子路径能够加载 `assets/**`。
|
||||
|
||||
如果离线缓存不足,P1 只能改为访问平台受控 npm registry mirror,并由 runner 写入受控 `.npmrc`;用户 snapshot 中的 `.npmrc`、registry、proxy、`package-lock.json` 篡改和新增依赖必须拒绝或重写。禁止为了开发速度复用当前仓库的 `node_modules`、源码目录或本机全局 npm cache 作为 runner 工作区输入。
|
||||
|
||||
P1 默认网络策略为 deny all。仅当使用平台受控 registry mirror 或资产只读签名域时开放精确白名单;必须阻断 RFC1918 内网、云 metadata 地址、api-server 管理端口、SpacetimeDB、生产数据库、Docker daemon,并覆盖 HTTP redirect 和 DNS rebinding 到内网的情况。
|
||||
|
||||
preview build 需要双层并发保护:全局最多同时运行 `GENARRATIVE_WEB_PROJECT_PREVIEW_BUILD_MAX_CONCURRENT_TASKS` 个构建任务,默认值为 `2`;同一个 project 只能同时占用一个 runner slot。全局 slot 不足时 api-server 返回 `429 TOO_MANY_REQUESTS`,避免多个预览任务一起挤爆本地构建资源。
|
||||
|
||||
## Preview Gateway
|
||||
|
||||
P1 开发态 preview 可以使用独立端口:
|
||||
|
||||
61
docs/technical/【技术方案】WebProjectRunnerP1实现补充-2026-06-15.md
Normal file
61
docs/technical/【技术方案】WebProjectRunnerP1实现补充-2026-06-15.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# WebProject Runner P1 实现补充
|
||||
|
||||
更新时间:`2026-06-16`
|
||||
|
||||
本文补充 `server-rs/crates/web-project-runner` 的 P1 实现口径,主约束仍以 [`【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md`](./【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md)、[`【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md`](./【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md) 和 [`【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md`](./【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md) 为准。
|
||||
|
||||
## 当前实现边界
|
||||
|
||||
- `web-project-runner` 是独立 workspace crate,提供 `run_web_project_build(input) -> output` 库 API。
|
||||
- 输入只接受 `jobId`、`projectId`、`snapshotId`、`files` 和服务侧配置传入的 `artifactRoot`。
|
||||
- 输入不接受用户命令、构建参数、工作区路径或 artifact 输出路径。
|
||||
- 每个 job 创建任务级临时目录,先写入 runner 受控 React / Vite / TypeScript 模板,再写入 snapshot 文件。
|
||||
- snapshot 路径必须是相对路径,拒绝绝对路径、`..`、`.env`、`.npmrc`、`.git`、`.ssh`、`package-lock.json` 和超限文件。
|
||||
- snapshot 只能覆盖 P1 可编辑文件:`src/App.tsx`、`src/App.css`、`src/components/**`、`src/assets/**` 与 `public/**` 下的受控静态文本资源;`index.html`、`src/main.tsx`、`tsconfig.json`、`vite.config.ts` 等固定模板控制文件不得由 snapshot 覆盖。
|
||||
- snapshot 中的 `package.json` 不作为依赖事实源;P1 拒绝其中的 scripts、dependencies、devDependencies、registry、workspace 等供应链字段。
|
||||
- Preview token 使用 `wpt_<issuedMicros>_<randomSecret>` 不透明票据,gateway 只按完整 token 回查 DB 中保存的 `preview_token_id`,并校验 24 小时 TTL;URL token 不承载可信 owner 或 job 信息。
|
||||
- Preview gateway 的 CSP 保持 `connect-src 'none'`、`worker-src 'none'`、`object-src 'none'` 和 `no-store`;`frame-ancestors` 由 `GENARRATIVE_WEB_PROJECT_PREVIEW_FRAME_ANCESTORS` 配置允许平台编辑器宿主嵌入,开发默认允许 `http://127.0.0.1:3000 http://localhost:3000`,生产必须显式配置主站 origin。
|
||||
- 构建命令固定为 argv 常量:
|
||||
- `npm ci --ignore-scripts --offline --no-audit --fund=false`
|
||||
- `npm exec --offline -- vite build`
|
||||
- Windows 下 runner 仍以固定命令语义执行 `npm`,实际进程层通过 `cmd.exe /d /s /c npm.cmd ...` 启动,避免直接 spawn `.cmd` 失败;其它平台直接执行 `npm`。
|
||||
- 子进程使用环境变量白名单,不继承平台密钥和 `.env`;Windows 仅额外保留 Node / npm 启动所需的 `SystemRoot`、`ComSpec`、`TEMP` 等系统变量。
|
||||
- 构建成功后只复制 `dist/` 到 runner 计算出的 immutable artifact 目录;失败返回结构化 `errorSummary` 和日志片段。
|
||||
- runner 会清理任务级临时目录;preview gateway 不应服务 runner 临时工作区。
|
||||
- 固定 Vite 模板设置 `base: './'`,确保 preview gateway 的 `/p/<previewToken>/` 子路径可以加载 `assets/**`。
|
||||
|
||||
## 依赖缓存口径
|
||||
|
||||
runner 内置 `templates/react-vite-ts-static/package.json` 和 `package-lock.json`。模板 lockfile 必须在模板目录内用 npm 生成,不能手工从仓库根 lockfile 裁剪依赖闭包;当前 `vite@6.4.1` 对应的嵌套 `esbuild` 锁定为 `0.25.12`。
|
||||
|
||||
更新模板依赖或 lockfile 时使用:
|
||||
|
||||
```powershell
|
||||
Push-Location server-rs/crates/web-project-runner/templates/react-vite-ts-static
|
||||
npm install --package-lock-only --ignore-scripts --no-audit --fund=false --registry=https://registry.npmjs.org
|
||||
Pop-Location
|
||||
```
|
||||
|
||||
本地运行态 smoke 前如果 npm 离线缓存未预热,可在模板目录或临时目录执行一次非 offline 的 `npm ci --ignore-scripts --no-audit --fund=false` 预热缓存;runner 本身仍必须使用 `--offline`。
|
||||
|
||||
P1 不为了本地构建成功放开网络、scripts、用户 registry 或用户 lockfile。如果机器离线 npm cache 未预热,`npm ci --offline` 可以失败,runner 应返回 `failed` 和可读错误摘要。后续要提升成功率,应通过平台受控模板缓存预热或受控 npm mirror 解决,不允许从请求体传入 registry、proxy 或替换构建命令。
|
||||
|
||||
## 已覆盖测试
|
||||
|
||||
本轮 P1 主线已确认 `cargo test -p web-project-runner --manifest-path server-rs/Cargo.toml` 通过,覆盖:
|
||||
|
||||
- 路径逃逸和敏感文件拒绝。
|
||||
- 普通源码相对路径通过。
|
||||
- `package.json` 新增依赖、脚本和 package manager 字段被拒绝。
|
||||
- 固定模板控制文件 snapshot 在执行任何命令前被拒绝。
|
||||
- Windows npm 启动路径和系统环境白名单。
|
||||
- 构建失败返回结构化错误,不生成 artifact。
|
||||
- 成功路径写入模板、固定命令 argv 并复制 `dist/` 到 immutable artifact 目录。
|
||||
- 危险 snapshot 在执行任何命令前被拒绝。
|
||||
|
||||
## 后续接入提醒
|
||||
|
||||
- api-server 只能调用 runner API 或触发独立 runner 执行面,不应在 handler 中自行执行 `npm` / `vite`。
|
||||
- api-server 传入的 `artifactRoot` 必须来自服务配置,不得来自用户请求。
|
||||
- runner 当前是 P1 一次性构建形态;持久队列、lease、取消、资源 cgroup、构建期网络 deny-all 和子进程资源隔离仍属于后续切片或部署层门禁。
|
||||
- 2026-06-16 已完成本地运行态 API / preview gateway smoke:`/healthz` 200,`做一个蓝色计数按钮页面` 构建成功并产出 preview URL,`破坏构建` 返回 failed 且上一版 preview 仍可访问。生产 `web_project_service_identity` 授权初始化、真实浏览器点击 smoke 和部署层网络 / Docker socket 门禁仍按 P1 计划保留。
|
||||
@@ -41,6 +41,8 @@ MVP 不支持:
|
||||
- [ ] AI patch plan 新增或修改一个 React 组件后,api-server 校验通过并生成新 snapshot。
|
||||
- [ ] patch 校验失败时不生成 snapshot,不创建 build job,并在聊天中展示可读校验错误。
|
||||
- [ ] 前端 debounce 后创建 preview build job。
|
||||
- [ ] 预览 build 全局并发上限生效,超限时返回 `429 TOO_MANY_REQUESTS`。
|
||||
- [ ] 同一个 project 不会同时跑多个 runner 构建。
|
||||
- [ ] runner 构建成功并产出 immutable artifact。
|
||||
- [ ] SSE 返回 `queued -> running -> succeeded`。
|
||||
- [ ] iframe 切换到新 preview URL。
|
||||
@@ -74,6 +76,7 @@ MVP 不支持:
|
||||
- [ ] 大 Data URL 被拒绝或转资产流程。
|
||||
- [ ] 二进制膨胀被拒绝。
|
||||
- [ ] rename 后目标路径仍需重新校验。
|
||||
- [ ] rename 目标路径已存在时返回 `400`,不静默覆盖。
|
||||
|
||||
## 构建与状态机
|
||||
|
||||
@@ -164,6 +167,8 @@ MVP 不支持:
|
||||
- [ ] iframe sandbox 不允许 camera / mic。
|
||||
- [ ] CSP 禁止未白名单 `connect-src`。
|
||||
- [ ] Service Worker 被禁用。
|
||||
- [ ] iframe sandbox 只保留 `allow-scripts`。
|
||||
- [ ] preview gateway 的 JS / CSS / 资源响应带 `Access-Control-Allow-Origin: *`,可在 sandbox opaque origin 下正常加载。
|
||||
|
||||
## 日志与错误
|
||||
|
||||
@@ -220,6 +225,7 @@ git diff --check
|
||||
提交一次 AI patch
|
||||
等待静态构建成功
|
||||
确认 iframe 展示新预览
|
||||
点击预览中的计数按钮,文案从 `已点击 0 次` 变为 `已点击 1 次`
|
||||
提交一次故意破坏构建的 patch
|
||||
确认错误出现且上一版预览仍保留
|
||||
刷新页面确认项目、日志和 active preview 可恢复
|
||||
|
||||
@@ -429,6 +429,26 @@ npm run check:server-rs-ddd
|
||||
- Rust 结构体:`DatabaseMigrationOperator`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/migration.rs`
|
||||
|
||||
### `web_project_service_identity`
|
||||
|
||||
- Rust 结构体:`WebProjectServiceIdentity`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/web_project.rs`
|
||||
- 说明:Web Project BFF / Preview Gateway 的窄权限服务身份 allowlist。`web_project` 相关 procedure 先校验 SpacetimeDB `ctx.sender()` 命中该表,再信任 api-server 从 `AuthenticatedAccessToken` 派生并代传的业务 `owner_user_id`。该表保存运行环境级授权,不作为普通业务迁移数据跨环境导入导出;首个服务身份通过 `GENARRATIVE_SPACETIME_WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET` 引导授权,后续由已授权服务身份增删。
|
||||
- 本地授权:默认由 `npm run dev` 自动完成。dev 脚本会在本地发布当前 `spacetime-module` 时注入随机 `GENARRATIVE_SPACETIME_WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET`,复用 / 创建本地 api-server SpacetimeDB Web identity,并在 api-server 启动前把该 identity 写入 allowlist;identity / token 只缓存在 gitignored 的本机 SpacetimeDB data dir。若跳过发布或排查特殊数据库,可使用显式目标 server 手动调用 `authorize_web_project_service_identity`,只传占位密钥示例,不在文档或日志写明文:
|
||||
|
||||
```bash
|
||||
spacetime call <database> authorize_web_project_service_identity '{"bootstrap_secret":"<bootstrapSecret>","service_identity_hex":"<serviceIdentityHex>","note":"local api-server web project"}' --server http://127.0.0.1:3101
|
||||
```
|
||||
|
||||
- 生产 / 预发授权:ops 必须通过 Jenkins Secret Text / 服务器 Secret Store 在构建 wasm 时注入 `GENARRATIVE_SPACETIME_WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET`,发布后对生产 `<database>` 和 `<server>` 执行同一 `spacetime call`,把生产 `api-server` 使用的 SpacetimeDB identity 加入 allowlist。该生产初始化步骤不随代码发布自动完成;未实际执行前,不得把 `/editor/agent` P1 视为生产可用。
|
||||
- 轮换 / 撤销:首个身份存在后,新增身份由已授权服务 identity 调用 `authorize_web_project_service_identity`,`bootstrap_secret` 不再赋权;撤销旧身份使用:
|
||||
|
||||
```bash
|
||||
spacetime call <database> revoke_web_project_service_identity '{"service_identity_hex":"<oldServiceIdentityHex>"}' --server <server-url>
|
||||
```
|
||||
|
||||
- 排障:授权失败优先核对 `--server` / database、CLI token 对应 `ctx.sender()`、wasm 是否带 bootstrap secret、`service_identity_hex` 是否为 64 位十六进制 identity;禁止回退到旧 `maincloud` 命令或人工 `spacetime --root-dir` 口径。
|
||||
|
||||
### `editor_project`
|
||||
|
||||
- Rust 结构体:`EditorProject`
|
||||
|
||||
@@ -30,8 +30,9 @@ npm run dev
|
||||
- Rust `api-server`。
|
||||
- 主站 Vite。
|
||||
- 后台 Vite。
|
||||
- Web Project preview gateway,作为 `api-server` 内嵌的独立预览监听入口。
|
||||
|
||||
`npm run dev` 和单模块 `npm run dev:web`、`npm run dev:api-server`、`npm run dev:spacetime`、`npm run dev:admin-web` 启动后都会更新根目录 `.app/dev-stack.json`。该文件记录本次命令、数据库、更新时间,以及 `spacetime`、`api-server`、`web`、`admin-web` 的 `pid`、监听 host / port、可访问 URL、启动状态和当前命令。`.app/` 是本地运行态目录,不提交 Git;端口漂移、服务重启或子进程退出后以该文件里的实际状态为准。
|
||||
`npm run dev` 和单模块 `npm run dev:web`、`npm run dev:api-server`、`npm run dev:spacetime`、`npm run dev:admin-web` 启动后都会更新根目录 `.app/dev-stack.json`。该文件记录本次命令、数据库、更新时间,以及 `spacetime`、`api-server`、`web`、`admin-web`、`web-project-preview` 的 `pid`、监听 host / port、可访问 URL、启动状态和当前命令。`.app/` 是本地运行态目录,不提交 Git;端口漂移、服务重启或子进程退出后以该文件里的实际状态为准。
|
||||
|
||||
单独启动主站前端:
|
||||
|
||||
@@ -45,7 +46,7 @@ npm run dev:web
|
||||
npm run dev:api-server
|
||||
```
|
||||
|
||||
Linux 本机多用户并发开发时,`npm run dev` 和 `npm run dev:*` 单模块命令会先在系统级端口段注册表里给当前用户分配一个端口段,再把该段映射为 `web = start`、`api = start + 1`、`spacetime = start + 2`、`admin-web = start + 3`。默认注册表目录是 `/var/tmp/genarrative-dev-port-ranges/`,其中 `registry.json` 记录各用户的活跃段,`registry.lock` 负责串行化分配;可以用 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。系统自动分配时从 `10000-10099` 开始,每次占用 100 个端口块,后续块按 `10100-10199`、`10200-10299` 递增;`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range` 只在 Linux 上生效,Windows 仍按原来的 3000 / 8082 / 3101 / 3102 端口探测与漂移逻辑运行,不读这个系统级注册表。
|
||||
Linux 本机多用户并发开发时,`npm run dev` 和 `npm run dev:*` 单模块命令会先在系统级端口段注册表里给当前用户分配一个端口段,再把该段映射为 `web = start`、`api = start + 1`、`spacetime = start + 2`、`admin-web = start + 3`、`web-project-preview = start + 4`。默认注册表目录是 `/var/tmp/genarrative-dev-port-ranges/`,其中 `registry.json` 记录各用户的活跃段,`registry.lock` 负责串行化分配;可以用 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。系统自动分配时从 `10000-10099` 开始,每次占用 100 个端口块,后续块按 `10100-10199`、`10200-10299` 递增;`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range` 只在 Linux 上生效,Windows 仍按原来的 3000 / 8082 / 3101 / 3102 / 3104 端口探测与漂移逻辑运行,不读这个系统级注册表。`api-server` 启动时会同步注入 `GENARRATIVE_WEB_PROJECT_PREVIEW_HOST`、`GENARRATIVE_WEB_PROJECT_PREVIEW_PORT`、`GENARRATIVE_WEB_PROJECT_PREVIEW_PUBLIC_BASE_URL` 和 `GENARRATIVE_WEB_PROJECT_PREVIEW_FRAME_ANCESTORS`,确保前端 iframe 和 preview gateway 使用同一组实际端口。
|
||||
|
||||
后端日志默认写入 `logs/api-server/`。后端 API smoke 使用 `npm run dev:api-server` 并检查 `/healthz`;需要确认实例可接生产流量时检查 `/readyz`。不要使用旧 `api-server:maincloud` 或任何 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 口径。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user