Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e695cdf499 | |||
| 4ca93a6f16 | |||
| 2f1da79f9c | |||
| c59a912eef | |||
| 3d145379f2 | |||
| ae5c2d8821 | |||
| 8b4ec2c4d4 | |||
| 95e34b8760 | |||
| c51d88fd19 | |||
| 4d1c9d9b1b | |||
| 90cfdfdf97 | |||
| f65a813768 | |||
| b5dc99e067 | |||
| c94bb6d3ed | |||
| 101fd952d5 | |||
| 2c85ea0b83 | |||
| 13afdd7a3b | |||
| 05f0e2db06 | |||
| e368145f9d | |||
| 0123f3db76 | |||
| d6cb68cf13 | |||
| 132bf7db35 | |||
| 1575afc92c |
@@ -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,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,并同步必要的项目文档。
|
||||
@@ -27,7 +27,7 @@ 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.
|
||||
- Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either.
|
||||
@@ -106,21 +106,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`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `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` | `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` |
|
||||
|
||||
Poll all nine through:
|
||||
Poll all eight through:
|
||||
|
||||
```text
|
||||
GET /api/external/v1/generations/{operationId}
|
||||
@@ -73,7 +72,6 @@ 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.
|
||||
|
||||
@@ -81,21 +79,19 @@ Supply the `operationId` returned by submission. Poll no faster than `pollAfterM
|
||||
|
||||
After confirming a local upload, pass its stable `objectKey` into operations that accept object references:
|
||||
|
||||
| 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 | `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` |
|
||||
|
||||
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.
|
||||
|
||||
## Common Values
|
||||
|
||||
Use OpenAPI as the final authority; these common values are a routing aid:
|
||||
|
||||
@@ -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 `referenceId` 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.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Use this reference to build generation payloads, carry canvas/library context, p
|
||||
|
||||
## Asynchronous Submission
|
||||
|
||||
All nine generation POST routes require `Idempotency-Key` and return HTTP `202` with an `ExternalEditorGenerationSubmissionResponse` shaped like:
|
||||
All eight generation POST routes require `Idempotency-Key` and return HTTP `202` with an `ExternalEditorGenerationSubmissionResponse` shaped like:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -71,8 +71,6 @@ status = client.get_generation(operation_id)
|
||||
completed = client.wait_for_generation(operation_id)
|
||||
```
|
||||
|
||||
Background removal uses the same submission and polling state machine. `sourceImageSrc` must be a stable owner-scoped object key, project resource ID, or asset ID; never pass a Data URL, Blob URL, or expiring signed URL. An explicit resource ID or asset ID is resolved before any object-key fallback. If a raw object key has multiple registrations with conflicting authoritative metadata, pass `sourceResourceId` to disambiguate or the server returns `400`. Use `projectId + canvasCompletion` for normal canvas placement. When `canvasCompletion` is absent, `projectId + targetLayerId` replaces an existing resource-backed layer and is rejected before queueing if the target is invalid; for a raw object key, the target resource becomes the durable source binding rechecked by the Worker. If both placement fields are absent, the server does not add the result to the canvas. The completed compact result contains the stable output object key, dimensions, and persisted resource/asset references when requested.
|
||||
|
||||
## Canvas and Asset-Library Completion
|
||||
|
||||
For endpoints that support these fields, include:
|
||||
@@ -100,8 +98,6 @@ A minimal `canvasCompletion` is:
|
||||
|
||||
`dialogId` is optional. The placeholder supplies canvas placement and completion coordinates; it is not a final media pixel-size constraint. For successful pixel-art snapping, the result layer uses the final logical-grid PNG dimensions even when they differ from the placeholder. Do not reconstruct canvas state from completion results. Reload the project and asset library when complete authoritative snapshots are needed.
|
||||
|
||||
Background removal preserves the source image dimensions. For normal canvas placement, the Python helper therefore requires the real `source_width` and `source_height` whenever `canvasSession` is used without an explicit `canvasWidth` plus `canvasHeight`; it never substitutes a square default. Passing `targetLayerId` instead selects in-place replacement, so the helper keeps the session's project/library fields without injecting `canvasCompletion` and rejects callers that explicitly combine both placement modes. The request `assetKind` is optional, static-image only, and must equal the authoritative source type when one exists. An in-place target must resolve to the same authoritative source object; a raw object key is bound to that target resource instead of relying on project-list order.
|
||||
|
||||
Character animation accepts `assetFolderId` and `assetLabel` and persists the final transparent sequence directly. Its completed compact result includes the authoritative `assetKind="character-animation"` resource and asset with `imageSequenceFrames` and `imageSequenceDurationMs`. Use those records directly and never synthesize a duplicate asset from the first frame.
|
||||
|
||||
For the lower-level asset/resource creation endpoints, `generationInputs` is replayable request context rather than a media-runtime container. When `assetKind` is `character-animation`, the server rejects legacy runtime keys including `characterAnimation`, `frames`, `previewVideoPath`, `frameCount`, `fps`, and `durationSeconds`; send the formal sequence through `imageSequenceFrames` and `imageSequenceDurationMs`. Internal processing audit keys such as `screenColorHex`, `mattingProvider`, and `mattingModel` are removed before persistence.
|
||||
|
||||
@@ -561,58 +561,6 @@ class GenarrativeExternalClient:
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def remove_background(
|
||||
self,
|
||||
source_image_src: str,
|
||||
source_width: int | None = None,
|
||||
source_height: int | None = None,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
source_image_src = normalize_optional_text(source_image_src)
|
||||
if not source_image_src:
|
||||
raise GenarrativeApiError(
|
||||
"source_image_src must be an owner-scoped object key, resource ID, or asset ID"
|
||||
)
|
||||
if (source_width is None) != (source_height is None):
|
||||
raise GenarrativeApiError("source_width and source_height must be provided together")
|
||||
if source_width is not None and (
|
||||
source_width <= 0 or source_height is None or source_height <= 0
|
||||
):
|
||||
raise GenarrativeApiError("source_width and source_height must be positive integers")
|
||||
session = fields.get("canvasSession")
|
||||
if session is None:
|
||||
session = fields.get("canvas_session")
|
||||
target_layer_id = normalize_optional_text(fields.get("targetLayerId"))
|
||||
if target_layer_id and fields.get("canvasCompletion") is not None:
|
||||
raise GenarrativeApiError(
|
||||
"targetLayerId and canvasCompletion are mutually exclusive for background removal"
|
||||
)
|
||||
canvas_width = fields.get("canvasWidth")
|
||||
canvas_height = fields.get("canvasHeight")
|
||||
if (canvas_width is None) != (canvas_height is None):
|
||||
raise GenarrativeApiError("canvasWidth and canvasHeight must be provided together")
|
||||
if session is not None and canvas_width is None and not target_layer_id:
|
||||
if source_width is None or source_height is None:
|
||||
raise GenarrativeApiError(
|
||||
"remove_background requires source_width and source_height when canvasSession is used without canvasWidth/canvasHeight"
|
||||
)
|
||||
fields["canvasWidth"] = source_width
|
||||
fields["canvasHeight"] = source_height
|
||||
self._apply_canvas_session_fields(
|
||||
fields,
|
||||
fields.get("assetLabel", "去背景结果"),
|
||||
source_width or 1,
|
||||
source_height or 1,
|
||||
)
|
||||
if target_layer_id:
|
||||
fields.pop("canvasCompletion", None)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/images/background-removals",
|
||||
{"sourceImageSrc": source_image_src, **fields},
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def generate_icon_spritesheet(
|
||||
self,
|
||||
reference_id: str,
|
||||
@@ -739,14 +687,11 @@ def _self_test() -> None:
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
|
||||
b"\x00\x00\x00\x02\x00\x00\x00\x03\x08\x06\x00\x00\x00"
|
||||
)
|
||||
with tempfile.NamedTemporaryFile(suffix="Hero Image.png", delete=False) as fh:
|
||||
with tempfile.NamedTemporaryFile(suffix="Hero Image.png") as fh:
|
||||
fh.write(png)
|
||||
temp_path = fh.name
|
||||
try:
|
||||
assert image_dimensions(temp_path) == (2, 3)
|
||||
assert source_layer_id_from_path(temp_path).startswith("external-reference-")
|
||||
finally:
|
||||
Path(temp_path).unlink(missing_ok=True)
|
||||
fh.flush()
|
||||
assert image_dimensions(fh.name) == (2, 3)
|
||||
assert source_layer_id_from_path(fh.name).startswith("external-reference-")
|
||||
assert unwrap_envelope({"ok": True, "data": {"upload": 1}}) == {"upload": 1}
|
||||
client = GenarrativeExternalClient(api_key="test")
|
||||
session = {"projectId": "proj-demo", "assetFolderId": "editor-asset-folder-demo"}
|
||||
@@ -828,54 +773,6 @@ def _self_test() -> None:
|
||||
assert len(result["asset"]["imageSequenceFrames"]) == 2
|
||||
assert result["asset"]["imageSequenceDurationMs"] == 4000
|
||||
calls.clear()
|
||||
background_result = client.remove_background(
|
||||
"uploads/source.png",
|
||||
720,
|
||||
1280,
|
||||
canvasSession=session,
|
||||
assetLabel="去背景结果",
|
||||
)
|
||||
assert background_result["taskId"] == "task-demo"
|
||||
assert calls[0]["path"] == "/api/external/v1/editor/images/background-removals"
|
||||
assert calls[0]["body"]["sourceImageSrc"] == "uploads/source.png"
|
||||
assert calls[0]["body"]["projectId"] == "proj-demo"
|
||||
assert calls[0]["body"]["assetFolderId"] == "editor-asset-folder-demo"
|
||||
assert calls[0]["body"]["assetLabel"] == "去背景结果"
|
||||
assert calls[0]["body"]["canvasCompletion"]["title"] == "去背景结果"
|
||||
assert calls[0]["body"]["canvasCompletion"]["placeholder"]["width"] == 720
|
||||
assert calls[0]["body"]["canvasCompletion"]["placeholder"]["height"] == 1280
|
||||
calls.clear()
|
||||
client.remove_background(
|
||||
"uploads/source.png",
|
||||
canvasSession=session,
|
||||
targetLayerId="layer-1",
|
||||
assetLabel="原位去背景结果",
|
||||
)
|
||||
assert calls[0]["body"]["projectId"] == "proj-demo"
|
||||
assert calls[0]["body"]["assetFolderId"] == "editor-asset-folder-demo"
|
||||
assert calls[0]["body"]["assetLabel"] == "原位去背景结果"
|
||||
assert calls[0]["body"]["targetLayerId"] == "layer-1"
|
||||
assert "canvasCompletion" not in calls[0]["body"]
|
||||
calls.clear()
|
||||
try:
|
||||
client.remove_background(
|
||||
"uploads/source.png",
|
||||
canvasSession=session,
|
||||
targetLayerId="layer-1",
|
||||
canvasCompletion={"title": "冲突完成指令"},
|
||||
)
|
||||
except GenarrativeApiError as error:
|
||||
assert "targetLayerId and canvasCompletion are mutually exclusive" in str(error)
|
||||
else:
|
||||
raise AssertionError("background removal must reject conflicting canvas placement modes")
|
||||
assert calls == []
|
||||
try:
|
||||
client.remove_background("uploads/source.png", canvasSession=session)
|
||||
except GenarrativeApiError as error:
|
||||
assert "source_width and source_height" in str(error)
|
||||
else:
|
||||
raise AssertionError("canvas background removal must not guess source dimensions")
|
||||
assert calls == []
|
||||
client.generate_icon_spritesheet(
|
||||
"editor-resource-spec",
|
||||
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
|
||||
|
||||
@@ -3,8 +3,8 @@ name: genarrative-play-type-integration
|
||||
description: 在 Genarrative 中新增或补齐一个创作入口/玩法类型时,按入口配置、前端分流、契约、后端接口、工作台、独立生成页、结果页、发布、统一作品详情、正式 runtime、公开 read model、基础统计与作品架/广场的顺序接入。
|
||||
license: MIT
|
||||
metadata:
|
||||
author: Genarrative Team
|
||||
version: '1.0'
|
||||
author: Hermes Agent
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Genarrative 新增玩法创作工具平台 SOP
|
||||
@@ -90,14 +90,13 @@ metadata:
|
||||
12. **旧数据策略**:旧草稿、旧发布配置、旧分享码是迁移、降级展示、重新生成,还是明确不兼容。
|
||||
|
||||
- `AGENTS.md`
|
||||
- `docs/project-memory/shared-memory/`
|
||||
- `.hermes/shared-memory/`
|
||||
- `CONTEXT.md`
|
||||
- `docs/README.md`
|
||||
- `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
|
||||
- 相关玩法 PRD 或设计文档
|
||||
- `.codex/skills/genarrative-play-type-integration/references/genarrative-analytics-tracking-runtime.md`(涉及正式 runtime 埋点时)
|
||||
|
||||
如果文档不能精确指导字段、契约、资产槽位、生成流程和恢复语义,先补文档再编码。新增长期约定时同步 `docs/project-memory/shared-memory/`。
|
||||
如果文档不能精确指导字段、契约、资产槽位、生成流程和恢复语义,先补文档再编码。新增长期约定时同步 `.hermes/shared-memory/`。
|
||||
|
||||
### 2. 定玩法边界
|
||||
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
---
|
||||
name: genarrative-spacetimedb
|
||||
description: Genarrative 的 SpacetimeDB 项目适配规范。用于涉及 SpacetimeDB 架构、Rust module、schema、migration、reducer、procedure、view、绑定生成、CLI、MCP、发布、调试或运行时核验的任务。
|
||||
---
|
||||
|
||||
# Genarrative SpacetimeDB 项目指导
|
||||
|
||||
本 skill 只保存 Genarrative 的项目约束和操作边界;SpacetimeDB 的通用 API、语言 SDK 和 CLI 手册由已安装的官方插件提供。项目规则覆盖插件示例中的默认值或与本仓库冲突的建议。
|
||||
|
||||
## 官方插件依赖
|
||||
|
||||
开始 SpacetimeDB 任务时,按任务范围读取官方插件 skill:
|
||||
|
||||
- `spacetimedb:concepts`:核心语义、表、reducer、procedure、view、订阅和身份。
|
||||
- `spacetimedb:rust-server`:Rust module、表属性、访问器、迁移兼容性和 SDK API。
|
||||
- `spacetimedb:cli`:初始化、构建、发布、生成绑定、SQL、调用、日志和 server 管理。
|
||||
- `spacetimedb:typescript-client`:前端生成绑定、订阅和 TypeScript 客户端 SDK;其它语言客户端按需读取插件对应 skill。
|
||||
- `spacetimedb:mcp`:通过已连接的 MCP 操作运行中的数据库;没有 MCP 工具时使用 CLI 等价命令。
|
||||
|
||||
如果当前环境尚未安装插件,使用:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add clockworklabs/SpacetimeDB --sparse .agents --sparse codex-plugin
|
||||
codex plugin add spacetimedb\@spacetimedb-plugins
|
||||
```
|
||||
|
||||
插件不可用时,以当前源码、`docs/`、生成绑定和仓库脚本为准,不凭记忆发明 SpacetimeDB API。
|
||||
|
||||
## 架构边界
|
||||
|
||||
Genarrative 的唯一有效后端路线是:
|
||||
|
||||
```text
|
||||
server-rs + Axum + SpacetimeDB
|
||||
```
|
||||
|
||||
- `module-*`:领域模型、命令、应用规则、领域事件和领域错误;不得直接依赖 Axum、SpacetimeDB table/reducer/procedure、`spacetime-client`、外部平台或文件系统。
|
||||
- `spacetime-module`:SpacetimeDB 表、reducer、procedure、view、migration、事务 adapter 和 row mapper。
|
||||
- `spacetime-client`:后端访问 SpacetimeDB 的 typed facade;其它后端 crate 不直接创建第二套访问路径。
|
||||
- `api-server`:HTTP、SSE、BFF 和外部副作用编排。
|
||||
- `platform-*`:OSS、LLM、认证、语音等外部平台能力。
|
||||
- `shared-contracts` / `packages/shared`:前后端 DTO、公开契约和无业务真相的共享 TypeScript 代码。
|
||||
- 前端只负责表现、交互、临时 UI 状态和后端结果渲染,不绕过 BFF/投影直接读取私有表或推导正式业务状态。
|
||||
|
||||
SpacetimeDB 是数据和事务层,不替代 `api-server` BFF、`spacetime-client` facade 或公开 read model。插件提供的“SpacetimeDB 可替代传统服务端”通用描述不能改变本项目边界。
|
||||
|
||||
## 语义与安全不变量
|
||||
|
||||
- Reducer 是原子事务写路径,不向调用者返回业务数据;读取通过订阅、read model、view 或 BFF。
|
||||
- Reducer 必须确定性执行:不得访问文件系统、网络、系统时钟或外部随机源;使用 `ctx.timestamp`、`ctx.rng()` / `ctx.random()` 等 SpacetimeDB 能力。
|
||||
- 授权使用上下文中的 `ctx.sender()`(或当前语言对应 API),不信任调用参数传入的身份。
|
||||
- Auto-increment ID 不是排序依据;需要顺序时使用时间戳或显式序列字段。
|
||||
- Private table 是后端事实;用户可见状态通过 BFF、投影或明确的 public table/view 暴露。公共表仍只能由 reducer/procedure 写入。
|
||||
- Procedure 在 2.8 已稳定,可使用显式事务和 `ctx.http`;Genarrative 默认仍把外部 provider 协议放在 `platform-*`,把编排放在 `api-server`,除非当前架构明确要求 module procedure。
|
||||
- Event table 必须显式订阅,按插入事件消费;不要依赖其持久化行或 `OnUpdate`。需要更新回调时使用持久表或带主键的 procedural view。
|
||||
- Standalone MCP 是 operator/developer 集成面,不是 BFF、facade 或公开 read model 的替代品。MCP/SQL/CLI 的写入都必须有明确授权;日常 smoke 优先只读。
|
||||
|
||||
## Schema 与迁移
|
||||
|
||||
修改现有 SpacetimeDB persistent table 时:
|
||||
|
||||
1. 新字段只能追加到 Rust 表结构体末尾,并设置明确的 `#[default(...)]`。
|
||||
2. 删除、改名、重排、改类型或破坏性约束变更前,必须先询问用户并确认迁移计划。
|
||||
3. 同步更新 `server-rs/crates/spacetime-module/src/migration.rs`、后端架构文档中的表目录、生成绑定和相关契约/测试。
|
||||
4. 运行:
|
||||
|
||||
```bash
|
||||
npm run spacetime:generate
|
||||
npm run check:spacetime-schema
|
||||
```
|
||||
|
||||
Event table 的较宽松自动迁移规则不适用于 persistent table,不能借此绕过上述门禁。以当前源码和 `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` 为 schema 真相。
|
||||
|
||||
## CLI、目标 server 与本地开发
|
||||
|
||||
- 优先使用仓库 wrapper:`npm run dev:spacetime`、`npm run dev:api-server`、`npm run spacetime:generate`。
|
||||
- 直接使用 CLI 时始终显式传 `--server` 或 `--server-url`;不要依赖默认云端目标或个人 CLI 默认 server。
|
||||
- 不新增 `maincloud` / `MAINCLOUD` 命令、环境变量、脚本或文档;历史残留只按历史处理。
|
||||
- 人工命令、本地联调、排障步骤和文档示例禁止使用 `spacetime --root-dir`;本地数据隔离使用项目脚本或 `--data-dir`。
|
||||
- `spacetime publish` 的 `--delete-data=always` 只在明确授权的破坏性操作中使用;schema 冲突优先按项目脚本和受控迁移流程处理。
|
||||
- 项目 SpacetimeDB crate、SDK、CLI/standalone 和生成 bindings 按 `2.8.3` 对齐;官方发行资产、Rust crates 和容器镜像使用 `v2.8.3` 版本标签,仓库额外固定 CLI commit `8e410d2842147bd8e5a32a9589cc00c19f7478e2`。升级时核对 Cargo 精确 pin、实际 CLI 和运行中服务二进制,不把本地 CLI 重装当作仓库升级。
|
||||
|
||||
本地开发默认由项目启动器管理端口;实际监听地址以 `.app/dev-stack.json` 和启动日志为准,不能从文档默认端口推断当前目标。发布后确认 api-server 使用的是同一 database、server 和 token。
|
||||
|
||||
## MCP 与运行时核验
|
||||
|
||||
如果当前会话暴露 SpacetimeDB MCP 工具,读取运行中的数据库优先使用 typed MCP:先 `list_databases` / `get_schema`,再做只读 SQL 或 `ping`;调用 reducer 或 SQL 写入前确认目标、身份和授权。没有 MCP 工具时使用显式目标的 CLI。2.8 standalone 的 MCP HTTP endpoint 是 `POST /v1/database/{name_or_identity}/mcp`,提供 `ping`、`get_schema`、`sql`、`call`;升级 smoke 在隔离数据库中只做 `initialize`、`tools/list`、`ping`、`get_schema`,除非写入明确属于任务范围。
|
||||
|
||||
排查“服务健康但业务不可用”时按顺序核对:
|
||||
|
||||
1. SpacetimeDB standalone 是否运行(本地优先 `npm run dev:spacetime`,主机侧核对 systemd)。
|
||||
2. module 是否发布到 api-server 实际使用的同一个 server/database。
|
||||
3. 生成绑定是否来自当前 module。
|
||||
4. api-server 的 database、server URL 和 token 是否一致。
|
||||
5. reducer/procedure 是否真正被调用;区分超时、权限、schema 不存在和业务错误。
|
||||
6. `/healthz` / `/readyz` 通过但业务仍失败时,继续检查 API 日志和公开路由,不把健康检查当作业务成功证明。
|
||||
|
||||
主机升级需核对运行中进程而非只看 PATH:
|
||||
|
||||
```bash
|
||||
type -a spacetime
|
||||
spacetime --version
|
||||
pid="$(systemctl show spacetimedb.service -p MainPID --value)"
|
||||
readlink -f "/proc/${pid}/exe"
|
||||
"/proc/${pid}/exe" --version
|
||||
curl -fsS http://127.0.0.1:3101/v1/ping
|
||||
```
|
||||
|
||||
## 修改后的最小验证
|
||||
|
||||
按范围执行定向测试/类型检查,并至少运行:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
涉及 schema 时追加 `npm run spacetime:generate` 和 `npm run check:spacetime-schema`;涉及 API 时按当前后端文档启动 `npm run dev:api-server` 并检查 `/healthz`。无法运行的验证要在交付说明中标记为未验证并说明原因。
|
||||
|
||||
## 参考入口
|
||||
|
||||
- `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
|
||||
- `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
|
||||
- `server-rs/README.md`
|
||||
- `scripts/check-spacetime-schema-guard.mjs`
|
||||
- `scripts/check-server-rs-ddd-boundaries.mjs`
|
||||
@@ -1,7 +1,7 @@
|
||||
interface:
|
||||
display_name: 'GPT Image 2 VectorEngine'
|
||||
short_description: 'Generate project thumbnails through VectorEngine'
|
||||
brand_color: '#10B981'
|
||||
default_prompt: 'Use $gpt-image-2-apimart to dry-run or generate puzzle template thumbnails through VectorEngine.'
|
||||
display_name: "GPT Image 2 VectorEngine"
|
||||
short_description: "Generate project thumbnails through VectorEngine"
|
||||
brand_color: "#10B981"
|
||||
default_prompt: "Use $gpt-image-2-apimart to dry-run or generate puzzle template thumbnails through VectorEngine."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
|
||||
@@ -257,8 +257,7 @@ async function fetchJson(url, options, timeoutMs) {
|
||||
}
|
||||
|
||||
function shouldFallbackImageModel(error) {
|
||||
const raw =
|
||||
`${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
|
||||
const raw = `${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
|
||||
if (error?.vectorEngineResponseParse) {
|
||||
return !containsContentRejection(raw);
|
||||
}
|
||||
@@ -311,10 +310,11 @@ async function requestImagePayload(env, entry) {
|
||||
},
|
||||
env.timeoutMs,
|
||||
);
|
||||
const base64Image = decodeStrictBase64Image(
|
||||
extractBase64Images(payload)[0],
|
||||
);
|
||||
if (extractImageUrls(payload)[0] || base64Image) {
|
||||
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
|
||||
if (
|
||||
extractImageUrls(payload)[0] ||
|
||||
base64Image
|
||||
) {
|
||||
return payload;
|
||||
}
|
||||
const error = new Error(`VectorEngine returned no image for ${entry.id}`);
|
||||
@@ -350,9 +350,7 @@ async function downloadUrl(url, timeoutMs) {
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(
|
||||
`Generated image download timed out after ${timeoutMs}ms`,
|
||||
);
|
||||
throw new Error(`Generated image download timed out after ${timeoutMs}ms`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -372,9 +370,7 @@ async function generateOne(env, entry, outDir) {
|
||||
} else if (b64Images[0]) {
|
||||
const bytes = decodeStrictBase64Image(b64Images[0]);
|
||||
if (!bytes) {
|
||||
throw new Error(
|
||||
`VectorEngine returned invalid base64 image for ${entry.id}`,
|
||||
);
|
||||
throw new Error(`VectorEngine returned invalid base64 image for ${entry.id}`);
|
||||
}
|
||||
image = {
|
||||
bytes,
|
||||
|
||||
@@ -12,11 +12,7 @@ const promptsPath = path.join(
|
||||
'assets',
|
||||
'puzzle-template-prompts.json',
|
||||
);
|
||||
const defaultOutDir = path.join(
|
||||
repoRoot,
|
||||
'public',
|
||||
'puzzle-creation-templates',
|
||||
);
|
||||
const defaultOutDir = path.join(repoRoot, 'public', 'puzzle-creation-templates');
|
||||
const defaultTimeoutMs = 1000000;
|
||||
const preferredImageModel = 'gpt-image-2';
|
||||
const fallbackImageModel = 'gpt-image-2-c';
|
||||
@@ -227,8 +223,7 @@ async function fetchJson(url, options, timeoutMs) {
|
||||
}
|
||||
|
||||
function shouldFallbackImageModel(error) {
|
||||
const raw =
|
||||
`${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
|
||||
const raw = `${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
|
||||
if (error?.vectorEngineResponseParse) {
|
||||
return !containsContentRejection(raw);
|
||||
}
|
||||
@@ -281,15 +276,14 @@ async function requestImagePayload(env, template) {
|
||||
},
|
||||
env.timeoutMs,
|
||||
);
|
||||
const base64Image = decodeStrictBase64Image(
|
||||
extractBase64Images(payload)[0],
|
||||
);
|
||||
if (extractImageUrls(payload)[0] || base64Image) {
|
||||
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
|
||||
if (
|
||||
extractImageUrls(payload)[0] ||
|
||||
base64Image
|
||||
) {
|
||||
return payload;
|
||||
}
|
||||
const error = new Error(
|
||||
`VectorEngine returned no image for ${template.id}`,
|
||||
);
|
||||
const error = new Error(`VectorEngine returned no image for ${template.id}`);
|
||||
error.vectorEngineResponseParse = true;
|
||||
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
|
||||
throw error;
|
||||
@@ -322,9 +316,7 @@ async function downloadUrl(url, timeoutMs) {
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(
|
||||
`Generated image download timed out after ${timeoutMs}ms`,
|
||||
);
|
||||
throw new Error(`Generated image download timed out after ${timeoutMs}ms`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -344,9 +336,7 @@ async function generateOne(env, template, outDir) {
|
||||
} else if (b64Images[0]) {
|
||||
const bytes = decodeStrictBase64Image(b64Images[0]);
|
||||
if (!bytes) {
|
||||
throw new Error(
|
||||
`VectorEngine returned invalid base64 image for ${template.id}`,
|
||||
);
|
||||
throw new Error(`VectorEngine returned invalid base64 image for ${template.id}`);
|
||||
}
|
||||
image = {
|
||||
bytes,
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
---
|
||||
name: spacetimedb-cli
|
||||
description: SpacetimeDB 2.7 CLI reference for Genarrative. Use for spacetime build, publish, generate, call, sql, logs, server management, local dev, explicit server targeting, version checks, and remote runtime verification.
|
||||
---
|
||||
|
||||
# SpacetimeDB CLI
|
||||
|
||||
Use this skill when working with the `spacetime` CLI in Genarrative. Prefer repository scripts when they exist, and keep every operation pinned to an explicit target server or local process.
|
||||
|
||||
## Genarrative Rules
|
||||
|
||||
- Do not rely on the default SpacetimeDB cloud target. Pass `--server` or `--server-url` explicitly in scripts, docs, smoke tests, and manual troubleshooting.
|
||||
- Do not introduce `maincloud` / `MAINCLOUD` commands, env vars, or docs. Treat old references as historical residue.
|
||||
- Do not use `spacetime --root-dir` in manual commands or docs. Use project scripts, `--data-dir`, explicit `--server`, or the configured running service.
|
||||
- For repository version upgrades, update `server-rs/Cargo.toml` exact pins, regenerate bindings, and verify the actual CLI/runtime version. Do not treat a local CLI reinstall as a repo upgrade.
|
||||
- For host upgrades, verify the running service binary, not just shell PATH: `systemctl show ... MainPID` -> `/proc/$pid/exe --version` -> `/v1/ping`.
|
||||
|
||||
## Core Commands
|
||||
|
||||
```bash
|
||||
# Build module
|
||||
spacetime build
|
||||
spacetime build --debug
|
||||
|
||||
# Publish to an explicit server
|
||||
spacetime publish my-database --server http://127.0.0.1:3101 --yes=migrate,break-clients
|
||||
|
||||
# Destructive publish only when explicitly intended
|
||||
spacetime publish my-database --server http://127.0.0.1:3101 --delete-data=always --yes=delete-data,migrate
|
||||
|
||||
# Delete data only for breaking schema conflicts
|
||||
spacetime publish my-database --server http://127.0.0.1:3101 --delete-data=on-conflict --yes=migrate
|
||||
|
||||
# Generate bindings
|
||||
spacetime generate --lang typescript|csharp|rust|unrealcpp --out-dir ./bindings --module-path ./server
|
||||
```
|
||||
|
||||
## Genarrative Local Workflow
|
||||
|
||||
```bash
|
||||
# Prefer project wrappers
|
||||
npm run dev:spacetime
|
||||
npm run dev:api-server
|
||||
npm run spacetime:generate
|
||||
|
||||
# Query local database
|
||||
spacetime sql my-db --server http://127.0.0.1:3101 "SELECT * FROM players"
|
||||
|
||||
# Logs
|
||||
spacetime logs my-db --server http://127.0.0.1:3101 -f
|
||||
```
|
||||
|
||||
## Database Interaction
|
||||
|
||||
```bash
|
||||
# SQL / describe
|
||||
spacetime sql my-db --server http://127.0.0.1:3101 "SELECT * FROM users"
|
||||
spacetime describe my-db --server http://127.0.0.1:3101 --json
|
||||
spacetime describe my-db table users --server http://127.0.0.1:3101 --json
|
||||
|
||||
# Reducer/procedure calls. Arguments are positional JSON values.
|
||||
spacetime call --server http://127.0.0.1:3101 my-db my_reducer '"value"' '123'
|
||||
|
||||
# 2.5+ accepts hex strings for Identity arguments without full JSON tuple syntax.
|
||||
spacetime call --server http://127.0.0.1:3101 my-db reducer_needing_identity 0xabc123...
|
||||
|
||||
# Subscribe from CLI
|
||||
spacetime subscribe my-db "SELECT * FROM users" --num-updates 10 --server http://127.0.0.1:3101
|
||||
```
|
||||
|
||||
## Standalone MCP Endpoint (2.7)
|
||||
|
||||
SpacetimeDB 2.7 standalone exposes an authenticated JSON-RPC MCP endpoint at
|
||||
`POST /v1/database/{name_or_identity}/mcp`. It advertises `ping`, `get_schema`,
|
||||
`sql`, and `call`. The SQL and reducer tools execute with the bearer token's
|
||||
identity, so keep routine smoke checks read-only.
|
||||
|
||||
```bash
|
||||
curl -fsS \
|
||||
-H "Authorization: Bearer ${SPACETIME_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"genarrative-smoke","version":"1.0.0"}}}' \
|
||||
http://127.0.0.1:3101/v1/database/my-db/mcp
|
||||
|
||||
curl -fsS \
|
||||
-H "Authorization: Bearer ${SPACETIME_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ping","arguments":{"message":"genarrative"}}}' \
|
||||
http://127.0.0.1:3101/v1/database/my-db/mcp
|
||||
```
|
||||
|
||||
For repository upgrade validation, also call `tools/list` and the read-only
|
||||
`get_schema` tool against an isolated local database. Do not use `sql` or `call`
|
||||
for writes unless that mutation is explicitly in scope.
|
||||
|
||||
## Server & Auth
|
||||
|
||||
```bash
|
||||
spacetime server list
|
||||
spacetime server add local --url http://localhost:3000 --default
|
||||
spacetime server add genarrative-dev --url http://127.0.0.1:3101
|
||||
spacetime server ping genarrative-dev
|
||||
|
||||
spacetime login
|
||||
spacetime login --token <token>
|
||||
spacetime login show
|
||||
spacetime logout
|
||||
```
|
||||
|
||||
## Version & Runtime Verification
|
||||
|
||||
```bash
|
||||
# CLI resolution can be misleading; compare all candidates when diagnosing.
|
||||
type -a spacetime
|
||||
spacetime --version
|
||||
spacetime version list
|
||||
|
||||
# Verify a systemd service binary actually changed.
|
||||
pid="$(systemctl show spacetimedb.service -p MainPID --value)"
|
||||
readlink -f "/proc/${pid}/exe"
|
||||
"/proc/${pid}/exe" --version
|
||||
curl -fsS http://127.0.0.1:3101/v1/ping
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--server`, `-s` | Target server nickname, host, or URL |
|
||||
| `--yes`, `-y` | Non-interactive prompt skipping; in 2.6+ use scoped values |
|
||||
| `--delete-data`, `-c` | Publish data policy: `always`, `on-conflict`, or `never` |
|
||||
| `--module-path`, `-p` | Module project path |
|
||||
| `--bin-path`, `-b` | Publish/generate from compiled wasm |
|
||||
| `--no-config` | Ignore `spacetime.json` |
|
||||
| `--env` | Select config file layering environment |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Not Logged In
|
||||
|
||||
```bash
|
||||
spacetime login
|
||||
```
|
||||
|
||||
### Server Not Responding
|
||||
|
||||
```bash
|
||||
spacetime server ping <server>
|
||||
curl -fsS http://127.0.0.1:3101/v1/ping
|
||||
```
|
||||
|
||||
For local Genarrative work, start SpacetimeDB first with `npm run dev:spacetime`, then start `npm run dev:api-server`.
|
||||
|
||||
### Schema Conflict
|
||||
|
||||
```bash
|
||||
spacetime publish my-db --server http://127.0.0.1:3101 --delete-data=on-conflict --yes=migrate
|
||||
```
|
||||
|
||||
Use `--delete-data=always` only with explicit approval.
|
||||
|
||||
### Version Mismatch
|
||||
|
||||
```bash
|
||||
rg -n 'spacetimedb' server-rs/Cargo.toml
|
||||
spacetime --version
|
||||
spacetime version list
|
||||
pid="$(systemctl show spacetimedb.service -p MainPID --value)"
|
||||
"/proc/${pid}/exe" --version
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Procedure calls remain stable in 2.7; module HTTP handlers/webhooks and RLS capabilities still require their documented gates.
|
||||
- 2.5 fixed `publish --delete-data` config fallback; 2.6 kept that behavior and improved CLI binary distribution; 2.7 adds `spacetime sql --format json` and database `lock` / `unlock`.
|
||||
- The official 2.7.0 Linux release archives and container image currently use the `v2.7.0-hotfix3` asset tag while binaries report `2.7.0`; keep the asset tag distinct from the runtime version check.
|
||||
- Do not assume `spacetime version install 2.7.0` selected hotfix3: stale updater metadata can install bare-tag commit `a08663c7...`. For the current release, verify CLI commit `d220349a...` and use the official hotfix3 archive or repository provision flow when it differs.
|
||||
- Genarrative scripts should pass `--server` or `--server-url` explicitly instead of relying on CLI defaults.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user