diff --git a/.codex/skills/genarrative-external-editor-api/SKILL.md b/.codex/skills/genarrative-external-editor-api/SKILL.md index 876d7d078..6a99b11c1 100644 --- a/.codex/skills/genarrative-external-editor-api/SKILL.md +++ b/.codex/skills/genarrative-external-editor-api/SKILL.md @@ -1,109 +1,70 @@ --- name: genarrative-external-editor-api -description: Guide use of Genarrative's external editor/canvas integration through its hosted remote MCP or asynchronous `/api/external/v1` OpenAPI. Use when a user needs to discover the hosted MCP or complete Skill package, infer the right canvas or asset operation, submit and poll image/video/audio generation safely, prepare canvas and asset-library context, upload references, draft HTTP/SDK requests, or securely handle a Genarrative developer API Key. +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. --- # Genarrative External Editor API -Use the live integration manifest as the discovery entry: `GET https://www.genarrative.world/api/external/v1/agent-integration.json`. It advertises the hosted Streamable HTTP MCP endpoint, OpenAPI document, raw Skill entry, complete Skill archive and archive SHA-256. Use the live OpenAPI contract as the field-level 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`. +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`. -Prefer the hosted MCP at `https://www.genarrative.world/api/external/v1/mcp` when the Agent supports remote MCP with a custom Bearer token. The MCP exposes the OpenAPI operations as tools plus `usage`, `openapi`, and `skill` resources; it is hosted by Genarrative and does not require installing a local MCP server. Use this Skill package when the Agent does not support remote MCP or when local-file upload orchestration is required. +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 the bundled Python helper for runnable REST examples. It uses only Python stdlib, reads the local JSON API Key file, fixes the production base URL, and wraps upload, asynchronous submission, polling, and result retrieval. +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. ## Workflow -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 post-completion fallback to create a library asset from the first returned frame when the generated result has no direct `asset`. -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. Treat every generation POST as asynchronous. Send a stable `Idempotency-Key`, retain the returned `operationId`, and poll the returned `statusUrl` or `GET /api/external/v1/generations/{operationId}` according to `pollAfterMs`. Consume `result` only after `status=completed`; surface the safe error after `failed`. A timeout or lost response is not permission to submit again with a new key. -9. Use `scripts/genarrative_external_api.py` when the user wants runnable Python, reference image upload, canvas/folder session setup, art-spec carrying, or automatic submit-and-poll behavior. -10. Keep to `/api/external/v1` unless the user explicitly asks for internal profile/admin APIs. Never call internal worker, queue, or SpacetimeDB MCP endpoints. +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 generation; never substitute a temporary signed URL. +6. For generation endpoints that support the fields, include `projectId`, `assetFolderId`, an asset label, and `canvasCompletion` so the result enters both the canvas and its same-name library folder. +7. Treat every generation POST as asynchronous. Send one stable `Idempotency-Key` per logical request, retain the returned `operationId`, and poll the returned `statusUrl` or `GET /api/external/v1/generations/{operationId}` according to `pollAfterMs`. +8. Consume `result` only after `status=completed`. On `failed`, surface the safe error. On a client timeout or lost response, retain the operation/key; do not create a replacement request. +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. -## Core Routes +## Essential Invariants -| 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`, `expectedRevision` | -| 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` | -| Query generation | `GET /api/external/v1/generations/{operationId}` | `operationId` returned by the submission | +- Authenticate MCP and business API calls with `Authorization: Bearer `. 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 may need a post-completion library fallback from the first returned frame when no direct asset is present; the helper implements it. -All eight generation POST routes require `Idempotency-Key` and return HTTP `202`, not a completed media response. +## Documentation Navigation + +Read only the references needed for the task, but always verify exact schemas and enums against live OpenAPI: + +- `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. + +The hosted MCP exposes the same documents through: + +- `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` ## Hosted Integration Discovery -- Integration manifest: `GET /api/external/v1/agent-integration.json`. -- Hosted remote MCP: `POST /api/external/v1/mcp`, Streamable HTTP with the same Bearer API Key. +- 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`. -The Skill archive contains `SKILL.md`, `references/api-selection.md`, `scripts/genarrative_external_api.py`, and `agents/openai.yaml`. Verify its SHA-256 against `agent-integration.json` before installing it. The discovery, OpenAPI, and Skill download routes are public; MCP and business API calls require the API Key. +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. -## Art Spec Interface +## Python Helper -Maintain one current art spec per conversation. A compact spec is enough: - -```json -{ - "assetType": "character | background | prop | ui | icon | animation | video | audio", - "subject": "要生成的主体", - "style": "画风/材质/时代/参考风格", - "palette": "主色与禁用色", - "composition": "构图、镜头、姿态或布局", - "format": "比例、尺寸、分辨率、帧数、时长", - "constraints": "必须保留/禁止出现/透明或绿幕要求", - "references": ["objectKey 或本地路径说明"] -} -``` - -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. - -## API Key - -The external OpenAPI uses: - -```text -Authorization: Bearer -``` - -The OpenAPI, integration manifest, and Skill download endpoints are public. The hosted MCP and all project, asset, upload, generation, and generation-query operations require the Bearer API Key. - -Use this fixed production base URL: - -```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 -``` +Store the API Key outside the repository at `~/.config/genarrative/external-editor-api.json`: ```json { @@ -111,312 +72,45 @@ Guide the user to create a key from the logged-in product UI under `开发者 AP } ``` -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: +Set restrictive permissions where possible, then smoke-test 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 ``` -## Asynchronous Generation Contract - -Generate one stable idempotency key per logical generation. Reuse it for transport retries of the same request body. A successful submission returns HTTP `202` with: - -```json -{ - "operationId": "task-...", - "kind": "editor_image_generation", - "status": "queued", - "statusUrl": "/api/external/v1/generations/task-...", - "pollAfterMs": 1500, - "updatedAtMicros": 1785456000000000 -} -``` - -Poll until a terminal status: - -- `queued` / `running`: retain `operationId`; show `phaseLabel`, `phaseDetail`, and `progress`; wait at least `pollAfterMs`. -- `completed`: consume the compact stable `result`, then reload the project/library snapshot if the caller needs the complete authoritative state. -- `failed`: surface the returned safe `error`; do not infer provider internals. - -The compact result may contain `objectKey`, `resourceId`, `assetId`, dimensions, media type, warnings, and other stable artifact references. It deliberately excludes a complete project/canvas snapshot, Data URL, Blob URL, expiring signed URL, worker lease data, and internal provider diagnostics. Use `/assets/read-url` when a stable `objectKey` needs a temporary download URL. - -## Request Patterns - -For image and icon generation, the request-body top-level `style` field controls deterministic post-processing and is distinct from `generationInputs.artSpec.style`, which describes visual style for prompting. Pass `style="pixelArt"` in Python or `"style": "pixelArt"` in JSON to enable pixel-art snapping on supported generation types; use `"none"` or omit the field otherwise. Verify compatibility and fallback semantics in `references/api-selection.md`. - -For Python callers, prefer: +For a canvas-backed generation: ```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=art_spec, + artSpec={ + "assetType": "background", + "subject": "幻想森林主视觉", + "style": "手绘游戏概念图", + "palette": "翡翠绿与金色光斑", + "composition": "横版,中心留出角色站位", + "format": "16:9, 1K", + "constraints": "无文字、无 UI 按钮", + "references": [], + }, ) ``` -The helper method blocks only in the local client while it submits and polls; the server request itself is asynchronous. For explicit control, call `client.submit_generation(...)`, persist the returned `operationId`, then call `client.get_generation(...)` or `client.wait_for_generation(...)`. - -For a transparent game/UI atlas, call the dedicated helper instead of ordinary image generation: - -```python -client.generate_icon_spritesheet( - "editor-resource-current-art-spec", - ["蛇头四方向", "直身与四种转角", "尾部四方向", "四类可区分食物"], - canvasSession=session, - assetLabel="贪吃蛇透明图集", - screenColor="auto", -) -``` - -Pass the registered visual-spec resource ID as `reference_image_src`; do not pass the UI prototype or a local path. - -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. - -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": "", - "assetFolderId": "", - "assetLabel": "森林背景", - "generationInputs": { - "artSpec": { - "assetType": "background", - "style": "手绘游戏概念图" - } - }, - "canvasCompletion": { - "title": "森林背景", - "placeholder": { - "x": 0, - "y": 0, - "width": 1024, - "height": 576, - "originalWidth": 1024, - "originalHeight": 576 - } - } -} -``` - -Save the JSON above as `request.json`, then submit and poll with a stable key: - -```bash -idempotency_key="$(node -e 'process.stdout.write(require("node:crypto").randomUUID())')" -submission="$(curl -fsS "$api/api/external/v1/editor/images/generations" \ - "${auth[@]}" "${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")" -curl -fsS "$api/api/external/v1/generations/$operation_id" "${auth[@]}" -``` - -Continue polling according to `pollAfterMs`. If the submit response is lost, retry the same body with the same `Idempotency-Key`; never generate a replacement key merely because the outcome is unknown. - -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": "", - "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": "", - "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": "", - "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 short HTTP requests for submission/status reads and an overall 1800-second local polling budget. Direct clients should use their own bounded polling budget without keeping the generation POST connection open. - -Character animation can complete without a direct `asset` payload. To keep the "canvas + asset library" invariant, call `client.animate_character(..., canvasSession=session, canvasTitle="...")`; after polling reaches `completed`, the helper creates a library asset from the first returned frame when needed. - -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. - -## Completed Results with Warnings - -Character image generation (including character redraw through `kind: "character"`), icon spritesheet generation, and UI asset extraction can complete with warnings. The initial HTTP `202` means only that the task was accepted. Interpret warnings only after the query reaches `status=completed`: the query-level `warning` is display-ready text, while the compact `result.warning` / `result.sliceWarning` retain structured artifact semantics when present. - -- Apply the compact media references and then reload the authoritative project/library snapshots before interpreting optional derivatives. Character results use `resource` / `asset`, while icon spritesheet and UI extraction use `spritesheetResource` / `spritesheetAsset`. When `result.warning.code` is `postprocess-failed-source-preserved`, the saved source image is the authoritative main result. Character output has no transparent derivative; icon spritesheet and UI extraction have neither a transparent spritesheet nor slices. Display the reason and do not synthesize missing derivatives or restart generation. -- `result.sliceWarning` is a separate condition used only when transparent spritesheet post-processing succeeded but automatic slicing failed. Continue using the complete transparent spritesheet and do not claim missing slices. -- `warning` and `sliceWarning` are mutually exclusive only for `postprocess-failed-source-preserved`, because a failed transparent post-process never reaches slicing. Since 2026-07-29 a general `warning` may also come from image-style normalization (`unsupported-image-style`) or pixel-art snapping, and those can coexist with `sliceWarning` in the same response. Display both reasons; do not drop either one and do not misclassify a source-preserved result as a slicing-only warning. - -For reusable transparent game/UI sheets, do not substitute ordinary image generation merely because it can draw several objects in one image. Use icon spritesheet generation when a stable visual-spec reference and `iconDescriptions` exist; use UI extraction only for an existing annotated UI design. Pass `screenColor: "auto"` unless the art direction requires one of the supported solid chroma colors. A client must verify the returned full sheet really contains transparency before treating it as a transparent spritesheet. If a source-preserved `warning` is present, do not register the opaque provider source as the requested transparent deliverable. When only `sliceWarning` is present, the full transparent sheet remains usable, but no individual slices may be claimed. - -## AI Game Creator Canonical Visual DAG - -The AI game creator reuses its existing 16-task manifest; do not add a parallel task system or collapse the following artifacts into one ordinary generation request: - -1. `art-director` generates `assets/art-spec.png` with `POST /api/external/v1/editor/images/generations`, `kind: "spec"`, and registers it as `assetKind: "icon-spec"`. This is the real visual-spec image. The JSON value in `generationInputs.artSpec` is supporting structured context and does not replace this image. -2. `design-foundation` uses the registered External Editor resource ID for `assets/art-spec.png` in `referenceImageSrcs`, then generates the complete `assets/ui-prototype.png` through `POST /api/external/v1/editor/images/generations` with `kind: "ui-design"`. -3. `art-asset-plan` uses the same registered `assets/art-spec.png` resource ID as the required `referenceImageSrc` for `POST /api/external/v1/editor/icon-spritesheets/generations`, supplies concrete `iconDescriptions`, and registers the transparent full result as `assets/art-spritesheet.png`. - -Never use `assets/ui-prototype.png` as the icon spritesheet's visual-spec reference. `POST /api/external/v1/editor/ui-designs/assets/extractions` requires an existing UI design image with red-box annotations; it is not UI generation and is not part of this canonical DAG. +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`. ## Guardrails -- Do not invent endpoints outside the OpenAPI, especially internal worker, runtime task-list, SpacetimeDB, or queue routes. The only external generation query is `/api/external/v1/generations/{operationId}`. -- 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. -- Never treat HTTP `202` as a generated artifact. Preserve `operationId` until a terminal query result, and do not automatically replay an unknown-outcome request with a new idempotency key. -- A completed generation result is compact. Reload `project`, `resource`, or `asset` through their normal read endpoints when a complete authoritative snapshot is needed. - -## Resources - -- `references/api-selection.md`: intent routing and required-field cheat sheet. -- `scripts/genarrative_external_api.py`: stdlib Python helper for API Key loading, local reference upload, object confirm, project/canvas calls, asynchronous generation submission, polling, and result retrieval. +- 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. diff --git a/.codex/skills/genarrative-external-editor-api/references/api-operations.md b/.codex/skills/genarrative-external-editor-api/references/api-operations.md new file mode 100644 index 000000000..06fbf30a1 --- /dev/null +++ b/.codex/skills/genarrative-external-editor-api/references/api-operations.md @@ -0,0 +1,99 @@ +# 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 | +| 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. + +## 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` | `referenceImageSrc`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | +| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` | +| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `canvasCompletion` | +| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | +| Sound effect | `/api/external/v1/editor/audios/sound-effects/generations` | `prompt`, `duration` | `model`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` | +| Background music | `/api/external/v1/editor/audios/background-music/generations` | `gptDescriptionPrompt`, `makeInstrumental` | `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` | + +Poll all 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 does not accept the same library fields. If its completed compact result lacks a direct `asset`, create a library record from the first returned frame; do not duplicate one when an asset already exists. +- 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: + +| Target capability | Field | +| --- | --- | +| Image generation | `referenceImageSrcs` | +| Image edit/redraw | `sourceImageSrc`; additional references in `referenceImageSrcs` | +| Icon spritesheet | `referenceImageSrc`; additional style references in `referenceImageSrcs` | +| UI design extraction | `sourceImageSrc`; additional references in `referenceImageSrcs` | +| Character animation | `sourceImageSrc` | +| Video with image references | `referenceImageSrcs` | + +Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference. + +## Common Values + +Use OpenAPI as the final authority; these common values are a routing aid: + +- Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`; ordinary image generation may omit it. +- 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`. +- 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. diff --git a/.codex/skills/genarrative-external-editor-api/references/api-selection.md b/.codex/skills/genarrative-external-editor-api/references/api-selection.md deleted file mode 100644 index 708f8f43e..000000000 --- a/.codex/skills/genarrative-external-editor-api/references/api-selection.md +++ /dev/null @@ -1,261 +0,0 @@ -# External Editor API Routing - -Source of truth: `docs/openapi/genarrative-external-v1.openapi.json`. - -## Base - -- Fixed base URL: `https://www.genarrative.world/`. -- Integration discovery: `GET /api/external/v1/agent-integration.json` returns the hosted MCP URL, OpenAPI URL, raw Skill entry, complete Skill archive, archive file list, and archive SHA-256. -- Hosted MCP: `/api/external/v1/mcp`, Streamable HTTP, authenticated with the same Bearer API Key. It exposes the REST operations as tools and the usage/OpenAPI/Skill documents as resources. -- Public contract: `GET /api/external/v1/openapi.json`. -- Public Skill fallback: `GET /api/external/v1/skill/SKILL.md` or the complete `GET /api/external/v1/skill.zip` package. -- Authenticated calls: `Authorization: Bearer `. -- Default credentials file: `~/.config/genarrative/external-editor-api.json` with an `apiKey` string. -- All eight generation POST routes are asynchronous, require `Idempotency-Key`, and return HTTP `202`. Retain `operationId` and poll `GET /api/external/v1/generations/{operationId}` according to `pollAfterMs`; do not hold the submit connection open. -- Prefer hosted MCP for Agents that support remote Streamable HTTP plus custom Bearer tokens. Use the complete Skill package and helper when remote MCP is unavailable or local-file upload needs client-side orchestration. - -## 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. - -For the AI game creator's existing 16-task autonomous build, distinguish that JSON art spec from the required visual-spec image and keep this dependency chain: - -1. `art-director` -> `assets/art-spec.png` via `POST /api/external/v1/editor/images/generations`, with `kind=spec` and registered `assetKind=icon-spec`. -2. `design-foundation` -> `assets/ui-prototype.png` via the same image generation endpoint with `kind=ui-design`, using the registered art-spec resource ID in `referenceImageSrcs`. -3. `art-asset-plan` -> transparent `assets/art-spritesheet.png` via `POST /api/external/v1/editor/icon-spritesheets/generations`, using the registered art-spec resource ID as `referenceImageSrc` and providing `iconDescriptions`. - -Do not use the UI prototype as the spritesheet specification. UI extraction requires a stable source image with red-box annotations and is outside this canonical DAG. - -## 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 | -| Read integration manifest | `GET /api/external/v1/agent-integration.json` | No auth required | -| Download complete Skill | `GET /api/external/v1/skill.zip` | No auth required; verify `archiveSha256` from the manifest | -| 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`, `expectedRevision` | -| 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` | -| Query generation | `GET /api/external/v1/generations/{operationId}` | API Key and returned `operationId` | - -## Generation Endpoints - -Every row below requires a stable `Idempotency-Key` header and returns an `ExternalEditorGenerationSubmissionResponse`, not a media result. The request fields shown are body fields. - -| User intent | Endpoint | Required fields | Common optional fields | -| --- | --- | --- | --- | -| Generate image/spec/character/UI/publication material | `POST /api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `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` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | -| Extract assets from UI design | `POST /api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `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` | - -## Submission and Polling - -1. Generate one printable ASCII `Idempotency-Key` of 1-128 bytes for the logical request. -2. Submit the generation body. Persist `operationId`, `statusUrl`, and the idempotency key before doing more work. -3. On a lost or uncertain submission response, resend the exact body with the same key. Do not allocate a replacement key. -4. Poll `statusUrl` no faster than `pollAfterMs`: - - `queued` / `running`: display `phaseLabel`, `phaseDetail`, and `progress`, then continue polling. - - `completed`: consume `result`; if full canvas/library state is needed, reload the normal project or library read endpoint. - - `failed`: display the safe `error` and stop polling. -5. A client-side polling timeout leaves the operation pending. Keep `operationId` for later queries; it does not authorize a new generation request. - -`result` contains compact stable artifact references such as `objectKey`, resource/asset IDs, dimensions, media type, task ID, and warning data. It does not contain a complete project/canvas snapshot, Data URL, Blob URL, expiring signed URL, worker lease, or internal queue/provider diagnostics. Resolve preview/download access from a stable `objectKey` through `/assets/read-url`. - -## Image Post-processing Style - -The request-body top-level `style` field controls deterministic image post-processing. It is separate from `generationInputs.artSpec.style`, which only describes the requested visual language for prompting. - -- Omitted, `null`, an empty string, and `"none"` all disable post-processing without a warning. -- `"pixelArt"` enables deterministic pixel-art snapping for ordinary image generation (omit `kind`), `kind: "character"`, and icon spritesheet generation. -- Unknown strings, or `"pixelArt"` on unsupported image kinds such as `spec`, `quick-edit`, `ui-design`, or `publication-material`, continue without style processing and return `warning.code: "unsupported-image-style"`. -- A non-string JSON value is malformed and returns HTTP `400`. Keep the field extensible; do not treat the current examples as a closed client-side enum. - -Image or character generation with pixel-art snapping: - -```json -{ - "prompt": "生成一个正面站立的像素风冒险者角色", - "kind": "character", - "style": "pixelArt" -} -``` - -Icon spritesheet generation with pixel-art snapping: - -```json -{ - "referenceImageSrc": "generated-character-drafts/editor/external-editor-references/icon-spec.png", - "iconDescriptions": ["木剑", "圆盾", "红色药水"], - "style": "pixelArt" -} -``` - -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. Character animation may complete without an `asset`; after `status=completed`, create an asset from the first returned frame only when the compact result still lacks one. The bundled helper performs this fallback. - -## Completed Warning Handling - -The initial HTTP `202` only acknowledges durable submission. Character image generation (including character redraw through `kind: "character"`), icon spritesheet generation, and UI asset extraction may later reach `completed` with warning data; completion does not imply that all requested post-processed derivatives exist. The query-level `warning` is display-ready text, while compact `result.warning` / `result.sliceWarning` preserve structured artifact semantics when present. - -- Use compact references to reload the authoritative project/library state. Character results use `resource` / `asset`, while icon spritesheet and UI extraction use `spritesheetResource` / `spritesheetAsset`. `result.warning.code: "postprocess-failed-source-preserved"` means the saved source is the main result. Character output has no transparent derivative, while icon spritesheet and UI extraction have no transparent spritesheet and no slices. Display the reason; do not construct missing assets or retry generation from scratch. -- `result.sliceWarning` is only for a transparent spritesheet that was created successfully but could not be split automatically. Use the complete transparent spritesheet; do not claim individual slices. -- `warning` and `sliceWarning` are mutually exclusive only for `postprocess-failed-source-preserved`, because that failure never reaches slicing. A general `warning` produced by image-style normalization (`unsupported-image-style`) or pixel-art snapping can coexist with `sliceWarning`; render both reasons instead of picking one. - -## 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. A completed external result is compact; use returned resource/asset IDs and stable object keys, then reload the project or library endpoint 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`. diff --git a/.codex/skills/genarrative-external-editor-api/references/authentication-and-safety.md b/.codex/skills/genarrative-external-editor-api/references/authentication-and-safety.md new file mode 100644 index 000000000..ec51c8675 --- /dev/null +++ b/.codex/skills/genarrative-external-editor-api/references/authentication-and-safety.md @@ -0,0 +1,146 @@ +# 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 +``` + +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": "", + "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": "", + "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. diff --git a/.codex/skills/genarrative-external-editor-api/references/capability-routing.md b/.codex/skills/genarrative-external-editor-api/references/capability-routing.md new file mode 100644 index 000000000..b89050080 --- /dev/null +++ b/.codex/skills/genarrative-external-editor-api/references/capability-routing.md @@ -0,0 +1,87 @@ +# 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 may return no direct library asset; after completion, create one from the first returned frame only when the compact result still lacks an asset. + +## 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 `referenceImageSrc` plus concrete `iconDescriptions`. + +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}`. diff --git a/.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md b/.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md new file mode 100644 index 000000000..cbd26dfa7 --- /dev/null +++ b/.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md @@ -0,0 +1,236 @@ +# 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 may complete without a direct `asset` field. To preserve the canvas/library invariant, create a library asset from the first returned frame only if the compact result lacks one. Prefer `client.animate_character(..., canvasSession=session, canvasTitle="...")`, which implements this fallback. + +## Art Spec and Image Request + +Carry the current art spec in `generationInputs.artSpec` and reflect important constraints in the prompt: + +```json +{ + "prompt": "一张横版幻想森林背景,适合游戏主视觉,无文字", + "aspectRatio": "16:9", + "imageSize": "1K", + "projectId": "", + "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 controls deterministic post-processing: + +- Omitted, `null`, empty string, or `"none"`: disable post-processing without warning. +- `"pixelArt"`: enable pixel-art snapping for ordinary image generation, `kind: "character"`, and icon spritesheet generation. +- 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"]], +) +``` + +For character animation from a local-only source, use actual dimensions and a stable synthetic layer ID: + +```json +{ + "sourceLayerId": "external-reference-hero", + "sourceImageSrc": "", + "sourceWidth": 720, + "sourceHeight": 1280, + "promptText": "让角色自然呼吸并轻微转身", + "resolution": "720p", + "ratio": "9:16", + "frameCount": 40, + "durationSeconds": 5, + "model": "seedance2.0-fast" +} +``` + +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. +- `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. diff --git a/docs/openapi/genarrative-external-v1.openapi.json b/docs/openapi/genarrative-external-v1.openapi.json index c5419d682..2f14023cd 100644 --- a/docs/openapi/genarrative-external-v1.openapi.json +++ b/docs/openapi/genarrative-external-v1.openapi.json @@ -146,7 +146,7 @@ ], "operationId": "callExternalEditorMcp", "summary": "调用托管式远程 MCP", - "description": "MCP 2025-11-25 Streamable HTTP JSON 端点。使用与 REST API 相同的 Bearer API Key;生成工具立即返回异步 operation。", + "description": "MCP 2025-11-25 Streamable HTTP JSON 端点。使用与 REST API 相同的 Bearer API Key;生成工具立即返回异步 operation。resources/list 和 resources/read 提供 usage、OpenAPI、Skill 主入口以及 capability routing、API operations、authentication and safety、requests and outputs 四篇渐进式 reference;本地脚本仍只通过完整 Skill ZIP 提供。", "security": [ { "ExternalApiKey": [] diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 791b98e17..df3a484be 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5891,7 +5891,7 @@ - 异步契约:External v1 的图片生成、图片编辑、图标图集、UI 素材提取、角色动画、视频、音效和背景音乐八类 POST 固定持久化入 `external_generation_job` 并返回 HTTP `202 + operationId/statusUrl/pollAfterMs`;不受站内 `GENARRATIVE_EXTERNAL_GENERATION_MODE=inline` 影响。每次逻辑生成必须携带稳定 `Idempotency-Key`,网络结果未知或调用方轮询超时时复用原键和原 operationId,不得换键重提。 - 查询与结果:新增 owner-safe `GET /api/external/v1/generations/{operationId}`。`queued/running` 返回 phase/progress,`completed` 返回 compact 稳定 artifact 引用,`failed` 返回脱敏错误,跨 owner 按不存在处理。compact result 允许 objectKey、resource/asset ID、assetObjectId、尺寸、媒体类型、taskId 和告警;禁止完整 project/canvas、Data URL、Blob URL、临时 signed URL、内部 provider 原文和 lease/fencing 控制字段。 -- MCP:新增托管 `/api/external/v1/mcp`,使用现有 External API Key Bearer 鉴权和无协议 session 的 Streamable HTTP JSON direct 模式。MCP tools 从同一 OpenAPI operation 形成并复用 External REST router;生成 tool 显式要求 `idempotencyKey`,另有统一任务查询 tool。MCP resources 提供使用说明、OpenAPI 和 Skill 入口。禁止开放内部 SpacetimeDB MCP、worker procedure、controller 或队列控制面。 -- Agent 发现:新增公开 `agent-integration.json`、`skill/SKILL.md` 和 `skill.zip`。manifest 同时声明 MCP、OpenAPI、完整 Skill archive、SHA-256 和包内清单;archive 必须包含 `SKILL.md`、API 选择参考、stdlib Python helper 和 `agents/openai.yaml`,不能只提供 OpenAPI JSON,也不能包含 API Key、本机路径或个人配置。不支持 MCP 或需要本地文件上传编排的 Agent 使用该 Skill 包。 +- MCP:新增托管 `/api/external/v1/mcp`,使用现有 External API Key Bearer 鉴权和无协议 session 的 Streamable HTTP JSON direct 模式。MCP tools 从同一 OpenAPI operation 形成并复用 External REST router;生成 tool 显式要求 `idempotencyKey`,另有统一任务查询 tool。MCP resources 提供使用说明、OpenAPI、Skill 入口 `SKILL.md` 和 `references/capability-routing.md`、`references/api-operations.md`、`references/authentication-and-safety.md`、`references/requests-and-outputs.md` 四篇稳定 reference;日后新增 reference 时必须同步新增独立 resource。MCP Agent 直接调用托管 tools,不安装 CLI,也不将脚本、测试或 workflow 暴露为 MCP resources。禁止开放内部 SpacetimeDB MCP、worker procedure、controller 或队列控制面。 +- Agent 发现:新增公开 `agent-integration.json`、`skill/SKILL.md` 和 `skill.zip`。manifest 同时声明 MCP、OpenAPI、完整 Skill archive、SHA-256 和包内清单;archive 必须包含 `SKILL.md`、上述四篇 references、stdlib Python helper 和 `agents/openai.yaml` 七个声明文件,不能只提供 OpenAPI JSON,也不能包含 API Key、本机路径或个人配置。完整 `skill.zip` 只供不支持 MCP 或需要本地文件上传编排的 Agent 使用,不作为 MCP resource。 - 兼容边界:这是基于「截至 2026-07-31 尚无外部第三方存量调用方」接受的 v1 原地 breaking change;一旦出现外部活跃 Key、公开契约或联调方,后续破坏性变更必须保留兼容、经过弃用期或升级 `/api/external/v2`。 - 关联文档:`docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md`、`docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md`、`.codex/skills/genarrative-external-editor-api/SKILL.md`。 diff --git a/docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md b/docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md index 66c41f2b8..95df5f0bb 100644 --- a/docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md +++ b/docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md @@ -45,7 +45,7 @@ v1 只开放以下能力: - `GET /api/external/v1/agent-integration.json`:公开导出 Agent 集成发现 manifest,声明 MCP、OpenAPI、Skill 入口、完整 Skill archive、archive SHA-256 和包内文件清单。 - `GET /api/external/v1/skill/SKILL.md`:公开读取 Skill 原始入口。 - `GET /api/external/v1/skill.zip`:公开下载完整 Skill 包。 -- `POST /api/external/v1/mcp`:使用相同 Bearer API Key 的托管式 Streamable HTTP MCP;对外暴露本节 OpenAPI operation tools 以及使用说明、OpenAPI、Skill 三类资源,不开放内部 SpacetimeDB MCP。 +- `POST /api/external/v1/mcp`:使用相同 Bearer API Key 的托管式 Streamable HTTP MCP;对外暴露本节 OpenAPI operation tools 以及使用说明、OpenAPI、Skill 入口 `SKILL.md` 和逐个 Skill reference 文档,不开放内部 SpacetimeDB MCP。 八类生成 POST 全部要求 `Idempotency-Key`,成功只返回 HTTP `202 Accepted`、`operationId`、`kind`、`status`、`statusUrl`、`pollAfterMs` 和 `updatedAtMicros`。调用方不得把 `202` 当作媒体生成完成,也不得在网络结果不确定时换一个幂等键重新提交。 @@ -79,26 +79,35 @@ provider 原图已保存但透明背景处理最终失败时,worker 保留原 MCP tools 从同一份 OpenAPI operation 自动形成 snake_case 名称,并在进程内复用 External REST router,因此鉴权、scope、owner、入参、幂等、计费和结果查询契约只有一份。生成 tools 把 `idempotencyKey` 显式放进参数,因为 MCP transport 的 Authorization 头不能代替逐次业务幂等键。工具结果使用 `structuredContent`;业务失败使用 `isError=true` 的结构化安全错误,协议不可路由时才返回 JSON-RPC error。 -MCP 暴露三个稳定资源: +MCP 暴露下列稳定文本资源: - `genarrative://external-editor/usage`:关键工作流和异步轮询规则。 - `genarrative://external-editor/openapi`:完整 External v1 OpenAPI。 -- `genarrative://external-editor/skill`:Skill 入口正文与完整 Skill 包下载地址。 +- `genarrative://external-editor/skill`:Skill 入口原文;保留首版已声明的稳定 URI。 +- `genarrative://external-editor/skill/references/capability-routing.md`:能力选路与场景边界。 +- `genarrative://external-editor/skill/references/api-operations.md`:公开 API 操作、必填字段与调用顺序。 +- `genarrative://external-editor/skill/references/authentication-and-safety.md`:API Key 鉴权、幂等与安全边界。 +- `genarrative://external-editor/skill/references/requests-and-outputs.md`:异步提交、状态轮询与 compact 结果语义。 + +Skill 日后新增 `references/` 文档时,MCP 必须按包内相对路径逐个增加 `genarrative://external-editor/skill/references/` resource,不得只暴露 `SKILL.md` 而让 Agent 无法读取其引用。当前稳定 reference 精确为上述四篇,不得声明不存在的 reference。MCP Agent 直接调用托管 tools,不下载或安装 Python CLI;`scripts/`、`tests/` 和 `.github/workflows/` 不作为 MCP resources。 MCP 必须始终复用 `require_external_api_key`,owner 从 `ExternalApiPrincipal` 获取,不接受请求参数伪造 owner。禁止透传内部 `external_generation_job` procedure、worker controller、SpacetimeDB MCP 或 lease/fencing 控制面。 ## Agent 集成发现与完整 Skill 包 -`agent-integration.json` 是机器可读的统一发现入口。支持远程 MCP 的 Agent 读取其中 `mcp.transport/url/authentication`;不支持 MCP 的 Agent 下载 `skill.archive`,核对 `archiveSha256`,解压后从 `genarrative-external-editor-api/SKILL.md` 进入。 +`agent-integration.json` 是机器可读的统一发现入口。支持远程 MCP 的 Agent 读取其中 `mcp.transport/url/authentication`,通过 MCP resources 读取 Skill 入口和所需 references,直接调用 MCP tools,不安装 CLI。仅不支持 MCP,或需要在 Agent 所在机器上编排本地文件上传的调用方下载 `skill.archive`,核对 `archiveSha256`,解压后从 `genarrative-external-editor-api/SKILL.md` 进入。 Skill archive 必须至少包含: - `SKILL.md` -- `references/api-selection.md` +- `references/capability-routing.md` +- `references/api-operations.md` +- `references/authentication-and-safety.md` +- `references/requests-and-outputs.md` - `scripts/genarrative_external_api.py` - `agents/openai.yaml` -包由 api-server 直接从仓库同源文件构建,不能只返回光秃秃的 OpenAPI JSON,也不能把个人 API Key、环境配置或本机路径写入包。Python helper 对上层保持便利的同步函数外观,但内部必须执行“异步提交 → 保存 operationId → 按 pollAfterMs 查询 → completed 返回 result”,查询超时应保留 operationId 供后续继续,不得换键重提。 +包由 api-server 直接从仓库同源文件构建,不能只返回光秃秃的 OpenAPI JSON,也不能把个人 API Key、环境配置或本机路径写入包。完整 `skill.zip` 只服务不支持 MCP 或需要本地文件编排的 Agent,不是 MCP resource catalog 的压缩包镜像。Python helper 对上层保持便利的同步函数外观,但内部必须执行“异步提交 → 保存 operationId → 按 pollAfterMs 查询 → completed 返回 result”,查询超时应保留 operationId 供后续继续,不得换键重提。 api-server 使用 `include_str!` 嵌入 OpenAPI 与 Skill 源文件;容器构建阶段必须同时复制 `docs/openapi/` 和 `.codex/skills/genarrative-external-editor-api/`,不能只复制 `server-rs/`,否则本地 Cargo 验证虽可通过,隔离镜像构建会在编译期找不到同源资源。 @@ -243,8 +252,8 @@ docs/openapi/genarrative-external-v1.openapi.json - 外部视频、角色动画、音效和音乐接口使用站内编辑器相同的请求校验、模型限制和价格校验。 - OpenAPI JSON 能被 `serde_json` 解析,且 security scheme 为 Bearer API Key。 - OpenAPI JSON 不包含 `/api/profile/api-keys`、`UserAccessToken` 或 API Key 管理 schema。 -- `agent-integration.json` 能发现 MCP、OpenAPI、Skill entry/archive;下载 archive 的 SHA-256 与 manifest 一致,ZIP 包含四个声明文件且不含凭据。 -- MCP 在无 Bearer 时返回 `401`,合法 Key 可完成 initialize、tools/list、resources/list/read 和生成提交/查询;多实例不依赖 sticky session,不暴露内部 SpacetimeDB MCP 或 worker 控制面。 +- `agent-integration.json` 能发现 MCP、OpenAPI、Skill entry/archive;下载 archive 的 SHA-256 与 manifest 一致,ZIP 包含 `SKILL.md`、四篇 references、Python helper 和 `agents/openai.yaml` 七个声明文件且不含凭据。 +- MCP 在无 Bearer 时返回 `401`,合法 Key 可完成 initialize、tools/list、resources/list/read 和生成提交/查询;resource catalog 必须包含 usage、OpenAPI、`skill` 主入口和当前全部 Skill references,当前精确为 `skill/references/capability-routing.md`、`skill/references/api-operations.md`、`skill/references/authentication-and-safety.md` 与 `skill/references/requests-and-outputs.md`,且不包含 CLI 脚本、测试或 workflow;多实例不依赖 sticky session,不暴露内部 SpacetimeDB MCP 或 worker 控制面。 - 外部素材库接口覆盖当前已有素材操作:直传凭证、素材对象确认、签名读取、读取素材库、创建 / 更新 / 删除文件夹、创建 / 更新 / 删除素材、创建项目画布资源。 - 外部项目接口覆盖当前已有项目管理操作:项目列表、最近项目、创建、读取、重命名、删除和默认画布保存。 - 外部素材生成接口覆盖当前已有编辑器素材操作:生图、重绘 / 调整、规范图生成、宣发素材生成、图标素材生成与拆分、UI 设计图生成与拆分、角色动画、视频、音效和背景音乐。 diff --git a/server-rs/crates/api-server/src/external_mcp.rs b/server-rs/crates/api-server/src/external_mcp.rs index 712d41b70..6f5c17dfa 100644 --- a/server-rs/crates/api-server/src/external_mcp.rs +++ b/server-rs/crates/api-server/src/external_mcp.rs @@ -29,12 +29,32 @@ const OPENAPI_JSON: &str = include_str!("../../../../docs/openapi/genarrative-external-v1.openapi.json"); const SKILL_MD: &str = include_str!("../../../../.codex/skills/genarrative-external-editor-api/SKILL.md"); +const SKILL_CAPABILITY_ROUTING_MD: &str = include_str!( + "../../../../.codex/skills/genarrative-external-editor-api/references/capability-routing.md" +); +const SKILL_API_OPERATIONS_MD: &str = include_str!( + "../../../../.codex/skills/genarrative-external-editor-api/references/api-operations.md" +); +const SKILL_AUTHENTICATION_AND_SAFETY_MD: &str = include_str!( + "../../../../.codex/skills/genarrative-external-editor-api/references/authentication-and-safety.md" +); +const SKILL_REQUESTS_AND_OUTPUTS_MD: &str = include_str!( + "../../../../.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md" +); const USAGE_URI: &str = "genarrative://external-editor/usage"; const OPENAPI_URI: &str = "genarrative://external-editor/openapi"; const SKILL_URI: &str = "genarrative://external-editor/skill"; +const SKILL_CAPABILITY_ROUTING_URI: &str = + "genarrative://external-editor/skill/references/capability-routing.md"; +const SKILL_API_OPERATIONS_URI: &str = + "genarrative://external-editor/skill/references/api-operations.md"; +const SKILL_AUTHENTICATION_AND_SAFETY_URI: &str = + "genarrative://external-editor/skill/references/authentication-and-safety.md"; +const SKILL_REQUESTS_AND_OUTPUTS_URI: &str = + "genarrative://external-editor/skill/references/requests-and-outputs.md"; const MAX_MCP_REST_RESPONSE_BYTES: usize = 4 * 1024 * 1024; -const MCP_INSTRUCTIONS: &str = r#"陶泥儿外部编辑器工具。先创建或复用画布项目,并创建与画布同名的素材文件夹;生成结果应同时写入画布和素材库。参考本地文件时先走上传票据和对象确认,不要把 Data URL、Blob URL 或临时签名 URL写入生成参数。所有生成工具都是异步提交:必须提供 idempotencyKey,提交后按 pollAfterMs 调用 get_external_editor_generation_job,只有 status=completed 时消费 result;查询超时不能重新提交。warning 表示主结果可用但存在降级,sliceWarning 表示完整透明图集可用但切片未完成。详细说明、OpenAPI 和 Skill 下载信息见 resources/list。"#; +const MCP_INSTRUCTIONS: &str = r#"陶泥儿外部编辑器工具。先创建或复用画布项目,并创建与画布同名的素材文件夹;生成结果应同时写入画布和素材库。参考本地文件时先走上传票据和对象确认,不要把 Data URL、Blob URL 或临时签名 URL写入生成参数。所有生成工具都是异步提交:必须提供 idempotencyKey,提交后按 pollAfterMs 调用 get_external_editor_generation_job,只有 status=completed 时消费 result;查询超时不能重新提交。warning 表示主结果可用但存在降级,sliceWarning 表示完整透明图集可用但切片未完成。详细说明、OpenAPI、Skill 主入口和分主题 references 见 resources/list;需要本地文件编排或不支持 MCP 时再下载 skill.zip。"#; #[derive(Clone, Debug)] struct McpOperation { @@ -135,20 +155,7 @@ impl ServerHandler for GenarrativeExternalMcp { _request: Option, _context: McpRequestContext, ) -> Result { - Ok(ListResourcesResult::with_all_items(vec![ - Resource::new(USAGE_URI, "usage") - .with_title("陶泥儿外部编辑器使用说明") - .with_description("画布、素材、上传、异步生成和告警处理工作流") - .with_mime_type("text/markdown"), - Resource::new(OPENAPI_URI, "openapi") - .with_title("陶泥儿外部编辑器 OpenAPI") - .with_description("MCP 工具所映射的完整 REST 契约") - .with_mime_type("application/json"), - Resource::new(SKILL_URI, "skill") - .with_title("陶泥儿外部编辑器 Skill") - .with_description("不支持 MCP 的 Agent 可下载完整 Skill 包") - .with_mime_type("text/markdown"), - ])) + Ok(ListResourcesResult::with_all_items(mcp_resources())) } async fn read_resource( @@ -156,23 +163,65 @@ impl ServerHandler for GenarrativeExternalMcp { request: ReadResourceRequestParams, _context: McpRequestContext, ) -> Result { - let (text, mime_type) = match request.uri.as_str() { - USAGE_URI => (MCP_INSTRUCTIONS.to_string(), "text/markdown"), - OPENAPI_URI => (OPENAPI_JSON.to_string(), "application/json"), - SKILL_URI => ( - format!( - "{SKILL_MD}\n\n完整 Skill 包:\n原始入口:" - ), - "text/markdown", - ), - _ => return Err(ErrorData::resource_not_found("资源不存在", None)), - }; + let (text, mime_type) = mcp_resource_contents(request.uri.as_str()) + .ok_or_else(|| ErrorData::resource_not_found("资源不存在", None))?; Ok(ReadResourceResult::new(vec![ ResourceContents::text(text, request.uri).with_mime_type(mime_type), ])) } } +fn mcp_resources() -> Vec { + vec![ + Resource::new(USAGE_URI, "usage") + .with_title("陶泥儿外部编辑器使用说明") + .with_description("画布、素材、上传、异步生成和告警处理工作流") + .with_mime_type("text/markdown"), + Resource::new(OPENAPI_URI, "openapi") + .with_title("陶泥儿外部编辑器 OpenAPI") + .with_description("MCP 工具所映射的完整 REST 契约") + .with_mime_type("application/json"), + Resource::new(SKILL_URI, "skill") + .with_title("陶泥儿外部编辑器 Skill") + .with_description("外部编辑器 Skill 主入口;细节按 references 渐进读取") + .with_mime_type("text/markdown"), + Resource::new(SKILL_CAPABILITY_ROUTING_URI, "skill-capability-routing") + .with_title("陶泥儿外部编辑器能力路由") + .with_description("按用户意图选择 MCP tool 或 External v1 API") + .with_mime_type("text/markdown"), + Resource::new(SKILL_API_OPERATIONS_URI, "skill-api-operations") + .with_title("陶泥儿外部编辑器 API 操作") + .with_description("项目、素材、上传、异步生成和任务查询操作表") + .with_mime_type("text/markdown"), + Resource::new( + SKILL_AUTHENTICATION_AND_SAFETY_URI, + "skill-authentication-and-safety", + ) + .with_title("陶泥儿外部编辑器认证与安全") + .with_description("API Key、幂等、重试、本地文件和安全边界") + .with_mime_type("text/markdown"), + Resource::new(SKILL_REQUESTS_AND_OUTPUTS_URI, "skill-requests-and-outputs") + .with_title("陶泥儿外部编辑器请求与输出") + .with_description("请求构造、异步轮询、完成结果和告警处理") + .with_mime_type("text/markdown"), + ] +} + +fn mcp_resource_contents(uri: &str) -> Option<(&'static str, &'static str)> { + match uri { + USAGE_URI => Some((MCP_INSTRUCTIONS, "text/markdown")), + OPENAPI_URI => Some((OPENAPI_JSON, "application/json")), + SKILL_URI => Some((SKILL_MD, "text/markdown")), + SKILL_CAPABILITY_ROUTING_URI => Some((SKILL_CAPABILITY_ROUTING_MD, "text/markdown")), + SKILL_API_OPERATIONS_URI => Some((SKILL_API_OPERATIONS_MD, "text/markdown")), + SKILL_AUTHENTICATION_AND_SAFETY_URI => { + Some((SKILL_AUTHENTICATION_AND_SAFETY_MD, "text/markdown")) + } + SKILL_REQUESTS_AND_OUTPUTS_URI => Some((SKILL_REQUESTS_AND_OUTPUTS_MD, "text/markdown")), + _ => None, + } +} + fn build_mcp_operations() -> Vec { let openapi: Value = serde_json::from_str(OPENAPI_JSON).expect("embedded OpenAPI must parse"); let mut operations = Vec::new(); @@ -556,6 +605,42 @@ mod tests { header::{ACCEPT, HOST}, }; + #[test] + fn mcp_resources_expose_complete_progressive_skill_documents() { + let resources = + serde_json::to_string(&mcp_resources()).expect("resources should serialize"); + let expected = [ + (USAGE_URI, MCP_INSTRUCTIONS, "text/markdown"), + (OPENAPI_URI, OPENAPI_JSON, "application/json"), + (SKILL_URI, SKILL_MD, "text/markdown"), + ( + SKILL_CAPABILITY_ROUTING_URI, + SKILL_CAPABILITY_ROUTING_MD, + "text/markdown", + ), + ( + SKILL_API_OPERATIONS_URI, + SKILL_API_OPERATIONS_MD, + "text/markdown", + ), + ( + SKILL_AUTHENTICATION_AND_SAFETY_URI, + SKILL_AUTHENTICATION_AND_SAFETY_MD, + "text/markdown", + ), + ( + SKILL_REQUESTS_AND_OUTPUTS_URI, + SKILL_REQUESTS_AND_OUTPUTS_MD, + "text/markdown", + ), + ]; + assert_eq!(mcp_resources().len(), expected.len()); + for (uri, contents, mime_type) in expected { + assert!(resources.contains(uri), "missing MCP resource {uri}"); + assert_eq!(mcp_resource_contents(uri), Some((contents, mime_type))); + } + } + #[test] fn openapi_operations_become_unique_mcp_tools() { let names: std::collections::BTreeMap<_, _> = MCP_OPERATIONS @@ -728,42 +813,52 @@ mod tests { ); } - let request = Request::builder() - .method(Method::POST) - .uri("/api/external/v1/mcp") - .header(HOST, "localhost") - .header(CONTENT_TYPE, "application/json") - .header(ACCEPT, "application/json, text/event-stream") - .header("mcp-protocol-version", "2025-11-25") - .body(Body::from( - json!({ - "jsonrpc": "2.0", - "id": 3, - "method": "resources/read", - "params": {"uri": OPENAPI_URI} - }) - .to_string(), - )) - .expect("resource read request should build"); - let response = service() - .oneshot(request) - .await - .expect("MCP service should be infallible"); - assert_eq!(response.status(), StatusCode::OK); - let payload: Value = serde_json::from_slice( - &response - .into_body() - .collect() + for (id, uri, expected_text) in [ + (3, OPENAPI_URI, "陶泥儿外部编辑器 OpenAPI"), + ( + 4, + SKILL_REQUESTS_AND_OUTPUTS_URI, + "All eight generation POST routes require", + ), + ] { + let request = Request::builder() + .method(Method::POST) + .uri("/api/external/v1/mcp") + .header(HOST, "localhost") + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json, text/event-stream") + .header("mcp-protocol-version", "2025-11-25") + .body(Body::from( + json!({ + "jsonrpc": "2.0", + "id": id, + "method": "resources/read", + "params": {"uri": uri} + }) + .to_string(), + )) + .expect("resource read request should build"); + let response = service() + .oneshot(request) .await - .expect("resource response body should read") - .to_bytes(), - ) - .expect("resource response should be JSON"); - assert!( - payload["result"]["contents"][0]["text"] - .as_str() - .is_some_and(|value| value.contains("陶泥儿外部编辑器 OpenAPI")) - ); + .expect("MCP service should be infallible"); + assert_eq!(response.status(), StatusCode::OK); + let payload: Value = serde_json::from_slice( + &response + .into_body() + .collect() + .await + .expect("resource response body should read") + .to_bytes(), + ) + .expect("resource response should be JSON"); + assert!( + payload["result"]["contents"][0]["text"] + .as_str() + .is_some_and(|value| value.contains(expected_text)), + "{uri}" + ); + } } #[tokio::test] diff --git a/server-rs/crates/api-server/src/external_skill_api.rs b/server-rs/crates/api-server/src/external_skill_api.rs index d5c03af14..c072c2bf6 100644 --- a/server-rs/crates/api-server/src/external_skill_api.rs +++ b/server-rs/crates/api-server/src/external_skill_api.rs @@ -16,15 +16,33 @@ use zip::{ZipWriter, write::SimpleFileOptions}; use crate::http_error::AppError; const SKILL_ROOT: &str = "genarrative-external-editor-api"; -const SKILL_FILES: [(&str, &str); 4] = [ +const SKILL_FILES: [(&str, &str); 7] = [ ( "SKILL.md", include_str!("../../../../.codex/skills/genarrative-external-editor-api/SKILL.md"), ), ( - "references/api-selection.md", + "references/capability-routing.md", include_str!( - "../../../../.codex/skills/genarrative-external-editor-api/references/api-selection.md" + "../../../../.codex/skills/genarrative-external-editor-api/references/capability-routing.md" + ), + ), + ( + "references/api-operations.md", + include_str!( + "../../../../.codex/skills/genarrative-external-editor-api/references/api-operations.md" + ), + ), + ( + "references/authentication-and-safety.md", + include_str!( + "../../../../.codex/skills/genarrative-external-editor-api/references/authentication-and-safety.md" + ), + ), + ( + "references/requests-and-outputs.md", + include_str!( + "../../../../.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md" ), ), ( @@ -119,6 +137,7 @@ mod tests { fn archive_contains_complete_skill_bundle() { let bytes = build_external_skill_archive().expect("skill archive should build"); let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).expect("archive should parse"); + assert_eq!(archive.len(), SKILL_FILES.len()); for (path, contents) in SKILL_FILES { let name = format!("{SKILL_ROOT}/{path}"); let mut file = archive.by_name(&name).expect("skill file should exist"); @@ -127,4 +146,20 @@ mod tests { assert_eq!(actual, contents); } } + + #[tokio::test] + async fn integration_manifest_matches_complete_skill_archive() { + let bytes = build_external_skill_archive().expect("skill archive should build"); + let Json(manifest) = get_external_agent_integration_manifest() + .await + .expect("integration manifest should build"); + let expected_files = SKILL_FILES + .map(|(path, _)| format!("{SKILL_ROOT}/{path}")) + .to_vec(); + assert_eq!(manifest["skill"]["files"], json!(expected_files)); + assert_eq!( + manifest["skill"]["archiveSha256"], + json!(format!("{:x}", Sha256::digest(&bytes))) + ); + } }