Merge branch 'master' into editor-agent-right-click

# Conflicts:
#	src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx
This commit is contained in:
2026-07-21 10:41:23 +08:00
1066 changed files with 139112 additions and 32290 deletions
@@ -503,6 +503,9 @@
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"409": {
"$ref": "#/components/responses/Conflict"
}
}
}
@@ -1327,6 +1330,16 @@
}
}
},
"Conflict": {
"description": "画布 revision 冲突,需重新读取最新快照后合并或重试",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"UpstreamError": {
"description": "上游生成服务失败",
"content": {
@@ -1755,8 +1768,17 @@
"$ref": "#/components/schemas/EditorCanvasViewport"
},
"layers": {
"type": "object",
"description": "画布图层 JSON,最大约 256KB。"
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
},
"description": "兼容画布布局 JSON,最大 2 MiB;服务端会拆分为结构化图层与生成对话框行。"
},
"expectedRevision": {
"type": "integer",
"minimum": 0,
"description": "可选的画布 revision CAS;不匹配时返回 409。"
}
},
"additionalProperties": false
@@ -2110,7 +2132,11 @@
"$ref": "#/components/schemas/EditorCanvasViewport"
},
"layers": {
"type": "object"
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
},
"resources": {
"type": "array",
@@ -2132,6 +2158,8 @@
"title",
"viewport",
"layers",
"revision",
"layoutStorageVersion",
"createdAt",
"updatedAt"
],
@@ -2149,7 +2177,25 @@
"$ref": "#/components/schemas/EditorCanvasViewport"
},
"layers": {
"type": "object"
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
},
"revision": {
"type": "integer",
"minimum": 0
},
"layoutStorageVersion": {
"type": "integer",
"minimum": 0
},
"backgroundColor": {
"type": [
"string",
"null"
]
},
"createdAt": {
"type": "string",
@@ -26,6 +26,16 @@
---
## 2026-07-19 角色动作视频使用单进程批量抽帧
- 背景:角色动作生成在拿到预览视频后,原实现会为 `32 / 40 / 48` 个采样点分别启动一次 FFmpeg、重复解码同一视频。release 的 2 vCPU 主机在一次 32 帧任务中因此出现约 10 秒的 CPU 尖刺,且进程启动和重复解码都不是业务必需开销。
- 决策:角色动作抽帧必须先沿用 `compute_sample_time_seconds()` 计算全部采样点,再通过一个 FFmpeg filter graph 对输入统一 `setpts`、`split`,各分支按精确 `select=gte(t\,<target>)` 输出一帧。不得改用会漂移现有采样时刻的粗粒度 `fps` 抽帧。单次命令完成后逐一确认全部目标文件存在,任一缺帧继续使用原有用户错误文案,并在 details 中保留首个缺帧的 `targetSeconds / outputPath`、整批 `missingFrames` 和 stdout/stderr。视频封面等单帧调用保留兼容 helper,但内部复用同一批量实现。
- 影响范围:`server-rs/crates/api-server/src/character_animation_assets.rs` 的角色动作视频本地抽帧和单帧视频封面抽取;不改变尾帧安全步长、BgFilter 并发、OSS 路径、帧编号、透明化后处理或前后端结果契约。
- 验证方式:运行 `cargo test -p api-server editor_character_animation --manifest-path server-rs/Cargo.toml`,真实短视频回归必须由一次 FFmpeg 命令产出整批帧,并继续断言 `32帧·4秒` 最后一帧为 `3.875s`;追加 `cargo check -p api-server --manifest-path server-rs/Cargo.toml`、Rust 格式、编码和 diff 门禁。
- 关联文档:`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`、`docs/project-memory/shared-memory/pitfalls.md`。
---
## 2026-07-18 图片生成 K 档由 provider 直接生成
- 背景:旧 gpt-image-2 尺寸表会把 2K 竖版回落到 `1024x1536`,图标入口又使用固定 `360x360 / 512x512` 占位;角色去背景结果变小时还会直接放大整张透明成品,导致 UI 显示的 2K 与模型实际生成清晰度不一致。
@@ -750,6 +760,7 @@
- 2026-06-20 桌面能力清单单测边界:Tauri `capabilities.rs` 必须用 Rust 单测同时覆盖桌面 runtime capability 清单顺序、无重复、真实桌面能力完整包含,并显式排除 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact` 等未接入能力;桌面单端配置检查会反查该测试边界,避免只靠方案文档或共享 profile 发现桌面壳能力伪声明。
- 2026-06-20 桌面本地通知契约镜像:Tauri `notification.showLocal` 的 title / body 归一化、长度上限和成功结果 action 必须镜像共享 HostBridge 契约;Rust 侧常量使用 `HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH`、`HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH` 和 `HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_ACTION` 命名,桌面单端配置检查会与 `packages/shared/src/contracts/hostBridge.ts` 比对数值并反查成功结果由该 action 常量组装,避免通知 payload 边界变成桌面壳本地规则。
- 2026-06-19 桌面壳外链打开 helper 共用:Tauri WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `open_normalized_desktop_external_url` 执行系统外链打开动作;HostBridge 分支仍先用 `normalize_external_url` 保留 payload 错误语义并把 opener 错误回传给 H5,WebView 拦截保持 best-effort 静默处理。桌面壳配置检查会拒绝 `dispatch.rs` 直接调用 `app.opener().open_url` 绕过该 helper,避免两条离壳路径漂移。
> 2026-07-18 覆盖说明:本段后续关于微信 `navigation.openNativePage`、生成结果订阅页、`[subscribe-message]` 日志和订阅页路由门禁的 2026-06 决策均已由旧创作模板退役决策废止,只作为历史记录。Expo / Tauri 的同源 H5 受控导航及微信登录、支付、分享能力继续有效。
- 2026-06-20 H5 原生导航预校验:`navigateHostNativePage()` 在 `native_app` 下发送 `navigation.openNativePage` 前必须先拒绝空值、控制字符、协议相对 URL、外域绝对 URL 和非 `http:` / `https:` 协议目标;同源绝对 URL、`/path` 和保留给桌面壳兼容的相对 route 继续交给 Expo / Tauri 壳二次归一并补写宿主上下文。微信小程序分支仍按小程序页面 URL 语义走 `wx.miniProgram.navigateTo`,不套原生 App 同源 H5 预校验。根级 `npm run check:native-shells` 会反查 H5 facade 仍使用 `normalizeNativeAppPageUrl(...)` 且发送归一后的 URL,避免明显不安全目标触达原生壳。
- 2026-06-20 微信受控原生页能力声明:微信小程序壳真实 capability profile 声明 `navigation.openNativePage`,用于承接已经登记并测试的小程序原生页 flow;当前订阅生成结果通知页通过 H5 `requestGenerationResultSubscribePermission()` 调用 `navigateHostNativePage()` 打开 `/pages/subscribe-message/index`,小程序页再调用真实 `wx.requestSubscribeMessage` 并按既有结果协议回灌。根级 `npm run check:native-shells` 必须把该能力反查到共享 profile、微信 `WECHAT_HOST_CAPABILITIES` 镜像、订阅页协议常量、H5 入口、小程序 host-bridge / shell / page 文件和相关测试;该能力不代表开放任意小程序页面跳转。
- 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities`;`openHostShare`、`writeHostClipboardText`、`requestHostHapticsImpact`、`setHostAppTitle`、`exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示受控分享动作,并按 `hostShell` 区分 Expo 系统分享面板和 Tauri 剪贴板复制表达,避免旧壳或裁剪壳露出不可用入口。
@@ -878,6 +889,8 @@
## 2026-06-17 H5 宿主壳能力统一走 HostBridge
> 2026-07-18 覆盖说明:以下订阅授权、订阅页和旧玩法导航部分已退役;登录、支付、分享、九宫切图与通用 HostBridge 分层仍有效。
- 背景:主站同时运行在普通浏览器、微信小程序 `web-view` 和未来可能出现的原生 App WebView 中;登录、支付、分享、订阅授权和运行态分享目标同步曾散落在业务组件与服务文件里,后续新增宿主壳会导致同一业务重复分叉。
- 决策:前端宿主运行态识别、微信小程序 JS SDK 加载、原生页跳转、支付跳转、登录跳转、九宫切图和 `postMessage` 统一收口到 `src/services/host-bridge/hostBridge.ts`,业务层优先调用 `getHostRuntime`、`requestHostLogin`、`requestHostPayment`、`navigateHostNativePage`、`setHostShareTarget` 和 `openHostShareGrid`。`authService`、分享服务、订阅授权和个人中心充值可保留兼容导出或业务编排,但不再自行加载微信 JS SDK 或直接判断 `wx.miniProgram`。固定内置玩法不走代码包下载流程;AI 生成 H5 沙箱后续单独定义受限 `GameBridge`,不得直接暴露完整 `HostBridge`。
- 影响范围:`src/services/host-bridge/`、`src/services/authService.ts`、`src/services/payment/paymentPlatform.ts`、`src/services/wechatMiniProgramShareGrid.ts`、`src/services/wechatMiniProgramShareTarget.ts`、`src/services/wechatMiniProgramSubscribe.ts`、`src/components/platform-entry/usePlatformProfileCenterController.ts`、微信小程序壳和未来原生 App 壳接入。
@@ -4317,3 +4330,27 @@
- 决策:旧创作模板、旧创作入口及其专属运行服务进入下线范围,不再为跳一跳、抓大鹅 Match3D、儿童动作 Demo 等旧链路修复兼容问题、补生成脚本或维持专属门禁。
- 边界:共享账号、钱包、资产、图片编辑器、公开作品、通用 HostBridge、API、SpacetimeDB、发布运维和安全能力不属于旧链路,仍需维持正式门禁。历史文档只作为背景材料,不再作为旧入口继续运行的依据。
- 清理方式:允许直接删除已经失效的旧素材生成命令、入口路由、专属服务和对应测试;删除工程链路时仍需核对是否被当前共享能力引用,不能连带移除仍在使用的公共契约或持久化事实。
- 最终落地:本次退役范围覆盖整个旧创作模板体系,包括 RPG / 自定义世界、拼图、拼消消、大鱼吃小鱼、敲木鱼、方洞挑战、视觉小说、汪汪声浪、寓教于乐、Creative Agent、Match3D、跳一跳和儿童动作 Demo。全部相关历史表继续作为数据壳参与 `spacetime-module` 编译,`migration.rs` 白名单与历史数据不变;旧 reducer/procedure/view、API 路由/handler/worker、前端页面/工作台/运行态、共享业务 DTO 和纯业务 crate 从编译链与依赖图移除,但旧源码和素材保留在仓库中用于历史追溯。
- 兼容读取:只保留历史审计、迁移和资产归属核对所需的最小读取定义;旧 `worldType`、公开作品号、URL、详情页和专属运行态均不再形成用户可访问入口。
- 方案文档:`docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md`。
## 2026-07-18 恢复现役平台公共壳但禁止旧业务依赖回流
- 背景:旧创作模板退役时误把新版 `/creation`、桌面公共侧边栏和“我的”完整资料页一起缩减;只恢复视觉后,现役 profile client 又经 `rpg-entry` barrel 把旧作品库、旧 runtime request 和展示模型重新带入 Vite 与 TypeScript 图。
- 决策:桌面端继续使用原平台公共结构,一级导航固定为 `创作 / 项目 / 我的`;顶栏保留编辑器项目 / 素材搜索、泥点入口和账号胶囊;“我的”全宽保留资料编辑、陶泥号、三项统计、充值、兑换码、社区、反馈、通用设置、API Key 和法律信息。搜索只面向编辑器项目与公开编辑器素材,不恢复旧公开作品搜索。
- 依赖边界:公共 dashboard、钱包、充值、兑换码、邀请码、API Key 和设置请求迁入 `services/platform-entry`,公共账单展示迁入现役 profile model。Vite 新增退役模块 graph 门禁,ESLint 对现役源码禁止导入旧目录;目录 watch ignore、Tailwind source、tsconfig include 和 tree-shaking 都不能作为依赖隔离证明。
- 路由与响应式边界:`/creation`、`/project`、`/profile` 都是可刷新、可前进 / 后退的稳定路由;桌面端使用侧边栏,移动端必须提供同样 `创作 / 项目 / 我的` 的三项底部 dock,不得因隐藏桌面侧边栏而丢失移动导航。
- 公共设置边界:`runtime_setting` 保持原表结构与历史数据,但它是音乐音量和平台主题的现役账号级公共能力,不归入旧玩法数据壳。鉴权后的 `GET/PUT /api/runtime/settings` 必须经 `spacetime-client` 调用 `get_runtime_setting_or_default` / `upsert_runtime_setting_and_return`;保留该路由不构成恢复旧 runtime API 的先例。
- 编译门禁:除旧业务目录外,`src/uiAssets.ts`、`src/types.ts`、`src/types/**`、`src/services/runtimeAudioFeedback.ts` 和 `src/services/publicWorkCode.ts` 也是顶层退役 module,必须同时退出 Vite module graph、TypeScript、ESLint 和 Vitest;`/audio/**`、`/chat.png`、`/fusion-pixel.ttf` 及旧 pixel / story-tab / 玩法 CSS 不得进入 dev 服务或生产产物。验收时必须同时检查 `tsc --listFilesOnly`、Vite 依赖图 / 产物和退役资产路径,不能只依赖 tree-shaking。
- Rust 产物边界:`module-runtime` 继续承载账号、钱包、公共设置、追踪和 feature gate,但 `CreationEntry*`、旧公开作品、存档、浏览历史与游玩统计 DTO / command / mapper / 规则必须退出实际 rlib;只保留历史表需要的 `RuntimeBrowseHistoryThemeMode`、完整保序的钱包流水来源枚举等持久化 ABI。`check:server-rs-ddd` 必须执行 `check:module-runtime-artifact`,同时验证旧符号和字面量为零、必要 ABI 仍存在,不能以源码存在 `#[cfg(any())]` 或路由未挂载代替产物证明。
- 外围编译边界:`platform-auth` 不再编译 runtime guest token,`platform-wechat` 不再编译旧玩法生成结果订阅消息,小程序不再注册订阅授权页;旧公开作品资产授权 view 退出 SpacetimeDB module,匿名素材读取只保留现役 editor showcase 派生授权。
- 历史队列边界:现役 external generation worker 只领取 `source_module = editor-canvas` 的任务,历史旧玩法 pending / running 行保持原状态,不得被新 worker 领取后改写为失败。
- Agent crate 边界:`platform-agent` 的执行器、工具注册表、回调和拼图 Phase 1 输入均属于已退役 Creative Agent 业务,不得因现役编辑器 Agent 共用一个模型名常量而留在 workspace 或 `api-server` 依赖图。该常量收口到 `platform-llm`,`platform-agent` 与仅由它引入的 `langchainrust` 退出在运 Cargo resolve graph,源码目录继续仅作历史追溯。
- 防回流补充:顶层 `creationEntryConfigService`、`creationUrlState`、`customWorld*`、`runtimeGuestAuth`、`runtimeRequest`、`input-devices`、`useCombatFlow`、`useStoryOptions`、`useMocapInput` 和微信生成订阅 facade 同样属于退役前端模块;Vite dev 对旧 `/api/creation*` 与 `/api/public-works*` 前缀直接返回 404,不能回落 SPA HTML。
- Vite 全量边界补充:`src/games/**`、`src/data/**`、`src/prompts/**`、旧顶层 App / Playground、旧路由和 `services/ai.ts` 必须由 pre-transform 门禁直接拒绝;所有同源 `/generated-*` 裸读在 dev 与生产统一为空 `404`,历史对象只经现役签名读取接口兼容,不允许 SPA fallback 伪装成资产成功响应。
- 前端混合根目录补充:`src/components`、`src/hooks`、`src/persistence`、`src/routing`、`src/services` 的根级文件实行现役白名单,Vite 与 ESLint 使用同一口径阻断旧 RPG / 玩法根文件;子目录仍按现役目录和退役目录分别管理,新增公共根文件必须显式登记。
- 影响范围:`PlatformEntryActiveFlowShell`、`PlatformActiveProfileView`、编辑器 / 项目搜索、平台 profile clients、`module-runtime`、`platform-llm`、Cargo workspace / resolve graph、Vite / ESLint / Rust 产物门禁及旧业务退役方案。
## 2026-07-20 VectorEngine 图片任务预算收口到 worker deadline
- 决策:`editor_image_generation`、`editor_image_edit`、`editor_icon_spritesheet_generation` 和 `editor_ui_design_asset_extraction` 使用默认 `1800s` long job 预算。worker 从同一起点计算绝对 job deadline,并向 provider 提前保留 `min(60s, job 预算 / 2)` 作为审计、OSS 和终态写回窗口。deadline 只经进程内 `RequestContext` 传递;VectorEngine 单 attempt 取配置 timeout 与剩余预算的较小值,退避加下一次 attempt 无法落在同一 deadline 内时停止重试,参考图和响应图片下载也受同一 deadline 限制。普通 HTTP / `inline` 保持无 deadline 行为;`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 默认仍为 `1000000`,配置加载层允许显式值更低。lease 续租 / fencing、迟到写回仲裁、attempt 耗尽和原子退款语义不变。
@@ -186,7 +186,7 @@ npm run check:rustfmt
cargo fmt --all --manifest-path server-rs/Cargo.toml
```
- 后端通用用户行为埋点统一通过 `record_tracking_event_and_return` procedure、`SpacetimeRuntimeClient::record_tracking_event(...)` 与 api-server `tracking` 中间件写入 `tracking_event` / `tracking_daily_stat`;后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 默认排除;作品级游玩埋点统一使用 `work_play_start`,详细事件清单见 `docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md`。
- 后端通用用户行为埋点统一通过 `record_tracking_event_and_return` procedure、`SpacetimeRuntimeClient::record_tracking_event(...)` 与 api-server `tracking` 中间件写入 `tracking_event` / `tracking_daily_stat`;现役账号、钱包、编辑器、项目、精选素材和公共设置路由按 `tracking.rs` 的显式静态路由表记录,后台路由默认排除。旧玩法、公开作品和专属运行态路由已经退役,不再维护作品级游玩埋点覆盖。
编码检查:
@@ -231,23 +231,15 @@ npm run check:native-shells
```
该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、完整相对路径文档反查、微信 capability 到真实 WebView / 支付 / 分享页面流程和测试清单的映射门禁、H5 HostBridge 事件订阅双能力门控反查、H5 `navigation.canGoBack` 消费 hook 与直达二级页返回锚点测试、移动端和桌面端单端源码清单门禁、Expo 壳 typecheck / test / EAS build config smoke / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面壳 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描,确认 Expo managed config、移动端 EAS 原生包构建 profile、移动端 iOS / Android production bundle、打包 H5 资产、Tauri release 入口、H5 页面内导航保留完整原生宿主上下文和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、Tauri `Info.plist`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件,但不扫描 Expo export、Tauri `target/`、Cargo / Metro 缓存或 release 构建产物。移动壳配置检查必须反查 EAS 生产 profile、文本 / 文档 / 图片 / 音频导入边界都来自共享 HostBridge 契约。登录与支付外链跳转必须保持在该调用链扫描内,`src/services/authService.ts` 和 `src/services/payment/paymentRedirect.ts` 是必扫文件;`AuthGate` 的登录成功、退出登录、身份边界刷新和登录状态异常重试都必须通过 `app.reloadWebView` 优先路径,并由 `src/components/auth/AuthGate.test.tsx` 进入该门禁。壳源码和配置继续严格禁止 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造 / 未实现 / 临时;H5 业务调用链允许正常表单 `placeholder` 属性、业务占位图文案和真实兼容 / 故障语义中的“未实现”“临时”表述,但仍禁止 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
创作 Agent 原生壳文档导入优先走 `file.importDocument`,旧壳只声明 `file.importText` 时才回退文本导入;相关变更必须让根级和单端门禁覆盖共享 method、capability profile、文档 MIME / 5 MiB 上限、读取前 size 校验,以及 H5 base64 转 `File` 后继续走后端文档解析的链路。
创作 Agent 参考图上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后继续交给既有 `onReferenceImageChange` 校验链路;用户取消原生选择不应再连带弹出浏览器文件输入,普通浏览器、小程序和未声明能力的裁剪壳才使用原隐藏文件输入。
创作 Agent 轻输入 composer 的参考图按钮在原生壳声明 `file.importImage` 时必须优先走宿主图片导入;移动壳声明 `file.captureImage` 时才显示拍摄参考图入口,并把宿主图片同样转为 `File` 后复用 `readPuzzleReferenceImageAsDataUrl` 的类型、大小、压缩和预览链路。
反馈页上传凭证在原生壳声明 `file.importImage` 时必须优先走宿主图片导入;移动壳声明 `file.captureImage` 时才显示拍摄凭证入口,并把拍摄图片同样转为 `File` 后复用反馈页原有数量、大小、MIME、data URL 预览和提交 payload 校验。
固定内置 H5 体验入口在原生壳声明 `navigation.openNativePage` 时必须优先走 `navigateHostNativePage()`;例如儿童动作热身 Demo 从平台首页进入 `/child-motion-demo` 时应由 HostBridge 发出 `navigation.openNativePage`,宿主不可用时才回退浏览器跳转。
Expo / Tauri 声明 `navigation.openNativePage` 时,只用于现役同源 H5 路由的受控导航和宿主上下文续接;微信小程序不再声明该能力。旧儿童动作 Demo、模板工作台、生成页、结果页和运行态不得作为 HostBridge 导航验收入口。
H5 支付链接跳转在原生壳声明 `app.openExternalUrl` 时必须优先走宿主系统浏览器;原生壳未接真实支付 SDK 前不得声明 `payment.request`,也不得把外部 H5 支付跳转伪装成原生支付成功。
微信 OAuth 登录授权 URL 在原生壳声明 `app.openExternalUrl` 时必须优先走宿主系统浏览器;原生壳未接真实登录 SDK 前不得声明 `auth.requestLogin`,也不得把网页登录跳转伪装成原生登录成功。
汪汪声浪结果页玩家 / 对手 / UI 背景三图槽位上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后继续交给 `uploadBarkBattleAsset` 与当前槽位写回链路;用户取消原生选择不应再连带弹出浏览器文件输入,普通浏览器、小程序和未声明能力的裁剪壳才使用原隐藏文件输入。
抓大鹅结果页发布封面图和封面参考图上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后复用现有封面 data URL 读取、AI 重绘开关、参考图集合和封面生成 payload 链路;用户取消原生选择不应再连带弹出浏览器文件输入。
RPG 角色资产工作室的角色参考图上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后复用现有 `readFileAsDataUrl` 参考图集合和角色形象生成 payload 链路;用户取消原生选择不应再连带弹出浏览器文件输入。
RPG 作品封面上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后复用现有 10 MiB 校验、图片尺寸读取、16:9 裁剪和 `uploadCustomWorldCoverImage` 保存链路;用户取消原生选择不应再连带弹出浏览器文件输入。
RPG 作品封面参考图上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后复用现有 `readImageFileAsDataUrl` 读取、预览和 `generateCustomWorldCoverImage` payload 链路;用户取消原生选择不应再连带弹出浏览器文件输入。
RPG 场景图片参考图上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后复用现有 `readImageFileAsDataUrl` 读取、预览和 `rpgCreationAssetClient.generateSceneImage` payload 链路;用户取消原生选择不应再连带弹出浏览器文件输入。
现役编辑器与个人中心新增文件导入能力时,继续复用共享 `file.importDocument`、`file.importImage`、`file.captureImage` 和 MIME / 大小门禁,不得把已退役模板的上传 service、玩法 DTO 或页面测试重新纳入原生壳门禁。
视觉小说结果页封面 / 角色 / 场景图片和音乐 / 环境音上传在原生壳声明 `file.importImage` / `file.importAudio` 时必须优先走宿主受控导入,并把 H5 base64 转 `File` 后继续交给 `uploadVisualNovelAsset` 与当前素材字段写回链路;用户取消原生选择不应再连带弹出浏览器文件输入,历史素材选择和 AI 图片生成保持原链路。
该命令会反查微信小程序 `WECHAT_HOST_CAPABILITIES` 与共享 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES` 一致;小程序生产代码继续保留 CommonJS 运行时镜像,不直接 import TypeScript shared 包。
该命令同时会运行微信小程序 `miniprogram/host-bridge/`、`miniprogram/shell/`、`pages/web-view` 样式和 `scripts/miniprogram-web-view-auth.test.ts` 的壳层测试,保证微信桥接层拆分后的支付、订阅消息、九宫切图、分享目标和 WebView 登录 / 分享入口行为与 Expo、Tauri 壳一起验收。
该命令还会反查微信小程序 `app.json.pages` 与 `host-bridge/protocol.js` 页面 URL、H5 小程序页面常量、H5 订阅授权页面常量、WebView 分享入口、分享目标消息类型、`WEB_VIEW_SOURCE_QUERY`、微信请求头运行时标记、H5 runtime parser、H5 路由保留字段和 H5 / API base URL 格式,避免页面路由、来源标记、宿主上下文 query 或域名配置在微信壳、H5 HostBridge 与运行时配置之间分叉。生产 / 开发 H5 与 API 域名都必须显式配置为纯 HTTPS domain,运行时开发域名回退生产域名只作为异常兜底。
该命令还会反查微信小程序 `app.json.pages` 与 `host-bridge/protocol.js` 现役页面 URL、H5 小程序页面常量、WebView 分享入口、分享目标消息类型、`WEB_VIEW_SOURCE_QUERY`、微信请求头运行时标记、H5 runtime parser、H5 路由保留字段和 H5 / API base URL 格式,避免页面路由、来源标记、宿主上下文 query 或域名配置在微信壳、H5 HostBridge 与运行时配置之间分叉。旧生成结果订阅授权页和对应 H5 service 不再进入清单。生产 / 开发 H5 与 API 域名都必须显式配置为纯 HTTPS domain,运行时开发域名回退生产域名只作为异常兜底。
内容检查:
@@ -314,8 +306,8 @@ npm run check:server-rs-ddd
- 移动端优先,再兼容网页端。
- 页面只展示后端返回的状态,不自行计算结论型业务状态。
- 创作中心入口配置事实源在 SpacetimeDB,通过 `GET /api/creation-entry/config` 下发;前端只在 `platformEntryCreationTypes.ts` 做展示派生,api-server 路由熔断也使用同一份配置,禁止恢复前端硬编码入口配置文件。底部加号创作入口页公告位也跟随后端 `eventBanners` 配置,前端只做展示和轮播;后台公告用表单维护标题与 HTML 内容,保存时再序列化为后端 `eventBannersJson` 传输字段。`最近创作` 不属于模板分类,不能作为分类缺失兜底;生成中和生成失败的真实草稿摘要都应进入最近创作。
- 一期统一创作页字段 spec 同样跟随 `GET /api/creation-entry/config`,由 `creationTypes[].unifiedCreationSpec` 下发;拼图、抓大鹅、敲木鱼之外的模板不接入该扩展位,前端只保留旧后端缺字段时的兜底默认。
- 现役一级入口为 `/creation`、`/project`、`/profile`,桌面侧边栏和移动端底部 dock 都固定显示“创作 / 项目 / 我的”。`/creation` 只读取图片编辑器项目与 `GET /api/editor/showcase/resources`,不得重新接入旧模板入口配置、旧作品架或专属运行态。
- 旧创作模板目录和顶层旧业务模块必须持续退出 Vite、TypeScript、ESLint 与 Vitest;旧 `/api/creation-entry/config`、模板 API、公开作品详情和运行态 API 必须保持未挂载。SpacetimeDB 历史表、迁移白名单与必要兼容类型只作为数据壳保留,不得据此恢复业务逻辑。
- 优先复用现有面板、抽屉、弹窗,不新建独立大系统。
- 不在 UI 中默认写功能说明类文本。
- 弹出独立面板的交互不要实现成在当前面板下方追加内容。
@@ -329,7 +321,7 @@ npm run check:server-rs-ddd
## 提交前建议让 Agent 执行
涉及拼图、抓大鹅、敲木鱼统一创作 / 生成链路、Phase 2 之后的跨玩法回归或本地 dev 栈时,先按 `quality-gates/README.md`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md` 和对应单项门禁文档执行自动脚本与体验检查。
涉及现役编辑器、项目、精选素材、账号、钱包、公共设置或本地 dev 栈时,按对应定向测试、类型检查、Rust / SpacetimeDB 门禁、真实浏览器 smoke 与本文件当前命令验收。旧跨玩法回归和单玩法质量门禁只作历史记录,不再作为提交前现役检查入口。
```text
请检查当前 git diff,指出:
+46 -5
View File
@@ -30,6 +30,14 @@
- 验证:覆盖三类任务正常成功都落透明结果与原图、原图位于透明图右侧、`generatedLayerId` 指向透明图、图标 / UI 拆分素材位于原图右侧,以及透明处理失败仍由原图单独完成占位。
- 关联:`server-rs/crates/api-server/src/editor_project.rs`、`docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md`。
## Vite 源码 CSS 清理插件必须早于 Tailwind 执行
- 现象:生产构建通过,但本地 dev 打开主站后全页白屏,`/src/index.css` 返回 500,Vite 报 `Unknown word updateStyle`。
- 原因:自定义 CSS 插件使用 `enforce: 'post'`,在 Tailwind/Vite 已把 CSS 转为包含 `updateStyle` import 的 JavaScript 模块后,仍调用 `postcss.parse`。
- 处理:需要改写原始 CSS 的 transform 固定使用 `enforce: 'pre'`;最终构建产物清理继续放在 `generateBundle`,不要混用两个阶段的输入格式。
- 验证:真实启动 `npm run dev` 后请求 `/src/index.css` 必须返回 200,并在浏览器确认 `#root` 已挂载且控制台无 CSS transform 错误。
- 关联:`vite.config.ts`、`scripts/vite-retired-css-plugin.test.ts`。
## phase 上报的业务拒绝与传输失败不能共用字符串错误
- 现象:provider 已经返回并保存原图,worker 上报 `processing` 时一次断连或超时就直接把任务判为失败;或者为了规避误杀而重试所有错误,导致 stale lease 的旧 worker 继续执行后处理。
@@ -280,12 +288,12 @@
- 验证:`npm run test -- src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx`;`cargo test -p api-server editor_character_animation --manifest-path server-rs/Cargo.toml`。
- 关联:`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`server-rs/crates/api-server/src/character_animation_assets.rs`、`server-rs/crates/api-server/src/app.rs`、`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`。
## 图片编辑器角色动画抽帧不要采到视频尾点
## 图片编辑器角色动画抽帧不要采到视频尾点或逐帧重启 FFmpeg
- 现象:画板角色图点击 `生成动画` 后,Ark 视频已生成并上传 OSS,但后端返回 `ffmpeg 已执行但未产出动作帧文件(requestId:...)`。
- 原因:FFmpeg 在 `-ss` 采样时间落到视频尾点附近时可能退出码仍为 `0`,但实际输出 `0` 帧;如果后端按 `duration - 0.001` 抽最后一帧,低帧率或短视频很容易踩到不可解码尾点。
- 处理:角色动画抽帧按目标帧数预留一个采样步长,例如 `32帧·4秒` 最后一帧采 `3.875s`,不要采 `3.999s`;`ffmpeg` 返回成功但无输出文件时,错误 details 保留 `targetSeconds`、`stdout`、`stderr` 和输出路径,用户主文案保持简短。
- 验证:`cargo test -p api-server editor_character_animation --manifest-path server-rs/Cargo.toml`,其中 `editor_character_animation_extracts_final_sample_from_short_video` 应覆盖本机 FFmpeg 8 的 0 帧回归。
- 原因:FFmpeg 在采样时间落到视频尾点附近时可能退出码仍为 `0`,但实际输出 `0` 帧;如果后端按 `duration - 0.001` 抽最后一帧,低帧率或短视频很容易踩到不可解码尾点。旧实现还会为 `32 / 40 / 48` 个采样点分别启动 FFmpeg、重复解码同一视频,在低配 worker 上形成不必要的多秒 CPU 尖刺。
- 处理:角色动画先按目标帧数计算全部安全采样时刻,例如 `32帧·4秒` 最后一帧采 `3.875s`,不要采 `3.999s`;随后使用单个 `setpts + split + select` filter graph 批量输出全部帧,不改用粗粒度 `fps` 抽帧。命令返回后逐一检查输出,缺帧时用户主文案保持简短,details 保留首个缺帧的 `targetSeconds / outputPath`、整批 `missingFrames` 和 stdout/stderr。
- 验证:`cargo test -p api-server editor_character_animation --manifest-path server-rs/Cargo.toml`;`editor_character_animation_batch_extracts_all_samples_from_short_video` 必须用一次 FFmpeg 产出整批短视频帧,尾帧测试继续锁定 `3.875s`。
- 关联:`server-rs/crates/api-server/src/character_animation_assets.rs`、`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`。
## Windows 本地角色动画抽帧找不到 ffmpeg 先查 dev 子进程环境
@@ -368,6 +376,14 @@
- 验证:测试断言菜单不包含在底部工具栏 / 参考图行里,并且生成面板打开时底部 `AI画布工具栏` 仍存在;规范参考图来源菜单应能通过 portal 点击“从画布中选择 / 上传图片”并写回规范参考图。
- 关联:`src/components/common/PlatformFloatingMenu.tsx`、`src/components/image-editor/ImageCanvasEditorView.tsx`、`src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx`。
## 图片编辑器 portal 菜单必须显式继承画板主题 token
- 现象:生成视频参数面板里点击“静音”后,开关轨道和白色滑块一起消失;如果直接把轨道改成 `#00ff00`,虽然重新可见,却变成与画板主题不一致的荧光绿。相同比例、清晰度、slider、时长文字和模型选中勾选也可能丢失选中态主题。
- 原因:`renderEditorPortal(...)` 把 `.image-canvas-editor__portal-menu` 挂到 `document.body`,它不再是 `.image-canvas-editor` 的后代,无法继承只定义在编辑器根节点上的 `--image-canvas-brand-*` 自定义属性。浏览器会把依赖缺失变量且没有 fallback 的声明按无效值处理,轨道背景最终为透明。
- 处理:portal 继续挂到 `document.body` 以避免局部 `overflow` 裁切,但外层必须通过 `.image-canvas-editor__portal-theme` 同步当前 `platform-theme--light / platform-theme--dark`;画板品牌 token 由 `.image-canvas-editor`、主题桥接层与 `.image-canvas-editor__portal-menu` 共用同一组声明。控件继续消费主题变量,不使用单点硬编码颜色,也不要只给静音轨道补 fallback 而遗漏同一 portal 内其它 token 消费者。
- 验证:`scripts/image-canvas-portal-theme.test.ts` 应锁定编辑器根节点、portal 主题桥接层与 portal 菜单共享完整品牌 token,静音 pressed 轨道仍使用 `var(--image-canvas-brand-accent)` 且不出现 `#00ff00`;`useImageCanvasGenerationSurface.test.tsx` 应覆盖暗色主题 class 被桥接到 `document.body` 下的 portal。真实浏览器从 `生成视频 -> 视频参数 -> 静音` 点击后,轨道 computed background 应为非透明当前主题色,portal 内 `--image-canvas-brand-accent`、`--image-canvas-brand-border-strong` 和 `--image-canvas-brand-soft` 均应有值。
- 关联:`src/index.css`、`scripts/image-canvas-portal-theme.test.ts`、`src/components/image-editor/ImageCanvasEditorPortal.tsx`、`src/components/image-editor/useImageCanvasGenerationSurface.tsx`、`src/components/image-editor/ImageCanvasGenerationComposerView.tsx`。
## 图片编辑器规范图片面板不要脱离统一生成 shell
- 现象:生成 UI 设计图或新建图标规范时,面板参考图、输入区和底部生成按钮相对生成图片 / 生成角色 / 生成视频错位;图标规范甚至可能缺少首行参考图入口。
@@ -2961,6 +2977,8 @@
## 小程序订阅消息授权不要依赖 web-view bindmessage
> 2026-07-18:本节及下一节只作为历史记录。生成结果订阅页、H5 service、HostBridge capability 和后端发送链路已随旧创作模板业务退役,不得按这些排障步骤恢复。
- 现象:拼图点击生成后,H5 以为已经请求了生成结果订阅授权,但小程序没有弹出 `wx.requestSubscribeMessage` 授权框。
- 原因:`web-view bindmessage` / `wx.miniProgram.postMessage` 不适合承接“当前用户点击后立刻请求授权”的时序,消息可能等到 web-view 后退、分享或销毁时才派发,导致授权请求没有发生在 `compile_puzzle_draft` 前。
- 处理:不要在原生页 `onLoad` 自动触发 `wx.requestSubscribeMessage`,真机会闪页返回且不弹授权框。H5 在 `compile_puzzle_draft` 前应先进入生成进度态并立即发起生成 action,再通过微信 JS SDK `miniProgram.navigateTo` 非阻塞跳转到小程序原生订阅页尝试请求授权;用户接受、拒绝或返回都不能阻塞生成。原生页不要改写上一页 `webViewUrl`,否则 web-view 可能重新加载首页并丢失进度页状态。后端发送订阅消息仍只允许在拼图资产成功或失败终态后执行。
@@ -2969,6 +2987,8 @@
## 微信订阅消息 time 字段不能用内部时间戳
> 2026-07-18:该能力已退役,本节不再作为现役排障入口。
- 现象:dev 服务器拼图资产生成终态后已经调用订阅消息发送,但日志出现 `微信订阅消息发送失败:argument invalid! data.time4.value invalid`,用户收不到生成结果通知。
- 原因:微信模板 `time` 字段不接受内部微秒时间戳、秒级时间戳或带 `Z` / 时区后缀的字符串;发送 `1713686401.234567Z` 或类似 `2026-06-08 08:09:18Z` 会被微信拒绝。
- 处理:`api-server` 构造生成结果订阅消息时,`time4` 固定格式化为北京时间 `YYYY-MM-DD HH:mm`;不要复用 `shared_kernel::format_timestamp_micros`。
@@ -3204,4 +3224,25 @@
- 原因:`UnifiedModal` 默认 portal 到 `document.body`;若业务入口只在页面内层继承 `platform-theme`,portal 根节点不会继承该容器的 CSS 变量。此时 `.platform-modal-shell` 的 `background: var(--platform-modal-fill)` 和 `.platform-overlay` 的背景声明都会失效。
- 处理:平台白底工具弹窗优先复用 `PlatformToolModalShell`,由共享壳读取当前 `AuthUiContext.platformTheme`,并把 `platform-theme platform-theme--<light|dark>` 挂到 portal overlay;不要用硬编码白底掩盖主题变量缺失。必须直接使用 `UnifiedModal` 的特殊场景,也要在 `overlayClassName` 显式传递当前平台主题。
- 验证:在 light / dark 主题下打开 portal 弹窗,断言 dialog 的 overlay 携带对应主题类,并在真实浏览器核对 panel 与遮罩的 computed background 均非透明。
- 关联:`src/components/project/ProjectGalleryView.tsx`、`src/components/common/PlatformToolModalShell.tsx`、`src/components/common/UnifiedModal.tsx`。
- 关联:`src/components/project/ProjectGalleryView.tsx`、`src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx`、`src/components/common/PlatformToolModalShell.tsx`、`src/components/common/UnifiedModal.tsx`。
## 待用户确认的 Agent 工具不能依赖模型自行结束回合
- 现象:画布 Agent 已生成有效工具规划,却最终只保存 `ERROR max turns reached: 3`,助手文本和待确认工具卡都消失。
- 原因:八类画布工具的 `call()` 只返回待用户确认的规划结果,但 function-calling runner 在成功工具后仍继续请求 LLM,只靠 prompt 要求模型不再重试;模型连续返回工具调用直到上限后,错误结果又丢弃此前累积的输出。
- 处理:工具通过框架契约显式声明 `requires_user_confirmation`;当本批全部工具都成功且等待确认时,runner 在处理完整批次后立即返回已有助手文本和工具结果。未知工具、参数错误、hook skip、普通连续工具和不可解析响应仍继续受 `max_turns` 门禁保护。不要用单纯提高轮次上限掩盖终止条件缺失。
- 验证:runner 回归测试必须同时覆盖“待确认工具只调用一次 LLM 并成功结束”和“普通连续工具仍会触发 max-turn 门禁”。
- 关联:`server-rs/crates/platform-editor-agent/src/framework/run.rs`、`server-rs/crates/platform-editor-agent/src/framework/tool.rs`、`server-rs/crates/platform-editor-agent/src/agent/tools/`。
## 前端退役目录不能只靠扫描和 ignore 隔离
- 现象:Tailwind `@source`、TypeScript 根 `include`、ESLint ignore 和 Vitest include 都排除了旧创作目录,但干净打开新版页面时,Vite 仍转换 `services/rpg-entry/index.ts`,构建产物也包含旧作品库和旧 profile 逻辑。
- 原因:现役模块的静态 import 会让 Vite、TypeScript 和打包器递归解析依赖;watch ignore 只停止监听,Tailwind source 只控制 class 扫描,tree-shaking 也发生在模块已经加载之后。经 barrel 只取一个公共函数尤其容易把同文件的旧导出一起带回图中。
- 处理:把仍在用的公共账号 / 钱包 / 设置能力迁到明确的现役 client 与 presentation model;Vite `pre` transform 对退役模块真实路径直接失败,ESLint 在现役源上增加 restricted imports。每次恢复公共 UI 后用 `tsc --listFilesOnly` 和全新浏览器 context 复核,不能用已有 HMR 会话判绿。
- 关联:`vite.config.ts`、`.eslintrc.cjs`、`src/services/platform-entry/`、`docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md`。
## VectorEngine 请求超时不能脱离 worker 绝对预算(2026-07-20)
- 现象:VectorEngine 单次请求超时大于 worker job 执行预算时,worker 已停止续租,provider 才超时或开始重试;最终 lease 过期、任务失败并退款,上游却可能继续消耗资源或迟到成功。
- 原因:单 attempt timeout、重试退避、图片下载与 worker / lease 分别使用独立的相对计时,没有共享同一绝对 deadline;只抬高 worker timeout 或单独压低 provider timeout 都无法保证留出终态写回窗口。
- 处理:实际调用 VectorEngine 的四类图片 job 使用 `1800s` long 预算;从 job 开始的同一起点派生 provider deadline,常规提前 `60s`、短预算提前一半。每次 attempt、退避、下一次 attempt 和图片下载都必须在该 deadline 内;普通 HTTP / `inline` 不伪造 worker deadline。修复时不改动 lease fencing、迟到写回仲裁和原子退款语义。
@@ -1,6 +1,6 @@
# Genarrative 项目共享概览
更新时间:`2026-07-17`
更新时间:`2026-07-18`
## 一句话定位
@@ -10,12 +10,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台,把 A
## 当前主要能力
- RPG / 自定义世界创作与运行时。
- 拼图玩法创作、草稿、发布、运行态和排行榜。
- 拼消消玩法创作、素材图集生成、结果页、发布、统一作品详情、正式运行态和基础统计。
- 敲木鱼玩法创作、草稿、发布、运行态、公开详情和分享码。
- 抓大鹅 Match3D 创作、2D 多视角素材生成、发布和运行态。
- 大鱼吃小鱼、方洞挑战、视觉小说、汪汪声浪和儿童向寓教于乐玩法。
- 图片画布编辑器、项目与资产管理。
- 账号、短信 / 密码 / 微信登录、个人资料、任务、钱包、邀请码、充值、反馈、法律信息和后台管理。
## 当前入口
@@ -26,7 +21,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台,把 A
- 小程序 WebView 外壳:`miniprogram/`。
- 法律文本:`media/files/user_agreement.md`、`media/files/privacy_policy.md`、`media/files/disclaimer.md`。
移动端一级 Tab:`推荐 / 发现 / 我的`。桌面端导航保留 `创作` 并新增 `项目`,其中 `/creation` 是独立创作工具主页,`/project` 是画布项目入口。
桌面端侧边栏和移动端底部 dock 的一级入口统一为 `创作 / 项目 / 我的`。`/creation` 是独立创作工具主页,`/project` 是画布项目入口,`/profile` 是“我的”稳定路由,继续承载账号、钱包、统计和通用设置等平台公共能力;刷新及浏览器前进 / 后退必须保持当前入口与选中态一致。
## 当前后端路线
@@ -50,6 +45,8 @@ server-rs + Axum + SpacetimeDB
明确废弃:旧 `server-node`、Express、PostgreSQL、Go 服务端、`maincloud`、人工 `spacetime --root-dir` 口径,以及前端承接正式业务真相的路线。
全部旧创作模板及其创作、生成、发布、公开业务详情与专属运行态已于 2026-07-17 退役。相关历史持久化表只以最小 schema 数据壳继续参与 `spacetime-module` 编译,迁移白名单与历史数据保持不变;旧前端、API、worker、procedure/reducer 和纯业务 crate 源码仅供追溯,不属于当前编译或运行能力。
## 当前文档入口
- `docs/README.md`
@@ -1,5 +1,7 @@
# 【前端架构】Platform Selection Stage Model 收口计划
> 2026-07-18 退役覆盖:本文涉及旧玩法 stage、生成页、结果页、公开详情和运行态的规则仅作为历史设计记录,不再进入现役前端编译链。当前稳定入口为 `/creation`、`/project` 与 `/profile`。
## 背景
`PlatformEntryFlowShellImpl.tsx` 在受保护数据失效后会清空当前用户的私有作品、运行态、草稿 notice 和生成状态。清理完成后,壳层还要判断当前 `SelectionStage` 是否还能继续展示:公开首页、公开详情、工作台入口等阶段可保留;结果页、生成页、运行态、个人反馈等依赖私有数据或运行态快照的阶段必须回到首页。
@@ -121,6 +121,7 @@
- 生成器快照刷新后必须恢复;待生成、生成中、失败和已生成后跟随成品图层的生成器都不能因为刷新丢失输入、参数、参考图或占位框位置。宣发素材生成器刷新后必须继续显示正确的卡片类型、游戏名、分类、描述和已绑定参考图。
- 画布多选语义必须同时覆盖普通图层和仍显示占位框的生成器对象:Shift 点选或框选可把生成器加入当前选择;拖动任一已选图层或生成器时,所有已选普通图层和生成器占位框同步移动;删除 / Backspace / Delete 作用于完整选择集合,移除所有已选图层和生成器对象。生成器对象在选择集合中使用稳定 `generation-dialog:<id>` 目标 ID,不把生成器伪装成普通图层,也不新增后端表。
- 生成类入口打开画布内面板时,底部 AI 工具栏必须保持可见;`生成规范`、角色 / 图标规范来源、角色常规参考图来源这类轻量菜单通过页面级 fixed portal 渲染,不能留在底部工具栏或参考图横向滚动容器内部,避免被局部 `overflow` 裁切。角色规范和常规参考图来源菜单必须向上弹出;常规参考图点击后先选择“从画布中选择”或“上传图片”,从画布取图时只绑定参考图,不触发普通画布图层选中、聚焦、面板隐藏或拖拽逻辑,绑定后退出画布选择状态。所有生成面板参考图槽位统一为方形图标组件;角色规范槽位只显示规范 logo 和 `角色规范` 四字,绑定来源标题只保留给可访问名称、悬浮 title 和图片信息。已有参考图槽位只有在 hover / focus 时显示右上角 `×`,点击后只解绑对应参考图。角色形象生成面板每次成功绑定角色规范后,在当前编辑器生命周期内缓存为上一张角色规范;再次新建角色形象时自动带入该缓存。图标素材和 UI 设计图面板每次成功绑定图标规范后,同样缓存为上一张图标规范;再次新建需要图标规范的素材时自动带入该缓存。生成规范菜单里的图标规范对象自身只把首行参考图作为可选参考,不要求必须先绑定图标规范。
- 所有挂到 `document.body` 的 `.image-canvas-editor__portal-menu` 必须放在同步当前 `platform-theme--light / platform-theme--dark` 的 `.image-canvas-editor__portal-theme` 桥接层下,并与 `.image-canvas-editor` 共用完整的 `--image-canvas-brand-*` token 声明。portal 内的比例 / 清晰度选中态、视频和音频 slider、静音开关、时长文字与模型选中勾选继续消费同一组主题变量;不得用 `#00ff00` 等硬编码颜色绕过变量作用域,也不得只修单个控件而让其它 portal 选中态继续退回无效声明。
- 生成规范类图片面板底部必须以禁用态参数按钮显示 `16:9·2K` 和 `gpt-image-2`,视觉对齐可编辑面板参数控件,提交到 `/api/editor/images/generations` 时也固定携带这些参数。
- 快速编辑面板底部只显示模型选择和 `修改` 按钮;打开时视口聚焦必须预留底部面板空间,面板位于素材下方,不得遮挡原素材,且素材在当前屏幕内完整可见。快速编辑请求只把原图或红框序号标注图作为 `sourceImageSrc` 直接提交,信息面板输入快照只展示用户填写的快速编辑提示词。
- 点击生成、生成规范、生成角色形象或生成图标素材后创建的占位图可继续保留;点击画布空白区域让当前图片或占位图失焦时,关闭当前生成面板并移除图片选中样式,但不删除占位图本身。
@@ -1,5 +1,7 @@
# 外部生成 Worker 化方案
> 2026-07-18 退役覆盖:旧创作模板 job 类型、玩法写回和玩法恢复链路均已退出现役 worker。当前 worker 只领取 `source_module = editor-canvas` 的任务;本文涉及拼图、跳一跳、拼消消、敲木鱼等玩法的内容仅作为历史设计记录,历史队列行不得被领取或改写。
更新时间:`2026-07-15`
## 背景
@@ -122,7 +124,11 @@ worker 配置:
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS`:空队列轮询间隔。
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS`:任务 lease 时长,默认 `600`;worker 会按约三分之一 lease、最长 30 秒的间隔续租。该值应覆盖一次心跳网络抖动窗口,不需要大于完整外部生成链路耗时。
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS`:普通外部生成 job 的执行预算,默认 `900`。超过预算后当前 worker 停止续租并释放 worker 槽位,但不取消已启动的业务 future,也不主动写入失败 / 重试状态;在途执行交由 lease fencing 仲裁:写回在租约有效期内到达则照常完成,否则被拒绝,租约过期后任务可被重新认领,attempt 耗尽时由认领事务原子标记失败并结算退款。
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS`:视频、角色动作等长耗时 job 的执行预算,默认 `1800`。
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS`:VectorEngine 图片生成 / 编辑、图标 spritesheet 生成、UI 素材提取以及角色动作、视频等长耗时 job 的执行预算,默认 `1800`。其中四类 VectorEngine 图片 job 固定为 `editor_image_generation`、`editor_image_edit`、`editor_icon_spritesheet_generation` 和 `editor_ui_design_asset_extraction`;手动去背景等不直接调用 VectorEngine 的 job 继续使用普通预算。
worker 在单次 job 开始执行时从同一个单调时钟起点计算绝对 `job deadline` 和更早的 `provider deadline`:常规情况下为终态审计、OSS 持久化及 `complete/fail` 回写保留 `60` 秒;当整个 job 预算小于 `120` 秒时,保留其一半,避免 provider 预算被全部吃掉。该 deadline 只通过进程内 `RequestContext` 传给 VectorEngine 图片调用,不写入 HTTP DTO、队列 payload 或 SpacetimeDB;普通 HTTP / `inline` 上下文没有 deadline,保持原有行为。
VectorEngine 每次发送的实际 timeout 取 `min(VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS, provider 剩余预算)`。遇到可重试传输错误或 408 / 429 / 5xx 时,只有当剩余预算还容得下本次退避和下一次 attempt 才继续;否则立即停止重试并返回当前 provider 错误,deadline 耗尽时返回 timeout。同一绝对 deadline 同时覆盖参考图下载、provider 请求 / 响应和响应图片 URL 下载,不允许请求已返回后的图片下载越过 provider 预算。`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 默认仍为 `1000000`;配置加载层允许显式值低于默认值,不再在读取环境变量时强制抬升。
controller 配置:
@@ -134,7 +140,7 @@ controller 配置:
- `GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_SERVICE_TEMPLATE`:systemd worker 模板,默认 `genarrative-external-generation-worker@{}.service`。
- `GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_DRY_RUN`:只记录决策不执行 systemctl,默认 `false`。
动态缩扩容方式:生产默认由 `deploy/systemd/genarrative-external-generation-controller.service` 启动 `GENARRATIVE_PROCESS_ROLE=external-generation-controller`,controller 读取 `get_external_generation_queue_stats_and_return` 后对 `genarrative-external-generation-worker@N.service` 执行精确 `systemctl start/stop`;无需改变 HTTP 进程数。controller 只操作 `@1..@MAX` 中的缺口或最高编号多余实例,保留 `@1` 作为保底 worker。缩容或发布重启 worker 时,进程收到 SIGINT/SIGTERM 后会停止 claim 新任务并等待当前任务完成;若进程被硬杀、机器断电或超过 systemd `TimeoutStopSec`,未完成任务会在 lease 过期后被其它 worker 重新领取。若 worker 内业务 future 长时间无返回,执行预算到期后 worker 会停止续租并释放槽位,在途 future 继续运行至租约仲裁窗口;有效租约内的写回仍可完成,租约过期后才会由其它 worker 重新认领,避免客户端取消与服务端写回竞态。容器链路已有独立 `external-generation-worker` compose service;扩 worker 必须扩这个 worker service,不能只扩 `api-server` HTTP service。
动态缩扩容方式:生产默认由 `deploy/systemd/genarrative-external-generation-controller.service` 启动 `GENARRATIVE_PROCESS_ROLE=external-generation-controller`,controller 读取 `get_external_generation_queue_stats_and_return` 后对 `genarrative-external-generation-worker@N.service` 执行精确 `systemctl start/stop`;无需改变 HTTP 进程数。controller 只操作 `@1..@MAX` 中的缺口或最高编号多余实例,保留 `@1` 作为保底 worker。缩容或发布重启 worker 时,进程收到 SIGINT/SIGTERM 后会停止 claim 新任务并等待当前任务完成;若进程被硬杀、机器断电或超过 systemd `TimeoutStopSec`,未完成任务会在 lease 过期后被其它 worker 重新领取。VectorEngine 图片链路会先于整个 job 执行预算停止 provider 发送 / 重试,以便 worker 在有效 lease 内完成终态写回;若其他业务 future 仍长时间无返回,执行预算到期后 worker 会停止续租并释放槽位,在途 future 继续运行至租约仲裁窗口;有效租约内的写回仍可完成,租约过期后才会由其它 worker 重新认领,避免客户端取消与服务端写回竞态。本次预算收口不改变 lease 续租 / fencing、迟到写回仲裁、attempt 耗尽收口和原子退款语义。容器链路已有独立 `external-generation-worker` compose service;扩 worker 必须扩这个 worker service,不能只扩 `api-server` HTTP service。
## 已接入的拼图纵切
@@ -1,5 +1,7 @@
# 统一公开作品 ReadModel 设计
> 2026-07-18 退役覆盖:统一公开作品 BFF、read model、逐玩法 source view 和互动链路均已退出现役编译与路由。相关历史表只作为 schema 数据壳保留;新版 `/creation` 只读取编辑器精选素材。本文其余内容仅作为历史设计记录。
更新时间:`2026-05-26`
## 背景
@@ -18,7 +18,7 @@
- 代理路径上的上游连接失败会返回统一 JSON;`502` 使用 `GATEWAY_UPSTREAM_ERROR`,`504` 使用 `GATEWAY_UPSTREAM_TIMEOUT`,避免 shadow / canary 阶段把框架默认错误页透给前端或巡检。
- 上游超时显式配置在 Pingora peer 上:连接超时默认 `3000ms`,没有 Nginx 显式长超时的代理路由读取默认 `60s`,通用 `/api`、公开列表 / 详情和 SpacetimeDB subscribe 读取默认 `3600s`,写入超时默认 `3600s`。读 / 写 / 连接超时统一映射为 JSON `504 GATEWAY_UPSTREAM_TIMEOUT`。
- 默认开启 gzip 响应压缩,`GENARRATIVE_PINGORA_GATEWAY_GZIP_LEVEL=5` 和 `GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES=1024` 对齐当前 Nginx `gzip_comp_level 5` / `gzip_min_length 1024`;`GENARRATIVE_PINGORA_GATEWAY_GZIP_ENABLED=false` 时禁用。`GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS=gzip` 是当前唯一允许的压缩算法白名单;网关会在进入 Pingora compression 模块前把 `Accept-Encoding` 收敛为 gzip,避免未验收的 `br` / `zstd` 被隐式打开。压缩能力由 `check:pingora-gateway-smoke` 用小响应不压缩、图片资源不压缩、大响应 `Accept-Encoding: gzip`、`Accept-Encoding: br, gzip`、`Content-Encoding: gzip`、`Vary: Accept-Encoding` 和解压后的响应体一起验证。Brotli 不进入当前 Pingora 正式化口径,仍由 Nginx / 前置代理能力探测承担;直连 Pingora 时不把 Brotli parity 作为切换门禁。
- 默认开启单进程内接流保护,按客户端 IP 与路由组执行并发上限、RPS 和 burst 限制。保护组默认对齐当前 Nginx:`admin_api=64/30rps/16burst`、`gallery_list=320/5000rps/4096burst`、`gallery_detail=32/300rps/32burst`、`api=64/300rps/64burst`、`spacetime=256/1000rps/256burst`。超限返回 JSON `429` 并带 `Retry-After: 1`。
- 默认开启单进程内接流保护,按客户端 IP 与路由组执行并发上限、RPS 和 burst 限制。保护组默认对齐当前 Nginx:`admin_api=64/30rps/16burst`、`api=64/300rps/64burst`、`spacetime=256/1000rps/256burst`。超限返回 JSON `429` 并带 `Retry-After: 1`。
- 当前接流保护的默认正式口径是单 Pingora 实例。`GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT` 默认 `1`;若 `GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED=true` 且 `GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1`,必须先落地共享限流 / 共享并发保护层,并显式设置 `GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED=true`,否则网关启动和目标机 direct preflight 都会失败。若关闭网关保护后横向多实例运行,则全局接流保护必须由前置 Nginx / LB 承担。
- 默认以 TCP 对端 IP 作为限流 client key;只有在 Pingora 前置代理已经清洗 `X-Forwarded-For` 时,才允许开启 `GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=true` 使用首个转发 IP。开启时必须同时设置 `GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=true`,否则网关会拒绝启动。若 Pingora 直接监听公网地址,`TRUST_X_FORWARDED_FOR` 必须保持 `false`,目标机 direct preflight 会在看到公网监听加该开关时失败。
- 可选配置 `GENARRATIVE_PINGORA_GATEWAY_GITEA_HOSTS` 和 `GENARRATIVE_PINGORA_GATEWAY_GITEA_UPSTREAM` 后,网关会先按 `Host` 归一化匹配 Gitea 域名,命中时整站代理到 Gitea 上游。Gitea 路由不走应用维护页、API 请求体上限或网关接流保护,避免影响 git clone / push。用于同一公网 IP 同时承载 `dev.genarrative.world` 与 `git.genarrative.world` 的直连切换时,TLS 证书必须同时覆盖两个域名;当前单 listener 配置只加载一组 cert/key。
@@ -70,7 +70,7 @@ npm run check:pingora-release-readiness
`check:pingora-canary-docker` 会启动 mock `api-server`、mock SpacetimeDB、真实 `pingora-gateway` 和 Docker Nginx,把前缀 canary 与真实路径 canary 两份 snippet 都渲染到临时 Nginx 中,再复用 `check:pingora-canary-live` 验证 Nginx -> Pingora -> 上游的 handoff 链路。临时 Nginx 使用生产同口径 `genarrative_upstream` access log,live smoke 后会继续调用 `scripts/check-pingora-canary-access-log-parity.mjs`,按同一 `request_id` 对账 canary healthz、代表性 API、SpacetimeDB identity 和静态资源路径;前缀模式确认 Nginx rewrite 后路径与 Pingora access log 一致,真实路径模式除 healthz 探针外要求 Nginx path 与 Pingora path 完全一致。默认不拉取镜像;缺少 Docker daemon 或 `nginx:1.27-alpine` 镜像时跳过。CI / 目标 agent 上需要把它作为硬门禁时执行 `node scripts/check-pingora-canary-docker.mjs --require-docker --pull`。
`check:pingora-canary-access-log-parity` 会烟测只读脚本 `scripts/check-pingora-canary-access-log-parity.mjs`,用于目标机 canary 后按 `request_id` 对照 Nginx handoff access log 和 Pingora tab-separated access log。真实目标机前缀 canary 执行时使用 `/opt/genarrative/current/scripts/check-pingora-canary-access-log-parity.mjs --nginx-log-file /var/log/nginx/genarrative.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log --path /__genarrative_pingora_canary/healthz --path /__genarrative_pingora_canary/api/creation-entry/config`;真实路径 canary 执行时追加 `--realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log --path /__genarrative_pingora_realpath_canary/healthz --path /api/creation-entry/config --path /v1/identity --path /assets/app.js`。脚本只读日志,不修改日志、不 reload Nginx 或 Pingora。
`check:pingora-canary-access-log-parity` 会烟测只读脚本 `scripts/check-pingora-canary-access-log-parity.mjs`,用于目标机 canary 后按 `request_id` 对照 Nginx handoff access log 和 Pingora tab-separated access log。真实目标机前缀 canary 执行时使用 `/opt/genarrative/current/scripts/check-pingora-canary-access-log-parity.mjs --nginx-log-file /var/log/nginx/genarrative.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log --path /__genarrative_pingora_canary/healthz --path /__genarrative_pingora_canary/api/editor/showcase/resources`;真实路径 canary 执行时追加 `--realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log --path /__genarrative_pingora_realpath_canary/healthz --path /api/editor/showcase/resources --path /v1/identity --path /assets/app.js`。脚本只读日志,不修改日志、不 reload Nginx 或 Pingora。
`check:pingora-direct-preflight` 默认只检查仓库内主 service、direct-entry drop-in 和 env 示例,适合本机提交前护栏。目标机直连切换窗口必须提供真实 env 并打开现场检查:
@@ -351,8 +351,8 @@ node -- /opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch
4. 在 `server {}` 内人工 include 该 snippet,并保持 `allow 127.0.0.1; allow ::1; deny all;` 或改成当次可信来源。
5. 执行 `npm run check:nginx-pingora-canary`;目标机或 CI 有 Nginx 时执行 `node scripts/check-nginx-pingora-canary.mjs --require-nginx`,再执行 `nginx -t && nginx -s reload`。
6. 执行 `GENARRATIVE_PINGORA_CANARY_BASE_URL=http://127.0.0.1 GENARRATIVE_PINGORA_CANARY_HOST=<域名> npm run check:pingora-canary-live`,确认 healthz、API、SpacetimeDB identity、静态资源和拒绝入口都带 `X-Genarrative-Nginx-Handoff: pingora-canary`。
7. 必要时再用同一前缀访问其它代表性路由,例如 `/__genarrative_pingora_canary/api/creation-entry/config`、`/__genarrative_pingora_canary/v1/identity` 和 `/__genarrative_pingora_canary/assets/app.js`,并对照直连 Nginx 正常入口和 Pingora shadow 日志。
8. 执行 current release 随包 `scripts/check-pingora-canary-access-log-parity.mjs --nginx-log-file /var/log/nginx/genarrative.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log --path /__genarrative_pingora_canary/healthz --path /__genarrative_pingora_canary/api/creation-entry/config`,确认同一 `request_id`、`path`、`status` 和 `proxy_target` 与 Nginx access log 可对齐。对账脚本的日志路径、canary prefix、必需路径和 `--since-lines` / `GENARRATIVE_PINGORA_CANARY_ACCESS_LOG_SINCE_LINES` 都不能包含换行或 NUL;若 access log 行里解析出的 URI / path 含控制字符,脚本也会把对应行记为失败,避免污染值进入 JSON 对账输出。
7. 必要时再用同一前缀访问其它代表性路由,例如 `/__genarrative_pingora_canary/api/editor/showcase/resources`、`/__genarrative_pingora_canary/v1/identity` 和 `/__genarrative_pingora_canary/assets/app.js`,并对照直连 Nginx 正常入口和 Pingora shadow 日志。
8. 执行 current release 随包 `scripts/check-pingora-canary-access-log-parity.mjs --nginx-log-file /var/log/nginx/genarrative.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log --path /__genarrative_pingora_canary/healthz --path /__genarrative_pingora_canary/api/editor/showcase/resources`,确认同一 `request_id`、`path`、`status` 和 `proxy_target` 与 Nginx access log 可对齐。对账脚本的日志路径、canary prefix、必需路径和 `--since-lines` / `GENARRATIVE_PINGORA_CANARY_ACCESS_LOG_SINCE_LINES` 都不能包含换行或 NUL;若 access log 行里解析出的 URI / path 含控制字符,脚本也会把对应行记为失败,避免污染值进入 JSON 对账输出。
9. 验证结束后移除 include 并 reload Nginx;不要把该前缀入口当作正式公网 URL。
## dev shadow service 验收记录
@@ -499,7 +499,7 @@ dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_CONNECT_TIMEOUT_MS` | `3000` | 连接上游的超时,必须大于 `0`。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_DEFAULT_READ_TIMEOUT_SECONDS` | `60` | 没有 Nginx 显式长超时的代理路由读取超时,必须大于 `0`。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_API_READ_TIMEOUT_SECONDS` | `3600` | 通用 `/api` 路由读取超时,对齐当前 Nginx `proxy_read_timeout 3600s`。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_LONG_READ_TIMEOUT_SECONDS` | `3600` | 公开列表 / 详情和 SpacetimeDB subscribe 长连接读取超时。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_LONG_READ_TIMEOUT_SECONDS` | `3600` | SpacetimeDB subscribe 长连接读取超时。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_WRITE_TIMEOUT_SECONDS` | `3600` | 写上游请求头 / 请求体超时,对齐当前 Nginx `proxy_send_timeout 3600s` 口径。 |
| `GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR` | `false` | 是否用 `X-Forwarded-For` 首个 IP 作为接流保护 client key;公网直连 Pingora 时必须保持 `false`,direct preflight 会阻断公网监听误开启。 |
| `GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED` | `false` | 开启 `TRUST_X_FORWARDED_FOR` 时必须显式设为 `true`,表示前置代理会清洗 `X-Forwarded-For`。 |
@@ -509,12 +509,6 @@ dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清
| `GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_MAX_CONCURRENT` | `64` | `/admin/api/*` 每 client 并发上限;`0` 表示不限制并发。 |
| `GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_RATE_PER_SECOND` | `30` | `/admin/api/*` 每 client token bucket 回填速率;`0` 表示不限制 RPS。 |
| `GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_BURST` | `16` | `/admin/api/*` 每 client 额外 burst。 |
| `GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_MAX_CONCURRENT` | `320` | 公开列表路由每 client 并发上限。 |
| `GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_RATE_PER_SECOND` | `5000` | 公开列表路由每 client RPS。 |
| `GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_BURST` | `4096` | 公开列表路由每 client burst。 |
| `GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_MAX_CONCURRENT` | `32` | 公开详情兼容路由每 client 并发上限。 |
| `GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_RATE_PER_SECOND` | `300` | 公开详情兼容路由每 client RPS。 |
| `GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_BURST` | `32` | 公开详情兼容路由每 client burst。 |
| `GENARRATIVE_PINGORA_GATEWAY_API_MAX_CONCURRENT` | `64` | 通用 `/api` 路由每 client 并发上限。 |
| `GENARRATIVE_PINGORA_GATEWAY_API_RATE_PER_SECOND` | `300` | 通用 `/api` 路由每 client RPS。 |
| `GENARRATIVE_PINGORA_GATEWAY_API_BURST` | `64` | 通用 `/api` 路由每 client burst。 |
@@ -536,13 +530,11 @@ dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清
| `/admin/assets/*` | 从 Web 根目录精确读取静态文件;带 Vite 指纹的文件默认长期缓存,其它文件默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 |
| `/admin/*` | 先读取静态文件或目录 index,失败回退 `/admin/index.html`,HTML 默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 |
| `/assets/*` | 从 Web 根目录精确读取静态文件;带 Vite 指纹的文件默认长期缓存,其它文件默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 |
| `/api/runtime/puzzle/gallery`、`/api/runtime/custom-world-gallery` | 转发到 `api-server`。 |
| `/api/runtime/puzzle/gallery/{id}`、`/api/runtime/custom-world-gallery/{profile}/{owner}` | 转发到 `api-server`。 |
| `/api`、`/api/*` | 转发到 `api-server`,按配置执行 `Content-Length` 与流式 body 累计上限检查。 |
| `/v1/database/{db}/subscribe`、`/v1/identity*` | 转发到 SpacetimeDB,保留 WebSocket Upgrade 头。 |
| `/__genarrative_pingora/healthz` | 仅在携带 `X-Genarrative-Pingora-Probe` 且匹配配置 token 时返回 shadow JSON,否则 404。 |
| `/v1/*`、`/generated-*`、`/healthz*`、`/readyz*` | 返回 404,保持生产公网不暴露口径。 |
| 主站 SPA allowlist | 只对当前前端完整路由及兼容恢复路径 `/creation/rpg/agent` 失败回退 `/index.html`;匹配大小写不敏感并允许一个尾部斜杠,HTML 默认 `no-cache`。 |
| 主站 SPA allowlist | 只对 `/`、`/creation`、`/project`、`/profile` 与 `/editor/canvas` 失败回退 `/index.html`;匹配大小写不敏感并允许一个尾部斜杠,HTML 默认 `no-cache`。 |
| 其它 Web 路径 | 只读取真实静态文件或目录 index,缺失时返回真实 404;`/creation/not-exist`、`/runtime/not-exist`、`/puzzle/not-exist` 不进入 SPA fallback。 |
维护模式下,公网 API-like 路由返回 JSON `503`;公网 Web 静态路由先读取 `GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_PAGE_FILE` 指向的 release 外运行态公告,缺失时回退 `GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT/maintenance.html`,两者都不存在时返回纯文本 `503`。版本化默认页不得包含日期或具体时段,临时公告由 `maintenance-on.sh --page-file` 安装并在 `maintenance-off.sh` 时清理。IPv4 loopback / RFC1918 / link-local 和 IPv6 loopback / ULA / link-local 来源绕过整站维护闸,主站页面与静态资源、普通 API、后台页面与后台 API、SpacetimeDB 路由均按非维护状态继续处理;应用层登录、管理员鉴权和其它业务鉴权保持不变。Pingora 直连按 TCP peer 判定来源;仅当 peer 是 loopback 的同机 Nginx 时才接受 Nginx 强制覆盖的 `X-Real-IP`,绝不使用客户端可伪造的 `X-Forwarded-For` 做维护放行。该放行只绕过网关维护响应;若 `pause-after-stdb` 已停止 api-server,内网普通 API 和后台 API 仍不可用。
@@ -556,7 +548,7 @@ dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清
2. 涉及 Nginx 模板、Pingora 路由、限流分组或路由文档时,同步更新 `deploy/pingora/nginx-route-parity.matrix.json`,并运行 `npm run check:pingora-route-parity` 与 `cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml matches_nginx_route_parity_matrix`。
3. 容器内使用同一份 Web 产物、同一组真实上游地址跑 Pingora smoke,并继续对照 `deploy/nginx/genarrative.conf` 扩展真实上游路由 parity 自动测试。
4. 使用 `deploy/nginx/snippets/genarrative-pingora-canary.conf` 做 Nginx 前缀 canary;启用前先跑 `npm run check:nginx-pingora-canary` 和 `npm run check:pingora-canary-docker`,有 Nginx 或 Docker 的目标环境分别强制跑 `node scripts/check-nginx-pingora-canary.mjs --require-nginx` 与 `node scripts/check-pingora-canary-docker.mjs --require-docker --pull`,其中 Docker handoff 会自动对账临时 Nginx 与 Pingora access log。启用后跑 `npm run check:pingora-canary-live`,再用 current release 随包 access log parity 脚本对账目标机 Nginx 与 Pingora access log。canary live 的 base URL、prefix、Host、额外 path 和 timeout 不能包含换行或 NUL;canary live timeout 和 access log `since-lines` 必须是正整数,非法值直接失败。
5. 前缀 canary 通过后,再使用 current release 随包 `/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-enable.sh --apply --probe-token <token> --host <域名> --base-url http://127.0.0.1:18083` 启用真实路径 canary;它会把 `deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf` 渲染成独立本机 `server`,写入 `/etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf`,不能 include 到生产 `443` server 内。该片段使用 `access_log ... genarrative_upstream`,文件名必须保证晚于定义 `log_format genarrative_upstream` 的主站配置加载;否则 `nginx -t` 会报 `unknown log format "genarrative_upstream"`。启用脚本会先执行 `nginx -t`、reload Nginx,再默认运行 realpath live smoke,任一阶段失败都会恢复写入前配置。关闭时执行 `/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-disable.sh --apply`,脚本会在 `nginx -t` 或 reload 失败时恢复删除前配置。启用后跑 `node -- /opt/genarrative/current/scripts/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host <域名>`,再用 current release 随包 access log parity 脚本传 `--realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log` 对账 `/api/creation-entry/config`、`/v1/identity` 和 `/assets/app.js` 等真实路径。
5. 前缀 canary 通过后,再使用 current release 随包 `/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-enable.sh --apply --probe-token <token> --host <域名> --base-url http://127.0.0.1:18083` 启用真实路径 canary;它会把 `deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf` 渲染成独立本机 `server`,写入 `/etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf`,不能 include 到生产 `443` server 内。该片段使用 `access_log ... genarrative_upstream`,文件名必须保证晚于定义 `log_format genarrative_upstream` 的主站配置加载;否则 `nginx -t` 会报 `unknown log format "genarrative_upstream"`。启用脚本会先执行 `nginx -t`、reload Nginx,再默认运行 realpath live smoke,任一阶段失败都会恢复写入前配置。关闭时执行 `/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-disable.sh --apply`,脚本会在 `nginx -t` 或 reload 失败时恢复删除前配置。启用后跑 `node -- /opt/genarrative/current/scripts/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host <域名>`,再用 current release 随包 access log parity 脚本传 `--realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log` 对账 `/api/editor/showcase/resources`、`/v1/identity` 和 `/assets/app.js` 等真实路径。
6. 目标机 canary include 后必须跑正式切换聚合门禁,并按现场已启用的 canary 入口选择参数:前缀 canary 已启用时,源码 checkout / CI / 构建环境执行 `node scripts/check-pingora-release-readiness.mjs --require-docker --pull-docker --require-nginx --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`,目标机 current release 执行 `/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`;真实路径 canary 已启用时,追加或单独使用 `--require-realpath-live --realpath-live-base-url http://127.0.0.1:18083 --realpath-live-host <域名> --realpath-live-nginx-access-log /var/log/nginx/genarrative-pingora-realpath-canary.access.log --realpath-live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`。如果现场只启用了真实路径 canary,不要同时传 `--require-live`;缺少 Host 会直接失败,避免 live canary 误测默认 vhost。live smoke 后还会按 `request_id` 对账 Nginx 与 Pingora access log,缺少同一请求的 Pingora 日志、状态码、方法或 path 漂移都会失败。
7. 如需评估 Pingora 直连公网入口,必须显式配置 `TLS_LISTEN`、证书、私钥和 `HTTP_REDIRECT_LISTEN`;Certbot 证书先用随包 `scripts/deploy/pingora-tls-cert-sync.mjs` 同步到 `/etc/genarrative/pingora-tls/<域名>/`,不要直接 chmod Let’s Encrypt live/archive 原路径;同一 IP 上还有 Gitea 域名时,还必须配置 `GITEA_HOSTS` / `GITEA_UPSTREAM` 并确认 TLS 证书覆盖所有由 Pingora 直连接管的 Host。绑定 `80/443` 时还必须人工启用 `genarrative-pingora-gateway-direct-entry.conf` drop-in 授予 `CAP_NET_BIND_SERVICE`。随后用 `npm run check:pingora-gateway-smoke` 覆盖 TLS / HTTP/2 ALPN / redirect / WSS subscribe / Gitea Host 分流;目标机必须先跑 `npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env --systemd-cat --check-cert-readable --check-service-env-file --check-service-user-cert-readable --check-service-binary-executable --check-ports-free`,再跑 `npm run check:pingora-direct-live` 或 release readiness 的 `--require-direct`,且 `--require-direct` 必须带 direct HTTPS base URL、direct HTTP base URL、正式域名 Host/SNI、redirect Location host、Pingora access log 文件、health patrol env 文件、direct preflight env 文件、systemd drop-in 生效检查、service EnvironmentFile 一致性检查、当前用户和 systemd 服务用户证书可读检查、service 二进制可执行检查、显式 SpacetimeDB 数据库名,并会拒绝 `--skip-wss`。高端口 rehearsal 使用 `https://127.0.0.1:<高端口>` 打入但期望 HTTP redirect Location 指向正式域名默认 HTTPS 入口时,额外传 `--direct-redirect-base-url https://<域名>`;`--direct-redirect-host` 仍必须保留,用于 runbook Host 一致性约束。direct live 会用生成的 `request_id` 反查 Pingora access log;缺少对应日志、method 漂移、path 漂移或 status 漂移都算直连门禁失败。direct preflight 会拒绝开启网关保护但未确认共享保护层的 `GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1` 配置;`--env-file`、`--systemd-service`、服务用户和 env 中的 listen / cert / key 值都不能包含换行或 NUL,执行 `systemctl cat` 或 `sudo -u <serviceUser> test -r <file>` 前还会复核子命令参数,避免污染参数进入目标机预检命令;direct live 的 URL、Host、redirect base URL、probe token、额外 path、数据库名、access log 路径、timeout 和布尔 env 也不能包含换行或 NUL,且会在发起请求前失败;direct live timeout 必须是正整数,直连相关布尔 env 只接受 `true/false`、`1/0`、`yes/no`、`on/off` 或空值,非法值直接失败。证书申请与续期仍由 Certbot / 外部自动化承担,网关只读取现有文件。
8. 正式直连 runbook 的启用前基础门禁和启用后 `--require-direct` 复核必须调用 `/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only`。该脚本、`scripts/check-pingora-canary-live.mjs`、`scripts/ops/pingora-direct-rehearsal-status.mjs`、realpath canary 启停脚本和 `deploy/nginx/` 必须进入生产 API release、Jenkins API Build 归档、Jenkins API Deploy 复制清单和目标机 current release;缺失时部署应 fail-fast,切换窗口不能依赖源码 checkout 或 Jenkins workspace。
@@ -0,0 +1,85 @@
# 旧创作模板业务退役方案
更新时间:`2026-07-18`
## 目标
退役整个旧创作模板体系及其专属运行态,同时保留全部相关历史持久化表、迁移白名单和必要的兼容读取定义。历史数据不删除,持久化表及字段不删除、不改名、不重排、不改变类型。
本次执行口径是“数据壳保留,业务实现退役”:历史表继续以最小 schema 代码随 `spacetime-module` 编译和发布;旧模板选择、生成、发布、公开业务详情、作品广场、排行榜和专属运行态不再进入正式编译链或运行路由。基于图片编辑器项目与公开素材的新版 `/creation` 创作工具主页及平台公共侧边栏继续作为现役能力保留。
## 范围
### 退役
- RPG / 自定义世界、拼图、拼消消、大鱼吃小鱼、敲木鱼、方洞挑战、视觉小说、汪汪声浪、寓教于乐、Creative Agent、Match3D、跳一跳和儿童动作 Demo 的前端页面、路由、工作台、结果页、运行态、service、测试及专属素材生成入口。
- `api-server` 中上述业务的 router、handler、service、生成与发布编排、公开详情、专属 runtime 和 worker 启动点。
- `spacetime-client` 的模板业务 facade、mapper、reducer/procedure 调用和业务 DTO 依赖。
- `spacetime-module` 的模板 reducer、procedure、业务 view、初始化逻辑和领域规则调用。
- 所有纯模板 crate、RPG 专属运行态 crate 及旧 Creative Agent 的 `platform-agent` crate 的 workspace/default 目标和在运依赖边。
- 后台及主站中只服务于旧创作入口的配置、灰度、展示、搜索、启动、追踪和运维门禁。
### 保留
- 全部旧模板历史持久化表及其原有字段顺序、字段类型、默认值、索引和可见性。
- `migration.rs` 中相关表的迁移白名单、表名兼容和字段目录。
- 为历史审计、迁移、资产归属核对所必需的最小只读表定义;不得借兼容读取重新暴露旧创作、发布、公开详情或运行接口。
- 编辑器、项目、账号、钱包、资产、HostBridge、运维和安全等平台公共能力。
- 新版 `/creation` 创作工具主页、`/project` 项目入口、稳定的 `/profile` 个人页路由、`creation-home` 展示组件与现役静态资产。桌面端保留“创作 / 项目 / 我的”公共侧边栏,移动端保留同样三项的底部 dock;“我的”保留头像 / 昵称编辑、陶泥号复制、钱包与账单、统计、充值、兑换码、玩家社区、反馈、通用设置、开发者 API Key 和法律信息,不恢复旧模板入口、旧作品架或生成队列。
- `runtime_setting` 是账号级公共设置事实,不属于旧模板运行态。原表结构和数据不变,继续由鉴权后的 `GET/PUT /api/runtime/settings`、`get_runtime_setting_or_default` 与 `upsert_runtime_setting_and_return` procedure 支撑音乐音量和平台主题读写。
- 旧页面、测试、素材、handler、service、worker、生成 bindings 和纯业务 crate 的源码目录;它们仅用于历史追溯,不属于任何正式入口或编译目标。
## 编译边界
### 前端
- 主路由、Vite 入口和运行时动态 import 不得引用旧业务目录。
- 正式应用入口使用 `active-main.tsx`、`ActiveApp.tsx`、`routing/activeAppRoutes.tsx`、`routing/activeAppPageRoutes.ts`、`services/activeAppTitle.ts`、`platform-entry/PlatformEntryActiveFlowShell.tsx`、`platformEntryActiveTypes.ts` 和 `creation-home/`;原同名非 active 文件保持历史源码原貌并退出 Vite、TypeScript、ESLint 和 Vitest。
- 旧业务源码与素材保留在仓库中;Vite 不得再引用入口或动态 import,TypeScript、ESLint、Vitest 必须明确排除旧目录和专属测试。退役源码只用于历史追溯,不允许从在运代码重新导入。
- 平台公共 profile 请求与展示模型必须位于 `services/platform-entry/` 和现役 `platform-entry` 文件,不得因为沿用账号、钱包或设置能力而继续 import `services/rpg-entry`、`services/rpg-runtime` 或 `components/rpg-entry`。Vite 对退役模块实行实际 module graph 门禁,命中即中止 dev/build;ESLint restricted imports 作为更早的源码反馈。
- 原 `main.tsx`、`App.tsx`、旧路由、旧标题映射、`PlatformEntryFlowShellImpl.tsx` 与旧入口类型保持原样;正式链路由 `active-main.tsx`、`ActiveApp.tsx`、`activeApp*`、`PlatformEntryActiveFlowShell.tsx` 和 `platformEntryActiveTypes.ts` 承载,只包含新版创作主页、项目、编辑器、账号、设置与钱包公共能力。`retired/legacy-creation-templates/frontend/original/` 另保留逐文件原样快照。
- 退役 CSS 的源码过滤必须在 Tailwind/Vite 转换前执行,产物过滤留在 `generateBundle`;禁止在 `post` transform 中把 Vite 已生成的 JavaScript 样式模块重新交给 PostCSS 解析。
- 顶层退役 module 也必须受编译门禁约束:`src/uiAssets.ts`、`src/types.ts`、`src/types/**`、`src/services/runtimeAudioFeedback.ts`、`src/services/publicWorkCode.ts`、`creationEntryConfigService`、`creationUrlState`、`customWorld*`、`runtimeGuestAuth`、`runtimeRequest`、`input-devices/**`、`useMocapInput`、`wechatMiniProgramSubscribe`、`useCombatFlow` 和 `useStoryOptions` 不得进入 Vite module graph、TypeScript、ESLint 或 Vitest,现役公共品牌资产应从独立公共定义导入。
- `src/games/**`、`src/data/**`、`src/prompts/**`、旧 `App.tsx` / `main.tsx` / `RpgRuntimeApp.tsx` / `*PlaygroundApp.tsx`、旧 `appRoutes` / `appPageRoutes` 和 `services/ai.ts` 同属退役源码边界;Vite 必须在 pre-transform 阶段拒绝直接请求,不能只依赖现役入口未 import 或构建 tree-shaking。
- `src/components`、`src/hooks`、`src/persistence`、`src/routing` 和 `src/services` 根级文件是新旧混合区,Vite 与 ESLint 必须使用显式现役白名单。当前只放行正式入口实际依赖的根级公共模块;新增根级公共模块时必须同步登记,未登记文件按退役源码处理。各现役子目录继续按独立目录边界放行。
- 退役静态资产 `/audio/**`、`/chat.png` 和 `/fusion-pixel.ttf` 不得由 Vite dev server 提供,也不得进入生产产物;旧 pixel / story-tab / 玩法 CSS 仅能在历史源码中存在。
- 所有同源 `/generated-*` 裸读路径在 Vite、Nginx、Pingora 和 `api-server` 均返回空 `404`;历史对象 key 只允许作为 `legacyPublicPath` 进入 `/api/assets/read-url` 等现役签名读取链,不恢复旧生成资产代理。
- `/creation`、`/project` 和 `/profile` 是现役稳定路由,刷新及浏览器前进 / 后退必须保持当前页签。生产网关对旧子路径返回 404,客户端若收到未知旧页面路径则回落当前平台公共首页;Vite dev 对旧 `/api/creation*` 与 `/api/public-works*` 直接返回 404,不能回落为 SPA HTML。小程序不再注册旧生成结果订阅授权页。
### Rust
- `api-server` 不声明模板模块,不挂模板路由,不保留模板 worker 启动点。
- 现役 external generation worker 只领取 `source_module = editor-canvas` 的任务,历史旧玩法 pending / running 行不得被领取或改写。
- `spacetime-client` 不保留模板业务 facade 和 mutation 调用;历史表生成绑定只允许服务必要兼容读取。
- `spacetime-module` 对旧模板只编译历史表结构,不导出旧 reducer、procedure、业务 view 或领域规则。
- 旧 `public_work_asset_read_grant` view 与十类旧作品授权计算退出 module;匿名资产读取只保留现役 editor showcase 授权。
- `spacetime-module` 与 `spacetime-client` 的 Cargo `lib.path` 固定指向各自的 `src/active.rs`;原 `src/lib.rs` 及旧业务源码继续原位保留,但不再作为 crate 根参与编译。
- 历史表最小定义集中在 `spacetime-module/src/legacy_schema/` 与 `spacetime-module/src/runtime/legacy_schema/`,混合 profile 表的在运数据壳位于 `spacetime-module/src/runtime/active/profile.rs`;这些目录只允许 schema 和必要兼容读取定义。
- `module-runtime` 仍是账号、钱包、公共设置、追踪和 feature gate 的现役领域 crate;其混合源码中的 `CreationEntry*`、旧公开作品、旧存档 / 浏览历史 / 游玩统计 DTO、command、mapper 和规则必须以编译条件退出,且不再依赖只为旧创作契约存在的 `shared-contracts`。历史 schema 只继续编译 `RuntimeBrowseHistoryThemeMode` 六个变体和完整保序的 `RuntimeProfileWalletLedgerSourceType` 等持久化 ABI,不保留围绕这些类型的旧业务实现。
- 纯模板 crate 和专属运行态 crate 不属于 workspace members、default members 或任何在运 crate 的依赖图;源码目录保持原样。
- `platform-agent` 及其专属 `langchainrust` 依赖同样退出 workspace 与 `api-server` 依赖图;现役编辑器 Agent 仅需的模型常量收口到 `platform-llm`,不再通过旧拼图 Phase 1 / Creative Agent 执行器 crate 复用。
- `platform-auth` 不再编译 runtime guest token;`platform-wechat` 不再编译旧生成结果订阅服务,只保留现役认证和支付协议。
## 验收
- 旧 URL 不再命中旧页面或后端路由。
- `/creation`、`/project` 与 `/profile` 在桌面端显示“创作 / 项目 / 我的”公共侧边栏,在 `390x844` 等移动视口显示同样三项的底部 dock;点击、刷新及浏览器前进 / 后退均保持路由与选中态一致。新创作主页只调用编辑器项目和公开编辑器素材接口;顶栏保持现役搜索、公共泥点入口与账号胶囊,不重新拼装平行账号按钮组。
- “我的”桌面布局按原平台公共资料页全宽展示四个常用入口、两行设置和法律栏;头像、昵称、复制、充值、兑换码、社区、反馈、API Key 等入口可用,但不发起旧模板、旧公开作品或旧运行态请求。
- 鉴权访问 `GET/PUT /api/runtime/settings` 不得返回 404,读写必须经 `spacetime-client` 调用现役 settings procedure;未鉴权请求返回 401,不恢复任何旧运行态设置路由。
- `tsc --listFilesOnly` 与 Vite 干净加载均不得出现旧业务目录、上述顶层退役 module 或小程序旧订阅授权实现。
- 直接请求代表性的旧 `games` / `data` / `prompts` / 顶层 App 模块必须被 Vite module graph 门禁拒绝;现役 `creation-home`、项目、profile 与 editor 模块仍正常转换。
- 根级旧组件、hook、persistence、routing 和 service 必须同时被 Vite 拒绝并被 ESLint 忽略;现役根级图片解析、设置、路由和 API client 文件必须继续参与两套门禁。
- Vite 构建产物和依赖图不包含旧前端业务目录、Fusion Pixel / pixel 业务样式或旧 runtime 声音签名;产物中不存在 `dist/audio/**`、`dist/chat.png` 或 `dist/fusion-pixel.ttf`。
- 旧生成资产前缀和现役 editor 对象前缀的同源裸读都必须返回空 `404`,不能返回 `index.html` 形成 soft 404;编辑器真实资产读取继续走签名 URL。
- `cargo tree` 中不存在纯模板 crate、专属运行态 crate、`platform-agent` 或 `langchainrust`。
- `npm run check:module-runtime-artifact` 对实际 `module_runtime.rlib` 的 object 成员执行负向扫描:旧创作、存档、浏览与游玩符号和字面量必须为零,同时 `RuntimeBrowseHistoryThemeMode`、`RuntimeProfileWalletLedgerSourceType` 与 `RuntimeSettingSnapshot` 等兼容 ABI 必须仍存在。
- `spacetime-module` 编译结果仍包含全部历史表,但不包含任何旧模板 reducer、procedure 和业务 view。
- `platform_auth.rlib` 不包含 runtime guest token 符号,`platform_wechat.rlib` 不包含订阅消息发送符号;历史旧队列行不满足现役 worker claim 条件。
- `npm run check:spacetime-schema`、定向 Rust 检查、前端类型检查、`npm run check:encoding`、`git diff --check` 通过。
## 历史记录兼容
- 历史数据库行、作品号、`worldType` 和资产对象前缀可以继续被审计或迁移工具识别,但不再形成面向用户的列表、详情、创作或运行入口。
- 旧公开作品号、创作 URL 和统一创作规格不再作为前端可启动契约;未知或旧 URL 统一回落平台公共首页。
- 历史表名、资产对象前缀和后台数据库表目录可继续出现旧域名,它们属于数据审计与资产读取边界,不代表旧业务仍在运行。
- 原本与历史表混在同一文件的 reducer/procedure 实现按模块保存在 `retired/legacy-creation-templates/rust/`,不属于任何 Cargo workspace/module;从全局样式表移出的专属 CSS 保存在同目录的 `frontend/` 下且不被 Vite 导入。
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -56,7 +56,7 @@ RAG 默认不安装运行时依赖,也不把 LanceDB、Transformers.js 或本
- 移动端优先,同时保证桌面端体验完整。
- 弹出独立面板的交互使用弹窗、抽屉、popover 或页面级 portal,不在当前面板下面追加内容。
- 页面展示以后端返回状态为准,不在前端自行计算结论型业务状态。
- 创作入口事实源来自 SpacetimeDB,经 `/api/creation-entry/config` 下发;前端只做展示派生。
- 现役平台入口固定为 `/creation`、`/project`、`/profile`:创作主页只读取图片编辑器项目与公开编辑器素材,个人页只复用账号、钱包和公共设置能力。旧 `/api/creation-entry/config` 及模板工作台、公开作品和专属运行态已经退役,不得因历史表仍在而恢复前端入口或后端接口。
- 优先扩展现有公共组件,例如平台弹窗、图片输入、媒体预览、状态提示和动作按钮,不在业务页复制通用逻辑。
## 后端与数据真相
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -20,7 +20,6 @@
- API 契约:`packages/shared/src/contracts/runtime.ts`、`server-rs/crates/shared-contracts/src/runtime.rs`
- 后端下单与订单编排:`server-rs/crates/api-server/src/runtime_profile.rs`、`server-rs/crates/api-server/src/wechat/pay.rs`
- 微信支付 / 虚拟支付协议适配:`server-rs/crates/platform-wechat/src/pay.rs`
- 微信订阅消息协议适配:`server-rs/crates/platform-wechat/src/subscribe_message.rs`
- WebView 回流确认:`GET /api/profile/recharge/orders/{orderId}/wechat/events`、`POST /api/profile/recharge/orders/{orderId}/wechat/confirm`
- 微信登录态保存:`server-rs/crates/platform-auth/src/lib.rs`、`server-rs/crates/module-auth/src/lib.rs`
@@ -38,9 +37,6 @@ WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_QUERY_ORDER_ENDPOINT=https://api.weixin.qq.c
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_NOTIFY_PROVIDE_GOODS_ENDPOINT=https://api.weixin.qq.com/xpay/notify_provide_goods
WECHAT_MINIPROGRAM_MESSAGE_TOKEN=<微信消息推送 Token>
WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY=<微信消息推送 EncodingAESKey>
WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED=true
WECHAT_MINIPROGRAM_GENERATION_RESULT_TEMPLATE_ID=m5z7BkkBhJGbcH0cdDeHaeRU2tViDEguP38XdrRRCdU
WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE=formal
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV=0
```
@@ -187,5 +183,4 @@ npm run spacetime:wechat-virtual-payment:reconcile -- \
- Web 侧在拉起虚拟支付后会短时轮询 `wx_pay_result`,即使小程序 `web-view` 回写 hash 没触发浏览器 `hashchange`,也必须展示回写的微信错误内容。
- WebView 返回但没有拿到 `wx_pay_result` 时,前端必须主动调用订单确认接口,并接入 `/api/profile/recharge/orders/{orderId}/wechat/events` 的 SSE 事件流作为服务端推送兜底;虚拟支付确认接口会使用当前用户后端保存的小程序 `openid` 调用官方 `/xpay/query_order`,查到已支付且契约校验通过后写入订单。后端通过消息推送或查单入账后都会发布订单更新,SSE 先推当前订单快照,再在订单结束时推 `done`。
- Web Native 二维码弹窗也必须在展示后立即订阅同一订单 SSE,收到支付回调入账后的 `paid` 快照时自动关闭二维码、刷新充值中心与全局余额并展示一次成功结果;“我已支付”只作为主动查单兜底,不能是扫码付款后的唯一状态推进入口。SSE 在等待窗口结束或短暂断线时按订单过期时间重连,关闭弹窗时必须取消订阅。
- 小程序订阅消息用于 AI 创作生成结果通知:H5 在生成动作发起前先把页面切到生成进度态并立即调用生成 action,同时非阻塞跳转到小程序原生订阅授权页尝试请求授权;授权接受、拒绝或页面返回都不得阻塞或取消生成。原生页不得改写上一页 `webViewUrl`,避免返回后丢失 H5 当前进度页状态。通知发送只允许发生在玩法草稿生成成功或失败终态之后,api-server 使用当前用户微信登录保存的 openid 调用微信 `subscribeMessage.send`。发送失败只记录 warning,不阻断作品生成。模板 `thing1` 发送玩法模板名,`number6` 发送本次生成结算后的实际泥点扣除,失败退款后固定为 `0`;模板 `time4` 字段必须是北京时间 `YYYY-MM-DD HH:mm`。`WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE` 支持 `formal` / `trial` / `developer`,应与当前发布环境一致。
- WebView 返回后,在订单状态拉取或 SSE 等待期间展示不可关闭遮罩“正在确认支付”,阻止用户离开或继续操作;只有确认到最终订单状态后才展示一次最终结果弹窗,不能先弹“正在支付/支付已提交”再二次弹成功。
@@ -1,5 +1,7 @@
# 创作主页与项目入口改版计划
> 2026-07-18 退役覆盖:本文关于旧模板入口、`/creation/<play>`、移动端隐藏“创作 / 项目”和 `/api/creation-entry/config` 的内容均已被后续实现替代,只保留为阶段设计记录。现役口径是桌面侧边栏与移动端底部 dock 都显示“创作 / 项目 / 我的”,稳定路由为 `/creation`、`/project`、`/profile`;旧模板业务只保留历史数据壳。当前实现与验收以 `docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md` 为准。
日期:2026-06-18
## 背景
@@ -1,8 +1,24 @@
# 平台入口与玩法链路
更新时间:`2026-06-10`
> 2026-07-17 退役覆盖:全部旧创作模板的前端、API、worker、reducer/procedure、纯业务 crate、生成发布链路、公开业务详情和专属运行态已下线,仅保留相关历史表的数据壳与必要兼容读取。本文后续玩法章节只作为历史设计记录,不再描述当前可用能力;当前实现与验收以 `docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md` 为准。
## 平台创作入口
更新时间:`2026-07-18`
## 现役平台壳
旧创作模板退役后,桌面端继续保留统一平台壳,一级导航固定为 `创作 / 项目 / 我的`:
- `/creation` 展示基于图片编辑器的创作工具主页,只读取编辑器项目与公开编辑器素材。
- `/project` 展示当前账号的图片编辑器项目,项目卡继续进入 `/editor/canvas`。
- `/profile` 是“我的”稳定路由,保留头像与昵称编辑、陶泥号复制、泥点余额与账单、累计统计、泥点充值、兑换码、玩家社区、反馈与建议、通用设置、开发者 API Key 和法律信息等平台公共能力。
- 桌面顶栏保留现役项目 / 素材搜索、泥点入口和账号胶囊。搜索只筛选当前编辑器项目与已读取的公开编辑器素材,不恢复旧公开作品号搜索、旧广场、旧作品详情或旧运行态。
- 桌面端使用公共侧边栏,移动端使用同样包含“创作 / 项目 / 我的”的三项底部 dock;点击、刷新及浏览器前进 / 后退都必须保持 URL、标题和选中态一致。
现役入口和公共资料能力只能依赖 `creation-home`、`project`、`image-editor`、公共组件及 `services/platform-entry` 等现役模块。Vite 模块门禁会拒绝 `components/rpg-entry`、`services/rpg-entry`、旧玩法目录和旧平台业务模块进入依赖图;Tailwind `@source`、TypeScript `include`、ESLint ignore 或 Vite watch ignore 都不能替代这条运行时依赖门禁。
## 历史平台创作入口
本节及后续玩法章节保留退役前的设计记录,其中出现的 `/api/creation-entry/config`、`/creation/<play>`、模板工作台、公开作品和专属运行态均不是现役契约,不得用于当前实现或运维验收。
创作入口配置事实源在 SpacetimeDB,通过 `GET /api/creation-entry/config` 下发;后台通过 `/admin/api/creation-entry/config` 管理入口开关,通过 `/admin/api/creation-entry/config/interactions` 管理公开作品点赞 / 改造能力矩阵。前端只在展示层派生可见卡片、入口状态和作品详情互动状态,`api-server` 路由熔断也使用同一份配置。不要恢复前端硬编码入口配置文件。
@@ -0,0 +1,106 @@
# 图片画布结构化持久化与迁移回滚方案
## 1. 背景与目标
图片画布当前把全部图层和生成对话框整体序列化到 `editor_canvas.layers_json`。该字段接近原 256 KiB 上限时,资源登记仍可成功,而布局保存会返回 `413`;任务完成后重新读取后端旧快照,会把前端尚未持久化的参考图和生成结果从画布移除。
本方案分两步处理:
1. 先把 `api-server` 与 SpacetimeDB 的 legacy layout 校验统一提高到 **2 MiB**,为存量画布止血。HTTP 路由的 body envelope 必须覆盖 2 MiB payload 及 JSON 包装开销,不能只修改领域常量。
2. 再把画布拆为结构化的 layer、generation dialog、layout migration/state 三张表;`editor_canvas.layers_json` 保留为旧数据、迁移输入和受限回滚载体,不再作为激活结构化存储后的权威读取来源。
2 MiB 是迁移窗口内的临时兼容上限,不是继续扩大整体 JSON 的长期容量方案。媒体正文、Data URL、生成输入快照仍不得进入画布布局;媒体和生成元数据继续以 `editor_project_resource` / `editor_asset` 为真相源。
## 2. 权威边界
结构化模式激活后,画布事实分工如下:
| 数据 | 权威位置 | 说明 |
| --- | --- | --- |
| 工程归属、默认画布、viewport、当前 revision | `editor_project` / `editor_canvas` | `editor_canvas` 保留 `layers_json` legacy 列,但其内容不参与结构化模式下的正常写入仲裁 |
| 图层实例、几何、层级、分组、显示与锁定状态、资源引用 | `editor_canvas_layer` | 一行一个图层;模式专属的有界扩展字段可放 JSON,不能复制媒体正文或完整资源元数据 |
| 生成对话框、占位层、来源层、结果层、任务与状态 | `editor_canvas_generation_dialog` | 生成任务和 UI 对话框的持久关联,不再嵌入全量 layout JSON |
| 存储模式、迁移阶段、校验 hash、激活与回滚审计 | `editor_canvas_layout_migration` | 每个 canvas 一行,控制 legacy / structured 读取,不允许前端自行切换 |
| 图片、视频、音频等媒体及生成元数据 | `editor_project_resource` / `editor_asset` | layer/dialog 只保存稳定 ID 引用;资源创建成功不能被当作布局保存成功 |
结构化快照由后端在同一 revision 下读取 `editor_canvas`、layer、dialog 和 layout migration state 后组装。前端只消费 BFF 快照,不直接订阅表、做 join 或推断迁移状态。
## 3. 表与写入契约
### 3.1 `editor_canvas_layer`
保存 `layer_id`、`canvas_id`、`project_id`、`owner_user_id`、坐标、宽高、原始尺寸、层级顺序、可选 `group_id`、hidden / locked / flip 状态、`resource_id`、有界的未结构化扩展 JSON、创建与更新时间。查询有 canvas / project 索引;同一 canvas 的层级顺序由 `sort_order` 决定。
layer 只表达“某个资源怎样放在画布上”。`src / prompt / actualPrompt / model / provider / taskId / objectKey / assetObjectId / sourceResourceId / assetKind / generationInputs / sourceType` 不得为方便展示而重复进入 `item_json` 或成为 layer 真相;迁移或结构化保存时先逐字段核对 `editor_project_resource`,缺少资源、字段冲突或无法无损重组时必须 fail-closed。历史 layout 若把 `sourceResourceId` 错写成当前图层自己的 `resourceId`,这是无意义的自引用,不作为 A/B 来源冲突:迁移时删除该重复字段并以项目资源表为真相;其他非空且不一致的来源 ID 继续拒绝。唯一存量缺资源例外是历史角色动作产生的自包含本地图层:`resourceId` 必须以 `local-` 开头、`sourceType=generated`、`mediaType=image-sequence`,至少包含一帧;帧序号必须从 1 连续递增,宽高必须是有限正数;所有 `imageSequenceFrames[].imageSrc` 及可选 `thumbnailSrc / previewVideoPath` 都必须是无 query / fragment、无路径回退段的站内根路径,且不能含 `data:`、`blob:`、HTTP 或签名 URL;可选帧 `objectKey` 必须与 `imageSrc` 去掉首斜杠后完全一致,图层级 `imageSrc / objectKey / assetObjectId` 必须为空,图层级 `src` 只允许为空或与首帧 `imageSrc` 完全一致。满足这些条件但资源行已不存在时,保留其有界媒体扩展和生成元数据以便前端从首帧恢复,不把这些字段从 canonical hash 中剥离;`sourceResourceId` 目标仍存在时必须属于同工程和 owner,目标已删除时保留原引用参与 hash,不据此伪造资源行。该例外不适用于普通图片、视频、音频、非本地 ID、空帧序列或已有资源字段冲突。存量 `assetKind / generationInputs` 仅允许在资源行尚未记录时由 apply 事务补入资源表,dry-run 只计算预览而不写库。前端读取项目快照时继续使用随项目返回的 resources 按 `resource_id` hydrate,兼容现有画布快照语义。
### 3.2 `editor_canvas_generation_dialog`
保存 `dialog_id`、`canvas_id`、`project_id`、`owner_user_id`、生成模式、状态、可选 `source_layer_id`、`generated_layer_id`、占位几何、有界扩展 JSON、创建与更新时间。扩展 JSON 只保留未结构化的生成参数,`mode / status / source / generated / placeholder.x/y/width/height` 以 typed 列为真相;`placeholder.originalWidth / originalHeight`、其他未知 placeholder 字段、dialog 内未知字段及 generation-dialog 顶层未知字段必须原样保存在扩展 JSON。`canvas-settings` 暂无扩展列,出现未识别字段时直接拒绝迁移,不能静默丢弃。当前 external job 关联仍以 worker 任务为准,未在本表重复建立任务真相。
### 3.3 `editor_canvas_layout_migration`
保存 `canvas_id`、schema version、`backfilled / active / rolled_back` 状态、最后校验 revision、legacy / structured canonical hash、layer / dialog 数量、资源引用集合 hash、迁移 / 激活 / 回滚时间。状态变更只允许已授权 database migration operator 调用 procedure 执行。procedure 错误作为调用结果和运维日志保留,不另外写入可能与失败事务脱节的 `last_error` 列。
### 3.4 revision CAS
所有用户布局写入必须携带读取快照时获得的 `expectedRevision`。procedure 在事务中校验 canvas 当前 revision;不一致返回 `409`,前端应重载后端最新快照,不能只换上新 revision 就原样重放冲突前的整包布局。
前端保存队列只对无 HTTP 响应的传输失败以及 `408 / 425 / 429 / 502 / 503 / 504` 做有界退避重试,`400 / 403 / 404 / 413` 等确定性错误不重复提交。若旧请求执行期间已有更新布局排队,旧请求失败后必须继续发送最新布局;`409` 后权威快照暂时加载失败时保留 pending save 并定时重新进入冲突恢复,不能等待用户再次拖动画布才恢复保存。
本次结构化 V1 先保留旧 `{ viewport, layers }` PATCH 作为兼容输入。legacy canvas 即使携带 `expectedRevision` 也只做 CAS legacy 保存,不允许用户写入绕过 migration operator 直接激活 structured;只有已完成 backfill / activate、且 active 迁移记录的 revision / hash / 数量 / 资源引用校验均通过时,后端才在单个事务内把兼容输入拆成 layer / dialog 行并递增一次 revision。旧无 CAS procedure 不得写 structured canvas。V1 快照从 typed 列重组,`item_json / dialog_json` 只保留最大 512 KiB 的未结构化扩展字段。自包含本地图片序列在 active canvas 中只能继续保存已回填且 `layerId / resourceId / sourceType / item_json` 语义完全一致的原行;允许修改几何、层级、分组、显隐等 typed 布局字段。前端序列化按正常资源真相边界省略 `assetKind / generationInputs` 时,后端只从既有结构化行恢复这两个冻结字段再校验;显式修改仍拒绝。active 路径不再经过 legacy 元数据清洗,拒绝新增缺资源序列或改写既有帧、预览、prompt 和生成扩展。后续将新增、移动、缩放、删除、重排和分组收窄为有界 batch mutation;在此之前 2 MiB 仍是兼容整包入口的上限。
### 3.5 worker 原子完成
worker 完成生成任务时,本次先用读取时 revision 调用 CAS 保存;发生并发变更时拒绝覆盖并让任务保留可诊断失败,不再静默覆盖用户布局。最终收口仍是受 `job_id + worker_id + lease_token` 栅栏保护的后端 procedure 在同一事务内:
1. 校验 job、owner、project、canvas、dialog 和租约;
2. 幂等创建或确认 `editor_project_resource`;
3. 创建 / 替换结果 layer,并删除或更新占位 layer;
4. 把 dialog 更新为终态并关联 `generated_layer_id`;
5. 递增 canvas revision,最后才允许完成 external job。
重复 completion 必须返回同一资源、layer 和 dialog 终态,不得重复插入,也不能因 dialog 暂时缺失而返回 `changed=false` 后仍把任务标记完成。任一步失败时整笔业务写回回滚,任务保留可诊断的失败或可重试状态。
## 4. 存量迁移
迁移按 canvas 执行 `backfill → hash 核对 → activate`,并保持幂等:
1. **Backfill**:在短事务中读取 `editor_canvas.layers_json`、canvas revision 和更新时间;解析 legacy items,把普通图层与 generation dialog 分别归一到两张结构化表。按稳定 `canvas_id + item id` upsert,重复运行不能产生新 ID 或重复行。记录本次基准 revision 和 legacy canonical hash。
2. **Hash 核对**:按固定字段顺序、数值归一规则和稳定 item 排序,把结构化行重组成 canonical legacy 语义;分别计算 layer 数、dialog 数、资源引用集合以及 SHA-256 canonical hash。raw JSON 的空白、对象 key 顺序和无意义默认值差异不作为不一致;任何不可识别字段必须保存在有界扩展字段中或使迁移失败,不能静默丢弃。项目资源存在时继续逐字段核对;除 `sourceResourceId == resourceId` 的历史自引用按资源表真相剥离外,其他 `sourceResourceId` 等冲突都拒绝迁移。只有上一节定义的缺资源自包含本地图片序列保留 layout 内元数据并参与 hash。
3. **Activate**:只有 structured hash、数量和资源引用集合均匹配,且 `editor_canvas.revision` 仍等于 backfill 基准 revision 时,才以 CAS 把 layout migration state 切为 `active` 并把 canvas storage version 切到 structured。revision 已变化时丢弃本轮验证结果并重新 backfill;不得覆盖迁移期间的用户更新。
当前提供单项目、可重跑的运维入口:`npm run spacetime:editor-canvas-layout:migrate -- --database <db> --server <server> --project-id <id> --owner-user-id <id> --action <backfill|activate|rollback>`。默认 dry-run,只有显式追加 `--apply` 才写入;backfill dry-run 即使遇到尚无 `editor_canvas` 的旧工程,也只用 `editor_project` 构造内存预览,不创建 canvas、不补资源元数据。每个项目依次执行 backfill dry-run / apply 和 activate dry-run / apply,不在一个长事务中扫全表。脚本只调用受 migration operator 保护的 procedure,不直接修改生产表。
若全量 backfill 审计发现普通图层缺少 `editor_project_resource`,先使用定向资源修复入口:`npm run spacetime:editor-canvas-resources:repair -- --database <db> --server <server> --plan-file </absolute/outside-repo/repair-plan.json>`。plan 必须是仓库外、当前用户持有、权限严格为 `0600` 的普通文件;每个 canvas 绑定 owner / project、expected revision、`editor_canvas.layers_json` 与 legacy `editor_project.layers_json` 两份原始字节 SHA-256,以及精确 layer/resource/sourceResourceId 或 asset_object 修复动作。默认逐 canvas dry-run;apply 必须追加 `--apply --confirm-plan-sha256 <dry-run 输出>`,成功后脚本自动以同一 plan 再 dry-run,并要求全部返回 `already_repaired`。
`repair_editor_canvas_resources_and_return` 只允许 migration operator 调用,并且只修尚无迁移记录的 legacy canvas。图片动作仅在缺失旧 resourceId、精确旧 sourceResourceId、同工程唯一现存资源、layout 资源元数据与 private asset_object owner/objectKey/task 谱系全部一致时,替换 `resourceId` 并删除重复 `sourceResourceId`。音频动作仅在原 resourceId 全局不存在、图层与 private asset_object 的 owner/objectKey/source job/entity/content type/长度全部匹配时,恢复 `420x120` 的 `sound-effect` / `background-music` 项目资源行;资源 `asset_kind` 分别映射 asset_object 的 `editor_sound_effect` / `editor_background_music`。apply 在一个事务内插入资源、同步两份 legacy layout、递增一次 canvas revision;任一 guard 失败整画布回滚。plan、脚本输出、测试和文档均不得包含生产真实 ID。
## 5. 兼容与回滚
- 结构化代码上线后先保持 `legacy` 主读写,允许 schema 与 procedure 先行发布;确认新 API 可用后才开始 backfill 和逐 canvas 激活。
- `layers_json` 在迁移和观察期内保留原值,不删除、不改名、不重排现有字段。激活结构化模式不立即清空 legacy JSON。
- 回滚前由后端在一个一致 revision 下读取 layer 与 dialog,按 legacy schema 重组并计算 canonical hash。重组结果必须是合法 JSON 且 UTF-8 大小 **不超过 2 MiB**,随后在事务内写回 `editor_canvas.layers_json`、递增 revision、记录回滚审计并把 state 切回 `legacy`。对已处于 `rolled_back` 的重复回滚请求,也必须重新复核 revision、structured hash、layer/dialog 数量、资源引用集合以及 legacy/structured 一致性,任一漂移都 fail-closed。
- 超过 2 MiB、包含无法降级字段、hash / 资源引用核对失败或 revision CAS 冲突时,回滚必须拒绝并保持 structured,禁止截断图层、丢弃 dialog 或只切换读取标志。生产二进制回滚应保留能读取 structured 的前向兼容版本,不能把超限 canvas 强行交给旧二进制。
- 观察期结束前不删除三张结构化表或 legacy 列。后续是否停止生成 legacy 回滚快照、是否清理旧 JSON,需另行评审和迁移窗口,不随本次结构化上线自动执行。
## 6. 发布次序
1. 发布 2 MiB hotfix:先发布不变更 schema 的 SpacetimeDB 模块校验,再部署同一上限和足够 body envelope 的 `api-server`;确认 release manifest、服务状态和健康检查。
2. 发布新增三表、revision / CAS、typed 快照重组与迁移 procedure 的 SpacetimeDB 模块;同步 migration table 白名单和生成绑定。此时保持 legacy 主读写。
3. 部署可同时读取 legacy / structured 的 `spacetime-client` 与 `api-server`,再部署携带 revision 的前端。混合版本期间未激活 canvas 必须仍可正常编辑,旧 API 写 structured canvas 必须失败而不是无 CAS 覆盖。
4. 先对测试账号和单个真实 canvas dry-run、backfill、核对、activate,再按批次扩大;持续观测 CAS conflict、迁移失败、快照缺资源和 worker completion 指标。
5. 验收完成并经过观察期后,才把新 canvas 默认设为 structured。legacy 回滚能力继续保留。
SpacetimeDB 必须先于依赖新 procedure / bindings 的 API 发布;前端必须最后发布。任何阶段失败都停留在当前可读模式,不做跨版本破坏性清理。
## 7. 验收清单
- 2 MiB hotfix 后,原先约 254 KiB 的受影响画布和大于 256 KiB 的测试布局可保存并刷新恢复;超过 2 MiB 的 legacy layout 在 API 与 SpacetimeDB 两层都稳定拒绝,错误可观测。
- backfill 重跑不会增加 layer / dialog 行,canonical hash、数量和资源引用集合一致;迁移中发生用户写入时 activate CAS 失败并安全重跑。
- release 存量抽样中的缺资源 `local-*` 角色动作序列可无损 round-trip;同形状但空帧、相对路径、HTTP / 签名 URL、`data:` / `blob:` 引用必须拒绝。已有资源的 `sourceResourceId == resourceId` 历史自引用应按资源表真相安全剥离,其他来源 ID 或资源字段冲突仍必须拒绝。
- release 全量审计暴露的普通缺资源行必须先通过定向 repair dry-run;图片只能复用同工程唯一资源,音频只能从已登记 private asset_object 恢复。修复后同一 plan 全部命中 `already_repaired`,再重跑全量 backfill dry-run,要求所有 canvas 均通过。
- structured 模式下 typed 列而非扩展 JSON 决定几何、层级、分组、显示 / 锁定、资源引用和 dialog 状态;两个客户端基于同一 revision 写入时只允许一个成功,冲突方重载后端最新快照,不换上新 revision 原样重放旧整包。细粒度 batch mutation 是取消 2 MiB 兼容入口的后续项,不冒充为本次已完成。
- worker completion 当前以读取时 revision 做 CAS,冲突时拒绝覆盖;V2 保存和保存后快照在同一 procedure 结果内返回,避免“已提交但后续 GET 失败”的不确定结果。lease-fenced 资源 / layer / dialog / job 单事务 completion 仍是后续收口项。
- structured 快照刷新后,上传参考图、生成结果、占位与 dialog 状态均可恢复;资源存在但布局写入失败时不会伪装为保存成功。
- 回滚重组结果经 schema 校验、canonical hash / 资源引用核对且不超过 2 MiB;超限或不一致时明确拒绝且 structured 快照仍可读取。
- 完成 `npm run spacetime:generate`、`npm run check:spacetime-runtime-access`、`npm run check:spacetime-schema`、相关 Rust / API / 前端定向测试、`npm run check:encoding` 和 `git diff --check`。
@@ -71,6 +71,7 @@
- 「素材库」页签:账号级素材库(复用 `ImageCanvasAssetLibrary` 数据源);
- 多选 + 底部「取消 / 应用」。
- 网格末尾上传格为后续补齐项;在上传格未落地前,对话附件只从已有画布资源和账号素材库选择。后续若从对话入口上传图片,必须复用素材库 / 画布资源登记链路,不新增对话私有图片类型。
- 附件选择弹窗使用 `PlatformToolModalShell` 承接 portal 主题变量和不透明 panel 背景;不能直接把未注入 `platform-theme` 的 `UnifiedModal` portal 到 `document.body`,否则 `--platform-modal-fill` 失效后面板会变透明。
- 应用后附件以胶囊 chip 挂在输入框上方;发出的消息内附件渲染为纯文本胶囊 chip(名称 + 小图标),**默认无缩略图,鼠标悬浮才浮出缩略图预览**。
- 附件领域形状:统一为画布资源 / 素材库对象引用(`resourceId` / `assetId` + 可选 `objectKey`),不存在只属于对话的第三种图;单条消息上限 9 张(前后端共同校验)。前端可携带展示用 `imageSrc` / `thumbnailSrc`,后端必须按当前工程和当前账号重新归一、校验归属与 `objectKey`。
- 输入区附件临时状态统一收口到 `useConversationAttachments`,选择弹窗由独立的 `AttachmentPicker` 负责纯展示;选择、引用、粘贴上传完成、移除、发送清空和失败恢复都必须经同一最新状态更新入口。异步粘贴完成时基于当时的最新附件去重并重新校验 9 张上限,不能用上传开始时捕获的旧列表覆盖期间新增的引用。
@@ -101,6 +102,7 @@
- 用户使用「这张」「刚才那个」「上一张」「把衣服换成……」等方式指代或编辑上一张结果图时,LLM 默认选择 `edit_image` 并引用 `latestGeneratedImage` 作为源图;除非用户明确要求全新生成,否则不能因为本轮没有重新上传附件而降级为 `generate_image`。
- 规划 prompt 必须显式区分“规范展板”和“实际素材产出”:规范图、视觉规范图、风格规范图、素材规范展板、角色规范图等规范展板请求走 `generate_image`,并补齐统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等要求;实际角色立绘才走 `generate_character`,多个图标素材 / 图集才走 `generate_icon_spritesheet`。
- 画布 Agent 规划请求使用 Chat Completions、1024 `max_tokens` 和 60 秒 Agent 专用请求超时;生成图片/编辑图片仍走对应生成工具和模型计费。
- function-calling runner 必须把“等待用户确认”作为显式工具语义:当本批所有工具都校验成功并进入待确认状态时,立即以成功结果结束当前规划回合并持久化助手文本与待确认卡,不得继续依赖 LLM 自行停止;未知工具、参数错误、普通连续工具和不可解析响应仍受 `max_turns` 保护。
- **对话回合免费**(聊天、分析回复不扣泥点),仅 Agent 实际触发生成工具时按对应模型定价扣泥点。
- 工具调用前后端校验泥点余额;不足时该次生成失败并在对话中以明确错误气泡告知,对话本身可继续。
@@ -162,6 +162,7 @@
- 视频生成完成后,后端先把带纯色背景的预览视频登记为 OSS 私有对象、`asset_object`、项目资源和账号素材,再按面板选择抽取对应帧数:`32`、`40` 或 `48`。未传 `assetFolderId` 时进入默认“项目”素材文件夹;后续抽帧或抠图失败不能抹掉这份已经生成成功的可恢复视频。
- 抽帧采样必须按目标帧数预留视频尾部安全步长,例如 `32帧·4秒` 最后一帧采 `3.875s`,避免 FFmpeg 在尾点附近返回成功但输出 `0` 帧。
- 后端先计算整批精确采样时刻,再用单个 FFmpeg filter graph 统一解码预览视频并输出 `32 / 40 / 48` 张源帧;不得为每帧重新启动 FFmpeg、重复解码同一视频,也不得用会改变现有尾帧安全时刻的粗粒度 `fps` 抽帧替代。批量命令成功后必须逐一确认全部目标帧文件存在,缺少任一帧都按整批失败处理并保留缺帧编号、目标时刻和输出路径诊断。
- 每帧绿幕源图字节由上传 owned 消费(`frame.bytes` 移入 put,上传完成后释放原帧缓冲,不克隆保留);后续只持 object key。每次 BgFilter attempt 重新签发 600 秒 GET URL,multipart 仅传 `image_url`(加 `background_mode=flat`、`seg_model=birefnet`、`cross_check=on` 与同一次生成已选定的 `screenColor`),不传 `file`。BgFilter 主路径不重新下载原帧;失败后走 `阿里云通用抠图(按签名 URL 单独下载)→ 本地 editor_green_screen(再按 object key 独立下载一次并在产出后释放)`。BgFilter 每一次 HTTP attempt 的 timeout 使用“`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 基准值 + `2000ms × 本次实际帧数`”,默认 `32 / 40 / 48` 帧分别为 `244000 / 260000 / 276000ms`;首次失败后重试 `1` 次。
- 全部 `32 / 40 / 48` 帧以覆盖本次所有帧的无序在途集合连续发射,允许乱序完成并最终按 `frameIndex` 排序;任一帧最终失败时先排空全部已启动 Future,再让整项任务失败退款,不发布缺帧动画。
- 抽帧结果写入 OSS,并返回帧路径、帧尺寸、帧数、fps、预览视频路径、模型、价格和实际 prompt。
@@ -1,5 +1,7 @@
# 当前产品与工程约束
> 2026-07-17 状态更新:旧创作入口、全部模板创作/生成/发布/公开业务详情及专属运行态已退役。本文涉及 RPG、拼图、拼消消、大鱼、木鱼、方洞、视觉小说、汪汪声浪、寓教于乐等玩法的内容仅作为历史设计记录;当前退役边界以 `docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md` 为准。
更新时间:`2026-06-05`
## 项目定位