Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dca066a87 | |||
| 02ae3d3cad | |||
| d04339ad7f | |||
| 6fe52a92bb | |||
| 75e3f1d041 | |||
| 8a4b71bf8e | |||
| 53e37ea361 | |||
| dc7c7dc6cc | |||
| 3ec3a09380 | |||
| 752505547d | |||
| 203ce5b9e7 | |||
| d1a47c0fe7 | |||
| 2e0ef02fda | |||
| ab16c87c5d | |||
| 6d8c7ae496 | |||
| 80bcb4ba0e | |||
| df4e61a208 | |||
| fef53b634e | |||
| ba996aad80 | |||
| d374f3292a | |||
| 2c65878b60 | |||
| 7339bb5da1 | |||
| c24e3010f8 | |||
| d8064eff49 | |||
| bda0d0d398 | |||
| b95da30721 | |||
| 7f038490f1 | |||
| 05c608214e | |||
| 37f4a63112 | |||
| fe34eee052 | |||
| cced839da5 | |||
| e77d187497 | |||
| 29dce59b35 | |||
| f582ecf032 | |||
| a431ab47cd | |||
| 11140b9fb6 | |||
| 246fd1d9d6 | |||
| 68ea9bdff3 | |||
| 0b6be155dd | |||
| 8531a9af3e |
@@ -1 +1 @@
|
||||
{ "containers": [], "config": {} }
|
||||
{"containers":[],"config":{}}
|
||||
@@ -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/` 冲突,以代码和最新文档为准,并修正过期工具说明。
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
@@ -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.'
|
||||
@@ -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 应包含 playType、workId、sourceRoute 和 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` 与冷编译超时的修复记录。
|
||||
-34
@@ -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 / Token,Rust 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,16 +6,16 @@ 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; 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` |
|
||||
|
||||
Canvas save uses optimistic revision control. Pass the last authoritative `expectedRevision`; on conflict, reload instead of replaying a stale full layout.
|
||||
|
||||
@@ -28,18 +28,18 @@ Project listing supports two views:
|
||||
|
||||
## 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 +47,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 +72,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 +102,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
Reference in New Issue
Block a user