Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00053cf1d3 |
@@ -17,7 +17,7 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
|
||||
2. Before the first generation in a new conversation, obtain a canvas name unless an existing `projectId` and `assetFolderId` were supplied. Create or reuse a project and a same-name asset-library folder. Retain `canvasName`, `projectId`, `assetFolderId`, and the current art spec.
|
||||
3. Normalize art requests into a reusable spec. Ask only for missing values that block the selected operation. Reuse the spec until the user changes its style, subject family, palette, format, or constraints.
|
||||
4. Infer the operation from the user's intent. Do not ask the user to select an API unless two operations would produce materially different artifacts.
|
||||
5. If a reference exists only as a local file, upload and confirm it first. Pass the stable returned `objectKey` to operations that accept object references; never substitute a temporary signed URL. For an icon-spritesheet primary spec, additionally create a project resource or asset record with `assetKind="icon-spec"`, then pass the returned resource or asset ID as `referenceId`.
|
||||
5. If a reference exists only as a local file, upload and confirm it first. Pass the stable returned `objectKey` to generation; never substitute a temporary signed URL.
|
||||
6. For generation endpoints that support the fields, include `projectId`, `assetFolderId`, an asset label, and `canvasCompletion` so the result enters both the canvas and its same-name library folder.
|
||||
7. Treat every generation POST as asynchronous. Send one stable `Idempotency-Key` per logical request, retain the returned `operationId`, and poll the returned `statusUrl` or `GET /api/external/v1/generations/{operationId}` according to `pollAfterMs`.
|
||||
8. Consume `result` only after `status=completed`. On `failed`, surface the safe error. On a client timeout or lost response, retain the operation/key; do not create a replacement request.
|
||||
@@ -29,10 +29,9 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
|
||||
- Authenticate MCP and business API calls with `Authorization: Bearer <tnr_sk_...>`. Never ask the user to paste a key into chat or place one in repository files.
|
||||
- All eight generation POST routes require `Idempotency-Key` and return HTTP `202`; `202` is durable acceptance, not a media result.
|
||||
- Retry an uncertain submission only with the exact same body and the same idempotency key. A polling timeout is not permission to generate again.
|
||||
- Use stable references such as `objectKey`, project resource ID, or asset ID where each operation permits them. Image edit/redraw is stricter: `sourceReferenceId` accepts only a registered project resource ID or asset ID; upload confirmation alone is not enough. Use `/assets/read-url` only for temporary preview/download access.
|
||||
- Use stable references such as `objectKey`, project resource ID, or asset ID in generation requests. Use `/assets/read-url` only for temporary preview/download access.
|
||||
- Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either.
|
||||
- Do not invent missing derivatives. A source-preserved warning means the main source remains usable but requested post-processing failed. A slice warning means the complete transparent sheet is usable but individual slices are absent.
|
||||
- For successful `style="pixelArt"`, treat completed-result and nested resource/asset dimensions as the final logical-grid PNG dimensions. They may differ from `size`, `imageSize`, the provider image, and `canvasCompletion.placeholder`; do not rescale or reject the artifact to match those inputs.
|
||||
- Keep generated artifacts in the canvas and asset library together. Character animation accepts `assetFolderId` and `assetLabel`; its completed result directly returns the final `assetKind="character-animation"` resource and asset with formal sequence fields. Do not create a duplicate first-frame record.
|
||||
|
||||
## Documentation Navigation
|
||||
|
||||
@@ -8,7 +8,7 @@ All paths below are relative to `https://www.genarrative.world`. Discovery and S
|
||||
|
||||
| Operation | Method and path | Minimum input |
|
||||
| --- | --- | --- |
|
||||
| List projects | `GET /api/external/v1/editor/projects` | Authentication; optional `view=full\|summary` (default `full`) |
|
||||
| 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` |
|
||||
@@ -19,13 +19,6 @@ All paths below are relative to `https://www.genarrative.world`. Discovery and S
|
||||
|
||||
Canvas save uses optimistic revision control. Pass the last authoritative `expectedRevision`; on conflict, reload instead of replaying a stale full layout.
|
||||
|
||||
Project listing supports two views:
|
||||
|
||||
- `view=full` is the REST default and returns the complete project, canvas, layers, and resources.
|
||||
- `view=summary` returns only `projectId`, `title`, `updatedAt`, and nullable `cover`, so callers can display, search, disambiguate same-name projects, and select a safe target without loading every canvas snapshot.
|
||||
- Hosted MCP `list_editor_projects` always uses `summary`; call `get_editor_project` after selecting a `projectId` when complete authoritative state is required.
|
||||
- `cover` contains only `resourceId`, stable `objectKey`, dimensions, and `updatedAt`. It never embeds image bytes, a Data URL, or a signed URL. To display it, pass `cover.objectKey` to `get_external_asset_read_url`; signed URLs are temporary and must not be persisted or reused as generation references.
|
||||
|
||||
## Asset and Upload Operations
|
||||
|
||||
| Operation | Method and path | Minimum input |
|
||||
@@ -50,12 +43,12 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
|
||||
| Capability | POST path | Required body fields | Common optional body fields |
|
||||
| --- | --- | --- | --- |
|
||||
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceImageSrc` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `sourceResourceId`, `targetLayerId`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceImageSrc`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
|
||||
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Sound effect | `/api/external/v1/editor/audios/sound-effects/generations` | `prompt` | `model`, `duration`, `loop`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| 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:
|
||||
@@ -77,27 +70,24 @@ Supply the `operationId` returned by submission. Poll no faster than `pollAfterM
|
||||
|
||||
## Reference Field Mapping
|
||||
|
||||
After confirming a local upload, pass its stable `objectKey` into operations that accept object references:
|
||||
After confirming a local upload, pass its stable `objectKey` into:
|
||||
|
||||
| Target capability | Field |
|
||||
| --- | --- |
|
||||
| Image generation | `referenceImageSrcs` |
|
||||
| Image edit/redraw | `sourceReferenceId` must be a registered project resource ID or asset ID; additional references remain in `referenceImageSrcs` |
|
||||
| Icon spritesheet | Register the primary spec as an `assetKind="icon-spec"` project resource or asset, then pass its returned ID as `referenceId`; additional style references remain in `referenceImageSrcs` |
|
||||
| Image edit/redraw | `sourceImageSrc`; additional references in `referenceImageSrcs` |
|
||||
| Icon spritesheet | `referenceImageSrc`; additional style references in `referenceImageSrcs` |
|
||||
| UI design extraction | `sourceImageSrc`; additional references in `referenceImageSrcs` |
|
||||
| Character animation | `sourceImageSrc` |
|
||||
| Video with image references | `referenceImageSrcs` |
|
||||
|
||||
For image edit/redraw, confirming an upload is not sufficient: create a project resource or asset-library record first, then pass that record's ID as `sourceReferenceId`. The main source never accepts objectKey, URL, Data URL, or Blob URL. Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference.
|
||||
|
||||
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
|
||||
Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference.
|
||||
|
||||
## Common Values
|
||||
|
||||
Use OpenAPI as the final authority; these common values are a routing aid:
|
||||
|
||||
- Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`; ordinary image generation may omit it.
|
||||
- External v1 currently has no structured game-scene generation operation. Do not send `kind: "scene"` or `assetKind: "scene"` through generic image generation; the server rejects both before queueing.
|
||||
- Image `model`: `gpt-image-2`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`.
|
||||
- Image `aspectRatio`: `1:1`, `2:3`, `3:2`, `9:16`, `16:9`.
|
||||
- Image `imageSize`: `0.5K`, `1K`, `2K`.
|
||||
@@ -105,7 +95,6 @@ Use OpenAPI as the final authority; these common values are a routing aid:
|
||||
- Video `aspectRatio`: `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `21:9`.
|
||||
- Video `resolution`: `480p`, `720p`, `1080p`; `mode`: `std`; `sound`: `on` or `off`.
|
||||
- Character animation uses `model: "seedance2.0-fast"`; `resolution`: `480p` or `720p`; `frameCount`: `32`, `40`, or `48`; `durationSeconds`: `4`, `5`, or `6`; `ratio`: `same`, `1:1`, `4:3`, `16:9`, `9:16`, or `3:4`.
|
||||
- Sound effect uses canonical model `eleven_text_to_sound_v2`; omit `duration` or send `null` for automatic duration, otherwise send a finite `0.5-30` number. `loop` defaults to `false` and remains independent from Prompt text.
|
||||
- UI extraction uses `aspectRatio: "1:1"`; use `imageSize: "1K"` for normal/small extraction and `2K` for dense designs.
|
||||
|
||||
Do not hard-code this list as a replacement client schema. In particular, the top-level image `style` field is intentionally extensible; see `requests-and-outputs.md` for its fallback behavior.
|
||||
|
||||
@@ -78,7 +78,7 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
|
||||
|
||||
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
|
||||
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceImageSrc` plus concrete `iconDescriptions`.
|
||||
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ 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.
|
||||
`dialogId` is optional. Do not reconstruct canvas state from completion results. Reload the project and asset library when complete authoritative snapshots are needed.
|
||||
|
||||
Character animation accepts `assetFolderId` and `assetLabel` and persists the final transparent sequence directly. Its completed compact result includes the authoritative `assetKind="character-animation"` resource and asset with `imageSequenceFrames` and `imageSequenceDurationMs`. Use those records directly and never synthesize a duplicate asset from the first frame.
|
||||
|
||||
@@ -104,8 +104,6 @@ For the lower-level asset/resource creation endpoints, `generationInputs` is rep
|
||||
|
||||
## Art Spec and Image Request
|
||||
|
||||
Generic External v1 image generation does not expose the main-site structured game-scene contract. `kind: "scene"` and `assetKind: "scene"` are both invalid and return HTTP `400` before any generation job is queued. Do not replace the structured scene fields and server-owned prompt assembly with a generic image prompt.
|
||||
|
||||
Carry the current art spec in `generationInputs.artSpec` and reflect important constraints in the prompt:
|
||||
|
||||
```json
|
||||
@@ -145,7 +143,7 @@ Carry the current art spec in `generationInputs.artSpec` and reflect important c
|
||||
The top-level `style` field is not the art spec's visual-style prose. It appends a short server-side clause to the prompt sent to the provider and enables deterministic post-processing:
|
||||
|
||||
- Omitted, `null`, empty string, or `"none"`: no clause is appended and no post-processing runs, without warning.
|
||||
- `"pixelArt"`: append one short pixel-art line to the end of the prompt sent to the provider, and enable pixel-art snapping, for ordinary image generation, `kind: "character"`, and icon spritesheet generation. On successful snapping, each detected grid cell becomes one output pixel and the logical-grid PNG is persisted directly; it is not resized back to `size`, `imageSize`, the provider image, or `canvasCompletion.placeholder`. The line is appended, not substituted — the rest of your prompt is unchanged. For the exact per-kind wording, read the `style` field description in the OpenAPI document; it is the contract, and this guide deliberately does not copy it.
|
||||
- `"pixelArt"`: append one short pixel-art line to the end of the prompt sent to the provider, and enable pixel-art snapping, for ordinary image generation, `kind: "character"`, and icon spritesheet generation. The line is appended, not substituted — the rest of your prompt is unchanged. For the exact per-kind wording, read the `style` field description in the OpenAPI document; it is the contract, and this guide deliberately does not copy it.
|
||||
- Unknown strings, or `"pixelArt"` on unsupported kinds such as `spec`, `quick-edit`, `ui-design`, or `publication-material`: continue without style processing and return `warning.code: "unsupported-image-style"`.
|
||||
- Non-string JSON values: malformed request, HTTP `400`.
|
||||
|
||||
@@ -169,10 +167,6 @@ client.generate_image(
|
||||
)
|
||||
```
|
||||
|
||||
Image edit/redraw has a stricter main-source identity rule. After upload confirmation, create either a project resource or an asset-library record and pass its `resourceId` or `assetId` as `sourceReferenceId`. Do not pass the uploaded objectKey as the main source; objectKey remains valid only for auxiliary `referenceImageSrcs` where the OpenAPI permits it.
|
||||
|
||||
Icon spritesheet generation has a stricter primary-spec contract. After upload confirmation, create a project resource or asset record with `assetKind: "icon-spec"`, retain its returned `resourceId` or `assetId`, and pass that ID as `referenceId`. The primary spec does not accept the uploaded `objectKey` directly; only additional style references may continue to use stable object keys in `referenceImageSrcs`.
|
||||
|
||||
For character animation from a local-only source, use actual dimensions and a stable synthetic layer ID:
|
||||
|
||||
```json
|
||||
@@ -198,8 +192,7 @@ Do not guess dimensions or pass a temporary signed read URL. See `authentication
|
||||
|
||||
The completed `result` may contain stable artifact fields such as:
|
||||
|
||||
- `objectKey`, media type, dimensions, or task ID. For successful `pixelArt`, image `width`/`height`, icon `spritesheetWidth`/`spritesheetHeight`, and nested resource/asset dimensions are the actual final logical-grid PNG dimensions rather than requested, provider, or placeholder dimensions.
|
||||
- Sound-effect `durationSeconds` is the probed MP3 duration and `loop` is the frozen request boolean; neither is inferred from Prompt text.
|
||||
- `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.
|
||||
@@ -211,10 +204,6 @@ It deliberately excludes a complete project/canvas/library snapshot, Data URL, B
|
||||
|
||||
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.
|
||||
|
||||
### Delivery-size normalization result before pixel-art snapping
|
||||
|
||||
`result.warning.code: "dimension-restore-fallback"` records only the delivery-size normalization result established before any subsequent `pixelArt` snapping: the provider image could not be safely normalized, so its dimensions were preserved at that processing boundary. It does not describe or constrain the dimensions after `pixelArt`; if snapping succeeds, use the completed result's actual logical-grid dimensions as authoritative.
|
||||
|
||||
### Source-preserved post-processing failure
|
||||
|
||||
When `result.warning.code` is `postprocess-failed-source-preserved`:
|
||||
|
||||
@@ -548,28 +548,22 @@ class GenarrativeExternalClient:
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def edit_image(self, prompt: str, source_reference_id: str, **fields: Any) -> Any:
|
||||
source_reference_id = source_reference_id.strip()
|
||||
if not source_reference_id:
|
||||
raise GenarrativeApiError("source_reference_id must be a registered resource or asset ID")
|
||||
def edit_image(self, prompt: str, source_image_src: str, **fields: Any) -> Any:
|
||||
self._apply_canvas_session_fields(fields, prompt, 1024, 1024)
|
||||
prompt = self._apply_art_spec(fields, prompt)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/images/edits",
|
||||
{"prompt": prompt, "sourceReferenceId": source_reference_id, **fields},
|
||||
{"prompt": prompt, "sourceImageSrc": source_image_src, **fields},
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def generate_icon_spritesheet(
|
||||
self,
|
||||
reference_id: str,
|
||||
reference_image_src: str,
|
||||
icon_descriptions: list[str],
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
reference_id = normalize_optional_text(reference_id)
|
||||
if not reference_id:
|
||||
raise GenarrativeApiError("reference_id must be a registered icon-spec resource or asset ID")
|
||||
descriptions = [item.strip() for item in icon_descriptions if item.strip()]
|
||||
if not descriptions:
|
||||
raise GenarrativeApiError("icon_descriptions must contain at least one non-empty item")
|
||||
@@ -580,9 +574,9 @@ class GenarrativeExternalClient:
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
{
|
||||
**fields,
|
||||
"referenceId": reference_id,
|
||||
"referenceImageSrc": reference_image_src,
|
||||
"iconDescriptions": descriptions,
|
||||
**fields,
|
||||
},
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
@@ -655,19 +649,13 @@ class GenarrativeExternalClient:
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def generate_sound_effect(
|
||||
self,
|
||||
prompt: str,
|
||||
duration: float | None = None,
|
||||
loop: bool = False,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
def generate_sound_effect(self, prompt: str, duration: int, **fields: Any) -> Any:
|
||||
self._apply_canvas_session_fields(fields, prompt, 360, 120)
|
||||
prompt = self._apply_art_spec(fields, prompt)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/audios/sound-effects/generations",
|
||||
{"prompt": prompt, "duration": duration, "loop": loop, **fields},
|
||||
{"prompt": prompt, "duration": duration, **fields},
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
@@ -778,11 +766,9 @@ def _self_test() -> None:
|
||||
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
|
||||
canvasSession=session,
|
||||
assetLabel="贪吃蛇透明图集",
|
||||
referenceId="must-not-override-explicit-reference",
|
||||
iconDescriptions=["不得覆盖显式图标描述"],
|
||||
)
|
||||
assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations"
|
||||
assert calls[0]["body"]["referenceId"] == "editor-resource-spec"
|
||||
assert calls[0]["body"]["referenceImageSrc"] == "editor-resource-spec"
|
||||
assert calls[0]["body"]["screenColor"] == "auto"
|
||||
assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上"
|
||||
assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo"
|
||||
|
||||
@@ -130,11 +130,6 @@ VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn"
|
||||
VECTOR_ENGINE_API_KEY=""
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS="1000000"
|
||||
|
||||
# ElevenLabs editor sound-effect generation is server-side only.
|
||||
ELEVENLABS_BASE_URL="https://api.elevenlabs.io"
|
||||
ELEVENLABS_API_KEY=""
|
||||
ELEVENLABS_REQUEST_TIMEOUT_MS="180000"
|
||||
|
||||
# 阿里云 OSS 配置。
|
||||
# Rust `server-rs` 的 `api-server` 会优先从 `.env` / `.env.local` 读取这些变量,
|
||||
# 用于签发浏览器 PostObject 直传票据,并保持 `/generated-*` 旧路径习惯。
|
||||
|
||||
@@ -46,7 +46,6 @@ module.exports = {
|
||||
{
|
||||
files: [
|
||||
'apps/admin-web/src/pages/*.tsx',
|
||||
'src/components/platform-entry/PlatformActiveMobileWelcomeDialog.tsx',
|
||||
'src/components/platform-entry/PlatformMobileHomeWelcomeDialog.tsx',
|
||||
],
|
||||
rules: {
|
||||
@@ -73,7 +72,6 @@ module.exports = {
|
||||
'src/components/image-editor/**/*.{ts,tsx}',
|
||||
'src/components/project/**/*.{ts,tsx}',
|
||||
'src/components/platform-entry/PlatformActiveProfileView*.tsx',
|
||||
'src/components/platform-entry/PlatformActiveMobileWelcomeDialog*.tsx',
|
||||
'src/components/platform-entry/PlatformEntryActiveFlowShell*.tsx',
|
||||
'src/components/platform-entry/PlatformEntryFlowShell.tsx',
|
||||
'src/components/platform-entry/PlatformProfileApiKeysModal.tsx',
|
||||
|
||||
@@ -69,8 +69,23 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run repository checks
|
||||
run: npm run check:repository-ci
|
||||
- name: Run repository lint gates
|
||||
run: npm run lint
|
||||
|
||||
- name: Build web applications
|
||||
run: npm run build
|
||||
|
||||
- name: Validate content data
|
||||
run: npm run check:content
|
||||
|
||||
- name: Check committed whitespace
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_ref="${SPACETIME_SCHEMA_BASE_REF:-}"
|
||||
test -n "${base_ref}"
|
||||
git cat-file -e "${base_ref}^{commit}"
|
||||
git diff --check "${base_ref}"...HEAD
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend tests
|
||||
|
||||
@@ -39,8 +39,6 @@ temp*build*/
|
||||
/apps/ai-game-creator-shell/game-creator.config.local.json
|
||||
/apps/mobile-shell/.expo/
|
||||
/apps/mobile-shell/.expo-export-smoke/
|
||||
/apps/preview-deployer-web/dist/
|
||||
/apps/preview-deployer-web/node_modules/
|
||||
/server-rs/.spacetimedb/
|
||||
/server-rs/.data/
|
||||
/public/generated-animations
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: genarrative-dev-stack-port-routing
|
||||
short_description: 修改 Genarrative 本地 dev 启动端口、代理目标、端口冲突处理时使用。
|
||||
description: 在 Genarrative 中修改 npm run dev / dev:spacetime / dev:api-server / dev:bgfilter-worker / dev:web / dev:admin-web 的本地启动端口、端口可用性探测、端口漂移、SpacetimeDB publish server、Rust 进程环境变量、Vite 代理目标和后台 admin-web 启动串联时使用。
|
||||
version: 1.2.0
|
||||
version: 1.1.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
@@ -17,7 +17,7 @@ metadata:
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 修改 `scripts/dev.mjs`、`scripts/dev-utils.mjs`、`scripts/dev-stack-port-utils.mjs` 或 AI 游戏创作客户端 dev 启动器。
|
||||
- 修改 `scripts/dev.mjs`、`scripts/dev-utils.mjs`、`scripts/dev-stack-port-utils.mjs`。
|
||||
- 处理 `3000`、`3101`、`3102`、`8082` 等端口被占用导致本地开发栈启动失败。
|
||||
- 排查 Vite 代理仍指向旧 api-server 端口、前端打开了旧 dev server、后台代理错配。
|
||||
- 调整 SpacetimeDB standalone、publish、Rust `api-server`、主站 Vite、后台 Vite 的启动顺序。
|
||||
@@ -32,11 +32,10 @@ metadata:
|
||||
3. SpacetimeDB standalone:`3101`,健康检查为 `http://127.0.0.1:<spacetime-port>/v1/ping`。
|
||||
4. 后台 Vite:`3102`,后台地址为 `http://127.0.0.1:<admin-web-port>/admin/`。
|
||||
5. 独立 BgFilter worker:`8083`,就绪检查为 `http://127.0.0.1:<bgfilter-worker-port>/readyz`。
|
||||
6. AI 游戏创作 Vite:非 Linux 兼容首选 `3080`;Linux 使用当前用户端口段的 `start + 5`。
|
||||
|
||||
端口不可用时,脚本会从优先端口开始向后寻找可用端口。后续流程必须以解析后的实际端口为准,不能继续使用默认端口。
|
||||
|
||||
Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range` 会先向系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json` 申请一个端口段,再把该段映射为 `web = start`、`api = start + 1`、`spacetime = start + 2`、`adminWeb = start + 3`、`bgfilterWorker = start + 4`、`agcVite = start + 5`。注册表锁文件是 `/var/tmp/genarrative-dev-port-ranges/registry.lock`,可通过 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。自动分配从 `10000-10099` 起,每次占用 100 个端口块,后续块按 `10100-10199`、`10200-10299` 递增;当前口径是“一个用户固定占用一个段,后续启动继续复用这段并在段内漂移”;该注册表只在 Linux 上生效;Windows 继续沿用原有统一端口探测和漂移逻辑,不读系统级注册表。
|
||||
Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range` 会先向系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json` 申请一个端口段,再把该段映射为 `web = start`、`api = start + 1`、`spacetime = start + 2`、`adminWeb = start + 3`、`bgfilterWorker = start + 4`。注册表锁文件是 `/var/tmp/genarrative-dev-port-ranges/registry.lock`,可通过 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。自动分配从 `10000-10099` 起,每次占用 100 个端口块,后续块按 `10100-10199`、`10200-10299` 递增;当前口径是“一个用户固定占用一个段,后续启动继续复用这段并在段内漂移”;该注册表只在 Linux 上生效;Windows 继续沿用原有统一端口探测和漂移逻辑,不读系统级注册表。
|
||||
|
||||
## 实现入口
|
||||
|
||||
@@ -52,12 +51,9 @@ Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range`
|
||||
- `scripts/dev.mjs`
|
||||
- 解析 CLI 参数后统一计算 client host、端口、`SPACETIME_SERVER`、`RUST_SERVER_TARGET`。
|
||||
- 完整栈按 SpacetimeDB、publish、BgFilter worker readiness、api-server readiness、主站 Vite、后台 Vite 顺序启动。
|
||||
- Linux 下会先申请系统级端口段并映射成六个预留槽位;主 dev 栈使用前五个,AGC Vite 使用 `start + 5`,自动分配从 `10000-10099` 起。Windows 的主 dev 栈和 AGC 则各自沿用统一端口探测与漂移逻辑。
|
||||
- Linux 下会先申请系统级端口段并把它映射成五个 dev 端口;自动分配从 `10000-10099` 起,Windows 则把第五个服务纳入原有统一参数解析与端口漂移逻辑。
|
||||
- 完整栈和 `dev:api-server` 把两个 Rust 进程作为同一重启单元,先全部停止,再先启动 BgFilter worker、后启动 api-server;不要为同一份 Rust 源码创建两个并发 `cargo` watcher。
|
||||
- 单模块命令复用同一套参数和 env 解析。
|
||||
- `apps/ai-game-creator-shell/scripts/dev-port.mjs`
|
||||
- 复用系统级用户端口段,解析 AGC Vite 的 `start + 5` 首选槽位。
|
||||
- 把最终端口通过 `GENARRATIVE_AGC_VITE_PORT` 同步给 `beforeDevCommand` 和配套后端端口解析器,通过 Tauri CLI `--config` 同步 `build.devUrl`,并通过 Vite CLI `--port` 同步 `strictPort` 监听。
|
||||
|
||||
## 必须保持的传递链路
|
||||
|
||||
@@ -73,7 +69,6 @@ Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range`
|
||||
8. 后台 Vite:`ADMIN_API_TARGET`、`GENARRATIVE_API_TARGET`、`GENARRATIVE_API_PORT`、`--port=${ADMIN_WEB_PORT}`。
|
||||
9. 控制台日志:`[dev:ports]` 和 `[dev] web/admin web/api-server/bgfilter-worker/spacetime` 必须显示最终实际地址。
|
||||
10. Linux 端口段注册:`[dev] port-range:` 与 `[dev] port-range-registry:` 只在 Linux 输出,Windows 不应依赖系统级注册表。
|
||||
11. AI 游戏创作客户端:外层启动器解析最终 AGC Vite 端口后,通过 Tauri CLI `--config` 覆盖 `build.devUrl`,把同一 `GENARRATIVE_AGC_VITE_PORT` 传给 `beforeDevCommand` 与配套后端端口解析器,并用 Vite CLI `--port` 启动严格监听;后端端口漂移必须跳过该预留端口。
|
||||
|
||||
如果只改了其中一段,通常会出现:浏览器打开的前端可用,但 `/api/*` 代理到旧端口;后台页面可用但后台 API 失败;SpacetimeDB 启动在新端口但 publish 仍发往旧端口。
|
||||
|
||||
@@ -133,7 +128,6 @@ node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 ap
|
||||
- [ ] `npm run dev` 的 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 都使用实际端口。
|
||||
- [ ] BgFilter worker 在 api-server 前 ready,父子共享实际 base URL / Token,Rust watch 只触发一次组合重启。
|
||||
- [ ] `npm run dev:web` 在主站端口不可用时能切换到可用端口。
|
||||
- [ ] `npm run agc` / `npm run agc:game-chat` 在 Linux 使用用户段 `start + 5`,Tauri、Vite、marker 和预检使用同一最终端口。
|
||||
- [ ] 文档同步更新 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
- [ ] 长期踩坑同步更新 `docs/project-memory/shared-memory/pitfalls.md`。
|
||||
- [ ] 修改中文文件后运行 `npm run check:encoding`。
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
npm run format:staged
|
||||
@@ -1 +0,0 @@
|
||||
npm run check:pre-push-master -- "$@"
|
||||
@@ -26,12 +26,6 @@
|
||||
- 后续新增 Markdown 文档文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;历史文档不要求批量重命名,除非本次任务明确涉及。
|
||||
- 工程修改要同步更新对应 `docs/` 文档;产生长期有效的架构约定、接口变化、排障经验、开发流程或协作规则时,同步更新 `docs/project-memory/shared-memory/`。
|
||||
- 默认保持系统简洁:优先复用、修改、扩展现有系统、页面和公共组件,不新建平行系统或平行页面。
|
||||
- 对已明确退役且不存在现役调用方、公开契约、持久化数据、活跃实例或迁移要求的对象,坚持“四不写”:
|
||||
1. 不写历史兼容代码。
|
||||
2. 不写用于维持退役行为的防御性兼容测试。
|
||||
3. 不写仅说明其曾存在或已删除的墓碑注释。
|
||||
4. 不写仅记录其已删除的墓碑文档;直接将权威文档更新为当前状态。
|
||||
- 公开 API、持久化数据、SpacetimeDB schema、跨版本重放、活跃实例和正式迁移不适用“四不写”;必要兼容应最小化、白名单化并配套契约或迁移测试,迁移完成后同步删除兼容实现与对应测试。
|
||||
- UI 面板中不要默认写功能说明、规则描述或开发解释文案;移动端优先,同时保证网页端可正常显示和操作。
|
||||
- 点击按钮弹出独立面板的设计,不要实现成在当前面板下面追加内容。
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"agentMode": "codex_app_server",
|
||||
"llm": {
|
||||
"apiKey": "",
|
||||
"baseUrl": "https://api.openai.com/v1",
|
||||
@@ -16,5 +15,9 @@
|
||||
"retryBackoffMs": 500
|
||||
},
|
||||
"agentLlm": {},
|
||||
"editorApi": {
|
||||
"baseUrl": "http://127.0.0.1:8082",
|
||||
"apiKey": ""
|
||||
},
|
||||
"mcpServers": {}
|
||||
}
|
||||
|
||||
-11
@@ -10,9 +10,7 @@
|
||||
"dependencies": {
|
||||
"@lexical/react": "^0.47.0",
|
||||
"@lexical/utils": "^0.47.0",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.9",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"lexical": "^0.47.0",
|
||||
@@ -2210,15 +2208,6 @@
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-http": {
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.9.tgz",
|
||||
"integrity": "sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-opener": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz",
|
||||
|
||||
@@ -36,9 +36,7 @@
|
||||
"dependencies": {
|
||||
"@lexical/react": "^0.47.0",
|
||||
"@lexical/utils": "^0.47.0",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.9",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"lexical": "^0.47.0",
|
||||
|
||||
@@ -253,24 +253,12 @@ export function defaultRuntimeConfigDirCandidates({
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const configuredRoot = environment.XDG_CONFIG_HOME;
|
||||
const posixAbsoluteConfiguredRoot =
|
||||
configuredRoot && path.posix.isAbsolute(configuredRoot);
|
||||
const hostAbsoluteConfiguredRoot =
|
||||
configuredRoot &&
|
||||
!posixAbsoluteConfiguredRoot &&
|
||||
path.isAbsolute(configuredRoot);
|
||||
const configRoot =
|
||||
configuredRoot &&
|
||||
(posixAbsoluteConfiguredRoot || hostAbsoluteConfiguredRoot)
|
||||
? configuredRoot
|
||||
environment.XDG_CONFIG_HOME &&
|
||||
path.posix.isAbsolute(environment.XDG_CONFIG_HOME)
|
||||
? environment.XDG_CONFIG_HOME
|
||||
: path.posix.join(homeDirectory, '.config');
|
||||
pushUnique(
|
||||
candidates,
|
||||
hostAbsoluteConfiguredRoot
|
||||
? path.join(configRoot, appIdentifier)
|
||||
: path.posix.join(configRoot, appIdentifier),
|
||||
);
|
||||
pushUnique(candidates, path.posix.join(configRoot, appIdentifier));
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
@@ -62,14 +62,6 @@ const viteConfigSource = fs.readFileSync(
|
||||
new URL('../vite.config.ts', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const devPortSource = fs.readFileSync(
|
||||
new URL('../scripts/dev-port.mjs', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const startTauriDevSource = fs.readFileSync(
|
||||
new URL('../scripts/start-tauri-dev.mjs', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const appSource = [
|
||||
readSourceTree(new URL('../src/', import.meta.url), '.ts'),
|
||||
readSourceTree(new URL('../src/', import.meta.url), '.tsx'),
|
||||
@@ -114,22 +106,9 @@ const rustSharedContractSource = fs.readFileSync(
|
||||
'utf8',
|
||||
);
|
||||
const allowedUncalledTauriCommands = [
|
||||
'archive_failed_local_project_resource_edit',
|
||||
'chat_with_game_creator_agent',
|
||||
'commit_local_project_asset',
|
||||
'confirm_local_project_asset_canvas_generation_service_identity',
|
||||
'create_local_project_asset_canvas_draft',
|
||||
'discard_local_project_asset_canvas_draft',
|
||||
'generate_local_project_asset_canvas_image',
|
||||
'open_game_creator_launcher_window',
|
||||
'open_game_creator_workspace_window',
|
||||
'read_local_project_asset_canvas_draft',
|
||||
'read_local_project_asset_canvas_media',
|
||||
'recover_local_project_asset_canvas_transactions',
|
||||
'recover_local_project_asset_canvas_generations',
|
||||
'stage_local_project_asset_canvas_image',
|
||||
'store_local_project_asset_canvas_media',
|
||||
'update_local_project_asset_canvas_draft',
|
||||
];
|
||||
const sourceExtensions = new Set([
|
||||
'.json',
|
||||
@@ -664,12 +643,10 @@ async function runConfigWizardRegressionChecks() {
|
||||
apiKind: 'openai_responses',
|
||||
},
|
||||
);
|
||||
assert.equal(updatedPrimary.agentMode, 'provider');
|
||||
await writeGameCreatorWizardConfig(overlayState, updatedPrimary);
|
||||
const reloadedState =
|
||||
await readGameCreatorWizardConfigState(overlayConfigDir);
|
||||
assert.equal(reloadedState.effectiveConfig.llm.apiKey, 'fixture-new-key');
|
||||
assert.equal(reloadedState.effectiveConfig.agentMode, 'provider');
|
||||
assert.equal(reloadedState.effectiveConfig.llm.model, 'new-model');
|
||||
assert.equal(
|
||||
reloadedState.effectiveConfig.llm.baseUrl,
|
||||
@@ -1175,12 +1152,6 @@ if (defaultAppConfig.llm?.apiKey !== '') {
|
||||
throw new Error('AI game creator shell default llm.apiKey must stay empty');
|
||||
}
|
||||
|
||||
if (defaultAppConfig.agentMode !== 'codex_app_server') {
|
||||
throw new Error(
|
||||
'AI game creator shell default agentMode must be codex_app_server',
|
||||
);
|
||||
}
|
||||
|
||||
const allowedLlmReasoningEfforts = new Set([
|
||||
'default',
|
||||
'low',
|
||||
@@ -1207,9 +1178,9 @@ for (const [agentId, agentConfig] of Object.entries(
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultAppConfig.editorApi !== undefined) {
|
||||
if (defaultAppConfig.editorApi?.apiKey !== '') {
|
||||
throw new Error(
|
||||
'AI game creator shell ordinary default config must not contain editorApi',
|
||||
'AI game creator shell default editorApi.apiKey must stay empty',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1237,16 +1208,16 @@ if (
|
||||
clientWindow.width !== 1280 ||
|
||||
clientWindow.height !== 800 ||
|
||||
clientWindow.minWidth !== 1280 ||
|
||||
clientWindow.minHeight !== 720
|
||||
clientWindow.minHeight !== 800
|
||||
) {
|
||||
throw new Error(
|
||||
'AI game creator shell client window must default to 1280x800 and stay at least 1280x720',
|
||||
'AI game creator shell client window must keep the landscape workbench size',
|
||||
);
|
||||
}
|
||||
|
||||
if (tauriConfig.build?.devUrl !== 'http://127.0.0.1:3080/') {
|
||||
throw new Error(
|
||||
'AI game creator shell Tauri config must retain the non-launcher fallback devUrl',
|
||||
'AI game creator shell Tauri devUrl must stay on the fixed Vite dev port',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1256,41 +1227,12 @@ if (!viteConfigSource.includes("host: '127.0.0.1'")) {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!viteConfigSource.includes('port: 3080') ||
|
||||
!viteConfigSource.includes('port: server.config.server.port')
|
||||
) {
|
||||
if (!viteConfigSource.includes('port: 3080')) {
|
||||
throw new Error(
|
||||
'AI game creator shell Vite config must retain its fallback and report the actual CLI-selected port',
|
||||
'AI game creator shell Vite dev port must match Tauri devUrl',
|
||||
);
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
'mapDevPortsToPortRange',
|
||||
'agcVitePort',
|
||||
'resolveAgcDevEndpoint',
|
||||
'GENARRATIVE_AGC_VITE_PORT',
|
||||
]) {
|
||||
if (!devPortSource.includes(snippet)) {
|
||||
throw new Error(
|
||||
`AI game creator shell dev port resolver drifted: ${snippet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
'resolveAgcDevEndpoint',
|
||||
'withAgcDevEndpointEnv',
|
||||
"'--config'",
|
||||
'configOverride',
|
||||
]) {
|
||||
if (!startTauriDevSource.includes(snippet)) {
|
||||
throw new Error(
|
||||
`AI game creator shell Tauri dev port injection drifted: ${snippet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!viteConfigSource.includes('strictPort: true')) {
|
||||
throw new Error(
|
||||
'AI game creator shell Vite dev server must not drift away from Tauri devUrl',
|
||||
@@ -1311,7 +1253,7 @@ if (
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'AI game creator shell beforeDevCommand must start the selected Vite dev server',
|
||||
'AI game creator shell beforeDevCommand must reuse or start the fixed Vite dev server',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1472,22 +1414,6 @@ if (
|
||||
);
|
||||
}
|
||||
|
||||
const gameChatReleaseWindows = gameChatReleaseTauriConfig.app?.windows ?? [];
|
||||
const gameChatReleaseClientWindow = gameChatReleaseWindows[0];
|
||||
if (
|
||||
gameChatReleaseWindows.length !== 1 ||
|
||||
gameChatReleaseClientWindow?.label !== 'client' ||
|
||||
gameChatReleaseClientWindow?.url !== 'index.html' ||
|
||||
gameChatReleaseClientWindow?.width !== 1280 ||
|
||||
gameChatReleaseClientWindow?.height !== 800 ||
|
||||
gameChatReleaseClientWindow?.minWidth !== 1280 ||
|
||||
gameChatReleaseClientWindow?.minHeight !== 720
|
||||
) {
|
||||
throw new Error(
|
||||
'AI game creator game-chat client window must default to 1280x800 and stay at least 1280x720',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
tauriConfig.version !== '0.1.0' ||
|
||||
packageConfig.version !== '0.1.0' ||
|
||||
@@ -1537,15 +1463,12 @@ if (
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
'resolveAgcDevEndpoint',
|
||||
'withAgcDevEndpointEnv',
|
||||
'const port = 3080',
|
||||
"response.body.includes('<title>AI 游戏创作</title>')",
|
||||
'function isPortListening()',
|
||||
'cannot be safely reused',
|
||||
'reuse existing Vite dev server',
|
||||
'non-HTTP or unrecognized server',
|
||||
"'--config'",
|
||||
"'vite.config.ts'",
|
||||
"'--port'",
|
||||
"'--config', 'vite.config.ts'",
|
||||
]) {
|
||||
if (!devServerSource.includes(snippet)) {
|
||||
throw new Error(
|
||||
@@ -1567,6 +1490,7 @@ for (const snippet of [
|
||||
'$verified = $targetItem.GetAccessControl()',
|
||||
'$rules.Count -ne 1',
|
||||
'[System.Security.AccessControl.FileSystemRights]::FullControl',
|
||||
"runChildCapture('powershell.exe'",
|
||||
"'-NoProfile'",
|
||||
"'-Command'",
|
||||
'windowsPrivateAclScript',
|
||||
@@ -1579,11 +1503,6 @@ for (const snippet of [
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!/runChildCapture\(\s*['"]powershell\.exe['"]/u.test(configWizardSource)) {
|
||||
throw new Error(
|
||||
'AI game creator config wizard guardrail drifted: runChildCapture(powershell.exe)',
|
||||
);
|
||||
}
|
||||
if (/\bGet-Acl\b/u.test(configWizardSource)) {
|
||||
throw new Error(
|
||||
'AI game creator config wizard must not rely on Get-Acl module auto-loading',
|
||||
@@ -1610,8 +1529,7 @@ for (const snippet of [
|
||||
'const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.json"',
|
||||
'const DEFAULT_GAME_CREATOR_APP_CONFIG_JSON: &str = include_str!("../../game-creator.config.json")',
|
||||
'fn configure_game_creator_runtime_config_dir(',
|
||||
'game_creator_runtime_config_dir()',
|
||||
'.unwrap_or_else(|| app.path().app_config_dir())?',
|
||||
'app.path().app_config_dir()?',
|
||||
'fn load_game_creator_app_config()',
|
||||
'fn read_game_creator_app_config()',
|
||||
'fn write_game_creator_app_config(',
|
||||
@@ -1705,8 +1623,7 @@ for (const snippet of [
|
||||
"'write_game_creator_app_config'",
|
||||
'aria-label="运行时配置"',
|
||||
'LLM API Key',
|
||||
'External Editor Base URL',
|
||||
'External Editor API Key',
|
||||
'画板 API Key',
|
||||
'runtime_config.save',
|
||||
"'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'",
|
||||
"'activate_local_game_preview'",
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import {
|
||||
findAvailablePort,
|
||||
formatPortDecision,
|
||||
mapDevPortsToPortRange,
|
||||
normalizePort,
|
||||
reserveLinuxDevPortRange,
|
||||
} from '../../../scripts/dev-stack-port-utils.mjs';
|
||||
|
||||
const agcDevHost = '127.0.0.1';
|
||||
const legacyAgcDevPort = 3080;
|
||||
const agcVitePortEnvKey = 'GENARRATIVE_AGC_VITE_PORT';
|
||||
|
||||
function readConfiguredAgcDevPort(env = process.env) {
|
||||
const rawPort = String(env[agcVitePortEnvKey] ?? '').trim();
|
||||
if (!rawPort) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const port = normalizePort(rawPort, -1);
|
||||
if (port < 1024) {
|
||||
throw new Error(`${agcVitePortEnvKey} 必须是 1024-65535 的有效端口`);
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
function createAgcDevEndpoint(port, portRange = null) {
|
||||
const url = `http://${agcDevHost}:${port}/`;
|
||||
return {
|
||||
host: agcDevHost,
|
||||
port,
|
||||
url,
|
||||
markerUrl: `${url}__agc_dev_server.json`,
|
||||
portRange,
|
||||
};
|
||||
}
|
||||
|
||||
function readAgcDevEndpoint(env = process.env) {
|
||||
return createAgcDevEndpoint(
|
||||
readConfiguredAgcDevPort(env) ?? legacyAgcDevPort,
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveAgcDevEndpoint({
|
||||
env = process.env,
|
||||
platform = process.platform,
|
||||
strictConfigured = false,
|
||||
reservePortRange = reserveLinuxDevPortRange,
|
||||
findPort = findAvailablePort,
|
||||
} = {}) {
|
||||
const configuredPort = readConfiguredAgcDevPort(env);
|
||||
let portRange = null;
|
||||
let preferredPort = configuredPort ?? legacyAgcDevPort;
|
||||
|
||||
if (platform === 'linux') {
|
||||
const allocation = await reservePortRange({ env });
|
||||
if (!allocation?.range) {
|
||||
throw new Error('无法取得当前 Linux 用户的 dev 端口段');
|
||||
}
|
||||
portRange = allocation.range;
|
||||
const mappedAgcVitePort = mapDevPortsToPortRange(portRange)?.agcVitePort;
|
||||
if (!Number.isInteger(mappedAgcVitePort)) {
|
||||
throw new Error(
|
||||
`当前 Linux dev 端口段 ${portRange.label} 缺少 AGC Vite 槽位;请先迁移为至少 6 个端口且不与其它用户重叠的端口段`,
|
||||
);
|
||||
}
|
||||
if (configuredPort != null && configuredPort < mappedAgcVitePort) {
|
||||
throw new Error(
|
||||
`${agcVitePortEnvKey} ${configuredPort} 占用了 Linux dev 端口段 ${portRange.label} 的前五个服务槽位`,
|
||||
);
|
||||
}
|
||||
preferredPort = configuredPort ?? mappedAgcVitePort;
|
||||
}
|
||||
|
||||
const port = await findPort({
|
||||
host: agcDevHost,
|
||||
preferredPort,
|
||||
portRange,
|
||||
strict: strictConfigured && configuredPort != null,
|
||||
});
|
||||
console.log(
|
||||
formatPortDecision({
|
||||
name: 'ai-game-creator-shell',
|
||||
host: agcDevHost,
|
||||
preferredPort,
|
||||
resolvedPort: port,
|
||||
}),
|
||||
);
|
||||
if (portRange) {
|
||||
console.log(`[ai-game-creator-shell] dev port-range: ${portRange.label}`);
|
||||
}
|
||||
|
||||
return createAgcDevEndpoint(port, portRange);
|
||||
}
|
||||
|
||||
function withAgcDevEndpointEnv(endpoint, env = process.env) {
|
||||
return {
|
||||
...env,
|
||||
[agcVitePortEnvKey]: String(endpoint.port),
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
agcDevHost,
|
||||
agcVitePortEnvKey,
|
||||
createAgcDevEndpoint,
|
||||
legacyAgcDevPort,
|
||||
readAgcDevEndpoint,
|
||||
readConfiguredAgcDevPort,
|
||||
resolveAgcDevEndpoint,
|
||||
withAgcDevEndpointEnv,
|
||||
};
|
||||
@@ -211,7 +211,6 @@ export function buildGameCreatorWizardConfig(existingConfig, llmInput) {
|
||||
}
|
||||
return {
|
||||
...source,
|
||||
agentMode: 'provider',
|
||||
llm: {
|
||||
...previousLlm,
|
||||
apiKey,
|
||||
@@ -291,25 +290,21 @@ export async function secureWindowsGameCreatorPathForCurrentUser(
|
||||
targetPath,
|
||||
{ isDirectory },
|
||||
) {
|
||||
const result = await runChildCapture(
|
||||
'powershell.exe',
|
||||
[
|
||||
'-NoLogo',
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-Command',
|
||||
windowsPrivateAclScript,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
GENARRATIVE_AGC_PRIVATE_PATH: targetPath,
|
||||
GENARRATIVE_AGC_PRIVATE_IS_DIRECTORY: String(isDirectory),
|
||||
},
|
||||
const result = await runChildCapture('powershell.exe', [
|
||||
'-NoLogo',
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-Command',
|
||||
windowsPrivateAclScript,
|
||||
], {
|
||||
env: {
|
||||
...process.env,
|
||||
GENARRATIVE_AGC_PRIVATE_PATH: targetPath,
|
||||
GENARRATIVE_AGC_PRIVATE_IS_DIRECTORY: String(isDirectory),
|
||||
},
|
||||
);
|
||||
});
|
||||
if (result.code !== 0 || result.signal) {
|
||||
const detail = result.stderr.trim() || result.stdout.trim();
|
||||
throw new Error(
|
||||
|
||||
@@ -46,8 +46,6 @@ export function buildProcessSessionFixtureSource({
|
||||
' if (!echoed && line === challenge) {',
|
||||
' echoed = true;',
|
||||
" console.log(echoPrefix + ' ' + challenge);",
|
||||
" } else if (line === challenge + ':stop') {",
|
||||
' stop();',
|
||||
' }',
|
||||
' }',
|
||||
'});',
|
||||
|
||||
@@ -4,9 +4,8 @@ import fs from 'node:fs/promises';
|
||||
import http from 'node:http';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const appRoot = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
const localConfigPath = path.join(appRoot, 'game-creator.config.local.json');
|
||||
const projectRoot = path.join(
|
||||
os.tmpdir(),
|
||||
@@ -885,28 +884,7 @@ function readBrowserDom(url) {
|
||||
}
|
||||
|
||||
function resolveChromeBin() {
|
||||
const windowsRoot = path.parse(os.homedir()).root;
|
||||
for (const candidate of [
|
||||
path.join(
|
||||
windowsRoot,
|
||||
'Program Files/Google/Chrome/Application/chrome.exe',
|
||||
),
|
||||
path.join(
|
||||
windowsRoot,
|
||||
'Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
||||
),
|
||||
path.join(
|
||||
os.homedir(),
|
||||
'AppData/Local/Google/Chrome/Application/chrome.exe',
|
||||
),
|
||||
path.join(
|
||||
windowsRoot,
|
||||
'Program Files/Microsoft/Edge/Application/msedge.exe',
|
||||
),
|
||||
path.join(
|
||||
windowsRoot,
|
||||
'Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
|
||||
),
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
||||
|
||||
@@ -3,11 +3,10 @@ import http from 'node:http';
|
||||
import net from 'node:net';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { resolveAgcDevEndpoint, withAgcDevEndpointEnv } from './dev-port.mjs';
|
||||
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const endpoint = await resolveAgcDevEndpoint();
|
||||
const { host, port, url: devUrl } = endpoint;
|
||||
const host = '127.0.0.1';
|
||||
const port = 3080;
|
||||
const devUrl = `http://${host}:${port}/`;
|
||||
|
||||
function readExistingServer() {
|
||||
return new Promise((resolve) => {
|
||||
@@ -70,10 +69,8 @@ function isPortListening() {
|
||||
const existing = await readExistingServer();
|
||||
if (existing) {
|
||||
if (isAiGameCreatorServer(existing)) {
|
||||
console.error(
|
||||
`[ai-game-creator-shell] ${devUrl} is already running but cannot be safely reused. Stop that process before starting the dev server.`,
|
||||
);
|
||||
process.exit(1);
|
||||
console.log(`[ai-game-creator-shell] reuse existing Vite dev server ${devUrl}`);
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(
|
||||
`[ai-game-creator-shell] ${devUrl} is already in use by another server. Stop that process before starting Tauri dev.`,
|
||||
@@ -91,20 +88,9 @@ if (await isPortListening()) {
|
||||
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
const child = spawn(
|
||||
npm,
|
||||
[
|
||||
'--prefix',
|
||||
'../..',
|
||||
'exec',
|
||||
'vite',
|
||||
'--',
|
||||
'--config',
|
||||
'vite.config.ts',
|
||||
'--port',
|
||||
String(endpoint.port),
|
||||
],
|
||||
['--prefix', '../..', 'exec', 'vite', '--', '--config', 'vite.config.ts'],
|
||||
{
|
||||
cwd: appRoot,
|
||||
env: withAgcDevEndpointEnv(endpoint),
|
||||
stdio: 'inherit',
|
||||
// Node 18.20+/20+/24 on Windows rejects spawning .cmd (npm.cmd) without a shell (EINVAL).
|
||||
shell: true,
|
||||
|
||||
@@ -5,16 +5,13 @@ import net from 'node:net';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
agcVitePortEnvKey,
|
||||
readAgcDevEndpoint,
|
||||
resolveAgcDevEndpoint,
|
||||
withAgcDevEndpointEnv,
|
||||
} from './dev-port.mjs';
|
||||
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const repoRoot = resolve(appRoot, '../..');
|
||||
const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json');
|
||||
const viteHost = '127.0.0.1';
|
||||
const vitePort = 3080;
|
||||
const viteUrl = `http://${viteHost}:${vitePort}/`;
|
||||
const viteMarkerUrl = `${viteUrl}__agc_dev_server.json`;
|
||||
const defaultApiTarget =
|
||||
process.env.RUST_SERVER_TARGET || 'http://127.0.0.1:8082';
|
||||
const backendDatabase = 'genarrative-game-creator-dev';
|
||||
@@ -134,13 +131,13 @@ async function isBackendReady() {
|
||||
);
|
||||
}
|
||||
|
||||
async function readExistingViteServer(endpoint = readAgcDevEndpoint()) {
|
||||
return httpGetText(endpoint.url);
|
||||
async function readExistingViteServer() {
|
||||
return httpGetText(viteUrl);
|
||||
}
|
||||
|
||||
function isVitePortListening(endpoint = readAgcDevEndpoint()) {
|
||||
function isVitePortListening() {
|
||||
return new Promise((resolveRequest) => {
|
||||
const socket = net.connect({ host: endpoint.host, port: endpoint.port });
|
||||
const socket = net.connect({ host: viteHost, port: vitePort });
|
||||
socket.once('connect', () => {
|
||||
socket.destroy();
|
||||
resolveRequest(true);
|
||||
@@ -163,8 +160,8 @@ function isAiGameCreatorServer(response) {
|
||||
);
|
||||
}
|
||||
|
||||
async function readExistingViteMarker(endpoint = readAgcDevEndpoint()) {
|
||||
const response = await httpGetText(endpoint.markerUrl, 2000);
|
||||
async function readExistingViteMarker() {
|
||||
const response = await httpGetText(viteMarkerUrl, 2000);
|
||||
if (!response || response.statusCode !== 200) {
|
||||
return null;
|
||||
}
|
||||
@@ -176,16 +173,15 @@ async function readExistingViteMarker(endpoint = readAgcDevEndpoint()) {
|
||||
}
|
||||
|
||||
async function preflightExistingVite({
|
||||
endpoint = readAgcDevEndpoint(),
|
||||
readServer = readExistingViteServer,
|
||||
portListening = isVitePortListening,
|
||||
readMarker = readExistingViteMarker,
|
||||
} = {}) {
|
||||
const existing = await readServer(endpoint);
|
||||
const existing = await readServer();
|
||||
if (!existing) {
|
||||
if (await portListening(endpoint)) {
|
||||
if (await portListening()) {
|
||||
throw new Error(
|
||||
`${endpoint.url} is already in use by a non-HTTP or unrecognized server. Stop it before starting Tauri dev.`,
|
||||
`${viteUrl} is already in use by a non-HTTP or unrecognized server. Stop it before starting Tauri dev.`,
|
||||
);
|
||||
}
|
||||
return { status: 'available', apiTarget: '' };
|
||||
@@ -193,11 +189,11 @@ async function preflightExistingVite({
|
||||
|
||||
if (!isAiGameCreatorServer(existing)) {
|
||||
throw new Error(
|
||||
`${endpoint.url} is already in use by another server. Stop it before starting Tauri dev.`,
|
||||
`${viteUrl} is already in use by another server. Stop it before starting Tauri dev.`,
|
||||
);
|
||||
}
|
||||
|
||||
const marker = await readMarker(endpoint);
|
||||
const marker = await readMarker();
|
||||
const markerApiTarget =
|
||||
marker?.schemaVersion === 1 &&
|
||||
marker?.app === 'ai-game-creator-shell' &&
|
||||
@@ -206,7 +202,7 @@ async function preflightExistingVite({
|
||||
: '';
|
||||
const actualTarget = markerApiTarget || 'unknown';
|
||||
throw new Error(
|
||||
`${endpoint.url} is already running with API target ${actualTarget}. Its owning worktree cannot be proven, so it will not be reused. Stop that Vite dev server before starting Tauri dev.`,
|
||||
`${viteUrl} is already running with API target ${actualTarget}. Its owning worktree cannot be proven, so it will not be reused. Stop that Vite dev server before starting Tauri dev.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -553,7 +549,7 @@ async function ensureBackend({
|
||||
}
|
||||
}
|
||||
|
||||
async function startVite(apiTarget, endpoint = readAgcDevEndpoint()) {
|
||||
async function startVite(apiTarget) {
|
||||
const { apiUrl } = readBackendTargets();
|
||||
if (apiUrl !== apiTarget) {
|
||||
throw new Error(
|
||||
@@ -561,32 +557,22 @@ async function startVite(apiTarget, endpoint = readAgcDevEndpoint()) {
|
||||
);
|
||||
}
|
||||
|
||||
const existing = await readExistingViteServer(endpoint);
|
||||
const existing = await readExistingViteServer();
|
||||
if (existing) {
|
||||
if (isAiGameCreatorServer(existing)) {
|
||||
throw new Error(
|
||||
`${endpoint.url} is already running and cannot be safely reused. Stop it before starting Tauri dev.`,
|
||||
`${viteUrl} is already running and cannot be safely reused. Stop it before starting Tauri dev.`,
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`${endpoint.url} is already in use by another server. Stop it before starting Tauri dev.`,
|
||||
`${viteUrl} is already in use by another server. Stop it before starting Tauri dev.`,
|
||||
);
|
||||
}
|
||||
|
||||
return spawnChild(
|
||||
npm,
|
||||
[
|
||||
'--prefix',
|
||||
'../..',
|
||||
'exec',
|
||||
'vite',
|
||||
'--',
|
||||
'--config',
|
||||
'vite.config.ts',
|
||||
'--port',
|
||||
String(endpoint.port),
|
||||
],
|
||||
{ cwd: appRoot, env: withAgcDevEndpointEnv(endpoint) },
|
||||
['--prefix', '../..', 'exec', 'vite', '--', '--config', 'vite.config.ts'],
|
||||
{ cwd: appRoot },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -607,9 +593,7 @@ async function main() {
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoint = await resolveAgcDevEndpoint({ strictConfigured: true });
|
||||
process.env[agcVitePortEnvKey] = String(endpoint.port);
|
||||
await preflightExistingVite({ endpoint });
|
||||
await preflightExistingVite();
|
||||
const backend = await ensureBackend({
|
||||
onBackendChild(child) {
|
||||
backendChild = child;
|
||||
@@ -623,7 +607,7 @@ async function main() {
|
||||
throw new Error(`启动期收到 ${shutdownSignal},已停止配套后端`);
|
||||
}
|
||||
|
||||
viteChild = await startVite(backend.targets.apiUrl, endpoint);
|
||||
viteChild = await startVite(backend.targets.apiUrl);
|
||||
if (shutdownSignal) {
|
||||
stopChild(viteChild, shutdownSignal);
|
||||
throw new Error(`启动期收到 ${shutdownSignal},已停止前端服务`);
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
readAgcDevEndpoint,
|
||||
resolveAgcDevEndpoint,
|
||||
withAgcDevEndpointEnv,
|
||||
} from './dev-port.mjs';
|
||||
import {
|
||||
preflightExistingVite,
|
||||
spawnChild,
|
||||
@@ -27,41 +22,17 @@ function parseLauncherArguments(argv) {
|
||||
return { gameChat, args };
|
||||
}
|
||||
|
||||
function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
||||
function buildTauriArguments(argv) {
|
||||
const { gameChat, args } = parseLauncherArguments(argv);
|
||||
const configOverride = JSON.stringify({ build: { devUrl } });
|
||||
if (gameChat) {
|
||||
return [
|
||||
'dev',
|
||||
'--config',
|
||||
configOverride,
|
||||
'--',
|
||||
'--',
|
||||
'--game-chat',
|
||||
...args,
|
||||
];
|
||||
return ['dev', '--', '--', '--game-chat', ...args];
|
||||
}
|
||||
const separatorIndex = args.indexOf('--');
|
||||
if (separatorIndex < 0) {
|
||||
return ['dev', ...args, '--config', configOverride];
|
||||
}
|
||||
const separatedArguments = args.slice(separatorIndex);
|
||||
if (separatedArguments[1] !== '--') {
|
||||
separatedArguments.unshift('--');
|
||||
}
|
||||
return [
|
||||
'dev',
|
||||
...args.slice(0, separatorIndex),
|
||||
'--config',
|
||||
configOverride,
|
||||
...separatedArguments,
|
||||
];
|
||||
return ['dev', ...args];
|
||||
}
|
||||
|
||||
function spawnTauriCli(argv, { env = process.env } = {}) {
|
||||
function spawnTauriCli(argv) {
|
||||
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
|
||||
cwd: appRoot,
|
||||
env,
|
||||
shell: false,
|
||||
});
|
||||
}
|
||||
@@ -69,20 +40,16 @@ function spawnTauriCli(argv, { env = process.env } = {}) {
|
||||
async function runTauriDev(
|
||||
argv = process.argv.slice(2),
|
||||
{
|
||||
resolveDevEndpoint = resolveAgcDevEndpoint,
|
||||
preflight = preflightExistingVite,
|
||||
spawnCli = spawnTauriCli,
|
||||
waitForCli = waitForChildTermination,
|
||||
terminateTree = terminateChildTree,
|
||||
} = {},
|
||||
) {
|
||||
const endpoint = await resolveDevEndpoint();
|
||||
await preflight({ endpoint });
|
||||
await preflight();
|
||||
|
||||
const tauriArguments = buildTauriArguments(argv, endpoint.url);
|
||||
const child = spawnCli(tauriArguments, {
|
||||
env: withAgcDevEndpointEnv(endpoint),
|
||||
});
|
||||
const tauriArguments = buildTauriArguments(argv);
|
||||
const child = spawnCli(tauriArguments);
|
||||
let resolveShutdown;
|
||||
let shutdownSignal = '';
|
||||
let repeatedSignal = false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user