Spaces:
Running
Running
Deploy 7af3d19 to Docker Space
Browse filesSource: MisonL/gpt-image-playground-customer@7af3d19e476e03dfd25eef49672c982b46c64be8
- README.md +1 -1
- docs/deployment/huggingface-space-free.md +1 -1
- public/hf-space-deploy-marker.json +3 -3
- scripts/agent-doctor.mjs +24 -0
- scripts/agent-skill-scripts.test.mjs +130 -4
- scripts/command-center.test.mjs +80 -13
- skills/gpt-image-playground-agent/SKILL.md +2 -2
- skills/gpt-image-playground-agent/references/api.md +2 -2
- skills/gpt-image-playground-agent/scripts/batch-images.mjs +18 -3
- skills/gpt-image-playground-agent/scripts/diagnose-request.mjs +5 -0
- skills/gpt-image-playground-agent/scripts/generate-image.mjs +117 -9
- src/app/api/deploy-marker/route.ts +1 -1
- src/lib/agent-api-contracts.test.ts +1 -1
- src/lib/agent-api-contracts.ts +3 -3
README.md
CHANGED
|
@@ -279,7 +279,7 @@ node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
|
| 279 |
- 远程 Space、云服务或内网服务必须显式传 `--base-url`,避免误用本机默认服务。
|
| 280 |
- Agent CLI 默认读取当前仓库根目录的 `.env.agent.local`;shell 环境变量优先。首次配置可复制 `.env.agent.local.example`。不要提交 token、访问码或哈希。
|
| 281 |
- `GPT_IMAGE_AGENT_TOKEN` 只用于 `/api/agent/*`;页面 SSE `/api/images` 可能还需要 `GPT_IMAGE_APP_PASSWORD_HASH`。
|
| 282 |
-
- dry-run 只验证本地请求构造;`verification_scope.mode=local_planning_only` 不是远端已可执行。远端合同检查用 `--contract-check`,真实执行必须加 `--allow-billable`。
|
| 283 |
- 多张真实任务优先用 `batch-images.mjs`、`--manifest`、`--resume` 和 `--dimension-check`;不要手动并行启动多个单张脚本。需要并发时先看 `/api/runtime-capabilities` 的 `streamingBatch.recommendedConcurrency` 和 `channelQueue.capacityPerCredential`。
|
| 284 |
- 选择 `responses-image-generation` 或兼容别名 `responses` 时,`partial_images` 必须优先按 `partial_images_by_backend["responses-image-generation"]` 校验,不能套用 Matsca Images API 的范围。
|
| 285 |
- 页面 SSE 返回 503 或断流时,先用诊断脚本读取结构化摘要,再用新的幂等键显式选择备用路径。`page_sse_supported=true` 只是声明支持,不代表实测一定成功。Agent edit 输出格式和尺寸可能与页面 SSE 不完全一致,尺寸敏感任务必须重新校验或用 `--dimension-check`。
|
|
|
|
| 279 |
- 远程 Space、云服务或内网服务必须显式传 `--base-url`,避免误用本机默认服务。
|
| 280 |
- Agent CLI 默认读取当前仓库根目录的 `.env.agent.local`;shell 环境变量优先。首次配置可复制 `.env.agent.local.example`。不要提交 token、访问码或哈希。
|
| 281 |
- `GPT_IMAGE_AGENT_TOKEN` 只用于 `/api/agent/*`;页面 SSE `/api/images` 可能还需要 `GPT_IMAGE_APP_PASSWORD_HASH`。
|
| 282 |
+
- dry-run 只验证本地请求构造;`verification_scope.mode=local_planning_only` 不是远端已可执行。需要只读读取远端 capabilities 和 runtime 时加 `--check-remote`,输出 `verification_scope.mode=remote_contract_and_local_planning`。远端合同检查用 `--contract-check`,真实执行必须加 `--allow-billable`。
|
| 283 |
- 多张真实任务优先用 `batch-images.mjs`、`--manifest`、`--resume` 和 `--dimension-check`;不要手动并行启动多个单张脚本。需要并发时先看 `/api/runtime-capabilities` 的 `streamingBatch.recommendedConcurrency` 和 `channelQueue.capacityPerCredential`。
|
| 284 |
- 选择 `responses-image-generation` 或兼容别名 `responses` 时,`partial_images` 必须优先按 `partial_images_by_backend["responses-image-generation"]` 校验,不能套用 Matsca Images API 的范围。
|
| 285 |
- 页面 SSE 返回 503 或断流时,先用诊断脚本读取结构化摘要,再用新的幂等键显式选择备用路径。`page_sse_supported=true` 只是声明支持,不代表实测一定成功。Agent edit 输出格式和尺寸可能与页面 SSE 不完全一致,尺寸敏感任务必须重新校验或用 `--dimension-check`。
|
docs/deployment/huggingface-space-free.md
CHANGED
|
@@ -199,7 +199,7 @@ node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
| 199 |
|
| 200 |
脚本会先读取 `GET /api/agent/capabilities`,再调用 Agent API。成功响应会保留相对 `content_url`,同时补充 `absolute_content_url` 和 `absolute_metadata_url`,便于在桌面环境直接下载产物。
|
| 201 |
|
| 202 |
-
远端 Agent 调用不要硬编码路径。
|
| 203 |
|
| 204 |
如果 Space 同时配置了 `APP_PASSWORD` 和 `AGENT_API_TOKEN`,Agent JSON 端点用 `GPT_IMAGE_AGENT_TOKEN` 发送 Bearer token;页面端 `/api/images` SSE 仍按 capabilities 的 `agent_streaming.page_sse.auth` 判断,可能需要额外设置 `GPT_IMAGE_APP_PASSWORD_HASH`,并通过 form-data `passwordHash` 发送页面访问码哈希。页面 SSE 会把业务 key 写入 `clientRequestId`,长度上限以 capabilities 中的 `agent_streaming.page_sse.client_request_id.max_length` 为准。
|
| 205 |
|
|
|
|
| 199 |
|
| 200 |
脚本会先读取 `GET /api/agent/capabilities`,再调用 Agent API。成功响应会保留相对 `content_url`,同时补充 `absolute_content_url` 和 `absolute_metadata_url`,便于在桌面环境直接下载产物。
|
| 201 |
|
| 202 |
+
远端 Agent 调用不要硬编码路径。普通文生图默认提交业务意图到 capabilities 声明的 `orchestration.endpoint`,由服务端选择内部执行路径、上游 request mode 和轮询方式;Agent 客户端不按尺寸、远端 HTTPS 或流式参数自行选择 Images、Responses、SSE 或非流式路径。页面高级字段、默认 WebP edit、高分辨率 edit 和复杂批量仍按 Skill 规则使用页面端 `/api/images` SSE;页面流式失败或不可用时,先诊断结构化错误,再用新的 `Idempotency-Key` 显式选择 Agent JSON、Agent edit 或 job 路径对照。job polling 只在显式选择时使用。需要诊断对照时可用 `--agent` 或 `--streaming-strategy off` 强制 Agent JSON,也可用 `--page-sse` 或 `--job` 显式选择路径。
|
| 203 |
|
| 204 |
如果 Space 同时配置了 `APP_PASSWORD` 和 `AGENT_API_TOKEN`,Agent JSON 端点用 `GPT_IMAGE_AGENT_TOKEN` 发送 Bearer token;页面端 `/api/images` SSE 仍按 capabilities 的 `agent_streaming.page_sse.auth` 判断,可能需要额外设置 `GPT_IMAGE_APP_PASSWORD_HASH`,并通过 form-data `passwordHash` 发送页面访问码哈希。页面 SSE 会把业务 key 写入 `clientRequestId`,长度上限以 capabilities 中的 `agent_streaming.page_sse.client_request_id.max_length` 为准。
|
| 205 |
|
public/hf-space-deploy-marker.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"schema_version": 1,
|
| 3 |
-
"local_sha": "
|
| 4 |
-
"created_at": "2026-06-
|
| 5 |
-
"deploy_id": "
|
| 6 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"schema_version": 1,
|
| 3 |
+
"local_sha": "7af3d19e476e03dfd25eef49672c982b46c64be8",
|
| 4 |
+
"created_at": "2026-06-26T01:06:33.777Z",
|
| 5 |
+
"deploy_id": "46b8343b-982b-4f3b-8526-ca1feabfd227"
|
| 6 |
}
|
scripts/agent-doctor.mjs
CHANGED
|
@@ -391,6 +391,10 @@ function buildSummary({ capabilities, runtime, contract, smoke }) {
|
|
| 391 |
runtime: runtime.ok ? runtime.body : undefined,
|
| 392 |
smoke
|
| 393 |
}),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
responses_gpt2image_ready:
|
| 395 |
capabilities.ok && runtime.ok
|
| 396 |
? capabilities.body?.supported?.image_backend_requirements?.['responses-image-generation']?.enabled === true &&
|
|
@@ -403,6 +407,26 @@ function buildSummary({ capabilities, runtime, contract, smoke }) {
|
|
| 403 |
};
|
| 404 |
}
|
| 405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
function summarizeSmokeCheck(smoke, name) {
|
| 407 |
const check = smoke.checks?.find((item) => item.name === name);
|
| 408 |
if (!check || check.skipped) return 'skipped';
|
|
|
|
| 391 |
runtime: runtime.ok ? runtime.body : undefined,
|
| 392 |
smoke
|
| 393 |
}),
|
| 394 |
+
runtime_environment: buildRuntimeEnvironmentSummary({
|
| 395 |
+
capabilities: capabilities.ok ? capabilities.body : undefined,
|
| 396 |
+
runtime: runtime.ok ? runtime.body : undefined
|
| 397 |
+
}),
|
| 398 |
responses_gpt2image_ready:
|
| 399 |
capabilities.ok && runtime.ok
|
| 400 |
? capabilities.body?.supported?.image_backend_requirements?.['responses-image-generation']?.enabled === true &&
|
|
|
|
| 407 |
};
|
| 408 |
}
|
| 409 |
|
| 410 |
+
function buildRuntimeEnvironmentSummary({ capabilities, runtime }) {
|
| 411 |
+
return {
|
| 412 |
+
state_backend: capabilities?.defaults?.state_backend || 'unknown',
|
| 413 |
+
image_storage_mode: capabilities?.storage?.image_storage_mode || 'unknown',
|
| 414 |
+
postgres_configured: capabilities?.storage?.postgres_configured === true,
|
| 415 |
+
page_sse_auth_required: capabilities?.agent_streaming?.page_sse?.auth?.required === true,
|
| 416 |
+
agent_auth_schemes: Array.isArray(capabilities?.auth?.schemes) ? capabilities.auth.schemes : [],
|
| 417 |
+
orchestration_endpoint: capabilities?.orchestration?.endpoint || null,
|
| 418 |
+
orchestration_transport_selection: capabilities?.orchestration?.transport_selection || null,
|
| 419 |
+
request_mode_control_policy: capabilities?.request_mode_controls?.agent_client_policy || null,
|
| 420 |
+
request_mode_controls: runtime?.channelRouting?.requestModeControls || capabilities?.request_mode_controls || null,
|
| 421 |
+
runtime_strategy: runtime?.channelRouting?.strategy || null,
|
| 422 |
+
effective_request_modes: readRequestModeList(runtime?.channelRouting?.effectiveRequestModes),
|
| 423 |
+
streaming_batch_enabled: runtime?.streamingBatch?.enabled === true,
|
| 424 |
+
recommended_concurrency: runtime?.streamingBatch?.recommendedConcurrency ?? null,
|
| 425 |
+
channel_queue_enabled: runtime?.channelQueue?.enabled === true,
|
| 426 |
+
channel_queue_capacity_per_credential: runtime?.channelQueue?.capacityPerCredential ?? null
|
| 427 |
+
};
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
function summarizeSmokeCheck(smoke, name) {
|
| 431 |
const check = smoke.checks?.find((item) => item.name === name);
|
| 432 |
if (!check || check.skipped) return 'skipped';
|
scripts/agent-skill-scripts.test.mjs
CHANGED
|
@@ -618,6 +618,18 @@ describe('Agent skill script argument validation', () => {
|
|
| 618 |
assert.equal(result.stderr.trim(), '');
|
| 619 |
});
|
| 620 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
it('prints server orchestration guidance for remote HTTPS small generate dry-runs', () => {
|
| 622 |
const result = runSkillScript('generate-image.mjs', ['--base-url', 'https://space.example.test', 'prompt']);
|
| 623 |
|
|
@@ -631,6 +643,70 @@ describe('Agent skill script argument validation', () => {
|
|
| 631 |
assert.equal(result.stderr.trim(), '');
|
| 632 |
});
|
| 633 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
it('keeps default generate on server orchestration when request modes are declared', async () => {
|
| 635 |
const requests = [];
|
| 636 |
let imageRequestBody = '';
|
|
@@ -789,7 +865,19 @@ describe('Agent skill script argument validation', () => {
|
|
| 789 |
requests.push({ method: request.method, url: request.url });
|
| 790 |
if (request.url === '/api/agent/capabilities') {
|
| 791 |
response.writeHead(200, { 'content-type': 'application/json' });
|
| 792 |
-
response.end(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
return;
|
| 794 |
}
|
| 795 |
if (
|
|
@@ -809,6 +897,11 @@ describe('Agent skill script argument validation', () => {
|
|
| 809 |
);
|
| 810 |
return;
|
| 811 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 812 |
response.writeHead(404, { 'content-type': 'application/json' });
|
| 813 |
response.end(JSON.stringify({ error: 'missing' }));
|
| 814 |
},
|
|
@@ -827,7 +920,8 @@ describe('Agent skill script argument validation', () => {
|
|
| 827 |
[
|
| 828 |
'/api/agent/images/generate',
|
| 829 |
'/api/agent/image-requests',
|
| 830 |
-
'/api/agent/jobs/images/generate'
|
|
|
|
| 831 |
]
|
| 832 |
);
|
| 833 |
assert.deepEqual(
|
|
@@ -836,7 +930,8 @@ describe('Agent skill script argument validation', () => {
|
|
| 836 |
'GET /api/agent/capabilities',
|
| 837 |
'POST /api/agent/images/generate',
|
| 838 |
'POST /api/agent/image-requests',
|
| 839 |
-
'POST /api/agent/jobs/images/generate'
|
|
|
|
| 840 |
]
|
| 841 |
);
|
| 842 |
}
|
|
@@ -3618,6 +3713,8 @@ describe('Agent skill script argument validation', () => {
|
|
| 3618 |
assert.match(skillText, /Codex 会话日志会持久保存命令输出/);
|
| 3619 |
assert.match(skillText, /npm run env:summary/);
|
| 3620 |
assert.match(skillText, /verification_scope\.mode=local_planning_only/);
|
|
|
|
|
|
|
| 3621 |
assert.match(skillText, /manifest_written=false/);
|
| 3622 |
assert.match(skillText, /Hugging Face Space Secrets 只能写入和列出名称/);
|
| 3623 |
|
|
@@ -3628,6 +3725,8 @@ describe('Agent skill script argument validation', () => {
|
|
| 3628 |
assert.match(apiReference, /channel_capacity_queue_aborted/);
|
| 3629 |
assert.match(apiReference, /npm run env:summary/);
|
| 3630 |
assert.match(apiReference, /verification_scope\.mode=local_planning_only/);
|
|
|
|
|
|
|
| 3631 |
assert.match(apiReference, /manifest_written=false/);
|
| 3632 |
assert.match(apiReference, /Hugging Face Space Secrets 只能写入和列出名称/);
|
| 3633 |
assert.match(apiReference, /这个失败表示上游已生成但本地验收未通过/);
|
|
@@ -3831,6 +3930,25 @@ describe('Agent skill script argument validation', () => {
|
|
| 3831 |
}
|
| 3832 |
});
|
| 3833 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3834 |
it('records enriched Agent diagnostics for failed batch tasks and manifests', async () => {
|
| 3835 |
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-diagnostics-'));
|
| 3836 |
try {
|
|
@@ -5990,6 +6108,9 @@ describe('Agent skill script argument validation', () => {
|
|
| 5990 |
assert.equal(result.stderr.trim(), '');
|
| 5991 |
const body = JSON.parse(result.stdout);
|
| 5992 |
assert.equal(body.client_request_id, 'web-diag');
|
|
|
|
|
|
|
|
|
|
| 5993 |
assert.equal(body.feedback.value, 'usable');
|
| 5994 |
assert.equal(body.diagnostics.matched_log_count, 1);
|
| 5995 |
assert.equal(body.request_count, 1);
|
|
@@ -6479,7 +6600,11 @@ describe('Agent skill script argument validation', () => {
|
|
| 6479 |
assert.equal(result.stderr.trim(), '');
|
| 6480 |
const body = JSON.parse(result.stdout);
|
| 6481 |
assert.deepEqual(body.diagnostics_retention, retention);
|
|
|
|
|
|
|
|
|
|
| 6482 |
assert.equal(body.diagnostics_note.code, 'no_matching_logs_in_retention_window');
|
|
|
|
| 6483 |
assert.equal(body.requests[0].diagnostics_note.code, 'no_matching_logs_in_retention_window');
|
| 6484 |
assert.equal(body.requests[0].diagnostics_note.retention.max_entries, 123);
|
| 6485 |
assert.match(body.requests[0].diagnostics_note.message, /最近 123 条本地应用日志/);
|
|
@@ -6570,7 +6695,8 @@ function runSkillScript(filename, args, env = {}, options = {}) {
|
|
| 6570 |
return spawnSync(process.execPath, [join(skillScriptsRoot, filename), ...args], {
|
| 6571 |
cwd: options.cwd || repoRoot,
|
| 6572 |
encoding: 'utf8',
|
| 6573 |
-
env: { ...baseEnv, ...env }
|
|
|
|
| 6574 |
});
|
| 6575 |
}
|
| 6576 |
|
|
|
|
| 618 |
assert.equal(result.stderr.trim(), '');
|
| 619 |
});
|
| 620 |
|
| 621 |
+
it('explains explicit page SSE dry-runs without blaming high-resolution routing', () => {
|
| 622 |
+
const result = runSkillScript('generate-image.mjs', ['--page-sse', '--size', '1024x1024', 'prompt']);
|
| 623 |
+
|
| 624 |
+
assert.equal(result.status, 0);
|
| 625 |
+
const body = JSON.parse(result.stdout);
|
| 626 |
+
assert.equal(body.endpoint, 'http://localhost:4783/api/images');
|
| 627 |
+
assert.equal(body.routing_guidance.transport, 'page_sse');
|
| 628 |
+
assert.match(body.routing_guidance.reason, /Explicit --page-sse/);
|
| 629 |
+
assert.doesNotMatch(body.routing_guidance.reason, /max_edge>2048/);
|
| 630 |
+
assert.equal(result.stderr.trim(), '');
|
| 631 |
+
});
|
| 632 |
+
|
| 633 |
it('prints server orchestration guidance for remote HTTPS small generate dry-runs', () => {
|
| 634 |
const result = runSkillScript('generate-image.mjs', ['--base-url', 'https://space.example.test', 'prompt']);
|
| 635 |
|
|
|
|
| 643 |
assert.equal(result.stderr.trim(), '');
|
| 644 |
});
|
| 645 |
|
| 646 |
+
it('can combine generate dry-run planning with non-billable remote checks', async () => {
|
| 647 |
+
const requests = [];
|
| 648 |
+
await withServer(
|
| 649 |
+
(request, response) => {
|
| 650 |
+
requests.push({ method: request.method, url: request.url });
|
| 651 |
+
if (request.url === '/api/agent/capabilities') {
|
| 652 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 653 |
+
response.end(
|
| 654 |
+
JSON.stringify(
|
| 655 |
+
agentGenerateCapabilities({
|
| 656 |
+
supported: {
|
| 657 |
+
request_modes: ['images-non-stream', 'images-sse']
|
| 658 |
+
},
|
| 659 |
+
agent_streaming: {
|
| 660 |
+
page_sse: { supported: true, endpoint: '/api/images' }
|
| 661 |
+
}
|
| 662 |
+
})
|
| 663 |
+
)
|
| 664 |
+
);
|
| 665 |
+
return;
|
| 666 |
+
}
|
| 667 |
+
if (request.url === '/api/runtime-capabilities') {
|
| 668 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 669 |
+
response.end(
|
| 670 |
+
JSON.stringify({
|
| 671 |
+
streamingBatch: { enabled: true, recommendedConcurrency: 2 },
|
| 672 |
+
channelRouting: { effectiveRequestModes: ['images-non-stream'] }
|
| 673 |
+
})
|
| 674 |
+
);
|
| 675 |
+
return;
|
| 676 |
+
}
|
| 677 |
+
response.writeHead(404, { 'content-type': 'application/json' });
|
| 678 |
+
response.end(JSON.stringify({ error: 'missing' }));
|
| 679 |
+
},
|
| 680 |
+
async (baseUrl) => {
|
| 681 |
+
const result = await runSkillScriptAsync(
|
| 682 |
+
'generate-image.mjs',
|
| 683 |
+
['--check-remote', 'prompt'],
|
| 684 |
+
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
|
| 685 |
+
);
|
| 686 |
+
|
| 687 |
+
assert.equal(result.status, 0);
|
| 688 |
+
assert.equal(result.stderr.trim(), '');
|
| 689 |
+
const body = JSON.parse(result.stdout);
|
| 690 |
+
assert.equal(body.verification_scope.mode, 'remote_contract_and_local_planning');
|
| 691 |
+
assert.equal(body.verification_scope.remote_capabilities_verified, true);
|
| 692 |
+
assert.equal(body.verification_scope.runtime_capacity_verified, true);
|
| 693 |
+
assert.equal(body.verification_scope.auth_verified, true);
|
| 694 |
+
assert.equal(body.verification_scope.billable_request_sent, false);
|
| 695 |
+
assert.deepEqual(body.verification_scope.remote_check.capabilities.request_modes, [
|
| 696 |
+
'images-non-stream',
|
| 697 |
+
'images-sse'
|
| 698 |
+
]);
|
| 699 |
+
assert.deepEqual(body.verification_scope.remote_check.runtime.effective_request_modes, [
|
| 700 |
+
'images-non-stream'
|
| 701 |
+
]);
|
| 702 |
+
assert.deepEqual(
|
| 703 |
+
requests.map((item) => `${item.method} ${item.url}`),
|
| 704 |
+
['GET /api/agent/capabilities', 'GET /api/runtime-capabilities']
|
| 705 |
+
);
|
| 706 |
+
}
|
| 707 |
+
);
|
| 708 |
+
});
|
| 709 |
+
|
| 710 |
it('keeps default generate on server orchestration when request modes are declared', async () => {
|
| 711 |
const requests = [];
|
| 712 |
let imageRequestBody = '';
|
|
|
|
| 865 |
requests.push({ method: request.method, url: request.url });
|
| 866 |
if (request.url === '/api/agent/capabilities') {
|
| 867 |
response.writeHead(200, { 'content-type': 'application/json' });
|
| 868 |
+
response.end(
|
| 869 |
+
JSON.stringify(
|
| 870 |
+
agentGenerateCapabilities({
|
| 871 |
+
agent_streaming: {
|
| 872 |
+
page_sse: {
|
| 873 |
+
supported: true,
|
| 874 |
+
endpoint: '/api/images',
|
| 875 |
+
client_request_id: { max_length: 128 }
|
| 876 |
+
}
|
| 877 |
+
}
|
| 878 |
+
})
|
| 879 |
+
)
|
| 880 |
+
);
|
| 881 |
return;
|
| 882 |
}
|
| 883 |
if (
|
|
|
|
| 897 |
);
|
| 898 |
return;
|
| 899 |
}
|
| 900 |
+
if (request.url === '/api/images') {
|
| 901 |
+
response.writeHead(400, { 'content-type': 'application/json' });
|
| 902 |
+
response.end(JSON.stringify({ error: 'clientRequestId 长度不能超过 128 个字符。' }));
|
| 903 |
+
return;
|
| 904 |
+
}
|
| 905 |
response.writeHead(404, { 'content-type': 'application/json' });
|
| 906 |
response.end(JSON.stringify({ error: 'missing' }));
|
| 907 |
},
|
|
|
|
| 920 |
[
|
| 921 |
'/api/agent/images/generate',
|
| 922 |
'/api/agent/image-requests',
|
| 923 |
+
'/api/agent/jobs/images/generate',
|
| 924 |
+
'/api/images'
|
| 925 |
]
|
| 926 |
);
|
| 927 |
assert.deepEqual(
|
|
|
|
| 930 |
'GET /api/agent/capabilities',
|
| 931 |
'POST /api/agent/images/generate',
|
| 932 |
'POST /api/agent/image-requests',
|
| 933 |
+
'POST /api/agent/jobs/images/generate',
|
| 934 |
+
'POST /api/images'
|
| 935 |
]
|
| 936 |
);
|
| 937 |
}
|
|
|
|
| 3713 |
assert.match(skillText, /Codex 会话日志会持久保存命令输出/);
|
| 3714 |
assert.match(skillText, /npm run env:summary/);
|
| 3715 |
assert.match(skillText, /verification_scope\.mode=local_planning_only/);
|
| 3716 |
+
assert.match(skillText, /--check-remote/);
|
| 3717 |
+
assert.match(skillText, /remote_contract_and_local_planning/);
|
| 3718 |
assert.match(skillText, /manifest_written=false/);
|
| 3719 |
assert.match(skillText, /Hugging Face Space Secrets 只能写入和列出名称/);
|
| 3720 |
|
|
|
|
| 3725 |
assert.match(apiReference, /channel_capacity_queue_aborted/);
|
| 3726 |
assert.match(apiReference, /npm run env:summary/);
|
| 3727 |
assert.match(apiReference, /verification_scope\.mode=local_planning_only/);
|
| 3728 |
+
assert.match(apiReference, /--check-remote/);
|
| 3729 |
+
assert.match(apiReference, /remote_contract_and_local_planning/);
|
| 3730 |
assert.match(apiReference, /manifest_written=false/);
|
| 3731 |
assert.match(apiReference, /Hugging Face Space Secrets 只能写入和列出名称/);
|
| 3732 |
assert.match(apiReference, /这个失败表示上游已生成但本地验收未通过/);
|
|
|
|
| 3930 |
}
|
| 3931 |
});
|
| 3932 |
|
| 3933 |
+
it('prints stdin batch dry-run plans without inventing a /dev/stdin manifest path', () => {
|
| 3934 |
+
const input = `${JSON.stringify({
|
| 3935 |
+
id: 'stdin-task',
|
| 3936 |
+
prompt: 'stdin prompt',
|
| 3937 |
+
size: '1024x1024'
|
| 3938 |
+
})}\n`;
|
| 3939 |
+
|
| 3940 |
+
const result = runSkillScript('batch-images.mjs', ['--input', '-'], {}, { input });
|
| 3941 |
+
|
| 3942 |
+
assert.equal(result.status, 0);
|
| 3943 |
+
assert.equal(result.stderr.trim(), '');
|
| 3944 |
+
const body = JSON.parse(result.stdout);
|
| 3945 |
+
assert.equal(body.input, '/dev/stdin');
|
| 3946 |
+
assert.equal(body.manifest, null);
|
| 3947 |
+
assert.equal(body.manifest_written, false);
|
| 3948 |
+
assert.equal(body.total, 1);
|
| 3949 |
+
assert.equal(body.tasks[0].id, 'stdin-task');
|
| 3950 |
+
});
|
| 3951 |
+
|
| 3952 |
it('records enriched Agent diagnostics for failed batch tasks and manifests', async () => {
|
| 3953 |
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-diagnostics-'));
|
| 3954 |
try {
|
|
|
|
| 6108 |
assert.equal(result.stderr.trim(), '');
|
| 6109 |
const body = JSON.parse(result.stdout);
|
| 6110 |
assert.equal(body.client_request_id, 'web-diag');
|
| 6111 |
+
assert.equal(body.found, true);
|
| 6112 |
+
assert.equal(body.page_request_query_count, 1);
|
| 6113 |
+
assert.equal(body.page_request_found_count, 1);
|
| 6114 |
assert.equal(body.feedback.value, 'usable');
|
| 6115 |
assert.equal(body.diagnostics.matched_log_count, 1);
|
| 6116 |
assert.equal(body.request_count, 1);
|
|
|
|
| 6600 |
assert.equal(result.stderr.trim(), '');
|
| 6601 |
const body = JSON.parse(result.stdout);
|
| 6602 |
assert.deepEqual(body.diagnostics_retention, retention);
|
| 6603 |
+
assert.equal(body.found, false);
|
| 6604 |
+
assert.equal(body.page_request_query_count, 1);
|
| 6605 |
+
assert.equal(body.page_request_found_count, 0);
|
| 6606 |
assert.equal(body.diagnostics_note.code, 'no_matching_logs_in_retention_window');
|
| 6607 |
+
assert.equal(body.requests[0].found, false);
|
| 6608 |
assert.equal(body.requests[0].diagnostics_note.code, 'no_matching_logs_in_retention_window');
|
| 6609 |
assert.equal(body.requests[0].diagnostics_note.retention.max_entries, 123);
|
| 6610 |
assert.match(body.requests[0].diagnostics_note.message, /最近 123 条本地应用日志/);
|
|
|
|
| 6695 |
return spawnSync(process.execPath, [join(skillScriptsRoot, filename), ...args], {
|
| 6696 |
cwd: options.cwd || repoRoot,
|
| 6697 |
encoding: 'utf8',
|
| 6698 |
+
env: { ...baseEnv, ...env },
|
| 6699 |
+
...(options.input !== undefined ? { input: options.input } : {})
|
| 6700 |
});
|
| 6701 |
}
|
| 6702 |
|
scripts/command-center.test.mjs
CHANGED
|
@@ -752,6 +752,11 @@ describe('Command center scripts', () => {
|
|
| 752 |
endpoint: '/api/agent/image-requests',
|
| 753 |
transport_selection: 'server_owned'
|
| 754 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
routing_rules: {
|
| 756 |
high_resolution_edit: {
|
| 757 |
conditions: { operation: 'edit', max_edge: { operator: 'gt', value: 2048 } }
|
|
@@ -796,9 +801,16 @@ describe('Command center scripts', () => {
|
|
| 796 |
defaultMode: 'auto',
|
| 797 |
unavailableMarkScope: 'channel+backend+strategy+operation'
|
| 798 |
},
|
| 799 |
-
streamingBatch: { enabled: true },
|
|
|
|
| 800 |
responsesImageBackend: { enabled: true, mode: 'experimental' },
|
| 801 |
channelRouting: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
configuredRequestModes: ['images-non-stream', 'images-sse', 'responses-sse'],
|
| 803 |
effectiveRequestModes: ['images-non-stream', 'images-sse'],
|
| 804 |
effectiveRequestModesByChannel: [
|
|
@@ -900,6 +912,27 @@ describe('Command center scripts', () => {
|
|
| 900 |
assert.equal(body.summary.request_modes.smoke['responses-sse'].billable, false);
|
| 901 |
assert.equal(body.summary.responses_gpt2image_ready, true);
|
| 902 |
assert.equal(body.summary.responses_image_backend_declared_supported, true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 903 |
assert.equal(body.summary.billable_smoke, 'skipped');
|
| 904 |
assert.equal(body.layers.find((layer) => layer.name === 'billable_smoke').skipped, true);
|
| 905 |
assert.ok(
|
|
@@ -1013,16 +1046,27 @@ describe('Command center scripts', () => {
|
|
| 1013 |
return;
|
| 1014 |
}
|
| 1015 |
if (request.url === '/api/images') {
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
''
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1026 |
return;
|
| 1027 |
}
|
| 1028 |
if (request.url === '/api/agent/image-requests') {
|
|
@@ -1127,8 +1171,19 @@ describe('Command center scripts', () => {
|
|
| 1127 |
return;
|
| 1128 |
}
|
| 1129 |
if (request.url === '/api/images') {
|
| 1130 |
-
|
| 1131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1132 |
return;
|
| 1133 |
}
|
| 1134 |
if (request.url === '/api/agent/jobs/images/generate') {
|
|
@@ -1356,3 +1411,15 @@ function runNodeCommandAsync(args, options = {}) {
|
|
| 1356 |
});
|
| 1357 |
});
|
| 1358 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 752 |
endpoint: '/api/agent/image-requests',
|
| 753 |
transport_selection: 'server_owned'
|
| 754 |
},
|
| 755 |
+
request_mode_controls: {
|
| 756 |
+
global_env: 'OPENAI_UPSTREAM_REQUEST_MODES',
|
| 757 |
+
channel_env_pattern: 'OPENAI_CHANNEL_N_REQUEST_MODES',
|
| 758 |
+
agent_client_policy: 'diagnostics_only'
|
| 759 |
+
},
|
| 760 |
routing_rules: {
|
| 761 |
high_resolution_edit: {
|
| 762 |
conditions: { operation: 'edit', max_edge: { operator: 'gt', value: 2048 } }
|
|
|
|
| 801 |
defaultMode: 'auto',
|
| 802 |
unavailableMarkScope: 'channel+backend+strategy+operation'
|
| 803 |
},
|
| 804 |
+
streamingBatch: { enabled: true, recommendedConcurrency: 2 },
|
| 805 |
+
channelQueue: { enabled: true, capacityPerCredential: 1 },
|
| 806 |
responsesImageBackend: { enabled: true, mode: 'experimental' },
|
| 807 |
channelRouting: {
|
| 808 |
+
strategy: 'round_robin',
|
| 809 |
+
requestModeControls: {
|
| 810 |
+
global_env: 'OPENAI_UPSTREAM_REQUEST_MODES',
|
| 811 |
+
channel_env_pattern: 'OPENAI_CHANNEL_N_REQUEST_MODES',
|
| 812 |
+
agent_client_policy: 'diagnostics_only'
|
| 813 |
+
},
|
| 814 |
configuredRequestModes: ['images-non-stream', 'images-sse', 'responses-sse'],
|
| 815 |
effectiveRequestModes: ['images-non-stream', 'images-sse'],
|
| 816 |
effectiveRequestModesByChannel: [
|
|
|
|
| 912 |
assert.equal(body.summary.request_modes.smoke['responses-sse'].billable, false);
|
| 913 |
assert.equal(body.summary.responses_gpt2image_ready, true);
|
| 914 |
assert.equal(body.summary.responses_image_backend_declared_supported, true);
|
| 915 |
+
assert.deepEqual(body.summary.runtime_environment, {
|
| 916 |
+
state_backend: 'memory',
|
| 917 |
+
image_storage_mode: 'indexeddb',
|
| 918 |
+
postgres_configured: false,
|
| 919 |
+
page_sse_auth_required: true,
|
| 920 |
+
agent_auth_schemes: [],
|
| 921 |
+
orchestration_endpoint: '/api/agent/image-requests',
|
| 922 |
+
orchestration_transport_selection: 'server_owned',
|
| 923 |
+
request_mode_control_policy: 'diagnostics_only',
|
| 924 |
+
request_mode_controls: {
|
| 925 |
+
global_env: 'OPENAI_UPSTREAM_REQUEST_MODES',
|
| 926 |
+
channel_env_pattern: 'OPENAI_CHANNEL_N_REQUEST_MODES',
|
| 927 |
+
agent_client_policy: 'diagnostics_only'
|
| 928 |
+
},
|
| 929 |
+
runtime_strategy: 'round_robin',
|
| 930 |
+
effective_request_modes: ['images-non-stream', 'images-sse'],
|
| 931 |
+
streaming_batch_enabled: true,
|
| 932 |
+
recommended_concurrency: 2,
|
| 933 |
+
channel_queue_enabled: true,
|
| 934 |
+
channel_queue_capacity_per_credential: 1
|
| 935 |
+
});
|
| 936 |
assert.equal(body.summary.billable_smoke, 'skipped');
|
| 937 |
assert.equal(body.layers.find((layer) => layer.name === 'billable_smoke').skipped, true);
|
| 938 |
assert.ok(
|
|
|
|
| 1046 |
return;
|
| 1047 |
}
|
| 1048 |
if (request.url === '/api/images') {
|
| 1049 |
+
(async () => {
|
| 1050 |
+
const requestText = await readRequestText(request);
|
| 1051 |
+
if (requestText.includes('contract check')) {
|
| 1052 |
+
response.writeHead(400, { 'content-type': 'application/json' });
|
| 1053 |
+
response.end(JSON.stringify({ error: 'clientRequestId 长度不能超过 128 个字符。' }));
|
| 1054 |
+
return;
|
| 1055 |
+
}
|
| 1056 |
+
response.writeHead(200, { 'content-type': 'text/event-stream' });
|
| 1057 |
+
response.end(
|
| 1058 |
+
[
|
| 1059 |
+
'data: {"type":"completed","filename":"doctor-page-sse.png","path":"/api/image/doctor-page-sse.png","output_format":"png"}',
|
| 1060 |
+
'',
|
| 1061 |
+
'data: {"type":"done","client_request_id":"doctor-page-sse-request","images":[{"filename":"doctor-page-sse.png"}]}',
|
| 1062 |
+
'',
|
| 1063 |
+
''
|
| 1064 |
+
].join('\n')
|
| 1065 |
+
);
|
| 1066 |
+
})().catch((error) => {
|
| 1067 |
+
response.writeHead(500, { 'content-type': 'application/json' });
|
| 1068 |
+
response.end(JSON.stringify({ error: error.message }));
|
| 1069 |
+
});
|
| 1070 |
return;
|
| 1071 |
}
|
| 1072 |
if (request.url === '/api/agent/image-requests') {
|
|
|
|
| 1171 |
return;
|
| 1172 |
}
|
| 1173 |
if (request.url === '/api/images') {
|
| 1174 |
+
(async () => {
|
| 1175 |
+
const requestText = await readRequestText(request);
|
| 1176 |
+
if (requestText.includes('contract check')) {
|
| 1177 |
+
response.writeHead(400, { 'content-type': 'application/json' });
|
| 1178 |
+
response.end(JSON.stringify({ error: 'clientRequestId 长度不能超过 128 个字符。' }));
|
| 1179 |
+
return;
|
| 1180 |
+
}
|
| 1181 |
+
response.writeHead(503, { 'content-type': 'text/plain' });
|
| 1182 |
+
response.end('503 Service temporarily unavailable');
|
| 1183 |
+
})().catch((error) => {
|
| 1184 |
+
response.writeHead(500, { 'content-type': 'application/json' });
|
| 1185 |
+
response.end(JSON.stringify({ error: error.message }));
|
| 1186 |
+
});
|
| 1187 |
return;
|
| 1188 |
}
|
| 1189 |
if (request.url === '/api/agent/jobs/images/generate') {
|
|
|
|
| 1411 |
});
|
| 1412 |
});
|
| 1413 |
}
|
| 1414 |
+
|
| 1415 |
+
function readRequestText(request) {
|
| 1416 |
+
return new Promise((resolve, reject) => {
|
| 1417 |
+
let body = '';
|
| 1418 |
+
request.setEncoding('utf8');
|
| 1419 |
+
request.on('data', (chunk) => {
|
| 1420 |
+
body += chunk;
|
| 1421 |
+
});
|
| 1422 |
+
request.on('end', () => resolve(body));
|
| 1423 |
+
request.on('error', reject);
|
| 1424 |
+
});
|
| 1425 |
+
}
|
skills/gpt-image-playground-agent/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ Agent API 是给自动化客户端使用的机器接口,不是自治 Agent 平
|
|
| 19 |
- 诊断上游图片接口:优先运行 `scripts/probe-upstream-image.mjs`。
|
| 20 |
- 不要临时编写 Node/Python/shell 脚本、curl 命令或手写 fetch/FormData 来重复实现这些脚本已经覆盖的 API 调用。
|
| 21 |
- 只有在内置脚本缺少用户明确需要的能力时,才修改或扩展 `scripts/` 内的预置脚本,并同步补测试;不要在仓库外留下 ad hoc 调用脚本。
|
| 22 |
-
- 先用 dry-run 或 `--contract-check` 检查请求、路由、鉴权和服务声明的默认编排入口;只有用户明确允许真实计费时才加 `--allow-billable`。
|
| 23 |
- 真实调用成功或失败后,优先读取脚本输出的 `summary`。它是面向 Agent 的机器摘要,包含 `billable`、请求 ID、幂等键、产物 URL、耗时、耗时拆分、路由、渠道、上游 host、脱敏请求头、重试和下一步动作;Agent JSON 失败时脚本会按幂等键做一次只读 Agent state 诊断补采样,补充 `agent_diagnostics_checked`、`agent_diagnostics_found`、`agent_diagnostics_unavailable_reason`、`agent_diagnostics_http_status`、`request_id`、渠道和上游 host。不要再先手查 SQLite、Docker logs 或上游后台。
|
| 24 |
- 新增 probe、diagnostics、路由健康或请求旅程能力时,先在服务端定义机器 API 契约,并通过 `GET /api/agent/capabilities`、`GET /api/agent/openapi.json` 或明确的 `/api/agent/diagnostics/*` 端点声明;Skill 脚本只做薄封装,不能复制页面 API、运行态 API 和 Agent API 的边界判断。
|
| 25 |
|
|
@@ -125,7 +125,7 @@ Authorization: Bearer <token>
|
|
| 125 |
- `scripts/diagnose-request.mjs`:按一个或多个页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断;支持读取批量 manifest 和 `--base-url`,不触发生图计费。
|
| 126 |
- `scripts/probe-upstream-image.mjs`:直接探测上游图片接口连通性。默认只检查 DNS、TLS 和 `/models`,必须添加 `--allow-billable` 才会真实调用 `/images/generations`。
|
| 127 |
|
| 128 |
-
生成、编辑和批量脚本的 dry-run 输出会包含 `verification_scope.mode=local_planning_only`,表示只验证了本地请求构造、参数归一化和静态路由规划;它不会读取远端 capabilities,不会验证远端鉴权、渠道容量或 manifest 写入。生成 dry-run 默认 `routing_guidance.transport=server_orchestrated`,表示真实请求只提交业务意图到服务端编排入口;显式 `--agent`、`--job`、`--page-sse` 才会显示对应诊断路径。批量 dry-run 还会包含 `guardrails`,提示真实执行要复用同一个 `--ordered-prefix`,固定尺寸任务是否建议加 `--dimension-check`。真实执行输出会包含 `summary`;成功摘要含 `ok=true`、`billable`、`request_id`、`idempotency_key`、`artifact_ids`、`content_urls`、`absolute_content_urls`、`image_dimensions`、`actual_dimensions`、`cached`、`elapsed_ms`、`server_elapsed_ms`、`elapsed_source`、`elapsed_breakdown`、`transport`、`endpoint`、`route_mode`、`image_backend`、`stream_mode`、`streaming_strategy`、`channel_request_mode`、`channel_request_mode_fallback_applied`、`route_decision`、`selected_channel_id`、`upstream_host` 和脱敏 `request_headers`。失败摘要含 `route_decision`、`transport_error_kind`、`retry_after_ms`、`cooldown_until`、`cooldown_target`、`retryable`、`dimension_check_failed`、`expected_dimensions`、`actual_dimensions`、`agent_diagnostics_checked`、`agent_diagnostics_found`、`agent_diagnostics_unavailable_reason`、`agent_diagnostics_http_status` 和 `next_action`;尺寸门禁失败时还会保留已生成产物的 `artifact_ids`、`content_urls`、`absolute_content_urls` 和 `image_dimensions`,便于人工审查。
|
| 129 |
所有生成、编辑、批量和探针脚本在 dry-run 或真实请求前都会校验尺寸参数。`gpt-image-2` 支持 `auto` 或任意正整数 `WIDTHxHEIGHT`;默认 OpenAI-compatible 上游的更严格尺寸边界由服务端 profile 或真实上游显式报错。非 `gpt-image-2` 模型只接受 `auto`、`1024x1024`、`1536x1024` 或 `1024x1536`。生成、页面编辑、批量页面 SSE 和上游探针默认请求 `output_format=webp`、`output_compression=100`;普通 Agent edit 不发送输出格式字段,输出格式固定为 Agent 契约。
|
| 130 |
|
| 131 |
如果当前上下文位于仓库根目录,管理员侧优先使用顶层命令:
|
|
|
|
| 19 |
- 诊断上游图片接口:优先运行 `scripts/probe-upstream-image.mjs`。
|
| 20 |
- 不要临时编写 Node/Python/shell 脚本、curl 命令或手写 fetch/FormData 来重复实现这些脚本已经覆盖的 API 调用。
|
| 21 |
- 只有在内置脚本缺少用户明确需要的能力时,才修改或扩展 `scripts/` 内的预置脚本,并同步补测试;不要在仓库外留下 ad hoc 调用脚本。
|
| 22 |
+
- 先用 dry-run、`--check-remote` 或 `--contract-check` 检查请求、路由、鉴权和服务声明的默认编排入口;只有用户明确允许真实计费时才加 `--allow-billable`。
|
| 23 |
- 真实调用成功或失败后,优先读取脚本输出的 `summary`。它是面向 Agent 的机器摘要,包含 `billable`、请求 ID、幂等键、产物 URL、耗时、耗时拆分、路由、渠道、上游 host、脱敏请求头、重试和下一步动作;Agent JSON 失败时脚本会按幂等键做一次只读 Agent state 诊断补采样,补充 `agent_diagnostics_checked`、`agent_diagnostics_found`、`agent_diagnostics_unavailable_reason`、`agent_diagnostics_http_status`、`request_id`、渠道和上游 host。不要再先手查 SQLite、Docker logs 或上游后台。
|
| 24 |
- 新增 probe、diagnostics、路由健康或请求旅程能力时,先在服务端定义机器 API 契约,并通过 `GET /api/agent/capabilities`、`GET /api/agent/openapi.json` 或明确的 `/api/agent/diagnostics/*` 端点声明;Skill 脚本只做薄封装,不能复制页面 API、运行态 API 和 Agent API 的边界判断。
|
| 25 |
|
|
|
|
| 125 |
- `scripts/diagnose-request.mjs`:按一个或多个页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断;支持读取批量 manifest 和 `--base-url`,不触发生图计费。
|
| 126 |
- `scripts/probe-upstream-image.mjs`:直接探测上游图片接口连通性。默认只检查 DNS、TLS 和 `/models`,必须添加 `--allow-billable` 才会真实调用 `/images/generations`。
|
| 127 |
|
| 128 |
+
生成、编辑和批量脚本的 dry-run 输出会包含 `verification_scope.mode=local_planning_only`,表示只验证了本地请求构造、参数归一化和静态路由规划;它不会读取远端 capabilities,不会验证远端鉴权、渠道容量或 manifest 写入。生成 dry-run 添加 `--check-remote` 后会只读查询 `/api/agent/capabilities` 和 `/api/runtime-capabilities`,输出 `verification_scope.mode=remote_contract_and_local_planning`,仍不会发送真实生图请求。生成 dry-run 默认 `routing_guidance.transport=server_orchestrated`,表示真实请求只提交业务意图到服务端编排入口;显式 `--agent`、`--job`、`--page-sse` 才会显示对应诊断路径。批量 dry-run 还会包含 `guardrails`,提示真实执行要复用同一个 `--ordered-prefix`,固定尺寸任务是否建议加 `--dimension-check`。真实执行输出会包含 `summary`;成功摘要含 `ok=true`、`billable`、`request_id`、`idempotency_key`、`artifact_ids`、`content_urls`、`absolute_content_urls`、`image_dimensions`、`actual_dimensions`、`cached`、`elapsed_ms`、`server_elapsed_ms`、`elapsed_source`、`elapsed_breakdown`、`transport`、`endpoint`、`route_mode`、`image_backend`、`stream_mode`、`streaming_strategy`、`channel_request_mode`、`channel_request_mode_fallback_applied`、`route_decision`、`selected_channel_id`、`upstream_host` 和脱敏 `request_headers`。失败摘要含 `route_decision`、`transport_error_kind`、`retry_after_ms`、`cooldown_until`、`cooldown_target`、`retryable`、`dimension_check_failed`、`expected_dimensions`、`actual_dimensions`、`agent_diagnostics_checked`、`agent_diagnostics_found`、`agent_diagnostics_unavailable_reason`、`agent_diagnostics_http_status` 和 `next_action`;尺寸门禁失败时还会保留已生成产物的 `artifact_ids`、`content_urls`、`absolute_content_urls` 和 `image_dimensions`,便于人工审查。
|
| 129 |
所有生成、编辑、批量和探针脚本在 dry-run 或真实请求前都会校验尺寸参数。`gpt-image-2` 支持 `auto` 或任意正整数 `WIDTHxHEIGHT`;默认 OpenAI-compatible 上游的更严格尺寸边界由服务端 profile 或真实上游显式报错。非 `gpt-image-2` 模型只接受 `auto`、`1024x1024`、`1536x1024` 或 `1024x1536`。生成、页面编辑、批量页面 SSE 和上游探针默认请求 `output_format=webp`、`output_compression=100`;普通 Agent edit 不发送输出格式字段,输出格式固定为 Agent 契约。
|
| 130 |
|
| 131 |
如果当前上下文位于仓库根目录,管理员侧优先使用顶层命令:
|
skills/gpt-image-playground-agent/references/api.md
CHANGED
|
@@ -29,9 +29,9 @@ Agent API 是给自动化客户端使用的机器接口,不是自治 Agent 平
|
|
| 29 |
- `scripts/diagnose-request.mjs`:按页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断,支持 `--base-url` 固定目标服务。
|
| 30 |
- `scripts/probe-upstream-image.mjs`:上游图片接口连通性探针。
|
| 31 |
|
| 32 |
-
生成、编辑和批量脚本默认只做 dry-run,不触发真实生图或编辑。dry-run 输出的 `verification_scope.mode=local_planning_only` 表示只完成本地请求构造、参数归一化和静态路由规划;它不会读取远端 capabilities,不会验证远端鉴权、渠道容量或 manifest 写入。必须显式添加 `--allow-billable` 才会调用真实端点。generate 默认提交到 `/api/agent/image-requests` 服务端编排入口;`--agent`、`--job`、`--page-sse` 才会显式改用 `/api/agent/images/generate`、`/api/agent/jobs/images/generate` 或页面端 `/api/images` SSE。
|
| 33 |
上游探针默认只检查 DNS、TLS 和 `/models`,必须显式添加 `--allow-billable` 才会调用上游 `/images/generations`。
|
| 34 |
-
脚本支持 `GPT_IMAGE_AGENT_CONTRACT_CHECK=1` 或 `--contract-check` 做只读契约检查,会覆盖服务声明的默认编排入口,不触发真实生图或编辑。
|
| 35 |
位于仓库根目录且是首次配置、换机器、服务地址不确定或 token 不确定时,先运行 `npm run first-run`。它只读、非计费、不写 env 文件,默认输出中文摘要,并报告 `service_base_url_source`、`interactive_confirmation_required`、服务可达性、当前进程鉴权、页面 SSE 鉴权和下一步动作。
|
| 36 |
自动化消费时使用 `npm run first-run -- --json`。
|
| 37 |
Agent 端点鉴权以 capabilities 的 `auth.schemes` 为准。配置 `AGENT_API_TOKEN` 时只接受 Bearer token;只有未配置 `AGENT_API_TOKEN` 且配置了 `APP_PASSWORD` 时,Agent 端点才接受访问码哈希 `GPT_IMAGE_APP_PASSWORD_HASH`。页面端 `/api/images` SSE 另看 `agent_streaming.page_sse.auth`;当其声明 `required=true` 时,form-data 必须包含 `passwordHash`。`GPT_IMAGE_AGENT_TOKEN` 不能替代页面 SSE 表单鉴权。
|
|
|
|
| 29 |
- `scripts/diagnose-request.mjs`:按页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断,支持 `--base-url` 固定目标服务。
|
| 30 |
- `scripts/probe-upstream-image.mjs`:上游图片接口连通性探针。
|
| 31 |
|
| 32 |
+
生成、编辑和批量脚本默认只做 dry-run,不触发真实生图或编辑。dry-run 输出的 `verification_scope.mode=local_planning_only` 表示只完成本地请求构造、参数归一化和静态路由规划;它不会读取远端 capabilities,不会验证远端鉴权、渠道容量或 manifest 写入。generate 可添加 `--check-remote` 做只读远端检查,输出 `verification_scope.mode=remote_contract_and_local_planning`,仅访问 `/api/agent/capabilities` 和 `/api/runtime-capabilities`,不会发送真实生图请求。必须显式添加 `--allow-billable` 才会调用真实端点。generate 默认提交到 `/api/agent/image-requests` 服务端编排入口;`--agent`、`--job`、`--page-sse` 才会显式改用 `/api/agent/images/generate`、`/api/agent/jobs/images/generate` 或页面端 `/api/images` SSE。
|
| 33 |
上游探针默认只检查 DNS、TLS 和 `/models`,必须显式添加 `--allow-billable` 才会调用上游 `/images/generations`。
|
| 34 |
+
脚本支持 `GPT_IMAGE_AGENT_CONTRACT_CHECK=1` 或 `--contract-check` 做只读契约检查,会覆盖服务声明的默认编排入口和页面 SSE 边界,不触发真实生图或编辑。
|
| 35 |
位于仓库根目录且是首次配置、换机器、服务地址不确定或 token 不确定时,先运行 `npm run first-run`。它只读、非计费、不写 env 文件,默认输出中文摘要,并报告 `service_base_url_source`、`interactive_confirmation_required`、服务可达性、当前进程鉴权、页面 SSE 鉴权和下一步动作。
|
| 36 |
自动化消费时使用 `npm run first-run -- --json`。
|
| 37 |
Agent 端点鉴权以 capabilities 的 `auth.schemes` 为准。配置 `AGENT_API_TOKEN` 时只接受 Bearer token;只有未配置 `AGENT_API_TOKEN` 且配置了 `APP_PASSWORD` 时,Agent 端点才接受访问码哈希 `GPT_IMAGE_APP_PASSWORD_HASH`。页面端 `/api/images` SSE 另看 `agent_streaming.page_sse.auth`;当其声明 `required=true` 时,form-data 必须包含 `passwordHash`。`GPT_IMAGE_AGENT_TOKEN` 不能替代页面 SSE 表单鉴权。
|
skills/gpt-image-playground-agent/scripts/batch-images.mjs
CHANGED
|
@@ -173,7 +173,7 @@ try {
|
|
| 173 |
process.exit(2);
|
| 174 |
}
|
| 175 |
|
| 176 |
-
const manifestPath = options
|
| 177 |
let planned;
|
| 178 |
try {
|
| 179 |
planned = tasks.map((task, index) => normalizeTask(task, index, options));
|
|
@@ -220,6 +220,11 @@ if (!options.allowBillable || options.dryRun) {
|
|
| 220 |
process.exit(0);
|
| 221 |
}
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
try {
|
| 224 |
const capacityFeedback = await resolveBatchCapacityFeedback();
|
| 225 |
const completed = options.resume ? readCompletedManifestKeys(manifestPath) : new Set();
|
|
@@ -275,7 +280,7 @@ function parseArgs(argv) {
|
|
| 275 |
else if (arg === '--resume') parsed.resume = true;
|
| 276 |
else if (arg === '--dimension-check') parsed.dimensionCheck = true;
|
| 277 |
else if (arg === '--help' || arg === '-h') parsed.help = true;
|
| 278 |
-
else if (arg === '--input') parsed.input = readOptionValue(argv, (index += 1), arg);
|
| 279 |
else if (arg === '--manifest') parsed.manifest = readOptionValue(argv, (index += 1), arg);
|
| 280 |
else if (arg === '--ordered-prefix') parsed.orderedPrefix = readOptionValue(argv, (index += 1), arg);
|
| 281 |
else if (arg === '--timeout-ms') parsed.timeoutMs = readOptionValue(argv, (index += 1), arg);
|
|
@@ -286,12 +291,22 @@ function parseArgs(argv) {
|
|
| 286 |
}
|
| 287 |
else if (arg === '--concurrency') parsed.concurrency = readOptionValue(argv, (index += 1), arg);
|
| 288 |
else if (arg.startsWith('--')) throw new Error(`未知参数:${arg}`);
|
| 289 |
-
else if (!parsed.input) parsed.input = arg;
|
| 290 |
else throw new Error(`未知位置参数:${arg}`);
|
| 291 |
}
|
| 292 |
return parsed;
|
| 293 |
}
|
| 294 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
function buildDryRunVerificationScope() {
|
| 296 |
return {
|
| 297 |
mode: 'local_planning_only',
|
|
|
|
| 173 |
process.exit(2);
|
| 174 |
}
|
| 175 |
|
| 176 |
+
const manifestPath = resolveManifestPath(options);
|
| 177 |
let planned;
|
| 178 |
try {
|
| 179 |
planned = tasks.map((task, index) => normalizeTask(task, index, options));
|
|
|
|
| 220 |
process.exit(0);
|
| 221 |
}
|
| 222 |
|
| 223 |
+
if (!manifestPath) {
|
| 224 |
+
console.error('--input - 真实执行必须显式设置 --manifest,避免无法续跑或审计。');
|
| 225 |
+
process.exit(2);
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
try {
|
| 229 |
const capacityFeedback = await resolveBatchCapacityFeedback();
|
| 230 |
const completed = options.resume ? readCompletedManifestKeys(manifestPath) : new Set();
|
|
|
|
| 280 |
else if (arg === '--resume') parsed.resume = true;
|
| 281 |
else if (arg === '--dimension-check') parsed.dimensionCheck = true;
|
| 282 |
else if (arg === '--help' || arg === '-h') parsed.help = true;
|
| 283 |
+
else if (arg === '--input') parsed.input = normalizeInputPath(readOptionValue(argv, (index += 1), arg));
|
| 284 |
else if (arg === '--manifest') parsed.manifest = readOptionValue(argv, (index += 1), arg);
|
| 285 |
else if (arg === '--ordered-prefix') parsed.orderedPrefix = readOptionValue(argv, (index += 1), arg);
|
| 286 |
else if (arg === '--timeout-ms') parsed.timeoutMs = readOptionValue(argv, (index += 1), arg);
|
|
|
|
| 291 |
}
|
| 292 |
else if (arg === '--concurrency') parsed.concurrency = readOptionValue(argv, (index += 1), arg);
|
| 293 |
else if (arg.startsWith('--')) throw new Error(`未知参数:${arg}`);
|
| 294 |
+
else if (!parsed.input) parsed.input = normalizeInputPath(arg);
|
| 295 |
else throw new Error(`未知位置参数:${arg}`);
|
| 296 |
}
|
| 297 |
return parsed;
|
| 298 |
}
|
| 299 |
|
| 300 |
+
function normalizeInputPath(value) {
|
| 301 |
+
return value === '-' ? '/dev/stdin' : value;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
function resolveManifestPath(parsed) {
|
| 305 |
+
if (parsed.manifest) return parsed.manifest;
|
| 306 |
+
if (parsed.input === '/dev/stdin') return null;
|
| 307 |
+
return `${parsed.input}.manifest.jsonl`;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
function buildDryRunVerificationScope() {
|
| 311 |
return {
|
| 312 |
mode: 'local_planning_only',
|
skills/gpt-image-playground-agent/scripts/diagnose-request.mjs
CHANGED
|
@@ -52,6 +52,8 @@ try {
|
|
| 52 |
service_base_url: baseUrl,
|
| 53 |
service_base_url_source: baseUrlInfo.source,
|
| 54 |
interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
|
|
|
|
|
|
|
| 55 |
page_request_count: requests.length,
|
| 56 |
agent_request_count: agentRequests.length,
|
| 57 |
request_count: requests.length,
|
|
@@ -62,6 +64,7 @@ try {
|
|
| 62 |
if (requests.length === 1) {
|
| 63 |
Object.assign(body, {
|
| 64 |
client_request_id: requests[0].client_request_id,
|
|
|
|
| 65 |
feedback: requests[0].feedback,
|
| 66 |
diagnostics: requests[0].diagnostics,
|
| 67 |
...(requests[0].diagnostics_note ? { diagnostics_note: requests[0].diagnostics_note } : {})
|
|
@@ -354,8 +357,10 @@ function readDiagnosticsClientRequestId(value) {
|
|
| 354 |
|
| 355 |
function buildDiagnosedRequest({ clientRequestId, feedback, diagnostics, diagnosticsCapabilities, diagnosticsRetention }) {
|
| 356 |
const diagnosticsNote = buildDiagnosticsNoMatchNote(diagnostics, diagnosticsCapabilities, diagnosticsRetention);
|
|
|
|
| 357 |
return {
|
| 358 |
client_request_id: clientRequestId,
|
|
|
|
| 359 |
feedback,
|
| 360 |
diagnostics,
|
| 361 |
...(diagnosticsNote ? { diagnostics_note: diagnosticsNote } : {})
|
|
|
|
| 52 |
service_base_url: baseUrl,
|
| 53 |
service_base_url_source: baseUrlInfo.source,
|
| 54 |
interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
|
| 55 |
+
page_request_query_count: clientRequestIds.length,
|
| 56 |
+
page_request_found_count: requests.filter((request) => request.found).length,
|
| 57 |
page_request_count: requests.length,
|
| 58 |
agent_request_count: agentRequests.length,
|
| 59 |
request_count: requests.length,
|
|
|
|
| 64 |
if (requests.length === 1) {
|
| 65 |
Object.assign(body, {
|
| 66 |
client_request_id: requests[0].client_request_id,
|
| 67 |
+
found: requests[0].found,
|
| 68 |
feedback: requests[0].feedback,
|
| 69 |
diagnostics: requests[0].diagnostics,
|
| 70 |
...(requests[0].diagnostics_note ? { diagnostics_note: requests[0].diagnostics_note } : {})
|
|
|
|
| 357 |
|
| 358 |
function buildDiagnosedRequest({ clientRequestId, feedback, diagnostics, diagnosticsCapabilities, diagnosticsRetention }) {
|
| 359 |
const diagnosticsNote = buildDiagnosticsNoMatchNote(diagnostics, diagnosticsCapabilities, diagnosticsRetention);
|
| 360 |
+
const found = Boolean(feedback || (diagnostics && diagnostics.matched_log_count > 0));
|
| 361 |
return {
|
| 362 |
client_request_id: clientRequestId,
|
| 363 |
+
found,
|
| 364 |
feedback,
|
| 365 |
diagnostics,
|
| 366 |
...(diagnosticsNote ? { diagnostics_note: diagnosticsNote } : {})
|
skills/gpt-image-playground-agent/scripts/generate-image.mjs
CHANGED
|
@@ -134,13 +134,20 @@ try {
|
|
| 134 |
}
|
| 135 |
|
| 136 |
if (isNonBillableDryRun(options, contractCheck)) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
console.log(
|
| 138 |
JSON.stringify(
|
| 139 |
{
|
| 140 |
ok: true,
|
| 141 |
billable: false,
|
| 142 |
dry_run: true,
|
| 143 |
-
verification_scope: buildDryRunVerificationScope(),
|
| 144 |
endpoint: dryRunEndpoint(requestBody, options.routeMode),
|
| 145 |
route_mode: options.routeMode,
|
| 146 |
routing_guidance: buildGenerateRoutingGuidance(requestBody, options.routeMode),
|
|
@@ -256,6 +263,7 @@ function parseArgs(argv) {
|
|
| 256 |
routeMode: 'auto',
|
| 257 |
dryRun: false,
|
| 258 |
allowBillable: false,
|
|
|
|
| 259 |
help: false,
|
| 260 |
promptParts: []
|
| 261 |
};
|
|
@@ -289,6 +297,7 @@ function parseArgs(argv) {
|
|
| 289 |
else if (arg === '--base-url') parsed.baseUrl = readOptionValue(expandedArgv, (index += 1), arg);
|
| 290 |
else if (arg === '--prompt-file') parsed.promptFile = readOptionValue(expandedArgv, (index += 1), arg);
|
| 291 |
else if (arg === '--idempotency-key') parsed.idempotencyKey = readOptionValue(expandedArgv, (index += 1), arg);
|
|
|
|
| 292 |
else if (arg.startsWith('--')) throw new Error(`未知参数:${arg}`);
|
| 293 |
else parsed.promptParts.push(arg);
|
| 294 |
}
|
|
@@ -497,20 +506,75 @@ function enrichImageUrls(result) {
|
|
| 497 |
};
|
| 498 |
}
|
| 499 |
|
| 500 |
-
function buildDryRunVerificationScope() {
|
| 501 |
return {
|
| 502 |
-
mode: 'local_planning_only',
|
| 503 |
service_base_url: baseUrl,
|
| 504 |
service_base_url_source: baseUrlInfo.source,
|
| 505 |
interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
remote_capabilities_verified: false,
|
| 507 |
runtime_capacity_verified: false,
|
| 508 |
auth_verified: false,
|
| 509 |
-
|
| 510 |
-
|
| 511 |
};
|
| 512 |
}
|
| 513 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
function dryRunEndpoint(body, routeMode) {
|
| 515 |
if (routeMode === 'auto' && shouldPreferServerOrchestration(body)) {
|
| 516 |
return `${baseUrl}${AGENT_ENDPOINTS.create_image_request}`;
|
|
@@ -549,9 +613,11 @@ function buildGenerateRoutingGuidance(body, routeMode) {
|
|
| 549 |
(routeMode !== 'agent' && (hasPageOnlyGenerateOptions(body) || isLargeGenerate(body)))) &&
|
| 550 |
isPageSseAllowed(body)
|
| 551 |
) {
|
| 552 |
-
const reason =
|
|
|
|
|
|
|
| 553 |
? 'Responses/GPT2Image-compatible generate options require the page form-data SSE endpoint; Agent JSON generate does not accept those fields.'
|
| 554 |
-
: 'Generate requests with
|
| 555 |
return {
|
| 556 |
recommended_endpoint: PAGE_SSE_ENDPOINT,
|
| 557 |
transport: 'page_sse',
|
|
@@ -724,7 +790,7 @@ function assertPageSseReady(capabilitiesValue) {
|
|
| 724 |
if (!supportsPageSse(capabilitiesValue)) {
|
| 725 |
throw createScriptError(
|
| 726 |
'page_sse_unavailable',
|
| 727 |
-
'
|
| 728 |
);
|
| 729 |
}
|
| 730 |
if (pageSse?.auth?.required === true && !passwordHash) {
|
|
@@ -1278,6 +1344,19 @@ async function runContractCheck(capabilitiesValue) {
|
|
| 1278 |
checks.push(await checkContractEndpoint(AGENT_ENDPOINTS.create_generate_job));
|
| 1279 |
}
|
| 1280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1281 |
console.log(JSON.stringify({ ok: true, billable: false, checks }, null, 2));
|
| 1282 |
}
|
| 1283 |
|
|
@@ -1298,6 +1377,35 @@ async function checkContractEndpoint(endpoint) {
|
|
| 1298 |
return { endpoint, status: response.status, error_code: result.error.code };
|
| 1299 |
}
|
| 1300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1301 |
function readOrchestrationEndpoint(capabilitiesValue) {
|
| 1302 |
const endpoint = capabilitiesValue?.orchestration?.endpoint;
|
| 1303 |
if (typeof endpoint !== 'string' || !endpoint.startsWith('/')) {
|
|
@@ -1498,7 +1606,7 @@ function printUsage() {
|
|
| 1498 |
console.error('用法:generate-image.mjs [options] <prompt>');
|
| 1499 |
console.error('默认只输出 dry-run;添加 --allow-billable 才会真实生图。');
|
| 1500 |
console.error(
|
| 1501 |
-
'常用参数:--model --size --quality --n --format --output-compression --response-mode --image-backend --responses-model --gpt-model --thinking --prompt-optimization --force-web --stream-mode --streaming-strategy --partial-images --sse-log --timeout-ms --base-url --prompt-file --idempotency-key --page-sse --agent --job --no-job(兼容别名)'
|
| 1502 |
);
|
| 1503 |
console.error(
|
| 1504 |
'契约检查:GPT_IMAGE_AGENT_CONTRACT_CHECK=1 generate-image.mjs 或 generate-image.mjs --contract-check'
|
|
|
|
| 134 |
}
|
| 135 |
|
| 136 |
if (isNonBillableDryRun(options, contractCheck)) {
|
| 137 |
+
let remoteCheck;
|
| 138 |
+
try {
|
| 139 |
+
remoteCheck = options.checkRemote ? await runRemotePlanningCheck() : buildSkippedRemotePlanningCheck();
|
| 140 |
+
} catch (error) {
|
| 141 |
+
console.error(errorMessage(error));
|
| 142 |
+
process.exit(1);
|
| 143 |
+
}
|
| 144 |
console.log(
|
| 145 |
JSON.stringify(
|
| 146 |
{
|
| 147 |
ok: true,
|
| 148 |
billable: false,
|
| 149 |
dry_run: true,
|
| 150 |
+
verification_scope: buildDryRunVerificationScope(remoteCheck),
|
| 151 |
endpoint: dryRunEndpoint(requestBody, options.routeMode),
|
| 152 |
route_mode: options.routeMode,
|
| 153 |
routing_guidance: buildGenerateRoutingGuidance(requestBody, options.routeMode),
|
|
|
|
| 263 |
routeMode: 'auto',
|
| 264 |
dryRun: false,
|
| 265 |
allowBillable: false,
|
| 266 |
+
checkRemote: false,
|
| 267 |
help: false,
|
| 268 |
promptParts: []
|
| 269 |
};
|
|
|
|
| 297 |
else if (arg === '--base-url') parsed.baseUrl = readOptionValue(expandedArgv, (index += 1), arg);
|
| 298 |
else if (arg === '--prompt-file') parsed.promptFile = readOptionValue(expandedArgv, (index += 1), arg);
|
| 299 |
else if (arg === '--idempotency-key') parsed.idempotencyKey = readOptionValue(expandedArgv, (index += 1), arg);
|
| 300 |
+
else if (arg === '--check-remote') parsed.checkRemote = true;
|
| 301 |
else if (arg.startsWith('--')) throw new Error(`未知参数:${arg}`);
|
| 302 |
else parsed.promptParts.push(arg);
|
| 303 |
}
|
|
|
|
| 506 |
};
|
| 507 |
}
|
| 508 |
|
| 509 |
+
function buildDryRunVerificationScope(remoteCheck = buildSkippedRemotePlanningCheck()) {
|
| 510 |
return {
|
| 511 |
+
mode: remoteCheck.remote_capabilities_verified ? 'remote_contract_and_local_planning' : 'local_planning_only',
|
| 512 |
service_base_url: baseUrl,
|
| 513 |
service_base_url_source: baseUrlInfo.source,
|
| 514 |
interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
|
| 515 |
+
remote_capabilities_verified: remoteCheck.remote_capabilities_verified,
|
| 516 |
+
runtime_capacity_verified: remoteCheck.runtime_capacity_verified,
|
| 517 |
+
auth_verified: remoteCheck.auth_verified,
|
| 518 |
+
billable_request_sent: false,
|
| 519 |
+
remote_check: {
|
| 520 |
+
capabilities: remoteCheck.capabilities,
|
| 521 |
+
runtime: remoteCheck.runtime
|
| 522 |
+
},
|
| 523 |
+
note: remoteCheck.remote_capabilities_verified
|
| 524 |
+
? 'Dry-run validated local request construction and read non-billable remote capabilities/runtime state; it did not send a billable image request.'
|
| 525 |
+
: 'Dry-run validates local request construction and routing guidance only; run --check-remote, --contract-check or --allow-billable to verify the remote service.'
|
| 526 |
+
};
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
async function runRemotePlanningCheck() {
|
| 530 |
+
const capabilities = await readCapabilities();
|
| 531 |
+
const runtime = await readRuntimeCapabilities();
|
| 532 |
+
return {
|
| 533 |
+
remote_capabilities_verified: true,
|
| 534 |
+
runtime_capacity_verified: true,
|
| 535 |
+
auth_verified: true,
|
| 536 |
+
capabilities: {
|
| 537 |
+
endpoint: AGENT_ENDPOINTS.capabilities,
|
| 538 |
+
orchestration_supported: capabilities?.orchestration?.supported === true,
|
| 539 |
+
orchestration_endpoint: capabilities?.orchestration?.endpoint || null,
|
| 540 |
+
page_sse_supported: capabilities?.agent_streaming?.page_sse?.supported === true,
|
| 541 |
+
request_modes: readRequestModeList(capabilities?.supported?.request_modes)
|
| 542 |
+
},
|
| 543 |
+
runtime: {
|
| 544 |
+
endpoint: '/api/runtime-capabilities',
|
| 545 |
+
streaming_batch_enabled: runtime?.streamingBatch?.enabled === true,
|
| 546 |
+
recommended_concurrency: runtime?.streamingBatch?.recommendedConcurrency ?? null,
|
| 547 |
+
effective_request_modes: readRequestModeList(runtime?.channelRouting?.effectiveRequestModes)
|
| 548 |
+
}
|
| 549 |
+
};
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
async function readRuntimeCapabilities() {
|
| 553 |
+
const { response, result, text } = await fetchJson(`${baseUrl}/api/runtime-capabilities`, {
|
| 554 |
+
headers: authHeaders(),
|
| 555 |
+
timeoutMs
|
| 556 |
+
});
|
| 557 |
+
if (!response.ok) {
|
| 558 |
+
throw new Error(`runtime-capabilities 请求失败,状态码 ${response.status}:${text}`);
|
| 559 |
+
}
|
| 560 |
+
return result;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
function buildSkippedRemotePlanningCheck() {
|
| 564 |
+
return {
|
| 565 |
remote_capabilities_verified: false,
|
| 566 |
runtime_capacity_verified: false,
|
| 567 |
auth_verified: false,
|
| 568 |
+
capabilities: null,
|
| 569 |
+
runtime: null
|
| 570 |
};
|
| 571 |
}
|
| 572 |
|
| 573 |
+
function readRequestModeList(value) {
|
| 574 |
+
if (!Array.isArray(value)) return [];
|
| 575 |
+
return value.filter((item) => typeof item === 'string');
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
function dryRunEndpoint(body, routeMode) {
|
| 579 |
if (routeMode === 'auto' && shouldPreferServerOrchestration(body)) {
|
| 580 |
return `${baseUrl}${AGENT_ENDPOINTS.create_image_request}`;
|
|
|
|
| 613 |
(routeMode !== 'agent' && (hasPageOnlyGenerateOptions(body) || isLargeGenerate(body)))) &&
|
| 614 |
isPageSseAllowed(body)
|
| 615 |
) {
|
| 616 |
+
const reason = routeMode === 'page_sse'
|
| 617 |
+
? 'Explicit --page-sse requests use the page form-data SSE endpoint.'
|
| 618 |
+
: hasPageOnlyGenerateOptions(body)
|
| 619 |
? 'Responses/GPT2Image-compatible generate options require the page form-data SSE endpoint; Agent JSON generate does not accept those fields.'
|
| 620 |
+
: 'Generate requests with page-workbench-only requirements should use the page form-data SSE endpoint; if the stream fails, diagnose first and rerun manually with Agent JSON.';
|
| 621 |
return {
|
| 622 |
recommended_endpoint: PAGE_SSE_ENDPOINT,
|
| 623 |
transport: 'page_sse',
|
|
|
|
| 790 |
if (!supportsPageSse(capabilitiesValue)) {
|
| 791 |
throw createScriptError(
|
| 792 |
'page_sse_unavailable',
|
| 793 |
+
'显式页面 SSE 或页面专属字段需要 agent_streaming.page_sse.supported=true;capabilities 未声明时不能静默降级到 Agent JSON。'
|
| 794 |
);
|
| 795 |
}
|
| 796 |
if (pageSse?.auth?.required === true && !passwordHash) {
|
|
|
|
| 1344 |
checks.push(await checkContractEndpoint(AGENT_ENDPOINTS.create_generate_job));
|
| 1345 |
}
|
| 1346 |
|
| 1347 |
+
if (supportsPageSse(capabilitiesValue)) {
|
| 1348 |
+
const pageSse = capabilitiesValue?.agent_streaming?.page_sse;
|
| 1349 |
+
if (pageSse?.auth?.required === true && !passwordHash) {
|
| 1350 |
+
checks.push({
|
| 1351 |
+
endpoint: PAGE_SSE_ENDPOINT,
|
| 1352 |
+
skipped: true,
|
| 1353 |
+
reason: '页面 SSE 需要 passwordHash;contract-check 在未提供 GPT_IMAGE_APP_PASSWORD_HASH 时跳过。'
|
| 1354 |
+
});
|
| 1355 |
+
} else {
|
| 1356 |
+
checks.push(await checkPageSseContract());
|
| 1357 |
+
}
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
console.log(JSON.stringify({ ok: true, billable: false, checks }, null, 2));
|
| 1361 |
}
|
| 1362 |
|
|
|
|
| 1377 |
return { endpoint, status: response.status, error_code: result.error.code };
|
| 1378 |
}
|
| 1379 |
|
| 1380 |
+
async function checkPageSseContract() {
|
| 1381 |
+
const formData = new FormData();
|
| 1382 |
+
formData.append('mode', 'generate');
|
| 1383 |
+
formData.append('prompt', 'contract check');
|
| 1384 |
+
formData.append('model', requestBody.model);
|
| 1385 |
+
formData.append('n', String(requestBody.n));
|
| 1386 |
+
formData.append('size', requestBody.size);
|
| 1387 |
+
formData.append('quality', requestBody.quality);
|
| 1388 |
+
formData.append('output_format', requestBody.output_format);
|
| 1389 |
+
formData.append('response_mode', requestBody.response_mode);
|
| 1390 |
+
formData.append('stream', 'true');
|
| 1391 |
+
formData.append('clientRequestId', 'x'.repeat(pageSseClientRequestIdMaxLength + 1));
|
| 1392 |
+
if (passwordHash) formData.append('passwordHash', passwordHash);
|
| 1393 |
+
const { response, text } = await fetchJson(`${baseUrl}${PAGE_SSE_ENDPOINT}`, {
|
| 1394 |
+
method: 'POST',
|
| 1395 |
+
body: formData,
|
| 1396 |
+
timeoutMs
|
| 1397 |
+
});
|
| 1398 |
+
if (response.status !== 400 || !text.includes('clientRequestId')) {
|
| 1399 |
+
console.error(JSON.stringify({ ok: false, billable: false, endpoint: PAGE_SSE_ENDPOINT, status: response.status, text }, null, 2));
|
| 1400 |
+
process.exit(1);
|
| 1401 |
+
}
|
| 1402 |
+
return {
|
| 1403 |
+
endpoint: PAGE_SSE_ENDPOINT,
|
| 1404 |
+
status: response.status,
|
| 1405 |
+
error_code: 'page_sse_client_request_id_too_long'
|
| 1406 |
+
};
|
| 1407 |
+
}
|
| 1408 |
+
|
| 1409 |
function readOrchestrationEndpoint(capabilitiesValue) {
|
| 1410 |
const endpoint = capabilitiesValue?.orchestration?.endpoint;
|
| 1411 |
if (typeof endpoint !== 'string' || !endpoint.startsWith('/')) {
|
|
|
|
| 1606 |
console.error('用法:generate-image.mjs [options] <prompt>');
|
| 1607 |
console.error('默认只输出 dry-run;添加 --allow-billable 才会真实生图。');
|
| 1608 |
console.error(
|
| 1609 |
+
'常用参数:--model --size --quality --n --format --output-compression --response-mode --image-backend --responses-model --gpt-model --thinking --prompt-optimization --force-web --stream-mode --streaming-strategy --partial-images --sse-log --timeout-ms --base-url --prompt-file --idempotency-key --check-remote --page-sse --agent --job --no-job(兼容别名)'
|
| 1610 |
);
|
| 1611 |
console.error(
|
| 1612 |
'契约检查:GPT_IMAGE_AGENT_CONTRACT_CHECK=1 generate-image.mjs 或 generate-image.mjs --contract-check'
|
src/app/api/deploy-marker/route.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { NextResponse } from 'next/server';
|
| 2 |
|
| 3 |
-
const deployMarker = {"schema_version":1,"local_sha":"
|
| 4 |
|
| 5 |
export const dynamic = 'force-dynamic';
|
| 6 |
|
|
|
|
| 1 |
import { NextResponse } from 'next/server';
|
| 2 |
|
| 3 |
+
const deployMarker = {"schema_version":1,"local_sha":"7af3d19e476e03dfd25eef49672c982b46c64be8","created_at":"2026-06-26T01:06:33.777Z","deploy_id":"46b8343b-982b-4f3b-8526-ca1feabfd227"} as const;
|
| 4 |
|
| 5 |
export const dynamic = 'force-dynamic';
|
| 6 |
|
src/lib/agent-api-contracts.test.ts
CHANGED
|
@@ -439,7 +439,7 @@ describe('buildAgentCapabilities', () => {
|
|
| 439 |
});
|
| 440 |
assert.equal(
|
| 441 |
capabilities.agent_streaming.page_sse.agent_usage,
|
| 442 |
-
'
|
| 443 |
);
|
| 444 |
assert.deepEqual(capabilities.upstream_request_headers.default, {
|
| 445 |
user_agent_effective: 'gpt-image-playground/2.1.0',
|
|
|
|
| 439 |
});
|
| 440 |
assert.equal(
|
| 441 |
capabilities.agent_streaming.page_sse.agent_usage,
|
| 442 |
+
'explicit_for_generate_recommended_for_page_only_fields_high_resolution_edit_and_complex_batch'
|
| 443 |
);
|
| 444 |
assert.deepEqual(capabilities.upstream_request_headers.default, {
|
| 445 |
user_agent_effective: 'gpt-image-playground/2.1.0',
|
src/lib/agent-api-contracts.ts
CHANGED
|
@@ -334,7 +334,7 @@ export type AgentCapabilities = {
|
|
| 334 |
source_header: 'Idempotency-Key';
|
| 335 |
max_length: number;
|
| 336 |
};
|
| 337 |
-
agent_usage: '
|
| 338 |
};
|
| 339 |
};
|
| 340 |
orchestration: {
|
|
@@ -1013,7 +1013,7 @@ export function buildAgentCapabilities(env: Record<string, string | undefined>):
|
|
| 1013 |
source_header: 'Idempotency-Key',
|
| 1014 |
max_length: PAGE_SSE_CLIENT_REQUEST_ID_MAX_LENGTH
|
| 1015 |
},
|
| 1016 |
-
agent_usage: '
|
| 1017 |
}
|
| 1018 |
},
|
| 1019 |
orchestration: {
|
|
@@ -1050,7 +1050,7 @@ export function buildAgentCapabilities(env: Record<string, string | undefined>):
|
|
| 1050 |
requires_new_idempotency_key_on_retry: true,
|
| 1051 |
no_automatic_fallback: true
|
| 1052 |
},
|
| 1053 |
-
reason: 'High-resolution edit
|
| 1054 |
},
|
| 1055 |
complex_ui_batch: {
|
| 1056 |
when: ['operation=generate_or_edit', 'complex_ui=true', 'batch=true'],
|
|
|
|
| 334 |
source_header: 'Idempotency-Key';
|
| 335 |
max_length: number;
|
| 336 |
};
|
| 337 |
+
agent_usage: 'explicit_for_generate_recommended_for_page_only_fields_high_resolution_edit_and_complex_batch';
|
| 338 |
};
|
| 339 |
};
|
| 340 |
orchestration: {
|
|
|
|
| 1013 |
source_header: 'Idempotency-Key',
|
| 1014 |
max_length: PAGE_SSE_CLIENT_REQUEST_ID_MAX_LENGTH
|
| 1015 |
},
|
| 1016 |
+
agent_usage: 'explicit_for_generate_recommended_for_page_only_fields_high_resolution_edit_and_complex_batch'
|
| 1017 |
}
|
| 1018 |
},
|
| 1019 |
orchestration: {
|
|
|
|
| 1050 |
requires_new_idempotency_key_on_retry: true,
|
| 1051 |
no_automatic_fallback: true
|
| 1052 |
},
|
| 1053 |
+
reason: 'High-resolution edit uses the page form-data SSE endpoint because Agent edit has a fixed output contract; if streaming has issues, diagnose first and explicitly fall back to Agent edit.'
|
| 1054 |
},
|
| 1055 |
complex_ui_batch: {
|
| 1056 |
when: ['operation=generate_or_edit', 'complex_ui=true', 'batch=true'],
|