Compare commits

..

1 Commits

Author SHA1 Message Date
kdletters 00053cf1d3 修复游戏创作工作流与UI图集事务
将固定图片规则降为默认合同并允许Supervisor持久委派扩展UI图集
修复条件Graph路由、终态投影、steer窗口和manifest只读刷新
将UI主图、切片、manifest与Agent DB审计收口为可恢复事务
加固跨平台句柄、journal重放、残留隔离和Agent DB容量门禁
补充工作流、事务恢复、Provider与项目夹具回归
同步技术方案与团队共享决策
2026-08-07 19:28:13 +08:00
3642 changed files with 690264 additions and 351287 deletions
+1 -1
View File
@@ -1 +1 @@
{ "containers": [], "config": {} }
{"containers":[],"config":{}}
-16
View File
@@ -1,16 +0,0 @@
# Genarrative Codex 项目工具
`.codex/` 是仓库级 Codex 工具目录,保存项目共享的 skills、插件资源、hooks 和相关配置模板。它只描述如何协作和加载工具,不承载项目业务知识。
## 目录约定
- `.codex/skills/` 是项目专属 skill 根目录。每个 skill 以目录中的 `SKILL.md` 为入口,配套的参考资料和脚本放在同一目录下。
- `.codex/plugins/` 保存随仓库分发的项目插件资源及其参考资料。当前的 `game-studio` 插件提供浏览器游戏设计、原型、2D/3D 技术栈、素材管线和 playtest 工作流;是否启用遵循当前 Codex 的插件加载机制,不依赖旧工具的环境变量或个人配置脚本。
- `.codex/hooks/``.codex/environments/` 等目录保存项目工具链所需的 hooks 和环境模板;它们不替代项目代码中的运行时配置。
- 长期有效的产品、架构、接口、排障和协作知识统一放在 `docs/``docs/project-memory/`,不复制到本目录。
## 使用边界
进入仓库后先读根目录 `AGENTS.md`,再按任务路由读取对应 skill。SpacetimeDB 的通用概念、Rust 服务端、CLI、TypeScript 客户端和 MCP 用法由已安装的官方插件提供;项目约束和入口由 `.codex/skills/genarrative-spacetimedb/SKILL.md` 统一编排。
个人 `~/.codex` 配置、凭据、会话、环境变量和本地路径不得复制到仓库。若本目录内容与当前代码或最新 `docs/` 冲突,以代码和最新文档为准,并修正过期工具说明。
+11 -23
View File
@@ -7,33 +7,23 @@ import { fileURLToPath } from 'node:url';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, '..', '..');
const logDir = resolve(repoRoot, '.codex', 'logs');
const hasCodegraphConfig = existsSync(
resolve(repoRoot, '.codegraph', 'config.json'),
);
const hasCodegraphConfig = existsSync(resolve(repoRoot, '.codegraph', 'config.json'));
const npmCommand = process.platform === 'win32' ? 'cmd' : 'npm';
if (!hasCodegraphConfig) {
console.log(
'[codex-hook] 未发现 .codegraph/config.json,跳过 CodeGraph 同步。',
);
console.log('[codex-hook] 未发现 .codegraph/config.json,跳过 CodeGraph 同步。');
process.exit(0);
}
const result = spawnSync(
npmCommand,
process.platform === 'win32'
? ['/d', '/s', '/c', 'npm run codegraph:sync']
: ['run', 'codegraph:sync'],
{
cwd: repoRoot,
shell: false,
encoding: 'utf8',
env: {
...process.env,
NO_COLOR: process.env.NO_COLOR ?? '1',
},
const result = spawnSync(npmCommand, process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run codegraph:sync'] : ['run', 'codegraph:sync'], {
cwd: repoRoot,
shell: false,
encoding: 'utf8',
env: {
...process.env,
NO_COLOR: process.env.NO_COLOR ?? '1',
},
);
});
mkdirSync(logDir, { recursive: true });
if (result.stdout) {
@@ -54,9 +44,7 @@ if (result.signal) {
}
if ((result.status ?? 0) !== 0) {
console.error(
'[codex-hook] CodeGraph 同步失败,请手动运行 npm run codegraph:sync 查看详情。',
);
console.error('[codex-hook] CodeGraph 同步失败,请手动运行 npm run codegraph:sync 查看详情。');
process.exit(result.status ?? 1);
}
@@ -1,41 +0,0 @@
{
"name": "game-studio",
"version": "0.1.0",
"description": "Design, prototype, and ship browser games with guided 2D and 3D workflows, asset pipelines, and playtesting support.",
"author": {
"name": "OpenAI",
"email": "support@openai.com",
"url": "https://openai.com/"
},
"homepage": "https://openai.com/",
"repository": "https://github.com/openai/plugins",
"license": "MIT",
"keywords": [
"games",
"phaser",
"threejs",
"react-three-fiber",
"gltf",
"rapier",
"webgl",
"sprites",
"playtest"
],
"skills": "./skills/",
"interface": {
"displayName": "Game Studio",
"shortDescription": "Design, prototype, and ship browser games",
"longDescription": "Plan, prototype, and build browser games with guided workflows for gameplay systems, UI, asset pipelines, and playtesting across 2D and 3D projects.",
"developerName": "OpenAI",
"category": "Coding",
"capabilities": ["Interactive", "Write"],
"websiteURL": "https://openai.com/",
"privacyPolicyURL": "https://openai.com/policies/privacy-policy/",
"termsOfServiceURL": "https://openai.com/policies/terms-of-use/",
"defaultPrompt": ["Design a browser game and plan the core loop"],
"brandColor": "#0F766E",
"composerIcon": "./assets/game-studio.svg",
"logo": "./assets/app-icon.png",
"screenshots": []
}
}
-7
View File
@@ -1,7 +0,0 @@
name: game-studio
version: 0.1.0
description:
Design, prototype, and ship browser games with guided 2D and 3D workflows,
asset pipelines, and playtesting support.
author: OpenAI
kind: standalone
@@ -1,43 +0,0 @@
# GLB Loading Starter
Use this as the canonical minimal pattern for loading shipped 3D content.
## Vanilla Three.js
```ts
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
const draco = new DRACOLoader();
draco.setDecoderPath('/draco/');
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(draco);
gltfLoader.load('/assets/hero.glb', (gltf) => {
const root = gltf.scene;
root.traverse((node) => {
if ('castShadow' in node) {
node.castShadow = true;
node.receiveShadow = true;
}
});
scene.add(root);
});
```
## React Three Fiber
```tsx
import { useGLTF } from '@react-three/drei';
function HeroModel() {
const gltf = useGLTF('/assets/hero.glb');
return <primitive object={gltf.scene} />;
}
```
## Notes
- Default shipping format is GLB or glTF 2.0.
- Keep optimization upstream in the asset pipeline; loader code should stay boring.
@@ -1,4 +0,0 @@
interface:
display_name: 'Game Playtest'
short_description: 'Run browser-game playtests and QA'
default_prompt: 'Playtest the browser game, check core interactions and visual state changes, and report concrete issues.'
@@ -1,4 +0,0 @@
interface:
display_name: 'Game Studio'
short_description: 'Route browser-game work to the right path'
default_prompt: 'Help me choose the right browser-game stack and workflow before implementation starts.'
@@ -1,4 +0,0 @@
interface:
display_name: 'Game UI Frontend'
short_description: 'Design browser-game HUDs, menus, and overlays'
default_prompt: 'Design a browser-game UI layer that supports the play experience without crowding the playfield.'
@@ -1,4 +0,0 @@
interface:
display_name: 'Phaser 2D Game'
short_description: 'Build 2D browser games with Phaser'
default_prompt: 'Implement this 2D browser game with Phaser, TypeScript, and a clear gameplay architecture.'
@@ -1,4 +0,0 @@
interface:
display_name: 'React Three Fiber Game'
short_description: 'Build React-hosted 3D browser games'
default_prompt: 'Build this 3D browser game with React Three Fiber and keep the 3D runtime aligned with the React app shell.'
@@ -1,4 +0,0 @@
interface:
display_name: 'Sprite Pipeline'
short_description: 'Generate and normalize 2D sprite animations'
default_prompt: 'Create and normalize 2D sprite animation assets for a browser game with consistent scale and anchors.'
@@ -1,4 +0,0 @@
interface:
display_name: 'Three WebGL Game'
short_description: 'Build browser-game runtimes with Three.js'
default_prompt: 'Implement this browser-game runtime with plain Three.js and keep the scene architecture easy to debug.'
@@ -1,4 +0,0 @@
interface:
display_name: 'Web 3D Asset Pipeline'
short_description: 'Prepare and optimize browser-game 3D assets'
default_prompt: 'Prepare these browser-game 3D assets for shipping as predictable runtime-ready GLB or glTF files.'
@@ -1,4 +0,0 @@
interface:
display_name: 'Web Game Foundations'
short_description: 'Set browser-game architecture before implementation'
default_prompt: 'Establish the core architecture for this browser game before implementation starts.'
-7
View File
@@ -1,7 +0,0 @@
# Genarrative 项目 skills
`.codex/skills/` 是仓库级、可通过 Git 共享的项目专属 skill 根目录。每个目录的 `SKILL.md` 是唯一入口;较长的参考资料、示例和脚本放在该 skill 的 `references/``assets/``scripts/` 下。
项目 skill 负责把当前仓库的目录、契约、验证门禁和历史约束串起来,不重复维护通用框架知识。涉及 SpacetimeDB 时,先读 `genarrative-spacetimedb/SKILL.md`,由它路由到已安装的官方 SpacetimeDB 插件 skills。
长期项目知识放在 `docs/``docs/project-memory/`;不要把个人配置、密钥、会话、缓存或临时计划写入本目录。发现 skill 与代码或最新文档不一致时,按当前实现更新 skill,并同步必要的项目文档。
@@ -1,401 +0,0 @@
---
name: behavior-driven-development
description: 在 Genarrative 中需要用 BDD/行为驱动方式把 PRD、用户故事、验收标准转成可执行场景、Gherkin 用例、测试计划或 TDD 落地顺序时使用。
license: MIT
metadata:
codex:
tags: [BDD, Gherkin, 验收标准, 用户故事, 测试, Genarrative]
related_skills:
[
writing-plans,
test-driven-development,
systematic-debugging,
requesting-code-review,
]
---
# BDD 行为驱动开发流程
用于在 Genarrative 项目中,把产品需求、用户故事、业务规则和验收标准沉淀成清晰、可讨论、可验证的行为场景,并进一步映射到前端测试、API 测试、领域服务测试或 E2E 测试。
BDD 的重点不是“先写一堆 UI 自动化脚本”,而是让团队先对“用户在什么上下文下做什么,系统应该给出什么可观察结果”达成一致。
## 适用场景
- 从 PRD、设计文档、用户故事中提炼验收标准。
- 功能需求容易产生理解偏差,需要先把行为边界说清楚。
- 涉及前端、后端、运行态、异步任务、权限、埋点或状态流转的跨层功能。
- 需要把“完成标准”写成 Given / When / Then 场景。
- 需要在编码前规划测试覆盖:单元测试、组件测试、API 测试、E2E 测试。
- 希望和 TDD 配合:先写行为场景,再把场景拆到 RED-GREEN-REFACTOR。
- 需要给产品、测试、前后端开发共同评审的一份中文验收说明。
不适用:
- 纯重构且对外行为不变,只需要 characterization tests 或回归测试。
- 一次性小改动,验收规则非常明确且无跨层状态。
- 只想记录实现细节、代码结构或技术方案;这类内容更适合技术设计文档。
## 必读约束
1. 先描述用户可观察行为,再讨论实现细节。
2. 场景必须可验证,避免“体验更好”“更智能”“合理展示”等不可判定表述。
3. 不要把 Gherkin 写成低层 UI 点击脚本;UI 细节只在确实属于业务行为时出现。
4. 中文 PRD、中文 UI 文案、中文剧情和注释不要擅自改成英文。
5. 如果项目文档不足以支持准确落地,应先补齐 `docs/` 下的 PRD/设计/技术文档,再进入编码。
6. BDD 场景要覆盖主要成功路径、关键失败路径、权限/登录态、边界条件和回归风险。
## 核心格式
推荐使用中文 Gherkin
```gherkin
功能: <业务能力名称>
</>
<>
<>
背景:
假如 <所有场景共享的前置条件>
场景: <具体行为名称>
假如 <上下文/已有状态>
<用户动作或系统事件>
那么 <可观察结果>
而且 <额外可观察结果>
场景大纲: <带参数的行为名称>
假如 <上下文中包含 <变量>>
<动作>
那么 <结果>
例子:
| | |
| A | X |
| B | Y |
```
英文关键字也可以使用:
```gherkin
Feature: Work publish permission
Scenario: Anonymous user attempts to publish a draft
Given an anonymous user has a generated draft
When the user clicks publish
Then the login modal should be shown
And the draft should remain unchanged
```
在 Genarrative 项目内,若参与评审的人主要使用中文,优先中文场景;测试框架要求英文命名时,可以保留中文场景标题并在测试文件中使用英文 describe/it。
## 从需求提炼 BDD 场景
### Step 1: 识别角色和业务目标
先回答:
- 谁在使用?游客、已登录用户、创作者、管理员、审核人员、系统任务?
- 用户想完成什么?创建、生成、保存、发布、试玩、查看、兑换、导出?
- 业务价值是什么?降低创作门槛、保护权限、保证数据一致性、提升运营可见性?
### Step 2: 抽取领域词汇
建立统一术语,避免同一概念多种叫法:
- work / 作品
- draft / 草稿
- session / 创作会话
- runtime / 运行态
- publish / 发布
- profile / 我的页签
- invite code / 邀请码
- analytics event / 埋点事件
场景中优先使用业务词,不要直接写组件名、函数名、数据库表名,除非这些就是用户可见对象。
### Step 3: 列出行为切片
按用户旅程切分:
1. 入口是否出现、是否可点击。
2. 进入页面或工作台后的初始状态。
3. 用户提交输入后的成功路径。
4. 失败路径:未登录、参数无效、权限不足、网络/API 失败、异步任务失败。
5. 状态持久化:刷新、返回、重新进入、跨设备或重新登录。
6. 对外副作用:保存、发布、埋点、通知、导出、生成资产。
7. 回归风险:旧入口、旧数据、移动端布局、中文编码。
### Step 4: 把每个切片写成 Given / When / Then
检查每个场景:
- Given 只描述前置状态,不写动作过程。
- When 只描述一个主要触发动作或事件。
- Then 描述可观察结果,可以被测试或人工验收。
- 一个场景只验证一个核心行为;不要把完整长流程塞进一个巨型场景。
## Genarrative 场景模板
### 前端入口 / 页面行为
```gherkin
功能: 我的页签反馈入口
场景: 已登录用户从我的页签进入反馈页面
假如
那么
而且 tab
而且
场景: 用户从反馈页面返回我的页签
假如
那么
而且 tab
```
### 登录态 / 权限行为
```gherkin
功能: 需要登录的发布能力
场景: 游客尝试发布生成草稿
假如稿
那么
而且
而且稿
```
### 后端 API / 领域规则
```gherkin
功能: 作品正式游玩开始埋点
场景大纲: 支持的玩法进入正式游玩
假如 <玩法>
那么 work_play_start
而且 scope_kind work
而且 metadata playTypeworkIdsourceRoute userId
例子:
| |
| puzzle |
| match3d |
| square-hole |
| custom-world |
| big-fish |
| visual-novel |
```
### 异步生成 / SSE 行为
```gherkin
功能: AI 创作会话流式回复
稿
场景: 成功生成草稿
假如
那么
而且稿
而且 AI
场景: 生成失败
假如
那么
而且
而且稿
```
## 映射到测试类型
| BDD 场景关注点 | 推荐测试层级 | 示例 |
| ---------------------------- | ----------------- | ------------------------------------ |
| 纯领域规则、状态机、校验 | Rust/TS 单元测试 | reducer、module-\*、schema validator |
| DTO 契约、API 请求响应 | API/contract 测试 | Axum handler、shared-contracts serde |
| 页面渲染、按钮状态、表单校验 | 组件测试 | Vitest + Testing Library |
| 路由、tab、页面阶段切换 | 前端集成测试 | appPageRoutes、FlowShell 行为 |
| 登录态、发布、运行态完整链路 | E2E/smoke | Playwright 或项目 smoke 脚本 |
| 埋点、副作用、后台导出 | 后端集成/API 测试 | tracking event、admin export |
原则:
- 不是每个 BDD 场景都必须落成 E2E。
- 能在低层稳定验证的规则,不要强行放到脆弱的浏览器自动化里。
- E2E 只覆盖最关键的用户旅程和跨层集成风险。
## 与 TDD 的配合方式
BDD 先回答“行为是什么”,TDD 再推动“代码怎么长出来”。
推荐顺序:
1. 写 BDD 场景,确认业务行为和验收标准。
2. 给每个场景标注测试层级:unit / component / API / E2E。
3. 选择一个最小场景进入 TDD。
4. RED:先写失败测试,测试名称对应场景标题。
5. GREEN:实现最小代码让测试通过。
6. REFACTOR:清理重复、命名、边界和文档。
7. 回到下一个场景,直到主要路径和关键失败路径覆盖。
测试命名建议:
```ts
describe('帮助与反馈入口', () => {
it('已登录用户从我的页签进入独立反馈页面', () => {
// Given ...
// When ...
// Then ...
});
});
```
Rust 测试命名建议:
```rust
#[test]
fn anonymous_user_cannot_publish_generated_draft() {
// Given
// When
// Then
}
```
## 推荐产物
根据任务复杂度选择产物位置。使用本 skill 产出 Gherkin/BDD 场景时,必须先决定落点,不要把正式验收场景随手写在聊天记录里。
### Gherkin/BDD 场景默认落点
| 产物类型 | 推荐路径 | 适用场景 |
| --------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| 实施前分析 / 临时计划 | 当前任务说明或 `.tmp/<task-name>-bdd-scenarios.md` | 某次 Codex 开发任务前,用于澄清行为、拆测试、辅助实现;不作为长期产品依据。 |
| 正式产品验收 / PRD 场景 | 当前 `docs/` 融合文档,必要时新增 `docs/【产品验收】<功能名>BDD场景-YYYY-MM-DD.md` | 产品、测试、开发都需要长期参考的验收标准、用户故事、功能边界。 |
| 技术/API/领域行为场景 | 当前 `docs/` 融合文档,必要时新增 `docs/【技术验收】<功能名>BDD场景-YYYY-MM-DD.md` | 后端 API、领域规则、状态机、SpacetimeDB reducer/table、SSE/异步任务、埋点副作用。 |
| 自动化 Gherkin feature 文件 | `tests/features/*.feature``e2e/features/*.feature` | 项目已接入 Cucumber/Playwright BDD 等 Gherkin runner 时。未接入前不要随意新建测试 runner 目录。 |
| 稳定流程或团队经验 | `docs/project-memory/shared-memory/``.codex/skills/` | 不是某个功能验收,而是长期可复用的团队流程、坑点、执行规范。 |
默认规则:
1. 用户只说“先用 BDD 梳理一下/写场景/写 Gherkin”,默认在当前任务上下文中输出;需要文件时写到 `.tmp/<task-name>-bdd-scenarios.md`
2. 用户说“正式验收标准/PRD/产品文档/给测试验收”,优先合并到当前 `docs/` 融合文档;无法容纳时新增带 `【产品验收】` 标签的 Markdown。
3. 用户说“API 行为/后端规则/状态机/埋点/异步任务/SpacetimeDB”,优先合并到当前后端架构或开发运维文档;无法容纳时新增带 `【技术验收】` 标签的 Markdown。
4. 用户明确要求“可执行 feature 文件”且项目已有 runner,再写 `.feature` 文件;否则先写 Markdown BDD 文档,并在测试映射中标注未来自动化落点。
5. 如果 BDD 场景会作为编码依据,文档中必须包含“测试映射”表,标注场景要落到哪些测试文件。
命名建议:
```text
.tmp/profile-feedback-bdd-scenarios.md
docs/【产品验收】帮助与反馈BDD场景-2026-05-11.md
docs/【技术验收】作品游玩埋点BDD场景-2026-05-11.md
tests/features/profile-feedback.feature
e2e/features/invite-code.feature
```
### 其他配套产物
除 BDD/Gherkin 场景外,相关配套内容可放在:
- 实施计划:当前任务上下文或 `.tmp/<task-name>.md`
- 产品/验收文档:当前 `docs/` 融合文档,必要时新增 `docs/【产品验收】中文标题-YYYY-MM-DD.md`
- 技术设计:当前 `docs/` 融合文档,必要时新增 `docs/【技术方案】中文标题-YYYY-MM-DD.md`
- 共享经验或稳定流程:`docs/project-memory/shared-memory/``.codex/skills/`
BDD 文档建议包含:
````markdown
# <功能名> BDD 验收场景
## 背景
- 需求来源:
- 相关文档:
- 相关入口/接口:
## 角色与目标
- 角色:
- 目标:
- 非目标:
## 场景清单
### 功能: <能力>
```gherkin
场景: <场景名>
假如 ...
当 ...
那么 ...
```
````
## 测试映射
| 场景 | 测试层级 | 目标文件 | 状态 |
| ---- | --------- | -------- | ------- |
| ... | component | ... | planned |
## 开放问题
- ...
````
注意:上面的 Markdown 模板中如果嵌套代码块,需要在真实文档里调整围栏长度,避免代码块提前闭合。
## 评审检查清单
- [ ] 每个场景都有清晰角色或业务上下文。
- [ ] Given / When / Then 没有混入过多实现细节。
- [ ] Then 都是可观察、可测试、可人工验收的结果。
- [ ] 覆盖成功路径、失败路径、权限/登录态、边界条件。
- [ ] 明确哪些场景需要自动化,哪些只需人工验收。
- [ ] 自动化测试层级合理,没有把所有行为都塞进 E2E。
- [ ] 中文文案、剧情、注释、文档没有被无意翻译或改写成英文。
- [ ] 涉及中文文件修改时计划运行编码检查。
## 常见坑
1. **把 BDD 写成 UI 操作流水账。** 例如“点击第一个按钮,再点第二个按钮”。应改为用户意图和业务结果。
2. **Then 不可验证。** “体验更顺滑”不是验收标准;要写成加载状态、错误提示、数据状态、页面阶段等可观察结果。
3. **一个场景塞太多断言。** 长流程应拆成多个小场景,避免失败时不知道真正坏在哪里。
4. **只写 happy path。** Genarrative 常见风险在登录态、刷新恢复、异步失败、端口/后端不可用、旧数据兼容和移动端布局。
5. **把实现方案当成业务规则。** “调用某函数”通常不是用户行为;除非是 API/技术验收,否则放到技术设计或测试实现里。
6. **BDD 和 TDD 脱节。** 写完场景后要映射测试层级和目标文件,否则场景容易停留在文档层。
7. **场景词汇不统一。** work、draft、session、runtime、publish 等概念要和项目现有文档/代码保持一致。
8. **忽略文档先行约束。** 若 PRD 不足以编码落地,先补文档,再开始工程修改。
## 验证与收口
执行 BDD 相关任务后,至少确认:
- [ ] 已产出或更新 BDD 场景文档/计划。
- [ ] 场景已映射到具体测试层级和目标文件。
- [ ] 若进入编码,已按 TDD 或等价方式先补测试。
- [ ] 已运行相关验证命令,例如:
```bash
npm run check:encoding
npm run typecheck
npm run test -- --run <相关测试文件>
````
- [ ] 若涉及后端 Rust/API,按相关 DDD/SpacetimeDB 文档运行对应 cargo/npm/API smoke 验证。
- [ ] 若产生长期有效经验,已同步到 `docs/project-memory/shared-memory/` 或合适的仓库级 skill。
@@ -1,233 +0,0 @@
---
name: genarrative-admin-backoffice
description: 在 Genarrative/陶泥儿后台新增或修改管理页、后台 BFF 接口、shared-contracts/admin DTO、admin-web 路由导航、Excel/表格导出与验证发布时使用。
license: MIT
metadata:
codex:
tags:
[
Genarrative,
陶泥儿后台,
admin-web,
后台接口,
Excel导出,
Rust,
Axum,
SpacetimeDB,
]
related_skills: [genarrative-play-type-integration]
---
# Genarrative / 陶泥儿后台管理功能接入流程
用于在 Genarrative 项目中新增或修改陶泥儿后台管理端能力,包括后台页面、后台 API、管理端 DTO、导航路由、表格明细、导出、鉴权与验证。
## 适用场景
- 新增陶泥儿后台页面或导航项,例如“埋点数据”“任务配置”“邀请码”。
- 新增 `/admin/api/*` 接口。
- 修改 `apps/admin-web` 的后台页面、API client、路由、Shell 导航。
- 在后台展示 SpacetimeDB 表明细或统计数据。
- 新增“总览 → 单表查询”这类表统计跳转与查询页联动能力时,优先复用现有总览页的表统计作为入口,不另造第二套表目录。
- 后台导出 CSV / Excel / `.xls` 表格文件。
- 后台数据页中与业务事件、任务、登录等链路相关的问题,不能只看后台页面;要追到对应前台/API/reducer 写入点,确认“数据何时产生”。例如排查 `daily_login` 时,不要假设它一定由认证登录接口写入;先核对当前分支实现。历史实现曾在 `GET /api/profile/tasks` 打开任务中心时写入、`POST /api/profile/tasks/{task_id}/claim` 领奖时兜底写入;后续方案A把“任务中心读取写埋点”拆出为独立 procedure,任务中心只读取/刷新进度,登录成功链路应显式调用每日登录埋点入口。
## 标准落地顺序
### 0. 先确认现有后台入口
在新增后台页或回答“后台某个数据在哪里”前,先核对是否已有入口,避免重复造页:
- 数据库表统计当前在后台“总览”页,不是独立页面:`apps/admin-web/src/pages/AdminOverviewPage.tsx` 的“表统计”面板。
- 表统计行可直接跳转到表查询页:点击后设置 `window.location.hash = #tables?table=<tableName>`,由单独的 `#tables` 页接收参数并查询。
- `#tables` 页应在首次加载和 `hashchange` 时都重新读取 `table` 参数,避免只在初次 mount 时生效。
- 前端通过 `apps/admin-web/src/api/adminApiClient.ts``getAdminOverview(token)` 请求 `GET /admin/api/overview`
- 后端路由在 `server-rs/crates/api-server/src/app.rs` 挂载 `/admin/api/overview`handler 为 `admin_overview`
- 表统计逻辑在 `server-rs/crates/api-server/src/admin.rs``fetch_database_overview`:先读 SpacetimeDB schema 表名,再逐表执行 `SELECT COUNT(*) AS row_count FROM {table_name}`;private 或当前身份不可见会显示“不可统计(private 或当前身份不可见)”。
- DTO 在 `server-rs/crates/shared-contracts/src/admin.rs``AdminOverviewResponse` / `AdminDatabaseOverviewPayload` / `AdminDatabaseTableStatPayload`,前端对应类型在 `apps/admin-web/src/api/adminApiTypes.ts`
- 如果本次需求是“每张表都能查”,优先新增 `GET /admin/api/database/tables``GET /admin/api/database/tables/{tableName}/rows` 两个只读接口,并在前端新建统一的表查询页,而不是把查询逻辑塞回总览页。
### 1. 先补技术方案文档
项目要求工程修改前先检查/补充落地文档。优先更新当前融合文档;后台、接口、表查询、埋点和运营查询通常落到 `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。只有现有文档无法容纳时,才新增 `docs/【标签名】中文标题-YYYY-MM-DD.md`,至少说明:
- 后台页面目标。
- 后端接口路径、鉴权、query/body、response。
- 数据来源和是否修改 SpacetimeDB schema。
- 前端页面字段、筛选项、导出格式。
- 验收命令。
示例参考:
- `references/admin-tracking-events-export-2026-05-07.md`
- `references/admin-database-table-query-2026-05-08.md`
### 2. 后端 DTO 放 shared-contracts/admin
文件:
- `server-rs/crates/shared-contracts/src/admin.rs`
做法:
- 新增 request/query/response DTO。
- 使用 `#[serde(rename_all = "camelCase")]`
- 添加中文注释。
- 字段名与前端管理端类型保持一致。
如果 `apps/admin-web` 当前没有直接消费 Rust shared-contracts 生成物,还要同步:
- `apps/admin-web/src/api/adminApiTypes.ts`
### 3. 后端 handler 放 api-server/admin.rs
文件:
- `server-rs/crates/api-server/src/admin.rs`
- `server-rs/crates/api-server/src/app.rs`
要求:
- Handler 使用 `Extension(_admin): Extension<AuthenticatedAdmin>`,并在 router 中套 `require_admin_auth`
- 只读接口也必须走后台鉴权。
- query 参数使用 `Query<T>`
- 返回 `json_success_body(Some(&request_context), payload)`
-`app.rs` 挂到 `/admin/api/...`
### 4. 读取 SpacetimeDB 表明细时优先 HTTP SQL 只读
适合后台只读运营页:
- 不改表结构。
- 不新增 reducer。
- API Server 通过 SpacetimeDB HTTP SQL 读取真实数据。
注意:
- SQL 字段固定白名单,不要 `SELECT *`
- 用户输入只允许有限筛选字段,手动 trim、白名单枚举、字符串转义。
- limit 必须 clamp,例如默认 200、最大 1000。
- SpacetimeDB 2.2 HTTP SQL 不支持 `ORDER BY`;如果后台需要倒序展示明细,SQL 中不要拼 `ORDER BY`,先查有限 `LIMIT`,再在 api-server 内按时间字段排序,否则会返回 `HTTP 400 Unsupported: SELECT ... ORDER BY ... LIMIT ...`
- 如果 HTTP SQL 返回 `no such table ... If the table exists, it may be marked private`,不要急着改表名或新增 reducer;先确认本地 CLI 是否以当前 standalone 的 identity/token 登录。清空本地数据库或重建 standalone 后,旧 CLI token 可能看不到 private table。按“本地 private table SQL 权限修复”流程用 `/v1/identity` 获取 token,再 `spacetime login --token` 登录。
- SQL 解析要兼容 SpacetimeDB HTTP SQL 的 statement array + rows 形态。
- SpacetimeDB HTTP SQL 读取 private table 时,enum / Option / Timestamp 可能以 SATS 原始 JSON 返回,例如 `scope_kind=[3,[]]``Some("user")=[0,"user"]``None=[1,[]]``Timestamp=[1778207451731746]`。后台列表、详情弹窗和 Excel 导出不要直接展示这些原始形态;应在 api-server 解析层或前端展示层转换为人可读值:enum 映射为业务字符串,Option 的 None 显示 `-`,微秒级 Timestamp 格式化为本地可读时间。
- 可复用已有 `/v1/database/{db}/sql` 请求风格和 token 配置。
### 5. 前端接入 admin-web
常改文件:
- `apps/admin-web/src/api/adminApiTypes.ts`
- `apps/admin-web/src/api/adminApiClient.ts`
- `apps/admin-web/src/app/adminRoutes.ts`
- `apps/admin-web/src/app/AdminShell.tsx`
- `apps/admin-web/src/app/AdminApp.tsx`
- `apps/admin-web/src/pages/<AdminXxxPage>.tsx`
- `apps/admin-web/src/styles/admin.css`
接入步骤:
1.`adminApiTypes.ts` 增加 query/entry/list 类型。
2.`adminApiClient.ts` 增加 API 方法;用 `URLSearchParams` 拼非空 query。
3.`adminRoutes.ts` 增加 route id、label、hash。
4.`AdminShell.tsx` 增加 route icon`routeIcons` 必须覆盖全部 `AdminRouteId`
5.`AdminApp.tsx` import 并按 routeId 渲染页面。
6. 新增页面组件,保持 UI 简洁,不写大段规则说明。
7. 如果页面通过 hash 携带子参数,路由解析和页内参数解析要分开:`resolveAdminRoute()` 只负责路由片段,页面组件自己解析 `?table=` 之类的查询参数;同时要监听 `hashchange`,避免切页后参数不同步。
8. 列表行点击跳转优先用 hash,不要额外引入全局路由库或重新发明一套页面状态系统。
## Excel 导出推荐做法
后台运营导出不一定要引入 `xlsx` 依赖;简单表格可用浏览器端 HTML table + `.xls`
- Blob MIME`application/vnd.ms-excel;charset=utf-8`
- 文件扩展名:`.xls`
- 文本前加 UTF-8 BOM / `<meta charset="UTF-8">`
- 所有单元格做 HTML escape。
- ID、大数字、日期类字段使用 `mso-number-format:'\@';` 保持文本格式,避免 Excel 科学计数法。
- 导出当前筛选结果,避免后端新增 Excel 库依赖。
## 本地启动与联调
后台改完后如需本地查看页面和接口,优先按本次联调范围选择脚本:
```bash
# 只看后台页面 + api-server,不要求 SpacetimeDB 真实数据
npm run dev:api-server
npm run dev:admin-web -- --admin-web-host 127.0.0.1
# 完整 Rust 本地栈:SpacetimeDB + 发布模块 + api-server + 主站 + 后台
npm run dev
```
验证地址通常为:
- `npm run dev:api-server` 单独启动:api-server 默认 `http://127.0.0.1:8082/healthz`,后台前端默认 `http://127.0.0.1:3102/admin/`
- `npm run dev` 完整栈:SpacetimeDB `http://127.0.0.1:3101/v1/ping`api-server `http://127.0.0.1:8082/healthz`,主站 `http://127.0.0.1:3000/`,后台 `http://127.0.0.1:3102/admin/`
注意:
- `npm run dev:api-server` 首次启动可能先编译 Rust,后台进程短时间内无完整日志;等待编译完成后再查端口。
- 不要默认用 `3200` 验证 api-server;当前脚本默认 `GENARRATIVE_API_PORT=8082`,但会按端口占用情况漂移。不确定时以 `[dev] api-server:` 日志为准,或读取进程环境核对,敏感值输出必须打码。
- `admin-web``/` 可能返回 302 跳转到 `/admin/`;验证前端时直接请求 `/admin/`
- api-server 启动日志中 SpacetimeDB `127.0.0.1:3101` 连接被拒绝,不一定代表 api-server 没起来;只表示依赖的本地 SpacetimeDB 不可用。后台中需要读 SpacetimeDB 的页面(如埋点明细、表查询)要等 SpacetimeDB 可用后才能返回真实数据。
- `npm run dev` 依赖 `spacetime` CLI;先用 `command -v spacetime && spacetime --version` 确认可用。
- 本地和人工排障不再使用 `spacetime --root-dir`。如果看到 `bin/current/spacetimedb-cli` 缺失类错误,优先确认是否仍在运行旧脚本或旧发布包;本地开发应使用 `npm run dev` / `npm run dev:spacetime`,通过项目脚本和 `--data-dir` 隔离 SpacetimeDB 数据目录,不再把用户级 SpacetimeDB 安装同步到项目目录。
- `scripts/dev.mjs` 默认 `api timeout: 600s`. 合并 master 后首次 Rust 依赖/工作区重编译可能超过默认等待窗口,导致完整 `npm run dev` 在 api-server 就绪前超时并回收 SpacetimeDB。先让 Rust 编译完成,或临时用 `npm run dev:api-server -- --api-timeout-seconds 900` 预热 api-server 编译;之后再重新跑完整 `npm run dev`
- 用户贴出的 Codex background watch 通知可能来自已退出的旧 session。先用 `process poll` 查该 session 状态,再判断是否需要处理;不要把旧失败误判成当前服务失败。
## 测试与验证
常用命令:
```bash
# Rust 格式化检查
cd server-rs
cargo fmt -p api-server -p shared-contracts --check
# 后端相关测试,按测试名过滤
cargo test -p api-server admin_tracking -- --nocapture
# 前端后台类型检查 / 构建
cd ..
npm run admin-web:typecheck
npm run admin-web:build
# 中文/编码检查
npm run check:encoding
# diff 空白检查
git diff --check
```
如果 `npm run admin-web:typecheck``Cannot find module .../node_modules/typescript/bin/tsc`,说明当前 worktree 未安装 npm 依赖;先运行:
```bash
npm install
```
不要把该错误误判成 TypeScript 代码错误。
## 常见坑
1. 只在 `app.rs` import handler 不够,必须实际 `.route(...)` 挂载,并套 `require_admin_auth`
2. `cargo fmt --manifest-path server-rs/Cargo.toml` 在该 workspace 可能报 `Failed to find targets`;进入 `server-rs` 后用 `cargo fmt --all``cargo fmt -p api-server -p shared-contracts --check`
3. `cargo fmt --all` 可能格式化不相关 Rust 文件;提交前用 `git status` 检查并 revert 非本任务文件。
4. patch 工具对 Rust 单文件 lint 可能用 Rust 2015 edition 误报 `async fn is not permitted in Rust 2015`;以 `cargo test/check` 为准。
5. `adminRoutes` 新增 route id 后,`AdminShell.routeIcons` 必须同步,否则 TypeScript 会因 `satisfies Record<AdminRouteId, ...>` 报错。
- 后台页面中的中文和 JSON 预览要避免整文件重写导致编码问题;修改后运行 `npm run check:encoding`
- 后台数据页移动端要保证表格横向滚动,不要让整页布局撑坏。
- 若用户追问“之前不是说要把 npm run dev 修好吗”这类已承诺的 dev 启动问题,不要只解释;先复现 `npm run dev`,再按启动日志修脚本并验证到服务就绪。WSL/Linux 下本地开发应走 `spacetime start --data-dir=server-rs/.spacetimedb/local/data` 这一类数据目录隔离,不再用项目级 `--root-dir`,详见 `references/dev-rust-stack-startup-2026-05-08.md`
- 涉及敏感配置、token、密码、连接串时,输出和文档中统一写 `[REDACTED]`
## 参考资料
- `references/admin-database-table-query-2026-05-08.md`:本次后台数据库表查询接入的实现要点、校验规则与验证结果。
- `references/admin-tracking-events-export-2026-05-07.md`:本次新增后台“埋点数据”页、SpacetimeDB HTTP SQL 只读明细、前端 `.xls` 导出的实现细节。
- `references/private-table-sql-token-refresh.md`:本地清库/重建 standalone 后,用 `/v1/identity` + `spacetime login --token` 刷新 CLI token,以便 HTTP SQL 读取 private table。
- `references/spacetimedb-http-sql-sats-display.md`:通过 HTTP SQL 读取 private table 时,enum / Option / Timestamp 的 SATS 原始 rows 如何转换为后台列表、详情和 Excel 可读值。
- `references/daily-login-tracking-trigger-points.md`:排查后台 `daily_login` 埋点为何不是登录接口写入,而是任务中心读取/领奖兜底写入的触发点记录。
- `references/daily-login-auth-closure.md`:将方案A拆出的每日登录埋点入口接入真实认证成功链路时的推荐接入点、非阻断语义、测试和提交注意事项。
- `references/dev-rust-stack-startup-2026-05-08.md``npm run dev` / `scripts/dev.mjs` 在 WSL/Linux 下改用用户级 SpacetimeDB CLI、项目数据目录和显式 publish server,避免项目级 `--root-dir` 与冷编译超时的修复记录。
@@ -1,34 +0,0 @@
# 本次后台表查询接入的可复用经验
## 需求落点
- 后台“总览”页的表统计仍保留,只把每张表的表名改成可点击跳转到 `#tables?table=<name>`
- 新增独立 `#tables` 页承载表选择、关键词搜索、结构化字段筛选、limit、行详情弹窗(详情内保留字段复制,列头漏斗按钮可按列添加条件;每条条件可勾选启用或停用,停用时保留字段和值;`in` / `notIn` 使用逐项值标签,支持粘贴多行值,逗号不再作为隐式分隔符)。
## 后端实现要点
- 新增只读接口:
- `GET /admin/api/database/tables`
- `GET /admin/api/database/tables/{table_name}/rows`
- 表名必须来自 schema 白名单;再加一层 identifier 校验,避免任意 SQL 表名注入。
- `limit` 必须 clamp;本次实现使用默认 100、最大 500。
- `search` / `filters` 不进入 SQL 字符串:
- SQL 只负责 `SELECT * FROM {table_name} LIMIT {limit}`
- 返回后在 api-server 内存中过滤
- `filters` 接受两种 JSON 形式:object(列名到等值,如 `{"user_id":"u1"}`)与条件数组(如 `[{"column":"points","op":"gt","value":"5"}]`,运算符含 `eq``ne``gt``gte``lt``lte``contains``notContains``startsWith``endsWith``in``notIn``isEmpty``isNotEmpty`,允许同列多条件,条件间为 AND);非 object 且非数组直接 400,未知运算符或 value 形态不匹配也 400。数组中的 `eq` / `ne` 和其他标量运算符一样必须提供 `value`;显式空值判断使用 `isEmpty` / `isNotEmpty`
- SpacetimeDB HTTP SQL 返回可能是 statement array + rows,解析时要兼容这一层结构。
## 前端实现要点
- `adminRoutes` 必须新增 `tables``AdminShell.routeIcons` 也要同步覆盖。
- `AdminApp` 需要显式渲染 `AdminDatabaseTablesPage`
- 预览表格数据行直接点击(或行自身聚焦后按 Enter / Space)打开详情,行内按钮 / 输入控件的键盘操作不冒泡打开详情;详情按钮不单独占列。详情字段仅提供复制操作,成功、剪贴板失败和空字段复制都使用右下角自动消失的 Toast,JSON 预览与平台亮色 / 暗色主题保持一致,表头保持单行并在空间不足时省略显示。表单和标题区查询按钮共用筛选完整性校验。
- worktree 下可能没有本地 `node_modules/typescript/bin/tsc`,而根目录有依赖;在验证前可以临时把根目录 `node_modules` 软链到 worktree 再执行 `npm run admin-web:typecheck`,验证后删除软链,避免污染 git 状态。
## 验证结果
- `cargo test -p api-server admin_database -- --nocapture` 通过。
- `cargo fmt --manifest-path Cargo.toml -p api-server -p shared-contracts --check` 通过。
- `npm run admin-web:typecheck` 通过。
- `npm run admin-web:build` 通过。
- `npm run check:encoding` 通过。
@@ -1,136 +0,0 @@
---
name: genarrative-auth-session-flow
description: 在 Genarrative 中排查或修改登录、access token、refresh cookie、AuthGate 会话恢复、登录态刷新、认证埋点链路时使用。
license: MIT
metadata:
codex:
tags:
[Genarrative, auth, session, cookie, refresh-token, AuthGate, tracking]
related_skills:
[
systematic-debugging,
test-driven-development,
genarrative-profile-features,
]
---
# Genarrative 认证会话与登录埋点链路
用于 Genarrative 中登录、会话恢复、refresh cookie 续期、access token 补票、AuthGate 恢复登录态,以及每日登录/认证相关埋点的排查与修改。
## 适用场景
- 用户反馈登录态、cookie、自动续期、刷新页面后状态异常。
- 修改 `AuthGate``apiClient``authService` 或 Rust `api-server` 认证接口。
- 排查“已登录但打开网页没有触发登录埋点”等 session restore 场景。
- 修改手机验证码登录、密码登录、微信登录、重置密码后自动登录、refresh session rotate。
- 需要判断某个前端动作是否真正调用了后端 refresh/session 或埋点 procedure。
## 关键代码路径
前端:
- `src/components/auth/AuthGate.tsx`
- 登录态 hydrate / restore 的入口。
- 监听 `AUTH_STATE_EVENT` 后重新 hydrate。
- 是否先 refresh、再 `/api/auth/me`,决定打开页面是否进入后端 refresh 链路。
- `src/services/apiClient.ts`
- access token 本地保存、`ensureStoredAccessToken()``refreshStoredAccessToken()``fetchWithApiAuth()`
- `ensureStoredAccessToken()` 有 token 时会直接复用,不一定触发后端 refresh。
- `refreshStoredAccessToken()` 应直接调用 refresh 接口,用于必须轮换 cookie / 写续期埋点的场景。
- `src/services/authService.ts`
- `getCurrentAuthUser()` 请求 `/api/auth/me`
- 登录、登出、账号安全相关 API client。
后端:
- `server-rs/crates/api-server/src/auth_session.rs`
- 创建 refresh cookie / access token。
- `record_daily_login_tracking_event_after_auth_success(...)` 统一写每日登录埋点;失败 warning,不阻断认证流程。
- `server-rs/crates/api-server/src/refresh_session.rs`
- `POST /api/auth/session/refresh`
- rotate refresh session、签发新 access token、记录每日登录埋点。
- `server-rs/crates/api-server/src/auth_me.rs`
- `/api/auth/me` 只读取当前 access token 对应用户,不应假设它会触发 refresh 或登录埋点。
- `server-rs/crates/api-server/src/phone_auth.rs`
- `server-rs/crates/api-server/src/password_entry.rs`
- `server-rs/crates/api-server/src/password_management.rs`
- `server-rs/crates/api-server/src/wechat_auth.rs`
- 各真实认证成功入口。
- `server-rs/crates/spacetime-client/src/runtime.rs`
- `record_daily_login_tracking_event(user_id)` 调用 SpacetimeDB procedure。
- `server-rs/crates/spacetime-module/src/runtime/profile.rs`
- `record_daily_login_tracking_event_and_return` procedure。
- 任务中心读取不应污染每日登录埋点;如看到 `get_profile_task_center` 顺手写 `daily_login`,优先复核是否回归。
## 调试顺序
1. 先明确用户场景属于哪类:
- 新登录成功。
- cookie/access token 已过期后的自动刷新。
- 已登录且 cookie/access token 未过期时打开网页。
- 只调用 `/api/auth/me` 或某个受保护业务接口。
2. 查前端实际调用链,不要只看后端埋点点位:
- `AuthGate` hydrate 是否调用 `refreshStoredAccessToken()`
- 是否只是 `ensureStoredAccessToken()` + `/api/auth/me`
- `fetchWithApiAuth()` 是否因为已有 access token 而跳过 refresh
3. 查后端实际埋点点位:
- 登录成功入口是否在 session 创建后调用 helper。
- refresh session 是否在 rotate 与 access token 签发成功后调用 helper。
- 失败策略是否只 warning、不阻断响应。
4. 如涉及 SpacetimeDB procedure/table/binding,按项目 SpacetimeDB skills 与文档同步检查绑定生成、`migration.rs`、private table 限制。
5. 修改前补齐当前 `docs/` 中对应方案/根因;修改后同步更新当前融合文档和必要的共享记忆。
## 关键经验:已登录打开网页也要主动 refresh 才能写登录埋点
常见误判:后端已经在 refresh cookie 续期时写每日登录埋点,就以为“打开网页”会触发埋点。
实际链路中,如果用户已经登录且本地 access token 还有效:
1. `ensureStoredAccessToken()` 会直接返回已有 token。
2. `AuthGate` 随后请求 `/api/auth/me`
3. `/api/auth/me` 只校验/读取用户,不会 rotate refresh session。
4. 因此后端 refresh/session 埋点不会触发。
若产品要求“已登录且 cookie 没过期时打开网页也记录登录埋点”,`AuthGate` 的 restore/hydrate 应主动调用 `refreshStoredAccessToken()`,再调用 `getCurrentAuthUser()`
## 每日登录埋点原则
- 真实登录成功:在 refresh session / access token 创建成功后记录。
- cookie refresh 续期:在 rotate refresh session 成功且新 access token 签发成功后记录。
- 已登录打开网页:前端必须主动走 refresh 续期链路,不能只请求 `/api/auth/me`
- `login_method` 对于 refresh 场景使用 refresh session 保存的 `issued_by_provider`
- 埋点失败不阻断登录、续期、会话恢复或 token 返回,只记录 warning。
- 任务中心读取不应作为登录埋点来源,避免后台查看/刷新任务中心污染登录数据。
## 测试与验证命令
按改动范围选择:
```bash
npm run test -- AuthGate.test.tsx
npm run typecheck
cd server-rs && cargo test -p api-server auth_session -- --nocapture
cd server-rs && cargo test -p api-server refresh_session_rotates_cookie_and_returns_new_access_token -- --nocapture
cd server-rs && cargo check -p api-server
cd server-rs && cargo check -p spacetime-client
cd server-rs && cargo check -p spacetime-module
npm run check:encoding
git diff --check
```
注意:Vitest 0.34 不支持 Jest 的 `--runInBand`;不要把 `--runInBand` 加到 `npm run test -- AuthGate.test.tsx` 后面。
## 常见坑
1.`/api/auth/me` 当作 refresh:它只读当前 access token,不会写 refresh 埋点。
2. 只在后端 refresh handler 加埋点,但前端有有效 access token 时根本不调用 refresh。
3. `ensureStoredAccessToken()` 有 token 时会直接返回;需要强制 refresh 时应使用 `refreshStoredAccessToken()`
4. 在埋点 helper 中返回错误并阻断登录/续期,会破坏认证主链路。
5. refresh 场景把 `login_method` 写死为 password,会丢失手机/微信来源。
6. 修改中文文件后忘记 `npm run check:encoding`
7. `cargo fmt -p api-server` 或前端测试可能让 `.env.local``.gitignore` 出现非业务改动;提交前用 `git status --short` 检查并撤回无关敏感/环境文件。
## 参考资料
- `references/session-restore-daily-login-tracking-2026-05-08.md`:已登录且 cookie 未过期时打开网页未触发每日登录埋点的根因与修复案例。
@@ -1,145 +0,0 @@
---
name: genarrative-dev-stack-port-routing
description: 在 Genarrative 中修改 npm run dev / dev:spacetime / dev:api-server / dev:bgfilter-worker / dev:web / dev:admin-web 的本地启动端口、端口可用性探测、端口漂移、SpacetimeDB publish server、Rust 进程环境变量、Vite 代理目标和后台 admin-web 启动串联时使用。
license: MIT
metadata:
codex:
tags:
[
Genarrative,
dev-stack,
端口探测,
Vite,
api-server,
SpacetimeDB,
npm-run-dev,
]
related_skills: [genarrative-admin-backoffice]
---
# Genarrative 本地 dev 启动端口与代理目标串联流程
用于维护 Genarrative 本地开发栈启动脚本,重点覆盖 `npm run dev` 与五个 `dev:*` 单模块命令的端口检查、端口漂移和后续流程目标传递。
## 适用场景
- 修改 `scripts/dev.mjs``scripts/dev-utils.mjs``scripts/dev-stack-port-utils.mjs` 或 AI 游戏创作客户端 dev 启动器。
- 处理 `3000``3101``3102``8082` 等端口被占用导致本地开发栈启动失败。
- 排查 Vite 代理仍指向旧 api-server 端口、前端打开了旧 dev server、后台代理错配。
- 调整 SpacetimeDB standalone、publish、Rust `api-server`、主站 Vite、后台 Vite 的启动顺序。
- 修改本地联调文档或 `docs/project-memory/shared-memory/pitfalls.md` 中的 dev 启动口径。
## 当前端口职责
默认优先端口:
1. 主站 Vite`3000`,对浏览器通常展示为 `http://127.0.0.1:<web-port>/`
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. 独立 BgFilter worker`8083`,就绪检查为 `http://127.0.0.1:<bgfilter-worker-port>/readyz`
6. AI 游戏创作 Vite:非 Linux 兼容首选 `3080`Linux 使用当前用户端口段的 `start + 5`
端口不可用时,脚本会从优先端口开始向后寻找可用端口。后续流程必须以解析后的实际端口为准,不能继续使用默认端口。
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``bgfilterWorker = start + 4``agcVite = start + 5`。注册表锁文件是 `/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:spacetime` / `dev:api-server` / `dev:bgfilter-worker` / `dev:web` / `dev:admin-web`:执行 `node scripts/dev.mjs <module>``dev:api-server` 会安全带起其依赖的 BgFilter worker。
- `scripts/dev-stack-port-utils.mjs`
- `isPortAvailable(...)`:探测端口是否可监听。
- `findAvailablePort(...)`:从优先端口向后寻找可用端口,`0` 表示申请临时端口。
- `resolveDevStackPorts(...)`:一次性解析 SpacetimeDB、api-server、主站 Vite、后台 Vite、BgFilter worker 端口,并避免本次解析结果互相冲突。
- 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 bgfilterWorker:127.0.0.1:8083`
- `scripts/dev.mjs`
- 解析 CLI 参数后统一计算 client host、端口、`SPACETIME_SERVER``RUST_SERVER_TARGET`
- 完整栈按 SpacetimeDB、publish、BgFilter worker readiness、api-server readiness、主站 Vite、后台 Vite 顺序启动。
- Linux 下会先申请系统级端口段并映射成六个预留槽位;主 dev 栈使用前五个,AGC Vite 使用 `start + 5`,自动分配从 `10000-10099` 起。Windows 的主 dev 栈和 AGC 则各自沿用统一端口探测与漂移逻辑。
- 完整栈和 `dev:api-server` 把两个 Rust 进程作为同一重启单元,先全部停止,再先启动 BgFilter worker、后启动 api-server;不要为同一份 Rust 源码创建两个并发 `cargo` watcher。
- 单模块命令复用同一套参数和 env 解析。
- `apps/ai-game-creator-shell/scripts/dev-port.mjs`
- 复用系统级用户端口段,解析 AGC Vite 的 `start + 5` 首选槽位。
- 把最终端口通过 `GENARRATIVE_AGC_VITE_PORT` 同步给 `beforeDevCommand` 和配套后端端口解析器,通过 Tauri CLI `--config` 同步 `build.devUrl`,并通过 Vite CLI `--port` 同步 `strictPort` 监听。
## 必须保持的传递链路
`npm run dev` 和五个 `dev:*` 单模块命令中端口解析后,必须同步到以下位置:
1. SpacetimeDB 启动:`spacetime start --listen-addr "${SPACETIME_HOST}:${SPACETIME_PORT}"`
2. SpacetimeDB 发布:`spacetime publish ... --server "${SPACETIME_SERVER}"`
3. Rust api-server`GENARRATIVE_API_HOST``GENARRATIVE_API_PORT``GENARRATIVE_SPACETIME_SERVER_URL``GENARRATIVE_SPACETIME_DATABASE`
4. api-server 健康检查:`wait_for_api_server "${RUST_SERVER_TARGET}/healthz" ...`
5. BgFilter worker`GENARRATIVE_PROCESS_ROLE=bgfilter-worker`、解析后的 `HOST / PORT`、与父 API 相同的 `GENARRATIVE_BGFILTER_WORKER_BASE_URL` / `GENARRATIVE_BGFILTER_INTERNAL_TOKEN`,以及显式有效的 `N / Q`
6. BgFilter worker readiness:父 API 启动前检查解析后地址的 `/readyz`
7. 主站 Vite`RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET``ADMIN_WEB_TARGET``ADMIN_WEB_PORT``--port=${WEB_PORT}``--host=${WEB_HOST}`
8. 后台 Vite`ADMIN_API_TARGET``GENARRATIVE_API_TARGET``GENARRATIVE_API_PORT``--port=${ADMIN_WEB_PORT}`
9. 控制台日志:`[dev:ports]``[dev] web/admin web/api-server/bgfilter-worker/spacetime` 必须显示最终实际地址。
10. Linux 端口段注册:`[dev] port-range:``[dev] port-range-registry:` 只在 Linux 输出,Windows 不应依赖系统级注册表。
11. AI 游戏创作客户端:外层启动器解析最终 AGC Vite 端口后,通过 Tauri CLI `--config` 覆盖 `build.devUrl`,把同一 `GENARRATIVE_AGC_VITE_PORT` 传给 `beforeDevCommand` 与配套后端端口解析器,并用 Vite CLI `--port` 启动严格监听;后端端口漂移必须跳过该预留端口。
如果只改了其中一段,通常会出现:浏览器打开的前端可用,但 `/api/*` 代理到旧端口;后台页面可用但后台 API 失败;SpacetimeDB 启动在新端口但 publish 仍发往旧端口。
## 修改流程
1. 先读当前脚本和文档:
- `scripts/dev-stack-port-utils.mjs`
- `scripts/dev.mjs`
- `scripts/dev-utils.mjs`
- `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
- `docs/project-memory/shared-memory/pitfalls.md`
2. 优先改公共端口工具,不要把端口探测逻辑复制到多个脚本。
3. 修改 `scripts/dev.mjs` 时确认变量顺序:先解析参数和端口,再构造 `SPACETIME_SERVER` / `RUST_SERVER_TARGET`,最后启动对应 service。
4. 修改 watch 时保持模块边界:SpacetimeDB 只监听 `spacetime-module` 且改动后重新 publish,不重启 standalone 宿主;api-server 排除 `spacetime-module`web/admin-web 源码变化交给 Vite 自身 HMR,外层调度器不要再监听前端目录重启 Vite。
5. 修改 `dev:web` 时不要自动改后端目标策略;`dev:web` 只负责主站 Vite 端口可用性与已有后端目标选择。
6. 同步更新技术文档和团队共享记忆。
7. 如果修改 Linux 端口段注册口径,确认 Windows 分支仍保持旧行为,不要把系统级注册表逻辑扩散到 Windows。
## 测试与验证
最小验证:
```bash
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 bgfilterWorker:127.0.0.1:0
```
端口冲突回归测试建议:
1. 用测试或临时 Node server 占用某个优先端口。
2. 调用 `findAvailablePort`,断言结果大于被占用端口。
3. 调用 `resolveDevStackPorts`,断言五个结果互不相同。
4. 如果实际启动完整栈,观察控制台:
- `[dev:ports] ... 不可用,改用 ...`
- `[dev] api-server: http://...:<actual-api-port>`
- `[dev] spacetime: http://...:<actual-spacetime-port>`
- 主站和后台 Vite 启动端口与日志一致。
完整启动属于长驻进程。需要 smoke 时用 background 方式启动,并另开命令检查 api-server `/healthz`、BgFilter worker `/readyz`、SpacetimeDB `/v1/ping` 和两个页面端口;不要等待 `npm run dev` 自然退出。检查地址必须取 `.app/dev-stack.json` 或启动日志中的实际端口,不能假定 worker 一定停在 `8083`
## 常见坑
1. **只让 Vite 自己漂移端口。** 这样终端可能出现可访问前端,但脚本和文档仍认为是 `3000`,后台目标或日志会错。
2. **只改 SpacetimeDB start,不改 publish。** standalone 可能监听新端口,但 publish 仍连旧 `3101`
3. **只改 `GENARRATIVE_API_PORT`,不改 `RUST_SERVER_TARGET`。** api-server 已在新端口监听,但 Vite 代理仍打旧端口。
4. **使用 `0.0.0.0` 作为浏览器访问地址。** 监听可以是 `0.0.0.0`,展示给用户和健康检查通常用 `127.0.0.1`
5. **端口探测和实际启动之间存在竞态。** 已经探测可用的端口仍可能被外部进程抢占;SpacetimeDB 启动后仍要解析实际监听地址,api-server 和 Vite 失败时要打印清晰日志。
6. **运行全仓库 lint 误判。** 当前仓库可能有既有 lint 问题。验证本功能时优先运行定向测试、Bash 语法检查、编码检查,并在最终说明中区分既有 lint 失败与本次改动。
## 验收清单
- [ ] 端口工具有测试覆盖端口被占用和多端口互斥解析。
- [ ] Linux 注册表分配、同用户复用固定段并继续漂移、自动分配从 `10000-10099` 起、Windows bypass 都有测试覆盖。
- [ ] `scripts/dev.mjs` 通过 `node --check`
- [ ] `npm run dev` 的 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 都使用实际端口。
- [ ] BgFilter worker 在 api-server 前 ready,父子共享实际 base URL / TokenRust watch 只触发一次组合重启。
- [ ] `npm run dev:web` 在主站端口不可用时能切换到可用端口。
- [ ] `npm run agc` 在 Linux 使用用户段 `start + 5`Tauri、Vite、marker 和预检使用同一最终端口。
- [ ] 文档同步更新 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
- [ ] 长期踩坑同步更新 `docs/project-memory/shared-memory/pitfalls.md`
- [ ] 修改中文文件后运行 `npm run check:encoding`
@@ -17,7 +17,7 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
2. Before the first generation in a new conversation, obtain a canvas name unless an existing `projectId` and `assetFolderId` were supplied. Create or reuse a project and a same-name asset-library folder. Retain `canvasName`, `projectId`, `assetFolderId`, and the current art spec.
3. Normalize art requests into a reusable spec. Ask only for missing values that block the selected operation. Reuse the spec until the user changes its style, subject family, palette, format, or constraints.
4. Infer the operation from the user's intent. Do not ask the user to select an API unless two operations would produce materially different artifacts.
5. If a reference exists only as a local file, upload and confirm it first. Pass the stable returned `objectKey` to operations that accept object references; never substitute a temporary signed URL. For an icon-spritesheet primary spec, additionally create a project resource or asset record with `assetKind="icon-spec"`, then pass the returned resource or asset ID as `referenceId`.
5. If a reference exists only as a local file, upload and confirm it first. Pass the stable returned `objectKey` to generation; never substitute a temporary signed URL.
6. For generation endpoints that support the fields, include `projectId`, `assetFolderId`, an asset label, and `canvasCompletion` so the result enters both the canvas and its same-name library folder.
7. Treat every generation POST as asynchronous. Send one stable `Idempotency-Key` per logical request, retain the returned `operationId`, and poll the returned `statusUrl` or `GET /api/external/v1/generations/{operationId}` according to `pollAfterMs`.
8. Consume `result` only after `status=completed`. On `failed`, surface the safe error. On a client timeout or lost response, retain the operation/key; do not create a replacement request.
@@ -27,12 +27,11 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
## Essential Invariants
- Authenticate MCP and business API calls with `Authorization: Bearer <tnr_sk_...>`. Never ask the user to paste a key into chat or place one in repository files.
- All nine generation POST routes require `Idempotency-Key` and return HTTP `202`; `202` is durable acceptance, not a media result.
- All eight generation POST routes require `Idempotency-Key` and return HTTP `202`; `202` is durable acceptance, not a media result.
- Retry an uncertain submission only with the exact same body and the same idempotency key. A polling timeout is not permission to generate again.
- Use stable references such as `objectKey`, project resource ID, or asset ID where each operation permits them. Image edit/redraw is stricter: `sourceReferenceId` accepts only a registered project resource ID or asset ID; upload confirmation alone is not enough. Use `/assets/read-url` only for temporary preview/download access.
- Use stable references such as `objectKey`, project resource ID, or asset ID in generation requests. Use `/assets/read-url` only for temporary preview/download access.
- Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either.
- Do not invent missing derivatives. A source-preserved warning means the main source remains usable but requested post-processing failed. A slice warning means the complete transparent sheet is usable but individual slices are absent.
- For successful `style="pixelArt"`, treat completed-result and nested resource/asset dimensions as the final logical-grid PNG dimensions. They may differ from `size`, `imageSize`, the provider image, and `canvasCompletion.placeholder`; do not rescale or reject the artifact to match those inputs.
- Keep generated artifacts in the canvas and asset library together. Character animation accepts `assetFolderId` and `assetLabel`; its completed result directly returns the final `assetKind="character-animation"` resource and asset with formal sequence fields. Do not create a duplicate first-frame record.
## Documentation Navigation
@@ -106,21 +105,6 @@ client.generate_image(
)
```
For background removal, pass a stable owner-scoped object key, project resource ID, or asset ID; the helper keeps the same asynchronous submission and polling contract:
```python
session = client.prepare_canvas_session("去背景画布")
client.remove_background(
"editor-upload/object.png",
source_width=720,
source_height=1280,
canvasSession=session,
assetLabel="去背景结果",
)
```
Background removal preserves the source pixel size. For normal canvas placement with `canvasSession`, pass the real `source_width` and `source_height`, or provide both `canvasWidth` and `canvasHeight`; the helper rejects missing dimensions instead of guessing a square placeholder. `assetKind` may only describe a static image and must match the authoritative source record. Prefer a project resource ID or asset ID when the same object key has multiple semantic registrations; for a raw object key outside in-place replacement, pass `sourceResourceId` to disambiguate. Passing `targetLayerId` selects in-place replacement: the helper retains the session's project/library context but does not inject `canvasCompletion`, and it rejects an explicit `canvasCompletion` combined with `targetLayerId`. The target layer must point to the same authoritative object as the source, and the server durably binds a raw object key to that target resource for Worker revalidation.
Helper convenience methods wait locally, but the server still uses short asynchronous submit/status requests. For durable caller-controlled orchestration, call `submit_generation`, persist its `operationId` and idempotency key, then call `get_generation` or `wait_for_generation`.
For character animation, pass the canvas session and asset label to `animate_character`. The helper submits asynchronously and returns the completed compact result containing the authoritative formal `resource` and `asset`; do not synthesize a library asset from the first frame.
@@ -1,6 +1,6 @@
interface:
display_name: 'Genarrative External Editor API'
short_description: 'Route async canvas generation safely'
default_prompt: 'Use $genarrative-external-editor-api to discover the hosted integration, prepare a canvas session, and submit and poll asset generation into the canvas and library.'
display_name: "Genarrative External Editor API"
short_description: "Route async canvas generation safely"
default_prompt: "Use $genarrative-external-editor-api to discover the hosted integration, prepare a canvas session, and submit and poll asset generation into the canvas and library."
policy:
allow_implicit_invocation: true
@@ -6,40 +6,33 @@ All paths below are relative to `https://www.genarrative.world`. Discovery and S
## Project and Canvas Operations
| Operation | Method and path | Minimum input |
| -------------------- | ------------------------------------------------------------- | -------------------------------------------------------------- |
| List projects | `GET /api/external/v1/editor/projects` | Authentication; optional `view=full\|summary` (default `full`) |
| Create project | `POST /api/external/v1/editor/projects` | Optional `title` |
| Load recent project | `GET /api/external/v1/editor/projects/recent` | Authentication |
| Get project | `GET /api/external/v1/editor/projects/{projectId}` | `projectId` |
| Delete project | `DELETE /api/external/v1/editor/projects/{projectId}` | `projectId` |
| Rename project | `PATCH /api/external/v1/editor/projects/{projectId}/metadata` | `title` |
| Save canvas | `PATCH /api/external/v1/editor/projects/{projectId}/canvas` | `viewport`, `layers`, `expectedRevision` |
| Add project resource | `POST /api/external/v1/editor/projects/{projectId}/resources` | `imageSrc`, `width`, `height`, `sourceType` |
| Operation | Method and path | Minimum input |
| --- | --- | --- |
| List projects | `GET /api/external/v1/editor/projects` | Authentication |
| Create project | `POST /api/external/v1/editor/projects` | Optional `title` |
| Load recent project | `GET /api/external/v1/editor/projects/recent` | Authentication |
| Get project | `GET /api/external/v1/editor/projects/{projectId}` | `projectId` |
| Delete project | `DELETE /api/external/v1/editor/projects/{projectId}` | `projectId` |
| Rename project | `PATCH /api/external/v1/editor/projects/{projectId}/metadata` | `title` |
| Save canvas | `PATCH /api/external/v1/editor/projects/{projectId}/canvas` | `viewport`, `layers`, `expectedRevision` |
| Add project resource | `POST /api/external/v1/editor/projects/{projectId}/resources` | `imageSrc`, `width`, `height`, `sourceType` |
Canvas save uses optimistic revision control. Pass the last authoritative `expectedRevision`; on conflict, reload instead of replaying a stale full layout.
Project listing supports two views:
- `view=full` is the REST default and returns the complete project, canvas, layers, and resources.
- `view=summary` returns only `projectId`, `title`, `updatedAt`, and nullable `cover`, so callers can display, search, disambiguate same-name projects, and select a safe target without loading every canvas snapshot.
- Hosted MCP `list_editor_projects` always uses `summary`; call `get_editor_project` after selecting a `projectId` when complete authoritative state is required.
- `cover` contains only `resourceId`, stable `objectKey`, dimensions, and `updatedAt`. It never embeds image bytes, a Data URL, or a signed URL. To display it, pass `cover.objectKey` to `get_external_asset_read_url`; signed URLs are temporary and must not be persisted or reused as generation references.
## Asset and Upload Operations
| Operation | Method and path | Minimum input |
| --------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------- |
| Create direct-upload ticket | `POST /api/external/v1/assets/direct-upload-tickets` | `legacyPrefix`, `fileName` |
| Confirm uploaded object | `POST /api/external/v1/assets/objects/confirm` | `objectKey`, `assetKind` |
| Get signed read URL | `GET /api/external/v1/assets/read-url` | `objectKey` or `legacyPublicPath` |
| Read asset library | `GET /api/external/v1/editor/assets/library` | Authentication |
| Create folder | `POST /api/external/v1/editor/assets/folders` | `label` |
| Update folder | `PATCH /api/external/v1/editor/assets/folders/{folderId}` | `label` or `collapsed` |
| Delete folder | `DELETE /api/external/v1/editor/assets/folders/{folderId}` | `folderId` |
| Create asset record | `POST /api/external/v1/editor/assets` | `folderId`, `label`, `imageSrc`, `width`, `height`, `sourceType` |
| Update asset record | `PATCH /api/external/v1/editor/assets/{assetId}` | `label` or `folderId` |
| Delete asset record | `DELETE /api/external/v1/editor/assets/{assetId}` | `assetId` |
| Operation | Method and path | Minimum input |
| --- | --- | --- |
| Create direct-upload ticket | `POST /api/external/v1/assets/direct-upload-tickets` | `legacyPrefix`, `fileName` |
| Confirm uploaded object | `POST /api/external/v1/assets/objects/confirm` | `objectKey`, `assetKind` |
| Get signed read URL | `GET /api/external/v1/assets/read-url` | `objectKey` or `legacyPublicPath` |
| Read asset library | `GET /api/external/v1/editor/assets/library` | Authentication |
| Create folder | `POST /api/external/v1/editor/assets/folders` | `label` |
| Update folder | `PATCH /api/external/v1/editor/assets/folders/{folderId}` | `label` or `collapsed` |
| Delete folder | `DELETE /api/external/v1/editor/assets/folders/{folderId}` | `folderId` |
| Create asset record | `POST /api/external/v1/editor/assets` | `folderId`, `label`, `imageSrc`, `width`, `height`, `sourceType` |
| Update asset record | `PATCH /api/external/v1/editor/assets/{assetId}` | `label` or `folderId` |
| Delete asset record | `DELETE /api/external/v1/editor/assets/{assetId}` | `assetId` |
Upload is a three-step client flow: create a ticket, POST the file and returned fields directly to the OSS form endpoint, then confirm the returned `objectKey`. See `authentication-and-safety.md` before implementing this flow.
@@ -47,19 +40,18 @@ Upload is a three-step client flow: create a ticket, POST the file and returned
Every generation row requires a stable `Idempotency-Key` header and returns HTTP `202` with an asynchronous submission, not the generated media.
| Capability | POST path | Required body fields | Common optional body fields |
| ------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
| Background removal | `/api/external/v1/editor/images/background-removals` | `sourceImageSrc` | `projectId`, `sourceResourceId`, `targetLayerId`, static-image `assetKind`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Sound effect | `/api/external/v1/editor/audios/sound-effects/generations` | `prompt` | `model`, `duration`, `loop`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Background music | `/api/external/v1/editor/audios/background-music/generations` | `gptDescriptionPrompt`, `makeInstrumental` | `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Capability | POST path | Required body fields | Common optional body fields |
| --- | --- | --- | --- |
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceImageSrc` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `sourceResourceId`, `targetLayerId`, `canvasCompletion` |
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceImageSrc`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Sound effect | `/api/external/v1/editor/audios/sound-effects/generations` | `prompt`, `duration` | `model`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Background music | `/api/external/v1/editor/audios/background-music/generations` | `gptDescriptionPrompt`, `makeInstrumental` | `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
Poll all nine through:
Poll all eight through:
```text
GET /api/external/v1/generations/{operationId}
@@ -73,35 +65,29 @@ Supply the `operationId` returned by submission. Poll no faster than `pollAfterM
- Pass `assetFolderId` plus `assetLabel` for image, edit, icon spritesheet, video, sound effect, and BGM operations when supported.
- UI extraction uses `assetFolderId` and `spritesheetLabel`.
- Character animation accepts `assetFolderId` and `assetLabel`. Its completed compact result directly returns the final `assetKind="character-animation"` resource and asset with `imageSequenceFrames` and `imageSequenceDurationMs`; never create a duplicate first-frame resource or asset.
- Background removal derives the final static-image `assetKind` from the authoritative source record. A conflicting request kind or any video, audio, animation, or image-sequence kind returns `400` before queueing. Without `canvasCompletion`, `targetLayerId` must point to the same authoritative object as `sourceImageSrc` (prefer `assetObjectId`, otherwise canonical bucket/object key).
- If a caller must manually create a `character-animation` resource or asset, put the authoritative frames and total sequence duration in `imageSequenceFrames` and `imageSequenceDurationMs`. Keep `generationInputs` replayable: it must not contain legacy runtime fields such as `characterAnimation`, `frames`, `previewVideoPath`, `frameCount`, `fps`, or `durationSeconds`.
- Reload project/library state after completion when full current state is required.
## Reference Field Mapping
After confirming a local upload, pass its stable `objectKey` into operations that accept object references:
After confirming a local upload, pass its stable `objectKey` into:
| Target capability | Field |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Image generation | `referenceImageSrcs` |
| Image edit/redraw | `sourceReferenceId` must be a registered project resource ID or asset ID; additional references remain in `referenceImageSrcs` |
| Icon spritesheet | Register the primary spec as an `assetKind="icon-spec"` project resource or asset, then pass its returned ID as `referenceId`; additional style references remain in `referenceImageSrcs` |
| UI design extraction | `sourceImageSrc`; additional references in `referenceImageSrcs` |
| Character animation | `sourceImageSrc` |
| Video with image references | `referenceImageSrcs` |
| Target capability | Field |
| --- | --- |
| Image generation | `referenceImageSrcs` |
| Image edit/redraw | `sourceImageSrc`; additional references in `referenceImageSrcs` |
| Icon spritesheet | `referenceImageSrc`; additional style references in `referenceImageSrcs` |
| UI design extraction | `sourceImageSrc`; additional references in `referenceImageSrcs` |
| Character animation | `sourceImageSrc` |
| Video with image references | `referenceImageSrcs` |
For image edit/redraw, confirming an upload is not sufficient: create a project resource or asset-library record first, then pass that record's ID as `sourceReferenceId`. The main source never accepts objectKey, URL, Data URL, or Blob URL. Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference.
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
`sliceLayout: "grid-2x2"` is an opt-in contract for four fixed game-runtime assets. The provider prompt and server persistence both preserve the ordered slots left-top, right-top, left-bottom, right-bottom. Omit it to retain the default connected-component slicing behaviour for ordinary free-form icon sheets.
Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference.
## Common Values
Use OpenAPI as the final authority; these common values are a routing aid:
- Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`; ordinary image generation may omit it.
- External v1 currently has no structured game-scene generation operation. Do not send `kind: "scene"` or `assetKind: "scene"` through generic image generation; the server rejects both before queueing.
- Image `model`: `gpt-image-2`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`.
- Image `aspectRatio`: `1:1`, `2:3`, `3:2`, `9:16`, `16:9`.
- Image `imageSize`: `0.5K`, `1K`, `2K`.
@@ -109,7 +95,6 @@ Use OpenAPI as the final authority; these common values are a routing aid:
- Video `aspectRatio`: `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `21:9`.
- Video `resolution`: `480p`, `720p`, `1080p`; `mode`: `std`; `sound`: `on` or `off`.
- Character animation uses `model: "seedance2.0-fast"`; `resolution`: `480p` or `720p`; `frameCount`: `32`, `40`, or `48`; `durationSeconds`: `4`, `5`, or `6`; `ratio`: `same`, `1:1`, `4:3`, `16:9`, `9:16`, or `3:4`.
- Sound effect uses canonical model `eleven_text_to_sound_v2`; omit `duration` or send `null` for automatic duration, otherwise send a finite `0.5-30` number. `loop` defaults to `false` and remains independent from Prompt text.
- UI extraction uses `aspectRatio: "1:1"`; use `imageSize: "1K"` for normal/small extraction and `2K` for dense designs.
Do not hard-code this list as a replacement client schema. In particular, the top-level image `style` field is intentionally extensible; see `requests-and-outputs.md` for its fallback behavior.
@@ -43,24 +43,23 @@ Infer what is already clear and ask only for missing fields that block the selec
## Intent Map
| User intent | MCP/REST capability |
| ----------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Generate a background, character, spec, UI mockup, or publication image | Image generation |
| Redraw, retouch, or replace an existing image | Image edit |
| Remove the background from an existing image | Background removal |
| Generate from a local reference | Upload and confirm the local file, then image generation or edit |
| Build a reusable transparent icon/game atlas from a visual spec | Icon spritesheet generation |
| Extract marked assets from an existing UI design | UI design asset extraction |
| Animate a character into frames | Character animation generation |
| Generate video | Video generation |
| Generate a sound effect | Sound-effect generation |
| Generate background music/BGM | Background-music generation |
| Upload a local image/audio/video asset | Upload ticket -> OSS form upload -> object confirm |
| Save viewport/layers | Canvas save |
| Create, load, rename, or delete a canvas | Project operations |
| Organize folders and asset records | Asset-library operations |
| Obtain temporary access to private media | Signed read URL |
| Check generation progress or retrieve its result | Generation query |
| User intent | MCP/REST capability |
| --- | --- |
| Generate a background, character, spec, UI mockup, or publication image | Image generation |
| Redraw, retouch, or replace an existing image | Image edit |
| Generate from a local reference | Upload and confirm the local file, then image generation or edit |
| Build a reusable transparent icon/game atlas from a visual spec | Icon spritesheet generation |
| Extract marked assets from an existing UI design | UI design asset extraction |
| Animate a character into frames | Character animation generation |
| Generate video | Video generation |
| Generate a sound effect | Sound-effect generation |
| Generate background music/BGM | Background-music generation |
| Upload a local image/audio/video asset | Upload ticket -> OSS form upload -> object confirm |
| Save viewport/layers | Canvas save |
| Create, load, rename, or delete a canvas | Project operations |
| Organize folders and asset records | Asset-library operations |
| Obtain temporary access to private media | Signed read URL |
| Check generation progress or retrieve its result | Generation query |
Do not present an API menu unless the request is genuinely ambiguous. Ask a follow-up when two routes create different artifacts, for example “处理这张图” could mean edit, extract marked UI assets, or use it as a reference for a new generation.
@@ -79,9 +78,9 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For the four-category game contract it must also send `sliceLayout: "grid-2x2"`; this is an explicit fixed-slot contract, not a client-side guessed crop.
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceImageSrc` plus concrete `iconDescriptions`.
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require response `sliceLayout: "grid-2x2"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.

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