Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 417f922303 | |||
| ecac0dc3fc |
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm test:*)",
|
||||
"Bash(netstat -ano)",
|
||||
"Bash(npm run:*)",
|
||||
"Bash(findstr :8081)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(findstr LISTENING)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(lsof -ti:8081)",
|
||||
"Bash(curl -s http://localhost:8081/health)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -14,40 +14,20 @@ if (hookInput && !isGitCommitCommand(extractShellCommand(hookInput))) {
|
||||
}
|
||||
|
||||
const validationSteps = [
|
||||
{
|
||||
label: 'Rust format check',
|
||||
command: npmCommand,
|
||||
args:
|
||||
process.platform === 'win32'
|
||||
? ['/d', '/s', '/c', 'npm run check:rustfmt']
|
||||
: ['run', 'check:rustfmt'],
|
||||
},
|
||||
{
|
||||
label: 'TypeScript typecheck',
|
||||
command: npmCommand,
|
||||
args:
|
||||
process.platform === 'win32'
|
||||
? ['/d', '/s', '/c', 'npm run typecheck']
|
||||
: ['run', 'typecheck'],
|
||||
args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run typecheck'] : ['run', 'typecheck'],
|
||||
},
|
||||
{
|
||||
label: 'Admin web typecheck',
|
||||
command: npmCommand,
|
||||
args:
|
||||
process.platform === 'win32'
|
||||
? ['/d', '/s', '/c', 'npm run admin-web:typecheck']
|
||||
: ['run', 'admin-web:typecheck'],
|
||||
args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run admin-web:typecheck'] : ['run', 'admin-web:typecheck'],
|
||||
},
|
||||
{
|
||||
label: 'Rust api-server compile check',
|
||||
command: 'cargo',
|
||||
args: [
|
||||
'check',
|
||||
'-p',
|
||||
'api-server',
|
||||
'--manifest-path',
|
||||
'server-rs/Cargo.toml',
|
||||
],
|
||||
args: ['check', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -86,9 +66,7 @@ function runStep(step) {
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
console.error(
|
||||
`[codex-hook] ${step.label} 启动失败:${result.error.message}`,
|
||||
);
|
||||
console.error(`[codex-hook] ${step.label} 启动失败:${result.error.message}`);
|
||||
return { ok: false, status: 1 };
|
||||
}
|
||||
|
||||
@@ -126,15 +104,12 @@ function extractShellCommand(input) {
|
||||
input?.command,
|
||||
];
|
||||
|
||||
const command = candidates.find(
|
||||
(value) => typeof value === 'string' && value.trim().length > 0,
|
||||
);
|
||||
const command = candidates.find(value => typeof value === 'string' && value.trim().length > 0);
|
||||
if (command) {
|
||||
return command;
|
||||
}
|
||||
|
||||
const shellCommand =
|
||||
input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd;
|
||||
const shellCommand = input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd;
|
||||
if (Array.isArray(shellCommand)) {
|
||||
return shellCommand.join(' ');
|
||||
}
|
||||
|
||||
@@ -1,70 +1,93 @@
|
||||
---
|
||||
name: genarrative-external-editor-api
|
||||
description: Guide use of Genarrative's hosted external editor/canvas MCP or asynchronous `/api/external/v1` OpenAPI. Use when an Agent needs to discover the hosted integration, choose a canvas or asset operation, upload local reference media, create or update projects and asset-library records, submit and poll image/video/audio generation, interpret generated artifacts and warnings, draft HTTP/Python calls, or securely handle a Genarrative developer API Key.
|
||||
description: Guide use of Genarrative's external editor/canvas OpenAPI. Use when a user describes a canvas/editor integration need and Codex must infer the right `/api/external/v1` API automatically, prepare canvas and asset-library context, abstract reusable art specs before generating assets, upload references, draft curl/HTTP/SDK requests, or set up and safely handle a Genarrative developer API Key.
|
||||
---
|
||||
|
||||
# Genarrative External Editor API
|
||||
|
||||
Discover the live integration through `GET https://www.genarrative.world/api/external/v1/agent-integration.json`. Treat `GET https://www.genarrative.world/api/external/v1/openapi.json` as the field-level source of truth. In this repository, the same contract is `docs/openapi/genarrative-external-v1.openapi.json`.
|
||||
Use the live OpenAPI contract as the source of truth: `GET https://www.genarrative.world/api/external/v1/openapi.json`. In this repository, the same contract is `docs/openapi/genarrative-external-v1.openapi.json`. If exact fields or enums matter, read the contract before emitting final code.
|
||||
|
||||
Prefer the hosted Streamable HTTP MCP at `https://www.genarrative.world/api/external/v1/mcp` when the Agent supports remote MCP with a custom Bearer token. It exposes the External v1 operations as tools and the Skill documentation as resources; it does not require a local MCP server. Use this complete Skill package when remote MCP is unavailable or local-file upload needs client-side orchestration.
|
||||
|
||||
Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses only Python stdlib, reads the local private API Key file, keeps the production base URL fixed, uploads local references, and wraps asynchronous submission, polling, and result retrieval.
|
||||
Prefer the bundled Python helper for runnable examples: `scripts/genarrative_external_api.py`. It uses only Python stdlib, reads the local JSON API Key file, fixes the production base URL, and wraps upload/confirm/generation routes.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Discover the integration manifest. Choose hosted MCP when supported; otherwise use the helper or direct REST.
|
||||
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`.
|
||||
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.
|
||||
9. Reload the normal project or asset-library read endpoint when the caller needs complete authoritative state. Generation results are intentionally compact.
|
||||
10. Stay within `/api/external/v1`. Never call internal workers, queues, admin/profile APIs, or SpacetimeDB endpoints unless the user explicitly changes scope.
|
||||
1. At the start of a new conversation, ask the user for the canvas name before the first generation call unless an existing session is already provided. Create or use a project with that name and an asset-library folder with the same name. Keep `canvasName`, `projectId`, `assetFolderId`, and the current art spec in conversation state.
|
||||
2. Before any art asset generation, abstract the user's request into a reusable art spec. Ask only for missing spec fields required by the selected asset type. If a current spec already exists and the user does not request a new style/spec, reuse it automatically.
|
||||
3. Classify the user's natural-language intent. Do not ask the user to choose an API:
|
||||
- "生成/生图/做一张图" -> image generation
|
||||
- "重绘/修改这张图" -> image edit
|
||||
- "用这张参考图/基于本地图生成" -> upload local reference image, then generation or edit
|
||||
- "上传本地素材" -> upload ticket, OSS form upload, object confirm
|
||||
- "保存画板/更新布局" -> canvas save
|
||||
- "读取私有素材" -> signed read URL
|
||||
4. Ask only for missing inputs that affect the request body or an actually ambiguous route:
|
||||
- credentials JSON path only if the user cannot use the default local path
|
||||
- canvas name when no current canvas session exists; existing `projectId`, folder/resource IDs only when resuming a known project
|
||||
- media type, prompt, references, dimensions, model, ratio, duration, and resolution
|
||||
- whether referenced media is already uploaded as `objectKey` or still local
|
||||
5. Every external generation must write to both the canvas and the asset library. Include `projectId`, `assetFolderId`, a display label, and `canvasCompletion` whenever the target endpoint supports them. For character animation, use the helper's two-step fallback: generate with `projectId` + `canvasCompletion`, then create a library asset from the first returned frame in the session folder.
|
||||
6. If the user lacks an API Key, guide setup before request design.
|
||||
7. Read `references/api-selection.md` before finalizing any request. Use the core table below for fast routing, then verify details in the reference.
|
||||
8. Use `scripts/genarrative_external_api.py` when the user wants runnable Python, reference image upload, canvas/folder session setup, art-spec carrying, or a chain that should execute with fewer hand-written curl steps.
|
||||
9. Keep to `/api/external/v1` unless the user explicitly asks for internal profile/admin APIs.
|
||||
|
||||
## Essential Invariants
|
||||
## Core Routes
|
||||
|
||||
- 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 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 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.
|
||||
- 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.
|
||||
| Intent | Method and path | Required fields |
|
||||
| --- | --- | --- |
|
||||
| List/create projects | `GET/POST /api/external/v1/editor/projects` | create: optional `title` |
|
||||
| Save canvas | `PATCH /api/external/v1/editor/projects/{projectId}/canvas` | `viewport`, `layers` |
|
||||
| Upload local media | `POST /api/external/v1/assets/direct-upload-tickets` -> OSS form -> `POST /api/external/v1/assets/objects/confirm` | ticket: `legacyPrefix`, `fileName`; confirm: `objectKey`, `assetKind` |
|
||||
| Read private media | `GET /api/external/v1/assets/read-url` | `objectKey` or `legacyPublicPath` |
|
||||
| Image generation | `POST /api/external/v1/editor/images/generations` | `prompt` |
|
||||
| Image edit/redraw | `POST /api/external/v1/editor/images/edits` | `prompt`, `sourceImageSrc` |
|
||||
| Icon spritesheet | `POST /api/external/v1/editor/icon-spritesheets/generations` | `referenceImageSrc`, `iconDescriptions` |
|
||||
| UI asset extraction | `POST /api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize`; use `assetFolderId` for library folder |
|
||||
| Character animation | `POST /api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` |
|
||||
| Video generation | `POST /api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` |
|
||||
| Sound effect | `POST /api/external/v1/editor/audios/sound-effects/generations` | `prompt`, `duration` |
|
||||
| Background music | `POST /api/external/v1/editor/audios/background-music/generations` | `gptDescriptionPrompt`, `makeInstrumental` |
|
||||
|
||||
## Documentation Navigation
|
||||
## Art Spec Interface
|
||||
|
||||
Read only the references needed for the task, but always verify exact schemas and enums against live OpenAPI:
|
||||
Maintain one current art spec per conversation. A compact spec is enough:
|
||||
|
||||
- `references/capability-routing.md`: read before selecting an MCP tool or REST operation, creating a canvas session, or working in the AI game creator visual DAG.
|
||||
- `references/api-operations.md`: read when constructing project, canvas, asset-library, upload, generation, or generation-status calls.
|
||||
- `references/authentication-and-safety.md`: read before handling credentials, local files, OSS form upload, retries, private media, or logs.
|
||||
- `references/requests-and-outputs.md`: read before building generation payloads, polling, interpreting compact results, applying canvas completion, or handling post-processing warnings.
|
||||
```json
|
||||
{
|
||||
"assetType": "character | background | prop | ui | icon | animation | video | audio",
|
||||
"subject": "要生成的主体",
|
||||
"style": "画风/材质/时代/参考风格",
|
||||
"palette": "主色与禁用色",
|
||||
"composition": "构图、镜头、姿态或布局",
|
||||
"format": "比例、尺寸、分辨率、帧数、时长",
|
||||
"constraints": "必须保留/禁止出现/透明或绿幕要求",
|
||||
"references": ["objectKey 或本地路径说明"]
|
||||
}
|
||||
```
|
||||
|
||||
The hosted MCP exposes the same documents through:
|
||||
For a first spec, infer fields from the user's words and ask only for missing fields that block the selected API. Examples: character animation needs source image/layer, dimensions, motion, ratio, frame count, and duration; UI extraction needs source design image plus target density; icon spritesheet needs reference image and icon descriptions. After a spec exists, reuse it for later assets unless the user changes style, subject family, palette, format, or constraints.
|
||||
|
||||
- `genarrative://external-editor/skill`
|
||||
- `genarrative://external-editor/skill/references/capability-routing.md`
|
||||
- `genarrative://external-editor/skill/references/api-operations.md`
|
||||
- `genarrative://external-editor/skill/references/authentication-and-safety.md`
|
||||
- `genarrative://external-editor/skill/references/requests-and-outputs.md`
|
||||
- `genarrative://external-editor/openapi`
|
||||
## API Key
|
||||
|
||||
## Hosted Integration Discovery
|
||||
The external OpenAPI uses:
|
||||
|
||||
- Manifest: `GET /api/external/v1/agent-integration.json`.
|
||||
- Hosted MCP: `POST /api/external/v1/mcp`, Streamable HTTP, same Bearer API Key.
|
||||
- OpenAPI: `GET /api/external/v1/openapi.json`.
|
||||
- Raw Skill entry: `GET /api/external/v1/skill/SKILL.md`.
|
||||
- Complete Skill archive: `GET /api/external/v1/skill.zip`.
|
||||
```text
|
||||
Authorization: Bearer <tnr_sk_...>
|
||||
```
|
||||
|
||||
The archive contains this main file, four one-level references, the Python helper, and `agents/openai.yaml`. Verify its SHA-256 against `agent-integration.json` before installing. Discovery, OpenAPI, and Skill downloads are public; MCP and business operations require authentication.
|
||||
The OpenAPI JSON endpoint is public; every other external endpoint requires the Bearer API Key.
|
||||
|
||||
## Python Helper
|
||||
Use this fixed production base URL:
|
||||
|
||||
Store the API Key outside the repository at `~/.config/genarrative/external-editor-api.json`:
|
||||
```text
|
||||
https://www.genarrative.world/
|
||||
```
|
||||
|
||||
Guide the user to create a key from the logged-in product UI under `开发者 API Key`. The raw key is shown only once; never ask the user to paste it into chat. Tell them to store it in this local private JSON file, outside the repository:
|
||||
|
||||
```text
|
||||
~/.config/genarrative/external-editor-api.json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -72,47 +95,238 @@ Store the API Key outside the repository at `~/.config/genarrative/external-edit
|
||||
}
|
||||
```
|
||||
|
||||
Set restrictive permissions where possible, then smoke-test without printing the key:
|
||||
Set the file readable only by the current user where possible: `chmod 600 ~/.config/genarrative/external-editor-api.json`. Do not use environment variables for this API.
|
||||
|
||||
Smoke test by reading the JSON file, without printing the key:
|
||||
|
||||
```bash
|
||||
api_key="$(node -e 'const fs=require("fs"); const p=process.argv[1]; const c=JSON.parse(fs.readFileSync(p,"utf8")); process.stdout.write(c.apiKey || "");' "$HOME/.config/genarrative/external-editor-api.json")"
|
||||
curl -fsS "https://www.genarrative.world/api/external/v1/editor/projects" \
|
||||
-H "Authorization: Bearer $api_key"
|
||||
```
|
||||
|
||||
For generated client code, read `apiKey` from the JSON file, fail with a clear missing-config error, and redact keys in logs.
|
||||
|
||||
Python smoke without printing the key:
|
||||
|
||||
```bash
|
||||
chmod 600 ~/.config/genarrative/external-editor-api.json
|
||||
python3 .codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py list-projects
|
||||
```
|
||||
|
||||
For a canvas-backed generation:
|
||||
## Request Patterns
|
||||
|
||||
For Python callers, prefer:
|
||||
|
||||
```python
|
||||
from genarrative_external_api import GenarrativeExternalClient
|
||||
|
||||
client = GenarrativeExternalClient()
|
||||
session = client.prepare_canvas_session("新画板")
|
||||
art_spec = {
|
||||
"assetType": "background",
|
||||
"subject": "幻想森林主视觉",
|
||||
"style": "手绘游戏概念图",
|
||||
"palette": "翡翠绿、金色光斑,避免低饱和灰",
|
||||
"composition": "16:9 横版,中心留出角色站位",
|
||||
"format": "16:9, 1K",
|
||||
"constraints": "无文字、无 UI 按钮",
|
||||
"references": [],
|
||||
}
|
||||
client.generate_image(
|
||||
"生成一张 16:9 幻想森林游戏背景",
|
||||
"生成幻想森林背景",
|
||||
canvasSession=session,
|
||||
assetLabel="森林背景",
|
||||
aspectRatio="16:9",
|
||||
imageSize="1K",
|
||||
artSpec={
|
||||
"assetType": "background",
|
||||
"subject": "幻想森林主视觉",
|
||||
"style": "手绘游戏概念图",
|
||||
"palette": "翡翠绿与金色光斑",
|
||||
"composition": "横版,中心留出角色站位",
|
||||
"format": "16:9, 1K",
|
||||
"constraints": "无文字、无 UI 按钮",
|
||||
"references": [],
|
||||
},
|
||||
artSpec=art_spec,
|
||||
)
|
||||
```
|
||||
|
||||
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`.
|
||||
Use the helper directly from this skill path, or copy it into the caller's project. Do not change the fixed base URL or move the API Key into environment variables.
|
||||
|
||||
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.
|
||||
Use this shared base:
|
||||
|
||||
```bash
|
||||
api="https://www.genarrative.world"
|
||||
credentials_file="$HOME/.config/genarrative/external-editor-api.json"
|
||||
api_key="$(node -e 'const fs=require("fs"); const p=process.argv[1]; const c=JSON.parse(fs.readFileSync(p,"utf8")); process.stdout.write(c.apiKey || "");' "$credentials_file")"
|
||||
auth=(-H "Authorization: Bearer $api_key")
|
||||
json=(-H "Content-Type: application/json")
|
||||
```
|
||||
|
||||
Create a project:
|
||||
|
||||
```bash
|
||||
curl -fsS "$api/api/external/v1/editor/projects" \
|
||||
"${auth[@]}" "${json[@]}" \
|
||||
-d '{"title":"新画板"}'
|
||||
```
|
||||
|
||||
Generate an image and save it into both the canvas and the asset-library folder:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "一张横版幻想森林背景,适合游戏主视觉",
|
||||
"kind": "spec",
|
||||
"aspectRatio": "16:9",
|
||||
"imageSize": "1K",
|
||||
"projectId": "<projectId>",
|
||||
"assetFolderId": "<assetFolderId>",
|
||||
"assetLabel": "森林背景",
|
||||
"generationInputs": {
|
||||
"artSpec": {
|
||||
"assetType": "background",
|
||||
"style": "手绘游戏概念图"
|
||||
}
|
||||
},
|
||||
"canvasCompletion": {
|
||||
"title": "森林背景",
|
||||
"placeholder": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 1024,
|
||||
"height": 576,
|
||||
"originalWidth": 1024,
|
||||
"originalHeight": 576
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then call `POST /api/external/v1/editor/images/generations`.
|
||||
|
||||
For direct HTTP/curl, create or find the folder first with `GET /api/external/v1/editor/assets/library` and `POST /api/external/v1/editor/assets/folders`. The folder label should match the canvas name.
|
||||
|
||||
## Reference Images
|
||||
|
||||
When the user provides a local reference image path/file, upload it first; do not ask the user to convert it to base64.
|
||||
|
||||
Python helper path:
|
||||
|
||||
```python
|
||||
from genarrative_external_api import GenarrativeExternalClient
|
||||
|
||||
client = GenarrativeExternalClient()
|
||||
session = client.prepare_canvas_session("新画板")
|
||||
ref = client.upload_reference_image("/path/to/reference.png")
|
||||
client.generate_image(
|
||||
"基于参考图生成一张 16:9 游戏背景",
|
||||
canvasSession=session,
|
||||
assetLabel="参考图背景",
|
||||
aspectRatio="16:9",
|
||||
imageSize="1K",
|
||||
referenceImageSrcs=[ref["objectKey"]],
|
||||
)
|
||||
```
|
||||
|
||||
Use the normal upload flow with:
|
||||
|
||||
```json
|
||||
{
|
||||
"legacyPrefix": "generated-character-drafts",
|
||||
"pathSegments": ["editor", "external-editor-references"],
|
||||
"fileName": "<original-file-name>",
|
||||
"contentType": "image/png",
|
||||
"access": "private"
|
||||
}
|
||||
```
|
||||
|
||||
After OSS form upload, confirm the object with `assetKind: "editor_reference_image"`. Put the returned `objectKey` into the generation request:
|
||||
|
||||
- image generation: `referenceImageSrcs`
|
||||
- image edit/redraw: `sourceImageSrc`; extra references go in `referenceImageSrcs`
|
||||
- icon spritesheet: `referenceImageSrc`
|
||||
- UI asset extraction: `sourceImageSrc`; extra references go in `referenceImageSrcs`
|
||||
- character animation: `sourceImageSrc`
|
||||
- video generation image references: `referenceImageSrcs`
|
||||
|
||||
Use `signedUrl` only for display/download. For generation requests, use `objectKey`, project resource ID, asset ID, public URL, or Data URL as the endpoint allows; prefer uploaded `objectKey` for local/private reference images.
|
||||
|
||||
OSS form upload shape, using the ticket response saved as `ticket.json`. The default response has `upload`; if the caller explicitly requested the API response envelope, use `data.upload`:
|
||||
|
||||
```bash
|
||||
node - <<'NODE' ticket.json /path/to/reference.png
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
(async () => {
|
||||
const body = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
const ticket = body.upload || body.data?.upload;
|
||||
if (!ticket) throw new Error('Upload ticket response missing upload payload');
|
||||
const filePath = process.argv[3];
|
||||
const form = new FormData();
|
||||
for (const [key, value] of Object.entries(ticket.formFields)) {
|
||||
if (value != null) form.append(key, value);
|
||||
}
|
||||
const bytes = fs.readFileSync(filePath);
|
||||
form.append(
|
||||
'file',
|
||||
new Blob([bytes], { type: ticket.contentType || 'application/octet-stream' }),
|
||||
path.basename(filePath),
|
||||
);
|
||||
const response = await fetch(ticket.host, { method: 'POST', body: form });
|
||||
if (!response.ok) {
|
||||
throw new Error(`OSS upload failed: ${response.status} ${await response.text()}`);
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
```
|
||||
|
||||
Then confirm with `contentLength`:
|
||||
|
||||
```json
|
||||
{
|
||||
"objectKey": "<ticket upload.objectKey>",
|
||||
"contentType": "image/png",
|
||||
"contentLength": 12345,
|
||||
"assetKind": "editor_reference_image",
|
||||
"accessPolicy": "private"
|
||||
}
|
||||
```
|
||||
|
||||
`contentLength` is a JSON number from the local file byte size, not a quoted string.
|
||||
|
||||
For character animation from an uploaded local image, set:
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceLayerId": "external-reference-hero",
|
||||
"sourceImageSrc": "<uploaded objectKey>",
|
||||
"sourceWidth": 720,
|
||||
"sourceHeight": 1280,
|
||||
"promptText": "让角色自然呼吸并轻微转身",
|
||||
"resolution": "720p",
|
||||
"ratio": "9:16",
|
||||
"frameCount": 40,
|
||||
"durationSeconds": 5,
|
||||
"model": "seedance2.0-fast"
|
||||
}
|
||||
```
|
||||
|
||||
Use an existing canvas layer ID when the image came from a project layer. If it came only from a local upload, derive a stable synthetic `sourceLayerId` from the file name, for example `external-reference-hero`. Read `sourceWidth` and `sourceHeight` from the actual image before upload; ask the user only if the dimensions cannot be determined.
|
||||
|
||||
The helper uses a 420 second timeout for generation calls, including character animation and video. Direct HTTP clients should not use a 70 second request timeout for animation.
|
||||
|
||||
Character animation currently returns canvas completion data but not a direct `asset` payload. To keep the "canvas + asset library" invariant, call `client.animate_character(..., canvasSession=session, canvasTitle="...")`; the helper creates a library asset from the first returned frame in the session folder after the animation call succeeds.
|
||||
|
||||
For video generation, always include `mode: "std"`. When using image/video/audio references, default to `model: "seedance2.0-fast"` unless the user asks for another listed model, because reference media support is limited to the Seedance 2.0 family.
|
||||
|
||||
For image edit/redraw that should replace an existing canvas layer, pass `projectId` and `targetLayerId`. If the user instead gives an explicit `canvasCompletion`, let that placement win.
|
||||
|
||||
For sound effects and BGM, `assetFolderId` and `assetLabel` can write the generated audio to the account asset library, same as image/video generation.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Do not change the fixed production base URL in generated examples.
|
||||
- Do not move the API Key into environment variables, source files, generated projects, logs, docs, screenshots, or shell snippets containing literal secrets.
|
||||
- Do not treat a Data URL, Blob URL, expiring signed URL, worker lease, or provider diagnostic as a durable result.
|
||||
- Do not reconstruct authoritative canvas, resource, or library snapshots from a compact generation response.
|
||||
- Do not replace icon-spritesheet generation with ordinary image generation when the deliverable requires a reusable transparent atlas.
|
||||
- Do not invent endpoints outside the OpenAPI, especially internal worker or runtime task-list routes.
|
||||
- Do not omit canvas/library context for generation. New generated assets should enter both the canvas and the asset-library folder named after the canvas.
|
||||
- Do not put API Keys in repository files, generated project files, command history snippets with literal secrets, logs, docs, commits, or screenshots. The only default storage is the user's local private JSON credentials file.
|
||||
- Do not use account JWT endpoints as the default external integration path. The profile API can create/revoke keys for logged-in product users, but it is not part of the external editor OpenAPI.
|
||||
- When an endpoint returns `project`, `resource`, or `asset`, treat those as the authoritative updated project/resource/asset snapshots.
|
||||
|
||||
## Resources
|
||||
|
||||
- `references/api-selection.md`: intent routing and required-field cheat sheet.
|
||||
- `scripts/genarrative_external_api.py`: stdlib Python helper for OpenAPI fetch, API Key loading, local reference upload, object confirm, project/canvas calls, and generation requests.
|
||||
|
||||
@@ -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."
|
||||
short_description: "Auto-route canvas API generation"
|
||||
default_prompt: "Use $genarrative-external-editor-api to prepare a canvas session, infer the right API, and generate assets into the canvas and library."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
# API Operations
|
||||
|
||||
Use this reference after selecting a capability. Treat `GET /api/external/v1/openapi.json` as authoritative for exact request/response schemas, required fields, constraints, and operation IDs.
|
||||
|
||||
All paths below are relative to `https://www.genarrative.world`. Discovery and Skill download routes are public. Project, asset, upload, generation, and generation-query operations require the Bearer API Key.
|
||||
|
||||
## 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` |
|
||||
|
||||
Canvas save uses optimistic revision control. Pass the last authoritative `expectedRevision`; on conflict, reload instead of replaying a stale full layout.
|
||||
|
||||
Project listing supports two views:
|
||||
|
||||
- `view=full` is the REST default and returns the complete project, canvas, layers, and resources.
|
||||
- `view=summary` returns only `projectId`, `title`, `updatedAt`, and nullable `cover`, so callers can display, search, disambiguate same-name projects, and select a safe target without loading every canvas snapshot.
|
||||
- Hosted MCP `list_editor_projects` always uses `summary`; call `get_editor_project` after selecting a `projectId` when complete authoritative state is required.
|
||||
- `cover` contains only `resourceId`, stable `objectKey`, dimensions, and `updatedAt`. It never embeds image bytes, a Data URL, or a signed URL. To display it, pass `cover.objectKey` to `get_external_asset_read_url`; signed URLs are temporary and must not be persisted or reused as generation references.
|
||||
|
||||
## Asset and Upload Operations
|
||||
|
||||
| Operation | Method and path | Minimum input |
|
||||
| --- | --- | --- |
|
||||
| Create direct-upload ticket | `POST /api/external/v1/assets/direct-upload-tickets` | `legacyPrefix`, `fileName` |
|
||||
| Confirm uploaded object | `POST /api/external/v1/assets/objects/confirm` | `objectKey`, `assetKind` |
|
||||
| Get signed read URL | `GET /api/external/v1/assets/read-url` | `objectKey` or `legacyPublicPath` |
|
||||
| Read asset library | `GET /api/external/v1/editor/assets/library` | Authentication |
|
||||
| Create folder | `POST /api/external/v1/editor/assets/folders` | `label` |
|
||||
| Update folder | `PATCH /api/external/v1/editor/assets/folders/{folderId}` | `label` or `collapsed` |
|
||||
| Delete folder | `DELETE /api/external/v1/editor/assets/folders/{folderId}` | `folderId` |
|
||||
| Create asset record | `POST /api/external/v1/editor/assets` | `folderId`, `label`, `imageSrc`, `width`, `height`, `sourceType` |
|
||||
| Update asset record | `PATCH /api/external/v1/editor/assets/{assetId}` | `label` or `folderId` |
|
||||
| Delete asset record | `DELETE /api/external/v1/editor/assets/{assetId}` | `assetId` |
|
||||
|
||||
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.
|
||||
|
||||
## Generation Operations
|
||||
|
||||
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`, `sourceImageSrc` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `sourceResourceId`, `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 eight through:
|
||||
|
||||
```text
|
||||
GET /api/external/v1/generations/{operationId}
|
||||
```
|
||||
|
||||
Supply the `operationId` returned by submission. Poll no faster than `pollAfterMs` and retain the ID after a caller-side timeout.
|
||||
|
||||
## Canvas and Library Field Rules
|
||||
|
||||
- Pass `projectId` and `canvasCompletion` to write generated output into the canvas.
|
||||
- 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.
|
||||
- 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:
|
||||
|
||||
| Target capability | Field |
|
||||
| --- | --- |
|
||||
| Image generation | `referenceImageSrcs` |
|
||||
| Image edit/redraw | `sourceImageSrc`; additional references 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` |
|
||||
|
||||
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.
|
||||
|
||||
## 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`.
|
||||
- Video `model`: `seedance2.0`, `seedance2.0-fast`, `kling3.0`, `kling3.0-omni`, `veo3.1`, `veo3.1-fast`.
|
||||
- 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.
|
||||
@@ -0,0 +1,194 @@
|
||||
# External Editor API Routing
|
||||
|
||||
Source of truth: `docs/openapi/genarrative-external-v1.openapi.json`.
|
||||
|
||||
## Base
|
||||
|
||||
- Fixed base URL: `https://www.genarrative.world/`.
|
||||
- Public contract: `GET /api/external/v1/openapi.json`.
|
||||
- Authenticated calls: `Authorization: Bearer <tnr_sk_...>`.
|
||||
- Default credentials file: `~/.config/genarrative/external-editor-api.json` with an `apiKey` string.
|
||||
- Generation clients should allow long-running responses. Use at least 420 seconds for character animation and video; 70 seconds is too short for animation.
|
||||
|
||||
## Canvas Session and Art Spec
|
||||
|
||||
At the start of a new conversation, ask for a canvas name before the first generation call unless the user already supplied `projectId` and `assetFolderId`. Create or reuse:
|
||||
|
||||
1. `POST /api/external/v1/editor/projects` with `title` = canvas name.
|
||||
2. `GET /api/external/v1/editor/assets/library`; if no folder has the same label, `POST /api/external/v1/editor/assets/folders` with `label` = canvas name.
|
||||
3. Keep `canvasName`, `projectId`, `assetFolderId`, and the current art spec in conversation state.
|
||||
|
||||
Before generating art assets, normalize the user's request into a current art spec with `assetType`, `subject`, `style`, `palette`, `composition`, `format`, `constraints`, and `references`. Ask follow-up questions only for missing fields that block the selected endpoint. Reuse the current spec automatically when the user asks for another asset without changing style/spec requirements. Put the spec in `generationInputs.artSpec` and summarize it in the prompt when useful.
|
||||
|
||||
## Intent Routing
|
||||
|
||||
Infer the endpoint from the user's description. Do not present this as a menu unless the request is genuinely ambiguous.
|
||||
|
||||
| User says | Route |
|
||||
| --- | --- |
|
||||
| "生成图片", "生图", "做一张背景/角色/宣发图" | `POST /api/external/v1/editor/images/generations` |
|
||||
| "重绘", "调整这张图", "基于这张图修改" | `POST /api/external/v1/editor/images/edits` |
|
||||
| "用这张参考图", "参考本地图片生成", "基于本地图做图" | Upload local image first, then pass returned `objectKey` into the generation/edit reference field |
|
||||
| "按规范图生成图标", "拆图标" | `POST /api/external/v1/editor/icon-spritesheets/generations` |
|
||||
| "从 UI 设计图提取素材" | `POST /api/external/v1/editor/ui-designs/assets/extractions` |
|
||||
| "让角色动起来", "生成角色动画帧" | `POST /api/external/v1/editor/character-animations/generations` |
|
||||
| "生成视频" | `POST /api/external/v1/editor/videos/generations` |
|
||||
| "生成音效" | `POST /api/external/v1/editor/audios/sound-effects/generations` |
|
||||
| "生成背景音乐/BGM" | `POST /api/external/v1/editor/audios/background-music/generations` |
|
||||
| "上传本地素材/图片/音频/视频" | Upload flow: direct upload ticket -> OSS form upload -> object confirm |
|
||||
| "保存画板布局" | `PATCH /api/external/v1/editor/projects/{projectId}/canvas` |
|
||||
| "创建/读取/删除画板项目" | Project endpoints |
|
||||
| "素材库/文件夹/素材记录" | Asset library endpoints |
|
||||
| "读取私有素材/拿可访问链接" | `GET /api/external/v1/assets/read-url` |
|
||||
|
||||
Ask a follow-up only when two routes could both be correct and produce different artifacts, for example "处理这张图" without saying edit, extract UI assets, or use it as a reference for new generation.
|
||||
|
||||
## Endpoint Map
|
||||
|
||||
| User intent | Endpoint | Minimum request |
|
||||
| --- | --- | --- |
|
||||
| Read contract | `GET /api/external/v1/openapi.json` | No auth required |
|
||||
| List projects | `GET /api/external/v1/editor/projects` | API Key |
|
||||
| Create project | `POST /api/external/v1/editor/projects` | Optional `title` |
|
||||
| Load recent project | `GET /api/external/v1/editor/projects/recent` | API Key |
|
||||
| Get/delete project | `GET` or `DELETE /api/external/v1/editor/projects/{projectId}` | `projectId` |
|
||||
| Rename project | `PATCH /api/external/v1/editor/projects/{projectId}/metadata` | `title` |
|
||||
| Save canvas layout | `PATCH /api/external/v1/editor/projects/{projectId}/canvas` | `viewport`, `layers` |
|
||||
| Add project resource | `POST /api/external/v1/editor/projects/{projectId}/resources` | `imageSrc`, `width`, `height`, `sourceType` |
|
||||
| Create 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` | API Key |
|
||||
| Create/update/delete folder | `POST /api/external/v1/editor/assets/folders`, `PATCH`/`DELETE /api/external/v1/editor/assets/folders/{folderId}` | create: `label`; update: `label` or `collapsed` |
|
||||
| Create asset record | `POST /api/external/v1/editor/assets` | `folderId`, `label`, `imageSrc`, `width`, `height`, `sourceType` |
|
||||
| Update/delete asset | `PATCH`/`DELETE /api/external/v1/editor/assets/{assetId}` | update: `label` or `folderId` |
|
||||
|
||||
## Generation Endpoints
|
||||
|
||||
| User intent | Endpoint | Required fields | Common optional fields |
|
||||
| --- | --- | --- | --- |
|
||||
| Generate image/spec/character/UI/publication material | `POST /api/external/v1/editor/images/generations` | `prompt` | `kind`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Edit/redraw image | `POST /api/external/v1/editor/images/edits` | `prompt`, `sourceImageSrc` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `sourceResourceId`, `targetLayerId`, `canvasCompletion` |
|
||||
| Generate icon spritesheet | `POST /api/external/v1/editor/icon-spritesheets/generations` | `referenceImageSrc`, `iconDescriptions` | `referenceImageSrcs`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `canvasCompletion` |
|
||||
| Extract assets from UI design | `POST /api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
|
||||
| Generate character animation | `POST /api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `canvasCompletion`; then create a library asset from the first returned frame |
|
||||
| Generate video | `POST /api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Generate sound effect | `POST /api/external/v1/editor/audios/sound-effects/generations` | `prompt`, `duration` | `model`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Generate background music | `POST /api/external/v1/editor/audios/background-music/generations` | `gptDescriptionPrompt`, `makeInstrumental` | `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
|
||||
All generation requests should be placed into both the current canvas and its same-name asset-library folder. For endpoints that support `assetLabel`, pass it. For UI extraction, use `spritesheetLabel`. For icon spritesheet, the folder is enough. For character animation, the endpoint does not return `asset`; after success call `POST /api/external/v1/editor/assets` using the first returned frame as `imageSrc`, the session `assetFolderId`, and `assetKind: "character-animation"`.
|
||||
|
||||
## Reference Image Upload
|
||||
|
||||
If the user provides a local file as a reference image, run upload before the generation request:
|
||||
|
||||
1. `POST /api/external/v1/assets/direct-upload-tickets`.
|
||||
Use `legacyPrefix: "generated-character-drafts"`, `pathSegments: ["editor", "external-editor-references"]`, original `fileName`, detected image `contentType`, and `access: "private"`.
|
||||
2. Upload the file to the returned OSS form endpoint with all returned `formFields`.
|
||||
3. `POST /api/external/v1/assets/objects/confirm` with returned `objectKey`, detected `contentType`, `contentLength` if known, `assetKind: "editor_reference_image"`, and `accessPolicy: "private"`.
|
||||
4. Use the returned `objectKey` in the actual editor request.
|
||||
|
||||
OSS form upload uses `upload.host` and every non-null `upload.formFields` entry, then the file part named `file`. Default responses expose `upload`; envelope responses expose `data.upload`. Save the upload ticket response as `ticket.json`:
|
||||
|
||||
```bash
|
||||
node - <<'NODE' ticket.json /path/to/reference.png
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
(async () => {
|
||||
const body = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
const ticket = body.upload || body.data?.upload;
|
||||
if (!ticket) throw new Error('Upload ticket response missing upload payload');
|
||||
const filePath = process.argv[3];
|
||||
const form = new FormData();
|
||||
for (const [key, value] of Object.entries(ticket.formFields)) {
|
||||
if (value != null) form.append(key, value);
|
||||
}
|
||||
const bytes = fs.readFileSync(filePath);
|
||||
form.append(
|
||||
'file',
|
||||
new Blob([bytes], { type: ticket.contentType || 'application/octet-stream' }),
|
||||
path.basename(filePath),
|
||||
);
|
||||
const response = await fetch(ticket.host, { method: 'POST', body: form });
|
||||
if (!response.ok) {
|
||||
throw new Error(`OSS upload failed: ${response.status} ${await response.text()}`);
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
```
|
||||
|
||||
Field mapping after upload:
|
||||
|
||||
| Target API | Put uploaded `objectKey` in |
|
||||
| --- | --- |
|
||||
| Image generation | `referenceImageSrcs` |
|
||||
| Image edit/redraw | `sourceImageSrc`; additional references in `referenceImageSrcs` |
|
||||
| Icon spritesheet | `referenceImageSrc`; additional style refs in `referenceImageSrcs` |
|
||||
| UI design extraction | `sourceImageSrc`; additional refs in `referenceImageSrcs` |
|
||||
| Character animation | `sourceImageSrc` |
|
||||
| Video generation with image references | `referenceImageSrcs` |
|
||||
|
||||
Do not put the signed read URL into generation fields. Signed URLs are for user-visible preview/download; generation fields should use the stable `objectKey` for uploaded private references.
|
||||
|
||||
## Common Enums
|
||||
|
||||
- Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`.
|
||||
- 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`.
|
||||
- Video `model`: `seedance2.0`, `seedance2.0-fast`, `kling3.0`, `kling3.0-omni`, `veo3.1`, `veo3.1-fast`.
|
||||
- Video `aspectRatio`: `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `21:9`.
|
||||
- Video `resolution`: `480p`, `720p`, `1080p`.
|
||||
- Video `mode`: always `std`.
|
||||
- Video `sound`: `on`, `off`.
|
||||
- Character animation `model`: always `seedance2.0-fast`.
|
||||
- Character animation `resolution`: `480p`, `720p`; `frameCount`: `32`, `40`, `48`; `durationSeconds`: `4`, `5`, `6`; `ratio`: `same`, `1:1`, `4:3`, `16:9`, `9:16`, `3:4`.
|
||||
|
||||
## Local Reference Media Details
|
||||
|
||||
- `contentLength` in object confirm is a JSON number from local byte size, not a string.
|
||||
- For character animation, use an existing project layer ID as `sourceLayerId` when available.
|
||||
- If the source is only an uploaded local image, derive `sourceLayerId` from the file name, such as `external-reference-hero`, and keep it stable across retries.
|
||||
- Read `sourceWidth` and `sourceHeight` from the local image. If dimensions cannot be read, ask instead of inventing dimensions.
|
||||
- UI design extraction uses fixed `aspectRatio: "1:1"`; choose `imageSize: "1K"` for normal/small extractions and `2K` for dense designs.
|
||||
- Video image/video/audio references are supported only by the Seedance 2.0 family; default referenced-media video requests to `model: "seedance2.0-fast"`, `mode: "std"`, and explicit `sound`.
|
||||
- Image edit/redraw can pass `targetLayerId` with `projectId` to replace an existing canvas layer when no explicit `canvasCompletion` is supplied.
|
||||
- Image, edit, video, sound effect, and BGM generation can pass `assetFolderId` and `assetLabel`; response `asset` is the created/updated library record.
|
||||
- Icon spritesheet and UI extraction can pass `assetFolderId`; UI extraction can also pass `spritesheetLabel`.
|
||||
|
||||
## Canvas Completion
|
||||
|
||||
Use `canvasCompletion` for generation in this skill so the generated result is written back into the project canvas by the backend.
|
||||
|
||||
Required:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "素材名称",
|
||||
"placeholder": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 512,
|
||||
"height": 512,
|
||||
"originalWidth": 512,
|
||||
"originalHeight": 512
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`dialogId` is optional. If the response includes `project`, `resource`, or `asset`, use those snapshots instead of reconstructing canvas/resource/library state locally.
|
||||
|
||||
## Upload Flow
|
||||
|
||||
For a local file that should become a project resource or library asset:
|
||||
|
||||
1. `POST /api/external/v1/assets/direct-upload-tickets` with `legacyPrefix`, `fileName`, and optional `contentType`, `access`, `maxSizeBytes`.
|
||||
2. Submit the file to the returned OSS form endpoint with returned `formFields`.
|
||||
3. `POST /api/external/v1/assets/objects/confirm` with returned `objectKey` and an `assetKind`.
|
||||
4. Create a project resource or library asset with the confirmed `assetObjectId`/`objectKey`.
|
||||
|
||||
For reading private/generated assets, call `GET /api/external/v1/assets/read-url?objectKey=...` and use the returned `signedUrl`.
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
# Authentication and Safety
|
||||
|
||||
Read this reference before handling credentials, local files, private objects, uploads, retries, or logs.
|
||||
|
||||
## Contents
|
||||
|
||||
- [API Key Setup](#api-key-setup)
|
||||
- [Idempotency and Unknown Outcomes](#idempotency-and-unknown-outcomes)
|
||||
- [Local Reference Upload](#local-reference-upload)
|
||||
- [Stable and Temporary Media References](#stable-and-temporary-media-references)
|
||||
- [Logging and Command Safety](#logging-and-command-safety)
|
||||
- [Scope and Retry Guardrails](#scope-and-retry-guardrails)
|
||||
|
||||
## API Key Setup
|
||||
|
||||
Authenticated calls use:
|
||||
|
||||
```text
|
||||
Authorization: Bearer <tnr_sk_...>
|
||||
```
|
||||
|
||||
Guide a logged-in user to create a key in the product UI under `开发者 API Key`. The raw key is shown only once. Never ask the user to paste it into chat.
|
||||
|
||||
Store it outside repositories in the user's private JSON file:
|
||||
|
||||
```text
|
||||
~/.config/genarrative/external-editor-api.json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "tnr_sk_..."
|
||||
}
|
||||
```
|
||||
|
||||
Set the file readable only by the current user where supported:
|
||||
|
||||
```bash
|
||||
chmod 600 ~/.config/genarrative/external-editor-api.json
|
||||
```
|
||||
|
||||
Use this fixed production base URL:
|
||||
|
||||
```text
|
||||
https://www.genarrative.world/
|
||||
```
|
||||
|
||||
Do not use environment variables as the default API Key storage for this integration. Generated clients must load the JSON file, fail clearly when it is absent or malformed, and redact credentials from errors and logs.
|
||||
|
||||
Smoke-test without printing the key:
|
||||
|
||||
```bash
|
||||
api_key="$(node -e 'const fs=require("fs"); const p=process.argv[1]; const c=JSON.parse(fs.readFileSync(p,"utf8")); process.stdout.write(c.apiKey || "");' "$HOME/.config/genarrative/external-editor-api.json")"
|
||||
curl -fsS "https://www.genarrative.world/api/external/v1/editor/projects" \
|
||||
-H "Authorization: Bearer $api_key"
|
||||
```
|
||||
|
||||
The OpenAPI document, integration manifest, raw Skill entry, and Skill archive are public. Hosted MCP and all project, asset, upload, generation, and generation-query operations require the Bearer API Key.
|
||||
|
||||
## Idempotency and Unknown Outcomes
|
||||
|
||||
For each logical generation:
|
||||
|
||||
1. Create one printable ASCII `Idempotency-Key` of 1-128 bytes.
|
||||
2. Persist the key with the exact request body and returned `operationId`.
|
||||
3. If submission transport fails or the response is lost, resend only the exact same body with the same key.
|
||||
4. Never allocate a new key merely because the outcome is unknown.
|
||||
5. On a polling timeout, retain `operationId` and query later. Do not submit another generation.
|
||||
|
||||
Treat a different body under the same key as invalid. Do not automatically replay a failed terminal generation unless the user intentionally requests a new logical generation.
|
||||
|
||||
## Local Reference Upload
|
||||
|
||||
Do not ask the user to convert local files to base64. Upload from the Agent/client machine:
|
||||
|
||||
1. Detect the original filename, MIME type, byte length, and image dimensions when relevant.
|
||||
2. Create a ticket with `POST /api/external/v1/assets/direct-upload-tickets`.
|
||||
3. POST all returned non-null `formFields` and the file part named `file` directly to `upload.host`.
|
||||
4. Confirm the object with `POST /api/external/v1/assets/objects/confirm`.
|
||||
5. Pass the confirmed stable `objectKey` to the selected editor operation.
|
||||
|
||||
For a private reference image, use a ticket body shaped like:
|
||||
|
||||
```json
|
||||
{
|
||||
"legacyPrefix": "generated-character-drafts",
|
||||
"pathSegments": ["editor", "external-editor-references"],
|
||||
"fileName": "<original-file-name>",
|
||||
"contentType": "image/png",
|
||||
"access": "private"
|
||||
}
|
||||
```
|
||||
|
||||
The default response exposes `upload`; an explicitly enveloped response exposes `data.upload`. Treat the returned host and form fields as opaque. Do not log the entire ticket or persist it longer than needed.
|
||||
|
||||
Confirm with the actual file metadata:
|
||||
|
||||
```json
|
||||
{
|
||||
"objectKey": "<upload.objectKey>",
|
||||
"contentType": "image/png",
|
||||
"contentLength": 12345,
|
||||
"assetKind": "editor_reference_image",
|
||||
"accessPolicy": "private"
|
||||
}
|
||||
```
|
||||
|
||||
`contentLength` is a JSON number in bytes, not a quoted string. Never invent `sourceWidth` or `sourceHeight`; read them from the local image or ask the user if they cannot be determined.
|
||||
|
||||
For character animation, reuse a real canvas layer ID when available. For a local-only source, derive a stable synthetic `sourceLayerId`, such as `external-reference-hero`, from the filename and keep it unchanged across retries.
|
||||
|
||||
The bundled helper implements ticket creation, a stdlib multipart upload, confirmation, dimension detection for common formats, and stable source-layer IDs:
|
||||
|
||||
```python
|
||||
from genarrative_external_api import GenarrativeExternalClient
|
||||
|
||||
client = GenarrativeExternalClient()
|
||||
reference = client.upload_reference_image("/path/to/reference.png")
|
||||
print(reference["objectKey"])
|
||||
```
|
||||
|
||||
Do not print the complete confirmation response if it may contain temporary access data. Prefer passing the returned `objectKey` directly to the next call.
|
||||
|
||||
## Stable and Temporary Media References
|
||||
|
||||
- Use `objectKey`, project resource ID, asset ID, or an allowed durable public URL for generation input.
|
||||
- Use a Data URL only when the endpoint explicitly allows it and the caller has a deliberate reason; do not persist it as a durable output.
|
||||
- Never use a Blob URL outside the browser process that created it.
|
||||
- Use `GET /api/external/v1/assets/read-url` to obtain a short-lived `signedUrl` for display/download.
|
||||
- Never store or feed an expiring signed URL back into generation when a stable `objectKey` exists.
|
||||
|
||||
## Logging and Command Safety
|
||||
|
||||
- Never place an API Key in repository files, generated projects, command arguments containing a literal key, docs, commits, screenshots, stack traces, test fixtures, or telemetry.
|
||||
- Redact `Authorization`, API Key values, upload signatures, cookies, signed URL query strings, and private absolute paths from logs and user-visible errors.
|
||||
- Do not print credentials while diagnosing JSON configuration. Report only presence/absence and safe validation errors.
|
||||
- Do not commit the credentials file or copy it into the Skill archive.
|
||||
- Do not expose provider diagnostics, worker leases, queue internals, or server filesystem paths returned by an unexpected error.
|
||||
|
||||
## Scope and Retry Guardrails
|
||||
|
||||
- Do not use account JWT/profile endpoints as the default external integration. Logged-in profile APIs may create/revoke developer keys, but they are outside this external editor contract.
|
||||
- Do not call internal workers, queues, SpacetimeDB, or admin endpoints.
|
||||
- Do not bypass upload confirmation or invent an object key.
|
||||
- Do not retry post-processing locally by fabricating assets. Respect completed warning semantics from `requests-and-outputs.md`.
|
||||
- Use bounded polling. A local wait budget ending does not cancel or fail the server operation.
|
||||
@@ -1,89 +0,0 @@
|
||||
# Capability Routing
|
||||
|
||||
Use this reference to translate user intent into a hosted MCP tool or its corresponding External v1 REST operation. Use `genarrative://external-editor/openapi` or `GET /api/external/v1/openapi.json` for exact schemas.
|
||||
|
||||
## Integration Surface
|
||||
|
||||
- Fixed production base URL: `https://www.genarrative.world/`.
|
||||
- Discovery manifest: `GET /api/external/v1/agent-integration.json`.
|
||||
- Hosted MCP: `/api/external/v1/mcp`, Streamable HTTP, authenticated with the same Bearer API Key as REST.
|
||||
- Public contract: `GET /api/external/v1/openapi.json`.
|
||||
- Skill fallback: `GET /api/external/v1/skill/SKILL.md` or `GET /api/external/v1/skill.zip`.
|
||||
|
||||
Prefer MCP when the Agent supports a remote endpoint plus a custom Bearer token. Prefer the complete Skill and Python helper when MCP is unavailable or a client-side local-file upload must be orchestrated. The MCP tool names are derived from OpenAPI `operationId` values in snake case; select by capability instead of memorizing the name.
|
||||
|
||||
## Canvas Session
|
||||
|
||||
Before the first generation in a new conversation, obtain a canvas name unless the user already supplied an existing `projectId` and `assetFolderId`.
|
||||
|
||||
1. List or create a project. When creating one, use the canvas name as `title`.
|
||||
2. Read the asset library. Reuse a folder with the same label or create one with the canvas name.
|
||||
3. Retain `canvasName`, `projectId`, `assetFolderId`, and the current art spec in conversation state.
|
||||
|
||||
Generated artifacts must enter both the current canvas and its same-name library folder whenever the endpoint supports that invariant. Pass `projectId`, `assetFolderId`, the endpoint's label field, and `canvasCompletion`. Character animation returns the final formal resource and asset directly; use those records and never create a duplicate from the first frame.
|
||||
|
||||
## Art Spec Routing
|
||||
|
||||
Before art generation, normalize the user's request into:
|
||||
|
||||
```json
|
||||
{
|
||||
"assetType": "character | background | prop | ui | icon | animation | video | audio",
|
||||
"subject": "要生成的主体",
|
||||
"style": "画风、材质、时代或参考风格",
|
||||
"palette": "主色与禁用色",
|
||||
"composition": "构图、镜头、姿态或布局",
|
||||
"format": "比例、尺寸、分辨率、帧数或时长",
|
||||
"constraints": "必须保留、禁止出现、透明或绿幕要求",
|
||||
"references": ["objectKey、资源 ID 或本地文件说明"]
|
||||
}
|
||||
```
|
||||
|
||||
Infer what is already clear and ask only for missing fields that block the selected endpoint. Reuse the current spec unless the user changes style, subject family, palette, format, or constraints. Store structured context under `generationInputs.artSpec` where supported and summarize it in the prompt when useful.
|
||||
|
||||
## 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 |
|
||||
| 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.
|
||||
|
||||
## Route-Specific Decisions
|
||||
|
||||
- Use image edit when the requested output replaces or modifies a source image. With `projectId`, pass `targetLayerId` to replace an existing layer when no explicit `canvasCompletion` is supplied.
|
||||
- Use icon spritesheet generation for a transparent reusable atlas when a stable visual-spec reference and concrete `iconDescriptions` exist. Do not use ordinary image generation just because it can draw several objects.
|
||||
- Use UI extraction only for an existing UI design image with red-box annotations. It is not UI generation.
|
||||
- Use a project layer ID as character animation `sourceLayerId` when one exists. For a local-only source, derive a stable synthetic ID from the filename.
|
||||
- For video with image/video/audio references, use a Seedance 2.0-family model; default to `seedance2.0-fast`, `mode: "std"`, and explicit `sound`.
|
||||
- Use `signedUrl` only for preview/download. Feed stable `objectKey` or registered resource/asset identifiers into generation.
|
||||
|
||||
## AI Game Creator Canonical Visual DAG
|
||||
|
||||
Keep the existing autonomous-build task graph. Do not add a parallel task system or collapse these artifacts into one ordinary generation request:
|
||||
|
||||
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 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.
|
||||
|
||||
## Scope Boundary
|
||||
|
||||
Stay within `/api/external/v1`. Do not invent worker, queue, runtime task-list, admin, profile, or SpacetimeDB calls. The only external generation query is `GET /api/external/v1/generations/{operationId}`.
|
||||
@@ -1,245 +0,0 @@
|
||||
# Requests and Outputs
|
||||
|
||||
Use this reference to build generation payloads, carry canvas/library context, poll asynchronous jobs, and interpret compact completed results. Verify exact schemas against `GET /api/external/v1/openapi.json`.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Asynchronous Submission](#asynchronous-submission)
|
||||
- [Polling State Machine](#polling-state-machine)
|
||||
- [Canvas and Asset-Library Completion](#canvas-and-asset-library-completion)
|
||||
- [Art Spec and Image Request](#art-spec-and-image-request)
|
||||
- [Local Reference Requests](#local-reference-requests)
|
||||
- [Compact Completed Result](#compact-completed-result)
|
||||
- [Warning Semantics](#warning-semantics)
|
||||
- [Output Handling Checklist](#output-handling-checklist)
|
||||
|
||||
## Asynchronous Submission
|
||||
|
||||
All eight generation POST routes require `Idempotency-Key` and return HTTP `202` with an `ExternalEditorGenerationSubmissionResponse` shaped like:
|
||||
|
||||
```json
|
||||
{
|
||||
"operationId": "task-...",
|
||||
"kind": "editor_image_generation",
|
||||
"status": "queued",
|
||||
"statusUrl": "/api/external/v1/generations/task-...",
|
||||
"pollAfterMs": 1500,
|
||||
"updatedAtMicros": 1785456000000000
|
||||
}
|
||||
```
|
||||
|
||||
The response acknowledges durable submission only. It is never the completed media response.
|
||||
|
||||
Submit with one stable key per logical request:
|
||||
|
||||
```bash
|
||||
api="https://www.genarrative.world"
|
||||
credentials_file="$HOME/.config/genarrative/external-editor-api.json"
|
||||
api_key="$(node -e 'const fs=require("fs"); const p=process.argv[1]; const c=JSON.parse(fs.readFileSync(p,"utf8")); process.stdout.write(c.apiKey || "");' "$credentials_file")"
|
||||
idempotency_key="$(node -e 'process.stdout.write(require("node:crypto").randomUUID())')"
|
||||
|
||||
submission="$(curl -fsS "$api/api/external/v1/editor/images/generations" \
|
||||
-H "Authorization: Bearer $api_key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Idempotency-Key: $idempotency_key" \
|
||||
-d @request.json)"
|
||||
operation_id="$(node -e 'const v=JSON.parse(process.argv[1]); process.stdout.write(v.operationId || v.data?.operationId || "")' "$submission")"
|
||||
```
|
||||
|
||||
Persist the key, exact request body, and `operationId`. If submission outcome is uncertain, reuse the same body and key; do not submit a replacement key.
|
||||
|
||||
## Polling State Machine
|
||||
|
||||
Poll `statusUrl`, or `GET /api/external/v1/generations/{operationId}`, no faster than `pollAfterMs`:
|
||||
|
||||
- `queued` / `running`: retain `operationId`; show `phaseLabel`, `phaseDetail`, and `progress` when present; wait before querying again.
|
||||
- `completed`: consume the compact `result` and all warning fields, then stop polling.
|
||||
- `failed`: surface the safe `error`, stop polling, and do not infer provider or worker internals.
|
||||
|
||||
A caller-side timeout leaves the operation pending. Persist the ID for later query. Do not keep the original POST connection open and do not infer failure from a local wait budget.
|
||||
|
||||
The helper's convenience generation methods block only in the local process while sending short submit and status requests. Its default overall wait budget is 1800 seconds. For explicit orchestration:
|
||||
|
||||
```python
|
||||
submission = client.submit_generation(
|
||||
"/api/external/v1/editor/images/generations",
|
||||
request_body,
|
||||
idempotency_key=stable_key,
|
||||
)
|
||||
operation_id = submission["operationId"]
|
||||
status = client.get_generation(operation_id)
|
||||
completed = client.wait_for_generation(operation_id)
|
||||
```
|
||||
|
||||
## Canvas and Asset-Library Completion
|
||||
|
||||
For endpoints that support these fields, include:
|
||||
|
||||
- `projectId`: target canvas project.
|
||||
- `assetFolderId`: folder whose label matches the canvas name.
|
||||
- `assetLabel` or UI extraction's `spritesheetLabel`: user-visible library label.
|
||||
- `canvasCompletion`: backend canvas placement instructions.
|
||||
|
||||
A minimal `canvasCompletion` is:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "素材名称",
|
||||
"placeholder": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 1024,
|
||||
"height": 576,
|
||||
"originalWidth": 1024,
|
||||
"originalHeight": 576
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`dialogId` is optional. Do not reconstruct canvas state from completion results. Reload the project and asset library when complete authoritative snapshots are needed.
|
||||
|
||||
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.
|
||||
|
||||
## Art Spec and Image Request
|
||||
|
||||
Generic External v1 image generation does not expose the main-site structured game-scene contract. `kind: "scene"` and `assetKind: "scene"` are both invalid and return HTTP `400` before any generation job is queued. Do not replace the structured scene fields and server-owned prompt assembly with a generic image prompt.
|
||||
|
||||
Carry the current art spec in `generationInputs.artSpec` and reflect important constraints in the prompt:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "一张横版幻想森林背景,适合游戏主视觉,无文字",
|
||||
"aspectRatio": "16:9",
|
||||
"imageSize": "1K",
|
||||
"projectId": "<projectId>",
|
||||
"assetFolderId": "<assetFolderId>",
|
||||
"assetLabel": "森林背景",
|
||||
"generationInputs": {
|
||||
"artSpec": {
|
||||
"assetType": "background",
|
||||
"subject": "幻想森林主视觉",
|
||||
"style": "手绘游戏概念图",
|
||||
"palette": "翡翠绿与金色光斑",
|
||||
"composition": "横版,中心留出角色站位",
|
||||
"format": "16:9, 1K",
|
||||
"constraints": "无文字、无 UI 按钮",
|
||||
"references": []
|
||||
}
|
||||
},
|
||||
"canvasCompletion": {
|
||||
"title": "森林背景",
|
||||
"placeholder": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 1024,
|
||||
"height": 576,
|
||||
"originalWidth": 1024,
|
||||
"originalHeight": 576
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The top-level `style` field is not the art spec's visual-style prose. It appends a short server-side clause to the prompt sent to the provider and enables deterministic post-processing:
|
||||
|
||||
- Omitted, `null`, empty string, or `"none"`: no clause is appended and no post-processing runs, without warning.
|
||||
- `"pixelArt"`: append one short pixel-art line to the end of the prompt sent to the provider, and enable pixel-art snapping, for ordinary image generation, `kind: "character"`, and icon spritesheet generation. The line is appended, not substituted — the rest of your prompt is unchanged. For the exact per-kind wording, read the `style` field description in the OpenAPI document; it is the contract, and this guide deliberately does not copy it.
|
||||
- Unknown strings, or `"pixelArt"` on unsupported kinds such as `spec`, `quick-edit`, `ui-design`, or `publication-material`: continue without style processing and return `warning.code: "unsupported-image-style"`.
|
||||
- Non-string JSON values: malformed request, HTTP `400`.
|
||||
|
||||
Keep this field extensible. Do not impose a closed client enum beyond the server contract.
|
||||
|
||||
## Local Reference Requests
|
||||
|
||||
Upload and confirm a local file before generation, then use the stable `objectKey`:
|
||||
|
||||
```python
|
||||
client = GenarrativeExternalClient()
|
||||
session = client.prepare_canvas_session("新画板")
|
||||
reference = client.upload_reference_image("/path/to/reference.png")
|
||||
client.generate_image(
|
||||
"基于参考图生成一张 16:9 游戏背景",
|
||||
canvasSession=session,
|
||||
assetLabel="参考图背景",
|
||||
aspectRatio="16:9",
|
||||
imageSize="1K",
|
||||
referenceImageSrcs=[reference["objectKey"]],
|
||||
)
|
||||
```
|
||||
|
||||
Icon spritesheet generation has a stricter primary-spec contract. After upload confirmation, create a project resource or asset record with `assetKind: "icon-spec"`, retain its returned `resourceId` or `assetId`, and pass that ID as `referenceId`. The primary spec does not accept the uploaded `objectKey` directly; only additional style references may continue to use stable object keys in `referenceImageSrcs`.
|
||||
|
||||
For character animation from a local-only source, use actual dimensions and a stable synthetic layer ID:
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceLayerId": "external-reference-hero",
|
||||
"sourceImageSrc": "<confirmed objectKey>",
|
||||
"sourceWidth": 720,
|
||||
"sourceHeight": 1280,
|
||||
"promptText": "让角色自然呼吸并轻微转身",
|
||||
"resolution": "720p",
|
||||
"ratio": "9:16",
|
||||
"frameCount": 40,
|
||||
"durationSeconds": 5,
|
||||
"model": "seedance2.0-fast",
|
||||
"assetFolderId": "<assetFolderId>",
|
||||
"assetLabel": "角色呼吸动画"
|
||||
}
|
||||
```
|
||||
|
||||
Do not guess dimensions or pass a temporary signed read URL. See `authentication-and-safety.md` for upload and credential rules.
|
||||
|
||||
## Compact Completed Result
|
||||
|
||||
The completed `result` may contain stable artifact fields such as:
|
||||
|
||||
- `objectKey`, media type, dimensions, or task ID.
|
||||
- Sound-effect `durationSeconds` is the probed MP3 duration and `loop` is the frozen request boolean; neither is inferred from Prompt text.
|
||||
- `resource`, `resourceId`, or equivalent canvas reference.
|
||||
- `asset`, `assetId`, or equivalent library reference.
|
||||
- `spritesheetResource`, `spritesheetAsset`, and stable spritesheet metadata.
|
||||
- `warning` and `sliceWarning` structures.
|
||||
|
||||
It deliberately excludes a complete project/canvas/library snapshot, Data URL, Blob URL, expiring signed URL, worker lease, queue state, and internal provider diagnostics. Use `/assets/read-url` for temporary access to a stable `objectKey`.
|
||||
|
||||
## Warning Semantics
|
||||
|
||||
Interpret warnings only after the query reaches `status=completed`. The query-level `warning` is display-ready text. Compact `result.warning` and `result.sliceWarning` preserve structured artifact semantics.
|
||||
|
||||
### Source-preserved post-processing failure
|
||||
|
||||
When `result.warning.code` is `postprocess-failed-source-preserved`:
|
||||
|
||||
- Treat the saved provider source as the authoritative main result.
|
||||
- For character output, do not claim a transparent derivative.
|
||||
- For icon spritesheet or UI extraction, do not claim a transparent spritesheet or individual slices.
|
||||
- Display the safe reason.
|
||||
- Do not fabricate derivatives or restart generation automatically.
|
||||
|
||||
Use `resource` / `asset` for character results and `spritesheetResource` / `spritesheetAsset` for icon/UI results, then reload authoritative project/library state.
|
||||
|
||||
### Slice failure after transparent-sheet success
|
||||
|
||||
`result.sliceWarning` means transparent spritesheet post-processing succeeded but automatic splitting failed:
|
||||
|
||||
- Continue using the complete transparent spritesheet.
|
||||
- Do not claim individual slices.
|
||||
- Display the slice reason.
|
||||
|
||||
### Coexisting warnings
|
||||
|
||||
`warning` and `sliceWarning` are mutually exclusive only for `postprocess-failed-source-preserved`, because that path never reaches slicing. A general warning from unsupported style normalization or pixel-art snapping can coexist with `sliceWarning`. Render both reasons.
|
||||
|
||||
Before registering a requested transparent deliverable, verify the full sheet actually contains transparency. If source-preserved warning is present, do not register the opaque provider source as the requested transparent atlas. If only `sliceWarning` is present, the transparent full sheet remains valid.
|
||||
|
||||
## Output Handling Checklist
|
||||
|
||||
1. Require terminal `completed` before consuming artifacts.
|
||||
2. Preserve stable IDs and `objectKey` values.
|
||||
3. Surface all warning channels without downgrading completion to failure.
|
||||
4. Avoid claiming absent transparent derivatives or slices.
|
||||
5. Obtain temporary preview/download URLs only through `/assets/read-url`.
|
||||
6. Reload authoritative project and library state when downstream logic needs complete records.
|
||||
+64
-228
@@ -11,7 +11,6 @@ import re
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
@@ -23,7 +22,7 @@ from typing import Any
|
||||
BASE_URL = "https://www.genarrative.world/"
|
||||
DEFAULT_CREDENTIALS_FILE = Path.home() / ".config/genarrative/external-editor-api.json"
|
||||
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
|
||||
GENERATION_WAIT_TIMEOUT_SECONDS = 1800
|
||||
GENERATION_REQUEST_TIMEOUT_SECONDS = 420
|
||||
|
||||
|
||||
class GenarrativeApiError(RuntimeError):
|
||||
@@ -126,18 +125,17 @@ class GenarrativeExternalClient:
|
||||
query: dict[str, Any] | None = None,
|
||||
auth: bool = True,
|
||||
timeout: int = DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||
headers: dict[str, str] | None = None,
|
||||
) -> Any:
|
||||
url = f"{self.base_url}{path}"
|
||||
if query:
|
||||
url = f"{url}?{urllib.parse.urlencode({k: v for k, v in query.items() if v is not None})}"
|
||||
data = None if body is None else json.dumps(body).encode("utf-8")
|
||||
request_headers = {"Accept": "application/json", **(headers or {})}
|
||||
headers = {"Accept": "application/json"}
|
||||
if data is not None:
|
||||
request_headers["Content-Type"] = "application/json"
|
||||
headers["Content-Type"] = "application/json"
|
||||
if auth:
|
||||
request_headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
request = urllib.request.Request(url, data=data, headers=request_headers, method=method.upper())
|
||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
request = urllib.request.Request(url, data=data, headers=headers, method=method.upper())
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
payload = response.read()
|
||||
@@ -272,21 +270,11 @@ class GenarrativeExternalClient:
|
||||
fields[asset_label_field] = normalize_optional_text(asset_label) or "生成素材"
|
||||
return fields
|
||||
|
||||
def save_canvas(
|
||||
self,
|
||||
project_id: str,
|
||||
viewport: dict[str, Any],
|
||||
layers: dict[str, Any],
|
||||
expected_revision: int,
|
||||
) -> Any:
|
||||
def save_canvas(self, project_id: str, viewport: dict[str, Any], layers: dict[str, Any]) -> Any:
|
||||
return self.request_json(
|
||||
"PATCH",
|
||||
f"/api/external/v1/editor/projects/{urllib.parse.quote(project_id, safe='')}/canvas",
|
||||
{
|
||||
"viewport": viewport,
|
||||
"layers": layers,
|
||||
"expectedRevision": expected_revision,
|
||||
},
|
||||
{"viewport": viewport, "layers": layers},
|
||||
)
|
||||
|
||||
def _apply_art_spec(self, fields: dict[str, Any], prompt: str) -> str:
|
||||
@@ -442,156 +430,34 @@ class GenarrativeExternalClient:
|
||||
def read_url(self, object_key: str) -> Any:
|
||||
return self.request_json("GET", "/api/external/v1/assets/read-url", query={"objectKey": object_key})
|
||||
|
||||
def submit_generation(
|
||||
self,
|
||||
path: str,
|
||||
body: dict[str, Any],
|
||||
idempotency_key: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
key = normalize_optional_text(idempotency_key) or str(uuid.uuid4())
|
||||
submission = None
|
||||
for attempt in range(2):
|
||||
try:
|
||||
submission = self.request_json(
|
||||
"POST",
|
||||
path,
|
||||
body,
|
||||
headers={"Idempotency-Key": key},
|
||||
)
|
||||
break
|
||||
except (urllib.error.URLError, TimeoutError) as error:
|
||||
if attempt == 0:
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
raise GenarrativeApiError(
|
||||
"Generation submission transport outcome is unknown. "
|
||||
f"Retry the same body with Idempotency-Key {key}; do not create a new key."
|
||||
) from error
|
||||
if not isinstance(submission, dict) or not normalize_optional_text(submission.get("operationId")):
|
||||
raise GenarrativeApiError("Generation submission response missing operationId.")
|
||||
submission.setdefault("idempotencyKey", key)
|
||||
return submission
|
||||
|
||||
def get_generation(self, operation_id: str) -> dict[str, Any]:
|
||||
result = self.request_json(
|
||||
"GET",
|
||||
f"/api/external/v1/generations/{urllib.parse.quote(operation_id, safe='')}",
|
||||
)
|
||||
if not isinstance(result, dict):
|
||||
raise GenarrativeApiError("Generation status response must be an object.")
|
||||
return result
|
||||
|
||||
def wait_for_generation(
|
||||
self,
|
||||
submission_or_operation_id: dict[str, Any] | str,
|
||||
timeout_seconds: int = GENERATION_WAIT_TIMEOUT_SECONDS,
|
||||
) -> dict[str, Any]:
|
||||
operation_id = (
|
||||
submission_or_operation_id.get("operationId")
|
||||
if isinstance(submission_or_operation_id, dict)
|
||||
else submission_or_operation_id
|
||||
)
|
||||
operation_id = normalize_optional_text(operation_id)
|
||||
if not operation_id:
|
||||
raise GenarrativeApiError("Generation operationId is required.")
|
||||
deadline = time.monotonic() + max(1, timeout_seconds)
|
||||
while True:
|
||||
try:
|
||||
job = self.get_generation(operation_id)
|
||||
except GenarrativeApiError as error:
|
||||
if any(f"HTTP {status}" in str(error) for status in (429, 502, 503, 504)):
|
||||
if time.monotonic() >= deadline:
|
||||
raise GenarrativeApiError(
|
||||
f"Generation {operation_id} is still running; keep this operationId and continue polling."
|
||||
) from error
|
||||
time.sleep(1.5)
|
||||
continue
|
||||
raise
|
||||
status = job.get("status")
|
||||
if status == "completed":
|
||||
result = job.get("result")
|
||||
if not isinstance(result, dict):
|
||||
raise GenarrativeApiError(
|
||||
f"Generation {operation_id} completed without a result payload."
|
||||
)
|
||||
return result
|
||||
if status == "failed":
|
||||
raise GenarrativeApiError(
|
||||
f"Generation {operation_id} failed: {job.get('error') or 'unknown error'}"
|
||||
)
|
||||
if time.monotonic() >= deadline:
|
||||
raise GenarrativeApiError(
|
||||
f"Generation {operation_id} is still running; keep this operationId and continue polling."
|
||||
)
|
||||
poll_after_ms = job.get("pollAfterMs", 1500)
|
||||
if not isinstance(poll_after_ms, (int, float)):
|
||||
poll_after_ms = 1500
|
||||
time.sleep(max(0.25, min(float(poll_after_ms) / 1000.0, 5.0)))
|
||||
|
||||
def submit_and_wait_generation(
|
||||
self,
|
||||
path: str,
|
||||
body: dict[str, Any],
|
||||
idempotency_key: str | None = None,
|
||||
timeout_seconds: int = GENERATION_WAIT_TIMEOUT_SECONDS,
|
||||
) -> dict[str, Any]:
|
||||
submission = self.submit_generation(path, body, idempotency_key=idempotency_key)
|
||||
return self.wait_for_generation(submission, timeout_seconds=timeout_seconds)
|
||||
|
||||
def generate_image(self, prompt: str, **fields: Any) -> Any:
|
||||
self._apply_canvas_session_fields(fields, prompt, 1024, 1024)
|
||||
prompt = self._apply_art_spec(fields, prompt)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
return self.request_json(
|
||||
"POST",
|
||||
"/api/external/v1/editor/images/generations",
|
||||
{"prompt": prompt, **fields},
|
||||
idempotency_key=idempotency_key,
|
||||
timeout=GENERATION_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
def edit_image(self, prompt: str, source_image_src: str, **fields: Any) -> Any:
|
||||
self._apply_canvas_session_fields(fields, prompt, 1024, 1024)
|
||||
prompt = self._apply_art_spec(fields, prompt)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
return self.request_json(
|
||||
"POST",
|
||||
"/api/external/v1/editor/images/edits",
|
||||
{"prompt": prompt, "sourceImageSrc": source_image_src, **fields},
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def generate_icon_spritesheet(
|
||||
self,
|
||||
reference_id: str,
|
||||
icon_descriptions: list[str],
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
reference_id = normalize_optional_text(reference_id)
|
||||
if not reference_id:
|
||||
raise GenarrativeApiError("reference_id must be a registered icon-spec resource or asset ID")
|
||||
descriptions = [item.strip() for item in icon_descriptions if item.strip()]
|
||||
if not descriptions:
|
||||
raise GenarrativeApiError("icon_descriptions must contain at least one non-empty item")
|
||||
label = fields.get("assetLabel", "图标图集")
|
||||
self._apply_canvas_session_fields(fields, label, 1024, 1024)
|
||||
fields.setdefault("screenColor", "auto")
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
{
|
||||
**fields,
|
||||
"referenceId": reference_id,
|
||||
"iconDescriptions": descriptions,
|
||||
},
|
||||
idempotency_key=idempotency_key,
|
||||
timeout=GENERATION_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
def extract_ui_assets(self, source_image_src: str, image_size: str = "1K", **fields: Any) -> Any:
|
||||
fields.pop("aspectRatio", None)
|
||||
self._apply_canvas_session_fields(fields, fields.get("spritesheetLabel", "UI 素材拆分"), 1024, 1024, "spritesheetLabel")
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
return self.request_json(
|
||||
"POST",
|
||||
"/api/external/v1/editor/ui-designs/assets/extractions",
|
||||
{"sourceImageSrc": source_image_src, "imageSize": image_size, **fields, "aspectRatio": "1:1"},
|
||||
idempotency_key=idempotency_key,
|
||||
timeout=GENERATION_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
def animate_character(
|
||||
@@ -603,15 +469,16 @@ class GenarrativeExternalClient:
|
||||
source_layer_id: str,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
self._apply_canvas_session_fields(
|
||||
session, asset_label = self._apply_canvas_session_fields(
|
||||
fields,
|
||||
fields.get("canvasTitle", "角色动画"),
|
||||
source_width,
|
||||
source_height,
|
||||
asset_label_field="assetLabel",
|
||||
asset_label_field=None,
|
||||
)
|
||||
prompt_text = self._apply_art_spec(fields, prompt_text)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
fields.pop("assetFolderId", None)
|
||||
fields.pop("assetLabel", None)
|
||||
body = {
|
||||
"sourceLayerId": source_layer_id,
|
||||
"sourceImageSrc": source_image_src,
|
||||
@@ -625,17 +492,40 @@ class GenarrativeExternalClient:
|
||||
**fields,
|
||||
"model": "seedance2.0-fast",
|
||||
}
|
||||
return self.submit_and_wait_generation(
|
||||
result = self.request_json(
|
||||
"POST",
|
||||
"/api/external/v1/editor/character-animations/generations",
|
||||
body,
|
||||
idempotency_key=idempotency_key,
|
||||
timeout=GENERATION_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
if isinstance(session, dict) and isinstance(result, dict) and not result.get("asset"):
|
||||
frames = result.get("frames")
|
||||
first_frame = frames[0] if isinstance(frames, list) and frames else None
|
||||
folder_id = normalize_optional_text(session.get("assetFolderId"))
|
||||
if isinstance(first_frame, dict) and folder_id:
|
||||
asset = self.create_asset(
|
||||
folder_id,
|
||||
asset_label,
|
||||
first_frame["imageSrc"],
|
||||
int(first_frame["width"]),
|
||||
int(first_frame["height"]),
|
||||
prompt=result.get("prompt"),
|
||||
model=result.get("model"),
|
||||
provider="ark",
|
||||
taskId=result.get("taskId"),
|
||||
assetKind="character-animation",
|
||||
generationInputs={
|
||||
"frames": frames,
|
||||
"previewVideoPath": result.get("previewVideoPath"),
|
||||
},
|
||||
)
|
||||
result["asset"] = unwrap_envelope(asset).get("asset")
|
||||
return result
|
||||
|
||||
def generate_video(self, prompt: str, **fields: Any) -> Any:
|
||||
fields.pop("mode", None)
|
||||
self._apply_canvas_session_fields(fields, prompt, 1280, 720)
|
||||
prompt = self._apply_art_spec(fields, prompt)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
body = {
|
||||
"prompt": prompt,
|
||||
"model": fields.pop("model", "seedance2.0-fast"),
|
||||
@@ -646,36 +536,31 @@ class GenarrativeExternalClient:
|
||||
**fields,
|
||||
"mode": "std",
|
||||
}
|
||||
return self.submit_and_wait_generation(
|
||||
return self.request_json(
|
||||
"POST",
|
||||
"/api/external/v1/editor/videos/generations",
|
||||
body,
|
||||
idempotency_key=idempotency_key,
|
||||
timeout=GENERATION_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
def generate_sound_effect(
|
||||
self,
|
||||
prompt: str,
|
||||
duration: float | None = None,
|
||||
loop: bool = False,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
def generate_sound_effect(self, prompt: str, duration: int, **fields: Any) -> Any:
|
||||
self._apply_canvas_session_fields(fields, prompt, 360, 120)
|
||||
prompt = self._apply_art_spec(fields, prompt)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
return self.request_json(
|
||||
"POST",
|
||||
"/api/external/v1/editor/audios/sound-effects/generations",
|
||||
{"prompt": prompt, "duration": duration, "loop": loop, **fields},
|
||||
idempotency_key=idempotency_key,
|
||||
{"prompt": prompt, "duration": duration, **fields},
|
||||
timeout=GENERATION_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
def generate_background_music(self, description: str, **fields: Any) -> Any:
|
||||
self._apply_canvas_session_fields(fields, description, 360, 120)
|
||||
description = self._apply_art_spec(fields, description)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
return self.request_json(
|
||||
"POST",
|
||||
"/api/external/v1/editor/audios/background-music/generations",
|
||||
{"gptDescriptionPrompt": description, **fields, "makeInstrumental": True},
|
||||
idempotency_key=idempotency_key,
|
||||
timeout=GENERATION_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -707,46 +592,17 @@ def _self_test() -> None:
|
||||
query: dict[str, Any] | None = None,
|
||||
auth: bool = True,
|
||||
timeout: int = DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||
headers: dict[str, str] | None = None,
|
||||
) -> Any:
|
||||
calls.append({
|
||||
"method": method,
|
||||
"path": path,
|
||||
"body": body,
|
||||
"timeout": timeout,
|
||||
"headers": headers,
|
||||
})
|
||||
generated = {
|
||||
calls.append({"method": method, "path": path, "body": body, "timeout": timeout})
|
||||
if path == "/api/external/v1/editor/assets":
|
||||
return {"asset": {"assetId": "editor-asset-demo"}}
|
||||
return {
|
||||
"taskId": "task-demo",
|
||||
"model": "seedance2.0-fast",
|
||||
"prompt": "角色呼吸",
|
||||
"previewVideoPath": "/generated/preview.mp4",
|
||||
"frames": [{"imageSrc": "/generated/frame01.png", "width": 512, "height": 768}],
|
||||
"resource": {
|
||||
"resourceId": "editor-resource-demo",
|
||||
"assetKind": "character-animation",
|
||||
"sourceResourceId": "editor-resource-preview-demo",
|
||||
"imageSequenceFrames": [
|
||||
{"imageSrc": "/generated/frame01.png", "width": 512, "height": 768},
|
||||
{"imageSrc": "/generated/frame02.png", "width": 512, "height": 768},
|
||||
],
|
||||
"imageSequenceDurationMs": 4000,
|
||||
},
|
||||
"asset": {
|
||||
"assetId": "editor-asset-demo",
|
||||
"assetKind": "character-animation",
|
||||
"imageSequenceFrames": [
|
||||
{"imageSrc": "/generated/frame01.png", "width": 512, "height": 768},
|
||||
{"imageSrc": "/generated/frame02.png", "width": 512, "height": 768},
|
||||
],
|
||||
"imageSequenceDurationMs": 4000,
|
||||
},
|
||||
"frames": [{"frameIndex": 1, "imageSrc": "/generated/frame01.png", "width": 512, "height": 768}],
|
||||
}
|
||||
if method == "POST":
|
||||
return {"operationId": "task-operation-demo", "status": "queued", "pollAfterMs": 1}
|
||||
if path == "/api/external/v1/generations/task-operation-demo":
|
||||
return {"operationId": "task-operation-demo", "status": "completed", "result": generated}
|
||||
return generated
|
||||
|
||||
client.request_json = fake_request_json # type: ignore[method-assign]
|
||||
result = client.animate_character(
|
||||
@@ -758,31 +614,11 @@ def _self_test() -> None:
|
||||
canvasSession=session,
|
||||
canvasTitle="角色呼吸动画",
|
||||
)
|
||||
assert calls[0]["timeout"] == DEFAULT_REQUEST_TIMEOUT_SECONDS
|
||||
assert calls[0]["headers"]["Idempotency-Key"]
|
||||
assert calls[0]["timeout"] == GENERATION_REQUEST_TIMEOUT_SECONDS
|
||||
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[1]["path"] == "/api/external/v1/generations/task-operation-demo"
|
||||
assert calls[1]["path"] == "/api/external/v1/editor/assets"
|
||||
assert result["asset"]["assetId"] == "editor-asset-demo"
|
||||
assert result["asset"]["assetKind"] == "character-animation"
|
||||
assert len(result["asset"]["imageSequenceFrames"]) == 2
|
||||
assert result["asset"]["imageSequenceDurationMs"] == 4000
|
||||
calls.clear()
|
||||
client.generate_icon_spritesheet(
|
||||
"editor-resource-spec",
|
||||
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
|
||||
canvasSession=session,
|
||||
assetLabel="贪吃蛇透明图集",
|
||||
referenceId="must-not-override-explicit-reference",
|
||||
iconDescriptions=["不得覆盖显式图标描述"],
|
||||
)
|
||||
assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations"
|
||||
assert calls[0]["body"]["referenceId"] == "editor-resource-spec"
|
||||
assert calls[0]["body"]["screenColor"] == "auto"
|
||||
assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上"
|
||||
assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo"
|
||||
print("self-test ok")
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: gpt-image-2-apimart
|
||||
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow with gpt-image-2-c fallback. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
|
||||
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs gpt-image-2 request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
|
||||
---
|
||||
|
||||
# gpt-image-2 VectorEngine
|
||||
|
||||
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine image path. Keep the product/price model identifier and primary provider request as `gpt-image-2`, then fall back once to `gpt-image-2-c` for eligible provider failures. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
|
||||
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine `gpt-image-2` path. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -65,9 +65,9 @@ size=1024x1024
|
||||
image=@reference.png
|
||||
```
|
||||
|
||||
In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Both paths prefer `gpt-image-2`; on an eligible upstream/model failure they retry with `gpt-image-2-c`. Do not fall back for authentication, local validation, request-budget exhaustion, uncertain send/connection failure, content-safety rejection, or a generated image URL download failure. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
|
||||
In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
|
||||
|
||||
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine image generation currently returns synchronously; do not poll APIMart task endpoints.
|
||||
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine GPT-image-2 currently returns synchronously; do not poll APIMart task endpoints.
|
||||
|
||||
## Environment
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ const skillRoot = path.resolve(__dirname, '..');
|
||||
const repoRoot = path.resolve(skillRoot, '..', '..', '..');
|
||||
const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations');
|
||||
const defaultTimeoutMs = 1000000;
|
||||
const preferredImageModel = 'gpt-image-2';
|
||||
const fallbackImageModel = 'gpt-image-2-c';
|
||||
|
||||
const prompts = [
|
||||
{
|
||||
@@ -167,25 +165,6 @@ function extractBase64Images(payload) {
|
||||
return values;
|
||||
}
|
||||
|
||||
function decodeStrictBase64Image(raw) {
|
||||
const normalized = String(raw || '').trim();
|
||||
if (
|
||||
!normalized ||
|
||||
normalized.length % 4 !== 0 ||
|
||||
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
|
||||
normalized,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const bytes = Buffer.from(normalized, 'base64');
|
||||
return bytes.length > 0 &&
|
||||
bytes.toString('base64') === normalized &&
|
||||
inferExtensionFromBytes(bytes)
|
||||
? bytes
|
||||
: null;
|
||||
}
|
||||
|
||||
function inferExtensionFromContentType(contentType) {
|
||||
const normalized = contentType.split(';')[0]?.trim().toLowerCase();
|
||||
if (normalized === 'image/png') {
|
||||
@@ -213,13 +192,7 @@ function inferExtensionFromBytes(bytes) {
|
||||
) {
|
||||
return 'webp';
|
||||
}
|
||||
if (
|
||||
bytes.subarray(0, 6).toString('ascii') === 'GIF87a' ||
|
||||
bytes.subarray(0, 6).toString('ascii') === 'GIF89a'
|
||||
) {
|
||||
return 'gif';
|
||||
}
|
||||
return null;
|
||||
return 'png';
|
||||
}
|
||||
|
||||
async function fetchJson(url, options, timeoutMs) {
|
||||
@@ -232,20 +205,9 @@ async function fetchJson(url, options, timeoutMs) {
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
const error = new Error(
|
||||
`VectorEngine ${response.status}: ${text.slice(0, 600)}`,
|
||||
);
|
||||
error.vectorEngineStatus = response.status;
|
||||
error.vectorEngineBody = text;
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
error.vectorEngineResponseParse = true;
|
||||
error.vectorEngineBody = text;
|
||||
throw error;
|
||||
throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`);
|
||||
}
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
|
||||
@@ -256,83 +218,6 @@ async function fetchJson(url, options, timeoutMs) {
|
||||
}
|
||||
}
|
||||
|
||||
function shouldFallbackImageModel(error) {
|
||||
const raw = `${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
|
||||
if (error?.vectorEngineResponseParse) {
|
||||
return !containsContentRejection(raw);
|
||||
}
|
||||
const status = Number(error?.vectorEngineStatus || 0);
|
||||
if (status === 408 || status >= 500) {
|
||||
return true;
|
||||
}
|
||||
if (status === 429) {
|
||||
return !containsContentRejection(raw);
|
||||
}
|
||||
const mentionsImageModel =
|
||||
raw.includes('model') ||
|
||||
raw.includes('模型') ||
|
||||
raw.includes(preferredImageModel) ||
|
||||
raw.includes(fallbackImageModel);
|
||||
return (
|
||||
[400, 404, 422].includes(status) &&
|
||||
mentionsImageModel &&
|
||||
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
|
||||
raw,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function containsContentRejection(raw) {
|
||||
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
|
||||
raw,
|
||||
);
|
||||
}
|
||||
|
||||
async function requestImagePayload(env, entry) {
|
||||
for (const model of [preferredImageModel, fallbackImageModel]) {
|
||||
const requestBody = {
|
||||
model,
|
||||
prompt: buildPrompt(entry),
|
||||
n: 1,
|
||||
size: '1024x1024',
|
||||
};
|
||||
try {
|
||||
const payload = await fetchJson(
|
||||
buildVectorEngineImagesGenerationUrl(env.baseUrl),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.apiKey}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
},
|
||||
env.timeoutMs,
|
||||
);
|
||||
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
|
||||
if (
|
||||
extractImageUrls(payload)[0] ||
|
||||
base64Image
|
||||
) {
|
||||
return payload;
|
||||
}
|
||||
const error = new Error(`VectorEngine returned no image for ${entry.id}`);
|
||||
error.vectorEngineResponseParse = true;
|
||||
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
|
||||
throw error;
|
||||
} catch (error) {
|
||||
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
|
||||
throw error;
|
||||
}
|
||||
console.warn(
|
||||
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new Error(`VectorEngine returned no image for ${entry.id}`);
|
||||
}
|
||||
|
||||
async function downloadUrl(url, timeoutMs) {
|
||||
const abortController = new AbortController();
|
||||
const timer = setTimeout(() => abortController.abort(), timeoutMs);
|
||||
@@ -359,7 +244,25 @@ async function downloadUrl(url, timeoutMs) {
|
||||
}
|
||||
|
||||
async function generateOne(env, entry, outDir) {
|
||||
const payload = await requestImagePayload(env, entry);
|
||||
const requestBody = {
|
||||
model: 'gpt-image-2',
|
||||
prompt: buildPrompt(entry),
|
||||
n: 1,
|
||||
size: '1024x1024',
|
||||
};
|
||||
const payload = await fetchJson(
|
||||
buildVectorEngineImagesGenerationUrl(env.baseUrl),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.apiKey}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
},
|
||||
env.timeoutMs,
|
||||
);
|
||||
|
||||
const urls = extractImageUrls(payload);
|
||||
const b64Images = extractBase64Images(payload);
|
||||
@@ -368,10 +271,7 @@ async function generateOne(env, entry, outDir) {
|
||||
if (urls[0]) {
|
||||
image = await downloadUrl(urls[0], env.timeoutMs);
|
||||
} else if (b64Images[0]) {
|
||||
const bytes = decodeStrictBase64Image(b64Images[0]);
|
||||
if (!bytes) {
|
||||
throw new Error(`VectorEngine returned invalid base64 image for ${entry.id}`);
|
||||
}
|
||||
const bytes = Buffer.from(b64Images[0], 'base64');
|
||||
image = {
|
||||
bytes,
|
||||
extension: inferExtensionFromBytes(bytes),
|
||||
@@ -404,9 +304,8 @@ if (dryRun) {
|
||||
requests: selectedPrompts.map((entry) => ({
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
fallbackModel: fallbackImageModel,
|
||||
body: {
|
||||
model: preferredImageModel,
|
||||
model: 'gpt-image-2',
|
||||
prompt: buildPrompt(entry),
|
||||
n: 1,
|
||||
size: '1024x1024',
|
||||
|
||||
@@ -14,8 +14,6 @@ const promptsPath = path.join(
|
||||
);
|
||||
const defaultOutDir = path.join(repoRoot, 'public', 'puzzle-creation-templates');
|
||||
const defaultTimeoutMs = 1000000;
|
||||
const preferredImageModel = 'gpt-image-2';
|
||||
const fallbackImageModel = 'gpt-image-2-c';
|
||||
|
||||
const args = new Map();
|
||||
for (let index = 2; index < process.argv.length; index += 1) {
|
||||
@@ -133,25 +131,6 @@ function extractBase64Images(payload) {
|
||||
return values;
|
||||
}
|
||||
|
||||
function decodeStrictBase64Image(raw) {
|
||||
const normalized = String(raw || '').trim();
|
||||
if (
|
||||
!normalized ||
|
||||
normalized.length % 4 !== 0 ||
|
||||
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
|
||||
normalized,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const bytes = Buffer.from(normalized, 'base64');
|
||||
return bytes.length > 0 &&
|
||||
bytes.toString('base64') === normalized &&
|
||||
inferExtensionFromBytes(bytes)
|
||||
? bytes
|
||||
: null;
|
||||
}
|
||||
|
||||
function inferExtensionFromContentType(contentType) {
|
||||
const normalized = contentType.split(';')[0]?.trim().toLowerCase();
|
||||
if (normalized === 'image/png') {
|
||||
@@ -179,13 +158,7 @@ function inferExtensionFromBytes(bytes) {
|
||||
) {
|
||||
return 'webp';
|
||||
}
|
||||
if (
|
||||
bytes.subarray(0, 6).toString('ascii') === 'GIF87a' ||
|
||||
bytes.subarray(0, 6).toString('ascii') === 'GIF89a'
|
||||
) {
|
||||
return 'gif';
|
||||
}
|
||||
return null;
|
||||
return 'png';
|
||||
}
|
||||
|
||||
async function fetchJson(url, options, timeoutMs) {
|
||||
@@ -198,20 +171,9 @@ async function fetchJson(url, options, timeoutMs) {
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
const error = new Error(
|
||||
`VectorEngine ${response.status}: ${text.slice(0, 600)}`,
|
||||
);
|
||||
error.vectorEngineStatus = response.status;
|
||||
error.vectorEngineBody = text;
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
error.vectorEngineResponseParse = true;
|
||||
error.vectorEngineBody = text;
|
||||
throw error;
|
||||
throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`);
|
||||
}
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
|
||||
@@ -222,83 +184,6 @@ async function fetchJson(url, options, timeoutMs) {
|
||||
}
|
||||
}
|
||||
|
||||
function shouldFallbackImageModel(error) {
|
||||
const raw = `${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
|
||||
if (error?.vectorEngineResponseParse) {
|
||||
return !containsContentRejection(raw);
|
||||
}
|
||||
const status = Number(error?.vectorEngineStatus || 0);
|
||||
if (status === 408 || status >= 500) {
|
||||
return true;
|
||||
}
|
||||
if (status === 429) {
|
||||
return !containsContentRejection(raw);
|
||||
}
|
||||
const mentionsImageModel =
|
||||
raw.includes('model') ||
|
||||
raw.includes('模型') ||
|
||||
raw.includes(preferredImageModel) ||
|
||||
raw.includes(fallbackImageModel);
|
||||
return (
|
||||
[400, 404, 422].includes(status) &&
|
||||
mentionsImageModel &&
|
||||
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
|
||||
raw,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function containsContentRejection(raw) {
|
||||
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
|
||||
raw,
|
||||
);
|
||||
}
|
||||
|
||||
async function requestImagePayload(env, template) {
|
||||
for (const model of [preferredImageModel, fallbackImageModel]) {
|
||||
const requestBody = {
|
||||
model,
|
||||
prompt: buildPrompt(template),
|
||||
n: 1,
|
||||
size: '1024x1024',
|
||||
};
|
||||
try {
|
||||
const payload = await fetchJson(
|
||||
buildVectorEngineImagesGenerationUrl(env.baseUrl),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.apiKey}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
},
|
||||
env.timeoutMs,
|
||||
);
|
||||
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
|
||||
if (
|
||||
extractImageUrls(payload)[0] ||
|
||||
base64Image
|
||||
) {
|
||||
return payload;
|
||||
}
|
||||
const error = new Error(`VectorEngine returned no image for ${template.id}`);
|
||||
error.vectorEngineResponseParse = true;
|
||||
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
|
||||
throw error;
|
||||
} catch (error) {
|
||||
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
|
||||
throw error;
|
||||
}
|
||||
console.warn(
|
||||
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new Error(`VectorEngine returned no image for ${template.id}`);
|
||||
}
|
||||
|
||||
async function downloadUrl(url, timeoutMs) {
|
||||
const abortController = new AbortController();
|
||||
const timer = setTimeout(() => abortController.abort(), timeoutMs);
|
||||
@@ -325,7 +210,25 @@ async function downloadUrl(url, timeoutMs) {
|
||||
}
|
||||
|
||||
async function generateOne(env, template, outDir) {
|
||||
const payload = await requestImagePayload(env, template);
|
||||
const requestBody = {
|
||||
model: 'gpt-image-2',
|
||||
prompt: buildPrompt(template),
|
||||
n: 1,
|
||||
size: '1024x1024',
|
||||
};
|
||||
const payload = await fetchJson(
|
||||
buildVectorEngineImagesGenerationUrl(env.baseUrl),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.apiKey}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
},
|
||||
env.timeoutMs,
|
||||
);
|
||||
|
||||
const urls = extractImageUrls(payload);
|
||||
const b64Images = extractBase64Images(payload);
|
||||
@@ -334,10 +237,7 @@ async function generateOne(env, template, outDir) {
|
||||
if (urls[0]) {
|
||||
image = await downloadUrl(urls[0], env.timeoutMs);
|
||||
} else if (b64Images[0]) {
|
||||
const bytes = decodeStrictBase64Image(b64Images[0]);
|
||||
if (!bytes) {
|
||||
throw new Error(`VectorEngine returned invalid base64 image for ${template.id}`);
|
||||
}
|
||||
const bytes = Buffer.from(b64Images[0], 'base64');
|
||||
image = {
|
||||
bytes,
|
||||
extension: inferExtensionFromBytes(bytes),
|
||||
@@ -374,9 +274,8 @@ if (dryRun) {
|
||||
requests: selectedTemplates.map((template) => ({
|
||||
id: template.id,
|
||||
title: template.title,
|
||||
fallbackModel: fallbackImageModel,
|
||||
body: {
|
||||
model: preferredImageModel,
|
||||
model: 'gpt-image-2',
|
||||
prompt: buildPrompt(template),
|
||||
n: 1,
|
||||
size: '1024x1024',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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.
|
||||
description: SpacetimeDB 2.5 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
|
||||
@@ -61,38 +61,13 @@ 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.
|
||||
# 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
|
||||
@@ -127,7 +102,7 @@ curl -fsS http://127.0.0.1:3101/v1/ping
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--server`, `-s` | Target server nickname, host, or URL |
|
||||
| `--yes`, `-y` | Non-interactive prompt skipping; in 2.6+ use scoped values |
|
||||
| `--yes`, `-y` | Non-interactive prompt skipping; in 2.5 prefer 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 |
|
||||
@@ -171,8 +146,6 @@ pid="$(systemctl show spacetimedb.service -p MainPID --value)"
|
||||
|
||||
## 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.
|
||||
- Procedure calls are stable in 2.5; module HTTP handlers/webhooks, unstable view features, and RLS remain behind unstable gates per release notes.
|
||||
- 2.5 fixes `publish --delete-data` config fallback so `spacetime.json` can provide the database name.
|
||||
- Genarrative scripts should pass `--server` or `--server-url` explicitly instead of relying on CLI defaults.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: spacetimedb-concepts
|
||||
description: Understand SpacetimeDB 2.7 architecture, reducer/procedure/table/view semantics, schema evolution, subscriptions, identity, and Genarrative-specific backend boundaries. Use when designing or reviewing SpacetimeDB-backed features.
|
||||
description: Understand SpacetimeDB 2.5 architecture, reducer/procedure/table/view semantics, schema evolution, subscriptions, identity, and Genarrative-specific backend boundaries. Use when designing or reviewing SpacetimeDB-backed features.
|
||||
---
|
||||
|
||||
# SpacetimeDB Core Concepts
|
||||
@@ -20,7 +20,7 @@ SpacetimeDB is a relational database that also executes application logic in upl
|
||||
|
||||
1. **Reducers are transactional**: they do not return data to callers. Read through subscriptions, read models, views, or BFF endpoints.
|
||||
2. **Reducers are deterministic**: no filesystem, network, wall-clock, or external RNG. Use `ctx.timestamp`, `ctx.rng()` / `ctx.random()`, and tables.
|
||||
3. **Procedures are stable in 2.7**: they can use explicit transactions and outgoing HTTP via `ctx.http`.
|
||||
3. **Procedures are stable in 2.5**: they can use explicit transactions and outgoing HTTP via `ctx.http`.
|
||||
4. **Identity comes from context**: use `ctx.sender()` or language equivalent for authorization. Never trust identity passed as an argument.
|
||||
5. **Auto-increment IDs are not ordering guarantees**: gaps are normal. Use timestamps or explicit sequence columns for ordering.
|
||||
6. **Schema changes need migration discipline**: existing Genarrative table fields must be appended with defaults; update migration code, table catalog, generated bindings, and run `npm run check:spacetime-schema`.
|
||||
@@ -44,25 +44,25 @@ Reducers are deterministic transactional functions. They are the primary client-
|
||||
|
||||
## Procedures
|
||||
|
||||
Procedures are stable in 2.7. They can be scheduled, can open explicit transactions with `with_tx` / `try_with_tx`, and can use outgoing HTTP (`ctx.http`).
|
||||
Procedures are stable in 2.5. They can be scheduled, can open explicit transactions with `with_tx` / `try_with_tx`, and can use outgoing HTTP (`ctx.http`).
|
||||
|
||||
Genarrative default: keep external provider protocols in `platform-*` and orchestration in `api-server` unless a task explicitly moves a workflow into a module procedure.
|
||||
|
||||
Module HTTP handlers/webhooks and RLS `client_visibility_filter` remain subject to their documented gates in 2.7.
|
||||
Module HTTP handlers/webhooks, unstable view features, and RLS `client_visibility_filter` remain gated behind unstable according to the 2.5 release notes.
|
||||
|
||||
## Views
|
||||
|
||||
Views expose computed read-only data. SpacetimeDB 2.7 supports primary keys on procedural views in Rust, TypeScript, C#, and C++. Clients can receive update events when subscribed to such views with primary keys. Ensure the view never returns duplicate primary keys, because that can fail view refresh and roll back the triggering transaction.
|
||||
Views expose computed read-only data. In 2.4.1 Rust and TypeScript gained primary key support for procedural views; in 2.5 C# gained the same. Clients can receive `OnUpdate` events when subscribed to such views with primary keys. Ensure the view never returns duplicate primary keys, because that can fail view refresh and roll back the triggering transaction.
|
||||
|
||||
## Event Tables
|
||||
|
||||
Event tables broadcast reducer/procedure-specific facts to subscribers and must be subscribed explicitly. They are excluded from `subscribe_to_all_tables()`.
|
||||
|
||||
Since 2.6, event tables support broader layout-altering automigrations, including column removal, reordering, and type changes that regular tables reject. This relaxed migration behavior is for event-only tables, not persistent tables.
|
||||
2.5 adds broader layout-altering automigrations for event tables, including column removal, reordering, and type changes that regular tables reject. This relaxed migration behavior is for event-only tables, not persistent tables.
|
||||
|
||||
Event-table primary keys and constraints are transaction-scoped. They can reject duplicate event rows within one transaction, but event rows are not retained in client cache, so clients observe event tables through insert callbacks only. Do not design Genarrative event tables around `OnUpdate` / `on_update` / `onUpdate`; use a persistent table or a primary-keyed procedural view when update callbacks are required.
|
||||
|
||||
Official 2.4.1 through 2.7 release notes document primary-key-backed update callbacks for procedural views, not event tables.
|
||||
Official 2.4.1/2.5 release notes document primary-key-backed update callbacks for procedural views, not event tables.
|
||||
|
||||
## Subscriptions
|
||||
|
||||
@@ -78,18 +78,7 @@ Best practices:
|
||||
- Avoid overlapping queries that duplicate row delivery.
|
||||
- Use indexes for subscribed filters.
|
||||
|
||||
## Standalone MCP
|
||||
|
||||
SpacetimeDB 2.7 standalone exposes `POST /v1/database/{name_or_identity}/mcp`
|
||||
using MCP JSON-RPC protocol `2025-06-18`. Its tools are `ping`, `get_schema`,
|
||||
`sql`, and `call`; SQL and reducer calls run with the authenticated caller's
|
||||
identity. In Genarrative this is an operator/developer integration surface, not
|
||||
a replacement for `api-server` BFF routes, `spacetime-client` facades, or public
|
||||
read models. Upgrade smoke should use an isolated local database and restrict
|
||||
itself to `initialize`, `tools/list`, `ping`, and `get_schema` unless writes are
|
||||
explicitly intended.
|
||||
|
||||
## 2.2.0 to 2.7.0 Delta
|
||||
## 2.2.0 to 2.5.0 Delta
|
||||
|
||||
Genarrative introduced SpacetimeDB around 2.2.0. Important changes since then:
|
||||
|
||||
@@ -98,9 +87,6 @@ Genarrative introduced SpacetimeDB around 2.2.0. Important changes since then:
|
||||
- **2.4.0**: unstable module HTTP handlers/webhooks, faster synchronous WASM reducer runtime, commitlog resume truncation fix for silent data loss risk, better commitlog decode context, V8 heap metrics for procedure workers, JS execution-time billing regression reverted.
|
||||
- **2.4.1**: Rust and TypeScript procedural views can declare primary keys, enabling `OnUpdate` events for subscribed views; fixed index schema from ST tables.
|
||||
- **2.5.0**: procedures are stable, C# procedural views gain primary keys, event tables allow broader layout-altering automigrations, BTreeSet storage makes row insertion deterministic and avoids accidentally quadratic bulk insert behavior, `wasm_memory_bytes` billing metric semantics changed, template version constraints unified, `publish --delete-data` config fallback fixed, CLI `call` accepts hex Identity arguments.
|
||||
- **2.6.0**: procedural-view primary keys are available across Rust, TypeScript, and C#, commitlog gains `max_segment_size` / `write_buffer_size` / `preallocate_segments`, the default write buffer increases for throughput, event-table automigrations improve, and CLI binary distribution expands.
|
||||
- **2.6.1**: procedure contexts again receive the caller `Identity` and `ConnectionId`; generated TypeScript `Option<T>` fields use optional keys; `spacetime init --template` lists available templates when no template argument is supplied.
|
||||
- **2.7.0**: existing tables can add unique or primary-key constraints when current data satisfies them; standalone exposes an authenticated database MCP endpoint; Rust adds context-capability and table-accessor traits; `spacetime sql --format json` and database locking are available; view cleanup, backing-table migration, connection metrics, and memory metrics improve. Official current release assets use the `v2.7.0-hotfix3` tag while binaries report `2.7.0`.
|
||||
|
||||
## Debugging Checklist
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: spacetimedb-rust
|
||||
description: Develop SpacetimeDB 2.7 server modules in Rust for Genarrative. Use when writing or reviewing tables, reducers, procedures, views, migrations, row mappers, schema changes, and module logic.
|
||||
description: Develop SpacetimeDB 2.5 server modules in Rust for Genarrative. Use when writing or reviewing tables, reducers, procedures, views, migrations, row mappers, schema changes, and module logic.
|
||||
---
|
||||
|
||||
# SpacetimeDB Rust Module Development
|
||||
@@ -28,7 +28,7 @@ ctx.db.player.find(id) // Use ctx.db.player().id().find(&id)
|
||||
ctx.sender // Use ctx.sender()
|
||||
ctx.db.user().name().update(..) // Update by primary key only
|
||||
|
||||
spacetimedb = { version = "...", features = ["unstable"] } // Not needed for procedures since 2.5
|
||||
spacetimedb = { version = "...", features = ["unstable"] } // Not needed for procedures in 2.5
|
||||
```
|
||||
|
||||
## Required Patterns
|
||||
@@ -181,11 +181,11 @@ fn deal_damage(ctx: &ReducerContext, target: Identity, amount: u32) {
|
||||
|
||||
Event tables must be subscribed explicitly and are excluded from `subscribe_to_all_tables()`.
|
||||
|
||||
Since 2.6, event tables support broader layout-altering automigrations than regular tables, including column removal, reordering, and type changes. This relaxed migration policy does not apply to persistent tables.
|
||||
In 2.5, event tables support broader layout-altering automigrations than regular tables, including column removal, reordering, and type changes. This relaxed migration policy does not apply to persistent tables.
|
||||
|
||||
Event-table primary keys and constraints are enforced only within the current transaction. They do not make event rows persistent, and client SDKs expose event tables as insert-only event streams. Do not rely on `OnUpdate` / `on_update` / `onUpdate` for event tables; use a persistent table or a primary-keyed procedural view when update callbacks are required.
|
||||
|
||||
Official 2.4.1 through 2.7 release notes tie primary-key-backed update callbacks to procedural views, not event tables.
|
||||
Official 2.4.1/2.5 release notes tie primary-key-backed update callbacks to procedural views, not event tables.
|
||||
|
||||
## Views
|
||||
|
||||
@@ -228,7 +228,7 @@ For scheduled reducers, check `ctx.sender_auth().is_internal()` when the reducer
|
||||
|
||||
## Procedures
|
||||
|
||||
Procedures remain stable in 2.7 and no longer require the `unstable` feature.
|
||||
Procedures are stable in 2.5 and no longer require the `unstable` feature.
|
||||
|
||||
```rust
|
||||
use spacetimedb::{procedure, ProcedureContext};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 微信小程序 web-view 登录配置。
|
||||
# 留空时不覆盖已有微信网页 OAuth 配置;正式联调时再填小程序 AppID / AppSecret。
|
||||
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=false
|
||||
|
||||
WECHAT_MINI_PROGRAM_APP_ID=""
|
||||
WECHAT_MINI_PROGRAM_APP_SECRET=""
|
||||
WECHAT_JS_CODE_SESSION_ENDPOINT=""
|
||||
|
||||
+18
-35
@@ -1,12 +1,11 @@
|
||||
# Server-side OpenAI-compatible LLM endpoint base URL.
|
||||
LLM_BASE_URL="https://api.vectorengine.cn/v1"
|
||||
LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
|
||||
|
||||
# Server-side API key used by the local Vite proxy.
|
||||
# Recommended: set `LLM_API_KEY` locally, or use `VECTOR_ENGINE_API_KEY`
|
||||
# through the Rust api-server proxy.
|
||||
# Recommended: set `LLM_API_KEY` or `ARK_API_KEY`.
|
||||
# Legacy compatibility: `VITE_LLM_API_KEY` is still supported by the proxy,
|
||||
# but it should not be relied on by browser code.
|
||||
LLM_API_KEY=""
|
||||
LLM_API_KEY="YOUR_API_KEY"
|
||||
|
||||
# Optional frontend override for the local proxy path.
|
||||
VITE_LLM_PROXY_BASE_URL="/api/llm"
|
||||
@@ -104,8 +103,6 @@ WECHAT_ACCESS_TOKEN_ENDPOINT="https://api.weixin.qq.com/sns/oauth2/access_token"
|
||||
WECHAT_USER_INFO_ENDPOINT="https://api.weixin.qq.com/sns/userinfo"
|
||||
WECHAT_JS_CODE_SESSION_ENDPOINT="https://api.weixin.qq.com/sns/jscode2session"
|
||||
WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT="https://api.weixin.qq.com/cgi-bin/stable_token"
|
||||
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_QUERY_ORDER_ENDPOINT="https://api.weixin.qq.com/xpay/query_order"
|
||||
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_NOTIFY_PROVIDE_GOODS_ENDPOINT="https://api.weixin.qq.com/xpay/notify_provide_goods"
|
||||
WECHAT_PHONE_NUMBER_ENDPOINT="https://api.weixin.qq.com/wxa/business/getuserphonenumber"
|
||||
WECHAT_STATE_TTL_MINUTES="15"
|
||||
WECHAT_MOCK_USER_ID="wx-mock-user"
|
||||
@@ -114,27 +111,30 @@ WECHAT_MOCK_DISPLAY_NAME="微信旅人"
|
||||
WECHAT_MOCK_AVATAR_URL=""
|
||||
WECHAT_MINIPROGRAM_MESSAGE_TOKEN=""
|
||||
WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY=""
|
||||
WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED="true"
|
||||
WECHAT_MINIPROGRAM_GENERATION_RESULT_TEMPLATE_ID="m5z7BkkBhJGbcH0cdDeHaeRU2tViDEguP38XdrRRCdU"
|
||||
WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE="formal"
|
||||
|
||||
# Model name for chat completions.
|
||||
VITE_LLM_MODEL="gpt-5.4-mini"
|
||||
GENARRATIVE_LLM_PROVIDER="openai-compatible"
|
||||
GENARRATIVE_LLM_BASE_URL="https://api.vectorengine.cn/v1"
|
||||
GENARRATIVE_LLM_API_KEY=""
|
||||
GENARRATIVE_LLM_MODEL="gpt-5.4-mini"
|
||||
VITE_LLM_MODEL="doubao-1-5-pro-32k-character-250715"
|
||||
|
||||
# Optional: enable upstream web search for RPG story text generation.
|
||||
RPG_LLM_WEB_SEARCH_ENABLED="true"
|
||||
|
||||
# Server-side DashScope endpoint and API key used by the local scene-image proxy.
|
||||
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1"
|
||||
DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY"
|
||||
|
||||
# VectorEngine LLM and GPT-image-2 / Gemini image generation config.
|
||||
VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn"
|
||||
# APIMart Responses config for creative-agent text/multimodal understanding.
|
||||
APIMART_BASE_URL="https://api.apimart.ai/v1"
|
||||
APIMART_API_KEY="YOUR_APIMART_API_KEY"
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS="180000"
|
||||
|
||||
# VectorEngine GPT-image-2 / Gemini image generation config.
|
||||
VECTOR_ENGINE_BASE_URL="https://api.vectorengine.ai"
|
||||
VECTOR_ENGINE_API_KEY=""
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS="1000000"
|
||||
|
||||
# ElevenLabs editor sound-effect generation is server-side only.
|
||||
ELEVENLABS_BASE_URL="https://api.elevenlabs.io"
|
||||
ELEVENLABS_API_KEY=""
|
||||
ELEVENLABS_REQUEST_TIMEOUT_MS="180000"
|
||||
|
||||
# 阿里云 OSS 配置。
|
||||
# Rust `server-rs` 的 `api-server` 会优先从 `.env` / `.env.local` 读取这些变量,
|
||||
# 用于签发浏览器 PostObject 直传票据,并保持 `/generated-*` 旧路径习惯。
|
||||
@@ -148,19 +148,6 @@ ALIYUN_OSS_POST_EXPIRE_SECONDS="600"
|
||||
ALIYUN_OSS_POST_MAX_SIZE_BYTES="20971520"
|
||||
ALIYUN_OSS_SUCCESS_ACTION_STATUS="200"
|
||||
|
||||
# BgFilter 受限资源 worker。父 api-server / external-generation-worker 与唯一的
|
||||
# `GENARRATIVE_PROCESS_ROLE=bgfilter-worker` 进程必须使用同一个内部 Token。
|
||||
# `npm run dev` 与 `npm run dev:api-server` 都会自动带起并验活唯一 worker,不要再开第二个终端重复启动。
|
||||
# 只有需要脱离父 API 单独验证 worker 时才运行 `npm run dev:bgfilter-worker`;不要让 `all` 角色兼任它。
|
||||
GENARRATIVE_BGFILTER_WORKER_HOST="127.0.0.1"
|
||||
GENARRATIVE_BGFILTER_WORKER_PORT="8083"
|
||||
GENARRATIVE_BGFILTER_WORKER_BASE_URL="http://127.0.0.1:8083"
|
||||
GENARRATIVE_BGFILTER_INTERNAL_TOKEN="CHANGE_ME_FOR_LOCAL"
|
||||
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY="16"
|
||||
GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS="5000"
|
||||
GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS="2048"
|
||||
GENARRATIVE_BGFILTER_WORKER_CONNECT_TIMEOUT_MS="2000"
|
||||
|
||||
# SpacetimeDB 数据目录备份到 OSS。备份 bucket 可与资源 bucket 分离;未设置时脚本回退使用 ALIYUN_OSS_BUCKET。
|
||||
GENARRATIVE_DATABASE_BACKUP_DATA_DIR=""
|
||||
GENARRATIVE_DATABASE_BACKUP_WORK_DIR=""
|
||||
@@ -212,10 +199,6 @@ VITE_LLM_DEBUG_LOG="false"
|
||||
# Set to "true" to expose local diagnostic panels, or "false" to hide them.
|
||||
VITE_DEBUG_MODE=""
|
||||
|
||||
# Optional: show the image editor right-side Agent entry at runtime.
|
||||
# This is read by api-server and exposed through /api/runtime/frontend-config.
|
||||
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR="false"
|
||||
|
||||
# Optional: official VikingDB credentials for regenerating build-tag similarities
|
||||
# with the Python embedding script. The script auto-loads `.env.local` and uses
|
||||
# the fixed `bge-large-zh` embedding model.
|
||||
|
||||
@@ -29,7 +29,6 @@ GENARRATIVE_LLM_PROVIDER="ark"
|
||||
GENARRATIVE_LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
|
||||
GENARRATIVE_LLM_API_KEY="eb750614-e0b5-402a-bfea-4224862d251e"
|
||||
GENARRATIVE_LLM_MODEL="doubao-1-5-pro-32k-character-250715"
|
||||
GENARRATIVE_EDITOR_BGFILTER_BASE_URL="https://u1082648-b442-cd409e05.westx.seetacloud.com:8443"
|
||||
APIMART_BASE_URL="https://api.apimart.ai/v1"
|
||||
APIMART_API_KEY=""
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
@@ -37,15 +36,12 @@ DASHSCOPE_SCENE_IMAGE_MODEL="wan2.2-t2i-flash"
|
||||
DASHSCOPE_REFERENCE_IMAGE_MODEL="qwen-image-2.0"
|
||||
DASHSCOPE_COVER_IMAGE_MODEL="wan2.2-t2i-flash"
|
||||
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
|
||||
|
||||
# 启用服务端大模型调试日志(记录所有输入输出)
|
||||
LLM_DEBUG_LOG="true"
|
||||
|
||||
# 注意:不要在客户端启用调试日志,避免敏感数据泄露
|
||||
# VITE_LLM_DEBUG_LOG="false"
|
||||
|
||||
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=true
|
||||
|
||||
ALIYUN_OSS_BUCKET="xushi-dev"
|
||||
ALIYUN_OSS_REGION="oss-cn-beijing"
|
||||
ALIYUN_OSS_ENDPOINT="oss-cn-beijing.aliyuncs.com"
|
||||
|
||||
+8
-340
@@ -24,106 +24,6 @@ module.exports = {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/scripts/**/*.{ts,js,mjs,cjs}'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.test.{ts,tsx,js,mjs,cjs}'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'unused-imports/no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['miniprogram/**/*.js'],
|
||||
globals: {
|
||||
App: 'readonly',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'apps/admin-web/src/pages/*.tsx',
|
||||
'src/components/platform-entry/PlatformActiveMobileWelcomeDialog.tsx',
|
||||
'src/components/platform-entry/PlatformMobileHomeWelcomeDialog.tsx',
|
||||
],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/components/platform-entry/PlatformEntryFlowShellImpl.tsx'],
|
||||
rules: {
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'src/active-main.tsx',
|
||||
'src/ActiveApp.tsx',
|
||||
'src/AuthenticatedApp.tsx',
|
||||
'src/hooks/useGameSettings.ts',
|
||||
'src/routing/activeApp*.ts',
|
||||
'src/routing/activeApp*.tsx',
|
||||
'src/components/auth/**/*.{ts,tsx}',
|
||||
'src/components/common/**/*.{ts,tsx}',
|
||||
'src/components/creation-home/**/*.{ts,tsx}',
|
||||
'src/components/image-editor/**/*.{ts,tsx}',
|
||||
'src/components/project/**/*.{ts,tsx}',
|
||||
'src/components/platform-entry/PlatformActiveProfileView*.tsx',
|
||||
'src/components/platform-entry/PlatformActiveMobileWelcomeDialog*.tsx',
|
||||
'src/components/platform-entry/PlatformEntryActiveFlowShell*.tsx',
|
||||
'src/components/platform-entry/PlatformEntryFlowShell.tsx',
|
||||
'src/components/platform-entry/PlatformProfileApiKeysModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileModalShell.tsx',
|
||||
'src/components/platform-entry/PlatformProfilePrimitives.tsx',
|
||||
'src/components/platform-entry/PlatformProfileRechargeModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileReferralModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx',
|
||||
'src/components/platform-entry/PlatformRechargePaymentStatusDialogs.tsx',
|
||||
'src/components/platform-entry/platformActiveProfileModel.ts',
|
||||
'src/components/platform-entry/platformEntryActiveTypes.ts',
|
||||
'src/components/platform-entry/platformProfileFundsModel.ts',
|
||||
'src/components/platform-entry/platformProfileHostClipboard.ts',
|
||||
'src/components/platform-entry/usePlatformProfileCenterController.ts',
|
||||
'src/services/image-editor/**/*.{ts,tsx}',
|
||||
'src/services/platform-entry/**/*.{ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
'**/components/rpg-entry/**',
|
||||
'**/components/*-creation/**',
|
||||
'**/components/*-result/**',
|
||||
'**/components/*-runtime/**',
|
||||
'**/components/creation-agent/**',
|
||||
'**/components/creative-agent/**',
|
||||
'**/components/custom-world-*/**',
|
||||
'**/components/unified-creation/**',
|
||||
'**/services/rpg-entry/**',
|
||||
'**/services/rpg-runtime/**',
|
||||
'**/services/*-creation/**',
|
||||
'**/services/*-runtime/**',
|
||||
'**/services/*-works/**',
|
||||
'**/services/creation-agent/**',
|
||||
'**/services/creative-agent/**',
|
||||
'**/services/puzzle-*/**',
|
||||
'**/services/storyEngine/**',
|
||||
],
|
||||
message: '现役前端不得重新导入已退役的创作模板模块。',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/components/game-canvas/**/*.tsx'],
|
||||
rules: {
|
||||
@@ -137,6 +37,12 @@ module.exports = {
|
||||
'simple-import-sort/exports': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/components/match3d-runtime/Match3DPhysicsBoard.tsx'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
@@ -155,242 +61,7 @@ module.exports = {
|
||||
'dist',
|
||||
'dist_check',
|
||||
'dist_check_monster_position',
|
||||
'coverage',
|
||||
'build',
|
||||
'output',
|
||||
'retired/**',
|
||||
'node_modules',
|
||||
'server-rs/target',
|
||||
'server-rs/target-*',
|
||||
'apps/desktop-shell/src-tauri/target',
|
||||
'target',
|
||||
'src/main.tsx',
|
||||
'src/App.tsx',
|
||||
'src/routing/appPageRoutes.ts',
|
||||
'src/routing/appPageRoutes.test.ts',
|
||||
'src/routing/appRoutes.tsx',
|
||||
'src/routing/appRoutes.test.ts',
|
||||
'src/services/appTitle.ts',
|
||||
'src/services/appTitle.test.ts',
|
||||
'src/components/CharacterAnimator.tsx',
|
||||
'src/components/*.ts',
|
||||
'src/components/*.tsx',
|
||||
'!src/components/ResolvedAssetAudio.tsx',
|
||||
'!src/components/ResolvedAssetImage.tsx',
|
||||
'!src/components/ResolvedAssetVideo.tsx',
|
||||
'src/hooks/*.ts',
|
||||
'src/hooks/*.tsx',
|
||||
'!src/hooks/useGameSettings.ts',
|
||||
'!src/hooks/useHostNavigationCanGoBack.ts',
|
||||
'!src/hooks/useHostNavigationCanGoBack.test.tsx',
|
||||
'!src/hooks/useResolvedAssetReadUrl.ts',
|
||||
'!src/hooks/useResolvedAssetReadUrl.test.tsx',
|
||||
'src/persistence/*.ts',
|
||||
'src/persistence/*.tsx',
|
||||
'!src/persistence/gameSettingsStorage.ts',
|
||||
'!src/persistence/gameSettingsStorage.test.ts',
|
||||
'!src/persistence/storage.ts',
|
||||
'!src/persistence/storage.test.ts',
|
||||
'src/routing/*.ts',
|
||||
'src/routing/*.tsx',
|
||||
'!src/routing/activeAppPageRoutes.ts',
|
||||
'!src/routing/activeAppPageRoutes.test.ts',
|
||||
'!src/routing/activeAppRoutes.tsx',
|
||||
'!src/routing/activeAppRoutes.test.ts',
|
||||
'src/services/*.ts',
|
||||
'src/services/*.tsx',
|
||||
'!src/services/activeAppTitle.ts',
|
||||
'!src/services/activeAppTitle.test.ts',
|
||||
'!src/services/apiClient.ts',
|
||||
'!src/services/apiClient.test.ts',
|
||||
'!src/services/assetReadUrlService.ts',
|
||||
'!src/services/assetReadUrlService.test.ts',
|
||||
'!src/services/authService.ts',
|
||||
'!src/services/authService.test.ts',
|
||||
'!src/services/clipboard.ts',
|
||||
'!src/services/clipboard.test.ts',
|
||||
'!src/services/frontendRuntimeConfigService.ts',
|
||||
'!src/services/frontendRuntimeConfigService.test.ts',
|
||||
'!src/services/sseStream.ts',
|
||||
'!src/services/sseStream.test.ts',
|
||||
'src/AdventurePanel.tsx',
|
||||
'src/AdventureEntityModal.tsx',
|
||||
'src/App.tsx',
|
||||
'src/App.test.tsx',
|
||||
'src/CharacterCreation.tsx',
|
||||
'src/RpgRuntimeApp.tsx',
|
||||
'src/routing/appPageRoutes.ts',
|
||||
'src/routing/appPageRoutes.test.ts',
|
||||
'src/routing/appRoutes.tsx',
|
||||
'src/routing/appRoutes.test.ts',
|
||||
'src/*PlaygroundApp.tsx',
|
||||
'src/ChildMotionDemoApp.tsx',
|
||||
'src/Match3DPlaygroundApp.tsx',
|
||||
'src/components/child-motion-demo/**',
|
||||
'src/components/asset-studio/**',
|
||||
'src/components/bark-battle-creation/**',
|
||||
'src/components/big-fish-creation/**',
|
||||
'src/components/big-fish-result/**',
|
||||
'src/components/big-fish-runtime/**',
|
||||
'src/components/creation-agent/**',
|
||||
'src/components/creative-agent/**',
|
||||
'src/components/custom-world-agent/**',
|
||||
'src/components/custom-world-home/**',
|
||||
'src/components/edutainment-creation/**',
|
||||
'src/components/edutainment-result/**',
|
||||
'src/components/edutainment-runtime/**',
|
||||
'src/components/game-canvas/**',
|
||||
'src/components/jump-hop-result/**',
|
||||
'src/components/jump-hop-runtime/**',
|
||||
'src/components/match3d-result/**',
|
||||
'src/components/match3d-runtime/**',
|
||||
'src/components/puzzle-clear-creation/**',
|
||||
'src/components/puzzle-clear-result/**',
|
||||
'src/components/puzzle-clear-runtime/**',
|
||||
'src/components/puzzle-gallery/**',
|
||||
'src/components/puzzle-result/**',
|
||||
'src/components/puzzle-runtime/**',
|
||||
'src/components/rpg-creation-asset-studio/**',
|
||||
'src/components/rpg-creation-result/**',
|
||||
'src/components/rpg-runtime-panels/**',
|
||||
'src/components/square-hole-creation/**',
|
||||
'src/components/square-hole-result/**',
|
||||
'src/components/square-hole-runtime/**',
|
||||
'src/components/unified-creation/**',
|
||||
'src/components/visual-novel-creation/**',
|
||||
'src/components/visual-novel-result/**',
|
||||
'src/components/visual-novel-runtime/**',
|
||||
'src/components/wooden-fish-result/**',
|
||||
'src/components/wooden-fish-runtime/**',
|
||||
'src/components/common/PublishShare*',
|
||||
'src/components/common/publishShare*',
|
||||
'src/components/platform-entry/PlatformEntryCreation*',
|
||||
'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx',
|
||||
'src/components/platform-entry/platformEntryTypes.ts',
|
||||
'src/components/platform-entry/PlatformEntryHome*',
|
||||
'src/components/platform-entry/PlatformEntryWorld*',
|
||||
'src/components/platform-entry/PlatformMobileHome*',
|
||||
'src/components/platform-entry/PlatformWork*',
|
||||
'src/components/platform-entry/PlatformDraft*',
|
||||
'src/components/platform-entry/PlatformTask*',
|
||||
'src/components/platform-entry/PlatformError*',
|
||||
'src/components/platform-entry/barkBattle*',
|
||||
'src/components/platform-entry/platformCreation*',
|
||||
'src/components/platform-entry/platformDialog*',
|
||||
'src/components/platform-entry/platformDraft*',
|
||||
'src/components/platform-entry/platformEdutainment*',
|
||||
'src/components/platform-entry/platformEntryCreation*',
|
||||
'src/components/platform-entry/platformExternal*',
|
||||
'src/components/platform-entry/platformGeneration*',
|
||||
'src/components/platform-entry/platformHost*',
|
||||
'src/components/platform-entry/platformMiniGame*',
|
||||
'src/components/platform-entry/platformPlayed*',
|
||||
'src/components/platform-entry/platformPublic*',
|
||||
'src/components/platform-entry/platformPuzzle*',
|
||||
'src/components/platform-entry/platformRecommend*',
|
||||
'src/components/platform-entry/platformRpg*',
|
||||
'src/components/platform-entry/platformSelection*',
|
||||
'src/components/platform-entry/puzzleDraft*',
|
||||
'src/components/platform-entry/usePlatformCreation*',
|
||||
'src/components/platform-entry/usePlatformEntry*',
|
||||
'src/components/platform-entry/PlatformEntryFlowShellImpl/**',
|
||||
'src/components/platform-entry/platformMatch3DRuntimeProfile*',
|
||||
'src/components/unified-creation/workspaces/JumpHopCreationWorkspace*',
|
||||
'src/components/unified-creation/workspaces/Match3DCreationWorkspace*',
|
||||
'src/components/rpg-creation-editor/**',
|
||||
'src/components/rpg-entry/**',
|
||||
'src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx',
|
||||
'src/components/custom-world-home/CustomWorldCreationHub.test.tsx',
|
||||
'src/components/custom-world-home/CustomWorldCreationHub.testAdapter.tsx',
|
||||
'src/components/custom-world-home/creationWorkShelf.test.ts',
|
||||
'src/components/rpg-runtime-shell/**',
|
||||
'src/hooks/rpg-runtime-story/**',
|
||||
'src/hooks/rpg-session/**',
|
||||
'src/hooks/combat/**',
|
||||
'src/hooks/useCombatFlow.ts',
|
||||
'src/hooks/useStoryOptions.ts',
|
||||
'src/prompts/customWorldPrompts.ts',
|
||||
'src/services/ai.ts',
|
||||
'src/services/appTitle.ts',
|
||||
'src/services/appTitle.test.ts',
|
||||
'src/services/miniGameDraftGenerationProgress.ts',
|
||||
'src/services/creationEntryConfigService.ts',
|
||||
'src/services/creationUrlState*',
|
||||
'src/services/customWorld*',
|
||||
'src/services/input-devices/**',
|
||||
'src/services/publicWorkCode.ts',
|
||||
'src/services/runtimeGuestAuth.ts',
|
||||
'src/services/runtimeRequest*',
|
||||
'src/services/runtimeAudioFeedback.ts',
|
||||
'src/services/useMocapInput*',
|
||||
'src/services/wechatMiniProgramSubscribe*',
|
||||
'src/services/bark-battle-creation/**',
|
||||
'src/services/bark-battle-runtime/**',
|
||||
'src/services/big-fish-creation/**',
|
||||
'src/services/big-fish-gallery/**',
|
||||
'src/services/big-fish-runtime/**',
|
||||
'src/services/big-fish-works/**',
|
||||
'src/services/creation-agent/**',
|
||||
'src/services/creation-audio/**',
|
||||
'src/services/creative-agent/**',
|
||||
'src/services/edutainment-baby-drawing/**',
|
||||
'src/services/edutainment-baby-object/**',
|
||||
'src/services/child-motion-demo/**',
|
||||
'src/services/jump-hop/**',
|
||||
'src/services/match3d-creation/**',
|
||||
'src/services/match3d-runtime/**',
|
||||
'src/services/match3d-works/**',
|
||||
'src/services/match3dGeneratedModelCache*',
|
||||
'src/services/match3dSpritesheetParser*',
|
||||
'src/services/puzzle-agent/**',
|
||||
'src/services/puzzle-gallery/**',
|
||||
'src/services/puzzle-onboarding/**',
|
||||
'src/services/puzzle-runtime/**',
|
||||
'src/services/puzzle-works/**',
|
||||
'src/services/rpg-creation/**',
|
||||
'src/services/rpg-entry/**',
|
||||
'src/services/rpg-runtime/**',
|
||||
'src/services/square-hole-creation/**',
|
||||
'src/services/square-hole-runtime/**',
|
||||
'src/services/square-hole-works/**',
|
||||
'src/services/storyEngine/**',
|
||||
'src/services/visual-novel-creation/**',
|
||||
'src/services/visual-novel-runtime/**',
|
||||
'src/services/visual-novel-works/**',
|
||||
'src/services/wooden-fish/**',
|
||||
'src/types.ts',
|
||||
'src/types/**',
|
||||
'src/uiAssets.ts',
|
||||
'scripts/loadtest/**',
|
||||
'packages/shared/src/contracts/jumpHop.ts',
|
||||
'packages/shared/src/contracts/match3dAgent.ts',
|
||||
'packages/shared/src/contracts/match3dRuntime.ts',
|
||||
'packages/shared/src/contracts/match3dWorks.ts',
|
||||
'packages/shared/src/contracts/barkBattle*',
|
||||
'packages/shared/src/contracts/bigFish*',
|
||||
'packages/shared/src/contracts/creationAgent*',
|
||||
'packages/shared/src/contracts/creationAudio*',
|
||||
'packages/shared/src/contracts/creativeAgent*',
|
||||
'packages/shared/src/contracts/customWorld*',
|
||||
'packages/shared/src/contracts/edutainment*',
|
||||
'packages/shared/src/contracts/playTypes*',
|
||||
'packages/shared/src/contracts/publicWork*',
|
||||
'packages/shared/src/contracts/puzzle*',
|
||||
'packages/shared/src/contracts/rpg*',
|
||||
'packages/shared/src/contracts/squareHole*',
|
||||
'packages/shared/src/contracts/story*',
|
||||
'packages/shared/src/contracts/visualNovel*',
|
||||
'packages/shared/src/contracts/woodenFish*',
|
||||
'scripts/export-match3d-resource-pipeline*',
|
||||
'scripts/generate-child-motion-demo-assets.mjs',
|
||||
'src/services/puzzle-clear/**',
|
||||
'src/games/**',
|
||||
'src/data/**',
|
||||
'src/prompts/**',
|
||||
'apps/admin-web/src/pages/AdminCreationEntrySwitchPage*',
|
||||
'apps/admin-web/src/pages/AdminWorkVisibilityPage*',
|
||||
'src/services/recommendedRuntimeGuestLaunch.test.ts',
|
||||
'src/data/sceneEncounterPreviews.ts',
|
||||
'public/Icons',
|
||||
'media',
|
||||
'.codex-logs',
|
||||
@@ -407,10 +78,7 @@ module.exports = {
|
||||
rules: {
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-refresh/only-export-components': [
|
||||
'error',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'react-refresh/only-export-components': ['error', {allowConstantExport: true}],
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
@@ -423,7 +91,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'no-constant-condition': 'error',
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'no-console': ['error', {allow: ['warn', 'error']}],
|
||||
'no-useless-escape': 'error',
|
||||
'prefer-const': 'error',
|
||||
},
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
name: Project CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- codex/ai-game-creator-app
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CI: 'true'
|
||||
CARGO_INCREMENTAL: '0'
|
||||
CARGO_HTTP_MULTIPLEXING: 'false'
|
||||
CARGO_NET_RETRY: '10'
|
||||
CARGO_TERM_COLOR: always
|
||||
NPM_CONFIG_AUDIT: 'false'
|
||||
NPM_CONFIG_FETCH_RETRIES: '10'
|
||||
NPM_CONFIG_FETCH_RETRY_FACTOR: '2'
|
||||
NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: '60000'
|
||||
NPM_CONFIG_FETCH_RETRY_MINTIMEOUT: '2000'
|
||||
NPM_CONFIG_FUND: 'false'
|
||||
NPM_CONFIG_PREFER_OFFLINE: 'true'
|
||||
RUSTUP_AUTO_INSTALL: '0'
|
||||
RUSTC_WRAPPER: ''
|
||||
CARGO_BUILD_RUSTC_WRAPPER: ''
|
||||
|
||||
jobs:
|
||||
repository-checks:
|
||||
name: Repository checks
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Resolve comparison base
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_ref="$(node -e '
|
||||
const fs = require("node:fs");
|
||||
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
||||
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
|
||||
')"
|
||||
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
|
||||
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
|
||||
echo "comparison base commit is unavailable: ${base_ref}" >&2
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
|
||||
fi
|
||||
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
|
||||
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
|
||||
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run repository lint gates
|
||||
run: npm run lint
|
||||
|
||||
- name: Build web applications
|
||||
run: npm run build
|
||||
|
||||
- name: Validate content data
|
||||
run: npm run check:content
|
||||
|
||||
- name: Check committed whitespace
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_ref="${SPACETIME_SCHEMA_BASE_REF:-}"
|
||||
test -n "${base_ref}"
|
||||
git cat-file -e "${base_ref}^{commit}"
|
||||
git diff --check "${base_ref}"...HEAD
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend tests
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout source from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '1'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Install AI game creator dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh --prefix apps/ai-game-creator-shell
|
||||
|
||||
- name: Run frontend and script tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run BgFilter worker smoke harness tests
|
||||
run: npm run bgfilter-worker:smoke-test
|
||||
|
||||
- name: Validate production health patrol behavior
|
||||
run: npm run check:production-health-patrol
|
||||
|
||||
- name: Validate production API release behavior
|
||||
run: npm run check:production-api-release
|
||||
|
||||
- name: Validate production API deploy behavior
|
||||
run: npm run check:production-api-deploy
|
||||
|
||||
backend-tests:
|
||||
name: Backend tests
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Resolve comparison base
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_ref="$(node -e '
|
||||
const fs = require("node:fs");
|
||||
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
||||
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
|
||||
')"
|
||||
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
|
||||
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
|
||||
echo "comparison base commit is unavailable: ${base_ref}" >&2
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
|
||||
fi
|
||||
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
|
||||
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
|
||||
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Prepare server-rs Rust dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch --locked \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path server-rs/Cargo.toml; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo 'server-rs Cargo dependency fetch failed after 5 attempts.' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
|
||||
- name: Check server-rs boundaries
|
||||
run: npm run check:server-rs-ddd
|
||||
|
||||
- name: Run server-rs workspace tests
|
||||
run: cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml
|
||||
|
||||
- name: Check api-server targets
|
||||
run: cargo check --locked -p api-server --all-targets --manifest-path server-rs/Cargo.toml
|
||||
|
||||
- name: Check SpacetimeDB module
|
||||
run: cargo check --locked -p spacetime-module --manifest-path server-rs/Cargo.toml
|
||||
|
||||
native-shell-tests:
|
||||
name: Native shell tests
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Install AI game creator dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh --prefix apps/ai-game-creator-shell
|
||||
|
||||
- name: Prepare native Rust dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for manifest_path in \
|
||||
apps/desktop-shell/src-tauri/Cargo.toml \
|
||||
apps/ai-game-creator-shell/src-tauri/Cargo.toml; do
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch --locked \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path "${manifest_path}"; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo "Cargo dependency fetch failed after 5 attempts: ${manifest_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
done
|
||||
|
||||
- name: Run native shell gates
|
||||
run: npm run check:native-shells
|
||||
|
||||
- name: Ensure native lockfiles are unchanged
|
||||
run: git diff --exit-code -- apps/desktop-shell/src-tauri/Cargo.lock apps/ai-game-creator-shell/src-tauri/Cargo.lock
|
||||
@@ -31,12 +31,6 @@ temp*build*/
|
||||
/apps/desktop-shell/src-tauri/target/
|
||||
/apps/desktop-shell/src-tauri/gen/
|
||||
/apps/desktop-shell/src-tauri/permissions/autogenerated/
|
||||
/apps/ai-game-creator-shell/src-tauri/target/
|
||||
/apps/ai-game-creator-shell/src-tauri/gen/
|
||||
/apps/ai-game-creator-shell/src-tauri/logs/
|
||||
/apps/ai-game-creator-shell/logs/
|
||||
/apps/ai-game-creator-shell/.llm-drafts/
|
||||
/apps/ai-game-creator-shell/game-creator.config.local.json
|
||||
/apps/mobile-shell/.expo/
|
||||
/apps/mobile-shell/.expo-export-smoke/
|
||||
/server-rs/.spacetimedb/
|
||||
@@ -48,17 +42,14 @@ temp*build*/
|
||||
/.app/
|
||||
/target/
|
||||
/logs
|
||||
/.claude/settings.local.json
|
||||
/.codegraph/
|
||||
/.playwright-cli/
|
||||
**/.playwright-cli/
|
||||
/output/playwright/
|
||||
/output/external-api-smoke/
|
||||
/server-rs/crates/*/logs/
|
||||
.worktrees/
|
||||
.rag/
|
||||
.env.secrets.local
|
||||
nohup.out
|
||||
spacetime.local.json
|
||||
deploy/container/api-server.env
|
||||
deploy/container/worker-smoke/
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: genarrative-dev-stack-port-routing
|
||||
short_description: 修改 Genarrative 本地 dev 启动端口、代理目标、端口冲突处理时使用。
|
||||
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 启动串联时使用。
|
||||
version: 1.2.0
|
||||
description: 在 Genarrative 中修改 npm run dev / dev:spacetime / dev:api-server / dev:web / dev:admin-web 的本地启动端口、端口可用性探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标和后台 admin-web 启动串联时使用。
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
@@ -13,11 +13,11 @@ metadata:
|
||||
|
||||
# Genarrative 本地 dev 启动端口与代理目标串联流程
|
||||
|
||||
用于维护 Genarrative 本地开发栈启动脚本,重点覆盖 `npm run dev` 与五个 `dev:*` 单模块命令的端口检查、端口漂移和后续流程目标传递。
|
||||
用于维护 Genarrative 本地开发栈启动脚本,重点覆盖 `npm run dev` 与四个 `dev:*` 单模块命令的端口检查、端口漂移和后续流程目标传递。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 修改 `scripts/dev.mjs`、`scripts/dev-utils.mjs`、`scripts/dev-stack-port-utils.mjs` 或 AI 游戏创作客户端 dev 启动器。
|
||||
- 修改 `scripts/dev.mjs`、`scripts/dev-utils.mjs`、`scripts/dev-stack-port-utils.mjs`。
|
||||
- 处理 `3000`、`3101`、`3102`、`8082` 等端口被占用导致本地开发栈启动失败。
|
||||
- 排查 Vite 代理仍指向旧 api-server 端口、前端打开了旧 dev server、后台代理错配。
|
||||
- 调整 SpacetimeDB standalone、publish、Rust `api-server`、主站 Vite、后台 Vite 的启动顺序。
|
||||
@@ -31,49 +31,40 @@ metadata:
|
||||
2. Rust `api-server`:`8082`,健康检查为 `http://127.0.0.1:<api-port>/healthz`。
|
||||
3. SpacetimeDB standalone:`3101`,健康检查为 `http://127.0.0.1:<spacetime-port>/v1/ping`。
|
||||
4. 后台 Vite:`3102`,后台地址为 `http://127.0.0.1:<admin-web-port>/admin/`。
|
||||
5. 独立 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 继续沿用原有统一端口探测和漂移逻辑,不读系统级注册表。
|
||||
Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range` 会先向系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json` 申请一个端口段,再把该段映射为 `web = start`、`api = start + 1`、`spacetime = start + 2`、`adminWeb = start + 3`。注册表锁文件是 `/var/tmp/genarrative-dev-port-ranges/registry.lock`,可通过 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。自动分配从 `10000-10099` 起,每次占用 100 个端口块,后续块按 `10100-10199`、`10200-10299` 递增;当前口径是“一个用户固定占用一个段,后续启动继续复用这段并在段内漂移”;该注册表只在 Linux 上生效;Windows 继续沿用原有端口探测、漂移和复用逻辑,不读系统级注册表。
|
||||
|
||||
## 实现入口
|
||||
|
||||
- `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。
|
||||
- `dev`:执行 `node scripts/dev.mjs`,启动完整四模块。
|
||||
- `dev:spacetime` / `dev:api-server` / `dev:web` / `dev:admin-web`:执行 `node scripts/dev.mjs <module>`。
|
||||
- `scripts/dev-stack-port-utils.mjs`
|
||||
- `isPortAvailable(...)`:探测端口是否可监听。
|
||||
- `findAvailablePort(...)`:从优先端口向后寻找可用端口,`0` 表示申请临时端口。
|
||||
- `resolveDevStackPorts(...)`:一次性解析 SpacetimeDB、api-server、主站 Vite、后台 Vite、BgFilter worker 端口,并避免本次解析结果互相冲突。
|
||||
- `resolveDevStackPorts(...)`:一次性解析 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,并避免本次解析结果互相冲突。
|
||||
- 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`。
|
||||
- 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`。
|
||||
- `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。
|
||||
- 完整栈按 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 顺序启动。
|
||||
- Linux 下会先申请系统级端口段并把它映射成四个 dev 端口;自动分配从 `10000-10099` 起,Windows 则直接沿用原有参数解析与端口漂移逻辑。
|
||||
- 单模块命令复用同一套参数和 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:*` 单模块命令中端口解析后,必须同步到以下位置:
|
||||
`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` 启动严格监听;后端端口漂移必须跳过该预留端口。
|
||||
5. 主站 Vite:`RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET`、`ADMIN_WEB_TARGET`、`ADMIN_WEB_PORT`、`--port=${WEB_PORT}`、`--host=${WEB_HOST}`。
|
||||
6. 后台 Vite:`ADMIN_API_TARGET`、`GENARRATIVE_API_TARGET`、`GENARRATIVE_API_PORT`、`--port=${ADMIN_WEB_PORT}`。
|
||||
7. 控制台日志:`[dev:ports]` 和 `[dev] web/admin web/api-server/spacetime` 必须显示最终实际地址。
|
||||
8. Linux 端口段注册:`[dev] port-range:` 与 `[dev] port-range-registry:` 只在 Linux 输出,Windows 不应依赖系统级注册表。
|
||||
|
||||
如果只改了其中一段,通常会出现:浏览器打开的前端可用,但 `/api/*` 代理到旧端口;后台页面可用但后台 API 失败;SpacetimeDB 启动在新端口但 publish 仍发往旧端口。
|
||||
|
||||
@@ -83,7 +74,7 @@ Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range`
|
||||
- `scripts/dev-stack-port-utils.mjs`
|
||||
- `scripts/dev.mjs`
|
||||
- `scripts/dev-utils.mjs`
|
||||
- `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
|
||||
- `docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`
|
||||
- `docs/project-memory/shared-memory/pitfalls.md`
|
||||
2. 优先改公共端口工具,不要把端口探测逻辑复制到多个脚本。
|
||||
3. 修改 `scripts/dev.mjs` 时确认变量顺序:先解析参数和端口,再构造 `SPACETIME_SERVER` / `RUST_SERVER_TARGET`,最后启动对应 service。
|
||||
@@ -100,21 +91,21 @@ Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range`
|
||||
node --check scripts/dev.mjs
|
||||
npm run test -- scripts/dev-stack-port-utils.test.ts
|
||||
npm run check:encoding
|
||||
node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 api:127.0.0.1:0 web:0.0.0.0:0 adminWeb:127.0.0.1:0 bgfilterWorker:127.0.0.1:0
|
||||
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
|
||||
```
|
||||
|
||||
端口冲突回归测试建议:
|
||||
|
||||
1. 用测试或临时 Node server 占用某个优先端口。
|
||||
2. 调用 `findAvailablePort`,断言结果大于被占用端口。
|
||||
3. 调用 `resolveDevStackPorts`,断言五个结果互不相同。
|
||||
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`。
|
||||
完整启动属于长驻进程。需要 smoke 时用 background 方式启动,并另开命令检查 `/healthz`、`/v1/ping` 和页面端口;不要等待 `npm run dev` 自然退出。
|
||||
|
||||
## 常见坑
|
||||
|
||||
@@ -131,9 +122,7 @@ node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 ap
|
||||
- [ ] Linux 注册表分配、同用户复用固定段并继续漂移、自动分配从 `10000-10099` 起、Windows bypass 都有测试覆盖。
|
||||
- [ ] `scripts/dev.mjs` 通过 `node --check`。
|
||||
- [ ] `npm run dev` 的 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 都使用实际端口。
|
||||
- [ ] BgFilter worker 在 api-server 前 ready,父子共享实际 base URL / Token,Rust watch 只触发一次组合重启。
|
||||
- [ ] `npm run dev:web` 在主站端口不可用时能切换到可用端口。
|
||||
- [ ] `npm run agc` / `npm run agc:game-chat` 在 Linux 使用用户段 `start + 5`,Tauri、Vite、marker 和预检使用同一最终端口。
|
||||
- [ ] 文档同步更新 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
- [ ] 文档同步更新 `docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`。
|
||||
- [ ] 长期踩坑同步更新 `docs/project-memory/shared-memory/pitfalls.md`。
|
||||
- [ ] 修改中文文件后运行 `npm run check:encoding`。
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
|
||||
- Issue 使用自托管 Gitea;优先用 Gitea UI/API 或 `tea` CLI,不使用 GitHub `gh` 或 GitLab `glab`,除非仓库已迁移。默认 triage 标签:`needs-triage`、`needs-info`、`ready-for-agent`、`ready-for-human`、`wontfix`。
|
||||
- 需要仓库级 Hermes skills/plugins 时,再读取 [`.hermes/README.md`](.hermes/README.md)。
|
||||
- 涉及 AI 游戏创作独立 App、多智能体 Runtime、本地项目产物或本地 HTTP 预览时,先读取 [`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`](docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)。
|
||||
- 新增、补齐、迁移或重构玩法入口、玩法类型、创作工作台、生成页、结果页、发布、运行态、作品架、广场或公开 read model 前,必须读取并按 [`genarrative-play-type-integration`](.codex/skills/genarrative-play-type-integration/SKILL.md) 执行。
|
||||
- 涉及 `npm run dev` / `npm run dev:spacetime` / `npm run dev:api-server` / `npm run dev:web` / `npm run dev:admin-web` 的端口探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标或后台 dev 端口时,按 [`.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md`](.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md) 执行。
|
||||
- 涉及 SpacetimeDB 的设计、实现、脚本、调试、发布、绑定生成、schema、reducer、procedure、view 或 Rust API 时,必须读取并按 [`spacetimedb-cli`](.codex/skills/spacetimedb-cli/SKILL.md)、[`spacetimedb-rust`](.codex/skills/spacetimedb-rust/SKILL.md)、[`spacetimedb-concepts`](.codex/skills/spacetimedb-concepts/SKILL.md) 中相关 skill 执行。
|
||||
@@ -45,7 +44,6 @@
|
||||
- DDD 分层边界按总纲执行:领域规则沉到 `module-*`,SpacetimeDB 表和事务编排留在 `spacetime-module`,后端访问 SpacetimeDB 统一经 `spacetime-client` facade,HTTP/SSE/BFF 留在 `api-server`,外部副作用留在 `platform-*`,前后端 DTO 留在 `shared-contracts`。
|
||||
- 前端只做表现、交互和临时 UI 状态,不承接正式业务真相,不绕过后端投影或后端 API 直接实现业务规则。
|
||||
- 契约、路由、DTO 去留和 breaking change 以当前后端架构文档、`server-rs/crates/api-server/src/app.rs`、`shared-contracts` 和 `packages/shared` 为准;不得在前端、`api-server` 或临时兼容层中重新发明旧接口。
|
||||
- 凡修改 `/api/external/v1` 的路由、HTTP 方法、请求 / 响应 DTO、请求头、状态码、鉴权或异步语义,必须在同一次变更中同步更新权威契约 [`docs/openapi/genarrative-external-v1.openapi.json`](docs/openapi/genarrative-external-v1.openapi.json) 及对应契约测试;Rust 实现与 OpenAPI 未保持一致时任务不得视为完成。
|
||||
- SpacetimeDB 已有表新增字段时,字段必须放在 Rust 表结构体最后,并设置明确默认值;需要删除、改名、重排或改类型时,必须先询问用户并确认迁移计划。
|
||||
- 修改 SpacetimeDB schema 后必须同步 `migration.rs`、表目录和生成绑定,并运行 `npm run check:spacetime-schema`。
|
||||
- 除 CI/CD 脚本内部受控用法外,人工命令、本地联调、排障步骤和文档示例禁止继续使用 `spacetime --root-dir`。
|
||||
|
||||
-16
@@ -16,22 +16,6 @@ _Avoid_: 在玩法页面内手写上传、参考图、重绘、预览、删除
|
||||
独立 `/editor` 中可保存、恢复和继续编辑的图片画布工作状态,包含画布视图、图层布局和资源引用;用于多图对比、生成结果衍生和画布级编辑,不替代玩法页面内的单图资产编辑。
|
||||
_Avoid_: 玩法结果页单图槽位、发布态作品、只存在前端内存里的临时画布
|
||||
|
||||
**项目开发画布**:
|
||||
GameAgent 独立客户端中某个本地游戏项目的开发工作区概念,用于承载项目名、路径、首条需求、附件导入结果、最近 run 状态,以及后续真正的项目开发画布与 Agent 协作界面;当前首页改造阶段先落占位页。它属于 AI 游戏创作本地项目域,不等同于 `/editor` 的图片画布工程。
|
||||
_Avoid_: `/editor` 图片画布工程、画布资源 / 图层布局、启动器 / 主窗口切换概念、只用于首页输入的临时草稿
|
||||
|
||||
**画布Agent对话**:
|
||||
图片画布工程右侧的对话式编辑器工具,用户通过自然语言调度画布已有的图片类生成与编辑能力(生成图片、生成角色形象、生成图标素材、生成 UI 设计图、基于附件的图片修改),并可附加画布素材或素材库图片作为参考;对话归属单个图片画布工程,可保存历史、新开会话和软删会话。属于画布域工具,不承接玩法创作、不产出玩法作品或模板,与「表单/图片输入创作工作台」的 Avoid 边界不冲突。
|
||||
_Avoid_: 对话式玩法创作工作台、绕过模型定价收口的生成入口、把对话消息当作画布布局真相、复用拼图专用 creative-agent 内存会话
|
||||
|
||||
**画布Agent会话记录**:
|
||||
画布Agent对话的持久化形态:SpacetimeDB 表只存会话元数据(会话 ID、所属工程、属主、标题、软删标记、聊天记录 OSS 对象引用、时间戳),完整消息内容以会话粒度 JSON 对象存 OSS,追加消息即整体重写对象。
|
||||
_Avoid_: api-server 内存会话、消息全文入 SpacetimeDB 表、对话混入工程布局快照、每条消息一个 OSS 对象
|
||||
|
||||
**画布Agent对话附件**:
|
||||
画布Agent对话消息携带的图片参考,统一为画布资源 / 素材库对象引用(resourceId / assetId + 可选 objectKey),单条消息上限 9 张;上传图片若从对话入口进入,必须复用素材库 / 画布资源登记链路,在上传格未落地前只从已有画布资源和账号素材库选择,不存在只属于对话的第三种图。
|
||||
_Avoid_: 对话私有图片副本、内嵌 base64 附件、音视频附件
|
||||
|
||||
**画布资源**:
|
||||
图片画布工程中可被一个或多个图层引用的图片资源记录,保存 OSS 对象引用、上传 / 生成来源、提示词、模型、任务和尺寸等资源元数据;同一资源可以在工程布局中出现多次。
|
||||
_Avoid_: 图层位置、前端 hover / selected 状态、直接内嵌图片二进制
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user