完成 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:
2026-06-16 17:31:25 +08:00
parent 80a382b034
commit 4b09ce3096
404 changed files with 14886 additions and 2497 deletions

View File

@@ -50,7 +50,7 @@ npm install
npm run dev
```
Linux 多用户共享同一台机器开发时,本地 dev 脚本会为当前 Linux 用户分配一个固定端口段并写入系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json`,自动分配从 `10000-10099` 开始,每段 100 个端口,四个 dev 服务依次使用 `start``start + 3`。可用 `GENARRATIVE_DEV_PORT_RANGE``npm run dev -- --port-range` 手动指定端口段用于特殊场景;注册表会阻止不同用户使用相同或重叠段,并让同一用户后续启动继续复用自己已占用的固定段。该机制只在 Linux 生效Windows 仍沿用原有端口探测与漂移逻辑。
Linux 多用户共享同一台机器开发时,本地 dev 脚本会为当前 Linux 用户分配一个固定端口段并写入系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json`,自动分配从 `10000-10099` 开始,每段 100 个端口,主站、api-server、SpacetimeDB、后台和 Web Project preview gateway 依次使用 `start``start + 4`。可用 `GENARRATIVE_DEV_PORT_RANGE``npm run dev -- --port-range` 手动指定端口段用于特殊场景;注册表会阻止不同用户使用相同或重叠段,并让同一用户后续启动继续复用自己已占用的固定段。该机制只在 Linux 生效Windows 仍沿用原有端口探测与漂移逻辑。
本地 `npm run dev``npm run dev:spacetime``npm run dev:api-server` 会在 Rust 子进程环境中绕过项目默认 `sccache` wrapper避免损坏的本机 cache daemon 阻断 `spacetime publish``api-server` 启动;显式设置的非 sccache 自定义 wrapper 会被保留。生产 / Jenkins 构建仍按流水线自身的 sccache 策略执行。
@@ -61,7 +61,7 @@ Linux 多用户共享同一台机器开发时,本地 dev 脚本会为当前 Li
- 主站 Vite
- 后台 Vite
`npm run dev` 和单模块 `dev:*` 命令会更新根目录 `.app/dev-stack.json`,记录四个本地服务的 pid、端口、URL、启动状态和当前命令。该目录只作本机运行态观测不提交 Git。
`npm run dev` 和单模块 `dev:*` 命令会更新根目录 `.app/dev-stack.json`,记录 `spacetime``api-server``web``admin-web``web-project-preview` 的 pid、端口、URL、启动状态和当前命令。该目录只作本机运行态观测不提交 Git。
开启自动刷新:

View File

@@ -2254,3 +2254,11 @@
- 处理:租约持有 `Arc<SpacetimeConnectionPool>` 并实现 `Drop` 统一复位槽位/归还连接;槽位改 `AtomicBool` CAS 抢占,删除自旋循环(持有 permit 必然命中空闲槽位)。任何新的"显式归还"资源在 async 取消语义下都要先想 Drop 兜底。
- 验证:`cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib``dropped_lease_releases_slot_and_permit``acquire_times_out_at_pool_acquire_when_pool_is_busy`)。
- 关联:`server-rs/crates/spacetime-client/src/lib.rs``docs/【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md`
## Web Project preview sandbox 资源响应必须带 CORS
- 现象:`/editor/agent` 的 preview iframe 已切到独立 `http://127.0.0.1:3104/p/<token>/`,但 iframe 内 JS / CSS 模块加载失败,浏览器控制台提示 origin `null` 被 CORS 拦截,按钮点击 smoke 看不到预览内容。
- 原因P1 iframe 按安全边界只允许 `sandbox="allow-scripts"`,不加 `allow-same-origin`;浏览器会把 iframe 文档放进 opaque origin模块脚本和样式再从 preview gateway 加载时需要资源响应显式允许跨域。
- 处理preview gateway 服务 artifact 的成功响应统一带 `Access-Control-Allow-Origin: *`,但仍保留独立 origin、`sandbox="allow-scripts"` 和 CSP `connect-src 'none'`,不要为了修加载问题放宽 iframe sandbox。
- 验证:`cargo test -p api-server web_project --manifest-path server-rs/Cargo.toml`,并用浏览器 smoke 确认 iframe 中计数按钮从 `已点击 0 次` 变为 `已点击 1 次`
- 关联:`server-rs/crates/api-server/src/web_project_preview_gateway.rs``docs/technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md`

View File

@@ -13,7 +13,7 @@ metadata:
# Genarrative 本地 dev 启动端口与代理目标串联流程
用于维护 Genarrative 本地开发栈启动脚本,重点覆盖 `npm run dev` 与四个 `dev:*` 单模块命令的端口检查、端口漂移和后续流程目标传递。
用于维护 Genarrative 本地开发栈启动脚本,重点覆盖 `npm run dev` 与四个 `dev:*` 单模块命令的端口检查、端口漂移和后续流程目标传递;完整栈当前包含五个服务端口
## 适用场景
@@ -31,26 +31,27 @@ metadata:
2. Rust `api-server``8082`,健康检查为 `http://127.0.0.1:<api-port>/healthz`
3. SpacetimeDB standalone`3101`,健康检查为 `http://127.0.0.1:<spacetime-port>/v1/ping`
4. 后台 Vite`3102`,后台地址为 `http://127.0.0.1:<admin-web-port>/admin/`
5. Web Project preview gateway`3104`,预览地址由 `api-server` 签发为 `http://127.0.0.1:<web-project-preview-port>/p/<preview-token>/`
端口不可用时,脚本会从优先端口开始向后寻找可用端口。后续流程必须以解析后的实际端口为准,不能继续使用默认端口。
Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE``--port-range` 会先向系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json` 申请一个端口段,再把该段映射为 `web = start``api = start + 1``spacetime = start + 2``adminWeb = start + 3`。注册表锁文件是 `/var/tmp/genarrative-dev-port-ranges/registry.lock`,可通过 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。自动分配从 `10000-10099` 起,每次占用 100 个端口块,后续块按 `10100-10199``10200-10299` 递增;当前口径是“一个用户固定占用一个段,后续启动继续复用这段并在段内漂移”;该注册表只在 Linux 上生效Windows 继续沿用原有端口探测、漂移和复用逻辑,不读系统级注册表。
Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE``--port-range` 会先向系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json` 申请一个端口段,再把该段映射为 `web = start``api = start + 1``spacetime = start + 2``adminWeb = start + 3``webProjectPreview = start + 4`。注册表锁文件是 `/var/tmp/genarrative-dev-port-ranges/registry.lock`,可通过 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。自动分配从 `10000-10099` 起,每次占用 100 个端口块,后续块按 `10100-10199``10200-10299` 递增;当前口径是“一个用户固定占用一个段,后续启动继续复用这段并在段内漂移”;该注册表只在 Linux 上生效Windows 继续沿用原有端口探测、漂移和复用逻辑,不读系统级注册表。
## 实现入口
- `package.json`
- `dev`:执行 `node scripts/dev.mjs`,启动完整四模块
- `dev`:执行 `node scripts/dev.mjs`,启动完整栈的五个服务端口
- `dev:spacetime` / `dev:api-server` / `dev:web` / `dev:admin-web`:执行 `node scripts/dev.mjs <module>`
- `scripts/dev-stack-port-utils.mjs`
- `isPortAvailable(...)`:探测端口是否可监听。
- `findAvailablePort(...)`:从优先端口向后寻找可用端口,`0` 表示申请临时端口。
- `resolveDevStackPorts(...)`:一次性解析 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,并避免本次解析结果互相冲突。
- `resolveDevStackPorts(...)`:一次性解析 SpacetimeDB、api-server、主站 Vite、后台 Vite、Web Project preview gateway 端口,并避免本次解析结果互相冲突。
- Linux 注册表分配:`reserveLinuxDevPortRange(...)` / `releaseLinuxDevPortRange(...)`,仅在 Linux 上启用系统级端口段登记与用户段复用,自动分配从 `10000-10099` 起。
- CLI 模式:`node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:3101 api:127.0.0.1:8082 web:0.0.0.0:3000 adminWeb:127.0.0.1:3102`
- CLI 模式:`node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:3101 api:127.0.0.1:8082 web:0.0.0.0:3000 adminWeb:127.0.0.1:3102 webProjectPreview:127.0.0.1:3104`
- `scripts/dev.mjs`
- 解析 CLI 参数后统一计算 client host、端口、`SPACETIME_SERVER``RUST_SERVER_TARGET`
- 完整栈按 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 顺序启动。
- Linux 下会先申请系统级端口段并把它映射成个 dev 端口;自动分配从 `10000-10099`Windows 则直接沿用原有参数解析与端口漂移逻辑。
- 完整栈按 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 顺序启动Web Project preview gateway 是 `api-server` 内嵌的独立监听端口
- Linux 下会先申请系统级端口段并把它映射成个 dev 端口;自动分配从 `10000-10099`Windows 则直接沿用原有参数解析与端口漂移逻辑。
- 单模块命令复用同一套参数和 env 解析。
## 必须保持的传递链路
@@ -63,8 +64,9 @@ Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range`
4. api-server 健康检查:`wait_for_api_server "${RUST_SERVER_TARGET}/healthz" ...`
5. 主站 Vite`RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET``ADMIN_WEB_TARGET``ADMIN_WEB_PORT``--port=${WEB_PORT}``--host=${WEB_HOST}`
6. 后台 Vite`ADMIN_API_TARGET``GENARRATIVE_API_TARGET``GENARRATIVE_API_PORT``--port=${ADMIN_WEB_PORT}`
7. 控制台日志:`[dev:ports]``[dev] web/admin web/api-server/spacetime` 必须显示最终实际地址
8. Linux 端口段注册`[dev] port-range:` `[dev] port-range-registry:` 只在 Linux 输出Windows 不应依赖系统级注册表
7. Web Project preview gateway`GENARRATIVE_WEB_PROJECT_PREVIEW_HOST``GENARRATIVE_WEB_PROJECT_PREVIEW_PORT``GENARRATIVE_WEB_PROJECT_PREVIEW_PUBLIC_BASE_URL``GENARRATIVE_WEB_PROJECT_PREVIEW_FRAME_ANCESTORS`
8. 控制台日志`[dev:ports]` `[dev] web/admin web/api-server/spacetime` 必须显示最终实际地址
9. Linux 端口段注册:`[dev] port-range:``[dev] port-range-registry:` 只在 Linux 输出Windows 不应依赖系统级注册表。
如果只改了其中一段,通常会出现:浏览器打开的前端可用,但 `/api/*` 代理到旧端口;后台页面可用但后台 API 失败SpacetimeDB 启动在新端口但 publish 仍发往旧端口。
@@ -91,14 +93,15 @@ Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range`
node --check scripts/dev.mjs
npm run test -- scripts/dev-stack-port-utils.test.ts
npm run check:encoding
node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 api:127.0.0.1:0 web:0.0.0.0:0 adminWeb:127.0.0.1:0
npm run test -- scripts/dev.test.ts scripts/dev-stack-port-utils.test.ts --reporter verbose
node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 api:127.0.0.1:0 web:0.0.0.0:0 adminWeb:127.0.0.1:0 webProjectPreview:127.0.0.1:0
```
端口冲突回归测试建议:
1. 用测试或临时 Node server 占用某个优先端口。
2. 调用 `findAvailablePort`,断言结果大于被占用端口。
3. 调用 `resolveDevStackPorts`,断言个结果互不相同。
3. 调用 `resolveDevStackPorts`,断言个结果互不相同。
4. 如果实际启动完整栈,观察控制台:
- `[dev:ports] ... 不可用,改用 ...`
- `[dev] api-server: http://...:<actual-api-port>`
@@ -121,7 +124,7 @@ node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 ap
- [ ] 端口工具有测试覆盖端口被占用和多端口互斥解析。
- [ ] Linux 注册表分配、同用户复用固定段并继续漂移、自动分配从 `10000-10099` 起、Windows bypass 都有测试覆盖。
- [ ] `scripts/dev.mjs` 通过 `node --check`
- [ ] `npm run dev` 的 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 都使用实际端口。
- [ ] `npm run dev` 的 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite、Web Project preview gateway 都使用实际端口。
- [ ] `npm run dev:web` 在主站端口不可用时能切换到可用端口。
- [ ] 文档同步更新 `docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`
- [ ] 长期踩坑同步更新 `.hermes/shared-memory/pitfalls.md`

View File

@@ -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)。

View File

@@ -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 的可执行波次、任务包、验收门禁和覆盖矩阵,完整承接技术落地计划。
## 维护规则

View 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 2api-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 3runner、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] 真实浏览器点击 smoke2026-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 构建命令只来自平台 allowlistWindows 下 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 和作品化发布必须进入后续独立评审。

View File

@@ -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 可以使用独立端口:

View 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 小时 TTLURL 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 计划保留。

View File

@@ -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 可恢复

View File

@@ -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 写入 allowlistidentity / 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`

View File

@@ -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_*` 口径。

View File

@@ -0,0 +1,110 @@
export const WEB_PROJECT_TEMPLATE_REACT_VITE_TS_STATIC =
'react-vite-ts-static' as const;
export type WebProjectPreviewBuildStatus =
| 'queued'
| 'running'
| 'succeeded'
| 'failed'
| 'cancelled'
| 'expired'
| 'stale';
export interface WebProject {
projectId: string;
ownerUserId: string;
title: string;
templateKey: typeof WEB_PROJECT_TEMPLATE_REACT_VITE_TS_STATIC;
activeSnapshotId: string;
activePreviewBuildId?: string | null;
createdAt: string;
updatedAt: string;
}
export interface WebProjectFile {
/** P1 只接受相对路径,拒绝路径穿越、敏感文件和固定模板控制文件。 */
path: string;
/** P1 只保存 UTF-8 文本源码或受控静态文本资源,大小由后端统一限制。 */
content: string;
mediaType: string;
encoding: 'utf-8';
/** 后端按 UTF-8 字节数计算,参与 snapshot 总大小限制。 */
sizeBytes: number;
}
export interface WebProjectSnapshot {
snapshotId: string;
projectId: string;
ownerUserId: string;
parentSnapshotId?: string | null;
templateKey: typeof WEB_PROJECT_TEMPLATE_REACT_VITE_TS_STATIC;
files: WebProjectFile[];
patchSummary: string;
createdBy: string;
createdAt: string;
}
export type WebProjectPatchOperation =
/** 仅允许创建 P1 可编辑范围内的文件,不能创建依赖、脚本或模板控制文件。 */
| { type: 'createFile'; path: string; content: string }
/** 仅允许更新 P1 可编辑范围内的文件。 */
| { type: 'updateFile'; path: string; content: string }
/** 删除同样受路径白名单约束,避免绕过固定模板边界。 */
| { type: 'deleteFile'; path: string }
/** 源路径和目标路径都必须通过 P1 可编辑路径校验。 */
| { type: 'renameFile'; fromPath: string; toPath: string }
/** package.json 不是依赖事实源P1 拒绝 scripts / dependencies 等供应链字段。 */
| { type: 'packageManifestRequest'; content: string };
export interface WebProjectPatch {
/** 所有 patch 操作必须先经过后端统一校验,再生成新 snapshot。 */
operations: WebProjectPatchOperation[];
}
export interface WebProjectPreviewBuild {
jobId: string;
projectId: string;
snapshotId: string;
ownerUserId: string;
status: WebProjectPreviewBuildStatus;
logs: string[];
artifactId?: string | null;
/** 后端保存的不透明预览票据;前端只消费 previewUrl不拼接或解析 token。 */
previewTokenId?: string | null;
previewUrl?: string | null;
errorSummary?: string | null;
createdAt: string;
startedAt?: string | null;
finishedAt?: string | null;
updatedAt: string;
}
export interface WebProjectPreviewBuildEvent {
jobId: string;
status: WebProjectPreviewBuildStatus;
message?: string | null;
build?: WebProjectPreviewBuild | null;
}
export interface MockAgentTurnRequest {
prompt: string;
baseSnapshotId: string;
}
export interface MockAgentTurnResponse {
snapshot: WebProjectSnapshot;
patch: WebProjectPatch;
summary: string;
}
export interface WebProjectResponse {
project: WebProject;
}
export interface WebProjectSnapshotResponse {
snapshot: WebProjectSnapshot;
}
export interface WebProjectPreviewBuildResponse {
build: WebProjectPreviewBuild;
}

View File

@@ -51,6 +51,7 @@ export type {
} from './contracts/squareHoleWorks';
export type * from './contracts/story';
export type * from './contracts/visualNovel';
export type * from './contracts/webProject';
export * from './http';
export * from './llm/narrativeLanguage';
export * from './llm/parsers';

View File

@@ -55,8 +55,8 @@ export function parsePortRangeSpec(value) {
throw new Error(`端口段无效: ${spec},端口必须在 1024-65535 且起始不大于结束`);
}
if (end - start + 1 < 4) {
throw new Error(`端口段至少需要 4 个端口: ${spec}`);
if (end - start + 1 < 5) {
throw new Error(`端口段至少需要 5 个端口: ${spec}`);
}
return {start, end, label: `${start}-${end}`};
@@ -118,6 +118,7 @@ export function mapDevPortsToPortRange(portRange) {
apiPort: normalizedRange.start + 1,
spacetimePort: normalizedRange.start + 2,
adminWebPort: normalizedRange.start + 3,
webProjectPreviewPort: normalizedRange.start + 4,
range: normalizedRange,
};
}
@@ -556,6 +557,7 @@ export async function resolveDevStackPorts(config) {
['api', config.api],
['web', config.web],
['adminWeb', config.adminWeb],
['webProjectPreview', config.webProjectPreview],
].filter(([, portConfig]) => Boolean(portConfig));
const result = {};

View File

