Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97b0a4b9b8 | |||
| e3ba986596 | |||
| 69af571242 | |||
| 166977baf0 | |||
| 5c58830545 | |||
| 14c2255d54 | |||
| f8f0479488 | |||
| a06c7d7f1e | |||
| c38e4b4175 | |||
| f3c362b1ec | |||
| fc53f362e4 | |||
| 8988e91ee9 | |||
| 784f38220b | |||
| 44798ba380 | |||
| 2d679122cf | |||
| 4cbbcecaa2 | |||
| 98bfa5efd0 | |||
| ab472b4562 | |||
| 1a1f124acd | |||
| 4dc199cf33 | |||
| ef34e0ed64 | |||
| 786898f7f8 | |||
| e880f58723 | |||
| 6f19e155ca | |||
| 6e9f991a76 | |||
| a6f99c8871 | |||
| 27e3d9bfd0 | |||
| c6f90c5b22 | |||
| 3b137ca369 | |||
| 3da0936c1f | |||
| c8311a47b0 | |||
| 1c1deff732 | |||
| 0459109897 | |||
| 6318baf7c6 | |||
| 28e6e67e98 | |||
| 4415723f7f | |||
| fa84a364d6 | |||
| bd0a06e3d3 | |||
| 939c35a9b9 | |||
| 42fdb57efe | |||
| 0c54cca81c | |||
| 1a2e60bf05 | |||
| 5ce267ae85 | |||
| 8924bf335e | |||
| ae257a6ff8 | |||
| ff4f42d2f2 | |||
| 2e2ed47bec | |||
| dedc58cce1 | |||
| c86ac04220 | |||
| 0817ea8158 | |||
| e393a803f2 | |||
| 89587c9e9e | |||
| 5cba2dbeb7 | |||
| d3407e8910 | |||
| 4888a0eb01 | |||
| 90b77d6bd0 | |||
| 1c950df116 | |||
| 6c33459b90 | |||
| 874d061cb9 | |||
| fa2b36654b | |||
| 345c9c3a99 | |||
| 99081455ee | |||
| 5b1b0ddb9a | |||
| f4315ffd23 | |||
| beb3c99240 | |||
| 1574f206a2 | |||
| 1367e9c04e | |||
| b294cbc3db | |||
| c34d24c2c8 | |||
| 0dbc34d13c |
@@ -102,7 +102,7 @@ Use OpenAPI as the final authority; these common values are a routing aid:
|
|||||||
|
|
||||||
- Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`; ordinary image generation may omit it.
|
- Image `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.
|
- 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 `model`: `gpt-image-2.5`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`. Persisted `gpt-image-2` is a legacy value resolved only when submitting a new task; the retired `gpt-image-2-c` is no longer accepted and is handled as an unsupported value.
|
||||||
- Image `aspectRatio`: `1:1`, `2:3`, `3:2`, `9:16`, `16:9`.
|
- Image `aspectRatio`: `1:1`, `2:3`, `3:2`, `9:16`, `16:9`.
|
||||||
- Image `imageSize`: `0.5K`, `1K`, `2K`.
|
- Image `imageSize`: `0.5K`, `1K`, `2K`.
|
||||||
- Video `model`: `seedance2.0`, `seedance2.0-fast`, `kling3.0`, `kling3.0-omni`, `veo3.1`, `veo3.1-fast`.
|
- Video `model`: `seedance2.0`, `seedance2.0-fast`, `kling3.0`, `kling3.0-omni`, `veo3.1`, `veo3.1-fast`.
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
---
|
---
|
||||||
name: gpt-image-2-apimart
|
name: gpt-image-2-apimart
|
||||||
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow with gpt-image-2-c fallback. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
|
description: Generate or inspect project image assets through this repository's Tiantoken GPT Image 2.5 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug TIANTOKEN_BASE_URL / TIANTOKEN_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
|
||||||
---
|
---
|
||||||
|
|
||||||
# gpt-image-2 VectorEngine
|
# GPT Image 2.5 project image workflow
|
||||||
|
|
||||||
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine image path. Keep the product/price model identifier and primary provider request as `gpt-image-2`, then fall back once to `gpt-image-2-c` for eligible provider failures. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
|
Use this skill for project-local image asset generation that must match the repository's image request contract. Provider routing is owned by `server-rs`; when this skill talks to the provider directly it must send the concrete provider model and the matching provider credentials, because the provider side only accepts concrete models:
|
||||||
|
|
||||||
|
- generation (no reference images): `gpt-image-2.5-flare-c` through Tiantoken
|
||||||
|
- edits (any reference image): `gpt-image-2.5-sunburst-c` through Tiantoken
|
||||||
|
- nanobanana (`gemini-3.1-flash-image-preview`) stays on VectorEngine
|
||||||
|
|
||||||
|
The business model name `gpt-image-2.5` is resolved to a concrete key by `server-rs` at the task boundary and is never sent to a provider directly. This client must not perform a cross-model fallback. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
@@ -24,15 +30,15 @@ Use this skill for project-local image asset generation that must match the repo
|
|||||||
```
|
```
|
||||||
|
|
||||||
5. Save final project assets under `public/` or another explicitly requested workspace path.
|
5. Save final project assets under `public/` or another explicitly requested workspace path.
|
||||||
6. Never print `VECTOR_ENGINE_API_KEY`. Report only whether configuration exists.
|
6. Never print `TIANTOKEN_API_KEY`. Report only whether configuration exists.
|
||||||
|
|
||||||
## Request Contract
|
## Request Contract
|
||||||
|
|
||||||
The repository image path uses:
|
The repository image path uses:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations
|
POST {TIANTOKEN_BASE_URL}/v1/images/generations
|
||||||
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
Authorization: Bearer {TIANTOKEN_API_KEY}
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -40,7 +46,7 @@ Default body:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"model": "gpt-image-2",
|
"model": "gpt-image-2.5-flare-c",
|
||||||
"prompt": "<prompt>",
|
"prompt": "<prompt>",
|
||||||
"n": 1,
|
"n": 1,
|
||||||
"size": "1024x1024"
|
"size": "1024x1024"
|
||||||
@@ -50,22 +56,22 @@ Default body:
|
|||||||
For visual references, use the edit endpoint instead of the create endpoint:
|
For visual references, use the edit endpoint instead of the create endpoint:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
POST {VECTOR_ENGINE_BASE_URL}/v1/images/edits
|
POST {TIANTOKEN_BASE_URL}/v1/images/edits
|
||||||
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
Authorization: Bearer {TIANTOKEN_API_KEY}
|
||||||
Content-Type: multipart/form-data
|
Content-Type: multipart/form-data
|
||||||
```
|
```
|
||||||
|
|
||||||
Multipart fields:
|
Multipart fields:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
model=gpt-image-2
|
model=gpt-image-2.5-sunburst-c
|
||||||
prompt=<prompt>
|
prompt=<prompt>
|
||||||
n=1
|
n=1
|
||||||
size=1024x1024
|
size=1024x1024
|
||||||
image=@reference.png
|
image=@reference.png
|
||||||
```
|
```
|
||||||
|
|
||||||
In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Both paths prefer `gpt-image-2`; on an eligible upstream/model failure they retry with `gpt-image-2-c`. Do not fall back for authentication, local validation, request-budget exhaustion, uncertain send/connection failure, content-safety rejection, or a generated image URL download failure. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
|
In this repository, calls with no reference images use `POST /v1/images/generations` with `gpt-image-2.5-flare-c`; calls with any reference image use `POST /v1/images/edits` with `gpt-image-2.5-sunburst-c` and pass references as one or more `image` form parts. Server-side calls still submit the business model `gpt-image-2.5` and let `server-rs` resolve the concrete key; provider routing and retry policy remain server-owned, and pricing follows the same reference-image rule (generation with a reference image is charged at the edit tier). Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
|
||||||
|
|
||||||
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine image generation currently returns synchronously; do not poll APIMart task endpoints.
|
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine image generation currently returns synchronously; do not poll APIMart task endpoints.
|
||||||
|
|
||||||
@@ -75,12 +81,14 @@ Load environment values from process env first, then `.env.secrets.local`, `.env
|
|||||||
|
|
||||||
Required for live generation:
|
Required for live generation:
|
||||||
|
|
||||||
- `VECTOR_ENGINE_BASE_URL`
|
- `TIANTOKEN_BASE_URL`
|
||||||
- `VECTOR_ENGINE_API_KEY`
|
- `TIANTOKEN_API_KEY`
|
||||||
|
|
||||||
Optional:
|
Optional:
|
||||||
|
|
||||||
- `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`
|
- `TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS`
|
||||||
|
|
||||||
|
`VECTOR_ENGINE_*` values no longer serve GPT Image 2.5: those credentials belong to VectorEngine, which only serves nanobanana. Do not fall back from `TIANTOKEN_*` to `VECTOR_ENGINE_*`.
|
||||||
|
|
||||||
If the key or base URL is missing, stop after dry-run or explain the missing configuration. Do not ask the user to paste the key in chat.
|
If the key or base URL is missing, stop after dry-run or explain the missing configuration. Do not ask the user to paste the key in chat.
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ const skillRoot = path.resolve(__dirname, '..');
|
|||||||
const repoRoot = path.resolve(skillRoot, '..', '..', '..');
|
const repoRoot = path.resolve(skillRoot, '..', '..', '..');
|
||||||
const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations');
|
const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations');
|
||||||
const defaultTimeoutMs = 1000000;
|
const defaultTimeoutMs = 1000000;
|
||||||
const preferredImageModel = 'gpt-image-2';
|
// GPT Image 2.5 生成任务只接受 concrete provider model;业务模型名 `gpt-image-2.5`
|
||||||
const fallbackImageModel = 'gpt-image-2-c';
|
// 由 server-rs 在任务边界解析,脚本直连 provider 时必须自己给出 concrete key。
|
||||||
|
const preferredImageModel = 'gpt-image-2.5-flare-c';
|
||||||
|
|
||||||
const prompts = [
|
const prompts = [
|
||||||
{
|
{
|
||||||
@@ -101,18 +102,18 @@ function resolveEnv() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
|
baseUrl: String(loaded.TIANTOKEN_BASE_URL || '')
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/\/+$/u, ''),
|
.replace(/\/+$/u, ''),
|
||||||
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
|
apiKey: String(loaded.TIANTOKEN_API_KEY || '').trim(),
|
||||||
timeoutMs: Number.parseInt(
|
timeoutMs: Number.parseInt(
|
||||||
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
|
String(loaded.TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildVectorEngineImagesGenerationUrl(baseUrl) {
|
function buildTiantokenImagesGenerationUrl(baseUrl) {
|
||||||
return baseUrl.endsWith('/v1')
|
return baseUrl.endsWith('/v1')
|
||||||
? `${baseUrl}/images/generations`
|
? `${baseUrl}/images/generations`
|
||||||
: `${baseUrl}/v1/images/generations`;
|
: `${baseUrl}/v1/images/generations`;
|
||||||
@@ -233,22 +234,22 @@ async function fetchJson(url, options, timeoutMs) {
|
|||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
`VectorEngine ${response.status}: ${text.slice(0, 600)}`,
|
`Tiantoken ${response.status}: ${text.slice(0, 600)}`,
|
||||||
);
|
);
|
||||||
error.vectorEngineStatus = response.status;
|
error.providerStatus = response.status;
|
||||||
error.vectorEngineBody = text;
|
error.providerBody = text;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(text);
|
return JSON.parse(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
error.vectorEngineResponseParse = true;
|
error.providerResponseParse = true;
|
||||||
error.vectorEngineBody = text;
|
error.providerBody = text;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name === 'AbortError') {
|
if (error?.name === 'AbortError') {
|
||||||
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
|
throw new Error(`Tiantoken request timed out after ${timeoutMs}ms`);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -256,81 +257,35 @@ async function fetchJson(url, options, timeoutMs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldFallbackImageModel(error) {
|
|
||||||
const raw =
|
|
||||||
`${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
|
|
||||||
if (error?.vectorEngineResponseParse) {
|
|
||||||
return !containsContentRejection(raw);
|
|
||||||
}
|
|
||||||
const status = Number(error?.vectorEngineStatus || 0);
|
|
||||||
if (status === 408 || status >= 500) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (status === 429) {
|
|
||||||
return !containsContentRejection(raw);
|
|
||||||
}
|
|
||||||
const mentionsImageModel =
|
|
||||||
raw.includes('model') ||
|
|
||||||
raw.includes('模型') ||
|
|
||||||
raw.includes(preferredImageModel) ||
|
|
||||||
raw.includes(fallbackImageModel);
|
|
||||||
return (
|
|
||||||
[400, 404, 422].includes(status) &&
|
|
||||||
mentionsImageModel &&
|
|
||||||
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
|
|
||||||
raw,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function containsContentRejection(raw) {
|
|
||||||
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
|
|
||||||
raw,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestImagePayload(env, entry) {
|
async function requestImagePayload(env, entry) {
|
||||||
for (const model of [preferredImageModel, fallbackImageModel]) {
|
const model = preferredImageModel;
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
model,
|
model,
|
||||||
prompt: buildPrompt(entry),
|
prompt: buildPrompt(entry),
|
||||||
n: 1,
|
n: 1,
|
||||||
size: '1024x1024',
|
size: '1024x1024',
|
||||||
};
|
};
|
||||||
try {
|
const payload = await fetchJson(
|
||||||
const payload = await fetchJson(
|
buildTiantokenImagesGenerationUrl(env.baseUrl),
|
||||||
buildVectorEngineImagesGenerationUrl(env.baseUrl),
|
{
|
||||||
{
|
method: 'POST',
|
||||||
method: 'POST',
|
headers: {
|
||||||
headers: {
|
Authorization: `Bearer ${env.apiKey}`,
|
||||||
Authorization: `Bearer ${env.apiKey}`,
|
Accept: 'application/json',
|
||||||
Accept: 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
},
|
||||||
},
|
body: JSON.stringify(requestBody),
|
||||||
body: JSON.stringify(requestBody),
|
},
|
||||||
},
|
env.timeoutMs,
|
||||||
env.timeoutMs,
|
);
|
||||||
);
|
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
|
||||||
const base64Image = decodeStrictBase64Image(
|
if (extractImageUrls(payload)[0] || base64Image) {
|
||||||
extractBase64Images(payload)[0],
|
return payload;
|
||||||
);
|
|
||||||
if (extractImageUrls(payload)[0] || base64Image) {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
const error = new Error(`VectorEngine returned no image for ${entry.id}`);
|
|
||||||
error.vectorEngineResponseParse = true;
|
|
||||||
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
|
|
||||||
throw error;
|
|
||||||
} catch (error) {
|
|
||||||
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
console.warn(
|
|
||||||
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw new Error(`VectorEngine returned no image for ${entry.id}`);
|
const error = new Error(`Tiantoken returned no image for ${entry.id}`);
|
||||||
|
error.providerResponseParse = true;
|
||||||
|
error.providerBody = JSON.stringify(payload).slice(0, 600);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadUrl(url, timeoutMs) {
|
async function downloadUrl(url, timeoutMs) {
|
||||||
@@ -373,7 +328,7 @@ async function generateOne(env, entry, outDir) {
|
|||||||
const bytes = decodeStrictBase64Image(b64Images[0]);
|
const bytes = decodeStrictBase64Image(b64Images[0]);
|
||||||
if (!bytes) {
|
if (!bytes) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`VectorEngine returned invalid base64 image for ${entry.id}`,
|
`Tiantoken returned invalid base64 image for ${entry.id}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
image = {
|
image = {
|
||||||
@@ -381,7 +336,7 @@ async function generateOne(env, entry, outDir) {
|
|||||||
extension: inferExtensionFromBytes(bytes),
|
extension: inferExtensionFromBytes(bytes),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`VectorEngine returned no image for ${entry.id}`);
|
throw new Error(`Tiantoken returned no image for ${entry.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdirSync(outDir, { recursive: true });
|
mkdirSync(outDir, { recursive: true });
|
||||||
@@ -408,7 +363,6 @@ if (dryRun) {
|
|||||||
requests: selectedPrompts.map((entry) => ({
|
requests: selectedPrompts.map((entry) => ({
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
title: entry.title,
|
title: entry.title,
|
||||||
fallbackModel: fallbackImageModel,
|
|
||||||
body: {
|
body: {
|
||||||
model: preferredImageModel,
|
model: preferredImageModel,
|
||||||
prompt: buildPrompt(entry),
|
prompt: buildPrompt(entry),
|
||||||
@@ -429,7 +383,7 @@ if (!env.baseUrl || !env.apiKey) {
|
|||||||
console.error(
|
console.error(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
|
error: 'Missing TIANTOKEN_BASE_URL or TIANTOKEN_API_KEY',
|
||||||
hasBaseUrl: Boolean(env.baseUrl),
|
hasBaseUrl: Boolean(env.baseUrl),
|
||||||
hasApiKey: Boolean(env.apiKey),
|
hasApiKey: Boolean(env.apiKey),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ const defaultOutDir = path.join(
|
|||||||
'puzzle-creation-templates',
|
'puzzle-creation-templates',
|
||||||
);
|
);
|
||||||
const defaultTimeoutMs = 1000000;
|
const defaultTimeoutMs = 1000000;
|
||||||
const preferredImageModel = 'gpt-image-2';
|
// GPT Image 2.5 生成任务只接受 concrete provider model;业务模型名 `gpt-image-2.5`
|
||||||
const fallbackImageModel = 'gpt-image-2-c';
|
// 由 server-rs 在任务边界解析,脚本直连 provider 时必须自己给出 concrete key。
|
||||||
|
const preferredImageModel = 'gpt-image-2.5-flare-c';
|
||||||
|
|
||||||
const args = new Map();
|
const args = new Map();
|
||||||
for (let index = 2; index < process.argv.length; index += 1) {
|
for (let index = 2; index < process.argv.length; index += 1) {
|
||||||
@@ -71,18 +72,18 @@ function resolveEnv() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
|
baseUrl: String(loaded.TIANTOKEN_BASE_URL || '')
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/\/+$/u, ''),
|
.replace(/\/+$/u, ''),
|
||||||
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
|
apiKey: String(loaded.TIANTOKEN_API_KEY || '').trim(),
|
||||||
timeoutMs: Number.parseInt(
|
timeoutMs: Number.parseInt(
|
||||||
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
|
String(loaded.TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildVectorEngineImagesGenerationUrl(baseUrl) {
|
function buildTiantokenImagesGenerationUrl(baseUrl) {
|
||||||
return baseUrl.endsWith('/v1')
|
return baseUrl.endsWith('/v1')
|
||||||
? `${baseUrl}/images/generations`
|
? `${baseUrl}/images/generations`
|
||||||
: `${baseUrl}/v1/images/generations`;
|
: `${baseUrl}/v1/images/generations`;
|
||||||
@@ -203,22 +204,22 @@ async function fetchJson(url, options, timeoutMs) {
|
|||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
`VectorEngine ${response.status}: ${text.slice(0, 600)}`,
|
`Tiantoken ${response.status}: ${text.slice(0, 600)}`,
|
||||||
);
|
);
|
||||||
error.vectorEngineStatus = response.status;
|
error.providerStatus = response.status;
|
||||||
error.vectorEngineBody = text;
|
error.providerBody = text;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(text);
|
return JSON.parse(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
error.vectorEngineResponseParse = true;
|
error.providerResponseParse = true;
|
||||||
error.vectorEngineBody = text;
|
error.providerBody = text;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name === 'AbortError') {
|
if (error?.name === 'AbortError') {
|
||||||
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
|
throw new Error(`Tiantoken request timed out after ${timeoutMs}ms`);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -226,83 +227,35 @@ async function fetchJson(url, options, timeoutMs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldFallbackImageModel(error) {
|
|
||||||
const raw =
|
|
||||||
`${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
|
|
||||||
if (error?.vectorEngineResponseParse) {
|
|
||||||
return !containsContentRejection(raw);
|
|
||||||
}
|
|
||||||
const status = Number(error?.vectorEngineStatus || 0);
|
|
||||||
if (status === 408 || status >= 500) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (status === 429) {
|
|
||||||
return !containsContentRejection(raw);
|
|
||||||
}
|
|
||||||
const mentionsImageModel =
|
|
||||||
raw.includes('model') ||
|
|
||||||
raw.includes('模型') ||
|
|
||||||
raw.includes(preferredImageModel) ||
|
|
||||||
raw.includes(fallbackImageModel);
|
|
||||||
return (
|
|
||||||
[400, 404, 422].includes(status) &&
|
|
||||||
mentionsImageModel &&
|
|
||||||
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
|
|
||||||
raw,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function containsContentRejection(raw) {
|
|
||||||
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
|
|
||||||
raw,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestImagePayload(env, template) {
|
async function requestImagePayload(env, template) {
|
||||||
for (const model of [preferredImageModel, fallbackImageModel]) {
|
const model = preferredImageModel;
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
model,
|
model,
|
||||||
prompt: buildPrompt(template),
|
prompt: buildPrompt(template),
|
||||||
n: 1,
|
n: 1,
|
||||||
size: '1024x1024',
|
size: '1024x1024',
|
||||||
};
|
};
|
||||||
try {
|
const payload = await fetchJson(
|
||||||
const payload = await fetchJson(
|
buildTiantokenImagesGenerationUrl(env.baseUrl),
|
||||||
buildVectorEngineImagesGenerationUrl(env.baseUrl),
|
{
|
||||||
{
|
method: 'POST',
|
||||||
method: 'POST',
|
headers: {
|
||||||
headers: {
|
Authorization: `Bearer ${env.apiKey}`,
|
||||||
Authorization: `Bearer ${env.apiKey}`,
|
Accept: 'application/json',
|
||||||
Accept: 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
},
|
||||||
},
|
body: JSON.stringify(requestBody),
|
||||||
body: JSON.stringify(requestBody),
|
},
|
||||||
},
|
env.timeoutMs,
|
||||||
env.timeoutMs,
|
);
|
||||||
);
|
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
|
||||||
const base64Image = decodeStrictBase64Image(
|
if (extractImageUrls(payload)[0] || base64Image) {
|
||||||
extractBase64Images(payload)[0],
|
return payload;
|
||||||
);
|
|
||||||
if (extractImageUrls(payload)[0] || base64Image) {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
const error = new Error(
|
|
||||||
`VectorEngine returned no image for ${template.id}`,
|
|
||||||
);
|
|
||||||
error.vectorEngineResponseParse = true;
|
|
||||||
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
|
|
||||||
throw error;
|
|
||||||
} catch (error) {
|
|
||||||
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
console.warn(
|
|
||||||
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw new Error(`VectorEngine returned no image for ${template.id}`);
|
const error = new Error(`Tiantoken returned no image for ${template.id}`);
|
||||||
|
error.providerResponseParse = true;
|
||||||
|
error.providerBody = JSON.stringify(payload).slice(0, 600);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadUrl(url, timeoutMs) {
|
async function downloadUrl(url, timeoutMs) {
|
||||||
@@ -345,7 +298,7 @@ async function generateOne(env, template, outDir) {
|
|||||||
const bytes = decodeStrictBase64Image(b64Images[0]);
|
const bytes = decodeStrictBase64Image(b64Images[0]);
|
||||||
if (!bytes) {
|
if (!bytes) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`VectorEngine returned invalid base64 image for ${template.id}`,
|
`Tiantoken returned invalid base64 image for ${template.id}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
image = {
|
image = {
|
||||||
@@ -353,7 +306,7 @@ async function generateOne(env, template, outDir) {
|
|||||||
extension: inferExtensionFromBytes(bytes),
|
extension: inferExtensionFromBytes(bytes),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`VectorEngine returned no image for ${template.id}`);
|
throw new Error(`Tiantoken returned no image for ${template.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdirSync(outDir, { recursive: true });
|
mkdirSync(outDir, { recursive: true });
|
||||||
@@ -384,7 +337,6 @@ if (dryRun) {
|
|||||||
requests: selectedTemplates.map((template) => ({
|
requests: selectedTemplates.map((template) => ({
|
||||||
id: template.id,
|
id: template.id,
|
||||||
title: template.title,
|
title: template.title,
|
||||||
fallbackModel: fallbackImageModel,
|
|
||||||
body: {
|
body: {
|
||||||
model: preferredImageModel,
|
model: preferredImageModel,
|
||||||
prompt: buildPrompt(template),
|
prompt: buildPrompt(template),
|
||||||
@@ -405,7 +357,7 @@ if (!env.baseUrl || !env.apiKey) {
|
|||||||
console.error(
|
console.error(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
|
error: 'Missing TIANTOKEN_BASE_URL or TIANTOKEN_API_KEY',
|
||||||
hasBaseUrl: Boolean(env.baseUrl),
|
hasBaseUrl: Boolean(env.baseUrl),
|
||||||
hasApiKey: Boolean(env.apiKey),
|
hasApiKey: Boolean(env.apiKey),
|
||||||
}),
|
}),
|
||||||
|
|||||||
+29
@@ -44,6 +44,35 @@ _Avoid_: 把同一资源的全局元数据和某一次摆放坐标混在同一
|
|||||||
由图片生成或图片修改流程产生的画布资源,必须记录来源资源、提示词、实际提示词、模型、provider、任务 ID 和生成时间;本期 `/editor` 的生成修改先允许 mock 生成资源,但仍按生成资源元数据形状保存。
|
由图片生成或图片修改流程产生的画布资源,必须记录来源资源、提示词、实际提示词、模型、provider、任务 ID 和生成时间;本期 `/editor` 的生成修改先允许 mock 生成资源,但仍按生成资源元数据形状保存。
|
||||||
_Avoid_: 无来源的静态素材、只显示在 UI 但不落工程资源记录的生成结果
|
_Avoid_: 无来源的静态素材、只显示在 UI 但不落工程资源记录的生成结果
|
||||||
|
|
||||||
|
**图片模型历史值与使用端解析**:
|
||||||
|
图片资源中已持久化的 `gpt-image-2` 是历史业务事实,读回时保持原值;新任务使用业务模型值 `gpt-image-2.5`。当用户基于历史资源再次发起生成或编辑任务时,服务端只在新任务的使用端把历史值解析为当前业务模型,不改写历史资源。provider route 属于服务端执行与审计边界,前端不接收、不持久化、不展示,也不据此分支。
|
||||||
|
_Avoid_: 读取数据库时改写历史模型值、把 provider route 暴露为前端模型选项或公开 DTO
|
||||||
|
|
||||||
|
**图片 provider 显式路由**:
|
||||||
|
api-server 在任务入口按业务语义显式选择具体 provider model name(生成或编辑),并把同一具体名传给图片平台适配器和后台定价解析;图片平台适配器不从参考图数量或前端字段猜测任务。具体 provider model name 只存在于服务端调用、定价配置和审计边界。
|
||||||
|
后台管理 Web/API 是明确例外,可以查看和编辑两个具体定价 key;主站普通前端与公开定价 API 不接收这些 key。
|
||||||
|
_Avoid_: 让图片适配器隐式猜路由、让主站前端携带 provider model name
|
||||||
|
|
||||||
|
**业务模型**:
|
||||||
|
面向任务与产品契约的稳定模型值;当前 GPT 图片新任务的业务模型是 `gpt-image-2.5`。业务模型不等同于 provider 的具体计费/请求 model,也不暴露 provider 凭证或 endpoint。
|
||||||
|
_Avoid_: 把 provider concrete model 当作前端业务选项、用业务模型值直接推断 provider 凭证
|
||||||
|
|
||||||
|
**具体模型**:
|
||||||
|
服务端发送请求和定价使用的 concrete model name。GPT Image 2.5 生成与编辑分别是 `gpt-image-2.5-flare-c` 和 `gpt-image-2.5-sunburst-c`;nanobanana 仍使用 `gemini-3.1-flash-image-preview`。具体模型只在服务端执行、定价和审计边界出现。
|
||||||
|
_Avoid_: 把具体模型写入普通前端 DTO、让未知字符串自动选择 provider
|
||||||
|
|
||||||
|
**provider client**:
|
||||||
|
按具体模型选出的外部图片 provider 连接配置,包含 provider identity、base URL 和 API key;VectorEngine 与 Tiantoken client 共享图片协议执行器,不复制请求/响应业务逻辑。两套 required client 在 api-server 启动时构造。
|
||||||
|
_Avoid_: 在首次请求时才创建 client、在 provider client 中复制尺寸/重试/审计逻辑、跨 provider credential fallback
|
||||||
|
|
||||||
|
**历史模型值**:
|
||||||
|
已持久化的 `gpt-image-2` 字符串,只作为历史事实原样读取和审计;基于历史资源提交新任务时,在使用端解析为当前 GPT Image 2.5 业务任务,不回写历史记录,也不把旧值作为现役 provider route。已退役的 `gpt-image-2-c` 已从代码整体删除,只有数据库里的历史审计字符串原样保留,任何入口传入该值都按不支持的值处理。
|
||||||
|
_Avoid_: 数据库批量改写历史值、把历史值重新路由到 VectorEngine、把兼容解析扩散到普通前端、为已删除的 `gpt-image-2-c` 重新加回常量或解析分支
|
||||||
|
|
||||||
|
**GPT Image 2.5 新生成展示名**:
|
||||||
|
`GPT Image 2.5` 是新生成任务的产品展示名;历史资源与既有编辑上下文不因新模型上线而改写展示语义。
|
||||||
|
_Avoid_: 把新生成展示名扩散到历史记录、历史生成器或旧编辑上下文
|
||||||
|
|
||||||
**系列素材图集生成**:
|
**系列素材图集生成**:
|
||||||
一组同类素材的统一批量生成方式,采用批量规划、sheet 生图、后端切图、透明化、OSS 持久化和局部重生成的通用流水线。
|
一组同类素材的统一批量生成方式,采用批量规划、sheet 生图、后端切图、透明化、OSS 持久化和局部重生成的通用流水线。
|
||||||
_Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成任一玩法专属 DTO
|
_Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成任一玩法专属 DTO
|
||||||
|
|||||||
@@ -185,7 +185,12 @@ test('nextVersion 只在 patch 位递增', () => {
|
|||||||
|
|
||||||
test('ossutil 参数默认使用 v1 签名,并可按需带 region 与 v4', () => {
|
test('ossutil 参数默认使用 v1 签名,并可按需带 region 与 v4', () => {
|
||||||
const base = {
|
const base = {
|
||||||
args: ['cp', '--force', '/tmp/a.json', 'oss://agc-dev/agc/global-version.json'],
|
args: [
|
||||||
|
'cp',
|
||||||
|
'--force',
|
||||||
|
'/tmp/a.json',
|
||||||
|
'oss://agc-dev/agc/global-version.json',
|
||||||
|
],
|
||||||
endpoint: 'oss-rg-china-mainland.aliyuncs.com',
|
endpoint: 'oss-rg-china-mainland.aliyuncs.com',
|
||||||
accessKeyId: 'id',
|
accessKeyId: 'id',
|
||||||
accessKeySecret: 'secret',
|
accessKeySecret: 'secret',
|
||||||
|
|||||||
@@ -1192,7 +1192,7 @@ async function main() {
|
|||||||
function isDirectModuleExecution() {
|
function isDirectModuleExecution() {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
process.argv[1] &&
|
process.argv[1] &&
|
||||||
resolve(process.argv[1]) === fileURLToPath(import.meta.url),
|
resolve(process.argv[1]) === fileURLToPath(import.meta.url),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,10 +166,10 @@ export function ActiveProjectRunsPanel({
|
|||||||
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
|
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
|
||||||
const isCurrent = Boolean(
|
const isCurrent = Boolean(
|
||||||
currentProjectPath &&
|
currentProjectPath &&
|
||||||
projectPathsMatchForInvalidation(
|
projectPathsMatchForInvalidation(
|
||||||
turn.projectPath,
|
turn.projectPath,
|
||||||
currentProjectPath,
|
currentProjectPath,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<li key={`${turn.projectPath}:${turn.turnId}`}>
|
<li key={`${turn.projectPath}:${turn.turnId}`}>
|
||||||
@@ -215,10 +215,10 @@ export function ActiveProjectRunsPanel({
|
|||||||
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
|
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
|
||||||
const isCurrent = Boolean(
|
const isCurrent = Boolean(
|
||||||
currentProjectPath &&
|
currentProjectPath &&
|
||||||
projectPathsMatchForInvalidation(
|
projectPathsMatchForInvalidation(
|
||||||
turn.projectPath,
|
turn.projectPath,
|
||||||
currentProjectPath,
|
currentProjectPath,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<li key={`${turn.projectPath}:${turn.turnId}`}>
|
<li key={`${turn.projectPath}:${turn.turnId}`}>
|
||||||
|
|||||||
+3
-1
@@ -4,4 +4,6 @@
|
|||||||
* 审批 / 提问请求与解决:本轮只透传,不并入聊天状态。
|
* 审批 / 提问请求与解决:本轮只透传,不并入聊天状态。
|
||||||
*/
|
*/
|
||||||
export type DirectThreadRequestKind =
|
export type DirectThreadRequestKind =
|
||||||
'approval.requested' | 'ask.requested' | 'request.resolved';
|
| 'approval.requested'
|
||||||
|
| 'ask.requested'
|
||||||
|
| 'request.resolved';
|
||||||
|
|||||||
+3
-1
@@ -40,7 +40,9 @@ export type ResourceCanvasGenerationPlaceholderPanel =
|
|||||||
* - `failed`:这次生成失败;占位留在画布上,点它可以用同一份输入与引用重试。
|
* - `failed`:这次生成失败;占位留在画布上,点它可以用同一份输入与引用重试。
|
||||||
*/
|
*/
|
||||||
export type ResourceCanvasGenerationPlaceholderStatus =
|
export type ResourceCanvasGenerationPlaceholderStatus =
|
||||||
'draft' | 'submitted' | 'failed';
|
| 'draft'
|
||||||
|
| 'submitted'
|
||||||
|
| 'failed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具点击后先在当前栏目创建的临时占位卡。
|
* 工具点击后先在当前栏目创建的临时占位卡。
|
||||||
|
|||||||
@@ -61,8 +61,9 @@ export function ResourceModelViewer({
|
|||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const THREE = await import('three');
|
const THREE = await import('three');
|
||||||
const { OrbitControls } =
|
const { OrbitControls } = await import(
|
||||||
await import('three/examples/jsm/controls/OrbitControls.js');
|
'three/examples/jsm/controls/OrbitControls.js'
|
||||||
|
);
|
||||||
if (disposed) {
|
if (disposed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -22,7 +22,8 @@ export type ResourceBatchTagTarget = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ResourceBatchTagTargetResolution =
|
export type ResourceBatchTagTargetResolution =
|
||||||
{ ok: true; assetIds: string[] } | { ok: false; reason: string };
|
| { ok: true; assetIds: string[] }
|
||||||
|
| { ok: false; reason: string };
|
||||||
|
|
||||||
export function resolveResourceBatchTagTargets(
|
export function resolveResourceBatchTagTargets(
|
||||||
resources: readonly {
|
resources: readonly {
|
||||||
|
|||||||
+4
-4
@@ -127,10 +127,10 @@ function validResourceCanvasCardSize(
|
|||||||
): size is ResourceCanvasCardSize {
|
): size is ResourceCanvasCardSize {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
size &&
|
size &&
|
||||||
Number.isFinite(size.width) &&
|
Number.isFinite(size.width) &&
|
||||||
Number.isFinite(size.height) &&
|
Number.isFinite(size.height) &&
|
||||||
size.width > 0 &&
|
size.width > 0 &&
|
||||||
size.height > 0,
|
size.height > 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,14 @@ export async function loadResourceModelObject(
|
|||||||
source: ResourceModelSource,
|
source: ResourceModelSource,
|
||||||
): Promise<ThreeTypes.Object3D> {
|
): Promise<ThreeTypes.Object3D> {
|
||||||
if (isResourceModelFbx(source)) {
|
if (isResourceModelFbx(source)) {
|
||||||
const { FBXLoader } =
|
const { FBXLoader } = await import(
|
||||||
await import('three/examples/jsm/loaders/FBXLoader.js');
|
'three/examples/jsm/loaders/FBXLoader.js'
|
||||||
|
);
|
||||||
return new FBXLoader().loadAsync(source.sourceUrl);
|
return new FBXLoader().loadAsync(source.sourceUrl);
|
||||||
}
|
}
|
||||||
const { GLTFLoader } =
|
const { GLTFLoader } = await import(
|
||||||
await import('three/examples/jsm/loaders/GLTFLoader.js');
|
'three/examples/jsm/loaders/GLTFLoader.js'
|
||||||
|
);
|
||||||
const gltf = await new GLTFLoader().loadAsync(source.sourceUrl);
|
const gltf = await new GLTFLoader().loadAsync(source.sourceUrl);
|
||||||
if (!gltf.scene) {
|
if (!gltf.scene) {
|
||||||
throw new Error('模型内容为空');
|
throw new Error('模型内容为空');
|
||||||
@@ -104,7 +106,8 @@ export function disposeResourceModelObject(object: ThreeTypes.Object3D) {
|
|||||||
entry as unknown as Record<string, unknown>,
|
entry as unknown as Record<string, unknown>,
|
||||||
)) {
|
)) {
|
||||||
const texture = value as
|
const texture = value as
|
||||||
{ isTexture?: boolean; dispose?: () => void } | undefined;
|
| { isTexture?: boolean; dispose?: () => void }
|
||||||
|
| undefined;
|
||||||
if (texture?.isTexture && typeof texture.dispose === 'function') {
|
if (texture?.isTexture && typeof texture.dispose === 'function') {
|
||||||
texture.dispose();
|
texture.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -439,7 +439,9 @@ function reconcileLayout(
|
|||||||
* 做,磁盘上仍是旧栏目取值的坐标不会因为 `section` 字面量不同而漏出整理范围。
|
* 做,磁盘上仍是旧栏目取值的坐标不会因为 `section` 字面量不同而漏出整理范围。
|
||||||
*/
|
*/
|
||||||
organizeSections:
|
organizeSections:
|
||||||
readonly ProjectResourceCanvasCategory[] | null | undefined = undefined,
|
| readonly ProjectResourceCanvasCategory[]
|
||||||
|
| null
|
||||||
|
| undefined = undefined,
|
||||||
) {
|
) {
|
||||||
// `undefined` = 不是整理;`null` = 整理全部栏目;否则按集合判定。
|
// `undefined` = 不是整理;`null` = 整理全部栏目;否则按集合判定。
|
||||||
const organizedSections =
|
const organizedSections =
|
||||||
|
|||||||
@@ -43,8 +43,9 @@ describe('AGC 客户端更新', () => {
|
|||||||
it('开发态开关关闭时不请求清单', async () => {
|
it('开发态开关关闭时不请求清单', async () => {
|
||||||
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '0');
|
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '0');
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
const { checkForAppUpdate, resetAppUpdateCheckForTests } =
|
const { checkForAppUpdate, resetAppUpdateCheckForTests } = await import(
|
||||||
await import('../src/services/appUpdate');
|
'../src/services/appUpdate'
|
||||||
|
);
|
||||||
|
|
||||||
await expect(checkForAppUpdate()).resolves.toBeNull();
|
await expect(checkForAppUpdate()).resolves.toBeNull();
|
||||||
expect(checkMock).not.toHaveBeenCalled();
|
expect(checkMock).not.toHaveBeenCalled();
|
||||||
@@ -55,8 +56,9 @@ describe('AGC 客户端更新', () => {
|
|||||||
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
|
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
checkMock.mockResolvedValue(fakeUpdate() as never);
|
checkMock.mockResolvedValue(fakeUpdate() as never);
|
||||||
const { checkForAppUpdate, resetAppUpdateCheckForTests } =
|
const { checkForAppUpdate, resetAppUpdateCheckForTests } = await import(
|
||||||
await import('../src/services/appUpdate');
|
'../src/services/appUpdate'
|
||||||
|
);
|
||||||
|
|
||||||
await expect(checkForAppUpdate()).resolves.toEqual({
|
await expect(checkForAppUpdate()).resolves.toEqual({
|
||||||
version: '99.0.0',
|
version: '99.0.0',
|
||||||
@@ -72,8 +74,9 @@ describe('AGC 客户端更新', () => {
|
|||||||
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
|
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
checkMock.mockRejectedValue(new Error('updater: manifest 404'));
|
checkMock.mockRejectedValue(new Error('updater: manifest 404'));
|
||||||
const { checkForAppUpdate, resetAppUpdateCheckForTests } =
|
const { checkForAppUpdate, resetAppUpdateCheckForTests } = await import(
|
||||||
await import('../src/services/appUpdate');
|
'../src/services/appUpdate'
|
||||||
|
);
|
||||||
|
|
||||||
await expect(checkForAppUpdate()).resolves.toBeNull();
|
await expect(checkForAppUpdate()).resolves.toBeNull();
|
||||||
resetAppUpdateCheckForTests();
|
resetAppUpdateCheckForTests();
|
||||||
@@ -107,8 +110,9 @@ describe('AGC 客户端更新', () => {
|
|||||||
it('没有待安装更新时安装请求失败关闭', async () => {
|
it('没有待安装更新时安装请求失败关闭', async () => {
|
||||||
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
|
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
const { installAppUpdate, resetAppUpdateCheckForTests } =
|
const { installAppUpdate, resetAppUpdateCheckForTests } = await import(
|
||||||
await import('../src/services/appUpdate');
|
'../src/services/appUpdate'
|
||||||
|
);
|
||||||
|
|
||||||
await expect(installAppUpdate()).rejects.toThrow('没有可安装的更新');
|
await expect(installAppUpdate()).rejects.toThrow('没有可安装的更新');
|
||||||
resetAppUpdateCheckForTests();
|
resetAppUpdateCheckForTests();
|
||||||
|
|||||||
@@ -703,11 +703,11 @@ describe('快速编辑浮层的下拉弹层', () => {
|
|||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
|
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
|
||||||
);
|
);
|
||||||
expect(screen.getByRole('button', { name: 'gpt-image-2' })).toBeTruthy();
|
expect(screen.getByRole('button', { name: 'GPT Image 2.5' })).toBeTruthy();
|
||||||
|
|
||||||
// 点面板内、弹层外的提示词区域:只收弹层,不动面板本身。
|
// 点面板内、弹层外的提示词区域:只收弹层,不动面板本身。
|
||||||
fireEvent.click(screen.getByLabelText('快速编辑提示词'));
|
fireEvent.click(screen.getByLabelText('快速编辑提示词'));
|
||||||
expect(screen.queryByRole('button', { name: 'gpt-image-2' })).toBeNull();
|
expect(screen.queryByRole('button', { name: 'GPT Image 2.5' })).toBeNull();
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
|
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
@@ -739,10 +739,10 @@ describe('快速编辑浮层的下拉弹层', () => {
|
|||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
|
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
|
||||||
);
|
);
|
||||||
expect(screen.getByRole('button', { name: 'gpt-image-2' })).toBeTruthy();
|
expect(screen.getByRole('button', { name: 'GPT Image 2.5' })).toBeTruthy();
|
||||||
|
|
||||||
fireEvent.keyDown(document, { key: 'Escape' });
|
fireEvent.keyDown(document, { key: 'Escape' });
|
||||||
|
|
||||||
expect(screen.queryByRole('button', { name: 'gpt-image-2' })).toBeNull();
|
expect(screen.queryByRole('button', { name: 'GPT Image 2.5' })).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
|
|
||||||
const homeProjectOverride = vi.hoisted(() => ({
|
const homeProjectOverride = vi.hoisted(() => ({
|
||||||
openProject: null as
|
openProject: null as
|
||||||
null | ((path: string, mode: 'open' | 'create') => Promise<void>),
|
| null
|
||||||
|
| ((path: string, mode: 'open' | 'create') => Promise<void>),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../src/features/app-shell/useHomeProjectCreation', async () => {
|
vi.mock('../src/features/app-shell/useHomeProjectCreation', async () => {
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
- [画板音乐生成入口](./【编辑器】画板音乐生成入口设计-2026-06-18.md):BGM/SFX 共享视图、独立业务规则和当前发布门禁。
|
- [画板音乐生成入口](./【编辑器】画板音乐生成入口设计-2026-06-18.md):BGM/SFX 共享视图、独立业务规则和当前发布门禁。
|
||||||
- [画布 Agent 对话面板](./【编辑器】画布Agent对话面板-2026-07-03.md)
|
- [画布 Agent 对话面板](./【编辑器】画布Agent对话面板-2026-07-03.md)
|
||||||
- [画布 Agent 会话消息存 OSS](./adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md)
|
- [画布 Agent 会话消息存 OSS](./adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md)
|
||||||
|
- [GPT Image 2.5 模型路由与历史值兼容](./adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md)
|
||||||
- [编辑器模型定价配置](./【编辑器】模型定价配置管理方案-2026-06-22.md)
|
- [编辑器模型定价配置](./【编辑器】模型定价配置管理方案-2026-06-22.md)
|
||||||
|
|
||||||
## 后端、运维与测试
|
## 后端、运维与测试
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# GPT Image 2.5 模型路由与历史值兼容
|
||||||
|
|
||||||
|
状态:accepted
|
||||||
|
|
||||||
|
新任务使用业务模型值 `gpt-image-2.5`,api-server 按任务显式选择具体 model:无参考图的生成使用 `gpt-image-2.5-flare-c`,编辑以及带参考图的生成使用 `gpt-image-2.5-sunburst-c`(参考图会让执行端必须走 edits multipart,因此执行与计价都按编辑 concrete model)。这两个 GPT Image 2.5 model 必须通过启动时构造的 Tiantoken client 发送;Tiantoken client 只读取显式配置的 `TIANTOKEN_BASE_URL`(部署值由环境设置为 `https://api.tiantoken.com`)和独立 `TIANTOKEN_API_KEY`,缺失即阻止 api-server 启动,不得回退到 VectorEngine 或其 API key。
|
||||||
|
|
||||||
|
图片协议执行逻辑保持 provider-neutral:请求 body、multipart、尺寸约束、重试、响应解码和审计由共享 image executor 承担;VectorEngine 与 Tiantoken client 只提供相同协议所需的 base URL、API key 和 provider identity。platform-image 根据 concrete model 做严格白名单路由:`gpt-image-2.5-flare-c` 与 `gpt-image-2.5-sunburst-c` 走 Tiantoken,`gemini-3.1-flash-image-preview`(nanobanana)走 VectorEngine;未知 model 直接拒绝。已持久化的 `gpt-image-2` 只在新任务提交边界按兼容规则解析为当前 GPT Image 2.5 任务,不改写历史资源,也不进入旧 VectorEngine 图片路由。
|
||||||
|
|
||||||
|
provider 白名单同时拒绝非 provider model:业务模型名 `gpt-image-2.5` 必须先由 api-server 在任务边界解析成 `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c`;`gpt-image-2` 继续作为历史可读值接受。两者都不进入 provider 路由。
|
||||||
|
|
||||||
|
`gpt-image-2-c` 已整体删除:它自兜底移除后只剩历史审计字符串,从未进入业务模型、持久化模型字段或前端契约,因此不再保留任何常量、解析分支、尺寸语义或 provider 路由。数据库里既有的审计字符串保持原样,不参与新任务。
|
||||||
|
|
||||||
|
普通主站前端只接触业务模型和新生成展示名 `GPT Image 2.5`,admin Web/API 可以查看和编辑两个具体定价 key;生成类任务带参考图时后端必须走 edits multipart,因此 dispatch、审计与计价统一按编辑 concrete model `gpt-image-2.5-sunburst-c`(包含图标素材图集的固定参考图与 UI 素材提取的来源图),不存在「生成档计价 + 编辑档执行」的分裂。新代码不再跨模型或跨 provider fallback,已退役的 `gpt-image-2-c` 审计记录原样保留在数据库,代码不再引用该值。
|
||||||
|
|
||||||
|
主站前端只把原本明确使用 GPT Image 2 的专用新任务改为业务模型值 `gpt-image-2.5`;普通图片、角色、场景、图标等原有 nanobanana 默认行为保持不变。画布参数或编辑布局读到 `gpt-image-2` 时,在使用端解析为 `gpt-image-2.5` 并触发参数迁移警告,原始资源、审计、metadata 和历史 fixture 不回写。AGC 现有资源生成界面与请求默认保持不变,不因本决策新增模型字段、选择器或尺寸行为。
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- 定价配置的活动 key 是两个具体 provider model;旧单 key 配置只允许受控 backfill,并留下兼容 TODO。
|
||||||
|
- 定价档位跟随任务最终 concrete model:带参考图的生成任务与编辑任务同价,admin 单独调整 `gpt-image-2.5-sunburst-c` 时生成带参考图的任务必须同步生效。
|
||||||
|
- 新任务的同模型重试固定使用 api-server dispatch 的具体 model,不切换到另一个 model。
|
||||||
|
- 两套 provider client 在 api-server 启动阶段同时构造;任一 required provider 配置缺失,启动失败而不是延迟到首次图片请求。
|
||||||
|
- 两套 provider client 的缺失配置报错点名对应环境变量(`VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY`、`TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY`),构造成功后用启动日志记录已启用的 provider。
|
||||||
|
- provider routing 只依据 concrete model 的白名单,业务名与已退役的 `gpt-image-2-c` 一律拒绝(`gpt-image-2-c` 已从代码中整体删除,传入即按未知 model 处理);provider client 不复制共享协议执行逻辑。
|
||||||
|
- 公开资源、`generationInputs` 和普通前端契约不包含具体 provider key;admin 定价管理是明确例外。
|
||||||
@@ -3017,7 +3017,7 @@
|
|||||||
},
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "支持 gpt-image-2、gemini-3.1-flash-image-preview、nanobanana2、nano-banana。未传时沿用编辑器默认。"
|
"description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-banana;gpt-image-2 仅作为历史值在新任务提交边界兼容解析;已退役的 gpt-image-2-c 不再接受,命中按不支持的值处理。kind=ui-design 与 kind=publication-material 固定使用 gpt-image-2.5,传入的 model 会被忽略;其余 kind 未传时默认 gpt-image-2.5。"
|
||||||
},
|
},
|
||||||
"aspectRatio": {
|
"aspectRatio": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -3033,7 +3033,7 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS 再提交。禁止 Data URL / Blob URL。普通生成最多 5 张;kind=quick-edit 时 gpt-image-2 最多 5 张、nanobanana2 最多 9 张。超限返回 400,不会静默截断。"
|
"description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS 再提交。禁止 Data URL / Blob URL。普通生成最多 5 张;kind=quick-edit 时 GPT Image 2.5 最多 5 张、nanobanana2 最多 9 张。超限返回 400,不会静默截断。"
|
||||||
},
|
},
|
||||||
"maxItems": 9
|
"maxItems": 9
|
||||||
},
|
},
|
||||||
@@ -3170,7 +3170,7 @@
|
|||||||
},
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "支持 gpt-image-2、gemini-3.1-flash-image-preview、nanobanana2、nano-banana。"
|
"description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-banana;gpt-image-2 仅作为历史值兼容解析;已退役的 gpt-image-2-c 不再接受,命中按不支持的值处理。"
|
||||||
},
|
},
|
||||||
"aspectRatio": {
|
"aspectRatio": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -3184,7 +3184,7 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceReferenceId 对应的主来源原图占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。"
|
"description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceReferenceId 对应的主来源原图占用 1 张 provider 容量,因此 GPT Image 2.5 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。"
|
||||||
},
|
},
|
||||||
"maxItems": 8
|
"maxItems": 8
|
||||||
},
|
},
|
||||||
@@ -3373,7 +3373,7 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "额外图标素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。referenceId 占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。"
|
"description": "额外图标素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。referenceId 占用 1 张 provider 容量,因此 GPT Image 2.5 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。"
|
||||||
},
|
},
|
||||||
"maxItems": 8
|
"maxItems": 8
|
||||||
},
|
},
|
||||||
@@ -3488,13 +3488,13 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "gemini-3.1-flash-image-preview",
|
"default": "gemini-3.1-flash-image-preview",
|
||||||
"description": "支持 gpt-image-2、gemini-3.1-flash-image-preview、nanobanana2、nano-banana。未传时默认使用 nanobanana。"
|
"description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-banana;gpt-image-2 仅作为历史值兼容解析;已退役的 gpt-image-2-c 不再接受,命中按不支持的值处理。未传时默认使用 nanobanana(gemini-3.1-flash-image-preview);传 gpt-image-2.5 时按 GPT Image 2.5 编辑任务执行。"
|
||||||
},
|
},
|
||||||
"referenceImageSrcs": {
|
"referenceImageSrcs": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "额外 UI 素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceImageSrc 占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 5 张;超限返回 400,不会静默截断。"
|
"description": "额外 UI 素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceImageSrc 占用 1 张 provider 容量,因此 GPT Image 2.5 最多再提交 4 张、nanobanana2 最多再提交 5 张;超限返回 400,不会静默截断。"
|
||||||
},
|
},
|
||||||
"maxItems": 5
|
"maxItems": 5
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# GPT Image 2.5 provider 边界重构实施计划
|
||||||
|
|
||||||
|
- Version: 3
|
||||||
|
- Status: active
|
||||||
|
- Date: 2026-09-18
|
||||||
|
- Parent Milestone: `docs/project-memory/plans/【里程碑】GPT Image 2.5 provider边界重构-2026-09-18.md`
|
||||||
|
|
||||||
|
## 实施边界
|
||||||
|
|
||||||
|
1. 先抽象 provider-neutral settings/client 与共享图片执行器接口;保留一套 body、multipart、尺寸、retry、响应和 audit 逻辑。
|
||||||
|
2. 在 api-server 配置/state 初始化阶段分别构造 VectorEngine 与 Tiantoken client;删除 Tiantoken 对 VectorEngine URL/key 的任何 fallback。
|
||||||
|
3. 在 platform-image 建立 concrete model 白名单路由:GPT Image 2.5 → Tiantoken,nanobanana → VectorEngine;legacy/unknown 按合同处理。
|
||||||
|
4. 重命名 provider-specific client/build/transport 符号,避免共享逻辑继续伪装成 `vector_engine_*`;仅保留确有 VectorEngine 语义的名称。
|
||||||
|
5. 迁移 api-server、Agent、raw edit、角色/图标/UI 入口和测试;核对 pricing/admin/public DTO 可见性。
|
||||||
|
6. 删除跨模型/跨 provider fallback 分支,保留同 concrete model retry。
|
||||||
|
|
||||||
|
## 前端同步边界(已确认)
|
||||||
|
|
||||||
|
1. 主站 `src/components/image-editor/` 中,凡是原本明确写死 `gpt-image-2` 的 GPT 专用新任务(快速编辑、UI 设计、宣发、规范及对应提交/锁定模型)统一改为业务模型值 `gpt-image-2.5`;普通图片、角色、场景、图标等原有 nanobanana 默认行为不变。
|
||||||
|
2. `normalizeEditorImageModel` 在编辑面板、画布生成参数和历史布局恢复的使用端把 `gpt-image-2` 与 `gpt-image-2-c` 解析为 `gpt-image-2.5`。该兼容命中必须触发既有参数回退警告;未知模型仍按原有无效参数处理。原始资源、审计、metadata 和历史 fixture 不回写。
|
||||||
|
3. 主站新请求的 `model` 字段只发送业务模型值或 nanobanana 业务值,不发送 `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c` 等 concrete provider model。
|
||||||
|
4. AGC(`apps/ai-game-creator-shell/src` 及其 Tauri 本地资源生成链路)本里程碑不增加模型字段、选择器或尺寸行为;继续使用现有请求默认值。AGC 代码只在确属历史兼容读取/测试证明的位置保留旧值,不借本次同步引入新功能。
|
||||||
|
5. OpenAPI、External editor skill reference、现役脚本和当前技术方案中的“新任务使用 GPT Image 2 / 2-c fallback”改为 GPT Image 2.5 口径;静态已生成 manifest、历史审计和兼容 fixture 保持旧值并补充历史语义断言。
|
||||||
|
|
||||||
|
## 前端实现顺序
|
||||||
|
|
||||||
|
1. 先在 `ImageCanvasGenerationModel` 集中定义历史别名解析与迁移警告结果,保持 nanobanana 默认值、尺寸矩阵和可选模型顺序不变。
|
||||||
|
2. 迁移 `ImageCanvasGenerationDialogModel`、`ImageCanvasGenerationSubmissionModel`、生成工作流、快速编辑弹窗、规范/宣发面板的 GPT 专用默认、锁定值和提交 payload。
|
||||||
|
3. 为 `generationInputs` / 画布布局 / 资源历史恢复补齐旧值迁移警告测试;确认新请求字段为 `gpt-image-2.5`,而历史原文仍未被改写。
|
||||||
|
4. 更新 OpenAPI 与现役工具/专题文档,区分业务模型、concrete provider model、历史值和产品展示名;不得把 AGC 默认行为改成前端显式模型选择。
|
||||||
|
|
||||||
|
## 验证命令
|
||||||
|
|
||||||
|
- `cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check`
|
||||||
|
- `cargo test -p platform-image`
|
||||||
|
- `cargo test -p platform-editor-agent`
|
||||||
|
- api-server 定向测试/`cargo check -p api-server`
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run check:doc-index`
|
||||||
|
- `npm run check:encoding`
|
||||||
|
- `git diff --check`
|
||||||
|
- 主站图片编辑定向测试:模型选项/展示名、GPT 专用提交值、历史 `gpt-image-2` 与 `gpt-image-2-c` 恢复及警告、nanobanana 默认回归。
|
||||||
|
- AGC 定向测试:确认未新增 `model` IPC 入参、未改变原有尺寸/UI 行为,现有资源生成请求继续依赖服务端默认。
|
||||||
|
- OpenAPI/External editor 契约测试:模型说明、参考图容量说明和新业务模型值一致,provider concrete model 不进入普通前端契约。
|
||||||
|
|
||||||
|
## 风险与回滚
|
||||||
|
|
||||||
|
- 风险:启动阶段依赖变化、历史任务兼容解析遗漏、nanobanana 被误路由到 Tiantoken、provider key 泄露到公开 DTO。
|
||||||
|
- 风险:主站把 nanobanana 的默认路径误改成 GPT Image 2.5,或把历史兼容值静默吞掉导致用户无法识别参数迁移。
|
||||||
|
- 回滚:以 provider-neutral seam、启动配置、model route、主站前端同步四个局部提交边界回滚;不执行数据库历史迁移,也不回滚历史资源值。
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# GPT Image 2.5 provider 边界重构
|
||||||
|
|
||||||
|
- Version: 2
|
||||||
|
- Status: active
|
||||||
|
- Date: 2026-09-18
|
||||||
|
- Parent Spec: `docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在保持图片协议执行逻辑共享的前提下,建立明确的 provider client 边界:GPT Image 2.5 通过 Tiantoken,nanobanana 通过 VectorEngine;路由依据 concrete model 严格白名单决定;两个 client 在 api-server 启动阶段构造。
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
- `platform-image`:provider-neutral 图片执行器、provider client 注入 seam、concrete model 路由和错误/审计 provider 标识。
|
||||||
|
- `api-server`:启动时构造 VectorEngine/Tiantoken 两个 client,分别读取各自环境变量;任务提交边界的历史模型兼容解析。
|
||||||
|
- 共享请求、multipart、尺寸、retry、响应和 audit 逻辑保持单一实现。
|
||||||
|
- Agent 与其它 server-side 图片调用方迁移到业务模型/concrete model 合同。
|
||||||
|
- 定价、公开 DTO、admin DTO 与 provider model 可见性保持既定 ADR 约束。
|
||||||
|
|
||||||
|
## 现役路由
|
||||||
|
|
||||||
|
| Concrete model | Provider client | 业务用途 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `gpt-image-2.5-flare-c` | Tiantoken | Generate,仅限不带参考图的生成任务 |
|
||||||
|
| `gpt-image-2.5-sunburst-c` | Tiantoken | Edit,包括快速编辑、原位修改、raw edit,以及带参考图的生成任务(图标素材图集的固定参考图、UI 素材提取的来源图同在内) |
|
||||||
|
| `gemini-3.1-flash-image-preview` | VectorEngine | nanobanana 生成/编辑能力 |
|
||||||
|
|
||||||
|
`gpt-image-2` 与 `gpt-image-2-c` 只保留为历史持久化/审计字符串;新任务不得 dispatch 到旧 GPT Image 2 路由。未知 model 直接拒绝。
|
||||||
|
|
||||||
|
## 必须成立的行为
|
||||||
|
|
||||||
|
1. api-server 启动时同时构造两个 required provider client;任一对应环境变量缺失,启动失败。
|
||||||
|
2. Tiantoken 只读取 `TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY`;VectorEngine 只读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY`,互不回退。
|
||||||
|
3. platform-image 根据 concrete model 选择已注入 client;共享执行器不复制 provider 协议逻辑。
|
||||||
|
4. 同 concrete model 可以 retry,但永不跨 concrete model 或跨 provider fallback。
|
||||||
|
5. 历史值读取不改写;新任务提交边界将旧值兼容为 GPT Image 2.5 业务任务。
|
||||||
|
6. 普通前端不接收 concrete provider model;admin 定价界面可查看和编辑两个具体 pricing key。
|
||||||
|
7. 生成任务的 concrete model 由参考图决定:同一判据同时用于 dispatch、审计与计价,带参考图的生成任务按编辑档计价。
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
- 不复制两套完整图片 client。
|
||||||
|
- 不新增 GPT Image 2 现役 VectorEngine 路由。
|
||||||
|
- 不修改历史数据库记录或旧审计字符串。
|
||||||
|
- 不把 provider client 选择下沉给普通前端。
|
||||||
|
|
||||||
|
## 验收证据
|
||||||
|
|
||||||
|
- provider routing 单元测试覆盖 flare/sunburst/nanobanana/legacy/unknown。
|
||||||
|
- 启动配置测试证明两套 client 独立读取环境变量,缺失任一配置即失败且无 VectorEngine/Tiantoken 回退。
|
||||||
|
- 请求审计测试证明 provider 与 concrete model 正确记录。
|
||||||
|
- platform-image 与 Agent 定向测试通过。
|
||||||
|
- api-server 类型/编译检查、前端类型检查、编码/文档/diff 门禁通过。
|
||||||
@@ -1,5 +1,28 @@
|
|||||||
# 决策记录
|
# 决策记录
|
||||||
|
|
||||||
|
## 2026-09-21 生成带参考图统一按编辑 concrete model 执行与计价
|
||||||
|
|
||||||
|
- 背景:GPT Image 2.5 迁移后,生成端在带参考图时会改走 `/v1/images/edits` 并提交 `gpt-image-2.5-sunburst-c`,但计价仍按 `gpt-image-2.5-flare-c` 生成档,dispatch 判断也分散在三个入口各自的 `if 带参考图` 表达式里;结果是「生成档扣费 + 编辑档执行/审计」分裂,admin 单独调价任一档位都会错价,ADR 里「普通生成即使因参考图使用 edits multipart,仍按生成 concrete model」与代码事实不符。
|
||||||
|
- 决策:保留「按是否带参考图决定 concrete model」这一现有行为,把三处文档改到与代码一致;同时新增 `editor_image_generation_concrete_model(model, has_reference_images)` 作为生成类任务选择 concrete model 的唯一入口,dispatch 与计价共用同一判据。`image_generation_mud_points` 增加 `has_reference_images`,命中编辑 concrete model 时改走编辑档 `image_edit_model_mud_points`;图标素材图集(固定参考图)、UI 素材提取(来源图)、图标规范带 `referenceId`、Agent 生成类工具带 `referenceImageIds` 全部同规则。`EditImageTool` 计价从生成档改为编辑档,与直连快速编辑、raw edit 对齐。
|
||||||
|
- 边界:nanobanana 生成与编辑共用一个 concrete model,参考图不改变档位;不引入跨模型 fallback;历史持久化值与审计字符串不改写。
|
||||||
|
- 同批修复:`raw_image_edit` 发送前复用共享 `ensure_provider_matches_model` 白名单校验;`platform-image` 兜底 task id 前缀改为跟随实际 provider(`tiantoken-edit` / `tiantoken-nanobanana` 不再带 `vector-engine` 前缀);`resolve_image_provider` 增加 `gpt-image-2 → Tiantoken` 断言把历史值意图钉死;删除 `openai_image_generation.rs` 中仅测试使用的 `create_openai_image_edit` / `create_openai_image_edit_with_references`(测试改用 `..._and_model` 变体,保留空参考图本地校验覆盖);`normalize_editor_generation_options` 的具体 provider key 改用常量;External v1 OpenAPI 的 `model` 描述改为与实现一致(`kind=ui-design` / `publication-material` 固定 `gpt-image-2.5`,UI 素材提取未传默认 nanobanana)。
|
||||||
|
- 影响范围:`api-server`(`editor_generation_config`、`editor_project`、`editor_project_icon`、`character_visual_assets`、`editor_agent/tool`、`openai_image_generation`)、`platform-image`(`raw_image_edit`、`runtime/executor`、集成测试)、ADR、里程碑文档、External v1 OpenAPI 描述、`.codex/skills/gpt-image-2-apimart` 脚本与 SKILL.md(改用 `TIANTOKEN_*` 凭据 + concrete model)。
|
||||||
|
- 验证:`cargo test -p api-server editor_generation_config`、`cargo test -p api-server editor_agent::tool`、`cargo test -p api-server`(1123 passed / 1 failed,失败项为 mock LLM 连接超时,单测隔离重跑通过)。
|
||||||
|
- 关联文档:[`docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`](../../adr/【ADR】GPT%20Image%202.5模型路由与历史值兼容-2026-09-18.md)、[`docs/project-memory/plans/【里程碑】GPT Image 2.5 provider边界重构-2026-09-18.md`](../plans/【里程碑】GPT%20Image%202.5%20provider边界重构-2026-09-18.md)。
|
||||||
|
|
||||||
|
## 2026-09-21 图片 provider 白名单收紧,并整体删除 gpt-image-2-c
|
||||||
|
|
||||||
|
- 背景:GPT Image 2.5 迁移后,`resolve_image_provider` 仍把业务模型名 `gpt-image-2.5` 与 `gpt-image-2-c` 一起判为 Tiantoken,等于把「非 provider model」留在 provider 边界白名单里。`gpt-image-2-c` 自 2026-07-21 起只是首选模型失败时的兜底 provider model(兜底移除后仅剩历史审计字符串),从未进入业务模型、持久化 `model` 字段或前端契约——api-server 持久化的模型一律取自 `generation_options.model`(业务值),前端在 2.5 迁移前只写 `gpt-image-2`。
|
||||||
|
- 决策:`resolve_image_provider` 白名单只接受现役具体 provider model,业务名 `gpt-image-2.5` 命中即拒绝,必须先在任务边界解析成具体 key;`gpt-image-2` 继续作为历史可读值接受。`gpt-image-2-c` 不再只做「拒绝」,而是整体删除:常量、re-export、`is_gpt_image_2_family_model` 尺寸语义、`auditable_image_model` 审计标签、api-server 的三处提交边界解析、前端常量与历史恢复分支全部移除;数据库既有审计字符串原样保留,代码不再引用该值。
|
||||||
|
- 影响范围:`platform-image`(constants / lib / image_provider mod / protocol request / runtime executor / 集成测试)、`api-server`(`openai_image_generation`、`editor_project`、`editor_generation_config`、`editor_agent/tool`)、主站前端 `ImageCanvasGenerationModel` 与其测试、External v1 OpenAPI 描述、ADR 与本文档。前端与 External v1 传入 `gpt-image-2-c` 现在按「不支持的值」处理(走既有未知值分支),不再解析为 GPT Image 2.5。
|
||||||
|
- 验证:`cargo test -p platform-image` 全绿(含新增 `resolve_image_provider_rejects_business_model_and_accepts_concrete_models`);`cargo test -p api-server` 1115 项;`npx vitest run src/components/image-editor/ImageCanvasGenerationModel.test.ts` 34 项通过;`check:encoding`、`check:doc-index` 通过。
|
||||||
|
|
||||||
|
## 2026-09-21 图片 provider 启动期缺配置的报错点名环境变量
|
||||||
|
|
||||||
|
- 背景:两个图片 provider client 都在 `AppState::new` 构造,缺 base URL / API key 即阻止 api-server 启动;旧报错只写「tiantoken 图片 provider 缺少 BASE_URL 配置」,没有点名变量名,而 `TIANTOKEN_*` 已不再从 `VECTOR_ENGINE_*` 回退,运维迁移时不易定位。
|
||||||
|
- 决策:保持「不回退、缺失即启动失败」,但报错文案补上具体变量名(`VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 或 `TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY`);client 构造成功后用 `info` 记录 provider、base_url、超时与对应 env 变量名,便于确认实际生效的 provider 配置。
|
||||||
|
- 验证:`cargo check -p api-server` 通过;`cargo test -p api-server` 1115 项通过(0 失败)。
|
||||||
|
|
||||||
## 2026-09-21 渠道进安装身份:不同渠道的 AGC 包体在同一台设备并存
|
## 2026-09-21 渠道进安装身份:不同渠道的 AGC 包体在同一台设备并存
|
||||||
|
|
||||||
- 背景:渠道此前只决定更新端点(`plugins.updater.endpoints`)与渲染层平台 origin,`productName` / `identifier` 与渠道无关,于是所有渠道共用 `%LOCALAPPDATA%\陶泥儿` 安装目录、同一个卸载项(`HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\陶泥儿`,另有 `HKCU\Software\genarrative\陶泥儿`)以及同一份 `%APPDATA%\world.genarrative.ai-game-creator` 数据目录。本机 0.1.48 安装实测:主程序二进制里只有 1 处 `agc/dev-win/latest.json`、0 处 release 端点,说明渠道在产物里只体现为端点。后果是后装的渠道静默顶掉先装的渠道,并接管更新端点、平台服务器与本地登录态/项目数据。
|
- 背景:渠道此前只决定更新端点(`plugins.updater.endpoints`)与渲染层平台 origin,`productName` / `identifier` 与渠道无关,于是所有渠道共用 `%LOCALAPPDATA%\陶泥儿` 安装目录、同一个卸载项(`HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\陶泥儿`,另有 `HKCU\Software\genarrative\陶泥儿`)以及同一份 `%APPDATA%\world.genarrative.ai-game-creator` 数据目录。本机 0.1.48 安装实测:主程序二进制里只有 1 处 `agc/dev-win/latest.json`、0 处 release 端点,说明渠道在产物里只体现为端点。后果是后装的渠道静默顶掉先装的渠道,并接管更新端点、平台服务器与本地登录态/项目数据。
|
||||||
@@ -9146,6 +9169,31 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
|||||||
- 验证:限速后 `Genarrative-Full-Build-And-Deploy` #289 / #290 SUCCESS;采样期 Jenkins 峰值 10.2~10.5 核、限流不足 2s(可忽略),runner 峰值 12.07 核且持续出现 throttling,整机回落到 2.6%~19.8%。
|
- 验证:限速后 `Genarrative-Full-Build-And-Deploy` #289 / #290 SUCCESS;采样期 Jenkins 峰值 10.2~10.5 核、限流不足 2s(可忽略),runner 峰值 12.07 核且持续出现 throttling,整机回落到 2.6%~19.8%。
|
||||||
- 关联文档:[开发运维](../../【开发运维】本地开发验证与生产运维-2026-05-15.md)。
|
- 关联文档:[开发运维](../../【开发运维】本地开发验证与生产运维-2026-05-15.md)。
|
||||||
|
|
||||||
|
## 2026-09-18 GPT Image 2.5 业务模型与具体 provider 定价路由
|
||||||
|
|
||||||
|
- **决策**:新任务使用业务模型值 `gpt-image-2.5`;api-server 按任务显式 dispatch 具体模型 `gpt-image-2.5-flare-c`(无参考图的生成)或 `gpt-image-2.5-sunburst-c`(编辑,以及带参考图的生成),并把同一具体 key 交给 `platform-image` 与后台定价解析。带参考图的生成任务因为必须走 edits multipart,dispatch、审计与计价统一按编辑 concrete model;同模型重试不跨模型 fallback。
|
||||||
|
- **历史兼容**:已持久化 `gpt-image-2` 读回原值不改写;基于旧资源发起新任务时,在提交边界解析为 `gpt-image-2.5`,新任务/新产物按新业务值和当前 task price 处理。旧 `gpt-image-2-c` 仅保留历史审计,不再作为 fallback 或业务模型。
|
||||||
|
- **可见性**:普通主站前端和公开定价 API 不接收具体 provider key;新生成 UI label 为 `GPT Image 2.5`,历史资源/旧编辑上下文不扩散该 label。admin Web/API 是明确例外,可查看和编辑两个具体定价 key。旧单 key 定价配置允许受控 backfill,并加 compatibility TODO。
|
||||||
|
- **关联 ADR**:[`docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`](../../adr/【ADR】GPT%20Image%202.5模型路由与历史值兼容-2026-09-18.md)。
|
||||||
|
|
||||||
|
- **补充**:GPT Image 2.5 的两个具体模型通过显式 `TIANTOKEN_BASE_URL` 与独立 `TIANTOKEN_API_KEY` 发送;环境变量缺失时必须失败,禁止使用 VectorEngine 配置或 API key 回退。
|
||||||
|
|
||||||
|
## 2026-09-18 GPT Image 2.5 provider-neutral 执行器与双 client 启动边界
|
||||||
|
|
||||||
|
- **决策**:图片协议执行逻辑保持单一共享实现;只抽出 provider client 的 identity、base URL、API key 和 client 构造,通过依赖注入复用请求 body、multipart、尺寸、retry、响应和 audit。
|
||||||
|
- **路由**:`gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c` 走 Tiantoken;`gemini-3.1-flash-image-preview`(nanobanana)走 VectorEngine;`gpt-image-2` / `gpt-image-2-c` 仅是历史字符串,新任务不再进入旧 GPT Image 2 路由;未知 model 拒绝。
|
||||||
|
- **启动**:api-server 启动时同时构造 VectorEngine 与 Tiantoken 两个 required client;各自只读取自己的环境变量,任一配置缺失即启动失败,不延迟到首次请求。
|
||||||
|
- **重试**:只在同一个 concrete model 内 retry,禁止跨 model、跨 provider fallback。
|
||||||
|
- **关联文档**:[`docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`](../../adr/【ADR】GPT%20Image%202.5模型路由与历史值兼容-2026-09-18.md)、[`docs/project-memory/plans/【里程碑】GPT Image 2.5 provider边界重构-2026-09-18.md`](../plans/【里程碑】GPT%20Image%202.5%20provider边界重构-2026-09-18.md)。
|
||||||
|
|
||||||
|
## 2026-09-19 Tiantoken 凭据落到 AppConfig 字段并修复编辑器 LLM 测试夹具
|
||||||
|
|
||||||
|
- **决策**:Tiantoken 凭据仍然只从 `TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY` 读取,不回退 VectorEngine;读取点从 `AppState::new` 前移到 `AppConfig::from_env()`,落到 `AppConfig.tiantoken_base_url` / `AppConfig.tiantoken_api_key` 两个字段,与其它 provider 的配置形态一致,`AppState` 构造与测试构造复用同一条路径;删除 `config::tiantoken_base_url()` / `config::tiantoken_api_key()` 两个现读环境变量的访问器。
|
||||||
|
- **原因**:2026-09-18 的「双 provider 启动边界」改成现读环境变量后,`editor_background_music_prompt_assist`、`editor_sound_effect_prompt_assist`、`vector_engine_audio_generation/sound_effect_translation` 三处 mock LLM 夹具仍用 `AppConfig.vector_engine_*` 构造状态,28 条用例拿到 503 `editor_llm_unavailable`。改用进程环境变量做夹具会让并行用例互相踩 `TIANTOKEN_*`,因此把凭据落到 config 字段而不是在测试里 set_var。
|
||||||
|
- **边界**:ADR 的「不得回退 VectorEngine 或其 API key」仍然成立,`from_env` 只读 `TIANTOKEN_*`,并有用例固定该行为。
|
||||||
|
- **验证**:`cargo test -p api-server` 1060 passed / 0 failed / 6 ignored;`cargo fmt --all --check`、`npm run check:encoding`、`git diff --check` 通过。
|
||||||
|
- **关联文档**:[`docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`](../../adr/【ADR】GPT%20Image%202.5模型路由与历史值兼容-2026-09-18.md)、[`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`](../../【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md)。
|
||||||
|
|
||||||
## 2026-09-17 AGC 抠图接入本地资源编辑恢复闭环
|
## 2026-09-17 AGC 抠图接入本地资源编辑恢复闭环
|
||||||
|
|
||||||
- 背景:`agc_remove_background` 原先只提交 `/api/editor/images/background-removals` 并返回 `queued`,没有轮询远端任务、下载完成媒体或写入本地 manifest;BgFilter 已成功处理但 Agent 因此永远只能看到受理回执。
|
- 背景:`agc_remove_background` 原先只提交 `/api/editor/images/background-removals` 并返回 `queued`,没有轮询远端任务、下载完成媒体或写入本地 manifest;BgFilter 已成功处理但 Agent 因此永远只能看到受理回执。
|
||||||
|
|||||||
@@ -2503,7 +2503,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
|||||||
|
|
||||||
- 现象:配置了 `APIMART_BASE_URL` / `APIMART_API_KEY` 后,RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现 `official_fallback` / `image_urls`。
|
- 现象:配置了 `APIMART_BASE_URL` / `APIMART_API_KEY` 后,RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现 `official_fallback` / `image_urls`。
|
||||||
- 原因:2026-05-21 后 GPT-image-2 图片生成按 VectorEngine 创建/编辑接口分流;2026-07-05 后创意 Agent 文本链路也改为 VectorEngine Chat Completions `gpt-5.4-mini`,APIMart 不再作为当前创意 Agent 来源。
|
- 原因:2026-05-21 后 GPT-image-2 图片生成按 VectorEngine 创建/编辑接口分流;2026-07-05 后创意 Agent 文本链路也改为 VectorEngine Chat Completions `gpt-5.4-mini`,APIMart 不再作为当前创意 Agent 来源。
|
||||||
- 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai`、`VECTOR_ENGINE_API_KEY`、`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认无参考图路径为 `/v1/images/generations`、有参考图路径为 `/v1/images/edits`,业务 / 计费与 provider 首发模型均为 `gpt-image-2`,仅在符合条件的 provider 失败后切到兜底模型 `gpt-image-2-c`。
|
- 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai`、`VECTOR_ENGINE_API_KEY`、`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认无参考图路径为 `/v1/images/generations`、有参考图路径为 `/v1/images/edits`,新任务业务模型为 `gpt-image-2.5`,provider 具体模型为 `gpt-image-2.5-flare-c`(不带参考图的生成)/ `gpt-image-2.5-sunburst-c`(编辑与带参考图的生成),带参考图的生成按编辑档计价,只在同一具体模型内重试。历史兜底模型 `gpt-image-2-c` 已从代码整体删除,不再存在任何 fallback 路径,请求体里出现该值即视为异常。
|
||||||
- 验证:运行 `cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml` 和相关玩法图片生成测试;真实联调只在本地私密环境放置 VectorEngine key。
|
- 验证:运行 `cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml` 和相关玩法图片生成测试;真实联调只在本地私密环境放置 VectorEngine key。
|
||||||
- 关联:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`server-rs/crates/api-server/src/openai_image_generation.rs`。
|
- 关联:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`server-rs/crates/api-server/src/openai_image_generation.rs`。
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user