This commit is contained in:
2026-05-09 18:24:08 +08:00
parent a0ed128bde
commit bc704d0c22
38 changed files with 481 additions and 378 deletions

View File

@@ -1,11 +1,11 @@
---
name: gpt-image-2-apimart
description: Generate or inspect project image assets through this repository's APIMart OpenAI-compatible gpt-image-2 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs gpt-image-2 request body, dry-run image prompts, batch-generate local project thumbnails, or debug APIMART_BASE_URL / APIMART_API_KEY image-generation configuration without exposing secrets.
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs gpt-image-2 request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
---
# gpt-image-2 APIMart
# gpt-image-2 VectorEngine
Use this skill for project-local image asset generation that must match the repository's `server-rs` APIMart `gpt-image-2` path.
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine `gpt-image-2-all` path. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
## Workflow
@@ -24,15 +24,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.
6. Never print `APIMART_API_KEY`. Report only whether configuration exists.
6. Never print `VECTOR_ENGINE_API_KEY`. Report only whether configuration exists.
## Request Contract
The repository image path uses:
```text
POST {APIMART_BASE_URL}/images/generations
Authorization: Bearer {APIMART_API_KEY}
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
Content-Type: application/json
```
@@ -40,11 +40,10 @@ Default body:
```json
{
"model": "gpt-image-2",
"model": "gpt-image-2-all",
"prompt": "<prompt>",
"n": 1,
"official_fallback": true,
"size": "1:1"
"size": "1024x1024"
}
```
@@ -52,17 +51,11 @@ For a reference image, add:
```json
{
"image_urls": ["data:image/png;base64,..."]
"image": ["data:image/png;base64,..."]
}
```
Poll async responses with:
```text
GET {APIMART_BASE_URL}/tasks/{task_id}
```
Accept image output from `data[].url`, `data[].b64_json`, direct nested `url` fields, or async task results.
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine GPT-image-2-all currently returns synchronously; do not poll APIMart task endpoints.
## Environment
@@ -70,12 +63,12 @@ Load environment values from process env first, then `.env.secrets.local`, `.env
Required for live generation:
- `APIMART_BASE_URL`
- `APIMART_API_KEY`
- `VECTOR_ENGINE_BASE_URL`
- `VECTOR_ENGINE_API_KEY`
Optional:
- `APIMART_IMAGE_REQUEST_TIMEOUT_MS`
- `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`
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.

View File

@@ -1,7 +1,7 @@
interface:
display_name: "GPT Image 2 APIMart"
short_description: "Generate project thumbnails through APIMart"
display_name: "GPT Image 2 VectorEngine"
short_description: "Generate project thumbnails through VectorEngine"
brand_color: "#10B981"
default_prompt: "Use $gpt-image-2-apimart to dry-run or generate puzzle template thumbnails through APIMart."
default_prompt: "Use $gpt-image-2-apimart to dry-run or generate puzzle template thumbnails through VectorEngine."
policy:
allow_implicit_invocation: true

View File

@@ -14,7 +14,6 @@ const promptsPath = path.join(
);
const defaultOutDir = path.join(repoRoot, 'public', 'puzzle-creation-templates');
const defaultTimeoutMs = 180000;
const pollDelayMs = 3000;
const args = new Map();
for (let index = 2; index < process.argv.length; index += 1) {
@@ -66,15 +65,23 @@ function resolveEnv() {
...process.env,
};
return {
baseUrl: String(loaded.APIMART_BASE_URL || '').trim().replace(/\/+$/u, ''),
apiKey: String(loaded.APIMART_API_KEY || '').trim(),
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
.trim()
.replace(/\/+$/u, ''),
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
timeoutMs: Number.parseInt(
String(loaded.APIMART_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
10,
),
};
}
function buildVectorEngineImagesGenerationUrl(baseUrl) {
return baseUrl.endsWith('/v1')
? `${baseUrl}/images/generations`
: `${baseUrl}/v1/images/generations`;
}
function buildPrompt(template) {
return [
'请生成一张高清 1:1 方形插画,用作拼图创作模板样例图。',
@@ -124,14 +131,6 @@ function extractBase64Images(payload) {
return values;
}
function extractTaskId(payload) {
const ids = [];
collectStringsByKey(payload, 'task_id', ids);
collectStringsByKey(payload, 'taskId', ids);
collectStringsByKey(payload, 'id', ids);
return ids[0] || null;
}
function inferExtensionFromContentType(contentType) {
const normalized = contentType.split(';')[0]?.trim().toLowerCase();
if (normalized === 'image/png') {
@@ -172,7 +171,7 @@ async function fetchJson(url, options, timeoutMs) {
});
const text = await response.text();
if (!response.ok) {
throw new Error(`APIMart ${response.status}: ${text.slice(0, 600)}`);
throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`);
}
return JSON.parse(text);
} finally {
@@ -200,52 +199,20 @@ async function downloadUrl(url, timeoutMs) {
}
}
async function waitForTask(env, taskId) {
const deadline = Date.now() + env.timeoutMs;
await new Promise((resolve) => setTimeout(resolve, 10000));
while (Date.now() < deadline) {
const payload = await fetchJson(
`${env.baseUrl}/tasks/${encodeURIComponent(taskId)}`,
{
headers: {
Authorization: `Bearer ${env.apiKey}`,
},
},
env.timeoutMs,
);
const statuses = [];
collectStringsByKey(payload, 'status', statuses);
collectStringsByKey(payload, 'task_status', statuses);
const status = String(statuses[0] || '').trim().toLowerCase();
if (['completed', 'succeeded', 'success'].includes(status)) {
return payload;
}
if (['failed', 'error', 'canceled', 'cancelled', 'unknown'].includes(status)) {
throw new Error(`APIMart task ${taskId} failed: ${JSON.stringify(payload).slice(0, 600)}`);
}
await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
}
throw new Error(`APIMart task ${taskId} timed out`);
}
async function generateOne(env, template, outDir) {
const requestBody = {
model: 'gpt-image-2',
model: 'gpt-image-2-all',
prompt: buildPrompt(template),
n: 1,
official_fallback: true,
size: '1:1',
size: '1024x1024',
};
const payload = await fetchJson(
`${env.baseUrl}/images/generations`,
buildVectorEngineImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
@@ -253,12 +220,8 @@ async function generateOne(env, template, outDir) {
env.timeoutMs,
);
const resolvedPayload =
extractImageUrls(payload).length || extractBase64Images(payload).length
? payload
: await waitForTask(env, extractTaskId(payload));
const urls = extractImageUrls(resolvedPayload);
const b64Images = extractBase64Images(resolvedPayload);
const urls = extractImageUrls(payload);
const b64Images = extractBase64Images(payload);
let image;
if (urls[0]) {
@@ -270,7 +233,7 @@ async function generateOne(env, template, outDir) {
extension: inferExtensionFromBytes(bytes),
};
} else {
throw new Error(`APIMart returned no image for ${template.id}`);
throw new Error(`VectorEngine returned no image for ${template.id}`);
}
mkdirSync(outDir, { recursive: true });
@@ -302,11 +265,10 @@ if (dryRun) {
id: template.id,
title: template.title,
body: {
model: 'gpt-image-2',
model: 'gpt-image-2-all',
prompt: buildPrompt(template),
n: 1,
official_fallback: true,
size: '1:1',
size: '1024x1024',
},
})),
},
@@ -322,7 +284,7 @@ if (!env.baseUrl || !env.apiKey) {
console.error(
JSON.stringify({
ok: false,
error: 'Missing APIMART_BASE_URL or APIMART_API_KEY',
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
hasBaseUrl: Boolean(env.baseUrl),
hasApiKey: Boolean(env.apiKey),
}),