Merge remote-tracking branch 'origin/master' into codex/game-agent-resource-workbench
# Conflicts: # docs/project-memory/shared-memory/decision-log.md
This commit is contained in:
@@ -27,7 +27,7 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
|
||||
## Essential Invariants
|
||||
|
||||
- Authenticate MCP and business API calls with `Authorization: Bearer <tnr_sk_...>`. Never ask the user to paste a key into chat or place one in repository files.
|
||||
- All eight generation POST routes require `Idempotency-Key` and return HTTP `202`; `202` is durable acceptance, not a media result.
|
||||
- All nine generation POST routes require `Idempotency-Key` and return HTTP `202`; `202` is durable acceptance, not a media result.
|
||||
- Retry an uncertain submission only with the exact same body and the same idempotency key. A polling timeout is not permission to generate again.
|
||||
- Use stable references such as `objectKey`, project resource ID, or asset ID where each operation permits them. Image edit/redraw is stricter: `sourceReferenceId` accepts only a registered project resource ID or asset ID; upload confirmation alone is not enough. Use `/assets/read-url` only for temporary preview/download access.
|
||||
- Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either.
|
||||
@@ -106,6 +106,21 @@ client.generate_image(
|
||||
)
|
||||
```
|
||||
|
||||
For background removal, pass a stable owner-scoped object key, project resource ID, or asset ID; the helper keeps the same asynchronous submission and polling contract:
|
||||
|
||||
```python
|
||||
session = client.prepare_canvas_session("去背景画布")
|
||||
client.remove_background(
|
||||
"editor-upload/object.png",
|
||||
source_width=720,
|
||||
source_height=1280,
|
||||
canvasSession=session,
|
||||
assetLabel="去背景结果",
|
||||
)
|
||||
```
|
||||
|
||||
Background removal preserves the source pixel size. For normal canvas placement with `canvasSession`, pass the real `source_width` and `source_height`, or provide both `canvasWidth` and `canvasHeight`; the helper rejects missing dimensions instead of guessing a square placeholder. `assetKind` may only describe a static image and must match the authoritative source record. Prefer a project resource ID or asset ID when the same object key has multiple semantic registrations; for a raw object key outside in-place replacement, pass `sourceResourceId` to disambiguate. Passing `targetLayerId` selects in-place replacement: the helper retains the session's project/library context but does not inject `canvasCompletion`, and it rejects an explicit `canvasCompletion` combined with `targetLayerId`. The target layer must point to the same authoritative object as the source, and the server durably binds a raw object key to that target resource for Worker revalidation.
|
||||
|
||||
Helper convenience methods wait locally, but the server still uses short asynchronous submit/status requests. For durable caller-controlled orchestration, call `submit_generation`, persist its `operationId` and idempotency key, then call `get_generation` or `wait_for_generation`.
|
||||
|
||||
For character animation, pass the canvas session and asset label to `animate_character`. The helper submits asynchronously and returns the completed compact result containing the authoritative formal `resource` and `asset`; do not synthesize a library asset from the first frame.
|
||||
|
||||
@@ -51,6 +51,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
|
||||
| --- | --- | --- | --- |
|
||||
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
|
||||
| Background removal | `/api/external/v1/editor/images/background-removals` | `sourceImageSrc` | `projectId`, `sourceResourceId`, `targetLayerId`, static-image `assetKind`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
|
||||
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
@@ -58,7 +59,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
|
||||
| Sound effect | `/api/external/v1/editor/audios/sound-effects/generations` | `prompt` | `model`, `duration`, `loop`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Background music | `/api/external/v1/editor/audios/background-music/generations` | `gptDescriptionPrompt`, `makeInstrumental` | `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
|
||||
Poll all eight through:
|
||||
Poll all nine through:
|
||||
|
||||
```text
|
||||
GET /api/external/v1/generations/{operationId}
|
||||
@@ -72,6 +73,7 @@ Supply the `operationId` returned by submission. Poll no faster than `pollAfterM
|
||||
- Pass `assetFolderId` plus `assetLabel` for image, edit, icon spritesheet, video, sound effect, and BGM operations when supported.
|
||||
- UI extraction uses `assetFolderId` and `spritesheetLabel`.
|
||||
- Character animation accepts `assetFolderId` and `assetLabel`. Its completed compact result directly returns the final `assetKind="character-animation"` resource and asset with `imageSequenceFrames` and `imageSequenceDurationMs`; never create a duplicate first-frame resource or asset.
|
||||
- Background removal derives the final static-image `assetKind` from the authoritative source record. A conflicting request kind or any video, audio, animation, or image-sequence kind returns `400` before queueing. Without `canvasCompletion`, `targetLayerId` must point to the same authoritative object as `sourceImageSrc` (prefer `assetObjectId`, otherwise canonical bucket/object key).
|
||||
- If a caller must manually create a `character-animation` resource or asset, put the authoritative frames and total sequence duration in `imageSequenceFrames` and `imageSequenceDurationMs`. Keep `generationInputs` replayable: it must not contain legacy runtime fields such as `characterAnimation`, `frames`, `previewVideoPath`, `frameCount`, `fps`, or `durationSeconds`.
|
||||
- Reload project/library state after completion when full current state is required.
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ Infer what is already clear and ask only for missing fields that block the selec
|
||||
| --- | --- |
|
||||
| Generate a background, character, spec, UI mockup, or publication image | Image generation |
|
||||
| Redraw, retouch, or replace an existing image | Image edit |
|
||||
| Remove the background from an existing image | Background removal |
|
||||
| Generate from a local reference | Upload and confirm the local file, then image generation or edit |
|
||||
| Build a reusable transparent icon/game atlas from a visual spec | Icon spritesheet generation |
|
||||
| Extract marked assets from an existing UI design | UI design asset extraction |
|
||||
|
||||
@@ -15,7 +15,7 @@ Use this reference to build generation payloads, carry canvas/library context, p
|
||||
|
||||
## Asynchronous Submission
|
||||
|
||||
All eight generation POST routes require `Idempotency-Key` and return HTTP `202` with an `ExternalEditorGenerationSubmissionResponse` shaped like:
|
||||
All nine generation POST routes require `Idempotency-Key` and return HTTP `202` with an `ExternalEditorGenerationSubmissionResponse` shaped like:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -71,6 +71,8 @@ status = client.get_generation(operation_id)
|
||||
completed = client.wait_for_generation(operation_id)
|
||||
```
|
||||
|
||||
Background removal uses the same submission and polling state machine. `sourceImageSrc` must be a stable owner-scoped object key, project resource ID, or asset ID; never pass a Data URL, Blob URL, or expiring signed URL. An explicit resource ID or asset ID is resolved before any object-key fallback. If a raw object key has multiple registrations with conflicting authoritative metadata, pass `sourceResourceId` to disambiguate or the server returns `400`. Use `projectId + canvasCompletion` for normal canvas placement. When `canvasCompletion` is absent, `projectId + targetLayerId` replaces an existing resource-backed layer and is rejected before queueing if the target is invalid; for a raw object key, the target resource becomes the durable source binding rechecked by the Worker. If both placement fields are absent, the server does not add the result to the canvas. The completed compact result contains the stable output object key, dimensions, and persisted resource/asset references when requested.
|
||||
|
||||
## Canvas and Asset-Library Completion
|
||||
|
||||
For endpoints that support these fields, include:
|
||||
@@ -98,6 +100,8 @@ A minimal `canvasCompletion` is:
|
||||
|
||||
`dialogId` is optional. The placeholder supplies canvas placement and completion coordinates; it is not a final media pixel-size constraint. For successful pixel-art snapping, the result layer uses the final logical-grid PNG dimensions even when they differ from the placeholder. Do not reconstruct canvas state from completion results. Reload the project and asset library when complete authoritative snapshots are needed.
|
||||
|
||||
Background removal preserves the source image dimensions. For normal canvas placement, the Python helper therefore requires the real `source_width` and `source_height` whenever `canvasSession` is used without an explicit `canvasWidth` plus `canvasHeight`; it never substitutes a square default. Passing `targetLayerId` instead selects in-place replacement, so the helper keeps the session's project/library fields without injecting `canvasCompletion` and rejects callers that explicitly combine both placement modes. The request `assetKind` is optional, static-image only, and must equal the authoritative source type when one exists. An in-place target must resolve to the same authoritative source object; a raw object key is bound to that target resource instead of relying on project-list order.
|
||||
|
||||
Character animation accepts `assetFolderId` and `assetLabel` and persists the final transparent sequence directly. Its completed compact result includes the authoritative `assetKind="character-animation"` resource and asset with `imageSequenceFrames` and `imageSequenceDurationMs`. Use those records directly and never synthesize a duplicate asset from the first frame.
|
||||
|
||||
For the lower-level asset/resource creation endpoints, `generationInputs` is replayable request context rather than a media-runtime container. When `assetKind` is `character-animation`, the server rejects legacy runtime keys including `characterAnimation`, `frames`, `previewVideoPath`, `frameCount`, `fps`, and `durationSeconds`; send the formal sequence through `imageSequenceFrames` and `imageSequenceDurationMs`. Internal processing audit keys such as `screenColorHex`, `mattingProvider`, and `mattingModel` are removed before persistence.
|
||||
|
||||
@@ -561,6 +561,58 @@ class GenarrativeExternalClient:
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def remove_background(
|
||||
self,
|
||||
source_image_src: str,
|
||||
source_width: int | None = None,
|
||||
source_height: int | None = None,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
source_image_src = normalize_optional_text(source_image_src)
|
||||
if not source_image_src:
|
||||
raise GenarrativeApiError(
|
||||
"source_image_src must be an owner-scoped object key, resource ID, or asset ID"
|
||||
)
|
||||
if (source_width is None) != (source_height is None):
|
||||
raise GenarrativeApiError("source_width and source_height must be provided together")
|
||||
if source_width is not None and (
|
||||
source_width <= 0 or source_height is None or source_height <= 0
|
||||
):
|
||||
raise GenarrativeApiError("source_width and source_height must be positive integers")
|
||||
session = fields.get("canvasSession")
|
||||
if session is None:
|
||||
session = fields.get("canvas_session")
|
||||
target_layer_id = normalize_optional_text(fields.get("targetLayerId"))
|
||||
if target_layer_id and fields.get("canvasCompletion") is not None:
|
||||
raise GenarrativeApiError(
|
||||
"targetLayerId and canvasCompletion are mutually exclusive for background removal"
|
||||
)
|
||||
canvas_width = fields.get("canvasWidth")
|
||||
canvas_height = fields.get("canvasHeight")
|
||||
if (canvas_width is None) != (canvas_height is None):
|
||||
raise GenarrativeApiError("canvasWidth and canvasHeight must be provided together")
|
||||
if session is not None and canvas_width is None and not target_layer_id:
|
||||
if source_width is None or source_height is None:
|
||||
raise GenarrativeApiError(
|
||||
"remove_background requires source_width and source_height when canvasSession is used without canvasWidth/canvasHeight"
|
||||
)
|
||||
fields["canvasWidth"] = source_width
|
||||
fields["canvasHeight"] = source_height
|
||||
self._apply_canvas_session_fields(
|
||||
fields,
|
||||
fields.get("assetLabel", "去背景结果"),
|
||||
source_width or 1,
|
||||
source_height or 1,
|
||||
)
|
||||
if target_layer_id:
|
||||
fields.pop("canvasCompletion", None)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/images/background-removals",
|
||||
{"sourceImageSrc": source_image_src, **fields},
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def generate_icon_spritesheet(
|
||||
self,
|
||||
reference_id: str,
|
||||
@@ -687,11 +739,14 @@ def _self_test() -> None:
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
|
||||
b"\x00\x00\x00\x02\x00\x00\x00\x03\x08\x06\x00\x00\x00"
|
||||
)
|
||||
with tempfile.NamedTemporaryFile(suffix="Hero Image.png") as fh:
|
||||
with tempfile.NamedTemporaryFile(suffix="Hero Image.png", delete=False) as fh:
|
||||
fh.write(png)
|
||||
fh.flush()
|
||||
assert image_dimensions(fh.name) == (2, 3)
|
||||
assert source_layer_id_from_path(fh.name).startswith("external-reference-")
|
||||
temp_path = fh.name
|
||||
try:
|
||||
assert image_dimensions(temp_path) == (2, 3)
|
||||
assert source_layer_id_from_path(temp_path).startswith("external-reference-")
|
||||
finally:
|
||||
Path(temp_path).unlink(missing_ok=True)
|
||||
assert unwrap_envelope({"ok": True, "data": {"upload": 1}}) == {"upload": 1}
|
||||
client = GenarrativeExternalClient(api_key="test")
|
||||
session = {"projectId": "proj-demo", "assetFolderId": "editor-asset-folder-demo"}
|
||||
@@ -773,6 +828,54 @@ def _self_test() -> None:
|
||||
assert len(result["asset"]["imageSequenceFrames"]) == 2
|
||||
assert result["asset"]["imageSequenceDurationMs"] == 4000
|
||||
calls.clear()
|
||||
background_result = client.remove_background(
|
||||
"uploads/source.png",
|
||||
720,
|
||||
1280,
|
||||
canvasSession=session,
|
||||
assetLabel="去背景结果",
|
||||
)
|
||||
assert background_result["taskId"] == "task-demo"
|
||||
assert calls[0]["path"] == "/api/external/v1/editor/images/background-removals"
|
||||
assert calls[0]["body"]["sourceImageSrc"] == "uploads/source.png"
|
||||
assert calls[0]["body"]["projectId"] == "proj-demo"
|
||||
assert calls[0]["body"]["assetFolderId"] == "editor-asset-folder-demo"
|
||||
assert calls[0]["body"]["assetLabel"] == "去背景结果"
|
||||
assert calls[0]["body"]["canvasCompletion"]["title"] == "去背景结果"
|
||||
assert calls[0]["body"]["canvasCompletion"]["placeholder"]["width"] == 720
|
||||
assert calls[0]["body"]["canvasCompletion"]["placeholder"]["height"] == 1280
|
||||
calls.clear()
|
||||
client.remove_background(
|
||||
"uploads/source.png",
|
||||
canvasSession=session,
|
||||
targetLayerId="layer-1",
|
||||
assetLabel="原位去背景结果",
|
||||
)
|
||||
assert calls[0]["body"]["projectId"] == "proj-demo"
|
||||
assert calls[0]["body"]["assetFolderId"] == "editor-asset-folder-demo"
|
||||
assert calls[0]["body"]["assetLabel"] == "原位去背景结果"
|
||||
assert calls[0]["body"]["targetLayerId"] == "layer-1"
|
||||
assert "canvasCompletion" not in calls[0]["body"]
|
||||
calls.clear()
|
||||
try:
|
||||
client.remove_background(
|
||||
"uploads/source.png",
|
||||
canvasSession=session,
|
||||
targetLayerId="layer-1",
|
||||
canvasCompletion={"title": "冲突完成指令"},
|
||||
)
|
||||
except GenarrativeApiError as error:
|
||||
assert "targetLayerId and canvasCompletion are mutually exclusive" in str(error)
|
||||
else:
|
||||
raise AssertionError("background removal must reject conflicting canvas placement modes")
|
||||
assert calls == []
|
||||
try:
|
||||
client.remove_background("uploads/source.png", canvasSession=session)
|
||||
except GenarrativeApiError as error:
|
||||
assert "source_width and source_height" in str(error)
|
||||
else:
|
||||
raise AssertionError("canvas background removal must not guess source dimensions")
|
||||
assert calls == []
|
||||
client.generate_icon_spritesheet(
|
||||
"editor-resource-spec",
|
||||
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@genarrative/ai-game-creator-shell",
|
||||
"private": true,
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/start-tauri-dev.mjs",
|
||||
|
||||
@@ -1733,12 +1733,12 @@ if (
|
||||
}
|
||||
|
||||
if (
|
||||
tauriConfig.version !== '0.1.1' ||
|
||||
packageConfig.version !== '0.1.1' ||
|
||||
cargoPackageVersion !== '0.1.1'
|
||||
tauriConfig.version !== '0.1.2' ||
|
||||
packageConfig.version !== '0.1.2' ||
|
||||
cargoPackageVersion !== '0.1.2'
|
||||
) {
|
||||
throw new Error(
|
||||
'AI game creator standard release must remain version 0.1.1 while game-chat uses its dedicated version',
|
||||
'AI game creator standard release must remain version 0.1.2 while game-chat uses its dedicated version',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1695,7 +1695,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"agent-runtime-core",
|
||||
"axum",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
||||
+6
-5
@@ -12,11 +12,12 @@ Let the client derive projections from real disk changes and trusted tool result
|
||||
1. Write executable source to the current `game/` files and media to the relative paths returned by approved tools.
|
||||
2. Before using or deriving an existing asset, call `agc_list_registered_assets` and select its `localAssetId`; never infer a source from a filename or submit a local path, platform ID, object key, operation ID, or idempotency key as a generation argument.
|
||||
3. When the user explicitly asks to create or derive video, character animation, sound effect, or background music, call `agc_create_or_derive_resource`. Use `create` only for video/audio without a source and `derive` with a registered `sourceLocalAssetId`; character animation is always derived from an image.
|
||||
4. Preserve existing relative paths when a small edit is sufficient so client resource identities remain stable.
|
||||
5. Do not edit `.agent/manifest.json`, revision counters, version records, resource IDs, canvas identities, source provenance, generation ledgers, or browser receipts by hand.
|
||||
6. Do not create a version when no game file changed. The client compares content fingerprints and advances revision only after an actual source change.
|
||||
7. Do not claim a resource or version is visible before the client projects it. If projection is missing, report the changed relative files and let the client re-read durable state.
|
||||
8. Never move HTML, CSS, or JavaScript into documentation folders. They belong to the game-code projection; prose, design notes, and instructions remain documents.
|
||||
4. When the user explicitly asks to remove an image background, call `agc_remove_background` with a registered image `sourceLocalAssetId` and an output name. The client requires the signed-in account, owns canvas/folder context and task identity, and returns only bounded queue state.
|
||||
5. Preserve existing relative paths when a small edit is sufficient so client resource identities remain stable.
|
||||
6. Do not edit `.agent/manifest.json`, revision counters, version records, resource IDs, canvas identities, source provenance, generation ledgers, or browser receipts by hand.
|
||||
7. Do not create a version when no game file changed. The client compares content fingerprints and advances revision only after an actual source change.
|
||||
8. Do not claim a resource or version is visible before the client projects it. If projection is missing, report the changed relative files and let the client re-read durable state.
|
||||
9. Never move HTML, CSS, or JavaScript into documentation folders. They belong to the game-code projection; prose, design notes, and instructions remain documents.
|
||||
|
||||
Call `agc_read_skill_resource` with `skillName="agc-client-projection"` and `relativePath="references/projection-contract.md"` when a request touches asset identity, revision behavior, or version history.
|
||||
|
||||
|
||||
+2
@@ -11,3 +11,5 @@ Do not collapse these facts. A playable file can exist before projection refresh
|
||||
`agc_list_registered_assets` is the only Direct read path for manifest resource identity. Its relative path and stable identifiers are evidence; omitted prompt, model, provider route, signed URL, host path, and credentials are intentionally not available to Codex.
|
||||
|
||||
`agc_create_or_derive_resource` accepts only semantic intent. The client resolves `sourceLocalAssetId`, creates stable request identities, recovers matching pending operations, serializes paid submissions, writes supported media into the current canvas and same-name asset folder, validates downloaded bytes, commits the local manifest transaction, and returns redacted warnings. A tool error or timeout is not permission to generate again with a new identity.
|
||||
|
||||
`agc_remove_background` is the semantic image post-processing path. It accepts only a registered image `sourceLocalAssetId` and output name; the client resolves the formal source resource, canvas/folder context, stable operation identity, idempotency key, and authenticated External v1 `/api/external/v1/editor/images/background-removals` call. Its result is bounded queue state; Codex must not poll internal workers, construct source URLs, or retry with a new identity after an uncertain response.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": "agc-skill-pack.v1",
|
||||
"version": "2026-08-24.1",
|
||||
"version": "2026-08-24.2",
|
||||
"skills": [
|
||||
{
|
||||
"name": "agc-project-structure",
|
||||
@@ -79,20 +79,22 @@
|
||||
"triggers": [
|
||||
"新增或替换游戏文件",
|
||||
"查询、创建或派生已登记媒体资源",
|
||||
"去除已登记图片背景",
|
||||
"素材或版本未显示",
|
||||
"推理 revision 与版本关系"
|
||||
],
|
||||
"requiredTools": [
|
||||
"agc_tools.agc_read_skill_resource",
|
||||
"agc_tools.agc_list_registered_assets",
|
||||
"agc_tools.agc_create_or_derive_resource"
|
||||
"agc_tools.agc_create_or_derive_resource",
|
||||
"agc_tools.agc_remove_background"
|
||||
],
|
||||
"files": [
|
||||
"SKILL.md",
|
||||
"agents/openai.yaml",
|
||||
"references/projection-contract.md"
|
||||
],
|
||||
"sha256": "4b49d54f430028839362a0accf1a5c83878fb1e4eb9b929e0c4912c17470f112"
|
||||
"sha256": "07d64f118dc1ecb36c37b92f272cf237649aa7f53fb939197e705d6afa2de315"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1299,6 +1299,97 @@ async fn bridge_create_or_derive_resource(
|
||||
}
|
||||
}
|
||||
|
||||
async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Value) -> Value {
|
||||
let result = async {
|
||||
bridge_reject_unknown_fields(arguments, &["sourceLocalAssetId", "assetName"])?;
|
||||
enforce_project_permission_policy(&state.root, "canvas.asset_generate")?;
|
||||
enforce_project_permission_policy(&state.root, "asset.register")?;
|
||||
let source_asset_id = bridge_bounded_string(arguments, "sourceLocalAssetId", 80)?;
|
||||
let asset_name = bridge_bounded_string(
|
||||
arguments,
|
||||
"assetName",
|
||||
DIRECT_TOOL_BRIDGE_MAX_RESOURCE_NAME_CHARS,
|
||||
)?;
|
||||
let manifest = read_existing_manifest_for_project(&state.root)?;
|
||||
let source_asset = manifest
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.id == source_asset_id)
|
||||
.ok_or_else(|| "sourceLocalAssetId 不属于当前项目已登记资源".to_string())?;
|
||||
if !source_asset.media_type.starts_with("image/") {
|
||||
return Err("抠图工具只接受当前项目已登记的图片资源".to_string());
|
||||
}
|
||||
let source_resource_id = source_asset
|
||||
.source
|
||||
.resource_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty() && !value.starts_with("local-asset:"))
|
||||
.ok_or_else(|| "图片资源缺少可供抠图服务使用的正式 resourceId".to_string())?
|
||||
.to_string();
|
||||
let (api_base_url, api_key, session) = resolve_canvas_sync_api_credentials(None, None)?;
|
||||
let access = ExternalEditorBindingAccess::new(&api_base_url, &api_key, session.as_ref())?;
|
||||
let client = reqwest::Client::new();
|
||||
let context =
|
||||
prepare_external_canvas_generation_context(&state.root, &client, &access).await?;
|
||||
let fingerprint = format!("{}\0{}", source_asset_id, asset_name);
|
||||
let (_operation_id, idempotency_key) = state.resource_request_ids(&fingerprint)?;
|
||||
let route = "/api/external/v1/editor/images/background-removals";
|
||||
let response = client
|
||||
.post(format!("{}{}", api_base_url, route))
|
||||
.bearer_auth(api_key)
|
||||
.header("Idempotency-Key", idempotency_key)
|
||||
.json(&json!({
|
||||
"sourceImageSrc": source_resource_id,
|
||||
"projectId": manifest.project_id,
|
||||
"assetKind": source_asset.kind,
|
||||
"assetFolderId": context.asset_folder_id,
|
||||
"assetLabel": asset_name,
|
||||
"sourceResourceId": source_resource_id,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("抠图服务提交失败:{error}"))?;
|
||||
let status = response.status();
|
||||
let payload = response
|
||||
.json::<Value>()
|
||||
.await
|
||||
.map_err(|error| format!("抠图服务响应无法解析:{error}"))?;
|
||||
if !status.is_success() {
|
||||
return Err(format!("抠图服务提交失败:HTTP {}", status.as_u16()));
|
||||
}
|
||||
let queue_state = external_editor_response_data(&payload).clone();
|
||||
Ok::<_, String>(json!({
|
||||
"status": "queued",
|
||||
"sourceLocalAssetId": source_asset_id,
|
||||
"assetName": asset_name,
|
||||
"projectId": manifest.project_id,
|
||||
"assetFolderId": context.asset_folder_id,
|
||||
"queueState": bridge_safe_queue_state(queue_state),
|
||||
}))
|
||||
}
|
||||
.await;
|
||||
match result {
|
||||
Ok(value) => bridge_tool_result(value.to_string(), Vec::new(), false),
|
||||
Err(error) => bridge_tool_result(
|
||||
redact_agent_runtime_error(&state.root, &error, 480),
|
||||
Vec::new(),
|
||||
true,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn bridge_safe_queue_state(value: Value) -> Value {
|
||||
let object = value.as_object();
|
||||
json!({
|
||||
"operationId": object.and_then(|value| value.get("operationId")).and_then(Value::as_str),
|
||||
"status": object.and_then(|value| value.get("status")).and_then(Value::as_str),
|
||||
"phaseLabel": object.and_then(|value| value.get("phaseLabel")).and_then(Value::as_str),
|
||||
"progress": object.and_then(|value| value.get("progress")).and_then(Value::as_u64),
|
||||
"updatedAtMicros": object.and_then(|value| value.get("updatedAtMicros")).and_then(Value::as_u64),
|
||||
})
|
||||
}
|
||||
|
||||
fn bridge_art_resources(
|
||||
root: &Path,
|
||||
asset_paths: &[String],
|
||||
@@ -1548,6 +1639,7 @@ async fn handle_direct_tool_bridge(
|
||||
"agc_create_or_derive_resource" => {
|
||||
bridge_create_or_derive_resource(&state, &request.arguments).await
|
||||
}
|
||||
"agc_remove_background" => bridge_remove_background(&state, &request.arguments).await,
|
||||
"agc_browser_playtest" => bridge_browser_playtest(&state.root, &request.arguments).await,
|
||||
"agc_web_search" => bridge_web_search(&state.root, &request.arguments).await,
|
||||
_ => bridge_tool_result("未知或未审核的客户端工具".to_string(), Vec::new(), true),
|
||||
@@ -1992,4 +2084,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_background_removal_queue_projection_is_bounded() {
|
||||
let projection = bridge_safe_queue_state(json!({
|
||||
"operationId": "background-removal-1",
|
||||
"status": "queued",
|
||||
"phaseLabel": "排队中",
|
||||
"progress": 0,
|
||||
"updatedAtMicros": 1,
|
||||
"error": "private provider detail",
|
||||
"signedUrl": "https://private.invalid/result"
|
||||
}));
|
||||
assert_eq!(projection["operationId"], "background-removal-1");
|
||||
assert!(projection.get("error").is_none());
|
||||
assert!(projection.get("signedUrl").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,28 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool) -> Value {
|
||||
"additionalProperties": false
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "agc_remove_background",
|
||||
"description": "为当前项目已登记的图片资源去除背景。客户端使用当前登录账号的抠图服务、项目画布和素材目录,模型只能提供已登记资源身份与结果名称;不会返回 Token、内部路由、宿主路径或临时签名 URL。",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sourceLocalAssetId": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 80,
|
||||
"description": "必须来自 agc_list_registered_assets 返回的当前项目图片资源 localAssetId"
|
||||
},
|
||||
"assetName": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS
|
||||
}
|
||||
},
|
||||
"required": ["sourceLocalAssetId", "assetName"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "agc_browser_playtest",
|
||||
"description": "使用当前客户端的受限 Chromium 对当前游戏执行真实 desktop/mobile 双视口运行、截图、控制台、网络、Canvas/WebGL 和有限交互探针。",
|
||||
@@ -380,6 +402,17 @@ fn validate_resource_generation_arguments(arguments: &Value) -> Result<(), Strin
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_remove_background_arguments(arguments: &Value) -> Result<(), String> {
|
||||
validate_tool_object_fields(arguments, &["sourceLocalAssetId", "assetName"])?;
|
||||
bounded_tool_string(arguments, "sourceLocalAssetId", 80)?;
|
||||
bounded_tool_string(
|
||||
arguments,
|
||||
"assetName",
|
||||
DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tool_attempt(arguments: &Value) -> Result<usize, String> {
|
||||
let attempt = arguments
|
||||
.get("attempt")
|
||||
@@ -518,6 +551,13 @@ async fn call_agc_create_or_derive_resource(arguments: &Value) -> Value {
|
||||
call_client_tool_bridge("agc_create_or_derive_resource", arguments).await
|
||||
}
|
||||
|
||||
async fn call_agc_remove_background(arguments: &Value) -> Value {
|
||||
if let Err(error) = validate_remove_background_arguments(arguments) {
|
||||
return mcp_tool_result(error, Vec::new(), true);
|
||||
}
|
||||
call_client_tool_bridge("agc_remove_background", arguments).await
|
||||
}
|
||||
|
||||
async fn call_agc_browser_playtest(arguments: &Value) -> Value {
|
||||
if let Err(error) = tool_attempt(arguments) {
|
||||
return mcp_tool_result(error, Vec::new(), true);
|
||||
@@ -589,6 +629,7 @@ async fn handle_direct_tools_mcp_request(_root: &Path, request: Value) -> Option
|
||||
"agc_create_or_derive_resource" => {
|
||||
call_agc_create_or_derive_resource(&arguments).await
|
||||
}
|
||||
"agc_remove_background" => call_agc_remove_background(&arguments).await,
|
||||
"agc_browser_playtest" => call_agc_browser_playtest(&arguments).await,
|
||||
"agc_web_search" => call_agc_web_search(&arguments).await,
|
||||
_ => mcp_tool_result("未知或未审核的 AGC 工具".to_string(), Vec::new(), true),
|
||||
@@ -713,6 +754,7 @@ mod tests {
|
||||
"taonier_prepare_game_art",
|
||||
"agc_list_registered_assets",
|
||||
"agc_create_or_derive_resource",
|
||||
"agc_remove_background",
|
||||
"agc_browser_playtest"
|
||||
]
|
||||
);
|
||||
@@ -767,6 +809,7 @@ mod tests {
|
||||
"taonier_prepare_game_art",
|
||||
"agc_list_registered_assets",
|
||||
"agc_create_or_derive_resource",
|
||||
"agc_remove_background",
|
||||
"agc_browser_playtest",
|
||||
"agc_web_search"
|
||||
]
|
||||
@@ -795,6 +838,17 @@ mod tests {
|
||||
"assetName": "森林过场"
|
||||
}))
|
||||
.is_ok());
|
||||
assert!(validate_remove_background_arguments(&json!({
|
||||
"sourceLocalAssetId": "asset-image-1",
|
||||
"assetName": "角色透明图"
|
||||
}))
|
||||
.is_ok());
|
||||
assert!(validate_remove_background_arguments(&json!({
|
||||
"sourceLocalAssetId": "asset-image-1",
|
||||
"assetName": "角色透明图",
|
||||
"projectPath": "/private"
|
||||
}))
|
||||
.is_err());
|
||||
assert!(validate_resource_generation_arguments(&json!({
|
||||
"kind": "character-animation",
|
||||
"mode": "derive",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Genarrative AI Game Creator",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"identifier": "world.genarrative.ai-game-creator",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'react';
|
||||
|
||||
import type { AuthUser } from '../../../../packages/shared/src/contracts/auth';
|
||||
import brandIcon from '../../../../packages/shared/src/icons/taonier-product-ip.png';
|
||||
import {
|
||||
clearStoredAuthAccessToken,
|
||||
getClientAuthErrorMessage,
|
||||
@@ -72,9 +73,12 @@ export class ClientRuntimeErrorBoundary extends Component<
|
||||
return this.props.children;
|
||||
}
|
||||
return (
|
||||
<main className="client-auth-shell" aria-label="客户端页面加载失败">
|
||||
<main
|
||||
className="client-auth-shell platform-theme platform-theme--light"
|
||||
aria-label="客户端页面加载失败"
|
||||
>
|
||||
<section className="client-auth-panel">
|
||||
<span className="client-auth-logo">tn</span>
|
||||
<img className="client-auth-logo" src={brandIcon} alt="陶泥儿" />
|
||||
<div>
|
||||
<h1>客户端页面加载失败</h1>
|
||||
<p className="client-auth-status">{this.state.errorMessage}</p>
|
||||
@@ -389,9 +393,12 @@ export function AuthenticatedClient({
|
||||
|
||||
if (authStatus === 'checking') {
|
||||
return (
|
||||
<main className="client-auth-shell" aria-label="登录状态检查">
|
||||
<main
|
||||
className="client-auth-shell platform-theme platform-theme--light"
|
||||
aria-label="登录状态检查"
|
||||
>
|
||||
<section className="client-auth-panel">
|
||||
<span className="client-auth-logo">tn</span>
|
||||
<img className="client-auth-logo" src={brandIcon} alt="陶泥儿" />
|
||||
<h1>正在检查登录状态</h1>
|
||||
</section>
|
||||
</main>
|
||||
@@ -400,9 +407,12 @@ export function AuthenticatedClient({
|
||||
|
||||
if (!authUser) {
|
||||
return (
|
||||
<main className="client-auth-shell" aria-label="登录">
|
||||
<main
|
||||
className="client-auth-shell platform-theme platform-theme--light"
|
||||
aria-label="登录"
|
||||
>
|
||||
<form className="client-auth-panel" onSubmit={handleLoginSubmit}>
|
||||
<span className="client-auth-logo">tn</span>
|
||||
<img className="client-auth-logo" src={brandIcon} alt="陶泥儿" />
|
||||
<div>
|
||||
<h1>登录陶泥儿 GameAgent</h1>
|
||||
<p>登录后进入首页和本地项目工作区</p>
|
||||
|
||||
@@ -123,50 +123,73 @@ textarea {
|
||||
display: grid;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background: #f8fafc;
|
||||
color: #111827;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: var(--platform-body-fill);
|
||||
color: var(--platform-text-strong);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.client-auth-panel {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: min(360px, 100%);
|
||||
gap: 16px;
|
||||
padding: 28px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 18px 52px rgb(15 23 42 / 12%);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 24px;
|
||||
background: var(--platform-panel-fill);
|
||||
box-shadow: var(--platform-panel-shadow);
|
||||
}
|
||||
|
||||
.client-auth-panel::before {
|
||||
position: absolute;
|
||||
top: -94px;
|
||||
right: -76px;
|
||||
width: 204px;
|
||||
height: 204px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
var(--platform-hero-glow-b),
|
||||
transparent 68%
|
||||
);
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.client-auth-logo {
|
||||
display: grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
background: #101010;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 9px 15px rgb(182 98 63 / 18%));
|
||||
}
|
||||
|
||||
.client-auth-panel h1 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.client-auth-panel p {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 6px 0 0;
|
||||
color: #6b7280;
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.client-auth-panel label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
color: #374151;
|
||||
color: var(--platform-text-base);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -175,42 +198,68 @@ textarea {
|
||||
height: 38px;
|
||||
min-width: 0;
|
||||
padding: 0 11px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
border: 1px solid var(--platform-subpanel-border);
|
||||
border-radius: 12px;
|
||||
background: var(--platform-input-fill);
|
||||
color: var(--platform-text-strong);
|
||||
font: inherit;
|
||||
transition:
|
||||
border-color 160ms ease,
|
||||
box-shadow 160ms ease,
|
||||
background 160ms ease;
|
||||
}
|
||||
|
||||
.client-auth-panel select {
|
||||
height: 38px;
|
||||
min-width: 0;
|
||||
padding: 0 11px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
border: 1px solid var(--platform-subpanel-border);
|
||||
border-radius: 12px;
|
||||
background: var(--platform-input-fill);
|
||||
color: var(--platform-text-strong);
|
||||
font: inherit;
|
||||
transition:
|
||||
border-color 160ms ease,
|
||||
box-shadow 160ms ease,
|
||||
background 160ms ease;
|
||||
}
|
||||
|
||||
.client-auth-panel select:focus {
|
||||
border-color: #111827;
|
||||
outline: 2px solid rgb(17 24 39 / 10%);
|
||||
border-color: var(--platform-surface-hover-border);
|
||||
outline: 2px solid var(--platform-input-focus-ring);
|
||||
background: var(--platform-input-fill-focus);
|
||||
}
|
||||
|
||||
.client-auth-panel input:focus {
|
||||
border-color: #111827;
|
||||
outline: 2px solid rgb(17 24 39 / 10%);
|
||||
border-color: var(--platform-surface-hover-border);
|
||||
outline: 2px solid var(--platform-input-focus-ring);
|
||||
background: var(--platform-input-fill-focus);
|
||||
}
|
||||
|
||||
.client-auth-panel button {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 38px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
border: 1px solid var(--platform-button-primary-border);
|
||||
border-radius: 12px;
|
||||
background: var(--platform-button-primary-fill);
|
||||
box-shadow: var(--platform-profile-action-shadow);
|
||||
color: var(--platform-button-primary-text);
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
box-shadow 160ms ease,
|
||||
filter 160ms ease;
|
||||
}
|
||||
|
||||
.client-auth-panel button:not(:disabled):hover {
|
||||
filter: brightness(1.04);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.client-auth-panel button:not(:disabled):active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.client-auth-panel button:disabled {
|
||||
@@ -219,23 +268,29 @@ textarea {
|
||||
}
|
||||
|
||||
.client-auth-tabs {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 4px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--platform-subpanel-border);
|
||||
border-radius: 14px;
|
||||
background: var(--platform-subpanel-fill);
|
||||
}
|
||||
|
||||
.client-auth-tabs button {
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
color: #4b5563;
|
||||
box-shadow: none;
|
||||
color: var(--platform-text-soft);
|
||||
}
|
||||
|
||||
.client-auth-tabs button.is-active {
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
border-color: var(--platform-button-primary-border);
|
||||
background: var(--platform-button-primary-fill);
|
||||
box-shadow: 0 8px 18px rgb(182 98 63 / 18%);
|
||||
color: var(--platform-button-primary-text);
|
||||
}
|
||||
|
||||
.client-auth-code-row {
|
||||
@@ -255,6 +310,17 @@ textarea {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.client-auth-shell {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.client-auth-panel {
|
||||
padding: 24px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.launcher-shell {
|
||||
--launcher-sidebar-width: 64px;
|
||||
display: grid;
|
||||
@@ -1604,8 +1670,7 @@ textarea {
|
||||
.launcher-development-canvas {
|
||||
display: grid;
|
||||
min-height: 300px;
|
||||
background:
|
||||
linear-gradient(#f3f4f6 1px, transparent 1px),
|
||||
background: linear-gradient(#f3f4f6 1px, transparent 1px),
|
||||
linear-gradient(90deg, #f3f4f6 1px, transparent 1px), #fff;
|
||||
background-size: 24px 24px;
|
||||
place-items: center;
|
||||
@@ -2456,8 +2521,7 @@ textarea {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(45deg, #101827 25%, transparent 25%),
|
||||
background: linear-gradient(45deg, #101827 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #101827 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #101827 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #101827 75%), #0b1220;
|
||||
@@ -3032,28 +3096,24 @@ textarea {
|
||||
}
|
||||
|
||||
.launcher-card-collage span:nth-child(1) {
|
||||
background:
|
||||
radial-gradient(circle at 45% 38%, #6b5c43 0 18%, transparent 19%),
|
||||
background: radial-gradient(circle at 45% 38%, #6b5c43 0 18%, transparent 19%),
|
||||
radial-gradient(circle at 62% 54%, #2f2a23 0 25%, transparent 26%),
|
||||
linear-gradient(135deg, #91a17e, #2c3327);
|
||||
}
|
||||
|
||||
.launcher-card-collage span:nth-child(2) {
|
||||
background:
|
||||
radial-gradient(circle at 58% 45%, #b28b5a 0 16%, transparent 17%),
|
||||
background: radial-gradient(circle at 58% 45%, #b28b5a 0 16%, transparent 17%),
|
||||
radial-gradient(circle at 48% 55%, #302114 0 24%, transparent 25%),
|
||||
linear-gradient(135deg, #d9e8ef, #9fb0a0);
|
||||
}
|
||||
|
||||
.launcher-card-collage span:nth-child(3) {
|
||||
background:
|
||||
radial-gradient(circle at 50% 54%, #eac9c0 0 22%, transparent 23%),
|
||||
background: radial-gradient(circle at 50% 54%, #eac9c0 0 22%, transparent 23%),
|
||||
linear-gradient(135deg, #f7d9d5, #dab6aa);
|
||||
}
|
||||
|
||||
.launcher-card-collage span:nth-child(4) {
|
||||
background:
|
||||
radial-gradient(circle at 56% 50%, #32271b 0 22%, transparent 23%),
|
||||
background: radial-gradient(circle at 56% 50%, #32271b 0 22%, transparent 23%),
|
||||
linear-gradient(135deg, #8e6c4e, #2b211b);
|
||||
}
|
||||
|
||||
@@ -3077,8 +3137,12 @@ textarea {
|
||||
.launcher-card-pattern > div {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
background:
|
||||
linear-gradient(45deg, transparent 46%, #fff 46% 54%, transparent 54%),
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
transparent 46%,
|
||||
#fff 46% 54%,
|
||||
transparent 54%
|
||||
),
|
||||
linear-gradient(135deg, #55405a, #e4b2a7);
|
||||
}
|
||||
|
||||
@@ -5539,8 +5603,7 @@ iframe.preview-frame {
|
||||
.game-resource-card-visual,
|
||||
.game-resource-card[data-preview-kind='media-image'] .game-resource-card-visual,
|
||||
.game-resource-card[data-preview-kind='video'] .game-resource-card-visual {
|
||||
background:
|
||||
linear-gradient(45deg, #f1ebe7 25%, transparent 25%),
|
||||
background: linear-gradient(45deg, #f1ebe7 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #f1ebe7 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #f1ebe7 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #f1ebe7 75%), #faf7f5;
|
||||
|
||||
@@ -35,6 +35,31 @@ export function registerAuthTests() {
|
||||
delete window.__TAURI__;
|
||||
});
|
||||
|
||||
it('renders the unauthenticated client with the shared light platform theme and product image', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(
|
||||
async (input: RequestInfo | URL) => {
|
||||
if (String(input) === '/api/auth/refresh') {
|
||||
return new Response('', { status: 401 });
|
||||
}
|
||||
throw new Error(`unexpected fetch ${String(input)}`);
|
||||
},
|
||||
);
|
||||
|
||||
render(
|
||||
React.createElement(AuthenticatedClient, null, () =>
|
||||
React.createElement('main', { 'aria-label': '已登录' }),
|
||||
),
|
||||
);
|
||||
|
||||
const login = await screen.findByRole('main', { name: '登录' });
|
||||
expect(login.className).toContain('platform-theme');
|
||||
expect(login.className).toContain('platform-theme--light');
|
||||
expect(screen.getByRole('img', { name: '陶泥儿' })).not.toBeNull();
|
||||
expect(
|
||||
screen.getByRole('heading', { name: '登录陶泥儿 GameAgent' }),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the client at login when the native platform session cannot be installed', async () => {
|
||||
window.__TAURI__ = {
|
||||
core: {
|
||||
|
||||
@@ -1170,6 +1170,60 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/external/v1/editor/images/background-removals": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Editor Images"
|
||||
],
|
||||
"operationId": "removeExternalEditorImageBackground",
|
||||
"summary": "去除编辑器图片背景",
|
||||
"description": "提交已有静态图片素材的异步去背景任务。sourceImageSrc 只接受当前账号拥有的稳定 objectKey、项目资源 ID 或素材 ID;禁止 Data URL、Blob URL 和临时 signed URL。assetKind 只能表达静态图片,并且存在权威来源记录时必须与其类型一致;视频、音频、动画和图片序列在入队前返回 400。服务端固定使用 complex 去背景模式,不会在失败时切换到其它 provider。需要写入画布时提供 projectId 与 canvasCompletion;仅需原位替换既有图层时提供 projectId 与 targetLayerId,且来源与目标必须指向同一权威对象。",
|
||||
"security": [
|
||||
{
|
||||
"ExternalApiKey": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/IdempotencyKey"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EditorBackgroundRemovalRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "去背景任务已持久化入队",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ExternalEditorGenerationSubmissionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/components/responses/Forbidden"
|
||||
},
|
||||
"502": {
|
||||
"$ref": "#/components/responses/UpstreamError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/external/v1/editor/icon-spritesheets/generations": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -2386,7 +2440,8 @@
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"description": "可选的来源项目资源 ID,用于在同一 objectKey 被多个资源以不同权威元数据登记时消歧。sourceImageSrc 已是项目资源 ID 或素材 ID 时按该显式 ID 定点解析;纯 objectKey 无法唯一确定权威来源时必须提供本字段,否则返回 400。原位替换使用纯 objectKey 且省略本字段时,服务端自动绑定 targetLayerId 对应的当前项目资源,并把该绑定写入队列供 Worker 复验。"
|
||||
},
|
||||
"assetKind": {
|
||||
"type": [
|
||||
@@ -3597,6 +3652,75 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EditorBackgroundRemovalRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"sourceImageSrc"
|
||||
],
|
||||
"properties": {
|
||||
"sourceImageSrc": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "当前账号拥有的稳定 objectKey、项目资源 ID 或素材 ID。禁止 Data URL、Blob URL 和临时 signed URL。"
|
||||
},
|
||||
"projectId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "可选项目上下文。提供 targetLayerId 时必须同时提供非空 projectId,否则在入队前返回 400。"
|
||||
},
|
||||
"targetLayerId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "提供时必须同时提供 projectId。仅在未提供 canvasCompletion 时用于原位替换既有画布图层,此时目标图层必须存在、关联当前项目静态图片资源;若图层提供 assetKindOverride,则以覆盖后的有效类型参与静态媒体门禁和来源类型比较。来源与目标必须指向同一权威对象:双方都有 assetObjectId 时按 ID 比较,任一缺失时回退 canonical bucket/objectKey;违反任一条件均在入队前返回 400。存在 canvasCompletion 时不参与原位替换。省略 targetLayerId 与 canvasCompletion 时只持久化请求指定的项目资源或素材库记录,不自动写入画布。"
|
||||
},
|
||||
"sourceResourceId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "可选的来源项目资源 ID,用于在同一 objectKey 被多个资源以不同权威元数据登记时消歧。sourceImageSrc 已是项目资源 ID 或素材 ID 时按该显式 ID 定点解析;纯 objectKey 无法唯一确定权威来源时必须提供本字段,否则返回 400。原位替换使用纯 objectKey 且省略本字段时,服务端自动绑定 targetLayerId 对应的当前项目资源,并把该绑定写入队列供 Worker 复验。"
|
||||
},
|
||||
"assetKind": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "可选静态图片语义类型。服务端优先采用来源项目资源或素材库记录的权威语义类型;资产对象存储类型只参与非静态媒体门禁,不覆盖语义类型。请求值与权威来源类型不一致,或任一记录表示 video、audio、animation、image-sequence、sound-effect、background-music 等非静态媒体时,在入队前返回 400。",
|
||||
"x-genarrative-media-family": "static-image"
|
||||
},
|
||||
"generationInputs": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
},
|
||||
"assetFolderId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"assetLabel": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"canvasCompletion": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EditorCanvasGenerationCompletion"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "画布生成占位完成指令。提供时优先按生成完成链路写入结果,targetLayerId 不参与原位替换。"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EditorImageGenerationResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
- 输出边界:资源查询和生成结果只投影相对路径、稳定 Canvas/resource/asset/task 身份、序列帧身份、pending 状态及脱敏告警;不返回完整 manifest、prompt、model、provider route、绝对路径、URL、Token、Cookie 或 API Key。角色动画及视频/音频新请求统一携带同名画布与素材目录上下文;已有冻结请求不迁移、不重写。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/SKILL.md`。
|
||||
|
||||
## 2026-08-23 External 去背景绑定权威静态来源与真实画布尺寸
|
||||
|
||||
- 背景:External v1 去背景曾把调用方 `assetKind` 原样带入持久化,并允许 `sourceImageSrc=A + targetLayerId=B` 覆盖不同资源;Python helper 又为所有画布完成请求固定生成 `1024×1024` 占位,导致非方形透明结果按占位尺寸拉伸。
|
||||
- 决策:入队前从当前 owner 的项目资源或素材库解析来源权威语义类型,资产对象存储类型只参与非静态媒体门禁;显式项目资源 ID / 素材 ID 优先于 objectKey 回退,同一纯 objectKey 对应的候选权威元数据不一致时返回 `400` 并要求用 `sourceResourceId` 或业务 ID 消歧,禁止按列表首条决定类型。请求类型冲突或任一记录属于视频、音频、动画、图片序列时返回 `400`,队列只保存服务端解析出的静态语义类型。无 `canvasCompletion` 的原位替换优先比较双方 `assetObjectId`,任一缺失时回退 canonical `(bucket, objectKey)`,并要求默认类型一致;纯 objectKey 省略 `sourceResourceId` 时自动绑定目标图层资源并写入队列,由 Worker 复验同一绑定。helper 使用画布会话时必须取得真实源宽高或显式 `canvasWidth + canvasHeight`,不再猜测方形尺寸。
|
||||
- 影响范围:External v1 去背景入队与 worker 复验、OpenAPI、Python helper、外部编辑器 skill 和相关契约测试;不修改 SpacetimeDB schema、BgFilter 协议或去背景输出尺寸语义。
|
||||
- 验证方式:覆盖非静态类型与权威类型冲突、来源/目标不同对象拒绝及同对象通过、非方形 helper completion;运行 api-server 定向测试、helper self-test、OpenAPI 解析、编码与 diff 门禁。
|
||||
|
||||
## 2026-08-20 UI Editor LLM 递归输出与参考图单文件限制
|
||||
|
||||
- 背景:结构识别、界面语义建议和多图合并直接把 LLM 工具 arguments 反序列化为递归树;结构识别与语义建议还在 async command 中同步读取并 base64 编码参考图。模型异常输出或过大图片可能造成不受控内存、栈和 async worker 占用。
|
||||
@@ -6499,6 +6506,7 @@
|
||||
- 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`。
|
||||
- 2026-08-23 补充:新增 `POST /api/external/v1/editor/images/background-removals` 后,External v1 生成 POST 由八类增至九类;该入口继续使用 `editor:image-generate` scope、稳定 `Idempotency-Key`、`202 + operationId` 与统一查询合同。外部请求只允许 OpenAPI 声明的去背景字段,拒绝内部 `taskId` 和其它未声明字段;入队前按当前 owner 解析稳定来源并规范化为权威 objectKey,同时预检、规范化项目与素材目录目标。提供 `targetLayerId` 时始终必须同时提供 `projectId`;没有 `canvasCompletion` 时目标图层必须存在并关联当前项目资源,存在 `canvasCompletion` 时沿用生成完成链路且不执行原位替换;目标无效、引用未登记或越权时不创建任务。未提供任一画布完成字段时不自动写入画布;任务 ID 仅由服务端队列生成。
|
||||
|
||||
## 2026-08-04 图片画布 BGM Prompt 采用唯一可见规范化文本与面板级同步提交锁
|
||||
|
||||
@@ -13571,6 +13579,7 @@
|
||||
- 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`。
|
||||
- 2026-08-23 补充:新增 `POST /api/external/v1/editor/images/background-removals` 后,External v1 生成 POST 由八类增至九类;该入口继续使用 `editor:image-generate` scope、稳定 `Idempotency-Key`、`202 + operationId` 与统一查询合同。外部请求只允许 OpenAPI 声明的去背景字段,拒绝内部 `taskId` 和其它未声明字段;入队前按当前 owner 解析稳定来源并规范化为权威 objectKey,同时预检、规范化项目与素材目录目标,未登记、越权引用和无效目标不创建任务;任务 ID 仅由服务端队列生成。托管 MCP 不再维护异步生成 operation 的幂等硬编码名单,而是从 OpenAPI operation/path 的 required `Idempotency-Key` header 自动生成 `idempotencyKey` 工具参数并转发同名 HTTP 头,避免新增 operation 只出现在 `tools/list` 却无法实际提交。
|
||||
|
||||
## 2026-08-04 图片画布 BGM Prompt 采用唯一可见规范化文本与面板级同步提交锁
|
||||
|
||||
@@ -14444,3 +14453,7 @@
|
||||
- 严格图集崩溃收口:workflow 在严格图集调用前先持久化 `strictSpritesheetPending` 并冻结底层严格事务覆盖的九项旧合同身份;旧路径可精确冻结为缺失。Provider 完成结果先绑定原 retained stage ledger。恢复在同一项目锁内对账严格事务;只有新九项合同、规范图/背景图替换锚点与 retained spritesheet result 三者一致才补写 `completed`,旧九项合同才允许补偿。旧合同判定、写 `compensating`、恢复两项素材与登记、回读和清锚点必须在同一项目锁内,重启已有 `compensating` 也重新判定;第三种混合、漂移或 foreign result 状态进入 reconciliation。不能在主图集与四切片已整体提交后仍按两文件 rollback 制造混合包;若中断前阶段告警尚未进入 durable completed result,恢复结果追加“原阶段告警无法完整重放”的明确 warning,不静默清空。
|
||||
- Direct 对话恢复从新到旧扫描全部合法 User 回合,遇到较新已回答回合继续向前,不得丢失更早未回答回合。成功返回时 Rust 已先持久化 assistant,前端冗余 append 失败也不得重跑 Provider;普通错误终态的显式 append 失败后,恢复 claim 必须保持到 React fallback writer 对同一稳定 assistant messageId 的写入明确成功或失败,不能在 writer 尚在途时按旧 `/history` 快照重跑。fallback 成功后释放 claim;fallback 失败时跳过该 writer 的无界迟到重试并释放 claim,后续显式 `/history` 才可复用原稳定 `clientTurnId`。终态收敛后删除 claim,避免长会话无界增长。
|
||||
- 正式资源提交结算遵守同一顺序:阶段三 commit 成功后先持久化 `asset-commit-settlement-pending`,恢复器幂等补齐 `asset-durable-committed` 公开投影与 staging revision,再发布私有终态;公开投影已经存在时不得重复增加草稿 revision。恢复必须把私有回执与阶段三 commit ledger、transaction journal、manifest 资产和事件 payload 的完整身份绑定,任一错配都保留 pending 并失败关闭。回归同时覆盖三个 durable write cut,以及私有回执、commit ledger、journal 错配。
|
||||
## 2026-08-24 AGC Direct 抠图语义工具
|
||||
|
||||
- 决策:将 External v1 `/api/external/v1/editor/images/background-removals` 通过 `agc_remove_background` 加入受控 `agc_tools`。工具只接受当前 manifest 的图片 `sourceLocalAssetId` 与结果名称;客户端负责正式 resourceId、画布/素材目录、稳定 operation/idempotency 身份、权限和错误脱敏,不向 Codex 暴露内部 BgFilter worker、凭据或任意 API。
|
||||
- 约束:异步结果只投影有界队列状态,不允许模型自行构造源 URL 或在不确定提交后更换请求身份;External v1 负责 API Key、幂等接收与统一 operation 查询,客户端不得绕过该契约。
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 背景
|
||||
|
||||
VectorEngine `gpt-image-2`、音频、LLM 等外部生成不能由面向外部调用方的 HTTP 请求长期等待上游、OSS 持久化和 SpacetimeDB 回写。站内保留受控 `inline` 排障模式;External v1 的八类生成则固定使用持久队列和统一查询接口,避免调用方超时后重复提交、重复扣费或丢失已完成结果。
|
||||
VectorEngine `gpt-image-2`、音频、LLM 等外部生成不能由面向外部调用方的 HTTP 请求长期等待上游、OSS 持久化和 SpacetimeDB 回写。站内保留受控 `inline` 排障模式;External v1 的九类生成则固定使用持久队列和统一查询接口,避免调用方超时后重复提交、重复扣费或丢失已完成结果。
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -117,7 +117,7 @@ pending/running -> cancelled (预留)
|
||||
- `queue`:默认值,HTTP handler 入队 `external_generation_job`,由 `external-generation-worker` 角色 claim lease 后执行;生产、预发和压测默认使用该模式。
|
||||
- `inline`:HTTP handler 直接调用同一个 worker executor,同步等待 provider、OSS 和 SpacetimeDB 写回完成后返回 `operation.status = completed`;只用于本地或低并发排查,不提供队列持久化、lease 重领和 worker 横向扩容。
|
||||
|
||||
External v1 八类生成不读取上述模式分支:即使进程配置为 `inline`,External handler 仍只做校验、幂等入队并返回 HTTP `202`。调用方按 `/api/external/v1/generations/{operationId}` 查询;这条外部契约不能因部署环境不同而从异步退化为同步响应。
|
||||
External v1 九类生成不读取上述模式分支:即使进程配置为 `inline`,External handler 仍只做校验、幂等入队并返回 HTTP `202`。调用方按 `/api/external/v1/generations/{operationId}` 查询;这条外部契约不能因部署环境不同而从异步退化为同步响应。
|
||||
|
||||
同一个 Rust binary 通过 `GENARRATIVE_PROCESS_ROLE` 切换:
|
||||
|
||||
@@ -220,7 +220,7 @@ inline 完成结果与 External v1 completed compact result 继续使用结构
|
||||
|
||||
### External v1 异步提交与查询
|
||||
|
||||
External v1 复用上述九类 editor job kind 中除手动去背景外的八类生成 kind。外部 POST handler 只负责 API Key scope、owner、请求校验和入队,不调用 `*_for_owner` 同步执行函数:
|
||||
External v1 复用上述全部九类 editor job kind,包括手动去背景。外部 POST handler 只负责 API Key scope、owner、请求校验和入队,不调用 `*_for_owner` 同步执行函数;去背景在入队前还会解析当前 owner 的稳定来源,并预检、规范化项目与素材目录目标。提供 `targetLayerId` 时必须同时提供 `projectId`;没有 `canvasCompletion` 时还要确认目标图层存在并关联当前项目资源,存在 `canvasCompletion` 时继续按生成完成链路写入而不执行原位替换:
|
||||
|
||||
1. 每个生成 POST 必须携带 `Idempotency-Key`。服务端把 owner、job kind、稳定键和规范请求纳入 dedupe;未知结果重试必须复用原键。
|
||||
2. 成功入队返回 HTTP `202`、`operationId`、`statusUrl`、`pollAfterMs`,并设置 `Location` / `Retry-After`;不返回 project、asset 或媒体结果。
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- 项目路径、projectId、当前 revision、源文件路径与媒体类型、operationId、Idempotency-Key、登录态、项目锁、付费提交、轮询恢复、下载校验与 manifest 事务全部由客户端持有。模型不能提交或覆盖这些字段。同一 Direct `clientTurnId + 规范语义参数` 生成稳定 UUID v4 身份;单回合同参重试复用原 operation,不同请求串行且最多四项。跨回合存在完全匹配的 pending 账本时优先恢复原 operation,不能换键重发。
|
||||
- 资源查询同时投影未完成 operation 的安全状态。媒体工具成功只返回 operation、本地相对路径、资源类型、Canvas/resource/asset/task 身份、正式序列帧以及脱敏后的 `warnings / sliceWarnings`;错误继续使用统一脱敏边界。客户端资源账本持久化 completed 结果的两类告警,committed replay 不能把历史告警伪装成空集合。
|
||||
- 角色动画、视频、音效和背景音乐在构造新的远端请求前统一准备当前项目同名画布与素材目录上下文,并在端点支持时携带 `projectId / assetFolderId / canvasCompletion`。角色动画 placeholder 使用源图片真实宽高,避免非方形角色进入画布时失真;正式 resource/asset 与序列帧继续直接复用 External 返回身份,不从首帧伪造重复资源。已有冻结 request body 或已受理 operation 保持不变,不因本次升级重建请求或重复扣费。
|
||||
- 抠图通过新增 `agc_remove_background` 语义工具开放:模型只提交当前 manifest 的图片 `sourceLocalAssetId` 与结果名称;客户端解析稳定 `resourceId`,准备同名画布/素材目录并生成稳定 operation/idempotency 身份,调用 External v1 `/api/external/v1/editor/images/background-removals` 后只返回有界队列状态。抠图服务仍由客户端和服务端负责源校验、BgFilter、素材登记与画布事务,Codex 不获得内部 worker、凭据或任意 API 调用权。
|
||||
|
||||
## 2026-08-23 AGC 资源生成补齐(视频 / 动画 / 音效 / 背景音乐)
|
||||
|
||||
@@ -16,7 +17,7 @@
|
||||
- 新建视频 / 音效 / 背景音乐时,先复用 `prepare_external_canvas_generation_context` 创建或匹配同名画板项目与素材库目录,并在生成请求中携带 `projectId / assetFolderId / canvasCompletion`,结果同时进入平台画布 / 素材库与本地 manifest。
|
||||
- durable 账本新增 `remoteLegacyPublicPath` 字段(旧账本默认缺省),用于角色动画预览视频的换签下载与恢复;资源提交、轮询、下载、manifest 提交和恢复队列继续复用既有资源编辑事务链。
|
||||
- 资源编辑请求指纹纳入 `generationMode`,同一 `operationId` 换生成模式重试时不再按旧账本模式恢复;旧账本继续通过 legacy 指纹兼容。
|
||||
- 去背景(`/api/editor/images/background-removals`)仍缺 External v1 契约,不在本次补齐范围;本次只补视频 / 角色动画 / 音效 / 背景音乐。
|
||||
- 去背景通过 External v1 语义工具开放;原始服务端 worker 和账号态内部路由仍不直接暴露给 Codex。
|
||||
|
||||
## 2026-08-19 UI Editor 节点右键菜单
|
||||
|
||||
@@ -780,6 +781,7 @@ game-project/
|
||||
|
||||
- `apps/ai-game-creator-shell` 是独立 Tauri App,不复用 `apps/desktop-shell`。
|
||||
- 独立客户端启动时先进入平台登录检查;未登录页默认展示手机号验证码登录,并保留密码登录切换。验证码登录调用平台后端 `/api/auth/phone/send-code` 与 `/api/auth/phone/login`,密码登录继续调用 `/api/auth/entry`;Tauri dev 下 `/api` 走本轮动态 AGC Vite 代理,普通发布版静态窗口下通过 Tauri 原生 HTTP transport 固定访问开发服务器 `https://dev.genarrative.world/api/*`,避开 WebView CORS 预检并持久化刷新 Cookie。原生 HTTP capability 只允许该 HTTPS origin 的 `/api/*`,客户端解析层也拒绝其它 release origin;game-chat release 继续不依赖平台登录和 `api-server`。网络层失败时展示登录服务不可达提示,不裸露 WebView 的 `Load failed`。
|
||||
- 登录检查、未登录和客户端页面加载失败三种认证外壳统一使用共享陶泥儿产品形象与平台浅色主题 token;登录卡片、输入框、方式切换和主按钮沿用平台暖色视觉,并在不超过 `480px` 的窄视口收紧留白。视觉优化不改变服务器选择、验证码、密码、登录态安装或错误恢复语义。
|
||||
- `npm run agc` 的本地 SpacetimeDB owner identity 以独立 `spacetimeDataDir` 为作用域,不绑定可能漂移的监听端口;旧端口作用域记录仅在同一 data dir 下身份唯一时自动迁移,出现多个不同旧身份时失败关闭。`.app/dev-stack.json` 必须记录规范化 `spacetimeDataDir`,独立壳只复用数据库名和该目录同时匹配且健康的后端,旧 schema 状态或共享目录状态缺少此字段时不得复用。POSIX 子进程在 `spawn` 返回时立即登记 `error / exit` 生命周期、保存 detached leader 的 PGID 并把句柄交给外层;即使 direct leader 已先退出,也必须继续向负 PGID 发信号清理同组后代。后端 ready 前的 SIGINT、SIGTERM、超时或 ENOENT 都必须走同一进程组清理链路,不能遗留 npm、Cargo 或 SpacetimeDB。非 Linux Runtime 执行 `project.verify` 时,`npm run` 参数校验必须允许受控的 `--silent`、`--ignore-scripts` 位于脚本名前,并继续拒绝缺少真实脚本名的调用。
|
||||
- Tauri Rust 入口保持薄壳:`src-tauri/src/main.rs` 只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;命令行入口放在 `cli.rs`,Tauri command 包装放在 `commands.rs`,运行时配置与 LLM 配置检查放在 `config.rs`,Agent loop 与生成编排放在 `agent.rs`,上传 / 画板 / 平台美术生成接入放在 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放在 `project.rs`,本地 HTTP 预览与 preview 命令放在 `preview.rs`,旧窗口兼容命令放在 `windows.rs`,Rust 单测放在 `tests.rs`。后续继续拆分时保持 Tauri command 名、JSON 字段、`.agent/*` 路径和错误语义不变。
|
||||
- 本地项目初始化会创建 `game/`、`assets/`、`memory/`、`memory/agents/`、`exports/`、`.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地项目索引 `.agent/agent.db`,并生成默认 `game/index.html`。
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Generated
+1
-1
@@ -93,7 +93,7 @@
|
||||
},
|
||||
"apps/ai-game-creator-shell": {
|
||||
"name": "@genarrative/ai-game-creator-shell",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"dependencies": {
|
||||
"@cubone/react-file-manager": "^1.35.0",
|
||||
"@genarrative/image-canvas-core": "0.1.0",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user