@@ -24,7 +24,7 @@ function reservePort(port) {
}
describe('dev stack port utils', () => {
it('解析端口段并映射到四个 dev 端口', () => {
it('解析端口段并映射到 dev 端口和预览网关第五端口', () => {
expect(parsePortRangeSpec('10000-10099')).toEqual({
start: 10000,
end: 10099,
@@ -35,6 +35,7 @@ describe('dev stack port utils', () => {
apiPort: 10001,
spacetimePort: 10002,
adminWebPort: 10003,
webProjectPreviewPort: 10004,
});
});
@@ -74,9 +75,11 @@ describe('dev stack port utils', () => {
api: {host: '127.0.0.1', preferredPort: 0},
web: {host: '127.0.0.1', preferredPort: 0},
adminWeb: {host: '127.0.0.1', preferredPort: 0},
webProjectPreview: {host: '127.0.0.1', preferredPort: 0},
});
expect(new Set(Object.values(resolvedPorts)).size).toBe(4);
expect(new Set(Object.values(resolvedPorts)).size).toBe(5);
expect(resolvedPorts.webProjectPreview).toBeGreaterThan(0);
});
it('端口段内会一直漂移到段尾,不会被默认 200 次尝试截断', async () => {

View File

@@ -37,9 +37,18 @@ const manifestPath = resolve(serverRsDir, 'Cargo.toml');
const modulePath = resolve(serverRsDir, 'crates/spacetime-module');
const viteCliPath = resolve(repoRoot, 'scripts/vite-cli.mjs');
const adminWebDir = resolve(repoRoot, 'apps/admin-web');
const LOCAL_DEV_RUSTC_WRAPPER_BYPASS = process.platform === 'win32' ? 'rustc' : '/usr/bin/env';
const webProjectRunnerBinPath = resolve(
serverRsDir,
'target/debug',
process.platform === 'win32' ? 'web-project-runner.exe' : 'web-project-runner',
);
const LOCAL_DEV_RUSTC_WRAPPER_BYPASS = process.platform === 'win32' ? '' : '/usr/bin/env';
const WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET_ENV =
'GENARRATIVE_SPACETIME_WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET';
const WEB_PROJECT_SERVICE_IDENTITY_CACHE_FILE = 'api-server-web-project-identity.json';
const SERVICE_NAMES = ['spacetime', 'api-server', 'web', 'admin-web'];
const SNAPSHOT_SERVICE_NAMES = [...SERVICE_NAMES, 'web-project-preview'];
const SERVICE_ALIASES = new Map([
['api', 'api-server'],
['admin', 'admin-web'],
@@ -62,6 +71,8 @@ function usage() {
--web-port <port> 主站 Vite 端口
--admin-web-host <host> 后台 Vite 监听地址
--admin-web-port <port> 后台 Vite 端口
--web-project-preview-host <host> Web Project preview gateway 监听地址
--web-project-preview-port <port> Web Project preview gateway 端口
--spacetime-host <host> SpacetimeDB 监听地址
--spacetime-port <port> SpacetimeDB 端口
--spacetime-data-dir <path> SpacetimeDB 本地数据目录
@@ -113,6 +124,12 @@ function parseArgs(argv, baseEnv) {
webPort: normalizePort(env.WEB_PORT, 3000),
adminWebHost: env.ADMIN_WEB_HOST || '127.0.0.1',
adminWebPort: normalizePort(env.ADMIN_WEB_PORT, 3102),
webProjectPreviewHost:
env.GENARRATIVE_WEB_PROJECT_PREVIEW_HOST || '127.0.0.1',
webProjectPreviewPort: normalizePort(
env.GENARRATIVE_WEB_PROJECT_PREVIEW_PORT,
3104,
),
spacetimeHost: env.SPACETIME_HOST || '127.0.0.1',
spacetimePort: normalizePort(env.SPACETIME_PORT, 3101),
spacetimeDataDir: resolve(serverRsDir, '.spacetimedb/local/data'),
@@ -130,6 +147,14 @@ function parseArgs(argv, baseEnv) {
preserveDatabase: false,
migrationBootstrapSecret: '',
migrationBootstrapSecretMode: 'auto',
webProjectServiceBootstrapSecret: String(
env[WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET_ENV] ?? '',
).trim(),
webProjectServiceBootstrapSecretMode: String(
env[WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET_ENV] ?? '',
).trim()
? 'manual'
: 'auto',
watch: false,
interactive: true,
};
@@ -175,6 +200,17 @@ function parseArgs(argv, baseEnv) {
options.adminWebPort = normalizePort(readValue(), options.adminWebPort);
explicitOptions.add('adminWebPort');
break;
case '--web-project-preview-host':
options.webProjectPreviewHost = readValue();
explicitOptions.add('webProjectPreviewHost');
break;
case '--web-project-preview-port':
options.webProjectPreviewPort = normalizePort(
readValue(),
options.webProjectPreviewPort,
);
explicitOptions.add('webProjectPreviewPort');
break;
case '--spacetime-host':
options.spacetimeHost = readValue();
options.spacetimeServerUrl = '';
@@ -226,6 +262,14 @@ function parseArgs(argv, baseEnv) {
options.migrationBootstrapSecret = '';
options.migrationBootstrapSecretMode = 'disabled';
break;
case '--web-project-service-bootstrap-secret':
options.webProjectServiceBootstrapSecret = readValue();
options.webProjectServiceBootstrapSecretMode = 'manual';
break;
case '--no-web-project-service-bootstrap-secret':
options.webProjectServiceBootstrapSecret = '';
options.webProjectServiceBootstrapSecretMode = 'disabled';
break;
case '--watch':
options.watch = true;
break;
@@ -262,9 +306,27 @@ function resolveDevStackStatePath(root = repoRoot) {
return join(root, '.app/dev-stack.json');
}
function buildWebProjectPreviewPublicBaseUrl({host, port}) {
return `http://${resolveClientHost(host)}:${port}`;
}
function resolveWebProjectPreviewFrameAncestors({baseEnv, webOrigin}) {
const explicitAncestors = String(
baseEnv.GENARRATIVE_WEB_PROJECT_PREVIEW_FRAME_ANCESTORS ?? '',
).trim();
if (explicitAncestors) {
const ancestors = uniqueNonEmpty([explicitAncestors, webOrigin]).join(' ');
return ancestors || webOrigin;
}
const webUrl = new URL(webOrigin);
const localhostOrigin = `${webUrl.protocol}//localhost:${webUrl.port}`;
return uniqueNonEmpty([webOrigin, localhostOrigin]).join(' ');
}
function buildDevStackSnapshot(runner, updatedAt = new Date().toISOString()) {
const services = {};
for (const serviceName of SERVICE_NAMES) {
for (const serviceName of SNAPSHOT_SERVICE_NAMES) {
services[serviceName] = buildDevStackServiceSnapshot(
runner,
serviceName,
@@ -341,6 +403,12 @@ function resolveDevStackServiceEndpoint(runner, serviceName) {
port: options.adminWebPort,
url: `http://${state.adminWebTargetHost}:${options.adminWebPort}/admin/`,
};
case 'web-project-preview':
return {
host: options.webProjectPreviewHost,
port: options.webProjectPreviewPort,
url: state.webProjectPreviewPublicBaseUrl,
};
default:
return {
host: null,
@@ -383,6 +451,8 @@ function resolveDevStackServiceCommand(runner, serviceName) {
`--port=${options.adminWebPort}`,
'--strictPort',
].join(' ');
case 'web-project-preview':
return 'api-server embedded Web Project preview gateway';
default:
return null;
}
@@ -779,6 +849,10 @@ class DevRunner {
adminWebTargetHost: resolveClientHost(options.adminWebHost),
spacetimeServer: initialSpacetimeServer,
apiTarget: `http://${resolveClientHost(options.apiHost)}:${options.apiPort}`,
webProjectPreviewPublicBaseUrl: buildWebProjectPreviewPublicBaseUrl({
host: options.webProjectPreviewHost,
port: options.webProjectPreviewPort,
}),
portRange: null,
portRangeReservation: null,
};
@@ -851,9 +925,16 @@ class DevRunner {
if (!this.explicitOptions.has('adminWebPort')) {
this.options.adminWebPort = mappedPorts.adminWebPort;
}
if (!this.explicitOptions.has('webProjectPreviewPort')) {
this.options.webProjectPreviewPort = mappedPorts.webProjectPreviewPort;
}
this.state.spacetimeServer = `http://${this.options.spacetimeHost}:${this.options.spacetimePort}`;
this.state.apiTarget = `http://${this.state.apiTargetHost}:${this.options.apiPort}`;
this.state.webProjectPreviewPublicBaseUrl = buildWebProjectPreviewPublicBaseUrl({
host: this.options.webProjectPreviewHost,
port: this.options.webProjectPreviewPort,
});
}
shouldValidateSpacetimeToolVersion(command) {
@@ -973,6 +1054,11 @@ class DevRunner {
preferredPort: options.apiPort,
portRange: this.state.portRange,
};
portConfig.webProjectPreview = {
host: options.webProjectPreviewHost,
preferredPort: options.webProjectPreviewPort,
portRange: this.state.portRange,
};
}
if (command === 'all' || command === 'web') {
@@ -1021,6 +1107,9 @@ class DevRunner {
if (resolvedPorts.adminWeb) {
options.adminWebPort = resolvedPorts.adminWeb;
}
if (resolvedPorts.webProjectPreview) {
options.webProjectPreviewPort = resolvedPorts.webProjectPreview;
}
this.state.apiTargetHost = resolveClientHost(options.apiHost);
this.state.adminWebTargetHost = resolveClientHost(options.adminWebHost);
@@ -1028,6 +1117,10 @@ class DevRunner {
this.state.spacetimeServer = `http://${options.spacetimeHost}:${options.spacetimePort}`;
}
this.state.apiTarget = `http://${this.state.apiTargetHost}:${options.apiPort}`;
this.state.webProjectPreviewPublicBaseUrl = buildWebProjectPreviewPublicBaseUrl({
host: options.webProjectPreviewHost,
port: options.webProjectPreviewPort,
});
}
registerServices() {
@@ -1092,6 +1185,7 @@ class DevRunner {
console.log(`[dev] web: http://127.0.0.1:${options.webPort}`);
console.log(`[dev] admin web: http://${state.adminWebTargetHost}:${options.adminWebPort}/admin/`);
console.log(`[dev] api-server: ${state.apiTarget}`);
console.log(`[dev] web project preview: ${state.webProjectPreviewPublicBaseUrl}`);
console.log(`[dev] spacetime: ${state.spacetimeServer}`);
console.log(`[dev] database: ${options.database}`);
}
@@ -1266,6 +1360,7 @@ class DevRunner {
async publishSpacetimeModule() {
const env = buildLocalRustProcessEnv(this.baseEnv);
this.prepareMigrationBootstrapSecret(env);
this.prepareWebProjectServiceBootstrapSecret(env);
const args = buildSpacetimePublishArgs({
database: this.options.database,
@@ -1304,12 +1399,55 @@ class DevRunner {
console.log(`[dev:spacetime] 迁移引导密钥: ${this.options.migrationBootstrapSecret}`);
}
prepareWebProjectServiceBootstrapSecret(env) {
switch (this.options.webProjectServiceBootstrapSecretMode) {
case 'auto':
if (!this.options.webProjectServiceBootstrapSecret) {
this.options.webProjectServiceBootstrapSecret = randomHex(32);
}
break;
case 'manual':
if (this.options.webProjectServiceBootstrapSecret.length < 16) {
throw new Error('Web Project 服务身份引导密钥至少需要 16 个字符');
}
break;
case 'disabled':
delete env[WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET_ENV];
console.log('[dev:spacetime] 未启用 Web Project 服务身份引导密钥');
return;
default:
throw new Error(
`未知 Web Project 服务身份引导密钥模式: ${this.options.webProjectServiceBootstrapSecretMode}`,
);
}
env[WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET_ENV] =
this.options.webProjectServiceBootstrapSecret;
this.state.webProjectServiceBootstrapSecretPrepared = true;
console.log(
`[dev:spacetime] 已准备 Web Project 服务身份引导密钥: mode=${this.options.webProjectServiceBootstrapSecretMode}, length=${this.options.webProjectServiceBootstrapSecret.length}`,
);
}
async ensureApiServerSpacetimeToken() {
const existingToken = String(this.baseEnv.GENARRATIVE_SPACETIME_TOKEN ?? '').trim();
if (existingToken && shouldTrustExistingSpacetimeToken(existingToken, this.state.spacetimeServer)) {
return;
}
const cachedIdentity = readCachedSpacetimeIdentity(
this.options.spacetimeDataDir,
this.state.spacetimeServer,
);
if (cachedIdentity) {
this.baseEnv.GENARRATIVE_SPACETIME_TOKEN = cachedIdentity.token;
this.state.spacetimeIdentity = cachedIdentity.identity;
console.log(
`[dev:spacetime] 复用本地 api-server identity: ${cachedIdentity.identity.slice(0, 12)}...`,
);
return;
}
const identityUrl = buildUrl(this.state.spacetimeServer, '/v1/identity');
if (!identityUrl) {
throw new Error(`无法构造 SpacetimeDB identity 地址: ${this.state.spacetimeServer}`);
@@ -1318,6 +1456,11 @@ class DevRunner {
const response = await fetchSpacetimeIdentity(identityUrl);
this.baseEnv.GENARRATIVE_SPACETIME_TOKEN = response.token;
this.state.spacetimeIdentity = response.identity;
cacheSpacetimeIdentity(
this.options.spacetimeDataDir,
this.state.spacetimeServer,
response,
);
console.log(
`[dev:spacetime] 已创建本地 Web identity: ${response.identity.slice(0, 12)}...`,
);
@@ -1325,6 +1468,8 @@ class DevRunner {
async startApiServer(service) {
await this.ensureApiServerSpacetimeToken();
await this.ensureWebProjectServiceIdentityAuthorized();
await this.ensureWebProjectRunnerBinary();
const mergedEnv = buildApiServerProcessEnv({
baseEnv: buildLocalRustProcessEnv(this.baseEnv),
@@ -1382,6 +1527,75 @@ class DevRunner {
service.registerChild(child);
}
async ensureWebProjectRunnerBinary() {
if (String(this.baseEnv.GENARRATIVE_WEB_PROJECT_RUNNER_BIN ?? '').trim()) {
return;
}
if (existsSync(webProjectRunnerBinPath)) {
return;
}
console.log('[dev:api-server] 构建 Web Project runner');
await runForeground(
'cargo',
['build', '-p', 'web-project-runner', '--manifest-path', 'server-rs/Cargo.toml'],
{
cwd: repoRoot,
env: buildLocalRustProcessEnv(this.baseEnv),
label: 'web-project-runner',
},
);
}
async ensureWebProjectServiceIdentityAuthorized() {
if (this.options.webProjectServiceBootstrapSecretMode === 'disabled') {
return;
}
const serviceIdentity = String(this.state.spacetimeIdentity ?? '').trim();
const token = String(this.baseEnv.GENARRATIVE_SPACETIME_TOKEN ?? '').trim();
if (!serviceIdentity || !token) {
console.warn('[dev:spacetime] 未能确认 api-server SpacetimeDB identity跳过 Web Project 自动授权');
return;
}
if (
this.options.webProjectServiceBootstrapSecretMode !== 'manual' &&
!this.state.webProjectServiceBootstrapSecretPrepared
) {
console.warn(
'[dev:spacetime] 本轮未发布带 Web Project 引导密钥的 spacetime-module跳过自动授权',
);
return;
}
const bootstrapSecret = String(
this.options.webProjectServiceBootstrapSecret ?? '',
).trim();
if (bootstrapSecret.length < 16) {
console.warn('[dev:spacetime] Web Project 服务身份引导密钥缺失,跳过自动授权');
return;
}
try {
const result = await authorizeWebProjectServiceIdentity({
serverUrl: this.state.spacetimeServer,
database: this.options.database,
token,
bootstrapSecret,
serviceIdentity,
});
console.log(
`[dev:spacetime] 已授权 Web Project 本地服务 identity: ${(result.service_identity_hex ?? serviceIdentity).slice(0, 12)}...`,
);
} catch (error) {
console.warn(
`[dev:spacetime] Web Project 本地服务 identity 自动授权失败:${error.message}`,
);
}
}
async waitForApiServer() {
const healthUrl = `${this.state.apiTarget}/healthz`;
const deadline = Date.now() + this.options.apiTimeoutSeconds * 1000;
@@ -1950,6 +2164,19 @@ function buildUrl(baseUrl, path) {
}
}
function buildSpacetimeProcedureUrl(serverUrl, database, procedureName) {
if (!database) {
throw new Error('必须提供 SpacetimeDB 数据库名');
}
try {
const baseUrl = new URL(serverUrl).href.replace(/\/+$/u, '');
return `${baseUrl}/v1/database/${encodeURIComponent(database)}/call/${encodeURIComponent(procedureName)}`;
} catch {
throw new Error(`无法构造 SpacetimeDB procedure 地址: ${serverUrl}`);
}
}
function safeUrlPort(rawUrl) {
try {
return Number(new URL(rawUrl).port);
@@ -2015,6 +2242,171 @@ async function fetchSpacetimeIdentity(url) {
return {identity, token};
}
function readCachedSpacetimeIdentity(spacetimeDataDir, serverUrl) {
const cachePath = resolve(spacetimeDataDir, WEB_PROJECT_SERVICE_IDENTITY_CACHE_FILE);
if (!existsSync(cachePath)) {
return null;
}
try {
const payload = JSON.parse(readFileSync(cachePath, 'utf8'));
if (
payload?.serverUrl !== serverUrl ||
typeof payload.identity !== 'string' ||
typeof payload.token !== 'string' ||
!payload.identity.trim() ||
!payload.token.trim()
) {
return null;
}
return {
identity: payload.identity.trim(),
token: payload.token.trim(),
};
} catch (error) {
console.warn(`[dev:spacetime] 忽略无效本地 api-server identity 缓存: ${error.message}`);
return null;
}
}
function cacheSpacetimeIdentity(spacetimeDataDir, serverUrl, identityPayload) {
const cachePath = resolve(spacetimeDataDir, WEB_PROJECT_SERVICE_IDENTITY_CACHE_FILE);
ensureParentDir(cachePath);
writeFileSync(
cachePath,
`${JSON.stringify(
{
schemaVersion: 1,
serverUrl,
identity: identityPayload.identity,
token: identityPayload.token,
updatedAt: new Date().toISOString(),
},
null,
2,
)}\n`,
'utf8',
);
}
async function authorizeWebProjectServiceIdentity({
serverUrl,
database,
token,
bootstrapSecret,
serviceIdentity,
}) {
const url = buildSpacetimeProcedureUrl(
serverUrl,
database,
'authorize_web_project_service_identity',
);
const input = {
bootstrap_secret: bootstrapSecret,
service_identity_hex: serviceIdentity,
note: 'local api-server web project',
};
let response;
try {
response = await fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify([input]),
});
} catch (error) {
throw new Error(
`SpacetimeDB Web Project 服务身份授权请求失败: ${url}; ${
error instanceof Error ? error.message : String(error)
}`,
);
}
const text = await response.text();
if (!response.ok) {
throw new Error(`SpacetimeDB HTTP ${response.status}: ${trimPreview(text)}`);
}
const result = parseWebProjectServiceIdentityResult(text);
if (!result.ok) {
throw new Error(result.error_message ?? 'Web Project 服务身份授权失败');
}
return result;
}
function parseWebProjectServiceIdentityResult(output) {
const candidates = [];
const trimmed = output.trim();
if (trimmed) {
candidates.push(trimmed);
}
for (const line of output.split(/\r?\n/u)) {
const value = line.trim();
if (value.startsWith('{') || value.startsWith('[')) {
candidates.push(value);
}
}
for (const candidate of candidates) {
try {
return normalizeWebProjectServiceIdentityResult(JSON.parse(candidate));
} catch {
// SpacetimeDB 输出可能夹带说明文本,继续尝试后续候选。
}
}
throw new Error(`无法解析 Web Project 服务身份授权返回值: ${trimPreview(trimmed)}`);
}
function normalizeWebProjectServiceIdentityResult(value) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value;
}
if (Array.isArray(value) && value.length >= 3) {
return {
ok: normalizeSatsValue(value[0]),
service_identity_hex: normalizeSatsOption(value[1]),
error_message: normalizeSatsOption(value[2]),
};
}
throw new Error('Web Project 服务身份授权返回值不是合法对象。');
}
function normalizeSatsValue(value) {
if (Array.isArray(value)) {
return value.map((item) => normalizeSatsValue(item));
}
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [key, normalizeSatsValue(entry)]),
);
}
return value;
}
function normalizeSatsOption(value) {
if (Array.isArray(value)) {
if (value.length === 2 && value[0] === 0) {
return normalizeSatsValue(value[1]);
}
if (value.length === 0 || value[0] === 1) {
return null;
}
}
return normalizeSatsValue(value);
}
function shouldTrustExistingSpacetimeToken(existingToken, serverUrl) {
const shellToken = String(process.env.GENARRATIVE_SPACETIME_TOKEN ?? '').trim();
if (shellToken && shellToken === existingToken) {
@@ -2156,24 +2548,45 @@ function buildApiServerProcessEnv({baseEnv, options, state}) {
GENARRATIVE_SPACETIME_SERVER_URL: state.spacetimeServer,
GENARRATIVE_SPACETIME_DATABASE: options.database,
GENARRATIVE_SPACETIME_TOKEN: baseEnv.GENARRATIVE_SPACETIME_TOKEN || '',
GENARRATIVE_WEB_PROJECT_RUNNER_BIN:
String(baseEnv.GENARRATIVE_WEB_PROJECT_RUNNER_BIN ?? '').trim() ||
webProjectRunnerBinPath,
GENARRATIVE_WEB_PROJECT_PREVIEW_HOST: options.webProjectPreviewHost,
GENARRATIVE_WEB_PROJECT_PREVIEW_PORT: String(options.webProjectPreviewPort),
GENARRATIVE_WEB_PROJECT_PREVIEW_PUBLIC_BASE_URL:
state.webProjectPreviewPublicBaseUrl ||
buildWebProjectPreviewPublicBaseUrl({
host: options.webProjectPreviewHost,
port: options.webProjectPreviewPort,
}),
GENARRATIVE_WEB_PROJECT_PREVIEW_FRAME_ANCESTORS:
resolveWebProjectPreviewFrameAncestors({
baseEnv,
webOrigin: `http://${resolveClientHost(options.webHost)}:${options.webPort}`,
}),
};
}
export {
assertReusableSpacetimeProcessVersionMatchesWorkspace,
assertSpacetimeToolVersionMatchesWorkspace,
authorizeWebProjectServiceIdentity,
buildApiServerProcessEnv,
buildDevStackSnapshot,
buildLocalRustProcessEnv,
buildSpacetimeProcedureUrl,
buildSpacetimePublishArgs,
cacheSpacetimeIdentity,
createDevServerSpawnOptions,
createWatchConfigs,
DevRunner,
isDirectModuleExecution,
isSpacetimePublishPermissionError,
normalizeCargoVersionRequirement,
parseWebProjectServiceIdentityResult,
parseArgs,
parseSpacetimeToolVersion,
readCachedSpacetimeIdentity,
resolveDevStackStatePath,
shouldAcceptWatchEvent,
};

View File

@@ -1,24 +1,29 @@
import {mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {join, resolve} from 'node:path';
import {afterEach, describe, expect, test, vi} from 'vitest';
import {
assertReusableSpacetimeProcessVersionMatchesWorkspace,
assertSpacetimeToolVersionMatchesWorkspace,
authorizeWebProjectServiceIdentity,
buildApiServerProcessEnv,
buildDevStackSnapshot,
buildLocalRustProcessEnv,
buildSpacetimeProcedureUrl,
buildSpacetimePublishArgs,
cacheSpacetimeIdentity,
createDevServerSpawnOptions,
createWatchConfigs,
DevRunner,
isDirectModuleExecution,
isSpacetimePublishPermissionError,
normalizeCargoVersionRequirement,
parseWebProjectServiceIdentityResult,
parseArgs,
parseSpacetimeToolVersion,
readCachedSpacetimeIdentity,
resolveDevStackStatePath,
shouldAcceptWatchEvent,
} from './dev.mjs';
@@ -109,7 +114,7 @@ describe('dev scheduler argument routing', () => {
);
});
linuxTest('Linux 启动时按系统级端口段映射四个 dev 端口', async () => {
linuxTest('Linux 启动时按系统级端口段映射 dev 端口和预览网关端口', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-dev-port-range-'));
try {
const {command, explicitOptions, options} = parseArgs([], {
@@ -132,8 +137,10 @@ describe('dev scheduler argument routing', () => {
expect(runner.options.apiPort).toBe(22001);
expect(runner.options.spacetimePort).toBe(22002);
expect(runner.options.adminWebPort).toBe(22003);
expect(runner.options.webProjectPreviewPort).toBe(22004);
expect(runner.state.apiTarget).toBe('http://127.0.0.1:22001');
expect(runner.state.spacetimeServer).toBe('http://127.0.0.1:22002');
expect(runner.state.webProjectPreviewPublicBaseUrl).toBe('http://127.0.0.1:22004');
} finally {
rmSync(tempDir, {recursive: true, force: true});
}
@@ -163,6 +170,7 @@ describe('dev scheduler argument routing', () => {
expect(runner.options.apiPort).toBe(8082);
expect(runner.options.spacetimePort).toBe(3101);
expect(runner.options.adminWebPort).toBe(3102);
expect(runner.options.webProjectPreviewPort).toBe(3104);
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform);
@@ -183,6 +191,42 @@ describe('dev scheduler api-server env', () => {
expect(env.GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED).toBe('true');
expect(env.GENARRATIVE_API_PORT).toBe('9091');
expect(env.GENARRATIVE_SPACETIME_SERVER_URL).toBe('http://127.0.0.1:3199');
expect(env.GENARRATIVE_WEB_PROJECT_PREVIEW_HOST).toBe('127.0.0.1');
expect(env.GENARRATIVE_WEB_PROJECT_PREVIEW_PORT).toBe('3104');
expect(env.GENARRATIVE_WEB_PROJECT_PREVIEW_PUBLIC_BASE_URL).toBe('http://127.0.0.1:3104');
expect(env.GENARRATIVE_WEB_PROJECT_PREVIEW_FRAME_ANCESTORS).toBe(
'http://127.0.0.1:3000 http://localhost:3000',
);
});
test('dev 脚本默认注入 Web Project runner 二进制路径', () => {
const {options} = parseArgs(['api-server'], {});
const env = buildApiServerProcessEnv({
baseEnv: {},
options,
state: {spacetimeServer: 'http://127.0.0.1:3101'},
});
const runnerName =
process.platform === 'win32' ? 'web-project-runner.exe' : 'web-project-runner';
expect(env.GENARRATIVE_WEB_PROJECT_RUNNER_BIN).toBe(
resolve('server-rs', 'target/debug', runnerName),
);
});
test('dev 脚本保留显式 Web Project runner 二进制路径', () => {
const {options} = parseArgs(['api-server'], {});
const env = buildApiServerProcessEnv({
baseEnv: {
GENARRATIVE_WEB_PROJECT_RUNNER_BIN: 'C:\\tools\\web-project-runner.exe',
},
options,
state: {spacetimeServer: 'http://127.0.0.1:3101'},
});
expect(env.GENARRATIVE_WEB_PROJECT_RUNNER_BIN).toBe(
'C:\\tools\\web-project-runner.exe',
);
});
});
@@ -199,6 +243,7 @@ describe('dev scheduler Rust build env', () => {
expect(env.RUSTC_WRAPPER).not.toBe('/usr/bin/sccache');
expect(env.RUSTC_WRAPPER).not.toBe('sccache');
expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBe(env.RUSTC_WRAPPER);
expect(env.RUSTC_WRAPPER).toBe(process.platform === 'win32' ? '' : '/usr/bin/env');
});
test('local dev Rust env keeps healthy custom wrapper untouched', () => {
@@ -233,6 +278,8 @@ describe('dev scheduler stack state file', () => {
webPort: 3010,
adminWebHost: '127.0.0.1',
adminWebPort: 3110,
webProjectPreviewHost: '127.0.0.1',
webProjectPreviewPort: 3112,
spacetimeHost: '127.0.0.1',
spacetimePort: 3120,
spacetimeDataDir: 'server-rs/.spacetimedb/local/data',
@@ -243,6 +290,7 @@ describe('dev scheduler stack state file', () => {
apiTarget: 'http://127.0.0.1:8090',
adminWebTargetHost: '127.0.0.1',
spacetimeServer: 'http://127.0.0.1:3120',
webProjectPreviewPublicBaseUrl: 'http://127.0.0.1:3112',
},
services: new Map([
[
@@ -285,6 +333,14 @@ describe('dev scheduler stack state file', () => {
port: 8090,
url: 'http://127.0.0.1:8090',
});
expect(snapshot.services['web-project-preview']).toMatchObject({
status: 'idle',
pid: null,
host: '127.0.0.1',
port: 3112,
url: 'http://127.0.0.1:3112',
command: 'api-server embedded Web Project preview gateway',
});
});
});
@@ -500,6 +556,85 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0;
);
});
test('发布 spacetime-module 时准备 Web Project 服务身份引导密钥', () => {
const {explicitOptions, options} = parseArgs([], {});
const runner = new DevRunner(options, {}, explicitOptions);
const env = {};
runner.prepareWebProjectServiceBootstrapSecret(env);
expect(env.GENARRATIVE_SPACETIME_WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET).toHaveLength(64);
expect(runner.state.webProjectServiceBootstrapSecretPrepared).toBe(true);
});
test('Web Project 服务身份授权使用本地 Web token 调用 procedure', async () => {
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
text: async () =>
JSON.stringify({
ok: true,
service_identity_hex: 'c200localidentity',
error_message: null,
}),
})) as unknown as typeof fetch;
await authorizeWebProjectServiceIdentity({
serverUrl: 'http://127.0.0.1:3101',
database: 'genarrative-dev',
token: 'local-web-token',
bootstrapSecret: '0123456789abcdef',
serviceIdentity: 'c200localidentity',
});
expect(globalThis.fetch).toHaveBeenCalledWith(
buildSpacetimeProcedureUrl(
'http://127.0.0.1:3101',
'genarrative-dev',
'authorize_web_project_service_identity',
),
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
Authorization: 'Bearer local-web-token',
}),
body: JSON.stringify([
{
bootstrap_secret: '0123456789abcdef',
service_identity_hex: 'c200localidentity',
note: 'local api-server web project',
},
]),
}),
);
});
test('Web Project 服务身份授权返回值兼容 BSATN 数组形态', () => {
expect(parseWebProjectServiceIdentityResult('[true,[0,"c200localidentity"],[1]]')).toEqual({
ok: true,
service_identity_hex: 'c200localidentity',
error_message: null,
});
});
test('本地 api-server SpacetimeDB identity 写入并复用 data-dir 缓存', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-'));
try {
cacheSpacetimeIdentity(tempDir, 'http://127.0.0.1:3101', {
identity: 'c200cachedidentity',
token: 'cached-token',
});
expect(readCachedSpacetimeIdentity(tempDir, 'http://127.0.0.1:3101')).toEqual({
identity: 'c200cachedidentity',
token: 'cached-token',
});
expect(readCachedSpacetimeIdentity(tempDir, 'http://127.0.0.1:3199')).toBeNull();
} finally {
rmSync(tempDir, {recursive: true, force: true});
}
});
test('手动刷新 spacetime 只重新发布模块,不重启 standalone 进程', async () => {
const {explicitOptions, options} = parseArgs([], {});
const runner = new DevRunner(options, {}, explicitOptions);
@@ -533,39 +668,55 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0;
});
test('启动 api-server 前为空 token 自动创建本地 Web identity', async () => {
const {explicitOptions, options} = parseArgs([], {
GENARRATIVE_SPACETIME_TOKEN: '',
});
const runner = new DevRunner(options, {}, explicitOptions);
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
text: async () =>
JSON.stringify({
identity: 'c200localidentity',
token: 'local-web-token',
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-'));
try {
const {explicitOptions, options} = parseArgs(
['--spacetime-data-dir', tempDir],
{
GENARRATIVE_SPACETIME_TOKEN: '',
},
);
const runner = new DevRunner(options, {}, explicitOptions);
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
text: async () =>
JSON.stringify({
identity: 'c200localidentity',
token: 'local-web-token',
}),
})) as unknown as typeof fetch;
await runner.ensureApiServerSpacetimeToken();
expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe('local-web-token');
expect(readCachedSpacetimeIdentity(tempDir, 'http://127.0.0.1:3101')).toEqual({
identity: 'c200localidentity',
token: 'local-web-token',
});
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://127.0.0.1:3101/v1/identity',
expect.objectContaining({
method: 'POST',
}),
})) as unknown as typeof fetch;
await runner.ensureApiServerSpacetimeToken();
expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe('local-web-token');
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://127.0.0.1:3101/v1/identity',
expect.objectContaining({
method: 'POST',
}),
);
);
} finally {
rmSync(tempDir, {recursive: true, force: true});
}
});
test('本地 SpacetimeDB 不信任 env 文件中的陈旧 token', async () => {
const originalToken = process.env.GENARRATIVE_SPACETIME_TOKEN;
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-'));
delete process.env.GENARRATIVE_SPACETIME_TOKEN;
try {
const {explicitOptions, options} = parseArgs([], {
GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token',
});
const {explicitOptions, options} = parseArgs(
['--spacetime-data-dir', tempDir],
{
GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token',
},
);
const runner = new DevRunner(options, {GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token'}, explicitOptions);
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
globalThis.fetch = vi.fn(async () => ({
@@ -582,6 +733,7 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0;
expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe('fresh-web-token');
} finally {
rmSync(tempDir, {recursive: true, force: true});
if (originalToken === undefined) {
delete process.env.GENARRATIVE_SPACETIME_TOKEN;
} else {

10
server-rs/Cargo.lock generated
View File

@@ -192,6 +192,7 @@ dependencies = [
"url",
"urlencoding",
"uuid",
"web-project-runner",
"webp",
"windows-sys 0.61.2",
"zip",
@@ -6104,6 +6105,15 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "web-project-runner"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "web-sys"
version = "0.3.95"

View File

@@ -47,6 +47,7 @@ members = [
"crates/spacetime-client",
"crates/spacetime-module",
"crates/tests-support",
"crates/web-project-runner",
]
[workspace.package]
@@ -92,6 +93,7 @@ shared-contracts = { path = "crates/shared-contracts", default-features = false
shared-kernel = { path = "crates/shared-kernel", default-features = false }
shared-logging = { path = "crates/shared-logging", default-features = false }
spacetime-client = { path = "crates/spacetime-client", default-features = false }
web-project-runner = { path = "crates/web-project-runner", default-features = false }
argon2 = "0.5"
aes = "0.8"

View File

@@ -56,6 +56,7 @@ shared-kernel = { workspace = true }
shared-logging = { workspace = true }
socket2 = { workspace = true }
spacetime-client = { workspace = true }
web-project-runner = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "time", "sync", "fs", "io-util", "signal", "process"] }
tokio-stream = { workspace = true }
futures-util = { workspace = true }

View File

@@ -44,6 +44,7 @@ pub fn build_router(state: AppState) -> Router {
.merge(modules::profile::router(state.clone()))
.merge(modules::assets::router(state.clone()))
.merge(modules::editor_project::router(state.clone()))
.merge(modules::web_project::router(state.clone()))
.merge(modules::platform::router(state.clone()))
.merge(modules::external_generation::router(state.clone()))
.merge(modules::play_flow::router(state.clone()))

View File

@@ -138,6 +138,13 @@ pub struct AppConfig {
pub spacetime_pool_size: u32,
pub spacetime_procedure_timeout: Duration,
pub spacetime_health_check_timeout: Duration,
pub web_project_runner_bin: Option<PathBuf>,
pub web_project_artifact_root: PathBuf,
pub web_project_preview_host: String,
pub web_project_preview_port: u16,
pub web_project_preview_public_base_url: String,
pub web_project_preview_frame_ancestors: String,
pub web_project_preview_build_max_concurrent_tasks: usize,
pub llm_provider: LlmProvider,
pub llm_base_url: String,
pub llm_api_key: Option<String>,
@@ -368,6 +375,14 @@ impl Default for AppConfig {
spacetime_health_check_timeout: Duration::from_secs(
DEFAULT_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS,
),
web_project_runner_bin: None,
web_project_artifact_root: PathBuf::from("server-rs/.data/web-project-artifacts"),
web_project_preview_host: "127.0.0.1".to_string(),
web_project_preview_port: 3104,
web_project_preview_public_base_url: "http://127.0.0.1:3104".to_string(),
web_project_preview_frame_ancestors: "http://127.0.0.1:3000 http://localhost:3000"
.to_string(),
web_project_preview_build_max_concurrent_tasks: 2,
llm_provider: LlmProvider::Ark,
llm_base_url: String::new(),
llm_api_key: None,
@@ -895,6 +910,43 @@ impl AppConfig {
config.spacetime_health_check_timeout =
Duration::from_secs(spacetime_health_check_timeout_seconds);
}
config.web_project_runner_bin =
read_first_non_empty_env(&["GENARRATIVE_WEB_PROJECT_RUNNER_BIN"]).map(PathBuf::from);
if let Some(artifact_root) =
read_first_non_empty_env(&["GENARRATIVE_WEB_PROJECT_ARTIFACT_ROOT"])
{
config.web_project_artifact_root = PathBuf::from(artifact_root);
}
if let Some(preview_host) =
read_first_non_empty_env(&["GENARRATIVE_WEB_PROJECT_PREVIEW_HOST"])
{
config.web_project_preview_host = preview_host;
}
if let Ok(preview_port) = env::var("GENARRATIVE_WEB_PROJECT_PREVIEW_PORT")
&& let Ok(parsed_port) = preview_port.parse::<u16>()
{
config.web_project_preview_port = parsed_port;
}
if let Some(preview_base_url) =
read_first_non_empty_env(&["GENARRATIVE_WEB_PROJECT_PREVIEW_PUBLIC_BASE_URL"])
{
config.web_project_preview_public_base_url = preview_base_url;
} else {
config.web_project_preview_public_base_url = format!(
"http://{}:{}",
config.web_project_preview_host, config.web_project_preview_port
);
}
if let Some(frame_ancestors) =
read_first_non_empty_env(&["GENARRATIVE_WEB_PROJECT_PREVIEW_FRAME_ANCESTORS"])
{
config.web_project_preview_frame_ancestors = frame_ancestors;
}
if let Some(max_concurrent_tasks) =
read_first_usize_env(&["GENARRATIVE_WEB_PROJECT_PREVIEW_BUILD_MAX_CONCURRENT_TASKS"])
{
config.web_project_preview_build_max_concurrent_tasks = max_concurrent_tasks;
}
if let Some(llm_provider) =
read_first_llm_provider_env(&["GENARRATIVE_LLM_PROVIDER", "LLM_PROVIDER"])
@@ -1699,6 +1751,7 @@ mod tests {
std::env::remove_var("GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS");
std::env::remove_var("GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS");
std::env::remove_var("GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS");
std::env::remove_var("GENARRATIVE_WEB_PROJECT_PREVIEW_BUILD_MAX_CONCURRENT_TASKS");
std::env::remove_var("GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_ENABLED");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_DIR");
@@ -1717,6 +1770,10 @@ mod tests {
std::env::set_var("GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS", "64");
std::env::set_var("GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS", "32");
std::env::set_var("GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS", "16");
std::env::set_var(
"GENARRATIVE_WEB_PROJECT_PREVIEW_BUILD_MAX_CONCURRENT_TASKS",
"3",
);
std::env::set_var("GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS", "3000");
std::env::set_var("GENARRATIVE_TRACKING_OUTBOX_ENABLED", "false");
std::env::set_var(
@@ -1744,6 +1801,7 @@ mod tests {
assert_eq!(config.gallery_max_concurrent_requests, Some(64));
assert_eq!(config.detail_max_concurrent_requests, Some(32));
assert_eq!(config.admin_max_concurrent_requests, Some(16));
assert_eq!(config.web_project_preview_build_max_concurrent_tasks, 3);
assert_eq!(
config.shutdown_outbox_flush_timeout,
std::time::Duration::from_millis(3_000)
@@ -1779,6 +1837,7 @@ mod tests {
std::env::remove_var("GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS");
std::env::remove_var("GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS");
std::env::remove_var("GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS");
std::env::remove_var("GENARRATIVE_WEB_PROJECT_PREVIEW_BUILD_MAX_CONCURRENT_TASKS");
std::env::remove_var("GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_ENABLED");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_DIR");

View File

@@ -9,10 +9,10 @@ use serde_json::{Value, json};
use shared_kernel::build_prefixed_uuid_id;
use spacetime_client::{
EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord, EditorAssetFolderUpdateRecordInput,
EditorAssetLibraryRecord, EditorAssetRecord, EditorAssetUpdateRecordInput, EditorCanvasRecord,
EditorCanvasViewportRecord, EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput,
EditorProjectGetRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord,
EditorAssetFolderUpdateRecordInput, EditorAssetLibraryRecord, EditorAssetRecord,
EditorAssetUpdateRecordInput, EditorCanvasRecord, EditorCanvasViewportRecord,
EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput, EditorProjectGetRecordInput,
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord, SpacetimeClientError,
};
@@ -1023,12 +1023,11 @@ fn map_editor_project_error(error: SpacetimeClientError) -> AppError {
"message": message,
}))
}
SpacetimeClientError::Runtime(message) => {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
SpacetimeClientError::Runtime(message) => AppError::from_status(StatusCode::BAD_REQUEST)
.with_details(json!({
"provider": "editor-project",
"message": message,
}))
}
})),
other => AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "spacetimedb",
"message": other.to_string(),

View File

@@ -744,6 +744,7 @@ mod tests {
started_at: Some("2026-06-03T00:00:00Z".to_string()),
completed_at: None,
updated_at: "2026-06-03T00:00:00Z".to_string(),
updated_at_micros: 1_749_165_600_000_000,
lease_token: lease_token.map(ToOwned::to_owned),
}
}

View File

@@ -94,6 +94,9 @@ mod vector_engine_audio_generation;
mod visual_novel;
mod volcengine_speech;
mod wallet_refund_outbox;
mod web_project;
mod web_project_mock_agent;
mod web_project_preview_gateway;
mod wechat;
mod wooden_fish;
mod work_author;
@@ -219,6 +222,8 @@ async fn run_http_role(config: AppConfig) -> Result<(), io::Error> {
{
Ok(state) => {
spawn_app_state_background_workers(&state);
let preview_shutdown = shutdown_signal_for_preview(state.clone());
spawn_web_project_preview_gateway(&state, preview_shutdown).await?;
let tracking_outbox = state.tracking_outbox();
let wallet_refund_outbox = state.wallet_refund_outbox();
let worker_state = process_role
@@ -275,6 +280,40 @@ async fn run_http_role(config: AppConfig) -> Result<(), io::Error> {
result
}
async fn spawn_web_project_preview_gateway(
state: &AppState,
shutdown: impl std::future::Future<Output = ()> + Send + 'static,
) -> Result<(), io::Error> {
let bind_address = format!(
"{}:{}",
state.config.web_project_preview_host, state.config.web_project_preview_port
)
.parse::<SocketAddr>()
.map_err(|error| io::Error::other(format!("Web Project preview 监听地址非法:{error}")))?;
let listener = build_tcp_listener(bind_address, state.config.listen_backlog)?;
let router = web_project_preview_gateway::router(
state.clone(),
state.config.web_project_artifact_root.clone(),
state.config.web_project_preview_frame_ancestors.clone(),
);
tokio::spawn(async move {
info!(%bind_address, "Web Project preview gateway 已启动");
if let Err(error) = axum::serve(listener, router)
.with_graceful_shutdown(shutdown)
.await
{
error!(error = %error, "Web Project preview gateway 已退出");
}
});
Ok(())
}
async fn shutdown_signal_for_preview(state: AppState) {
let signal = wait_for_shutdown_signal().await;
state.mark_not_ready();
info!(signal, "Web Project preview gateway 收到退出信号");
}
async fn shutdown_signal(context: ShutdownContext) {
let signal = wait_for_shutdown_signal().await;
if let Some(state) = context.app_state.as_ref() {

View File

@@ -20,4 +20,5 @@ pub mod puzzle_clear;
pub mod square_hole;
pub mod story;
pub mod visual_novel;
pub mod web_project;
pub mod wooden_fish;

View File

@@ -8,10 +8,10 @@ use crate::{
editor_project::{
create_editor_asset, create_editor_asset_folder, create_editor_project,
create_editor_project_resource, delete_editor_asset, delete_editor_asset_folder,
delete_editor_project, edit_editor_image, generate_editor_image,
get_editor_asset_library, get_editor_project, list_editor_projects,
load_recent_editor_project, rename_editor_project, save_editor_project_layout,
update_editor_asset, update_editor_asset_folder,
delete_editor_project, edit_editor_image, generate_editor_image, get_editor_asset_library,
get_editor_project, list_editor_projects, load_recent_editor_project,
rename_editor_project, save_editor_project_layout, update_editor_asset,
update_editor_asset_folder,
},
state::AppState,
};

View File

@@ -0,0 +1,72 @@
use axum::{
Router, middleware,
routing::{get, patch, post},
};
use crate::{
auth::require_bearer_auth,
state::AppState,
web_project::{
create_mock_agent_turn, create_web_project, create_web_project_preview_build,
get_web_project, get_web_project_preview_build, get_web_project_snapshot,
patch_web_project_files, stream_web_project_preview_build_events,
},
};
pub fn router(state: AppState) -> Router<AppState> {
Router::new()
.route(
"/api/creation/web-project/projects",
post(create_web_project).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/web-project/projects/{project_id}",
get(get_web_project).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/web-project/projects/{project_id}/snapshot",
get(get_web_project_snapshot).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/web-project/projects/{project_id}/files",
patch(patch_web_project_files).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/web-project/projects/{project_id}/mock-agent-turns",
post(create_mock_agent_turn).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/web-project/projects/{project_id}/preview-builds",
post(create_web_project_preview_build).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/web-project/preview-builds/{job_id}",
get(get_web_project_preview_build).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/web-project/preview-builds/{job_id}/events",
get(stream_web_project_preview_build_events)
.route_layer(middleware::from_fn_with_state(state, require_bearer_auth)),
)
}

View File

@@ -1,5 +1,5 @@
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
error::Error,
fmt,
sync::{
@@ -35,7 +35,7 @@ use spacetime_client::{
SpacetimeClient, SpacetimeClientConfig, SpacetimeClientError, SpacetimeClientHealthSnapshot,
};
use time::OffsetDateTime;
use tokio::sync::{Semaphore, broadcast};
use tokio::sync::{OwnedSemaphorePermit, Semaphore, broadcast};
use tracing::{info, warn};
use crate::config::AppConfig;
@@ -272,6 +272,9 @@ pub struct AppStateInner {
// creative_agent_* 表由任务 D 收口后,这里只保留读写 facade。
creative_agent_sessions: Arc<Mutex<HashMap<String, CreativeAgentSessionRuntimeRecord>>>,
profile_recharge_order_updates: broadcast::Sender<String>,
web_project_build_updates: broadcast::Sender<String>,
web_project_preview_build_limiter: Arc<Semaphore>,
web_project_preview_active_projects: Arc<Mutex<HashSet<String>>>,
#[cfg(test)]
// 测试环境允许在未启动 SpacetimeDB 时,用内存快照兜底当前 runtime story 回归链。
test_runtime_snapshot_store: Arc<Mutex<HashMap<String, RuntimeSnapshotRecord>>>,
@@ -283,6 +286,28 @@ struct CreativeAgentSessionRuntimeRecord {
snapshot: CreativeAgentSessionSnapshot,
}
#[derive(Debug)]
pub enum WebProjectPreviewBuildSlotError {
GlobalLimit,
ProjectAlreadyRunning,
}
#[derive(Debug)]
pub struct WebProjectPreviewBuildSlot {
project_key: String,
active_projects: Arc<Mutex<HashSet<String>>>,
_permit: OwnedSemaphorePermit,
}
impl Drop for WebProjectPreviewBuildSlot {
fn drop(&mut self) {
self.active_projects
.lock()
.expect("web project preview active project set should lock")
.remove(&self.project_key);
}
}
// 后台管理员运行态独立于普通玩家登录体系,只从环境变量构造。
#[derive(Clone, Debug)]
pub struct AdminRuntime {
@@ -414,6 +439,10 @@ impl AppState {
let creative_agent_gpt5_client = build_creative_agent_gpt5_client(&config)?;
let http_request_permit_pools = HttpRequestPermitPools::from_config(&config);
let (profile_recharge_order_updates, _) = broadcast::channel(128);
let (web_project_build_updates, _) = broadcast::channel(256);
let web_project_preview_build_limiter = Arc::new(Semaphore::new(
config.web_project_preview_build_max_concurrent_tasks.max(1),
));
Ok(Self(Arc::new(AppStateInner {
config,
@@ -451,6 +480,9 @@ impl AppState {
creative_agent_executor: Arc::new(MockLangChainRustAgentExecutor),
creative_agent_sessions: Arc::new(Mutex::new(HashMap::new())),
profile_recharge_order_updates,
web_project_build_updates,
web_project_preview_build_limiter,
web_project_preview_active_projects: Arc::new(Mutex::new(HashSet::new())),
#[cfg(test)]
test_runtime_snapshot_store: Arc::new(Mutex::new(HashMap::new())),
})))
@@ -919,6 +951,35 @@ impl AppState {
&self.spacetime_client
}
pub fn web_project_build_updates(&self) -> broadcast::Sender<String> {
self.web_project_build_updates.clone()
}
pub fn try_acquire_web_project_preview_build_slot(
&self,
project_id: &str,
) -> Result<WebProjectPreviewBuildSlot, WebProjectPreviewBuildSlotError> {
let project_key = project_id.to_string();
let mut active_projects = self
.web_project_preview_active_projects
.lock()
.expect("web project preview active project set should lock");
if active_projects.contains(&project_key) {
return Err(WebProjectPreviewBuildSlotError::ProjectAlreadyRunning);
}
let permit = self
.web_project_preview_build_limiter
.clone()
.try_acquire_owned()
.map_err(|_| WebProjectPreviewBuildSlotError::GlobalLimit)?;
active_projects.insert(project_key.clone());
Ok(WebProjectPreviewBuildSlot {
project_key,
active_projects: self.web_project_preview_active_projects.clone(),
_permit: permit,
})
}
pub fn puzzle_gallery_cache(&self) -> &PuzzleGalleryCache {
&self.puzzle_gallery_cache
}
@@ -1625,6 +1686,33 @@ mod tests {
assert_eq!(created.stages.len(), 4);
}
#[test]
fn web_project_preview_build_slot_enforces_global_and_project_limits() {
let mut config = AppConfig::default();
config.web_project_preview_build_max_concurrent_tasks = 1;
let state = AppState::new(config).expect("state should build");
let slot = state
.try_acquire_web_project_preview_build_slot("web-project-1")
.expect("first build should acquire slot");
assert!(matches!(
state.try_acquire_web_project_preview_build_slot("web-project-1"),
Err(WebProjectPreviewBuildSlotError::ProjectAlreadyRunning)
));
assert!(matches!(
state.try_acquire_web_project_preview_build_slot("web-project-2"),
Err(WebProjectPreviewBuildSlotError::GlobalLimit)
));
drop(slot);
assert!(
state
.try_acquire_web_project_preview_build_slot("web-project-2")
.is_ok()
);
}
#[test]
fn app_state_skips_llm_client_when_api_key_missing() {
let state = AppState::new(AppConfig::default()).expect("state should build");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,248 @@
use shared_contracts::web_project::{WebProjectPatch, WebProjectPatchOperation};
pub fn build_mock_agent_patch(prompt: &str) -> (WebProjectPatch, String) {
let normalized = prompt.trim();
let theme = if normalized.contains("绿色") {
WebProjectTheme::Green
} else if normalized.contains("粉色") {
WebProjectTheme::Pink
} else {
WebProjectTheme::Blue
};
if normalized.contains("破坏构建") {
return (
WebProjectPatch {
operations: vec![WebProjectPatchOperation::UpdateFile {
path: "src/App.tsx".to_string(),
content: broken_app_source(),
}],
},
"生成一个构建语法错误用于验证失败保留上一版预览".to_string(),
);
}
if normalized.contains("卡片") || normalized.contains("列表") {
return (
WebProjectPatch {
operations: vec![
WebProjectPatchOperation::UpdateFile {
path: "src/App.tsx".to_string(),
content: card_list_app_source(),
},
WebProjectPatchOperation::UpdateFile {
path: "src/App.css".to_string(),
content: themed_css(theme),
},
],
},
"更新为卡片列表页面".to_string(),
);
}
if normalized.contains("计数") || normalized.contains("按钮") {
return (
WebProjectPatch {
operations: vec![
WebProjectPatchOperation::UpdateFile {
path: "src/App.tsx".to_string(),
content: counter_app_source(),
},
WebProjectPatchOperation::UpdateFile {
path: "src/App.css".to_string(),
content: themed_css(theme),
},
],
},
"更新为计数按钮页面".to_string(),
);
}
(
WebProjectPatch {
operations: vec![
WebProjectPatchOperation::UpdateFile {
path: "src/App.tsx".to_string(),
content: default_app_source(normalized),
},
WebProjectPatchOperation::UpdateFile {
path: "src/App.css".to_string(),
content: themed_css(theme),
},
],
},
"更新页面标题和主题色".to_string(),
)
}
enum WebProjectTheme {
Blue,
Green,
Pink,
}
fn counter_app_source() -> String {
r#"import { useState } from 'react';
import './App.css';
export default function App() {
const [count, setCount] = useState(0);
return (
<main className="app-shell">
<section className="hero-panel">
<p className="eyebrow">Mock Agent</p>
<h1>蓝色计数按钮</h1>
<button className="primary-action" onClick={() => setCount((value) => value + 1)}>
已点击 {count} 次
</button>
</section>
</main>
);
}
"#
.to_string()
}
fn card_list_app_source() -> String {
r#"import './App.css';
const cards = ['构思', '搭建', '预览'];
export default function App() {
return (
<main className="app-shell">
<section className="hero-panel">
<p className="eyebrow">Mock Agent</p>
<h1>项目卡片列表</h1>
<div className="card-grid">
{cards.map((item) => (
<article className="feature-card" key={item}>{item}</article>
))}
</div>
</section>
</main>
);
}
"#
.to_string()
}
fn broken_app_source() -> String {
r#"import './App.css';
export default function App() {
return <main className="app-shell">破坏构建</main
}
"#
.to_string()
}
fn default_app_source(prompt: &str) -> String {
let title = prompt
.chars()
.take(30)
.collect::<String>()
.replace(['<', '>', '{', '}'], "");
format!(
r#"import './App.css';
export default function App() {{
return (
<main className="app-shell">
<section className="hero-panel">
<p className="eyebrow">Mock Agent</p>
<h1>{}</h1>
<p className="summary">页面已根据本轮指令更新。</p>
</section>
</main>
);
}}
"#,
if title.trim().is_empty() {
"Web 工程预览"
} else {
title.trim()
}
)
}
fn themed_css(theme: WebProjectTheme) -> String {
let (accent, surface) = match theme {
WebProjectTheme::Blue => ("#2563eb", "#eff6ff"),
WebProjectTheme::Green => ("#059669", "#ecfdf5"),
WebProjectTheme::Pink => ("#db2777", "#fdf2f8"),
};
format!(
r#":root {{
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #111827;
background: {surface};
}}
body {{
margin: 0;
}}
.app-shell {{
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
background: linear-gradient(135deg, {surface}, #ffffff);
}}
.hero-panel {{
width: min(520px, 100%);
border: 1px solid rgba(17, 24, 39, 0.12);
border-radius: 8px;
background: rgba(255, 255, 255, 0.9);
padding: 28px;
box-shadow: 0 16px 40px rgba(17, 24, 39, 0.12);
}}
.eyebrow {{
margin: 0 0 8px;
color: {accent};
font-size: 13px;
font-weight: 700;
}}
h1 {{
margin: 0 0 18px;
font-size: 32px;
line-height: 1.15;
}}
.summary {{
margin: 0;
color: #4b5563;
}}
.primary-action {{
border: 0;
border-radius: 8px;
background: {accent};
color: white;
padding: 12px 18px;
font: inherit;
font-weight: 700;
cursor: pointer;
}}
.card-grid {{
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}}
.feature-card {{
border-radius: 8px;
background: {surface};
border: 1px solid rgba(17, 24, 39, 0.08);
padding: 16px;
font-weight: 700;
}}
"#
)
}

View File

@@ -0,0 +1,283 @@
use std::{
path::{Component, Path, PathBuf},
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use axum::{
Router,
body::Body,
extract::{Path as AxumPath, State},
http::{
HeaderValue, StatusCode,
header::{
ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_SECURITY_POLICY, CONTENT_TYPE,
},
},
response::{IntoResponse, Response},
routing::get,
};
use tokio::fs;
use crate::state::AppState;
const PREVIEW_TOKEN_MAX_AGE_MICROS: i64 = 24 * 60 * 60 * 1_000_000;
#[derive(Clone, Debug)]
struct PreviewGatewayState {
app_state: AppState,
artifact_root: Arc<PathBuf>,
frame_ancestors: Arc<String>,
}
pub fn router(app_state: AppState, artifact_root: PathBuf, frame_ancestors: String) -> Router {
Router::new()
.route("/p/{preview_token_id}", get(serve_preview_index))
.route("/p/{preview_token_id}/", get(serve_preview_index))
.route(
"/p/{preview_token_id}/{*asset_path}",
get(serve_preview_asset),
)
.with_state(PreviewGatewayState {
app_state,
artifact_root: Arc::new(artifact_root),
frame_ancestors: Arc::new(normalize_frame_ancestors(&frame_ancestors)),
})
}
async fn serve_preview_index(
State(state): State<PreviewGatewayState>,
AxumPath(preview_token_id): AxumPath<String>,
) -> Response {
serve_preview_file(state, preview_token_id, "index.html".to_string()).await
}
async fn serve_preview_asset(
State(state): State<PreviewGatewayState>,
AxumPath((preview_token_id, asset_path)): AxumPath<(String, String)>,
) -> Response {
let asset_path = if asset_path.trim().is_empty() {
"index.html".to_string()
} else {
asset_path
};
serve_preview_file(state, preview_token_id, asset_path).await
}
async fn serve_preview_file(
state: PreviewGatewayState,
preview_token_id: String,
asset_path: String,
) -> Response {
if !preview_token_is_fresh(&preview_token_id) {
return gateway_error(StatusCode::GONE, "预览令牌已失效");
}
let build = match state
.app_state
.spacetime_client()
.get_web_project_preview_build_by_token(
spacetime_client::WebProjectPreviewBuildTokenGetRecordInput {
preview_token_id: preview_token_id.clone(),
},
)
.await
{
Ok(record) => record.build,
Err(_) => return gateway_error(StatusCode::GONE, "预览令牌已失效"),
};
let build = if build.preview_token_id.as_deref() == Some(preview_token_id.as_str())
&& build.status == "succeeded"
{
build
} else {
return gateway_error(StatusCode::GONE, "预览令牌已失效");
};
let Some(artifact_id) = build.artifact_id else {
return gateway_error(StatusCode::GONE, "预览产物不存在");
};
let artifact_root = match fs::canonicalize(state.artifact_root.as_ref()).await {
Ok(path) => path,
Err(_) => return gateway_error(StatusCode::GONE, "预览产物不存在"),
};
let artifact_dir = match fs::canonicalize(artifact_root.join(artifact_id)).await {
Ok(path) if path.starts_with(&artifact_root) => path,
_ => return gateway_error(StatusCode::GONE, "预览产物不存在"),
};
let requested_file = match resolve_artifact_path(&artifact_dir, &asset_path) {
Ok(path) => path,
Err(response) => return response,
};
let file_path = if fs::metadata(&requested_file)
.await
.map(|metadata| metadata.is_file())
.unwrap_or(false)
{
requested_file
} else {
artifact_dir.join("index.html")
};
let file_path = match fs::canonicalize(file_path).await {
Ok(path) if path.starts_with(&artifact_dir) && path.starts_with(&artifact_root) => path,
_ => return gateway_error(StatusCode::FORBIDDEN, "预览路径不合法"),
};
let Some(mime_type) = mime_type_for_path(&file_path) else {
return gateway_error(StatusCode::UNSUPPORTED_MEDIA_TYPE, "预览文件类型不支持");
};
match fs::read(&file_path).await {
Ok(bytes) => preview_response(bytes, mime_type, state.frame_ancestors.as_str()),
Err(_) => gateway_error(StatusCode::NOT_FOUND, "预览文件不存在"),
}
}
fn preview_token_is_fresh(preview_token_id: &str) -> bool {
let Some(issued_at_micros) = parse_preview_token_issued_at(preview_token_id) else {
return false;
};
let now = current_utc_micros();
issued_at_micros <= now && now.saturating_sub(issued_at_micros) <= PREVIEW_TOKEN_MAX_AGE_MICROS
}
fn parse_preview_token_issued_at(preview_token_id: &str) -> Option<i64> {
let mut parts = preview_token_id.split('_');
match (parts.next(), parts.next(), parts.next(), parts.next()) {
(Some("wpt"), Some(issued_at), Some(secret), None) if secret.len() >= 32 => {
issued_at.parse::<i64>().ok()
}
_ => None,
}
}
fn current_utc_micros() -> i64 {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should be after unix epoch");
i64::try_from(duration.as_micros()).expect("current unix micros should fit in i64")
}
fn resolve_artifact_path(artifact_dir: &Path, asset_path: &str) -> Result<PathBuf, Response> {
let mut relative = PathBuf::new();
for component in Path::new(asset_path).components() {
match component {
Component::Normal(segment) => relative.push(segment),
Component::CurDir => {}
_ => return Err(gateway_error(StatusCode::FORBIDDEN, "预览路径不合法")),
}
}
Ok(artifact_dir.join(relative))
}
fn preview_response(bytes: Vec<u8>, mime_type: &'static str, frame_ancestors: &str) -> Response {
let mut response = Response::new(Body::from(bytes));
*response.status_mut() = StatusCode::OK;
let headers = response.headers_mut();
headers.insert(CONTENT_TYPE, HeaderValue::from_static(mime_type));
let csp = format!(
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'none'; worker-src 'none'; frame-ancestors {}; object-src 'none'; base-uri 'none'",
normalize_frame_ancestors(frame_ancestors)
);
if let Ok(value) = HeaderValue::from_str(&csp) {
headers.insert(CONTENT_SECURITY_POLICY, value);
}
headers.insert(
CACHE_CONTROL,
HeaderValue::from_static("no-store, max-age=0, must-revalidate"),
);
headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
response
}
fn normalize_frame_ancestors(input: &str) -> String {
let values = input
.split_whitespace()
.filter(|value| {
*value == "'self'"
|| (value.starts_with("http://") || value.starts_with("https://"))
&& !value.contains(';')
&& !value.contains(',')
})
.collect::<Vec<_>>();
if values.is_empty() {
"'self'".to_string()
} else {
values.join(" ")
}
}
fn gateway_error(status: StatusCode, message: &'static str) -> Response {
(status, message).into_response()
}
fn mime_type_for_path(path: &Path) -> Option<&'static str> {
match path
.extension()
.and_then(|extension| extension.to_str())
.unwrap_or("")
.to_ascii_lowercase()
.as_str()
{
"html" => Some("text/html; charset=utf-8"),
"js" | "mjs" => Some("text/javascript; charset=utf-8"),
"css" => Some("text/css; charset=utf-8"),
"json" => Some("application/json; charset=utf-8"),
"svg" => Some("image/svg+xml"),
"png" => Some("image/png"),
"jpg" | "jpeg" => Some("image/jpeg"),
"webp" => Some("image/webp"),
"ico" => Some("image/x-icon"),
"woff" => Some("font/woff"),
"woff2" => Some("font/woff2"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preview_csp_allows_configured_editor_origin() {
let response = preview_response(
b"<html></html>".to_vec(),
"text/html; charset=utf-8",
"http://127.0.0.1:3000",
);
let csp = response
.headers()
.get(CONTENT_SECURITY_POLICY)
.expect("preview response should include csp")
.to_str()
.expect("csp should be ascii");
assert!(csp.contains("frame-ancestors http://127.0.0.1:3000"));
assert!(!csp.contains("frame-ancestors 'none'"));
assert!(csp.contains("connect-src 'none'"));
}
#[test]
fn preview_response_allows_sandboxed_module_assets() {
let response = preview_response(
b"export default 1;".to_vec(),
"text/javascript; charset=utf-8",
"http://127.0.0.1:3000",
);
assert_eq!(
response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN),
Some(&HeaderValue::from_static("*"))
);
let csp = response
.headers()
.get(CONTENT_SECURITY_POLICY)
.expect("preview response should include csp")
.to_str()
.expect("csp should be ascii");
assert!(csp.contains("connect-src 'none'"));
}
#[test]
fn frame_ancestors_falls_back_to_self_when_env_is_invalid() {
assert_eq!(
normalize_frame_ancestors("bad; https://ok.example"),
"https://ok.example"
);
assert_eq!(normalize_frame_ancestors("bad;"), "'self'");
}
}

View File

@@ -253,15 +253,28 @@ fn remove_generated_asset_sheet_green_screen_background(
let x = pixel_index % width;
let y = pixel_index / width;
if x == 0 || x == width.saturating_sub(1) || y == 0 || y == height.saturating_sub(1) {
if x == 0 || x == width.saturating_sub(1) || y == 0 || y == height.saturating_sub(1)
{
touches_border = true;
}
let neighbors = [
if x > 0 { Some(pixel_index - 1) } else { None },
if x + 1 < width { Some(pixel_index + 1) } else { None },
if y > 0 { Some(pixel_index - width) } else { None },
if y + 1 < height { Some(pixel_index + width) } else { None },
if x + 1 < width {
Some(pixel_index + 1)
} else {
None
},
if y > 0 {
Some(pixel_index - width)
} else {
None
},
if y + 1 < height {
Some(pixel_index + width)
} else {
None
},
];
for next in neighbors.into_iter().flatten() {

View File

@@ -32,4 +32,5 @@ pub mod square_hole_runtime;
pub mod square_hole_works;
pub mod story;
pub mod visual_novel;
pub mod web_project;
pub mod wooden_fish;

View File

@@ -0,0 +1,139 @@
use serde::{Deserialize, Serialize};
pub const WEB_PROJECT_TEMPLATE_REACT_VITE_TS_STATIC: &str = "react-vite-ts-static";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WebProjectPreviewBuildStatus {
Queued,
Running,
Succeeded,
Failed,
Cancelled,
Expired,
Stale,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebProject {
pub project_id: String,
pub owner_user_id: String,
pub title: String,
pub template_key: String,
pub active_snapshot_id: String,
pub active_preview_build_id: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebProjectFile {
/// P1 只接受相对路径,拒绝 `..`、敏感文件和固定模板控制文件。
pub path: String,
/// P1 只保存 UTF-8 文本源码或受控静态文本资源,单文件大小由后端校验限制。
pub content: String,
pub media_type: String,
pub encoding: String,
/// 后端按 UTF-8 字节数计算,参与 snapshot 总大小限制。
pub size_bytes: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebProjectSnapshot {
pub snapshot_id: String,
pub project_id: String,
pub owner_user_id: String,
pub parent_snapshot_id: Option<String>,
pub template_key: String,
pub files: Vec<WebProjectFile>,
pub patch_summary: String,
pub created_by: String,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebProjectPatch {
/// 所有 patch 操作必须先经过后端统一校验,再生成新 snapshot。
pub operations: Vec<WebProjectPatchOperation>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum WebProjectPatchOperation {
/// 仅允许创建 P1 可编辑范围内的文件,不能创建依赖、脚本或模板控制文件。
CreateFile { path: String, content: String },
/// 仅允许更新 P1 可编辑范围内的文件。
UpdateFile { path: String, content: String },
/// 删除同样受路径白名单约束,避免绕过固定模板边界。
DeleteFile { path: String },
/// 源路径和目标路径都必须通过 P1 可编辑路径校验。
RenameFile { from_path: String, to_path: String },
/// `package.json` 不是依赖事实源P1 只校验并拒绝 scripts / dependencies 等供应链字段。
PackageManifestRequest { content: String },
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebProjectPreviewBuild {
pub job_id: String,
pub project_id: String,
pub snapshot_id: String,
pub owner_user_id: String,
pub status: WebProjectPreviewBuildStatus,
pub logs: Vec<String>,
pub artifact_id: Option<String>,
/// 后端保存的不透明预览票据;前端只消费 previewUrl不拼接或解析 token。
pub preview_token_id: Option<String>,
pub preview_url: Option<String>,
pub error_summary: Option<String>,
pub created_at: String,
pub started_at: Option<String>,
pub finished_at: Option<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebProjectPreviewBuildEvent {
pub job_id: String,
pub status: WebProjectPreviewBuildStatus,
pub message: Option<String>,
pub build: Option<WebProjectPreviewBuild>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MockAgentTurnRequest {
pub prompt: String,
pub base_snapshot_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MockAgentTurnResponse {
pub snapshot: WebProjectSnapshot,
pub patch: WebProjectPatch,
pub summary: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebProjectResponse {
pub project: WebProject,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebProjectSnapshotResponse {
pub snapshot: WebProjectSnapshot,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebProjectPreviewBuildResponse {
pub build: WebProjectPreviewBuild,
}

View File

@@ -57,14 +57,15 @@ impl SpacetimeClient {
self.call_after_connect(
"get_editor_project_and_return",
move |connection, sender| {
connection
.procedures()
.get_editor_project_and_return_then(procedure_input, move |_, result| {
connection.procedures().get_editor_project_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_required_procedure_result);
send_once(&sender, mapped);
});
},
);
},
)
.await
@@ -79,15 +80,14 @@ impl SpacetimeClient {
self.call_after_connect(
"list_editor_projects_and_return",
move |connection, sender| {
connection.procedures().list_editor_projects_and_return_then(
procedure_input,
move |_, result| {
connection
.procedures()
.list_editor_projects_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_list_procedure_result);
send_once(&sender, mapped);
},
);
});
},
)
.await
@@ -102,15 +102,14 @@ impl SpacetimeClient {
self.call_after_connect(
"rename_editor_project_and_return",
move |connection, sender| {
connection.procedures().rename_editor_project_and_return_then(
procedure_input,
move |_, result| {
connection
.procedures()
.rename_editor_project_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_required_procedure_result);
send_once(&sender, mapped);
},
);
});
},
)
.await
@@ -125,15 +124,14 @@ impl SpacetimeClient {
self.call_after_connect(
"delete_editor_project_and_return",
move |connection, sender| {
connection.procedures().delete_editor_project_and_return_then(
procedure_input,
move |_, result| {
connection
.procedures()
.delete_editor_project_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_delete_procedure_result);
send_once(&sender, mapped);
},
);
});
},
)
.await
@@ -204,15 +202,12 @@ impl SpacetimeClient {
move |connection, sender| {
connection
.procedures()
.get_editor_asset_library_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_library_procedure_result);
send_once(&sender, mapped);
},
);
.get_editor_asset_library_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_library_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
@@ -302,14 +297,15 @@ impl SpacetimeClient {
self.call_after_connect(
"create_editor_asset_and_return",
move |connection, sender| {
connection
.procedures()
.create_editor_asset_and_return_then(procedure_input, move |_, result| {
connection.procedures().create_editor_asset_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_procedure_result);
send_once(&sender, mapped);
});
},
);
},
)
.await
@@ -324,14 +320,15 @@ impl SpacetimeClient {
self.call_after_connect(
"update_editor_asset_and_return",
move |connection, sender| {
connection
.procedures()
.update_editor_asset_and_return_then(procedure_input, move |_, result| {
connection.procedures().update_editor_asset_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_procedure_result);
send_once(&sender, mapped);
});
},
);
},
)
.await
@@ -346,14 +343,15 @@ impl SpacetimeClient {
self.call_after_connect(
"delete_editor_asset_and_return",
move |connection, sender| {
connection
.procedures()
.delete_editor_asset_and_return_then(procedure_input, move |_, result| {
connection.procedures().delete_editor_asset_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_procedure_result);
send_once(&sender, mapped);
});
},
);
},
)
.await

View File

@@ -37,8 +37,7 @@ pub use mapper::{
EditorCanvasViewportRecord, EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput,
EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput, EditorProjectRecord,
EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput,
EditorProjectResourceRecord,
ExternalGenerationJobClaimRecordInput,
EditorProjectResourceRecord, ExternalGenerationJobClaimRecordInput,
ExternalGenerationJobCompleteRecordInput, ExternalGenerationJobEnqueueRecordInput,
ExternalGenerationJobFailRecordInput, ExternalGenerationJobGetRecordInput,
ExternalGenerationJobRecord, ExternalGenerationJobRenewLeaseRecordInput,
@@ -48,7 +47,6 @@ pub use mapper::{
JumpHopGenerationStatus, JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult,
JumpHopLastJump, JumpHopPath, JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse,
JumpHopRunStatus, JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse,
JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset,
JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse,
JumpHopWorkProfileResponse, JumpHopWorkSummaryResponse, JumpHopWorksResponse,
@@ -103,11 +101,17 @@ pub use mapper::{
VisualNovelHistoryEntryRecordInput, VisualNovelRunRecord, VisualNovelRunSnapshotRecordInput,
VisualNovelRunStartRecordInput, VisualNovelRuntimeEventRecord,
VisualNovelRuntimeEventRecordInput, VisualNovelWorkCompileRecordInput,
VisualNovelWorkProfileRecord, VisualNovelWorkUpdateRecordInput, WoodenFishActionRequest,
WoodenFishActionResponse, WoodenFishActionType, WoodenFishAudioAsset,
WoodenFishCheckpointRunRequest, WoodenFishDraftResponse, WoodenFishFinishRunRequest,
WoodenFishGalleryCardResponse, WoodenFishGalleryDetailResponse, WoodenFishGalleryResponse,
WoodenFishGenerationStatus, WoodenFishImageAsset, WoodenFishRunResponse, WoodenFishRunStatus,
VisualNovelWorkProfileRecord, VisualNovelWorkUpdateRecordInput, WebProjectCreateRecordInput,
WebProjectFileRecord, WebProjectGetRecordInput, WebProjectPreviewBuildCreateRecordInput,
WebProjectPreviewBuildGetRecordInput, WebProjectPreviewBuildMutationRecord,
WebProjectPreviewBuildRecord, WebProjectPreviewBuildTokenGetRecordInput,
WebProjectPreviewBuildUpdateRecordInput, WebProjectRecord, WebProjectSnapshotGetRecordInput,
WebProjectSnapshotMutationRecord, WebProjectSnapshotRecord, WebProjectSnapshotSaveRecordInput,
WoodenFishActionRequest, WoodenFishActionResponse,
WoodenFishActionType, WoodenFishAudioAsset, WoodenFishCheckpointRunRequest,
WoodenFishDraftResponse, WoodenFishFinishRunRequest, WoodenFishGalleryCardResponse,
WoodenFishGalleryDetailResponse, WoodenFishGalleryResponse, WoodenFishGenerationStatus,
WoodenFishImageAsset, WoodenFishRunResponse, WoodenFishRunStatus,
WoodenFishRuntimeRunSnapshotResponse, WoodenFishSessionResponse,
WoodenFishSessionSnapshotResponse, WoodenFishStartRunRequest, WoodenFishWordCounter,
WoodenFishWorkDetailResponse, WoodenFishWorkMutationResponse, WoodenFishWorkProfileResponse,
@@ -141,6 +145,7 @@ pub mod square_hole;
pub mod story;
pub mod story_runtime;
pub mod visual_novel;
pub mod web_project;
pub mod wooden_fish;
use std::{

View File

@@ -23,6 +23,7 @@ mod runtime_profile;
mod square_hole;
mod story;
mod visual_novel;
mod web_project;
mod wooden_fish;
pub use self::ai::{
@@ -42,15 +43,6 @@ pub use self::combat::{
BarkBattleDraftConfigRecord, BarkBattleRunRecord, BarkBattleRuntimeConfigRecord,
ResolveCombatActionRecord,
};
pub use self::editor_project::{
EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord, EditorAssetFolderUpdateRecordInput,
EditorAssetLibraryRecord, EditorAssetRecord, EditorAssetUpdateRecordInput, EditorCanvasRecord,
EditorCanvasViewportRecord, EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput,
EditorProjectGetRecordInput,
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord,
};
pub use self::common::{
BigFishAgentMessageRecord, BigFishAnchorItemRecord, BigFishAnchorPackRecord,
BigFishBackgroundBlueprintRecord, BigFishDraftCompileRecordInput,
@@ -80,6 +72,15 @@ pub use self::common::{
VisualNovelRunSnapshotRecordInput, VisualNovelRunStartRecordInput,
VisualNovelWorkCompileRecordInput,
};
pub use self::editor_project::{
EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord,
EditorAssetFolderUpdateRecordInput, EditorAssetLibraryRecord, EditorAssetRecord,
EditorAssetUpdateRecordInput, EditorCanvasRecord, EditorCanvasViewportRecord,
EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput, EditorProjectGetRecordInput,
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord,
};
pub use self::external_generation::{
ExternalGenerationJobClaimRecordInput, ExternalGenerationJobCompleteRecordInput,
ExternalGenerationJobEnqueueRecordInput, ExternalGenerationJobFailRecordInput,
@@ -153,6 +154,14 @@ pub use self::runtime_profile::{
SquareHoleDropConfirmationRecord, SquareHoleDropFeedbackRecord, SquareHoleRunRecord,
};
pub use self::story::{VisualNovelRuntimeEventRecord, VisualNovelRuntimeEventRecordInput};
pub use self::web_project::{
WebProjectCreateRecordInput, WebProjectFileRecord, WebProjectGetRecordInput,
WebProjectPreviewBuildCreateRecordInput, WebProjectPreviewBuildGetRecordInput,
WebProjectPreviewBuildMutationRecord, WebProjectPreviewBuildRecord,
WebProjectPreviewBuildTokenGetRecordInput, WebProjectPreviewBuildUpdateRecordInput,
WebProjectRecord, WebProjectSnapshotGetRecordInput, WebProjectSnapshotMutationRecord,
WebProjectSnapshotRecord, WebProjectSnapshotSaveRecordInput,
};
pub use self::wooden_fish::{
WoodenFishActionRequest, WoodenFishActionResponse, WoodenFishActionType, WoodenFishAudioAsset,
WoodenFishCheckpointRunRequest, WoodenFishDraftResponse, WoodenFishFinishRunRequest,
@@ -197,11 +206,11 @@ pub(crate) use self::custom_world::{
parse_rpg_agent_stage_record,
};
pub(crate) use self::editor_project::{
map_editor_asset_folder_library_procedure_result,
map_editor_asset_folder_procedure_result, map_editor_asset_library_procedure_result,
map_editor_asset_procedure_result, map_editor_project_delete_procedure_result,
map_editor_project_list_procedure_result, map_editor_project_optional_procedure_result,
map_editor_project_required_procedure_result, map_editor_project_resource_procedure_result,
map_editor_asset_folder_library_procedure_result, map_editor_asset_folder_procedure_result,
map_editor_asset_library_procedure_result, map_editor_asset_procedure_result,
map_editor_project_delete_procedure_result, map_editor_project_list_procedure_result,
map_editor_project_optional_procedure_result, map_editor_project_required_procedure_result,
map_editor_project_resource_procedure_result,
};
pub(crate) use self::external_generation::{
map_external_generation_job_claim_result, map_external_generation_job_procedure_result,
@@ -289,6 +298,10 @@ pub(crate) use self::visual_novel::{
map_visual_novel_runtime_event_procedure_result, map_visual_novel_work_procedure_result,
map_visual_novel_works_procedure_result,
};
pub(crate) use self::web_project::{
map_web_project_preview_build_procedure_result, map_web_project_procedure_result,
map_web_project_snapshot_procedure_result,
};
pub(crate) use self::wooden_fish::{
map_wooden_fish_agent_session_procedure_result, map_wooden_fish_gallery_card_view_row,
map_wooden_fish_run_procedure_result, map_wooden_fish_work_procedure_result,

View File

@@ -486,8 +486,10 @@ fn map_editor_project_snapshot(
) -> Result<EditorProjectRecord, SpacetimeClientError> {
let layers: serde_json::Value =
serde_json::from_str(&snapshot.canvas.layers_json).map_err(|error| {
SpacetimeClientError::validation_failed(format!("图片画布图层布局 JSON 无法解析:{error}"))
})?;
SpacetimeClientError::validation_failed(format!(
"图片画布图层布局 JSON 无法解析:{error}"
))
})?;
let viewport = EditorCanvasViewportRecord {
x: snapshot.canvas.viewport.x,
y: snapshot.canvas.viewport.y,

View File

@@ -0,0 +1,383 @@
use super::*;
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct WebProjectFileRecord {
pub path: String,
pub content: String,
pub media_type: String,
pub encoding: String,
pub size_bytes: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct WebProjectRecord {
pub project_id: String,
pub owner_user_id: String,
pub title: String,
pub template_key: String,
pub active_snapshot_id: String,
pub active_preview_build_id: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct WebProjectSnapshotRecord {
pub snapshot_id: String,
pub project_id: String,
pub owner_user_id: String,
pub parent_snapshot_id: Option<String>,
pub template_key: String,
pub files: Vec<WebProjectFileRecord>,
pub patch_summary: String,
pub created_by: String,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct WebProjectPreviewBuildRecord {
pub job_id: String,
pub project_id: String,
pub snapshot_id: String,
pub owner_user_id: String,
pub status: String,
pub logs: Vec<String>,
pub artifact_id: Option<String>,
pub preview_token_id: Option<String>,
pub preview_url: Option<String>,
pub error_summary: Option<String>,
pub created_at: String,
pub started_at: Option<String>,
pub finished_at: Option<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WebProjectCreateRecordInput {
pub project_id: String,
pub snapshot_id: String,
pub owner_user_id: String,
pub title: String,
pub initial_files: Vec<WebProjectFileRecord>,
pub now_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WebProjectGetRecordInput {
pub project_id: String,
pub owner_user_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WebProjectSnapshotGetRecordInput {
pub project_id: String,
pub snapshot_id: Option<String>,
pub owner_user_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WebProjectSnapshotSaveRecordInput {
pub snapshot_id: String,
pub project_id: String,
pub owner_user_id: String,
pub parent_snapshot_id: Option<String>,
pub files: Vec<WebProjectFileRecord>,
pub patch_summary: String,
pub created_by: String,
pub now_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WebProjectPreviewBuildCreateRecordInput {
pub job_id: String,
pub project_id: String,
pub snapshot_id: String,
pub owner_user_id: String,
pub now_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WebProjectPreviewBuildGetRecordInput {
pub job_id: String,
pub owner_user_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WebProjectPreviewBuildTokenGetRecordInput {
pub preview_token_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WebProjectPreviewBuildUpdateRecordInput {
pub job_id: String,
pub owner_user_id: String,
pub status: String,
pub logs: Vec<String>,
pub artifact_id: Option<String>,
pub preview_token_id: Option<String>,
pub preview_url: Option<String>,
pub error_summary: Option<String>,
pub started_at_micros: Option<i64>,
pub finished_at_micros: Option<i64>,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct WebProjectSnapshotMutationRecord {
pub project: WebProjectRecord,
pub snapshot: WebProjectSnapshotRecord,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct WebProjectPreviewBuildMutationRecord {
pub project: WebProjectRecord,
pub build: WebProjectPreviewBuildRecord,
}
impl From<WebProjectFileRecord> for crate::module_bindings::WebProjectFileSnapshot {
fn from(input: WebProjectFileRecord) -> Self {
Self {
path: input.path,
content: input.content,
media_type: input.media_type,
encoding: input.encoding,
size_bytes: input.size_bytes,
}
}
}
impl From<crate::module_bindings::WebProjectFileSnapshot> for WebProjectFileRecord {
fn from(snapshot: crate::module_bindings::WebProjectFileSnapshot) -> Self {
Self {
path: snapshot.path,
content: snapshot.content,
media_type: snapshot.media_type,
encoding: snapshot.encoding,
size_bytes: snapshot.size_bytes,
}
}
}
impl From<WebProjectCreateRecordInput> for crate::module_bindings::WebProjectCreateInput {
fn from(input: WebProjectCreateRecordInput) -> Self {
Self {
project_id: input.project_id,
snapshot_id: input.snapshot_id,
owner_user_id: input.owner_user_id,
title: input.title,
initial_files: input.initial_files.into_iter().map(Into::into).collect(),
now_micros: input.now_micros,
}
}
}
impl From<WebProjectGetRecordInput> for crate::module_bindings::WebProjectGetInput {
fn from(input: WebProjectGetRecordInput) -> Self {
Self {
project_id: input.project_id,
owner_user_id: input.owner_user_id,
}
}
}
impl From<WebProjectSnapshotGetRecordInput> for crate::module_bindings::WebProjectSnapshotGetInput {
fn from(input: WebProjectSnapshotGetRecordInput) -> Self {
Self {
project_id: input.project_id,
snapshot_id: input.snapshot_id,
owner_user_id: input.owner_user_id,
}
}
}
impl From<WebProjectSnapshotSaveRecordInput>
for crate::module_bindings::WebProjectSnapshotSaveInput
{
fn from(input: WebProjectSnapshotSaveRecordInput) -> Self {
Self {
snapshot_id: input.snapshot_id,
project_id: input.project_id,
owner_user_id: input.owner_user_id,
parent_snapshot_id: input.parent_snapshot_id,
files: input.files.into_iter().map(Into::into).collect(),
patch_summary: input.patch_summary,
created_by: input.created_by,
now_micros: input.now_micros,
}
}
}
impl From<WebProjectPreviewBuildCreateRecordInput>
for crate::module_bindings::WebProjectPreviewBuildCreateInput
{
fn from(input: WebProjectPreviewBuildCreateRecordInput) -> Self {
Self {
job_id: input.job_id,
project_id: input.project_id,
snapshot_id: input.snapshot_id,
owner_user_id: input.owner_user_id,
now_micros: input.now_micros,
}
}
}
impl From<WebProjectPreviewBuildGetRecordInput>
for crate::module_bindings::WebProjectPreviewBuildGetInput
{
fn from(input: WebProjectPreviewBuildGetRecordInput) -> Self {
Self {
job_id: input.job_id,
owner_user_id: input.owner_user_id,
}
}
}
impl From<WebProjectPreviewBuildTokenGetRecordInput>
for crate::module_bindings::WebProjectPreviewBuildTokenGetInput
{
fn from(input: WebProjectPreviewBuildTokenGetRecordInput) -> Self {
Self {
preview_token_id: input.preview_token_id,
}
}
}
impl From<WebProjectPreviewBuildUpdateRecordInput>
for crate::module_bindings::WebProjectPreviewBuildUpdateInput
{
fn from(input: WebProjectPreviewBuildUpdateRecordInput) -> Self {
Self {
job_id: input.job_id,
owner_user_id: input.owner_user_id,
status: input.status,
logs: input.logs,
artifact_id: input.artifact_id,
preview_token_id: input.preview_token_id,
preview_url: input.preview_url,
error_summary: input.error_summary,
started_at_micros: input.started_at_micros,
finished_at_micros: input.finished_at_micros,
updated_at_micros: input.updated_at_micros,
}
}
}
pub(crate) fn map_web_project_procedure_result(
result: WebProjectProcedureResult,
) -> Result<WebProjectRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.project
.map(map_web_project_snapshot)
.ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程"))
}
pub(crate) fn map_web_project_snapshot_procedure_result(
result: WebProjectSnapshotProcedureResult,
) -> Result<WebProjectSnapshotMutationRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let project = result
.project
.map(map_web_project_snapshot)
.ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程"))?;
let snapshot = result
.snapshot
.map(map_web_project_source_snapshot)
.ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程快照"))?;
Ok(WebProjectSnapshotMutationRecord { project, snapshot })
}
pub(crate) fn map_web_project_preview_build_procedure_result(
result: WebProjectPreviewBuildProcedureResult,
) -> Result<WebProjectPreviewBuildMutationRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let project = result
.project
.map(map_web_project_snapshot)
.ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程"))?;
let build = result
.build
.map(map_web_project_preview_build_snapshot)
.ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程预览构建"))?;
Ok(WebProjectPreviewBuildMutationRecord { project, build })
}
fn map_web_project_snapshot(snapshot: WebProjectProjectSnapshot) -> WebProjectRecord {
WebProjectRecord {
project_id: snapshot.project_id,
owner_user_id: snapshot.owner_user_id,
title: snapshot.title,
template_key: snapshot.template_key,
active_snapshot_id: snapshot.active_snapshot_id,
active_preview_build_id: snapshot.active_preview_build_id,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn map_web_project_source_snapshot(snapshot: WebProjectSnapshot) -> WebProjectSnapshotRecord {
WebProjectSnapshotRecord {
snapshot_id: snapshot.snapshot_id,
project_id: snapshot.project_id,
owner_user_id: snapshot.owner_user_id,
parent_snapshot_id: snapshot.parent_snapshot_id,
template_key: snapshot.template_key,
files: snapshot.files.into_iter().map(Into::into).collect(),
patch_summary: snapshot.patch_summary,
created_by: snapshot.created_by,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
fn map_web_project_preview_build_snapshot(
snapshot: WebProjectPreviewBuildSnapshot,
) -> WebProjectPreviewBuildRecord {
WebProjectPreviewBuildRecord {
job_id: snapshot.job_id,
project_id: snapshot.project_id,
snapshot_id: snapshot.snapshot_id,
owner_user_id: snapshot.owner_user_id,
status: snapshot.status,
logs: snapshot.logs,
artifact_id: snapshot.artifact_id,
preview_token_id: snapshot.preview_token_id,
preview_url: snapshot.preview_url,
error_summary: snapshot.error_summary,
created_at: format_timestamp_micros(snapshot.created_at_micros),
started_at: snapshot.started_at_micros.map(format_timestamp_micros),
finished_at: snapshot.finished_at_micros.map(format_timestamp_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_web_project_procedure_error() {
let result = WebProjectProcedureResult {
ok: false,
project: None,
error_message: Some("无权访问该 Web 工程".to_string()),
};
assert_eq!(
map_web_project_procedure_result(result)
.expect_err("procedure error should map")
.to_string(),
"无权访问该 Web 工程"
);
}
}

View File

@@ -1,7 +1,7 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
// This was generated using spacetimedb cli version 2.5.0 (commit ca16958ef0a5f8c816700d2255a0b20ecacff901).
// This was generated using spacetimedb cli version 2.4.1 (commit 07b52763c9da8d7cf79780db222fec1ffcb84070).
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
@@ -102,6 +102,7 @@ pub mod auth_store_snapshot_table;
pub mod auth_store_snapshot_type;
pub mod auth_store_snapshot_upsert_input_type;
pub mod authorize_database_migration_operator_procedure;
pub mod authorize_web_project_service_identity_procedure;
pub mod bark_battle_draft_config_row_type;
pub mod bark_battle_draft_config_snapshot_type;
pub mod bark_battle_draft_config_table;
@@ -247,6 +248,8 @@ pub mod create_puzzle_agent_session_procedure;
pub mod create_puzzle_clear_agent_session_procedure;
pub mod create_square_hole_agent_session_procedure;
pub mod create_visual_novel_agent_session_procedure;
pub mod create_web_project_and_return_procedure;
pub mod create_web_project_preview_build_and_return_procedure;
pub mod create_wooden_fish_agent_session_procedure;
pub mod creation_entry_config_procedure_result_type;
pub mod creation_entry_config_snapshot_type;
@@ -471,6 +474,10 @@ pub mod get_story_session_state_procedure;
pub mod get_visual_novel_agent_session_procedure;
pub mod get_visual_novel_run_procedure;
pub mod get_visual_novel_work_detail_procedure;
pub mod get_web_project_and_return_procedure;
pub mod get_web_project_preview_build_and_return_procedure;
pub mod get_web_project_preview_build_by_token_and_return_procedure;
pub mod get_web_project_snapshot_and_return_procedure;
pub mod get_wooden_fish_agent_session_procedure;
pub mod get_wooden_fish_run_procedure;
pub mod get_wooden_fish_work_profile_procedure;
@@ -869,6 +876,7 @@ pub mod restart_square_hole_run_procedure;
pub mod resume_profile_save_archive_and_return_procedure;
pub mod retry_puzzle_clear_level_run_procedure;
pub mod revoke_database_migration_operator_procedure;
pub mod revoke_web_project_service_identity_procedure;
pub mod rpg_agent_draft_card_kind_type;
pub mod rpg_agent_draft_card_status_type;
pub mod rpg_agent_message_kind_type;
@@ -989,6 +997,7 @@ pub mod save_editor_project_layout_and_return_procedure;
pub mod save_puzzle_form_draft_procedure;
pub mod save_puzzle_generated_images_procedure;
pub mod save_puzzle_ui_background_procedure;
pub mod save_web_project_snapshot_and_return_procedure;
pub mod seed_analytics_date_dimensions_reducer;
pub mod select_puzzle_cover_image_procedure;
pub mod square_hole_agent_message_finalize_input_type;
@@ -1094,6 +1103,7 @@ pub mod update_puzzle_run_pause_procedure;
pub mod update_puzzle_work_procedure;
pub mod update_square_hole_work_procedure;
pub mod update_visual_novel_work_procedure;
pub mod update_web_project_preview_build_and_return_procedure;
pub mod update_wooden_fish_work_procedure;
pub mod upsert_chapter_progression_and_return_procedure;
pub mod upsert_chapter_progression_reducer;
@@ -1158,6 +1168,32 @@ pub mod visual_novel_work_snapshot_type;
pub mod visual_novel_work_update_input_type;
pub mod visual_novel_works_list_input_type;
pub mod visual_novel_works_procedure_result_type;
pub mod web_project_create_input_type;
pub mod web_project_file_snapshot_type;
pub mod web_project_get_input_type;
pub mod web_project_preview_build_create_input_type;
pub mod web_project_preview_build_get_input_type;
pub mod web_project_preview_build_procedure_result_type;
pub mod web_project_preview_build_row_type;
pub mod web_project_preview_build_snapshot_type;
pub mod web_project_preview_build_table;
pub mod web_project_preview_build_token_get_input_type;
pub mod web_project_preview_build_update_input_type;
pub mod web_project_procedure_result_type;
pub mod web_project_project_snapshot_type;
pub mod web_project_service_identity_authorize_input_type;
pub mod web_project_service_identity_procedure_result_type;
pub mod web_project_service_identity_revoke_input_type;
pub mod web_project_service_identity_table;
pub mod web_project_service_identity_type;
pub mod web_project_snapshot_get_input_type;
pub mod web_project_snapshot_procedure_result_type;
pub mod web_project_snapshot_row_type;
pub mod web_project_snapshot_save_input_type;
pub mod web_project_snapshot_table;
pub mod web_project_snapshot_type;
pub mod web_project_table;
pub mod web_project_type;
pub mod wooden_fish_agent_session_create_input_type;
pub mod wooden_fish_agent_session_get_input_type;
pub mod wooden_fish_agent_session_procedure_result_type;
@@ -1292,6 +1328,7 @@ pub use auth_store_snapshot_table::*;
pub use auth_store_snapshot_type::AuthStoreSnapshot;
pub use auth_store_snapshot_upsert_input_type::AuthStoreSnapshotUpsertInput;
pub use authorize_database_migration_operator_procedure::authorize_database_migration_operator;
pub use authorize_web_project_service_identity_procedure::authorize_web_project_service_identity;
pub use bark_battle_draft_config_row_type::BarkBattleDraftConfigRow;
pub use bark_battle_draft_config_snapshot_type::BarkBattleDraftConfigSnapshot;
pub use bark_battle_draft_config_table::*;
@@ -1437,6 +1474,8 @@ pub use create_puzzle_agent_session_procedure::create_puzzle_agent_session;
pub use create_puzzle_clear_agent_session_procedure::create_puzzle_clear_agent_session;
pub use create_square_hole_agent_session_procedure::create_square_hole_agent_session;
pub use create_visual_novel_agent_session_procedure::create_visual_novel_agent_session;
pub use create_web_project_and_return_procedure::create_web_project_and_return;
pub use create_web_project_preview_build_and_return_procedure::create_web_project_preview_build_and_return;
pub use create_wooden_fish_agent_session_procedure::create_wooden_fish_agent_session;
pub use creation_entry_config_procedure_result_type::CreationEntryConfigProcedureResult;
pub use creation_entry_config_snapshot_type::CreationEntryConfigSnapshot;
@@ -1661,6 +1700,10 @@ pub use get_story_session_state_procedure::get_story_session_state;
pub use get_visual_novel_agent_session_procedure::get_visual_novel_agent_session;
pub use get_visual_novel_run_procedure::get_visual_novel_run;
pub use get_visual_novel_work_detail_procedure::get_visual_novel_work_detail;
pub use get_web_project_and_return_procedure::get_web_project_and_return;
pub use get_web_project_preview_build_and_return_procedure::get_web_project_preview_build_and_return;
pub use get_web_project_preview_build_by_token_and_return_procedure::get_web_project_preview_build_by_token_and_return;
pub use get_web_project_snapshot_and_return_procedure::get_web_project_snapshot_and_return;
pub use get_wooden_fish_agent_session_procedure::get_wooden_fish_agent_session;
pub use get_wooden_fish_run_procedure::get_wooden_fish_run;
pub use get_wooden_fish_work_profile_procedure::get_wooden_fish_work_profile;
@@ -2059,6 +2102,7 @@ pub use restart_square_hole_run_procedure::restart_square_hole_run;
pub use resume_profile_save_archive_and_return_procedure::resume_profile_save_archive_and_return;
pub use retry_puzzle_clear_level_run_procedure::retry_puzzle_clear_level_run;
pub use revoke_database_migration_operator_procedure::revoke_database_migration_operator;
pub use revoke_web_project_service_identity_procedure::revoke_web_project_service_identity;
pub use rpg_agent_draft_card_kind_type::RpgAgentDraftCardKind;
pub use rpg_agent_draft_card_status_type::RpgAgentDraftCardStatus;
pub use rpg_agent_message_kind_type::RpgAgentMessageKind;
@@ -2179,6 +2223,7 @@ pub use save_editor_project_layout_and_return_procedure::save_editor_project_lay
pub use save_puzzle_form_draft_procedure::save_puzzle_form_draft;
pub use save_puzzle_generated_images_procedure::save_puzzle_generated_images;
pub use save_puzzle_ui_background_procedure::save_puzzle_ui_background;
pub use save_web_project_snapshot_and_return_procedure::save_web_project_snapshot_and_return;
pub use seed_analytics_date_dimensions_reducer::seed_analytics_date_dimensions;
pub use select_puzzle_cover_image_procedure::select_puzzle_cover_image;
pub use square_hole_agent_message_finalize_input_type::SquareHoleAgentMessageFinalizeInput;
@@ -2284,6 +2329,7 @@ pub use update_puzzle_run_pause_procedure::update_puzzle_run_pause;
pub use update_puzzle_work_procedure::update_puzzle_work;
pub use update_square_hole_work_procedure::update_square_hole_work;
pub use update_visual_novel_work_procedure::update_visual_novel_work;
pub use update_web_project_preview_build_and_return_procedure::update_web_project_preview_build_and_return;
pub use update_wooden_fish_work_procedure::update_wooden_fish_work;
pub use upsert_chapter_progression_and_return_procedure::upsert_chapter_progression_and_return;
pub use upsert_chapter_progression_reducer::upsert_chapter_progression;
@@ -2348,6 +2394,32 @@ pub use visual_novel_work_snapshot_type::VisualNovelWorkSnapshot;
pub use visual_novel_work_update_input_type::VisualNovelWorkUpdateInput;
pub use visual_novel_works_list_input_type::VisualNovelWorksListInput;
pub use visual_novel_works_procedure_result_type::VisualNovelWorksProcedureResult;
pub use web_project_create_input_type::WebProjectCreateInput;
pub use web_project_file_snapshot_type::WebProjectFileSnapshot;
pub use web_project_get_input_type::WebProjectGetInput;
pub use web_project_preview_build_create_input_type::WebProjectPreviewBuildCreateInput;
pub use web_project_preview_build_get_input_type::WebProjectPreviewBuildGetInput;
pub use web_project_preview_build_procedure_result_type::WebProjectPreviewBuildProcedureResult;
pub use web_project_preview_build_row_type::WebProjectPreviewBuildRow;
pub use web_project_preview_build_snapshot_type::WebProjectPreviewBuildSnapshot;
pub use web_project_preview_build_table::*;
pub use web_project_preview_build_token_get_input_type::WebProjectPreviewBuildTokenGetInput;
pub use web_project_preview_build_update_input_type::WebProjectPreviewBuildUpdateInput;
pub use web_project_procedure_result_type::WebProjectProcedureResult;
pub use web_project_project_snapshot_type::WebProjectProjectSnapshot;
pub use web_project_service_identity_authorize_input_type::WebProjectServiceIdentityAuthorizeInput;
pub use web_project_service_identity_procedure_result_type::WebProjectServiceIdentityProcedureResult;
pub use web_project_service_identity_revoke_input_type::WebProjectServiceIdentityRevokeInput;
pub use web_project_service_identity_table::*;
pub use web_project_service_identity_type::WebProjectServiceIdentity;
pub use web_project_snapshot_get_input_type::WebProjectSnapshotGetInput;
pub use web_project_snapshot_procedure_result_type::WebProjectSnapshotProcedureResult;
pub use web_project_snapshot_row_type::WebProjectSnapshotRow;
pub use web_project_snapshot_save_input_type::WebProjectSnapshotSaveInput;
pub use web_project_snapshot_table::*;
pub use web_project_snapshot_type::WebProjectSnapshot;
pub use web_project_table::*;
pub use web_project_type::WebProject;
pub use wooden_fish_agent_session_create_input_type::WoodenFishAgentSessionCreateInput;
pub use wooden_fish_agent_session_get_input_type::WoodenFishAgentSessionGetInput;
pub use wooden_fish_agent_session_procedure_result_type::WoodenFishAgentSessionProcedureResult;
@@ -2776,6 +2848,10 @@ pub struct DbUpdate {
visual_novel_runtime_history_entry: __sdk::TableUpdate<VisualNovelRuntimeHistoryEntryRow>,
visual_novel_runtime_run: __sdk::TableUpdate<VisualNovelRuntimeRunRow>,
visual_novel_work_profile: __sdk::TableUpdate<VisualNovelWorkProfileRow>,
web_project: __sdk::TableUpdate<WebProject>,
web_project_preview_build: __sdk::TableUpdate<WebProjectPreviewBuildRow>,
web_project_service_identity: __sdk::TableUpdate<WebProjectServiceIdentity>,
web_project_snapshot: __sdk::TableUpdate<WebProjectSnapshotRow>,
wooden_fish_agent_session: __sdk::TableUpdate<WoodenFishAgentSessionRow>,
wooden_fish_event: __sdk::TableUpdate<WoodenFishEventRow>,
wooden_fish_gallery_card_view: __sdk::TableUpdate<WoodenFishGalleryCardViewRow>,
@@ -3160,6 +3236,18 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
"visual_novel_work_profile" => db_update.visual_novel_work_profile.append(
visual_novel_work_profile_table::parse_table_update(table_update)?,
),
"web_project" => db_update
.web_project
.append(web_project_table::parse_table_update(table_update)?),
"web_project_preview_build" => db_update.web_project_preview_build.append(
web_project_preview_build_table::parse_table_update(table_update)?,
),
"web_project_service_identity" => db_update.web_project_service_identity.append(
web_project_service_identity_table::parse_table_update(table_update)?,
),
"web_project_snapshot" => db_update.web_project_snapshot.append(
web_project_snapshot_table::parse_table_update(table_update)?,
),
"wooden_fish_agent_session" => db_update.wooden_fish_agent_session.append(
wooden_fish_agent_session_table::parse_table_update(table_update)?,
),
@@ -3740,6 +3828,27 @@ impl __sdk::DbUpdate for DbUpdate {
&self.visual_novel_work_profile,
)
.with_updates_by_pk(|row| &row.profile_id);
diff.web_project = cache
.apply_diff_to_table::<WebProject>("web_project", &self.web_project)
.with_updates_by_pk(|row| &row.project_id);
diff.web_project_preview_build = cache
.apply_diff_to_table::<WebProjectPreviewBuildRow>(
"web_project_preview_build",
&self.web_project_preview_build,
)
.with_updates_by_pk(|row| &row.job_id);
diff.web_project_service_identity = cache
.apply_diff_to_table::<WebProjectServiceIdentity>(
"web_project_service_identity",
&self.web_project_service_identity,
)
.with_updates_by_pk(|row| &row.service_identity);
diff.web_project_snapshot = cache
.apply_diff_to_table::<WebProjectSnapshotRow>(
"web_project_snapshot",
&self.web_project_snapshot,
)
.with_updates_by_pk(|row| &row.snapshot_id);
diff.wooden_fish_agent_session = cache
.apply_diff_to_table::<WoodenFishAgentSessionRow>(
"wooden_fish_agent_session",
@@ -4184,6 +4293,18 @@ impl __sdk::DbUpdate for DbUpdate {
"visual_novel_work_profile" => db_update
.visual_novel_work_profile
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"web_project" => db_update
.web_project
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"web_project_preview_build" => db_update
.web_project_preview_build
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"web_project_service_identity" => db_update
.web_project_service_identity
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"web_project_snapshot" => db_update
.web_project_snapshot
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"wooden_fish_agent_session" => db_update
.wooden_fish_agent_session
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
@@ -4569,6 +4690,18 @@ impl __sdk::DbUpdate for DbUpdate {
"visual_novel_work_profile" => db_update
.visual_novel_work_profile
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"web_project" => db_update
.web_project
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"web_project_preview_build" => db_update
.web_project_preview_build
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"web_project_service_identity" => db_update
.web_project_service_identity
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"web_project_snapshot" => db_update
.web_project_snapshot
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"wooden_fish_agent_session" => db_update
.wooden_fish_agent_session
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
@@ -4723,6 +4856,10 @@ pub struct AppliedDiff<'r> {
__sdk::TableAppliedDiff<'r, VisualNovelRuntimeHistoryEntryRow>,
visual_novel_runtime_run: __sdk::TableAppliedDiff<'r, VisualNovelRuntimeRunRow>,
visual_novel_work_profile: __sdk::TableAppliedDiff<'r, VisualNovelWorkProfileRow>,
web_project: __sdk::TableAppliedDiff<'r, WebProject>,
web_project_preview_build: __sdk::TableAppliedDiff<'r, WebProjectPreviewBuildRow>,
web_project_service_identity: __sdk::TableAppliedDiff<'r, WebProjectServiceIdentity>,
web_project_snapshot: __sdk::TableAppliedDiff<'r, WebProjectSnapshotRow>,
wooden_fish_agent_session: __sdk::TableAppliedDiff<'r, WoodenFishAgentSessionRow>,
wooden_fish_event: __sdk::TableAppliedDiff<'r, WoodenFishEventRow>,
wooden_fish_gallery_card_view: __sdk::TableAppliedDiff<'r, WoodenFishGalleryCardViewRow>,
@@ -5312,6 +5449,22 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
&self.visual_novel_work_profile,
event,
);
callbacks.invoke_table_row_callbacks::<WebProject>("web_project", &self.web_project, event);
callbacks.invoke_table_row_callbacks::<WebProjectPreviewBuildRow>(
"web_project_preview_build",
&self.web_project_preview_build,
event,
);
callbacks.invoke_table_row_callbacks::<WebProjectServiceIdentity>(
"web_project_service_identity",
&self.web_project_service_identity,
event,
);
callbacks.invoke_table_row_callbacks::<WebProjectSnapshotRow>(
"web_project_snapshot",
&self.web_project_snapshot,
event,
);
callbacks.invoke_table_row_callbacks::<WoodenFishAgentSessionRow>(
"wooden_fish_agent_session",
&self.wooden_fish_agent_session,
@@ -5597,19 +5750,19 @@ impl __sdk::SubscriptionHandle for SubscriptionHandle {
/// either a [`DbConnection`] or an [`EventContext`] and operate on either.
pub trait RemoteDbContext:
__sdk::DbContext<
DbView = RemoteTables,
Reducers = RemoteReducers,
SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>,
>
DbView = RemoteTables,
Reducers = RemoteReducers,
SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>,
>
{
}
impl<
Ctx: __sdk::DbContext<
Ctx: __sdk::DbContext<
DbView = RemoteTables,
Reducers = RemoteReducers,
SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>,
>,
> RemoteDbContext for Ctx
> RemoteDbContext for Ctx
{
}
@@ -6120,6 +6273,10 @@ impl __sdk::SpacetimeModule for RemoteModule {
visual_novel_runtime_history_entry_table::register_table(client_cache);
visual_novel_runtime_run_table::register_table(client_cache);
visual_novel_work_profile_table::register_table(client_cache);
web_project_table::register_table(client_cache);
web_project_preview_build_table::register_table(client_cache);
web_project_service_identity_table::register_table(client_cache);
web_project_snapshot_table::register_table(client_cache);
wooden_fish_agent_session_table::register_table(client_cache);
wooden_fish_event_table::register_table(client_cache);
wooden_fish_gallery_card_view_table::register_table(client_cache);
@@ -6246,6 +6403,10 @@ impl __sdk::SpacetimeModule for RemoteModule {
"visual_novel_runtime_history_entry",
"visual_novel_runtime_run",
"visual_novel_work_profile",
"web_project",
"web_project_preview_build",
"web_project_service_identity",
"web_project_snapshot",
"wooden_fish_agent_session",
"wooden_fish_event",
"wooden_fish_gallery_card_view",

View File

@@ -47,9 +47,11 @@ pub trait accept_quest {
&self,
input: QuestRecordInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -58,9 +60,11 @@ impl accept_quest for super::RemoteReducers {
&self,
input: QuestRecordInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(AcceptQuestArgs { input }, callback)

View File

@@ -47,9 +47,11 @@ pub trait acknowledge_quest_completion {
&self,
input: QuestCompletionAckInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -58,9 +60,11 @@ impl acknowledge_quest_completion for super::RemoteReducers {
&self,
input: QuestCompletionAckInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(AcknowledgeQuestCompletionArgs { input }, callback)

View File

@@ -31,10 +31,10 @@ pub trait admin_disable_profile_redeem_code {
input: RuntimeProfileRedeemCodeAdminDisableInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_disable_profile_redeem_code for super::RemoteProcedures {
input: RuntimeProfileRedeemCodeAdminDisableInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait admin_disable_profile_task_config {
input: RuntimeProfileTaskConfigAdminDisableInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_disable_profile_task_config for super::RemoteProcedures {
input: RuntimeProfileTaskConfigAdminDisableInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileTaskConfigAdminProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait admin_list_profile_invite_codes {
input: RuntimeProfileInviteCodeAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileInviteCodeAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileInviteCodeAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_list_profile_invite_codes for super::RemoteProcedures {
input: RuntimeProfileInviteCodeAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileInviteCodeAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileInviteCodeAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileInviteCodeAdminListProcedureResult>(

View File

@@ -34,10 +34,10 @@ pub trait admin_list_profile_recharge_products {
input: RuntimeProfileRechargeProductAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl admin_list_profile_recharge_products for super::RemoteProcedures {
input: RuntimeProfileRechargeProductAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp.invoke_procedure_with_callback::<_, RuntimeProfileRechargeProductAdminListProcedureResult>(
"admin_list_profile_recharge_products",

View File

@@ -31,10 +31,10 @@ pub trait admin_list_profile_redeem_codes {
input: RuntimeProfileRedeemCodeAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_list_profile_redeem_codes for super::RemoteProcedures {
input: RuntimeProfileRedeemCodeAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminListProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait admin_list_profile_task_configs {
input: RuntimeProfileTaskConfigAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_list_profile_task_configs for super::RemoteProcedures {
input: RuntimeProfileTaskConfigAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileTaskConfigAdminListProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait admin_list_work_visibility {
input: AdminWorkVisibilityListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AdminWorkVisibilityListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AdminWorkVisibilityListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_list_work_visibility for super::RemoteProcedures {
input: AdminWorkVisibilityListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AdminWorkVisibilityListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AdminWorkVisibilityListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AdminWorkVisibilityListProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait admin_update_work_visibility {
input: AdminWorkVisibilityUpdateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AdminWorkVisibilityProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AdminWorkVisibilityProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_update_work_visibility for super::RemoteProcedures {
input: AdminWorkVisibilityUpdateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AdminWorkVisibilityProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AdminWorkVisibilityProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AdminWorkVisibilityProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait admin_upsert_profile_invite_code {
input: RuntimeProfileInviteCodeAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileInviteCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileInviteCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_upsert_profile_invite_code for super::RemoteProcedures {
input: RuntimeProfileInviteCodeAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileInviteCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileInviteCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileInviteCodeAdminProcedureResult>(

View File

@@ -34,10 +34,10 @@ pub trait admin_upsert_profile_recharge_product {
input: RuntimeProfileRechargeProductAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl admin_upsert_profile_recharge_product for super::RemoteProcedures {
input: RuntimeProfileRechargeProductAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileRechargeProductAdminProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait admin_upsert_profile_redeem_code {
input: RuntimeProfileRedeemCodeAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_upsert_profile_redeem_code for super::RemoteProcedures {
input: RuntimeProfileRedeemCodeAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait admin_upsert_profile_task_config {
input: RuntimeProfileTaskConfigAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_upsert_profile_task_config for super::RemoteProcedures {
input: RuntimeProfileTaskConfigAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileTaskConfigAdminProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait advance_puzzle_clear_next_level {
input: PuzzleClearRunNextLevelInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleClearRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleClearRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl advance_puzzle_clear_next_level for super::RemoteProcedures {
input: PuzzleClearRunNextLevelInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleClearRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleClearRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, PuzzleClearRunProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait advance_puzzle_next_level {
input: PuzzleRunNextLevelInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl advance_puzzle_next_level for super::RemoteProcedures {
input: PuzzleRunNextLevelInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait append_ai_text_chunk_and_return {
input: AiTextChunkAppendInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl append_ai_text_chunk_and_return for super::RemoteProcedures {
input: AiTextChunkAppendInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AiTaskProcedureResult>(

View File

@@ -34,10 +34,10 @@ pub trait append_visual_novel_runtime_history_entry {
input: VisualNovelRuntimeHistoryAppendInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<VisualNovelHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl append_visual_novel_runtime_history_entry for super::RemoteProcedures {
input: VisualNovelRuntimeHistoryAppendInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<VisualNovelHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelHistoryProcedureResult>(

View File

@@ -34,10 +34,10 @@ pub trait apply_chapter_progression_ledger_entry_and_return {
input: ChapterProgressionLedgerInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ChapterProgressionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<ChapterProgressionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl apply_chapter_progression_ledger_entry_and_return for super::RemoteProcedur
input: ChapterProgressionLedgerInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ChapterProgressionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<ChapterProgressionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, ChapterProgressionProcedureResult>(

View File

@@ -50,9 +50,11 @@ pub trait apply_chapter_progression_ledger_entry {
&self,
input: ChapterProgressionLedgerInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -61,9 +63,11 @@ impl apply_chapter_progression_ledger_entry for super::RemoteReducers {
&self,
input: ChapterProgressionLedgerInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp.invoke_reducer_with_callback(
ApplyChapterProgressionLedgerEntryArgs { input },

View File

@@ -47,9 +47,11 @@ pub trait apply_inventory_mutation {
&self,
input: InventoryMutationInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -58,9 +60,11 @@ impl apply_inventory_mutation for super::RemoteReducers {
&self,
input: InventoryMutationInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(ApplyInventoryMutationArgs { input }, callback)

View File

@@ -47,9 +47,11 @@ pub trait apply_quest_signal {
&self,
input: QuestSignalApplyInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -58,9 +60,11 @@ impl apply_quest_signal for super::RemoteReducers {
&self,
input: QuestSignalApplyInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(ApplyQuestSignalArgs { input }, callback)

View File

@@ -31,10 +31,10 @@ pub trait attach_ai_result_reference_and_return {
input: AiResultReferenceInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl attach_ai_result_reference_and_return for super::RemoteProcedures {
input: AiResultReferenceInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AiTaskProcedureResult>(

View File

@@ -34,10 +34,10 @@ pub trait authorize_database_migration_operator {
input: DatabaseMigrationAuthorizeOperatorInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<DatabaseMigrationOperatorProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<DatabaseMigrationOperatorProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl authorize_database_migration_operator for super::RemoteProcedures {
input: DatabaseMigrationAuthorizeOperatorInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<DatabaseMigrationOperatorProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<DatabaseMigrationOperatorProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, DatabaseMigrationOperatorProcedureResult>(

View File

@@ -0,0 +1,62 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::web_project_service_identity_authorize_input_type::WebProjectServiceIdentityAuthorizeInput;
use super::web_project_service_identity_procedure_result_type::WebProjectServiceIdentityProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct AuthorizeWebProjectServiceIdentityArgs {
pub input: WebProjectServiceIdentityAuthorizeInput,
}
impl __sdk::InModule for AuthorizeWebProjectServiceIdentityArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `authorize_web_project_service_identity`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait authorize_web_project_service_identity {
fn authorize_web_project_service_identity(
&self,
input: WebProjectServiceIdentityAuthorizeInput,
) {
self.authorize_web_project_service_identity_then(input, |_, _| {});
}
fn authorize_web_project_service_identity_then(
&self,
input: WebProjectServiceIdentityAuthorizeInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<WebProjectServiceIdentityProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl authorize_web_project_service_identity for super::RemoteProcedures {
fn authorize_web_project_service_identity_then(
&self,
input: WebProjectServiceIdentityAuthorizeInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<WebProjectServiceIdentityProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, WebProjectServiceIdentityProcedureResult>(
"authorize_web_project_service_identity",
AuthorizeWebProjectServiceIdentityArgs { input },
__callback,
);
}
}

View File

@@ -31,10 +31,10 @@ pub trait begin_story_session_and_return {
input: StorySessionInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<StorySessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<StorySessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl begin_story_session_and_return for super::RemoteProcedures {
input: StorySessionInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<StorySessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<StorySessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, StorySessionProcedureResult>(

View File

@@ -47,9 +47,11 @@ pub trait begin_story_session {
&self,
input: StorySessionInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -58,9 +60,11 @@ impl begin_story_session for super::RemoteReducers {
&self,
input: StorySessionInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(BeginStorySessionArgs { input }, callback)

View File

@@ -31,10 +31,10 @@ pub trait bind_asset_object_to_entity_and_return {
input: AssetEntityBindingInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AssetEntityBindingProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AssetEntityBindingProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl bind_asset_object_to_entity_and_return for super::RemoteProcedures {
input: AssetEntityBindingInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AssetEntityBindingProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AssetEntityBindingProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AssetEntityBindingProcedureResult>(

View File

@@ -47,9 +47,11 @@ pub trait bind_asset_object_to_entity {
&self,
input: AssetEntityBindingInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -58,9 +60,11 @@ impl bind_asset_object_to_entity for super::RemoteReducers {
&self,
input: AssetEntityBindingInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(BindAssetObjectToEntityArgs { input }, callback)

View File

@@ -31,10 +31,10 @@ pub trait cancel_ai_task_and_return {
input: AiTaskCancelInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl cancel_ai_task_and_return for super::RemoteProcedures {
input: AiTaskCancelInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AiTaskProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait checkpoint_wooden_fish_run {
input: WoodenFishRunCheckpointInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<WoodenFishRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<WoodenFishRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl checkpoint_wooden_fish_run for super::RemoteProcedures {
input: WoodenFishRunCheckpointInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<WoodenFishRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<WoodenFishRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, WoodenFishRunProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait claim_external_generation_jobs_and_return {
input: ExternalGenerationJobClaimInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl claim_external_generation_jobs_and_return for super::RemoteProcedures {
input: ExternalGenerationJobClaimInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, ExternalGenerationJobProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait claim_profile_task_reward_and_return {
input: RuntimeProfileTaskClaimInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileTaskClaimProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileTaskClaimProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl claim_profile_task_reward_and_return for super::RemoteProcedures {
input: RuntimeProfileTaskClaimInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileTaskClaimProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileTaskClaimProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileTaskClaimProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait claim_puzzle_background_compile_task {
input: PuzzleBackgroundCompileTaskClaimInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleBackgroundCompileTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleBackgroundCompileTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl claim_puzzle_background_compile_task for super::RemoteProcedures {
input: PuzzleBackgroundCompileTaskClaimInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleBackgroundCompileTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleBackgroundCompileTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, PuzzleBackgroundCompileTaskProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait claim_puzzle_work_point_incentive {
input: PuzzleWorkPointIncentiveClaimInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleWorkProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleWorkProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl claim_puzzle_work_point_incentive for super::RemoteProcedures {
input: PuzzleWorkPointIncentiveClaimInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleWorkProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleWorkProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, PuzzleWorkProcedureResult>(

View File

@@ -34,10 +34,10 @@ pub trait clear_database_migration_import_chunks {
input: DatabaseMigrationImportChunksClearInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<DatabaseMigrationProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<DatabaseMigrationProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl clear_database_migration_import_chunks for super::RemoteProcedures {
input: DatabaseMigrationImportChunksClearInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<DatabaseMigrationProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<DatabaseMigrationProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, DatabaseMigrationProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait clear_platform_browse_history_and_return {
input: RuntimeBrowseHistoryClearInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeBrowseHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeBrowseHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl clear_platform_browse_history_and_return for super::RemoteProcedures {
input: RuntimeBrowseHistoryClearInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeBrowseHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeBrowseHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeBrowseHistoryProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait click_match_3_d_item {
input: Match3DRunClickInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<Match3DClickItemProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<Match3DClickItemProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl click_match_3_d_item for super::RemoteProcedures {
input: Match3DRunClickInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<Match3DClickItemProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<Match3DClickItemProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, Match3DClickItemProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait compile_big_fish_draft {
input: BigFishDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<BigFishSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<BigFishSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl compile_big_fish_draft for super::RemoteProcedures {
input: BigFishDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<BigFishSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<BigFishSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, BigFishSessionProcedureResult>(

View File

@@ -34,10 +34,10 @@ pub trait compile_custom_world_published_profile {
input: CustomWorldPublishedProfileCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<CustomWorldPublishedProfileCompileResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<CustomWorldPublishedProfileCompileResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl compile_custom_world_published_profile for super::RemoteProcedures {
input: CustomWorldPublishedProfileCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<CustomWorldPublishedProfileCompileResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<CustomWorldPublishedProfileCompileResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, CustomWorldPublishedProfileCompileResult>(

View File

@@ -31,10 +31,10 @@ pub trait compile_jump_hop_draft {
input: JumpHopDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<JumpHopAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<JumpHopAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl compile_jump_hop_draft for super::RemoteProcedures {
input: JumpHopDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<JumpHopAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<JumpHopAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, JumpHopAgentSessionProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait compile_match_3_d_draft {
input: Match3DDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<Match3DAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<Match3DAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl compile_match_3_d_draft for super::RemoteProcedures {
input: Match3DDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<Match3DAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<Match3DAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, Match3DAgentSessionProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait compile_puzzle_agent_draft {
input: PuzzleDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl compile_puzzle_agent_draft for super::RemoteProcedures {
input: PuzzleDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, PuzzleAgentSessionProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait compile_puzzle_clear_draft {
input: PuzzleClearDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleClearAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleClearAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl compile_puzzle_clear_draft for super::RemoteProcedures {
input: PuzzleClearDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleClearAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<PuzzleClearAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, PuzzleClearAgentSessionProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait compile_square_hole_draft {
input: SquareHoleDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<SquareHoleAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<SquareHoleAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl compile_square_hole_draft for super::RemoteProcedures {
input: SquareHoleDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<SquareHoleAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<SquareHoleAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, SquareHoleAgentSessionProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait compile_visual_novel_work_profile {
input: VisualNovelWorkCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl compile_visual_novel_work_profile for super::RemoteProcedures {
input: VisualNovelWorkCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelAgentSessionProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait compile_wooden_fish_draft {
input: WoodenFishDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<WoodenFishAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<WoodenFishAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl compile_wooden_fish_draft for super::RemoteProcedures {
input: WoodenFishDraftCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<WoodenFishAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<WoodenFishAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, WoodenFishAgentSessionProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait complete_ai_stage_and_return {
input: AiStageCompletionInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl complete_ai_stage_and_return for super::RemoteProcedures {
input: AiStageCompletionInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AiTaskProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait complete_ai_task_and_return {
input: AiTaskFinishInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl complete_ai_task_and_return for super::RemoteProcedures {
input: AiTaskFinishInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AiTaskProcedureResult>(

View File

@@ -34,10 +34,10 @@ pub trait complete_external_generation_job_and_return {
input: ExternalGenerationJobCompleteInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl complete_external_generation_job_and_return for super::RemoteProcedures {
input: ExternalGenerationJobCompleteInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, ExternalGenerationJobProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait confirm_asset_object_and_return {
input: AssetObjectUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AssetObjectProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AssetObjectProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl confirm_asset_object_and_return for super::RemoteProcedures {
input: AssetObjectUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AssetObjectProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AssetObjectProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AssetObjectProcedureResult>(

View File

@@ -47,9 +47,11 @@ pub trait confirm_asset_object {
&self,
input: AssetObjectUpsertInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -58,9 +60,11 @@ impl confirm_asset_object for super::RemoteReducers {
&self,
input: AssetObjectUpsertInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(ConfirmAssetObjectArgs { input }, callback)

View File

@@ -31,10 +31,10 @@ pub trait consume_profile_wallet_points_and_return {
input: RuntimeProfileWalletAdjustmentInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileWalletAdjustmentProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileWalletAdjustmentProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl consume_profile_wallet_points_and_return for super::RemoteProcedures {
input: RuntimeProfileWalletAdjustmentInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileWalletAdjustmentProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileWalletAdjustmentProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileWalletAdjustmentProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait continue_story_and_return {
input: StoryContinueInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<StorySessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<StorySessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl continue_story_and_return for super::RemoteProcedures {
input: StoryContinueInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<StorySessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<StorySessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, StorySessionProcedureResult>(

View File

@@ -47,9 +47,11 @@ pub trait continue_story {
&self,
input: StoryContinueInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -58,9 +60,11 @@ impl continue_story for super::RemoteReducers {
&self,
input: StoryContinueInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(ContinueStoryArgs { input }, callback)

View File

@@ -31,10 +31,10 @@ pub trait create_ai_task_and_return {
input: AiTaskCreateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl create_ai_task_and_return for super::RemoteProcedures {
input: AiTaskCreateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AiTaskProcedureResult>(

View File

@@ -47,9 +47,11 @@ pub trait create_ai_task {
&self,
input: AiTaskCreateInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -58,9 +60,11 @@ impl create_ai_task for super::RemoteReducers {
&self,
input: AiTaskCreateInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(CreateAiTaskArgs { input }, callback)

View File

@@ -31,10 +31,10 @@ pub trait create_bark_battle_draft {
input: BarkBattleDraftCreateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<BarkBattleProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<BarkBattleProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl create_bark_battle_draft for super::RemoteProcedures {
input: BarkBattleDraftCreateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<BarkBattleProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<BarkBattleProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, BarkBattleProcedureResult>(

View File

@@ -31,10 +31,10 @@ pub trait create_battle_state_and_return {
input: BattleStateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<BattleStateProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<BattleStateProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl create_battle_state_and_return for super::RemoteProcedures {
input: BattleStateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<BattleStateProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<BattleStateProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, BattleStateProcedureResult>(

Some files were not shown because too many files have changed in this diff Show More