From b4746c24b59c2bc44e684d0abdadd7afa7059ffc Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 14 Jun 2026 19:34:44 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E8=A1=A5=E5=85=85AIWeb=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93=E8=90=BD=E5=9C=B0=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 明确 /editor/agent 智能体作为工程改动编排器,通过结构化 patch、校验、snapshot、构建和预览反馈闭环落地。 补充左侧聊天、中间预览、右侧 IDE 的页面布局约束,右侧展开文件内容后隐藏聊天栏。 同步验收清单,明确 MVP 不做 diff 视图并覆盖 agent turn 和布局验收。 --- ...】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md | 96 ++++++++++++++++++- ...例】AIWeb工程静态预览MVP验收清单-2026-06-13.md | 27 +++++- 2 files changed, 118 insertions(+), 5 deletions(-) diff --git a/docs/technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md b/docs/technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md index 236780ed..694bdb77 100644 --- a/docs/technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md +++ b/docs/technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md @@ -29,6 +29,7 @@ MVP 明确不做: - HMR / 长驻 dev server。 - 任意 npm 依赖安装。 - AI 自定义 build shell script。 +- diff 视图。 - 与主站同源的预览 iframe。 - 将 AI 生成工程写入当前 Genarrative 仓库源码目录。 @@ -38,8 +39,9 @@ MVP 明确不做: 入口路由固定为 `/editor/agent`。这一层只负责前端体验: -- 文件树、代码编辑器、AI 指令输入。 -- 当前项目版本、diff、构建日志和预览 iframe。 +- 左侧聊天、中间预览、右侧 IDE 的水平三分布局。 +- AI 指令输入、构建日志、当前项目版本和预览 iframe。 +- 右侧 IDE 默认只显示文件树;展开后显示当前文件内容,并隐藏左侧聊天栏。 - 保存用户编辑和 AI patch。 - 订阅构建 job SSE 状态。 - 在构建成功后切换 iframe 的 `previewUrl`。 @@ -49,6 +51,74 @@ MVP 明确不做: 如果后续接入现有创作链路,入口仍应走 `play_flow` 主干和 `shared-contracts` DTO,不在前端壳或 `app.rs` 中新建平行业务流程。 +### 智能体编排器 + +`/editor/agent` 的智能体不是普通聊天页,也不是直接生成整包代码的黑盒。它是“工程改动编排器”,负责把用户意图转成可审计、可校验、可回滚的 Web 工程 patch。 + +智能体职责: + +- 理解用户自然语言目标,并结合当前 snapshot、文件树、选中文件和最近构建结果形成任务上下文。 +- 输出结构化 patch plan,而不是直接写真实目录或执行 shell。 +- 将 patch plan 提交给 api-server 校验,校验通过后生成新 snapshot。 +- 创建 preview build job,并订阅 job SSE。 +- 读取构建日志、错误摘要和 preview 状态,失败时继续生成修复 patch,成功时推动预览切换。 +- 保留每轮 agent turn 的用户输入、上下文摘要、patch 摘要、校验结果、jobId 和最终状态,便于刷新恢复和审计。 + +智能体最小闭环: + +```text +用户在左侧聊天输入需求 + -> 前端提交 agent turn + -> api-server 读取当前 snapshot 和允许暴露的上下文 + -> LLM 返回结构化 patch plan + -> api-server 校验 patch plan + -> 校验通过后生成新 snapshot + -> 创建 preview build job + -> runner 构建 + -> SSE 回传 queued / running / log / succeeded / failed + -> 成功时中间预览 iframe reload + -> 失败时智能体基于错误摘要进入下一轮修复 +``` + +智能体输出只允许以下 patch 操作: + +- `create_file` +- `update_file` +- `delete_file` +- `rename_file` +- `package_manifest_request` + +`package_manifest_request` 只是依赖请求,不是事实源。MVP 中 api-server 必须拒绝或忽略任意新增依赖、生命周期脚本、私有 registry、`git:` / `file:` / `http:` 依赖和 AI 自定义构建命令。 + +智能体不得: + +- 直接写入当前 Genarrative 仓库源码目录。 +- 直接访问 runner 临时工作区。 +- 直接执行 shell、npm script 或任意构建命令。 +- 直接拿平台 access token、用户 cookie、SpacetimeDB 连接、OSS 写权限或 LLM provider 密钥。 +- 绕过 api-server 的路径、依赖、大小、敏感文件和 snapshot 校验。 + +### 页面布局 + +`/editor/agent` MVP 使用水平三分布局,不做 diff 视图: + +```text +┌───────────────┬──────────────────────────────┬────────────────┐ +│ 左侧聊天 │ 中间预览 │ 右侧 IDE │ +│ Agent 对话 │ sandbox iframe + 状态日志 │ 默认文件树 │ +└───────────────┴──────────────────────────────┴────────────────┘ +``` + +布局规则: + +- 左侧聊天承载用户输入、智能体回复、构建状态摘要和失败修复建议。 +- 中间预览始终是主工作区,展示当前 active preview iframe;构建失败时保留上一版 succeeded preview,并在聊天或日志区域展示失败摘要。 +- 右侧 IDE 默认只显示文件树,突出当前工程结构,不默认展示代码内容。 +- 用户展开右侧 IDE 后,右侧显示文件树和当前文件内容,左侧聊天栏隐藏,中间预览与右侧 IDE 形成双栏工作模式。 +- 用户收起右侧文件内容后,恢复左侧聊天 + 中间预览 + 右侧文件树三栏。 +- 文件内容查看用于理解和轻量编辑当前文件,不提供 diff 视图;版本变化通过 agent turn 摘要、snapshot 时间线和构建状态表达。 +- 移动端优先保持预览和聊天可用,右侧 IDE 通过抽屉或分段入口进入;展开文件内容时同样隐藏聊天。 + ### api-server 控制面 控制面只管理权限、快照、任务和状态,不执行 AI 工程代码: @@ -128,6 +198,7 @@ MVP 只服务静态构建产物。HMR、WebSocket 代理和长驻 dev server 后 POST /api/creation/web-project/projects GET /api/creation/web-project/projects/{projectId}/snapshot PATCH /api/creation/web-project/projects/{projectId}/files +POST /api/creation/web-project/projects/{projectId}/agent-turns POST /api/runtime/web-project/projects/{projectId}/preview-builds GET /api/runtime/web-project/preview-builds/{jobId} GET /api/runtime/web-project/preview-builds/{jobId}/events @@ -146,6 +217,27 @@ GET /api/runtime/web-project/preview-builds/{jobId}/events 前端刷新恢复时,先读取项目 active snapshot 和 active preview,再按未终态 job 继续订阅 SSE。 +`agent-turns` 的请求建议包含: + +- `projectId` +- `baseSnapshotId` +- `message` +- `selectedFilePath` +- `visibleFilePaths` +- `recentBuildJobId` + +`agent-turns` 的响应建议包含: + +- `turnId` +- `status` +- `assistantMessage` +- `patchSummary` +- `validationErrors[]` +- `snapshotId` +- `buildJobId` + +当 patch 校验失败时,不生成 snapshot,不创建 build job,只返回可展示的校验错误和智能体修正建议。 + ## 虚拟文件系统 平台内的“文件系统”是虚拟工作区,不是真实目录长期挂载。 diff --git a/docs/technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md b/docs/technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md index 1d97a0ff..45445ff2 100644 --- a/docs/technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md +++ b/docs/technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md @@ -14,6 +14,7 @@ MVP 必须只支持: - 独立 runner 静态构建。 - 独立 preview origin iframe 预览。 - 失败保留上一版可用预览。 +- 左侧聊天、中间预览、右侧 IDE 的 `/editor/agent` 三分布局;右侧 IDE 展开文件内容后隐藏左侧聊天。 MVP 不支持: @@ -23,19 +24,39 @@ MVP 不支持: - 任意端口代理。 - 任意 npm 安装。 - AI 自定义 package scripts。 +- diff 视图。 - Service Worker。 - 主站同源预览。 ## Happy Path -- [ ] 用户打开 `/editor/agent` 能看到文件树、编辑器、AI 输入区、构建日志区和预览 iframe。 +- [ ] 用户打开 `/editor/agent` 能看到左侧聊天、中间预览和右侧 IDE 文件树。 +- [ ] 右侧 IDE 默认只显示文件树,不默认显示文件内容。 +- [ ] 用户展开右侧 IDE 后能看到当前文件内容,左侧聊天栏隐藏。 +- [ ] 用户收起右侧文件内容后恢复左侧聊天、中间预览和右侧文件树三栏。 +- [ ] 页面不展示 diff 视图。 - [ ] 创建新 Web project 后得到固定模板文件树。 -- [ ] AI patch 新增或修改一个 React 组件后,api-server 生成新 snapshot。 +- [ ] 用户在左侧聊天提交需求后创建 agent turn。 +- [ ] 智能体结合当前 snapshot、选中文件和最近构建状态生成结构化 patch plan。 +- [ ] AI patch plan 新增或修改一个 React 组件后,api-server 校验通过并生成新 snapshot。 +- [ ] patch 校验失败时不生成 snapshot,不创建 build job,并在聊天中展示可读校验错误。 - [ ] 前端 debounce 后创建 preview build job。 - [ ] runner 构建成功并产出 immutable artifact。 - [ ] SSE 返回 `queued -> running -> succeeded`。 - [ ] iframe 切换到新 preview URL。 -- [ ] 刷新 `/editor/agent` 后能恢复当前 project、active snapshot、active preview 和未完成 job 状态。 +- [ ] 刷新 `/editor/agent` 后能恢复当前 project、active snapshot、active preview、agent turn 历史和未完成 job 状态。 + +## 智能体编排 + +- [ ] 智能体只通过 `agent-turns` 或等价受控接口提交用户需求,不直接写真实目录。 +- [ ] 智能体输出只包含 `create_file/update_file/delete_file/rename_file/package_manifest_request` 这类结构化 patch 操作。 +- [ ] 智能体不输出或执行 shell 命令。 +- [ ] 智能体不直接触发任意 npm script。 +- [ ] 智能体不直接访问 runner 临时工作区。 +- [ ] 智能体不接收平台 access token、用户 cookie、SpacetimeDB 连接、OSS 写权限或 LLM provider 密钥。 +- [ ] 每轮 agent turn 记录用户输入、上下文摘要、patch 摘要、校验结果、snapshotId、jobId 和最终状态。 +- [ ] 构建失败后,智能体基于受脱敏的错误摘要生成下一轮修复 patch。 +- [ ] 构建成功后,智能体不会覆盖非当前 active snapshot 的 preview。 ## Patch 与路径校验 From 0bc7db5bf6ac91698ca402b7da40029ad3c1a418 Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 15 Jun 2026 14:11:26 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E7=B4=A0=E6=9D=90=E5=AF=BC=E5=87=BA=E6=96=B9?= =?UTF-8?q?=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增画布素材 ZIP 导出技术方案 明确下载图标入口放在右上角标题栏 在 docs 总览补充图片画布素材导出文档入口 --- docs/README.md | 2 + ...端架构】图片画布素材导出方案-2026-06-15.md | 158 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 docs/technical/【前端架构】图片画布素材导出方案-2026-06-15.md diff --git a/docs/README.md b/docs/README.md index a2501862..78a839aa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,6 +23,8 @@ `/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)。 +`/editor/canvas` 图片画布编辑器的画布素材 ZIP 导出能力,入口放在右上角标题栏下载图标内,第一版采用前端 JSZip 打包画布中有效图层引用的上传图、生成图和修改结果,方案见 [【前端架构】图片画布素材导出方案-2026-06-15.md](./technical/【前端架构】图片画布素材导出方案-2026-06-15.md)。 + 本地通过 SSH alias 管理多台服务器、查看硬件 / systemd / HTTP 健康状态并执行受控服务启停的 egui 桌面工具见 [【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md](./technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md)。 生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。 diff --git a/docs/technical/【前端架构】图片画布素材导出方案-2026-06-15.md b/docs/technical/【前端架构】图片画布素材导出方案-2026-06-15.md new file mode 100644 index 00000000..f3503144 --- /dev/null +++ b/docs/technical/【前端架构】图片画布素材导出方案-2026-06-15.md @@ -0,0 +1,158 @@ +# 图片画布素材导出方案 + +日期:2026-06-15 + +## 背景 + +`/editor/canvas` 已承载项目画布、账号级素材库、生成图片、图层、分组、隐藏、锁定、翻转、缩放和右键菜单等编辑能力。用户需要一次性下载当前画布中使用到的全部素材,用于本地归档、外部编辑或跨工具交接。 + +素材导出应作为画布级能力,而不是单个图层的“导出为”。单图导出仍保留在目标右键菜单;全部素材导出放在画布标题栏,入口稳定、可发现且不打断画布操作。 + +## 入口设计 + +- 在画布右上角标题栏内增加下载图标按钮。 +- 图标使用 `lucide-react` 的 `Download`。 +- 按钮 `aria-label` 使用 `下载画布素材`。 +- 入口与返回项目页、项目名称同属标题栏,但视觉上靠右,不进入左下角画布工具组,也不进入底部 AI 工具栏。 +- 标题栏空间不足时,下载图标保持固定尺寸,项目名称使用省略号收缩。 + +## 第一版范围 + +第一版实现“导出画布素材 ZIP”: + +- 导出当前画布中有效图层引用的图片。 +- 包含上传图、生成图、修改生成结果。 +- 默认包含隐藏图层。 +- 跳过已被素材库删除且已判定无效的上传图层。 +- 锁定、分组、翻转等状态不影响图片文件导出,但写入元数据。 +- 空画布时按钮置灰,或点击后显示轻提示。 + +暂不实现: + +- 画布整体截图 PNG。 +- PSD / Figma / 工程包导出。 +- 后端异步打包任务。 +- 导入导出包恢复画布。 + +## ZIP 结构 + +导出文件名: + +```text +项目名-画布素材-YYYYMMDD.zip +``` + +包内结构: + +```text +项目名-画布素材/ +├─ images/ +│ ├─ 001-拼图素材.png +│ ├─ 002-生成图片.png +│ └─ 003-修改结果.png +├─ metadata.json +└─ manifest.txt +``` + +## 元数据结构 + +`metadata.json` 记录项目、导出时间、图层和图片文件映射: + +```json +{ + "projectId": "editor-project-default", + "projectTitle": "默认项目", + "exportedAt": "2026-06-15T00:00:00.000Z", + "layers": [ + { + "layerId": "layer-generated-1", + "title": "生成图片 1", + "file": "images/002-生成图片.png", + "width": 1024, + "height": 1024, + "canvas": { + "x": 120, + "y": 80, + "width": 420, + "height": 420, + "zIndex": 12, + "hidden": false, + "locked": false, + "flipX": false, + "flipY": false, + "groupId": null + }, + "sourceType": "generated", + "prompt": "一张明亮的拼图主视觉", + "actualPrompt": "一张明亮的拼图主视觉", + "model": "gpt-image-2", + "provider": "VectorEngine", + "taskId": "editor-real-task-1" + } + ] +} +``` + +`manifest.txt` 用于人工快速查看: + +```text +项目:默认项目 +导出时间:2026-06-15T00:00:00.000Z +素材数量:3 +图层数量:5 +``` + +## 去重规则 + +同一张图片被多个图层引用时,只导出一份图片,所有图层在 `metadata.json` 中指向同一个 `file`。 + +图片去重 key 优先级: + +```text +assetObjectId > objectKey > sourceAssetId > src +``` + +文件命名按图层 zIndex 从低到高排序,遇到重复名称追加序号。 + +## 前端实现 + +第一版优先使用客户端 ZIP: + +1. 从当前 `layers` 取目标图层。 +2. 过滤无效图层,保留隐藏图层。 +3. 按去重 key 合并图片源。 +4. 对每个图片源读取 Blob: + - `data:image/...` 直接转换为 Blob。 + - 同源或可访问 URL 使用 `fetch` 拉取 Blob。 + - 拉取失败时记录失败项,不中断整个导出。 +5. 使用 `JSZip` 写入 `images/`、`metadata.json` 和 `manifest.txt`。 +6. `zip.generateAsync({ type: 'blob' })` 生成文件。 +7. 用临时 `` 触发下载。 + +若当前仓库未安装 `jszip`,新增依赖时应只引入 `jszip`,不引入额外下载库。 + +## 错误处理 + +- 空画布:按钮置灰或显示短提示。 +- 部分图片下载失败:ZIP 仍生成,`metadata.json` 记录失败图片,UI 显示“部分素材未能导出”。 +- 全部图片失败:不下载 ZIP,显示失败提示。 +- 浏览器不支持 Blob 下载:显示失败提示。 + +## 测试计划 + +- 标题栏右上角显示 `下载画布素材` 图标入口。 +- 空画布时入口不可执行或提示为空。 +- 上传图和生成图都写入 ZIP。 +- 多图层引用同一素材时,图片文件只写一份。 +- 隐藏图层默认写入 `metadata.json`。 +- 图层的锁定、翻转、分组状态写入元数据。 +- `data:image` 图片不经过网络请求即可导出。 +- URL 图片 fetch 失败时不中断其他素材导出。 + +## 后续扩展 + +- 导出当前选中素材。 +- 导出画布快照 PNG。 +- 导出可恢复工程包。 +- 导入工程包恢复画布。 +- 后端异步打包大项目,前端只轮询下载结果。 From a3fed35cbdc29c3532c13f7dc075026f9346887d Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 15 Jun 2026 15:56:30 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E7=B4=A0=E6=9D=90=E4=B8=8E=E4=B8=8A=E4=B8=8B?= =?UTF-8?q?=E6=96=87=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加素材拖拽上传遮罩、素材移动和默认文件夹归一 补齐画布右键菜单、复制剪切粘贴、层级和翻转操作 补齐生成图入库、项目返回入口和项目名称标题 扩展画布测试覆盖素材、右键菜单、生成图和布局交互 --- .../ImageCanvasEditorPrimitives.tsx | 16 + .../ImageCanvasEditorView.test.tsx | 877 +++++++++- .../image-editor/ImageCanvasEditorView.tsx | 1449 ++++++++++++++--- src/index.css | 111 ++ 4 files changed, 2182 insertions(+), 271 deletions(-) diff --git a/src/components/image-editor/ImageCanvasEditorPrimitives.tsx b/src/components/image-editor/ImageCanvasEditorPrimitives.tsx index 86a7edb6..8b82d2b5 100644 --- a/src/components/image-editor/ImageCanvasEditorPrimitives.tsx +++ b/src/components/image-editor/ImageCanvasEditorPrimitives.tsx @@ -1,6 +1,7 @@ import type { ComponentType, DragEventHandler, + MouseEventHandler, PointerEventHandler, ReactNode, } from 'react'; @@ -68,9 +69,14 @@ export type SidebarMediaItemProps = { primaryClassName?: string; actions?: ReactNode; titleNode?: ReactNode; + draggable?: boolean; + onDragStart?: DragEventHandler; + onDragEnd?: DragEventHandler; onDragOver?: DragEventHandler; onDrop?: DragEventHandler; + onPointerDown?: PointerEventHandler; onPointerEnter?: PointerEventHandler; + onContextMenu?: MouseEventHandler; }; export function SidebarMediaItem({ @@ -87,16 +93,26 @@ export function SidebarMediaItem({ primaryClassName, actions, titleNode, + draggable, + onDragStart, + onDragEnd, onDragOver, onDrop, + onPointerDown, onPointerEnter, + onContextMenu, }: SidebarMediaItemProps) { return (
) : (
@@ -2625,16 +3493,24 @@ export function ImageCanvasEditorView() { selectSingleLayer(layer.id)} rowClassName="image-canvas-editor__layer-row" primaryClassName="image-canvas-editor__layer-row-button" thumbnailClassName="image-canvas-editor__layer-row-thumb" metaClassName="image-canvas-editor__layer-row-meta" + onContextMenu={(event) => handleLayerContextMenu(event, layer)} /> ))}
@@ -2644,9 +3520,19 @@ export function ImageCanvasEditorView() {
-
-

图片编辑器

- 画布 +
+ + +
+

{projectTitle}

+ 画布 +
@@ -2661,8 +3547,19 @@ export function ImageCanvasEditorView() { onPointerCancel={finishDrag} onWheel={handleWheel} onDragOver={handleCanvasDragOver} + onDragLeave={handleCanvasDragLeave} onDrop={handleCanvasDrop} + onContextMenu={handleViewportContextMenu} > + {uploadDropTarget === 'canvas' ? ( +
+ 添加到画布 + 松开即可添加 +
+ ) : null}
!layer.hidden) .sort((left, right) => left.zIndex - right.zIndex) .map((layer) => { const isSelected = selectedLayerIds.includes(layer.id); @@ -2696,7 +3594,7 @@ export function ImageCanvasEditorView() { + + + + + ) : ( + <> + + + + +
+ + + + +
+ + + + +
+ + + +
+ + + )} +
+ ) : null} + event.stopPropagation()} > -
- setIsZoomMenuOpen((open) => !open)} - > - {formatPercent(viewport.scale)} - - {isZoomMenuOpen ? ( - - { - updateScaleFromCenter(viewport.scale * 1.16); - setIsZoomMenuOpen(false); - }} - > - 放大 - - { - updateScaleFromCenter(viewport.scale * 0.86); - setIsZoomMenuOpen(false); - }} - > - 缩小 - - { - fitLayers(); - setIsZoomMenuOpen(false); - }} - > - 显示画布所有元素 - - {[0.5, 1, 2].map((scale) => ( - { - updateScaleFromCenter(scale); - setIsZoomMenuOpen(false); - }} - > - 缩放至{Math.round(scale * 100)}% - - ))} - - ) : null} -
+ +
setIsMinimapOpen((open) => !open)} /> +
+ setIsZoomMenuOpen((open) => !open)} + > + {formatPercent(viewport.scale)} + + {isZoomMenuOpen ? ( + + { + updateScaleFromCenter(viewport.scale * 1.16); + setIsZoomMenuOpen(false); + }} + > + 放大 + + { + updateScaleFromCenter(viewport.scale * 0.86); + setIsZoomMenuOpen(false); + }} + > + 缩小 + + { + fitLayers(); + setIsZoomMenuOpen(false); + }} + > + 显示画布所有元素 + + {[0.5, 1, 2].map((scale) => ( + { + updateScaleFromCenter(scale); + setIsZoomMenuOpen(false); + }} + > + 缩放至{Math.round(scale * 100)}% + + ))} + + ) : null} +
{isMinimapOpen && minimapModel ? ( diff --git a/src/index.css b/src/index.css index d8c799d3..548a2522 100644 --- a/src/index.css +++ b/src/index.css @@ -3328,6 +3328,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { } .image-canvas-editor__icon-button, +.image-canvas-editor__project-back-button, .image-canvas-editor__floating-toolbar button, .image-canvas-editor__bottom-toolbar button, .image-canvas-editor__reset-button { @@ -3345,6 +3346,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { } .image-canvas-editor__icon-button:hover, +.image-canvas-editor__project-back-button:hover, .image-canvas-editor__floating-toolbar button:hover, .image-canvas-editor__bottom-toolbar button:hover, .image-canvas-editor__reset-button:hover { @@ -3487,6 +3489,40 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { pointer-events: none; } +.image-canvas-editor__upload-drop-overlay { + position: absolute; + z-index: 30; + display: grid; + place-content: center; + gap: 0.3rem; + border: 1.5px dashed rgba(37, 99, 235, 0.72); + background: rgba(239, 246, 255, 0.78); + color: #1d4ed8; + pointer-events: none; + text-align: center; + backdrop-filter: blur(2px); +} + +.image-canvas-editor__upload-drop-overlay span { + font-size: 0.82rem; + font-weight: 820; +} + +.image-canvas-editor__upload-drop-overlay strong { + font-size: 1rem; + font-weight: 920; +} + +.image-canvas-editor__upload-drop-overlay--assets { + inset: 0.45rem; + border-radius: 0.55rem; +} + +.image-canvas-editor__upload-drop-overlay--canvas { + inset: 0.85rem; + border-radius: 0.65rem; +} + .image-canvas-editor__canvas-marquee { position: absolute; z-index: 7; @@ -3701,6 +3737,24 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { padding: 0.55rem 0.7rem; } +.image-canvas-editor__topbar-title { + display: inline-flex; + min-width: 0; + align-items: center; + gap: 0.55rem; +} + +.image-canvas-editor__project-back-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.15rem; + height: 2.15rem; + flex-shrink: 0; + border-radius: 0.45rem; + text-decoration: none; +} + .image-canvas-editor__zoom-menu-wrap { position: relative; z-index: 20; @@ -3799,6 +3853,63 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.9); } +.image-canvas-editor__layer--locked { + cursor: default; +} + +.image-canvas-editor__context-menu { + position: fixed; + z-index: 80; + display: grid; + min-width: 168px; + max-height: calc(100vh - 16px); + overflow: hidden; + padding: 0.35rem; + border: 1px solid rgba(148, 163, 184, 0.32); + border-radius: 0.45rem; + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 18px 52px rgba(15, 23, 42, 0.18); + color: #172033; +} + +.image-canvas-editor__context-menu button { + display: flex; + align-items: center; + width: 100%; + min-height: 2rem; + border: 0; + border-radius: 0.32rem; + background: transparent; + padding: 0 0.65rem; + color: inherit; + font-size: 0.86rem; + font-weight: 720; + text-align: left; + cursor: default; +} + +.image-canvas-editor__context-menu button:hover:not(:disabled), +.image-canvas-editor__context-menu button:focus-visible:not(:disabled) { + background: rgba(37, 99, 235, 0.1); + outline: none; +} + +.image-canvas-editor__context-menu button:disabled { + color: rgba(100, 116, 139, 0.5); +} + +.image-canvas-editor__context-menu hr { + width: 100%; + height: 1px; + border: 0; + background: rgba(148, 163, 184, 0.25); + margin: 0.3rem 0; +} + +.image-canvas-editor__context-menu-danger { + color: #b42318 !important; +} + .image-canvas-editor__generation-frame { position: absolute; z-index: 6; From 80a382b034299b02409a943e75c8f608a1ad7aff Mon Sep 17 00:00:00 2001 From: Linghong Date: Mon, 15 Jun 2026 16:16:56 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E6=96=B0=E5=A2=9EEditor=20Agent=20Mock=20A?= =?UTF-8?q?gent=20P1=E8=90=BD=E5=9C=B0=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增Editor Agent Mock Agent P1落地计划文档 明确P1使用确定性mock Agent生成结构化patch 补充runner命令白名单、工作区隔离、网络隔离和preview安全约束 更新docs总览中的editor agent文档索引 --- docs/README.md | 2 +- ...】EditorAgentMockAgentP1落地计划-2026-06-15.md | 488 ++++++++++++++++++ 2 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 docs/technical/【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md diff --git a/docs/README.md b/docs/README.md index 78a839aa..45aec037 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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)。 +`/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/canvas` 图片画布编辑器的画布素材 ZIP 导出能力,入口放在右上角标题栏下载图标内,第一版采用前端 JSZip 打包画布中有效图层引用的上传图、生成图和修改结果,方案见 [【前端架构】图片画布素材导出方案-2026-06-15.md](./technical/【前端架构】图片画布素材导出方案-2026-06-15.md)。 diff --git a/docs/technical/【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md b/docs/technical/【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md new file mode 100644 index 00000000..9991218a --- /dev/null +++ b/docs/technical/【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md @@ -0,0 +1,488 @@ +# Editor Agent Mock Agent P1 落地计划 + +更新时间:`2026-06-15` + +## 背景 + +`/editor/agent` 的长期目标是浏览器内 AI Web 工程编辑器:用户通过自然语言和代码编辑生成一个完整 Web 工程,并在独立沙箱中预览。当前基础方案已确定为“平台编辑器壳 + api-server 控制面 + 独立 runner + 独立 preview origin”的四层结构。 + +P1 阶段先不接真实 AI Agent,不调用 LLM、不接 `platform-agent`、不做 Agent 记忆、工具调用或多轮规划。P1 使用确定性的 mock Agent 生成结构化 patch,把风险集中在真正需要先验证的工程链路:项目、快照、patch 校验、静态构建、artifact、preview gateway、iframe 预览和刷新恢复。 + +## P1 目标 + +P1 完成一个可端到端验收的最小纵切: + +```text +/editor/agent 输入 mock 指令 + -> api-server mock Agent 生成结构化 patch + -> patch 校验并保存新 snapshot + -> 创建 preview build + -> runner 静态构建固定模板 + -> 产出 immutable artifact + -> preview gateway 签发独立预览 URL + -> 前端 iframe 切换预览 +``` + +P1 结束时,真实 Agent 仍可后置;后续只替换“需求文本 -> 结构化 patch”的来源,不推翻快照、构建和预览主链路。 + +## 非目标 + +P1 明确不做: + +- 真实 LLM / Agent 调用。 +- LangChain-Rust / `platform-agent` 接入。 +- 任意 npm 依赖安装。 +- AI 自定义 `package.json` scripts。 +- HMR、长驻 dev server、WebSocket 代理。 +- 终端 shell、后端服务、任意端口代理。 +- Web project 作品化发布。 +- 完整 lease / controller / worker 持久任务队列。 +- 与主站同源预览。 +- 把 AI 生成工程写入当前仓库源码目录。 + +## 阶段边界 + +P1 使用 mock Agent,但后续链路必须按生产架构实现: + +| 能力 | P1 做法 | 后续替换点 | +| --- | --- | --- | +| 需求理解 | api-server 内确定性 mock 规则 | Phase 2/3 接真实 Agent | +| patch 生成 | mock Agent 返回结构化 patch | 保留同一 patch DTO | +| patch 校验 | api-server 真实校验 | 继续复用 | +| 快照保存 | SpacetimeDB 真实持久化 | 可拆对象存储优化 | +| 静态构建 | runner 真实构建固定模板 | 可换成持久队列领取 | +| artifact | 本地 / 受控 artifact store | 可换 OSS 或专用 artifact store | +| 预览 | 独立 origin / 独立端口 iframe | 生产接独立域名 | + +## 与原始设计一致性检查 + +P1 与 2026-06-13 的技术方案、威胁模型和验收清单总体一致:mock Agent 只替换“需求文本 -> 结构化 patch”的来源,不改变“编辑器壳 -> api-server 控制面 -> runner 执行面 -> preview gateway”的信任边界。 + +为避免实现时放宽安全边界,P1 额外明确以下硬约束: + +| 主题 | 原始设计要求 | P1 执行口径 | +| --- | --- | --- | +| Agent | AI 只能提交结构化 patch | mock Agent 也只能返回结构化 patch,并必须经过同一后端校验 | +| api-server | 控制面不执行 AI 工程代码 | api-server 可以创建 job 和触发 runner,但不得直接执行 `npm` / `vite` / 用户工程代码 | +| 允许命令 | 平台固定构建命令,禁止 AI 自定义脚本 | runner 只允许固定 argv 命令清单,不经 shell,不执行 `npm run`、`npx` 或用户传入命令 | +| 依赖 | 固定模板依赖,不开放任意 npm | P1 默认离线使用 runner 管理的模板依赖缓存;如需 registry,只能访问平台受控 mirror | +| 工作区 | 独立临时目录或容器,无宿主源码挂载 | 每个 job 使用 runner 创建的任务级临时目录;禁止挂载当前仓库源码、`.env`、Docker socket 和宿主 `node_modules` | +| 网络 | 构建期默认无外网 | 默认 deny all;只允许受控 npm mirror 和只读资产签名域,必须阻断内网、metadata、api-server、SpacetimeDB、Docker daemon | +| artifact | immutable artifact,不复用临时目录 | artifact root 由 runner 配置,不来自请求;preview 只服务 artifact,不服务工作区 | +| 预览 | 独立 preview origin | 开发态也必须使用独立端口 / origin;不得把预览挂在主站同源路径下 | +| 状态机 | 失败不覆盖上一版预览 | 只有当前 active snapshot 的 `succeeded` build 能推进 active preview | + +## 契约设计 + +P1 新增 Web Project 契约,Rust `shared-contracts` 与前端 `packages/shared` 保持同名字段。 + +核心 DTO: + +```text +WebProject +WebProjectSnapshot +WebProjectFile +WebProjectPatch +WebProjectPatchOperation +WebProjectPreviewBuild +WebProjectPreviewBuildEvent +MockAgentTurnRequest +MockAgentTurnResponse +``` + +P1 字段建议: + +- `projectId`:不可枚举字符串 ID。 +- `ownerUserId`:项目归属用户。 +- `templateKey`:固定为 `react-vite-ts-static`。 +- `activeSnapshotId`:当前编辑快照。 +- `activePreviewBuildId`:当前成功预览构建。 +- `files`:P1 可先存为 `Vec` / JSON;只允许小型文本源码。 +- `patchSummary`:mock Agent 或用户编辑摘要。 +- `buildStatus`:`queued | running | succeeded | failed | cancelled | expired | stale`。 +- `previewUrl`:由 api-server 返回给前端,不由前端拼接。 + +路径和内容限制必须写入契约注释和后端校验: + +- 只允许相对路径。 +- 拒绝绝对路径和 `..`。 +- 拒绝 `.env`、`.npmrc`、`.git`、`.ssh`。 +- 拒绝符号链接语义。 +- 限制目录深度、单文件大小、snapshot 总大小。 +- P1 只允许文本源码和少量静态资源。 +- `package.json` 不是事实源;新增依赖和 scripts 在 P1 拒绝或忽略。 + +## 后端存储 + +P1 新增 SpacetimeDB 表: + +```text +web_project +web_project_snapshot +web_project_preview_build +``` + +`web_project`: + +- `project_id` +- `owner_user_id` +- `title` +- `template_key` +- `active_snapshot_id` +- `active_preview_build_id` +- `created_at` +- `updated_at` + +`web_project_snapshot`: + +- `snapshot_id` +- `project_id` +- `owner_user_id` +- `parent_snapshot_id` +- `template_key` +- `files_json` +- `patch_summary` +- `created_by` +- `created_at` + +`web_project_preview_build`: + +- `job_id` +- `project_id` +- `snapshot_id` +- `owner_user_id` +- `status` +- `logs_json` +- `artifact_id` +- `preview_token_id` +- `preview_url` +- `error_summary` +- `created_at` +- `started_at` +- `finished_at` +- `updated_at` + +落点: + +- `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` + +如果已有表新增字段,字段必须放在结构体最后并设置明确默认值;修改字段名或字段类型前必须确认迁移计划。schema 修改后必须执行: + +```bash +npm run spacetime:generate +npm run check:spacetime-schema +``` + +## api-server 控制面 + +新增模块: + +```text +server-rs/crates/api-server/src/web_project.rs +server-rs/crates/api-server/src/web_project_mock_agent.rs +server-rs/crates/api-server/src/modules/web_project.rs +``` + +P1 API: + +```text +POST /api/creation/web-project/projects +GET /api/creation/web-project/projects/{projectId} +GET /api/creation/web-project/projects/{projectId}/snapshot +PATCH /api/creation/web-project/projects/{projectId}/files +POST /api/creation/web-project/projects/{projectId}/mock-agent-turns +POST /api/runtime/web-project/projects/{projectId}/preview-builds +GET /api/runtime/web-project/preview-builds/{jobId} +GET /api/runtime/web-project/preview-builds/{jobId}/events +``` + +控制面职责: + +- 鉴权并校验项目 owner。 +- 创建固定模板项目。 +- 读取 active snapshot。 +- 校验用户编辑和 mock patch。 +- 保存新 snapshot。 +- 创建 preview build。 +- 触发 P1 runner 构建。 +- 记录构建日志和状态。 +- 构建成功后签发 preview URL。 +- 构建失败时保留上一版 active preview。 + +`/events` 复用 `src/services/sseStream.ts` 的事件口径。P1 可以先用 api-server 内存广播或短轮询兼容,但外部契约必须保持 SSE: + +```text +queued +running +log +succeeded +failed +cancelled +expired +stale +``` + +## Mock Agent 规则 + +mock Agent 必须确定性、可测试、不可绕过 patch 校验。 + +输入: + +```json +{ + "prompt": "做一个蓝色计数按钮页面", + "baseSnapshotId": "snapshot_xxx" +} +``` + +输出: + +```json +{ + "snapshot": "...", + "patch": { + "operations": [ + { + "type": "updateFile", + "path": "src/App.tsx", + "content": "..." + } + ] + }, + "summary": "更新首页计数按钮示例" +} +``` + +P1 至少支持以下 mock 指令族: + +- `计数` / `按钮`:生成带计数按钮的 React 页面。 +- `卡片` / `列表`:生成卡片列表页面。 +- `蓝色` / `绿色` / `粉色`:调整 CSS 主题色。 +- `破坏构建`:生成一个 TypeScript 编译错误,用于验收失败保留上一版预览。 +- 其它输入:只更新标题和说明文案,避免 mock 分支过多。 + +mock Agent 输出仍必须经过统一 `validate_web_project_patch(...)`,不得直接写表。 + +## Runner 与构建 + +P1 runner 可以先采用“api-server 创建 job 后触发一次性 runner 进程”的方式,不做持久 worker 队列;但执行面必须独立于 api-server、主站源码目录和预览域。api-server 只传递 job 身份和最小任务能力,不得在自身进程内执行构建命令。 + +建议新增: + +```text +server-rs/crates/web-project-runner +``` + +P1 runner 输入: + +- `jobId` +- `projectId` +- `snapshotId` +- snapshot files 包。 +- artifact root 配置键或受控 artifact 写入能力。 + +请求体、snapshot 或 mock Agent 输出中不得携带 runner 命令、构建参数、工作区路径或 artifact 输出路径。 + +P1 runner 步骤: + +1. 创建任务级临时目录。 +2. 用 runner 内置模板或 runner 管理的只读模板缓存写入固定 React / Vite / TypeScript 模板。 +3. 规范化 snapshot 文件路径,写入前确认最终路径仍在任务临时目录内。 +4. 忽略或重写用户 `package.json` 的危险字段。 +5. 使用环境变量白名单启动子进程,清空平台密钥、`.env`、token、代理和私有 registry 配置。 +6. 执行平台固定命令清单。 +7. 限制 CPU、内存、磁盘、进程数、打开文件数、构建时间、日志长度、单文件大小和 artifact 大小。 +8. 成功后把 `dist/` 复制到 immutable artifact 目录;artifact 目录由 runner 配置计算,不接受请求指定。 +9. 失败时返回错误摘要和日志片段,日志需脱敏并避免完整宿主路径。 +10. 清理临时目录。 + +P1 允许的构建命令只能来自平台常量,使用 argv 方式执行,不经 shell、不拼接字符串、不执行用户传入命令: + +```text +npm ci --ignore-scripts --offline --no-audit --fund=false +npm exec --offline -- vite build +``` + +如果离线缓存不足,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 Gateway + +P1 开发态 preview 可以使用独立端口: + +```text +http://127.0.0.1:/p// +``` + +生产形态继续对齐独立域名: + +```text +https://sandbox.genarrative.world/p// +``` + +gateway 必须: + +- 校验 token。 +- 绑定 owner / project / snapshot / artifact。 +- 禁止 path traversal。 +- MIME 类型按白名单输出。 +- `index.html` fallback 只在 artifact 根内生效。 +- token 过期或 artifact 删除后返回确定错误。 +- 输出 CSP,默认 `connect-src 'none'`,并禁用 Service Worker 和持久缓存。 +- 不向预览页注入平台 access token、用户 cookie、SpacetimeDB、OSS 写权限或 LLM provider 密钥。 + +preview gateway 可以与 api-server 共享代码仓库或部署单元,但服务静态 artifact 的入口必须是独立 listener / 端口 / vhost / origin;不得把 preview artifact 挂到主站同源路径下。 + +前端 iframe: + +```text +sandbox="allow-scripts" +``` + +P1 不增加 `allow-same-origin`、`allow-downloads`、`allow-popups`、`allow-top-navigation`。 + +## 前端落点 + +新增: + +```text +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 +``` + +入口路由: + +```text +/editor/agent +``` + +P1 桌面布局: + +- 左侧文件树。 +- 中间代码编辑区,P1 可先用 `