Spaces:
Running
Running
Deploy 3a8b0c7 to Docker Space
Browse filesSource: MisonL/gpt-image-playground-customer@3a8b0c75f35f7fb9ad0e6b81cae55bd49ca2572a
- .env.example +5 -1
- README.md +29 -95
- scripts/agent-doctor.mjs +39 -0
- scripts/agent-skill-scripts.test.mjs +558 -5
- scripts/command-center.test.mjs +31 -0
- scripts/first-run.mjs +23 -2
- skills/gpt-image-playground-agent/SKILL.md +7 -5
- skills/gpt-image-playground-agent/references/api.md +11 -8
- skills/gpt-image-playground-agent/scripts/batch-images.mjs +102 -7
- skills/gpt-image-playground-agent/scripts/edit-image.mjs +21 -10
- skills/gpt-image-playground-agent/scripts/generate-image.mjs +18 -1
- skills/gpt-image-playground-agent/scripts/lib/agent-diagnostics-summary.mjs +204 -0
- skills/gpt-image-playground-agent/scripts/lib/script-summary.mjs +48 -4
- src/app/api/agent/images/edit/route.ts +1 -1
- src/app/api/agent/images/generate/route.ts +1 -1
.env.example
CHANGED
|
@@ -96,9 +96,13 @@ OPENAI_API_BASE_URL=
|
|
| 96 |
# 要使用 Responses 后端,需要请求显式传入 image_backend=responses-image-generation,
|
| 97 |
# 或把 IMAGE_GENERATION_BACKEND 设为 responses-image-generation / responses;
|
| 98 |
# 两种方式都需要单独配置 /responses 顶层模型。
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
# 表单 model 只作为 image_generation 工具模型,不会复用为 /responses 顶层模型。
|
| 100 |
# ENABLE_RESPONSES_IMAGE_BACKEND=true
|
| 101 |
-
# OPENAI_RESPONSES_API_MODEL=
|
| 102 |
|
| 103 |
# 可选:独立真实上游 smoke 目标。默认不触发计费请求,必须显式运行 --allow-billable。
|
| 104 |
# 每组至少提供 BASE_URL 和 API_KEY;MODEL、SIZE、QUALITY、RESPONSES_MODEL 可按上游覆盖。
|
|
|
|
| 96 |
# 要使用 Responses 后端,需要请求显式传入 image_backend=responses-image-generation,
|
| 97 |
# 或把 IMAGE_GENERATION_BACKEND 设为 responses-image-generation / responses;
|
| 98 |
# 两种方式都需要单独配置 /responses 顶层模型。
|
| 99 |
+
# OPENAI_RESPONSES_API_MODEL 是服务端默认顶层模型,页面请求可用 responsesModel、
|
| 100 |
+
# responses_model、gptModel 或 gpt_model 覆盖单次请求。
|
| 101 |
+
# 该配置只影响本项目的 responses-image-generation 路径,不改变兼容上游自身
|
| 102 |
+
# /v1/images/generations 桥接层内部选择的模型。
|
| 103 |
# 表单 model 只作为 image_generation 工具模型,不会复用为 /responses 顶层模型。
|
| 104 |
# ENABLE_RESPONSES_IMAGE_BACKEND=true
|
| 105 |
+
# OPENAI_RESPONSES_API_MODEL=gpt-5.4
|
| 106 |
|
| 107 |
# 可选:独立真实上游 smoke 目标。默认不触发计费请求,必须显式运行 --allow-billable。
|
| 108 |
# 每组至少提供 BASE_URL 和 API_KEY;MODEL、SIZE、QUALITY、RESPONSES_MODEL 可按上游覆盖。
|
README.md
CHANGED
|
@@ -17,27 +17,20 @@ app_port: 4783
|
|
| 17 |
|
| 18 |
## 快速开始
|
| 19 |
|
| 20 |
-
第一次配置或换机器后,先跑只读就绪检查。它不会写配置、
|
| 21 |
|
| 22 |
```bash
|
| 23 |
npm run first-run
|
| 24 |
```
|
| 25 |
|
| 26 |
-
|
| 27 |
|
| 28 |
```bash
|
| 29 |
npm run first-run -- --base-url https://your-space.hf.space
|
| 30 |
npm run first-run -- --json --base-url https://your-space.hf.space
|
| 31 |
```
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
1. 运行 `npm install`。
|
| 36 |
-
2. 启动服务:本地开发用 `npm run dev`,Docker 用 `docker compose up -d --build --remove-orphans`。
|
| 37 |
-
3. 运行 `npm run first-run` 看中文摘要;如果要检查 Space 或内网服务,使用 `npm run first-run -- --base-url <url>`。
|
| 38 |
-
4. 如果 Agent API 需要鉴权,复制 `.env.agent.local.example` 为 `.env.agent.local` 并填入本机私有 token。Agent CLI 会从当前仓库根目录自动读取该文件;shell 环境变量仍然优先。
|
| 39 |
-
|
| 40 |
-
推荐 Docker:
|
| 41 |
|
| 42 |
```bash
|
| 43 |
docker compose up -d --build --remove-orphans
|
|
@@ -56,7 +49,7 @@ OPENAI_API_KEY=your_openai_api_key_here
|
|
| 56 |
OPENAI_API_BASE_URL=https://api.openai.com/v1
|
| 57 |
```
|
| 58 |
|
| 59 |
-
|
| 60 |
|
| 61 |
```bash
|
| 62 |
npm install
|
|
@@ -106,6 +99,7 @@ start-windows.bat
|
|
| 106 |
| 图片存储 | `NEXT_PUBLIC_IMAGE_STORAGE_MODE` | `fs` 或 `indexeddb`。Docker 默认使用 `fs`。 |
|
| 107 |
| Agent 状态 | `AGENT_STATE_BACKEND` | `memory`、`sqlite` 或 `postgres`。Docker 默认使用 `sqlite`。 |
|
| 108 |
| 默认后端 | `IMAGE_GENERATION_BACKEND` | 默认 `images-api`;可设为 `responses-image-generation`。 |
|
|
|
|
| 109 |
| 流式策略 | `IMAGE_STREAMING_STRATEGY` | 默认 `auto`;可设为 `off`、`openai-sse`、`responses-sse` 等。 |
|
| 110 |
| 并发容量 | `OPENAI_MAX_STREAMS_PER_CREDENTIAL` | 单个渠道凭证允许同时执行的图片请求数,默认 `1`。 |
|
| 111 |
| 渠道队列 | `OPENAI_CHANNEL_QUEUE_ENABLED`、`OPENAI_CHANNEL_QUEUE_MAX_WAIT_MS`、`OPENAI_CHANNEL_QUEUE_MAX_SIZE` | 控制超出凭证容量时等待还是立即失败。 |
|
|
@@ -142,18 +136,20 @@ OPENAI_CHANNEL_3_UPSTREAM_PROFILE=matsca
|
|
| 142 |
|
| 143 |
- 自定义 API URL 必须同时填写自定义 API Key,避免服务端密钥被发送到未知地址。
|
| 144 |
- Docker compose 本身不把默认图片后端改成 Responses;未在 `.env.local` 显式配置时仍是 `images-api` 和 `auto`。
|
| 145 |
-
- Responses image backend 需要 `ENABLE_RESPONSES_IMAGE_BACKEND=true` 和 `OPENAI_RESPONSES_API_MODEL`。
|
| 146 |
- Matsca、extra headers、provider manifest、真实上游 smoke 等高级配置以 [.env.example](./.env.example) 为准。
|
| 147 |
|
| 148 |
## Agent API
|
| 149 |
|
| 150 |
-
Agent API 是机器接口,不是自治 Agent 平台。自动化客户端应先读取 capabilities,再按返回的认证
|
|
|
|
|
|
|
| 151 |
|
| 152 |
常用入口:
|
| 153 |
|
| 154 |
| 接口 | 用途 |
|
| 155 |
| --- | --- |
|
| 156 |
-
| `GET /api/agent/capabilities` | 查询模型、限制、认证方式
|
| 157 |
| `GET /api/agent/openapi.json` | 获取 OpenAPI 描述。 |
|
| 158 |
| `POST /api/agent/images/generate` | JSON 文生图。 |
|
| 159 |
| `POST /api/agent/images/edit` | multipart 图片编辑,支持源图和 mask。 |
|
|
@@ -162,7 +158,7 @@ Agent API 是机器接口,不是自治 Agent 平台。自动化客户端应先
|
|
| 162 |
| `GET /api/agent/jobs/{id}/result` | 读取成功 job 的标准图片响应。 |
|
| 163 |
| `GET /api/agent/artifacts/{id}/content` | 下载产物图片。 |
|
| 164 |
| `POST /api/agent/diagnostics/page-requests` | 批量读取页面请求的脱敏日志诊断摘要。 |
|
| 165 |
-
| `GET /api/agent/diagnostics/requests` | 按
|
| 166 |
|
| 167 |
生成示例:
|
| 168 |
|
|
@@ -179,7 +175,7 @@ curl -s http://localhost:4783/api/agent/images/generate \
|
|
| 179 |
- [skills/gpt-image-playground-agent/SKILL.md](./skills/gpt-image-playground-agent/SKILL.md)
|
| 180 |
- [Agent API 参考](./skills/gpt-image-playground-agent/references/api.md)
|
| 181 |
|
| 182 |
-
脚本默认 dry-run,不触发真实计费请求;真实生成必须显式添加 `--allow-billable`
|
| 183 |
|
| 184 |
```bash
|
| 185 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
@@ -190,17 +186,7 @@ node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
| 190 |
|
| 191 |
### AI Agent 典型使用方式
|
| 192 |
|
| 193 |
-
AI Agent 集成时优先调用 skill
|
| 194 |
-
|
| 195 |
-
交互式任务中,Agent 应先定位服务地址:用户明确提供 URL 时直接使用该 URL;否则先检查 `GPT_IMAGE_PLAYGROUND_URL`,再探测默认本地地址 `http://localhost:4783`。如果只发现环境变量或本地服务,先向用户确认是否使用;用户提供其他地址时,以用户提供的地址为准。非交互式任务无法确认时,按同一顺序自动选择,并在输出里说明地址来源。
|
| 196 |
-
|
| 197 |
-
新环境或不确定服务地址时先运行 `npm run first-run`。它会只读报告 `service_base_url_source`、`interactive_confirmation_required`、服务可达性、当前进程是否拿到 Agent 鉴权,以及 `.env.agent.local` 是否存在私有鉴权配置。Agent CLI 会从当前仓库根目录自动读取 `.env.agent.local`,shell 环境变量仍然优先;如需禁用自动读取,设置 `GPT_IMAGE_AGENT_LOAD_ENV_FILE=0`。
|
| 198 |
-
|
| 199 |
-
dry-run 只做本地请求构造和静态路由规划,不读取远端 capabilities,也不验证远端鉴权、渠道容量或 manifest 写入。脚本输出里的 `verification_scope.mode=local_planning_only` 表示还没有证明远端服务可执行;需要远端合同检查时使用 `--contract-check`,真实执行必须显式添加 `--allow-billable`。
|
| 200 |
-
|
| 201 |
-
subagent 或自动化任务要固定服务地址时,优先给脚本传 `--base-url`,不要只依赖默认 localhost。`generate-image.mjs`、`edit-image.mjs`、`batch-images.mjs`、`diagnose-request.mjs` 和 `npm run agent:doctor -- --base-url <url>` 都支持显式服务地址。首次配置 Agent 鉴权时复制 `.env.agent.local.example` 为 `.env.agent.local` 并填入本机私有 token;不要把 `.env.agent.local` 提交或粘到任务日志。
|
| 202 |
-
|
| 203 |
-
公网部署常见有两层鉴权:`GPT_IMAGE_AGENT_TOKEN` 只用于 `/api/agent/*` Bearer 鉴权;页面 SSE `/api/images` 仍可能要求 `GPT_IMAGE_APP_PASSWORD_HASH` 作为 `passwordHash` 表单字段。使用 `--page-sse`、Responses backend edit、大图默认页面 SSE 或批量页面 SSE 前,先用 `npm run first-run -- --base-url <url> --json` 或 `npm run agent:doctor -- --base-url <url>` 检查 `page_sse_auth_available_to_process` / `page_sse_auth_ready`。
|
| 204 |
|
| 205 |
1. 只读检查当前服务能力,不触发计费:
|
| 206 |
|
|
@@ -211,7 +197,7 @@ node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
| 211 |
"capability check"
|
| 212 |
```
|
| 213 |
|
| 214 |
-
2.
|
| 215 |
|
| 216 |
```bash
|
| 217 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
@@ -223,7 +209,7 @@ node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
| 223 |
"a clean product photo of a ceramic mug"
|
| 224 |
```
|
| 225 |
|
| 226 |
-
3. 用户明确允许后
|
| 227 |
|
| 228 |
```bash
|
| 229 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
@@ -237,7 +223,7 @@ node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
| 237 |
"a clean product photo of a ceramic mug"
|
| 238 |
```
|
| 239 |
|
| 240 |
-
4. 图生图
|
| 241 |
|
| 242 |
```bash
|
| 243 |
node skills/gpt-image-playground-agent/scripts/edit-image.mjs \
|
|
@@ -266,17 +252,6 @@ node skills/gpt-image-playground-agent/scripts/batch-images.mjs \
|
|
| 266 |
--ordered-prefix product-set
|
| 267 |
```
|
| 268 |
|
| 269 |
-
真实批量执行时再添加 `--allow-billable`。多张真实任务优先使用 `batch-images.mjs`、`--manifest`、`--resume` 和 `--dimension-check`;不要手动并行启动多个单张脚本,否则会绕过续跑记录、容量反馈和尺寸门禁。需要并发时添加 `--concurrency N`,并确认 `/api/runtime-capabilities` 的 `streamingBatch.recommendedConcurrency` 或 `channelQueue.capacityPerCredential` 允许;建议并发为 `1` 时保持串行。
|
| 270 |
-
|
| 271 |
-
页面 SSE 返回 503 或断流时,先用诊断脚本读取结构化摘要,再用新的幂等键���式选择备用路径。`edit-image.mjs --agent --stream-mode non_stream --streaming-strategy off` 只适合作为对照诊断;Agent edit 输出格式和尺寸可能与页面 SSE 不完全一致,尺寸敏感任务必须重新校验或用 `--dimension-check`。
|
| 272 |
-
|
| 273 |
-
排查环境配置时不要直接输出 `.env.local`、`.env*.local`、secret 文件或原始 `docker inspect .Config.Env`。Codex 会话日志会持久保存命令输出;优先运行 `npm run env:summary`,或在命令中先把 `API_KEY`、`TOKEN`、`PASSWORD`、`SECRET` 值替换为 `<redacted>`。
|
| 274 |
-
|
| 275 |
-
```bash
|
| 276 |
-
npm run env:summary
|
| 277 |
-
npm run env:summary -- --file .env.local --container gpt-image-playground-customer
|
| 278 |
-
```
|
| 279 |
-
|
| 280 |
6. 失败或结果需要追踪时,用诊断脚本读摘要:
|
| 281 |
|
| 282 |
```bash
|
|
@@ -285,7 +260,7 @@ node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
|
| 285 |
--idempotency-key agent-demo-generate-001
|
| 286 |
```
|
| 287 |
|
| 288 |
-
页面 SSE 请求通常用同一个业务 key 作为 `clientRequestId`
|
| 289 |
|
| 290 |
```bash
|
| 291 |
node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
|
@@ -293,61 +268,20 @@ node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
|
| 293 |
--client-request-id agent-demo-edit-001
|
| 294 |
```
|
| 295 |
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
```bash
|
| 299 |
-
node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
| 300 |
-
--base-url https://your-space.hf.space \
|
| 301 |
-
--idempotency-key agent-demo-generate-001
|
| 302 |
-
```
|
| 303 |
-
|
| 304 |
-
首次配置和诊断输出字段速查:
|
| 305 |
-
|
| 306 |
-
| 字段 | 出现位置 | 判断口径 |
|
| 307 |
-
| --- | --- | --- |
|
| 308 |
-
| `service_base_url` / `verification_scope.service_base_url` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | 当前脚本准备访问的 Playground 服务地址。 |
|
| 309 |
-
| `service_base_url_source` / `verification_scope.service_base_url_source` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | `user_provided` 表示用户或命令行明确指定;`GPT_IMAGE_PLAYGROUND_URL` 表示来自环境变量;`default_local_probe` 表示默认本地探测。 |
|
| 310 |
-
| `interactive_confirmation_required` / `verification_scope.interactive_confirmation_required` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | 交互式任务中为 `true` 时,应先向用户确认是否使用该地址再发起真实请求。 |
|
| 311 |
-
| `agent_auth_process.has_token` | `first-run --json` | 当前进程是否已经拿到 `GPT_IMAGE_AGENT_TOKEN`。 |
|
| 312 |
-
| `page_sse_auth_available_to_process` | `first-run --json` | 目标服务要求页面 SSE `passwordHash` 时,当前进程是否已加载 `GPT_IMAGE_APP_PASSWORD_HASH`。 |
|
| 313 |
-
| `summary.page_sse_auth_ready` | `agent:doctor` | 页面 SSE 鉴权是否已满足;为 `false` 时不要运行 `--page-sse` 真实计费请求。 |
|
| 314 |
-
| `private_agent_env.exists` | `first-run --json` | 本机是否存在 `.env.agent.local` 私有配置;Agent CLI 默认从当前仓库根目录读取该文件。 |
|
| 315 |
-
| `capabilities.ok` | `first-run --json`、`agent:doctor` | 目标地址是否返回 Agent capabilities;失败时先看 HTTP 状态、鉴权提示和服务地址。 |
|
| 316 |
-
| `diagnostics_retention` | `diagnose-request.mjs` | 页面日志诊断的保留窗口;无匹配日志不等于请求一定没发生。 |
|
| 317 |
-
|
| 318 |
-
常用环境变量:
|
| 319 |
-
|
| 320 |
-
| 变量 | 用途 |
|
| 321 |
-
| --- | --- |
|
| 322 |
-
| `GPT_IMAGE_PLAYGROUND_URL` | 指向本机、内网或公网部署地址;未设置时脚本默认尝试 `http://localhost:4783`。交互式任务中,自动发现到本地服务后应先向用户确认。 |
|
| 323 |
-
| `GPT_IMAGE_AGENT_TOKEN` | Agent Bearer token,对应服务端 `AGENT_API_TOKEN`。 |
|
| 324 |
-
| `GPT_IMAGE_APP_PASSWORD_HASH` | 使用页面访问码部署时的访问码哈希;页面 SSE 会作为 `passwordHash` 表单字段发送。 |
|
| 325 |
-
| `GPT_IMAGE_AGENT_IDEMPOTENCY_KEY` | 跨脚本进程复用同一业务操作的幂等键。 |
|
| 326 |
-
|
| 327 |
-
Hugging Face Space Secrets 只能写入和列出名称,不能从 CLI 读回 secret 值。远端 Space 配置了 `AGENT_API_TOKEN` 后,本机 Agent 仍需要通过不入库的 shell 环境、keychain 或本地私有 env 文件注入 `GPT_IMAGE_AGENT_TOKEN`;如果 Space 同时配置了 `APP_PASSWORD`,页面 SSE 还需要 `GPT_IMAGE_APP_PASSWORD_HASH`。Agent CLI 默认读取当前仓库根目录的 `.env.agent.local`,shell 环境变量优先。不要把 token、访问码或哈希写进 README、任务 JSONL、manifest 或命令日志。仓库提供 `.env.agent.local.example` 作为私有本机配置模板。
|
| 328 |
-
|
| 329 |
-
接口边界:
|
| 330 |
|
| 331 |
-
- `
|
| 332 |
-
-
|
| 333 |
-
-
|
| 334 |
-
-
|
|
|
|
|
|
|
| 335 |
- 选择 `responses-image-generation` 或兼容别名 `responses` 时,`partial_images` 必须优先按 `partial_images_by_backend["responses-image-generation"]` 校验,不能套用 Matsca Images API 的范围。
|
| 336 |
-
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
| --- | --- | --- | --- |
|
| 342 |
-
| `POST /api/agent/images/generate`、`POST /api/agent/images/edit`、Agent jobs、Agent artifacts | Agent API | 是 | 通过 skill 脚本和 Agent 鉴权调用。 |
|
| 343 |
-
| `POST /api/images` | 页面 form-data SSE API | 否 | 默认 WebP edit、页面高级字段、大图或复杂批量需要时由 skill 显式选择。 |
|
| 344 |
-
| `GET /api/runtime-capabilities` | 页面运行态能力 API | 否 | 只读查看流式默认值、图片上游传输、渠道健康和队列状态。 |
|
| 345 |
-
| `PUT/DELETE /api/feedback` | 页面结果反馈 API | 否 | 页面写入和清理反馈;Agent 只读查询用 `/api/agent/page-requests/feedback` 或 `/api/agent/page-requests/{id}/feedback`。 |
|
| 346 |
-
| `POST /api/agent/page-requests/feedback`、`GET /api/agent/page-requests/{id}/feedback` | Agent 结果反馈只读 API | 是 | Agent 按页面 `clientRequestId` 查询反馈。 |
|
| 347 |
-
| `POST /api/agent/diagnostics/page-requests`、`GET /api/agent/diagnostics/page-requests/{id}` | Agent 页面请求诊断 API | 是 | Agent 按页面 `clientRequestId` 查询脱敏日志摘要,不直接读 `/api/logs`。 |
|
| 348 |
-
| `GET /api/logs` | 页面日志 SSE API | 否 | 页面使用访问码哈希读取;不接受 Agent token。 |
|
| 349 |
-
| `POST /api/shares`、`GET /api/shares/{token}`、`POST /api/shares/{token}/content` | 页面分享 API | 否 | 使用页面 cookie、访问码和分享 token。 |
|
| 350 |
-
| `POST /api/image-delete` | 页面图片文件删除 API | 否 | 按页面文件名删除 `generated-images/` 文件,不删除 Agent artifact 状态。 |
|
| 351 |
|
| 352 |
## Docker 与部署
|
| 353 |
|
|
|
|
| 17 |
|
| 18 |
## 快速开始
|
| 19 |
|
| 20 |
+
第一次配置或换机器后,先跑只读就绪检查。它不会写配置、输出密钥或触发真实生图:
|
| 21 |
|
| 22 |
```bash
|
| 23 |
npm run first-run
|
| 24 |
```
|
| 25 |
|
| 26 |
+
检查公网或 Space 服务时显式传地址;给脚本消费时加 `--json`:
|
| 27 |
|
| 28 |
```bash
|
| 29 |
npm run first-run -- --base-url https://your-space.hf.space
|
| 30 |
npm run first-run -- --json --base-url https://your-space.hf.space
|
| 31 |
```
|
| 32 |
|
| 33 |
+
本地服务推荐用 Docker:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
```bash
|
| 36 |
docker compose up -d --build --remove-orphans
|
|
|
|
| 49 |
OPENAI_API_BASE_URL=https://api.openai.com/v1
|
| 50 |
```
|
| 51 |
|
| 52 |
+
开发模式:
|
| 53 |
|
| 54 |
```bash
|
| 55 |
npm install
|
|
|
|
| 99 |
| 图片存储 | `NEXT_PUBLIC_IMAGE_STORAGE_MODE` | `fs` 或 `indexeddb`。Docker 默认使用 `fs`。 |
|
| 100 |
| Agent 状态 | `AGENT_STATE_BACKEND` | `memory`、`sqlite` 或 `postgres`。Docker 默认使用 `sqlite`。 |
|
| 101 |
| 默认后端 | `IMAGE_GENERATION_BACKEND` | 默认 `images-api`;可设为 `responses-image-generation`。 |
|
| 102 |
+
| Responses 顶层模型 | `OPENAI_RESPONSES_API_MODEL` | 仅在 `responses-image-generation` 后端生效;作为 `/responses` 的顶层 `model`,例如 `gpt-5.4`。 |
|
| 103 |
| 流式策略 | `IMAGE_STREAMING_STRATEGY` | 默认 `auto`;可设为 `off`、`openai-sse`、`responses-sse` 等。 |
|
| 104 |
| 并发容量 | `OPENAI_MAX_STREAMS_PER_CREDENTIAL` | 单个渠道凭证允许同时执行的图片请求数,默认 `1`。 |
|
| 105 |
| 渠道队列 | `OPENAI_CHANNEL_QUEUE_ENABLED`、`OPENAI_CHANNEL_QUEUE_MAX_WAIT_MS`、`OPENAI_CHANNEL_QUEUE_MAX_SIZE` | 控制超出凭证容量时等待还是立即失败。 |
|
|
|
|
| 136 |
|
| 137 |
- 自定义 API URL 必须同时填写自定义 API Key,避免服务端密钥被发送到未知地址。
|
| 138 |
- Docker compose 本身不把默认图片后端改成 Responses;未在 `.env.local` 显式配置时仍是 `images-api` 和 `auto`。
|
| 139 |
+
- Responses image backend 需要 `ENABLE_RESPONSES_IMAGE_BACKEND=true` 和 `OPENAI_RESPONSES_API_MODEL`。页面请求也可以用 `responsesModel`、`responses_model`、`gptModel` 或 `gpt_model` 覆盖单次 `/responses` 顶层模型;这些字段只影响本项目的 `responses-image-generation` 路径,不会改变兼容上游自身 `/v1/images/generations` 桥接层内部选择的模型。
|
| 140 |
- Matsca、extra headers、provider manifest、真实上游 smoke 等高级配置以 [.env.example](./.env.example) 为准。
|
| 141 |
|
| 142 |
## Agent API
|
| 143 |
|
| 144 |
+
Agent API 是机器接口,不是自治 Agent 平台。自动化客户端应先读取 capabilities,再按返回的认证、路由和端点能力选择路径。
|
| 145 |
+
|
| 146 |
+
新增 probe、diagnostics 或路由可观测能力时,先落 API / capabilities / OpenAPI 契约,再让 Skill 脚本做薄封装;不要让脚本自己复制页面与 Agent 的边界判断。
|
| 147 |
|
| 148 |
常用入口:
|
| 149 |
|
| 150 |
| 接口 | 用途 |
|
| 151 |
| --- | --- |
|
| 152 |
+
| `GET /api/agent/capabilities` | 查询模型、限制、认证方式和路由规则。 |
|
| 153 |
| `GET /api/agent/openapi.json` | 获取 OpenAPI 描述。 |
|
| 154 |
| `POST /api/agent/images/generate` | JSON 文生图。 |
|
| 155 |
| `POST /api/agent/images/edit` | multipart 图片编辑,支持源图和 mask。 |
|
|
|
|
| 158 |
| `GET /api/agent/jobs/{id}/result` | 读取成功 job 的标准图片响应。 |
|
| 159 |
| `GET /api/agent/artifacts/{id}/content` | 下载产物图片。 |
|
| 160 |
| `POST /api/agent/diagnostics/page-requests` | 批量读取页面请求的脱敏日志诊断摘要。 |
|
| 161 |
+
| `GET /api/agent/diagnostics/requests` | 按 request id 或幂等键查询诊断。 |
|
| 162 |
|
| 163 |
生成示例:
|
| 164 |
|
|
|
|
| 175 |
- [skills/gpt-image-playground-agent/SKILL.md](./skills/gpt-image-playground-agent/SKILL.md)
|
| 176 |
- [Agent API 参考](./skills/gpt-image-playground-agent/references/api.md)
|
| 177 |
|
| 178 |
+
脚本默认 dry-run,不触发真实计费请求;真实生成必须显式添加 `--allow-billable`:
|
| 179 |
|
| 180 |
```bash
|
| 181 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
|
|
| 186 |
|
| 187 |
### AI Agent 典型使用方式
|
| 188 |
|
| 189 |
+
AI Agent 集成时优先调用 skill 脚本,不要临时手写 fetch、curl 或表单提交逻辑。脚本会处理 capabilities、鉴权、幂等键、路由、超时、产物 URL 和结构化失败摘要。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
1. 只读检查当前服务能力,不触发计费:
|
| 192 |
|
|
|
|
| 197 |
"capability check"
|
| 198 |
```
|
| 199 |
|
| 200 |
+
2. dry-run 单张文生图,确认请求字段和路由:
|
| 201 |
|
| 202 |
```bash
|
| 203 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
|
|
| 209 |
"a clean product photo of a ceramic mug"
|
| 210 |
```
|
| 211 |
|
| 212 |
+
3. 用户明确允许后执行真实请求:
|
| 213 |
|
| 214 |
```bash
|
| 215 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
|
|
| 223 |
"a clean product photo of a ceramic mug"
|
| 224 |
```
|
| 225 |
|
| 226 |
+
4. 图生图:
|
| 227 |
|
| 228 |
```bash
|
| 229 |
node skills/gpt-image-playground-agent/scripts/edit-image.mjs \
|
|
|
|
| 252 |
--ordered-prefix product-set
|
| 253 |
```
|
| 254 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
6. 失败或结果需要追踪时,用诊断脚本读摘要:
|
| 256 |
|
| 257 |
```bash
|
|
|
|
| 260 |
--idempotency-key agent-demo-generate-001
|
| 261 |
```
|
| 262 |
|
| 263 |
+
页面 SSE 请求通常用同一个业务 key 作为 `clientRequestId`:
|
| 264 |
|
| 265 |
```bash
|
| 266 |
node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
|
|
|
| 268 |
--client-request-id agent-demo-edit-001
|
| 269 |
```
|
| 270 |
|
| 271 |
+
关键规则:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
|
| 273 |
+
- 交互式任务中,如果只发现本地服务或 `GPT_IMAGE_PLAYGROUND_URL`,先向用户确认;用户给了 URL 时以用户 URL 为准。
|
| 274 |
+
- 远程 Space、云服务或内网服务必须显式传 `--base-url`,避免误用本机默认服务。
|
| 275 |
+
- Agent CLI 默认读取当前仓库根目录的 `.env.agent.local`;shell 环境变量优先。首次配置可复制 `.env.agent.local.example`。不要提交 token、访问码或哈希。
|
| 276 |
+
- `GPT_IMAGE_AGENT_TOKEN` 只用于 `/api/agent/*`;页面 SSE `/api/images` 可能还需要 `GPT_IMAGE_APP_PASSWORD_HASH`。
|
| 277 |
+
- dry-run 只验证本地请求构造;`verification_scope.mode=local_planning_only` 不是远端已可执行。远端合同检查用 `--contract-check`,真实执行必须加 `--allow-billable`。
|
| 278 |
+
- 多张真实任务优先用 `batch-images.mjs`、`--manifest`、`--resume` 和 `--dimension-check`;不要手动并行启动多个单张脚本。需要并发时先看 `/api/runtime-capabilities` 的 `streamingBatch.recommendedConcurrency` 和 `channelQueue.capacityPerCredential`。
|
| 279 |
- 选择 `responses-image-generation` 或兼容别名 `responses` 时,`partial_images` 必须优先按 `partial_images_by_backend["responses-image-generation"]` 校验,不能套用 Matsca Images API 的范围。
|
| 280 |
+
- 页面 SSE 返回 503 或断流时,先用诊断脚本读取结构化摘要,再用新的幂等键显式选择备用路径。`page_sse_supported=true` 只是声明支持,不代表实测一定成功。Agent edit 输出格式和尺寸可能与页面 SSE 不完全一致,尺寸敏感任务必须重新校验或用 `--dimension-check`。
|
| 281 |
+
- 排查环境配置时优先运行 `npm run env:summary`,不要直接输出 `.env.local`、`.env*.local`、secret 文件或原始 `docker inspect .Config.Env`。
|
| 282 |
+
- Hugging Face Space Secrets 只能写入和列出名称,不能从 CLI 读回 secret 值。
|
| 283 |
+
- 边界矩阵精简版:
|
| 284 |
+
- `/api/agent/*` 返回最终 JSON;`POST /api/images`、`GET /api/runtime-capabilities`、`/api/feedback`、`/api/shares`、`/api/logs` 和 `POST /api/image-delete` 属于页面或运行态 API,不进入 Agent OpenAPI。Agent 只读反馈和诊断入口是 `/api/agent/page-requests/feedback`、`/api/agent/page-requests/{id}/feedback`、`/api/agent/diagnostics/page-requests` 和 `/api/agent/diagnostics/page-requests/{id}`。灵感相册和历史复用是浏览器工作台体验,不作为机器 API 契约承诺。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
## Docker 与部署
|
| 287 |
|
scripts/agent-doctor.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
| 11 |
const GENERATE_SCRIPT = fileURLToPath(new URL('../skills/gpt-image-playground-agent/scripts/generate-image.mjs', import.meta.url));
|
| 12 |
const EDIT_SCRIPT = fileURLToPath(new URL('../skills/gpt-image-playground-agent/scripts/edit-image.mjs', import.meta.url));
|
| 13 |
const AGENT_DOCTOR_TIMEOUT_MS = 75_000;
|
|
|
|
| 14 |
|
| 15 |
export function buildAgentDoctorArgs() {
|
| 16 |
return [GENERATE_SCRIPT, '--contract-check', '--timeout-ms', '60000', 'contract check'];
|
|
@@ -129,6 +130,7 @@ function buildSkippedSmoke(options) {
|
|
| 129 |
reason: 'requires --allow-billable',
|
| 130 |
checks: [
|
| 131 |
{ name: 'generate_1k', skipped: true, reason: 'requires --allow-billable' },
|
|
|
|
| 132 |
{
|
| 133 |
name: 'edit_1k',
|
| 134 |
skipped: true,
|
|
@@ -160,6 +162,28 @@ function runBillableSmoke(options, baseUrl) {
|
|
| 160 |
'--idempotency-key',
|
| 161 |
`agent-doctor-generate-${Date.now()}`,
|
| 162 |
'agent doctor 1k generate smoke'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
])
|
| 164 |
];
|
| 165 |
if (options.editImage) {
|
|
@@ -266,6 +290,7 @@ function buildLayers({ capabilities, runtime, contract, smoke }) {
|
|
| 266 |
function summarizeCapabilities(body) {
|
| 267 |
return {
|
| 268 |
page_sse: body?.agent_streaming?.page_sse?.supported === true,
|
|
|
|
| 269 |
page_sse_auth_required: body?.agent_streaming?.page_sse?.auth?.required === true,
|
| 270 |
page_sse_auth_ready:
|
| 271 |
body?.agent_streaming?.page_sse?.auth?.required === true
|
|
@@ -301,6 +326,7 @@ function summarizeStateBackend(body) {
|
|
| 301 |
function summarizeResponsesReadiness(capabilities, runtime) {
|
| 302 |
const requirements = capabilities?.supported?.image_backend_requirements?.['responses-image-generation'];
|
| 303 |
return {
|
|
|
|
| 304 |
backend_supported: requirements?.supported === true,
|
| 305 |
backend_enabled: requirements?.enabled === true,
|
| 306 |
runtime_enabled: runtime?.responsesImageBackend?.enabled === true,
|
|
@@ -317,19 +343,32 @@ function buildSummary({ capabilities, runtime, contract, smoke }) {
|
|
| 317 |
contract_check: contract.ok ? 'ok' : 'failed',
|
| 318 |
runtime: runtime.ok ? 'ok' : 'failed',
|
| 319 |
state_backend: capabilities.ok ? capabilities.body?.defaults?.state_backend : 'unknown',
|
|
|
|
|
|
|
|
|
|
| 320 |
page_sse_auth_ready:
|
| 321 |
capabilities.ok && capabilities.body?.agent_streaming?.page_sse?.auth?.required === true
|
| 322 |
? Boolean(process.env.GPT_IMAGE_APP_PASSWORD_HASH)
|
| 323 |
: capabilities.ok,
|
|
|
|
| 324 |
responses_gpt2image_ready:
|
| 325 |
capabilities.ok && runtime.ok
|
| 326 |
? capabilities.body?.supported?.image_backend_requirements?.['responses-image-generation']?.enabled === true &&
|
| 327 |
runtime.body?.responsesImageBackend?.enabled === true
|
| 328 |
: false,
|
|
|
|
|
|
|
|
|
|
| 329 |
billable_smoke: smoke.skipped ? 'skipped' : smoke.ok ? 'ok' : 'failed'
|
| 330 |
};
|
| 331 |
}
|
| 332 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
function authHeaders() {
|
| 334 |
if (process.env.GPT_IMAGE_AGENT_TOKEN) return { Authorization: `Bearer ${process.env.GPT_IMAGE_AGENT_TOKEN}` };
|
| 335 |
if (process.env.GPT_IMAGE_APP_PASSWORD_HASH) return { 'X-App-Password-Hash': process.env.GPT_IMAGE_APP_PASSWORD_HASH };
|
|
|
|
| 11 |
const GENERATE_SCRIPT = fileURLToPath(new URL('../skills/gpt-image-playground-agent/scripts/generate-image.mjs', import.meta.url));
|
| 12 |
const EDIT_SCRIPT = fileURLToPath(new URL('../skills/gpt-image-playground-agent/scripts/edit-image.mjs', import.meta.url));
|
| 13 |
const AGENT_DOCTOR_TIMEOUT_MS = 75_000;
|
| 14 |
+
const PAGE_SSE_GENERATE_SMOKE_NAME = 'responses_page_sse_generate_1k';
|
| 15 |
|
| 16 |
export function buildAgentDoctorArgs() {
|
| 17 |
return [GENERATE_SCRIPT, '--contract-check', '--timeout-ms', '60000', 'contract check'];
|
|
|
|
| 130 |
reason: 'requires --allow-billable',
|
| 131 |
checks: [
|
| 132 |
{ name: 'generate_1k', skipped: true, reason: 'requires --allow-billable' },
|
| 133 |
+
{ name: PAGE_SSE_GENERATE_SMOKE_NAME, skipped: true, reason: 'requires --allow-billable' },
|
| 134 |
{
|
| 135 |
name: 'edit_1k',
|
| 136 |
skipped: true,
|
|
|
|
| 162 |
'--idempotency-key',
|
| 163 |
`agent-doctor-generate-${Date.now()}`,
|
| 164 |
'agent doctor 1k generate smoke'
|
| 165 |
+
]),
|
| 166 |
+
runSmokeCommand(PAGE_SSE_GENERATE_SMOKE_NAME, [
|
| 167 |
+
GENERATE_SCRIPT,
|
| 168 |
+
'--base-url',
|
| 169 |
+
baseUrl,
|
| 170 |
+
'--allow-billable',
|
| 171 |
+
'--page-sse',
|
| 172 |
+
'--timeout-ms',
|
| 173 |
+
String(options.timeoutMs),
|
| 174 |
+
'--size',
|
| 175 |
+
'1024x1024',
|
| 176 |
+
'--quality',
|
| 177 |
+
'low',
|
| 178 |
+
'--image-backend',
|
| 179 |
+
'responses-image-generation',
|
| 180 |
+
'--stream-mode',
|
| 181 |
+
'stream',
|
| 182 |
+
'--streaming-strategy',
|
| 183 |
+
'responses-sse',
|
| 184 |
+
'--idempotency-key',
|
| 185 |
+
`agent-doctor-responses-page-sse-generate-${Date.now()}`,
|
| 186 |
+
'agent doctor responses page SSE generate smoke'
|
| 187 |
])
|
| 188 |
];
|
| 189 |
if (options.editImage) {
|
|
|
|
| 290 |
function summarizeCapabilities(body) {
|
| 291 |
return {
|
| 292 |
page_sse: body?.agent_streaming?.page_sse?.supported === true,
|
| 293 |
+
page_sse_declared_supported: body?.agent_streaming?.page_sse?.supported === true,
|
| 294 |
page_sse_auth_required: body?.agent_streaming?.page_sse?.auth?.required === true,
|
| 295 |
page_sse_auth_ready:
|
| 296 |
body?.agent_streaming?.page_sse?.auth?.required === true
|
|
|
|
| 326 |
function summarizeResponsesReadiness(capabilities, runtime) {
|
| 327 |
const requirements = capabilities?.supported?.image_backend_requirements?.['responses-image-generation'];
|
| 328 |
return {
|
| 329 |
+
declared_supported: requirements?.supported === true,
|
| 330 |
backend_supported: requirements?.supported === true,
|
| 331 |
backend_enabled: requirements?.enabled === true,
|
| 332 |
runtime_enabled: runtime?.responsesImageBackend?.enabled === true,
|
|
|
|
| 343 |
contract_check: contract.ok ? 'ok' : 'failed',
|
| 344 |
runtime: runtime.ok ? 'ok' : 'failed',
|
| 345 |
state_backend: capabilities.ok ? capabilities.body?.defaults?.state_backend : 'unknown',
|
| 346 |
+
page_sse_declared_supported: capabilities.ok
|
| 347 |
+
? capabilities.body?.agent_streaming?.page_sse?.supported === true
|
| 348 |
+
: false,
|
| 349 |
page_sse_auth_ready:
|
| 350 |
capabilities.ok && capabilities.body?.agent_streaming?.page_sse?.auth?.required === true
|
| 351 |
? Boolean(process.env.GPT_IMAGE_APP_PASSWORD_HASH)
|
| 352 |
: capabilities.ok,
|
| 353 |
+
page_sse_real_smoke: summarizeSmokeCheck(smoke, PAGE_SSE_GENERATE_SMOKE_NAME),
|
| 354 |
responses_gpt2image_ready:
|
| 355 |
capabilities.ok && runtime.ok
|
| 356 |
? capabilities.body?.supported?.image_backend_requirements?.['responses-image-generation']?.enabled === true &&
|
| 357 |
runtime.body?.responsesImageBackend?.enabled === true
|
| 358 |
: false,
|
| 359 |
+
responses_image_backend_declared_supported: capabilities.ok
|
| 360 |
+
? capabilities.body?.supported?.image_backend_requirements?.['responses-image-generation']?.supported === true
|
| 361 |
+
: false,
|
| 362 |
billable_smoke: smoke.skipped ? 'skipped' : smoke.ok ? 'ok' : 'failed'
|
| 363 |
};
|
| 364 |
}
|
| 365 |
|
| 366 |
+
function summarizeSmokeCheck(smoke, name) {
|
| 367 |
+
const check = smoke.checks?.find((item) => item.name === name);
|
| 368 |
+
if (!check || check.skipped) return 'skipped';
|
| 369 |
+
return check.ok ? 'passed' : 'failed';
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
function authHeaders() {
|
| 373 |
if (process.env.GPT_IMAGE_AGENT_TOKEN) return { Authorization: `Bearer ${process.env.GPT_IMAGE_AGENT_TOKEN}` };
|
| 374 |
if (process.env.GPT_IMAGE_APP_PASSWORD_HASH) return { 'X-App-Password-Hash': process.env.GPT_IMAGE_APP_PASSWORD_HASH };
|
scripts/agent-skill-scripts.test.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import {
|
|
| 3 |
readCapabilitiesImageTransportTimeoutMs,
|
| 4 |
resolveSameOriginUrl
|
| 5 |
} from '../skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs';
|
| 6 |
-
import {
|
|
|
|
| 7 |
import assert from 'node:assert/strict';
|
| 8 |
import { spawn, spawnSync } from 'node:child_process';
|
| 9 |
import { cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
@@ -85,6 +86,89 @@ describe('Agent skill script argument validation', () => {
|
|
| 85 |
assert.equal(probeResult.stdout.trim(), '');
|
| 86 |
});
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
it('converts local images to webp by default', () => {
|
| 89 |
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-convert-'));
|
| 90 |
try {
|
|
@@ -1138,6 +1222,243 @@ describe('Agent skill script argument validation', () => {
|
|
| 1138 |
);
|
| 1139 |
});
|
| 1140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1141 |
it('uses page SSE client request id max length declared by capabilities', async () => {
|
| 1142 |
const requests = [];
|
| 1143 |
await withServer(
|
|
@@ -1615,7 +1936,9 @@ describe('Agent skill script argument validation', () => {
|
|
| 1615 |
{
|
| 1616 |
filename: 'agent-off.png',
|
| 1617 |
content_url: '/api/agent/artifacts/artifact-off/content',
|
| 1618 |
-
metadata_url: '/api/agent/artifacts/artifact-off'
|
|
|
|
|
|
|
| 1619 |
}
|
| 1620 |
],
|
| 1621 |
timing: { server_elapsed_ms: 4321 }
|
|
@@ -1660,6 +1983,8 @@ describe('Agent skill script argument validation', () => {
|
|
| 1660 |
assert.equal(body.summary.route_mode, 'agent');
|
| 1661 |
assert.deepEqual(body.summary.content_urls, ['/api/agent/artifacts/artifact-off/content']);
|
| 1662 |
assert.deepEqual(body.summary.absolute_content_urls, [`${baseUrl}/api/agent/artifacts/artifact-off/content`]);
|
|
|
|
|
|
|
| 1663 |
assert.equal(typeof body.summary.elapsed_ms, 'number');
|
| 1664 |
assert.equal(body.summary.elapsed_source, 'client_script');
|
| 1665 |
assert.equal(body.summary.server_elapsed_ms, 4321);
|
|
@@ -2352,6 +2677,102 @@ describe('Agent skill script argument validation', () => {
|
|
| 2352 |
}
|
| 2353 |
});
|
| 2354 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2355 |
it('rejects default WebP edit dry-runs when streaming is explicitly disabled', () => {
|
| 2356 |
const result = runSkillScript('edit-image.mjs', [
|
| 2357 |
'--size',
|
|
@@ -2778,10 +3199,14 @@ describe('Agent skill script argument validation', () => {
|
|
| 2778 |
assert.match(skillText, /脚本参数仍写作 `--streaming-strategy responses-sse`/);
|
| 2779 |
assert.match(skillText, /batch JSONL 字段是 `streaming_strategy`/);
|
| 2780 |
assert.match(skillText, /`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段/);
|
|
|
|
|
|
|
| 2781 |
assert.match(skillText, /支持位置参数 `<image-path> <prompt>`,也支持 `--image <path> <prompt>` 别名/);
|
| 2782 |
assert.match(skillText, /`--image-backend responses-image-generation` 只用于页面 SSE edit/);
|
| 2783 |
assert.match(skillText, /不要把 Matsca `limits\.partial_images=0\.\.4` 误套到 `responses-image-generation`/);
|
| 2784 |
assert.match(apiReference, /limits\.partial_images_by_backend\[image_backend\]/);
|
|
|
|
|
|
|
| 2785 |
assert.match(apiReference, /Agent edit 不接收 `image_backend`、`output_format` 或 `output_compression`/);
|
| 2786 |
assert.match(apiReference, /强制 Agent edit 时输出格式固定为 PNG,`partial_images` 按默认 Images API\/profile 范围校验/);
|
| 2787 |
assert.match(apiReference, /图片路径可以用位置参数 `<image-path> <prompt>`,也可以用 `--image <path> <prompt>`/);
|
|
@@ -2819,6 +3244,7 @@ describe('Agent skill script argument validation', () => {
|
|
| 2819 |
assert.match(readmeText, /npm run env:summary/);
|
| 2820 |
assert.match(readmeText, /verification_scope\.mode=local_planning_only/);
|
| 2821 |
assert.match(readmeText, /Hugging Face Space Secrets 只能写入和列出名称/);
|
|
|
|
| 2822 |
|
| 2823 |
assert.match(skillText, /不要手动并行启动多个单张脚本/);
|
| 2824 |
assert.match(skillText, /capacity_feedback/);
|
|
@@ -2840,6 +3266,8 @@ describe('Agent skill script argument validation', () => {
|
|
| 2840 |
assert.match(apiReference, /verification_scope\.mode=local_planning_only/);
|
| 2841 |
assert.match(apiReference, /manifest_written=false/);
|
| 2842 |
assert.match(apiReference, /Hugging Face Space Secrets 只能写入和列出名称/);
|
|
|
|
|
|
|
| 2843 |
});
|
| 2844 |
|
| 2845 |
it('keeps WebUI page APIs out of the Agent OpenAPI contract', () => {
|
|
@@ -2894,6 +3322,24 @@ describe('Agent skill script argument validation', () => {
|
|
| 2894 |
assert.match(openApiSource, /高分辨率 edit 默认优先使用页面端 \/api\/images form-data SSE/);
|
| 2895 |
});
|
| 2896 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2897 |
it('runs from a copied standalone skill directory outside the repository', () => {
|
| 2898 |
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-playground-agent-'));
|
| 2899 |
const copiedSkillRoot = join(tempRoot, 'gpt-image-playground-agent');
|
|
@@ -2997,6 +3443,10 @@ describe('Agent skill script argument validation', () => {
|
|
| 2997 |
assert.equal(body.manifest_write_reason, 'dry_run');
|
| 2998 |
assert.equal(body.total, 2);
|
| 2999 |
assert.equal(body.concurrency, 1);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3000 |
assert.equal(body.tasks[0].endpoint, '/api/agent/images/generate');
|
| 3001 |
assert.equal(body.tasks[0].idempotency_key, 'demo-0001-first-item');
|
| 3002 |
assert.equal(body.tasks[0].request.model, 'gpt-image-2');
|
|
@@ -3017,6 +3467,77 @@ describe('Agent skill script argument validation', () => {
|
|
| 3017 |
}
|
| 3018 |
});
|
| 3019 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3020 |
it('rejects invalid batch concurrency before dry-run output', () => {
|
| 3021 |
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-concurrency-'));
|
| 3022 |
try {
|
|
@@ -4473,6 +4994,8 @@ describe('Agent skill script argument validation', () => {
|
|
| 4473 |
assert.equal(result.stderr.trim(), '');
|
| 4474 |
const body = JSON.parse(result.stdout);
|
| 4475 |
assert.equal(body.results[0].status, 'succeeded');
|
|
|
|
|
|
|
| 4476 |
}
|
| 4477 |
);
|
| 4478 |
} finally {
|
|
@@ -4912,7 +5435,14 @@ describe('Agent skill script argument validation', () => {
|
|
| 4912 |
}
|
| 4913 |
if (request.url === '/api/agent/images/generate') {
|
| 4914 |
response.writeHead(200, { 'content-type': 'application/json' });
|
| 4915 |
-
response.end(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4916 |
return;
|
| 4917 |
}
|
| 4918 |
response.writeHead(404, { 'content-type': 'application/json' });
|
|
@@ -4929,7 +5459,29 @@ describe('Agent skill script argument validation', () => {
|
|
| 4929 |
assert.equal(result.stderr.trim(), '');
|
| 4930 |
const body = JSON.parse(result.stdout);
|
| 4931 |
assert.equal(body.ok, false);
|
| 4932 |
-
assert.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4933 |
}
|
| 4934 |
);
|
| 4935 |
} finally {
|
|
@@ -5685,9 +6237,10 @@ function listTextFiles(root) {
|
|
| 5685 |
|
| 5686 |
function runSkillScriptAsync(filename, args, env = {}, options = {}) {
|
| 5687 |
return new Promise((resolve) => {
|
|
|
|
| 5688 |
const child = spawn(process.execPath, [join(skillScriptsRoot, filename), ...args], {
|
| 5689 |
cwd: repoRoot,
|
| 5690 |
-
env: { ...
|
| 5691 |
stdio: ['ignore', 'pipe', 'pipe']
|
| 5692 |
});
|
| 5693 |
let stdout = '';
|
|
|
|
| 3 |
readCapabilitiesImageTransportTimeoutMs,
|
| 4 |
resolveSameOriginUrl
|
| 5 |
} from '../skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs';
|
| 6 |
+
import { enrichFailureWithAgentDiagnostics } from '../skills/gpt-image-playground-agent/scripts/lib/agent-diagnostics-summary.mjs';
|
| 7 |
+
import { AGENT_ENDPOINTS } from '../skills/gpt-image-playground-agent/scripts/lib/agent-api-paths.mjs';
|
| 8 |
import assert from 'node:assert/strict';
|
| 9 |
import { spawn, spawnSync } from 'node:child_process';
|
| 10 |
import { cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
|
|
| 86 |
assert.equal(probeResult.stdout.trim(), '');
|
| 87 |
});
|
| 88 |
|
| 89 |
+
it('caps Agent diagnostics enrichment timeout independently of image request timeouts', async () => {
|
| 90 |
+
const startedAt = Date.now();
|
| 91 |
+
const result = await enrichFailureWithAgentDiagnostics({
|
| 92 |
+
baseUrl: 'http://example.test/playground',
|
| 93 |
+
authHeaders: () => ({}),
|
| 94 |
+
idempotencyKey: 'slow-diagnostics-key',
|
| 95 |
+
failureOutput: { ok: false, error: { code: 'network_error' } },
|
| 96 |
+
summary: {
|
| 97 |
+
ok: false,
|
| 98 |
+
retryable: true,
|
| 99 |
+
next_action: 'retry_after_wait'
|
| 100 |
+
},
|
| 101 |
+
timeoutMs: 420000,
|
| 102 |
+
diagnosticsTimeoutMs: 25,
|
| 103 |
+
fetchFn: (_url, init) =>
|
| 104 |
+
new Promise((_resolve, reject) => {
|
| 105 |
+
init.signal.addEventListener('abort', () => {
|
| 106 |
+
const error = new Error('aborted');
|
| 107 |
+
error.name = 'AbortError';
|
| 108 |
+
reject(error);
|
| 109 |
+
});
|
| 110 |
+
})
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
assert.equal(result.summary.agent_diagnostics_checked, true);
|
| 114 |
+
assert.equal(result.summary.agent_diagnostics_found, false);
|
| 115 |
+
assert.equal(result.summary.agent_diagnostics_unavailable_reason, 'diagnostics_timeout');
|
| 116 |
+
assert.equal(result.failureOutput.agent_failure_diagnostics.unavailable_reason, 'diagnostics_timeout');
|
| 117 |
+
assert.equal(result.summary.retryable, true);
|
| 118 |
+
assert.equal(result.summary.next_action, 'retry_after_wait');
|
| 119 |
+
assert.ok(Date.now() - startedAt < 1000);
|
| 120 |
+
});
|
| 121 |
+
|
| 122 |
+
it('reports non-json Agent diagnostics responses distinctly', async () => {
|
| 123 |
+
const result = await enrichFailureWithAgentDiagnostics({
|
| 124 |
+
baseUrl: 'http://example.test/playground',
|
| 125 |
+
authHeaders: () => ({}),
|
| 126 |
+
idempotencyKey: 'html-diagnostics-key',
|
| 127 |
+
failureOutput: { ok: false, error: { code: 'network_error' } },
|
| 128 |
+
summary: { ok: false },
|
| 129 |
+
timeoutMs: 420000,
|
| 130 |
+
fetchFn: async () => ({
|
| 131 |
+
ok: false,
|
| 132 |
+
status: 502,
|
| 133 |
+
headers: new Headers({ 'content-type': 'text/html' }),
|
| 134 |
+
text: async () => '<html>bad gateway</html>'
|
| 135 |
+
})
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
assert.equal(result.summary.agent_diagnostics_checked, true);
|
| 139 |
+
assert.equal(result.summary.agent_diagnostics_found, false);
|
| 140 |
+
assert.equal(result.summary.agent_diagnostics_unavailable_reason, 'non_json_response');
|
| 141 |
+
assert.equal(result.summary.agent_diagnostics_http_status, 502);
|
| 142 |
+
assert.equal(result.failureOutput.agent_failure_diagnostics.unavailable_reason, 'non_json_response');
|
| 143 |
+
assert.equal(result.failureOutput.agent_failure_diagnostics.http_status, 502);
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
it('treats found Agent diagnostics without payload as invalid', async () => {
|
| 147 |
+
const result = await enrichFailureWithAgentDiagnostics({
|
| 148 |
+
baseUrl: 'http://example.test/playground',
|
| 149 |
+
authHeaders: () => ({}),
|
| 150 |
+
idempotencyKey: 'missing-diagnostics-key',
|
| 151 |
+
failureOutput: { ok: false, error: { code: 'network_error' } },
|
| 152 |
+
summary: { ok: false },
|
| 153 |
+
timeoutMs: 420000,
|
| 154 |
+
fetchFn: async () => ({
|
| 155 |
+
ok: true,
|
| 156 |
+
status: 200,
|
| 157 |
+
headers: new Headers({ 'content-type': 'application/json' }),
|
| 158 |
+
text: async () => JSON.stringify({ found: true })
|
| 159 |
+
})
|
| 160 |
+
});
|
| 161 |
+
|
| 162 |
+
assert.equal(result.summary.agent_diagnostics_checked, true);
|
| 163 |
+
assert.equal(result.summary.agent_diagnostics_found, false);
|
| 164 |
+
assert.equal(result.summary.agent_diagnostics_unavailable_reason, 'invalid_response');
|
| 165 |
+
assert.equal(result.summary.agent_diagnostics_http_status, 200);
|
| 166 |
+
assert.equal(result.failureOutput.agent_failure_diagnostics.found, false);
|
| 167 |
+
assert.equal(result.failureOutput.agent_failure_diagnostics.unavailable_reason, 'invalid_response');
|
| 168 |
+
assert.equal(result.failureOutput.agent_failure_diagnostics.http_status, 200);
|
| 169 |
+
assert.equal(result.failureOutput.agent_failure_diagnostics.request_id, undefined);
|
| 170 |
+
});
|
| 171 |
+
|
| 172 |
it('converts local images to webp by default', () => {
|
| 173 |
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-convert-'));
|
| 174 |
try {
|
|
|
|
| 1222 |
);
|
| 1223 |
});
|
| 1224 |
|
| 1225 |
+
it('enriches failed Agent generate summaries with Agent state diagnostics', async () => {
|
| 1226 |
+
const requests = [];
|
| 1227 |
+
await withServer(
|
| 1228 |
+
(request, response) => {
|
| 1229 |
+
requests.push({ method: request.method, url: request.url });
|
| 1230 |
+
if (request.url === '/api/agent/capabilities') {
|
| 1231 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 1232 |
+
response.end(JSON.stringify({ ok: true }));
|
| 1233 |
+
return;
|
| 1234 |
+
}
|
| 1235 |
+
if (request.url === '/api/agent/images/generate') {
|
| 1236 |
+
response.writeHead(500, { 'content-type': 'application/json' });
|
| 1237 |
+
response.end(JSON.stringify({ error: { code: 'unexpected_error', message: 'fetch failed', retryable: false } }));
|
| 1238 |
+
return;
|
| 1239 |
+
}
|
| 1240 |
+
if (request.url === '/api/agent/diagnostics/requests?idempotency_key=diag-generate-key') {
|
| 1241 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 1242 |
+
response.end(
|
| 1243 |
+
JSON.stringify({
|
| 1244 |
+
found: true,
|
| 1245 |
+
diagnostics: {
|
| 1246 |
+
request: {
|
| 1247 |
+
request_id: 'req_generate_diag',
|
| 1248 |
+
idempotency_key: 'diag-generate-key',
|
| 1249 |
+
status: 'failed'
|
| 1250 |
+
},
|
| 1251 |
+
error: {
|
| 1252 |
+
code: 'unexpected_error',
|
| 1253 |
+
retryable: false,
|
| 1254 |
+
diagnostics: {
|
| 1255 |
+
selected_channel_id: 'channel-a',
|
| 1256 |
+
upstream_host: 'upstream.example.test',
|
| 1257 |
+
transport_error_kind: 'aborted'
|
| 1258 |
+
}
|
| 1259 |
+
}
|
| 1260 |
+
}
|
| 1261 |
+
})
|
| 1262 |
+
);
|
| 1263 |
+
return;
|
| 1264 |
+
}
|
| 1265 |
+
response.writeHead(404, { 'content-type': 'application/json' });
|
| 1266 |
+
response.end(JSON.stringify({ error: 'missing' }));
|
| 1267 |
+
},
|
| 1268 |
+
async (baseUrl) => {
|
| 1269 |
+
const result = await runSkillScriptAsync(
|
| 1270 |
+
'generate-image.mjs',
|
| 1271 |
+
['--allow-billable', '--idempotency-key', 'diag-generate-key', '--size', '1024x1024', 'prompt'],
|
| 1272 |
+
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
|
| 1273 |
+
);
|
| 1274 |
+
|
| 1275 |
+
assert.equal(result.status, 1);
|
| 1276 |
+
assert.equal(result.stdout.trim(), '');
|
| 1277 |
+
const body = JSON.parse(result.stderr);
|
| 1278 |
+
assert.equal(body.summary.request_id, 'req_generate_diag');
|
| 1279 |
+
assert.equal(body.summary.selected_channel_id, 'channel-a');
|
| 1280 |
+
assert.equal(body.summary.upstream_host, 'upstream.example.test');
|
| 1281 |
+
assert.equal(body.summary.transport_error_kind, 'aborted');
|
| 1282 |
+
assert.equal(body.summary.agent_diagnostics_checked, true);
|
| 1283 |
+
assert.equal(body.summary.agent_diagnostics_found, true);
|
| 1284 |
+
assert.equal(body.agent_failure_diagnostics.request_id, 'req_generate_diag');
|
| 1285 |
+
assert.equal(body.agent_failure_diagnostics.status, 'failed');
|
| 1286 |
+
assert.deepEqual(
|
| 1287 |
+
requests.map((item) => `${item.method} ${item.url}`),
|
| 1288 |
+
[
|
| 1289 |
+
'GET /api/agent/capabilities',
|
| 1290 |
+
'POST /api/agent/images/generate',
|
| 1291 |
+
'GET /api/agent/diagnostics/requests?idempotency_key=diag-generate-key'
|
| 1292 |
+
]
|
| 1293 |
+
);
|
| 1294 |
+
}
|
| 1295 |
+
);
|
| 1296 |
+
});
|
| 1297 |
+
|
| 1298 |
+
it('preserves path prefixes when fetching Agent state diagnostics', async () => {
|
| 1299 |
+
const requests = [];
|
| 1300 |
+
await withServer(
|
| 1301 |
+
(request, response) => {
|
| 1302 |
+
requests.push({ method: request.method, url: request.url });
|
| 1303 |
+
if (request.url === '/playground/api/agent/capabilities') {
|
| 1304 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 1305 |
+
response.end(JSON.stringify({ ok: true }));
|
| 1306 |
+
return;
|
| 1307 |
+
}
|
| 1308 |
+
if (request.url === '/playground/api/agent/images/generate') {
|
| 1309 |
+
response.writeHead(500, { 'content-type': 'application/json' });
|
| 1310 |
+
response.end(
|
| 1311 |
+
JSON.stringify({
|
| 1312 |
+
error: { code: 'unexpected_error', message: 'fetch failed', retryable: false }
|
| 1313 |
+
})
|
| 1314 |
+
);
|
| 1315 |
+
return;
|
| 1316 |
+
}
|
| 1317 |
+
if (request.url === '/playground/api/agent/diagnostics/requests?idempotency_key=path-diag-key') {
|
| 1318 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 1319 |
+
response.end(
|
| 1320 |
+
JSON.stringify({
|
| 1321 |
+
found: true,
|
| 1322 |
+
diagnostics: {
|
| 1323 |
+
request: {
|
| 1324 |
+
request_id: 'req_path_diag',
|
| 1325 |
+
idempotency_key: 'path-diag-key',
|
| 1326 |
+
status: 'failed'
|
| 1327 |
+
}
|
| 1328 |
+
}
|
| 1329 |
+
})
|
| 1330 |
+
);
|
| 1331 |
+
return;
|
| 1332 |
+
}
|
| 1333 |
+
response.writeHead(404, { 'content-type': 'application/json' });
|
| 1334 |
+
response.end(JSON.stringify({ error: 'missing' }));
|
| 1335 |
+
},
|
| 1336 |
+
async (baseUrl) => {
|
| 1337 |
+
const result = await runSkillScriptAsync(
|
| 1338 |
+
'generate-image.mjs',
|
| 1339 |
+
['--allow-billable', '--idempotency-key', 'path-diag-key', '--size', '1024x1024', 'prompt'],
|
| 1340 |
+
{ GPT_IMAGE_PLAYGROUND_URL: `${baseUrl}/playground` }
|
| 1341 |
+
);
|
| 1342 |
+
|
| 1343 |
+
assert.equal(result.status, 1);
|
| 1344 |
+
assert.equal(result.stdout.trim(), '');
|
| 1345 |
+
const body = JSON.parse(result.stderr);
|
| 1346 |
+
assert.equal(body.summary.request_id, 'req_path_diag');
|
| 1347 |
+
assert.equal(body.summary.agent_diagnostics_checked, true);
|
| 1348 |
+
assert.equal(body.summary.agent_diagnostics_found, true);
|
| 1349 |
+
assert.deepEqual(
|
| 1350 |
+
requests.map((item) => `${item.method} ${item.url}`),
|
| 1351 |
+
[
|
| 1352 |
+
'GET /playground/api/agent/capabilities',
|
| 1353 |
+
'POST /playground/api/agent/images/generate',
|
| 1354 |
+
'GET /playground/api/agent/diagnostics/requests?idempotency_key=path-diag-key'
|
| 1355 |
+
]
|
| 1356 |
+
);
|
| 1357 |
+
}
|
| 1358 |
+
);
|
| 1359 |
+
});
|
| 1360 |
+
|
| 1361 |
+
it('replaces stale retry guidance when Agent diagnostics report a terminal failure', async () => {
|
| 1362 |
+
await withServer(
|
| 1363 |
+
(request, response) => {
|
| 1364 |
+
if (request.url === '/api/agent/capabilities') {
|
| 1365 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 1366 |
+
response.end(JSON.stringify({ ok: true }));
|
| 1367 |
+
return;
|
| 1368 |
+
}
|
| 1369 |
+
if (request.url === '/api/agent/images/generate') {
|
| 1370 |
+
response.writeHead(503, { 'content-type': 'application/json', 'retry-after': '1' });
|
| 1371 |
+
response.end(
|
| 1372 |
+
JSON.stringify({
|
| 1373 |
+
error: { code: 'network_error', message: 'temporary failure', retryable: true }
|
| 1374 |
+
})
|
| 1375 |
+
);
|
| 1376 |
+
return;
|
| 1377 |
+
}
|
| 1378 |
+
if (request.url === '/api/agent/diagnostics/requests?idempotency_key=terminal-diag-key') {
|
| 1379 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 1380 |
+
response.end(
|
| 1381 |
+
JSON.stringify({
|
| 1382 |
+
found: true,
|
| 1383 |
+
diagnostics: {
|
| 1384 |
+
request: {
|
| 1385 |
+
request_id: 'req_terminal_diag',
|
| 1386 |
+
idempotency_key: 'terminal-diag-key',
|
| 1387 |
+
status: 'failed'
|
| 1388 |
+
},
|
| 1389 |
+
error: {
|
| 1390 |
+
code: 'upstream_failed',
|
| 1391 |
+
retryable: false
|
| 1392 |
+
}
|
| 1393 |
+
}
|
| 1394 |
+
})
|
| 1395 |
+
);
|
| 1396 |
+
return;
|
| 1397 |
+
}
|
| 1398 |
+
response.writeHead(404, { 'content-type': 'application/json' });
|
| 1399 |
+
response.end(JSON.stringify({ error: 'missing' }));
|
| 1400 |
+
},
|
| 1401 |
+
async (baseUrl) => {
|
| 1402 |
+
const result = await runSkillScriptAsync(
|
| 1403 |
+
'generate-image.mjs',
|
| 1404 |
+
['--allow-billable', '--idempotency-key', 'terminal-diag-key', '--size', '1024x1024', 'prompt'],
|
| 1405 |
+
{
|
| 1406 |
+
GPT_IMAGE_AGENT_MAX_ATTEMPTS: '1',
|
| 1407 |
+
GPT_IMAGE_PLAYGROUND_URL: baseUrl
|
| 1408 |
+
}
|
| 1409 |
+
);
|
| 1410 |
+
|
| 1411 |
+
assert.equal(result.status, 1);
|
| 1412 |
+
assert.equal(result.stdout.trim(), '');
|
| 1413 |
+
const body = JSON.parse(result.stderr);
|
| 1414 |
+
assert.equal(body.summary.request_id, 'req_terminal_diag');
|
| 1415 |
+
assert.equal(body.summary.retryable, false);
|
| 1416 |
+
assert.equal(body.summary.next_action, 'diagnose_then_new_idempotency_key');
|
| 1417 |
+
assert.equal(body.agent_failure_diagnostics.retryable, false);
|
| 1418 |
+
}
|
| 1419 |
+
);
|
| 1420 |
+
});
|
| 1421 |
+
|
| 1422 |
+
it('does not report diagnostics lookup failures as upstream transport errors', async () => {
|
| 1423 |
+
await withServer(
|
| 1424 |
+
(request, response) => {
|
| 1425 |
+
if (request.url === '/api/agent/capabilities') {
|
| 1426 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 1427 |
+
response.end(JSON.stringify({ ok: true }));
|
| 1428 |
+
return;
|
| 1429 |
+
}
|
| 1430 |
+
if (request.url === '/api/agent/images/generate') {
|
| 1431 |
+
response.writeHead(500, { 'content-type': 'application/json' });
|
| 1432 |
+
response.end(JSON.stringify({ error: { code: 'unexpected_error', message: 'fetch failed', retryable: false } }));
|
| 1433 |
+
return;
|
| 1434 |
+
}
|
| 1435 |
+
if (request.url === '/api/agent/diagnostics/requests?idempotency_key=diag-unavailable-key') {
|
| 1436 |
+
response.writeHead(500, { 'content-type': 'application/json' });
|
| 1437 |
+
response.end(JSON.stringify({ error: 'diagnostics unavailable' }));
|
| 1438 |
+
return;
|
| 1439 |
+
}
|
| 1440 |
+
response.writeHead(404, { 'content-type': 'application/json' });
|
| 1441 |
+
response.end(JSON.stringify({ error: 'missing' }));
|
| 1442 |
+
},
|
| 1443 |
+
async (baseUrl) => {
|
| 1444 |
+
const result = await runSkillScriptAsync(
|
| 1445 |
+
'generate-image.mjs',
|
| 1446 |
+
['--allow-billable', '--idempotency-key', 'diag-unavailable-key', '--size', '1024x1024', 'prompt'],
|
| 1447 |
+
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
|
| 1448 |
+
);
|
| 1449 |
+
|
| 1450 |
+
assert.equal(result.status, 1);
|
| 1451 |
+
assert.equal(result.stdout.trim(), '');
|
| 1452 |
+
const body = JSON.parse(result.stderr);
|
| 1453 |
+
assert.equal(body.summary.transport_error_kind, null);
|
| 1454 |
+
assert.equal(body.summary.agent_diagnostics_checked, true);
|
| 1455 |
+
assert.equal(body.summary.agent_diagnostics_found, false);
|
| 1456 |
+
assert.equal(body.summary.agent_diagnostics_unavailable_reason, 'status_500');
|
| 1457 |
+
assert.equal(body.agent_failure_diagnostics.unavailable_reason, 'status_500');
|
| 1458 |
+
}
|
| 1459 |
+
);
|
| 1460 |
+
});
|
| 1461 |
+
|
| 1462 |
it('uses page SSE client request id max length declared by capabilities', async () => {
|
| 1463 |
const requests = [];
|
| 1464 |
await withServer(
|
|
|
|
| 1936 |
{
|
| 1937 |
filename: 'agent-off.png',
|
| 1938 |
content_url: '/api/agent/artifacts/artifact-off/content',
|
| 1939 |
+
metadata_url: '/api/agent/artifacts/artifact-off',
|
| 1940 |
+
width: 1254,
|
| 1941 |
+
height: 1254
|
| 1942 |
}
|
| 1943 |
],
|
| 1944 |
timing: { server_elapsed_ms: 4321 }
|
|
|
|
| 1983 |
assert.equal(body.summary.route_mode, 'agent');
|
| 1984 |
assert.deepEqual(body.summary.content_urls, ['/api/agent/artifacts/artifact-off/content']);
|
| 1985 |
assert.deepEqual(body.summary.absolute_content_urls, [`${baseUrl}/api/agent/artifacts/artifact-off/content`]);
|
| 1986 |
+
assert.deepEqual(body.summary.actual_dimensions, { width: 1254, height: 1254 });
|
| 1987 |
+
assert.deepEqual(body.summary.image_dimensions, [{ width: 1254, height: 1254 }]);
|
| 1988 |
assert.equal(typeof body.summary.elapsed_ms, 'number');
|
| 1989 |
assert.equal(body.summary.elapsed_source, 'client_script');
|
| 1990 |
assert.equal(body.summary.server_elapsed_ms, 4321);
|
|
|
|
| 2677 |
}
|
| 2678 |
});
|
| 2679 |
|
| 2680 |
+
it('enriches failed Agent edit summaries with Agent state diagnostics', async () => {
|
| 2681 |
+
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-diagnostics-'));
|
| 2682 |
+
try {
|
| 2683 |
+
const imagePath = join(tempRoot, 'source.png');
|
| 2684 |
+
writeFileSync(imagePath, fakePngBuffer(2, 1));
|
| 2685 |
+
const requests = [];
|
| 2686 |
+
|
| 2687 |
+
await withServer(
|
| 2688 |
+
async (request, response) => {
|
| 2689 |
+
requests.push({ method: request.method, url: request.url });
|
| 2690 |
+
if (request.url === '/api/agent/capabilities') {
|
| 2691 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 2692 |
+
response.end(
|
| 2693 |
+
JSON.stringify({
|
| 2694 |
+
agent_streaming: {
|
| 2695 |
+
page_sse: { supported: true, endpoint: '/api/images' }
|
| 2696 |
+
}
|
| 2697 |
+
})
|
| 2698 |
+
);
|
| 2699 |
+
return;
|
| 2700 |
+
}
|
| 2701 |
+
if (request.url === '/api/agent/images/edit') {
|
| 2702 |
+
await readRequestText(request);
|
| 2703 |
+
response.writeHead(500, { 'content-type': 'application/json' });
|
| 2704 |
+
response.end(JSON.stringify({ error: { code: 'unexpected_error', message: 'edit failed', retryable: false } }));
|
| 2705 |
+
return;
|
| 2706 |
+
}
|
| 2707 |
+
if (request.url === '/api/agent/diagnostics/requests?idempotency_key=diag-edit-key') {
|
| 2708 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 2709 |
+
response.end(
|
| 2710 |
+
JSON.stringify({
|
| 2711 |
+
found: true,
|
| 2712 |
+
diagnostics: {
|
| 2713 |
+
request: {
|
| 2714 |
+
request_id: 'req_edit_diag',
|
| 2715 |
+
idempotency_key: 'diag-edit-key',
|
| 2716 |
+
status: 'failed'
|
| 2717 |
+
},
|
| 2718 |
+
error: {
|
| 2719 |
+
code: 'unexpected_error',
|
| 2720 |
+
retryable: false,
|
| 2721 |
+
diagnostics: {
|
| 2722 |
+
selected_channel_id: 'channel-edit',
|
| 2723 |
+
upstream_host: 'edit-upstream.example.test',
|
| 2724 |
+
transport_error_kind: 'socket_closed'
|
| 2725 |
+
}
|
| 2726 |
+
}
|
| 2727 |
+
}
|
| 2728 |
+
})
|
| 2729 |
+
);
|
| 2730 |
+
return;
|
| 2731 |
+
}
|
| 2732 |
+
response.writeHead(404, { 'content-type': 'application/json' });
|
| 2733 |
+
response.end(JSON.stringify({ error: 'missing' }));
|
| 2734 |
+
},
|
| 2735 |
+
async (baseUrl) => {
|
| 2736 |
+
const result = await runSkillScriptAsync(
|
| 2737 |
+
'edit-image.mjs',
|
| 2738 |
+
[
|
| 2739 |
+
'--allow-billable',
|
| 2740 |
+
'--agent',
|
| 2741 |
+
'--idempotency-key',
|
| 2742 |
+
'diag-edit-key',
|
| 2743 |
+
'--size',
|
| 2744 |
+
'3072x2048',
|
| 2745 |
+
imagePath,
|
| 2746 |
+
'prompt'
|
| 2747 |
+
],
|
| 2748 |
+
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
|
| 2749 |
+
);
|
| 2750 |
+
|
| 2751 |
+
assert.equal(result.status, 1);
|
| 2752 |
+
assert.equal(result.stdout.trim(), '');
|
| 2753 |
+
const body = JSON.parse(result.stderr);
|
| 2754 |
+
assert.equal(body.summary.request_id, 'req_edit_diag');
|
| 2755 |
+
assert.equal(body.summary.selected_channel_id, 'channel-edit');
|
| 2756 |
+
assert.equal(body.summary.upstream_host, 'edit-upstream.example.test');
|
| 2757 |
+
assert.equal(body.summary.transport_error_kind, 'socket_closed');
|
| 2758 |
+
assert.equal(body.summary.agent_diagnostics_checked, true);
|
| 2759 |
+
assert.equal(body.summary.agent_diagnostics_found, true);
|
| 2760 |
+
assert.equal(body.agent_failure_diagnostics.request_id, 'req_edit_diag');
|
| 2761 |
+
assert.deepEqual(
|
| 2762 |
+
requests.map((item) => `${item.method} ${item.url}`),
|
| 2763 |
+
[
|
| 2764 |
+
'GET /api/agent/capabilities',
|
| 2765 |
+
'POST /api/agent/images/edit',
|
| 2766 |
+
'GET /api/agent/diagnostics/requests?idempotency_key=diag-edit-key'
|
| 2767 |
+
]
|
| 2768 |
+
);
|
| 2769 |
+
}
|
| 2770 |
+
);
|
| 2771 |
+
} finally {
|
| 2772 |
+
rmSync(tempRoot, { recursive: true, force: true });
|
| 2773 |
+
}
|
| 2774 |
+
});
|
| 2775 |
+
|
| 2776 |
it('rejects default WebP edit dry-runs when streaming is explicitly disabled', () => {
|
| 2777 |
const result = runSkillScript('edit-image.mjs', [
|
| 2778 |
'--size',
|
|
|
|
| 3199 |
assert.match(skillText, /脚本参数仍写作 `--streaming-strategy responses-sse`/);
|
| 3200 |
assert.match(skillText, /batch JSONL 字段是 `streaming_strategy`/);
|
| 3201 |
assert.match(skillText, /`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段/);
|
| 3202 |
+
assert.match(skillText, /`capabilities` 里声明的 `page_sse_supported=true`/);
|
| 3203 |
+
assert.match(skillText, /`selected_channel_id`、`upstream_host` 为空/);
|
| 3204 |
assert.match(skillText, /支持位置参数 `<image-path> <prompt>`,也支持 `--image <path> <prompt>` 别名/);
|
| 3205 |
assert.match(skillText, /`--image-backend responses-image-generation` 只用于页面 SSE edit/);
|
| 3206 |
assert.match(skillText, /不要把 Matsca `limits\.partial_images=0\.\.4` 误套到 `responses-image-generation`/);
|
| 3207 |
assert.match(apiReference, /limits\.partial_images_by_backend\[image_backend\]/);
|
| 3208 |
+
assert.match(apiReference, /只代表“声明支持”,不代表当前渠道每次实测都能成功/);
|
| 3209 |
+
assert.match(apiReference, /如果 `selected_channel_id`、`upstream_host` 为空/);
|
| 3210 |
assert.match(apiReference, /Agent edit 不接收 `image_backend`、`output_format` 或 `output_compression`/);
|
| 3211 |
assert.match(apiReference, /强制 Agent edit 时输出格式固定为 PNG,`partial_images` 按默认 Images API\/profile 范围校验/);
|
| 3212 |
assert.match(apiReference, /图片路径可以用位置参数 `<image-path> <prompt>`,也可以用 `--image <path> <prompt>`/);
|
|
|
|
| 3244 |
assert.match(readmeText, /npm run env:summary/);
|
| 3245 |
assert.match(readmeText, /verification_scope\.mode=local_planning_only/);
|
| 3246 |
assert.match(readmeText, /Hugging Face Space Secrets 只能写入和列出名称/);
|
| 3247 |
+
assert.match(readmeText, /`page_sse_supported=true` 只是声明支持,不代表实测一定成功/);
|
| 3248 |
|
| 3249 |
assert.match(skillText, /不要手动并行启动多个单张脚本/);
|
| 3250 |
assert.match(skillText, /capacity_feedback/);
|
|
|
|
| 3266 |
assert.match(apiReference, /verification_scope\.mode=local_planning_only/);
|
| 3267 |
assert.match(apiReference, /manifest_written=false/);
|
| 3268 |
assert.match(apiReference, /Hugging Face Space Secrets 只能写入和列出名称/);
|
| 3269 |
+
assert.match(apiReference, /这个失败表示上游已生成但本地验收未通过/);
|
| 3270 |
+
assert.match(apiReference, /`validation_failure_count>0` 而 `request_failure_count=0`/);
|
| 3271 |
});
|
| 3272 |
|
| 3273 |
it('keeps WebUI page APIs out of the Agent OpenAPI contract', () => {
|
|
|
|
| 3322 |
assert.match(openApiSource, /高分辨率 edit 默认优先使用页面端 \/api\/images form-data SSE/);
|
| 3323 |
});
|
| 3324 |
|
| 3325 |
+
it('requires new probe and diagnostics work to keep API contracts ahead of Skill wrappers', () => {
|
| 3326 |
+
const readmeText = readFileSync(join(repoRoot, 'README.md'), 'utf8');
|
| 3327 |
+
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
|
| 3328 |
+
const apiReference = readFileSync(join(skillRoot, 'references/api.md'), 'utf8');
|
| 3329 |
+
|
| 3330 |
+
assert.match(readmeText, /新增 probe、diagnostics 或路由可观测能力时,先落 API \/ capabilities \/ OpenAPI 契约/);
|
| 3331 |
+
assert.match(readmeText, /Skill 脚本做薄封装/);
|
| 3332 |
+
assert.match(skillText, /新增 probe、diagnostics、路由健康或请求旅程能力时/);
|
| 3333 |
+
assert.match(skillText, /GET \/api\/agent\/capabilities/);
|
| 3334 |
+
assert.match(skillText, /GET \/api\/agent\/openapi\.json/);
|
| 3335 |
+
assert.match(skillText, /\/api\/agent\/diagnostics\/\*/);
|
| 3336 |
+
assert.match(skillText, /Skill 脚本只做薄封装/);
|
| 3337 |
+
assert.match(skillText, /不能复制页面 API、运行态 API 和 Agent API 的边界判断/);
|
| 3338 |
+
assert.match(apiReference, /新增 probe、diagnostics 或健康摘要时/);
|
| 3339 |
+
assert.match(apiReference, /capabilities、OpenAPI 或明确的 Agent 只读端点/);
|
| 3340 |
+
assert.match(apiReference, /不要让脚本自己拼 page API、runtime API 和 Agent API 的边界逻辑/);
|
| 3341 |
+
});
|
| 3342 |
+
|
| 3343 |
it('runs from a copied standalone skill directory outside the repository', () => {
|
| 3344 |
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-playground-agent-'));
|
| 3345 |
const copiedSkillRoot = join(tempRoot, 'gpt-image-playground-agent');
|
|
|
|
| 3443 |
assert.equal(body.manifest_write_reason, 'dry_run');
|
| 3444 |
assert.equal(body.total, 2);
|
| 3445 |
assert.equal(body.concurrency, 1);
|
| 3446 |
+
assert.equal(body.guardrails.ordered_prefix, 'demo');
|
| 3447 |
+
assert.equal(body.guardrails.repeat_ordered_prefix_on_real_run, true);
|
| 3448 |
+
assert.equal(body.guardrails.dimension_check_recommended, true);
|
| 3449 |
+
assert.match(body.guardrails.dimension_check_reason, /--dimension-check/);
|
| 3450 |
assert.equal(body.tasks[0].endpoint, '/api/agent/images/generate');
|
| 3451 |
assert.equal(body.tasks[0].idempotency_key, 'demo-0001-first-item');
|
| 3452 |
assert.equal(body.tasks[0].request.model, 'gpt-image-2');
|
|
|
|
| 3467 |
}
|
| 3468 |
});
|
| 3469 |
|
| 3470 |
+
it('records enriched Agent diagnostics for failed batch tasks and manifests', async () => {
|
| 3471 |
+
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-diagnostics-'));
|
| 3472 |
+
try {
|
| 3473 |
+
const inputPath = join(tempRoot, 'tasks.jsonl');
|
| 3474 |
+
const manifestPath = join(tempRoot, 'manifest.jsonl');
|
| 3475 |
+
writeFileSync(inputPath, JSON.stringify({ id: 'diag-task', prompt: 'prompt', idempotency_key: 'batch-diag-key' }));
|
| 3476 |
+
|
| 3477 |
+
await withServer(
|
| 3478 |
+
(request, response) => {
|
| 3479 |
+
if (request.url === '/api/agent/capabilities') {
|
| 3480 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 3481 |
+
response.end(JSON.stringify({ ok: true }));
|
| 3482 |
+
return;
|
| 3483 |
+
}
|
| 3484 |
+
if (request.url === '/api/agent/images/generate') {
|
| 3485 |
+
response.writeHead(500, { 'content-type': 'application/json' });
|
| 3486 |
+
response.end(JSON.stringify({ error: { code: 'unexpected_error', message: 'fetch failed', retryable: false } }));
|
| 3487 |
+
return;
|
| 3488 |
+
}
|
| 3489 |
+
if (request.url === '/api/agent/diagnostics/requests?idempotency_key=batch-diag-key') {
|
| 3490 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 3491 |
+
response.end(
|
| 3492 |
+
JSON.stringify({
|
| 3493 |
+
found: true,
|
| 3494 |
+
diagnostics: {
|
| 3495 |
+
request: {
|
| 3496 |
+
request_id: 'req_batch_diag',
|
| 3497 |
+
idempotency_key: 'batch-diag-key',
|
| 3498 |
+
status: 'failed'
|
| 3499 |
+
},
|
| 3500 |
+
error: {
|
| 3501 |
+
code: 'unexpected_error',
|
| 3502 |
+
retryable: false,
|
| 3503 |
+
diagnostics: {
|
| 3504 |
+
selected_channel_id: 'channel-b',
|
| 3505 |
+
upstream_host: 'batch-upstream.example.test'
|
| 3506 |
+
}
|
| 3507 |
+
}
|
| 3508 |
+
}
|
| 3509 |
+
})
|
| 3510 |
+
);
|
| 3511 |
+
return;
|
| 3512 |
+
}
|
| 3513 |
+
response.writeHead(404, { 'content-type': 'application/json' });
|
| 3514 |
+
response.end(JSON.stringify({ error: 'missing' }));
|
| 3515 |
+
},
|
| 3516 |
+
async (baseUrl) => {
|
| 3517 |
+
const result = await runSkillScriptAsync(
|
| 3518 |
+
'batch-images.mjs',
|
| 3519 |
+
['--allow-billable', '--input', inputPath, '--manifest', manifestPath, '--max-attempts', '1'],
|
| 3520 |
+
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
|
| 3521 |
+
);
|
| 3522 |
+
|
| 3523 |
+
assert.equal(result.status, 1);
|
| 3524 |
+
assert.equal(result.stderr.trim(), '');
|
| 3525 |
+
const body = JSON.parse(result.stdout);
|
| 3526 |
+
assert.equal(body.results[0].summary.request_id, 'req_batch_diag');
|
| 3527 |
+
assert.equal(body.results[0].summary.selected_channel_id, 'channel-b');
|
| 3528 |
+
assert.equal(body.results[0].summary.upstream_host, 'batch-upstream.example.test');
|
| 3529 |
+
assert.equal(body.results[0].summary.agent_diagnostics_checked, true);
|
| 3530 |
+
assert.equal(body.results[0].agent_failure_diagnostics.request_id, 'req_batch_diag');
|
| 3531 |
+
const manifestLines = readFileSync(manifestPath, 'utf8').trim().split(/\r?\n/).map(JSON.parse);
|
| 3532 |
+
assert.equal(manifestLines[0].summary.request_id, 'req_batch_diag');
|
| 3533 |
+
assert.equal(manifestLines[0].agent_failure_diagnostics.request_id, 'req_batch_diag');
|
| 3534 |
+
}
|
| 3535 |
+
);
|
| 3536 |
+
} finally {
|
| 3537 |
+
rmSync(tempRoot, { recursive: true, force: true });
|
| 3538 |
+
}
|
| 3539 |
+
});
|
| 3540 |
+
|
| 3541 |
it('rejects invalid batch concurrency before dry-run output', () => {
|
| 3542 |
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-concurrency-'));
|
| 3543 |
try {
|
|
|
|
| 4994 |
assert.equal(result.stderr.trim(), '');
|
| 4995 |
const body = JSON.parse(result.stdout);
|
| 4996 |
assert.equal(body.results[0].status, 'succeeded');
|
| 4997 |
+
assert.deepEqual(body.results[0].summary.actual_dimensions, { width: 1024, height: 1024 });
|
| 4998 |
+
assert.deepEqual(body.results[0].summary.image_dimensions, [{ width: 1024, height: 1024 }]);
|
| 4999 |
}
|
| 5000 |
);
|
| 5001 |
} finally {
|
|
|
|
| 5435 |
}
|
| 5436 |
if (request.url === '/api/agent/images/generate') {
|
| 5437 |
response.writeHead(200, { 'content-type': 'application/json' });
|
| 5438 |
+
response.end(
|
| 5439 |
+
JSON.stringify({
|
| 5440 |
+
images: [
|
| 5441 |
+
{ id: 'dim-image-bad', filename: 'dim-bad.png', b64_json: fakePngBase64(512, 512) },
|
| 5442 |
+
{ id: 'dim-image-ok', filename: 'dim-ok.png', b64_json: fakePngBase64(1024, 1024) }
|
| 5443 |
+
]
|
| 5444 |
+
})
|
| 5445 |
+
);
|
| 5446 |
return;
|
| 5447 |
}
|
| 5448 |
response.writeHead(404, { 'content-type': 'application/json' });
|
|
|
|
| 5459 |
assert.equal(result.stderr.trim(), '');
|
| 5460 |
const body = JSON.parse(result.stdout);
|
| 5461 |
assert.equal(body.ok, false);
|
| 5462 |
+
assert.equal(body.results[0].error.code, 'dimension_check_failed');
|
| 5463 |
+
assert.match(body.results[0].error.message, /尺寸校验失败/);
|
| 5464 |
+
assert.deepEqual(body.results[0].error.expected_dimensions, { width: 1024, height: 1024 });
|
| 5465 |
+
assert.deepEqual(body.results[0].error.actual_dimensions, { width: 512, height: 512 });
|
| 5466 |
+
assert.equal(body.results[0].validation_failure_kind, 'generated_artifact_failed_dimension_check');
|
| 5467 |
+
assert.equal(body.results[0].response.images[0].b64_json, undefined);
|
| 5468 |
+
assert.equal(body.results[0].response.images[0].b64_json_length, fakePngBase64(512, 512).length);
|
| 5469 |
+
assert.deepEqual(body.results[0].response.images[0].dimensions, { width: 512, height: 512 });
|
| 5470 |
+
assert.equal(body.results[0].response.images[1].b64_json, undefined);
|
| 5471 |
+
assert.equal(body.results[0].response.images[1].b64_json_length, fakePngBase64(1024, 1024).length);
|
| 5472 |
+
assert.deepEqual(body.results[0].response.images[1].dimensions, { width: 1024, height: 1024 });
|
| 5473 |
+
assert.deepEqual(body.results[0].summary.artifact_ids, ['dim-image-bad', 'dim-image-ok']);
|
| 5474 |
+
assert.deepEqual(body.results[0].summary.image_dimensions, [
|
| 5475 |
+
{ width: 512, height: 512 },
|
| 5476 |
+
{ width: 1024, height: 1024 }
|
| 5477 |
+
]);
|
| 5478 |
+
assert.deepEqual(body.results[0].summary.expected_dimensions, { width: 1024, height: 1024 });
|
| 5479 |
+
assert.deepEqual(body.results[0].summary.actual_dimensions, { width: 512, height: 512 });
|
| 5480 |
+
assert.equal(body.results[0].summary.dimension_check_failed, true);
|
| 5481 |
+
assert.equal(body.failure_summary.validation_failure_count, 1);
|
| 5482 |
+
assert.equal(body.failure_summary.request_failure_count, 0);
|
| 5483 |
+
assert.equal(body.failure_summary.tasks[0].failure_kind, 'generated_artifact_failed_dimension_check');
|
| 5484 |
+
assert.deepEqual(body.failure_summary.tasks[0].artifact_ids, ['dim-image-bad', 'dim-image-ok']);
|
| 5485 |
}
|
| 5486 |
);
|
| 5487 |
} finally {
|
|
|
|
| 6237 |
|
| 6238 |
function runSkillScriptAsync(filename, args, env = {}, options = {}) {
|
| 6239 |
return new Promise((resolve) => {
|
| 6240 |
+
const baseEnv = buildIsolatedSkillScriptEnv({ loadPrivateAgentEnv: options.loadPrivateAgentEnv });
|
| 6241 |
const child = spawn(process.execPath, [join(skillScriptsRoot, filename), ...args], {
|
| 6242 |
cwd: repoRoot,
|
| 6243 |
+
env: { ...baseEnv, ...env },
|
| 6244 |
stdio: ['ignore', 'pipe', 'pipe']
|
| 6245 |
});
|
| 6246 |
let stdout = '';
|
scripts/command-center.test.mjs
CHANGED
|
@@ -507,6 +507,11 @@ describe('Command center scripts', () => {
|
|
| 507 |
);
|
| 508 |
assert.equal(report.service.capabilities.page_sse_auth_required, true);
|
| 509 |
assert.equal(report.service.capabilities.page_sse_auth_form_field, 'passwordHash');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
assert.equal(
|
| 511 |
report.checks.find((check) => check.name === 'agent_auth_available_to_process')
|
| 512 |
.auth_in_private_env_file,
|
|
@@ -535,6 +540,7 @@ describe('Command center scripts', () => {
|
|
| 535 |
assert.match(text, /当前进程鉴权:未加载/);
|
| 536 |
assert.match(text, /私有 Agent env:不存在/);
|
| 537 |
assert.match(text, /下一步:/);
|
|
|
|
| 538 |
assert.doesNotMatch(text, /^\{/);
|
| 539 |
});
|
| 540 |
|
|
@@ -718,12 +724,22 @@ describe('Command center scripts', () => {
|
|
| 718 |
assert.equal(body.summary.runtime, 'ok');
|
| 719 |
assert.equal(body.summary.state_backend, 'memory');
|
| 720 |
assert.equal(body.summary.page_sse_auth_ready, false);
|
|
|
|
|
|
|
| 721 |
assert.equal(body.summary.responses_gpt2image_ready, true);
|
|
|
|
| 722 |
assert.equal(body.summary.billable_smoke, 'skipped');
|
| 723 |
assert.equal(body.layers.find((layer) => layer.name === 'billable_smoke').skipped, true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
assert.equal(body.layers.find((layer) => layer.name === 'capabilities').executable_routing_rules, true);
|
|
|
|
| 725 |
assert.equal(body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_required, true);
|
| 726 |
assert.equal(body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_ready, false);
|
|
|
|
| 727 |
assert.match(
|
| 728 |
body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_next_action,
|
| 729 |
/GPT_IMAGE_APP_PASSWORD_HASH/
|
|
@@ -798,6 +814,19 @@ describe('Command center scripts', () => {
|
|
| 798 |
}
|
| 799 |
return;
|
| 800 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
if (request.url === '/api/agent/jobs/images/generate') {
|
| 802 |
response.writeHead(400, { 'content-type': 'application/json' });
|
| 803 |
response.end(
|
|
@@ -824,7 +853,9 @@ describe('Command center scripts', () => {
|
|
| 824 |
const body = parseJsonPayload(result.stdout, 'agent doctor');
|
| 825 |
assert.equal(body.service_base_url_source, 'user_provided');
|
| 826 |
assert.equal(body.interactive_confirmation_required, false);
|
|
|
|
| 827 |
assert.ok(hits.includes('/api/agent/images/generate'));
|
|
|
|
| 828 |
}
|
| 829 |
);
|
| 830 |
});
|
|
|
|
| 507 |
);
|
| 508 |
assert.equal(report.service.capabilities.page_sse_auth_required, true);
|
| 509 |
assert.equal(report.service.capabilities.page_sse_auth_form_field, 'passwordHash');
|
| 510 |
+
assert.equal(report.service.capabilities.page_sse_declared_supported, true);
|
| 511 |
+
assert.equal(report.service.capabilities.page_sse_real_smoke, 'not_run_by_first_run');
|
| 512 |
+
assert.equal(report.service.capabilities.responses_image_backend_real_smoke, 'not_run_by_first_run');
|
| 513 |
+
assert.match(formatFirstRunText(report), /页面 SSE:声明支持,实测=未执行真实 smoke/);
|
| 514 |
+
assert.match(formatFirstRunText(report), /Responses 后端:声明未支持,启用=否,实测=未执行真实 smoke/);
|
| 515 |
assert.equal(
|
| 516 |
report.checks.find((check) => check.name === 'agent_auth_available_to_process')
|
| 517 |
.auth_in_private_env_file,
|
|
|
|
| 540 |
assert.match(text, /当前进程鉴权:未加载/);
|
| 541 |
assert.match(text, /私有 Agent env:不存在/);
|
| 542 |
assert.match(text, /下一步:/);
|
| 543 |
+
assert.doesNotMatch(text, /实测=passed/);
|
| 544 |
assert.doesNotMatch(text, /^\{/);
|
| 545 |
});
|
| 546 |
|
|
|
|
| 724 |
assert.equal(body.summary.runtime, 'ok');
|
| 725 |
assert.equal(body.summary.state_backend, 'memory');
|
| 726 |
assert.equal(body.summary.page_sse_auth_ready, false);
|
| 727 |
+
assert.equal(body.summary.page_sse_declared_supported, true);
|
| 728 |
+
assert.equal(body.summary.page_sse_real_smoke, 'skipped');
|
| 729 |
assert.equal(body.summary.responses_gpt2image_ready, true);
|
| 730 |
+
assert.equal(body.summary.responses_image_backend_declared_supported, true);
|
| 731 |
assert.equal(body.summary.billable_smoke, 'skipped');
|
| 732 |
assert.equal(body.layers.find((layer) => layer.name === 'billable_smoke').skipped, true);
|
| 733 |
+
assert.ok(
|
| 734 |
+
body.layers
|
| 735 |
+
.find((layer) => layer.name === 'billable_smoke')
|
| 736 |
+
.checks.some((check) => check.name === 'responses_page_sse_generate_1k' && check.skipped === true)
|
| 737 |
+
);
|
| 738 |
assert.equal(body.layers.find((layer) => layer.name === 'capabilities').executable_routing_rules, true);
|
| 739 |
+
assert.equal(body.layers.find((layer) => layer.name === 'capabilities').page_sse_declared_supported, true);
|
| 740 |
assert.equal(body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_required, true);
|
| 741 |
assert.equal(body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_ready, false);
|
| 742 |
+
assert.equal(body.layers.find((layer) => layer.name === 'responses_gpt2image_readiness').declared_supported, true);
|
| 743 |
assert.match(
|
| 744 |
body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_next_action,
|
| 745 |
/GPT_IMAGE_APP_PASSWORD_HASH/
|
|
|
|
| 814 |
}
|
| 815 |
return;
|
| 816 |
}
|
| 817 |
+
if (request.url === '/api/images') {
|
| 818 |
+
response.writeHead(200, { 'content-type': 'text/event-stream' });
|
| 819 |
+
response.end(
|
| 820 |
+
[
|
| 821 |
+
'data: {"type":"completed","filename":"doctor-page-sse.png","path":"/api/image/doctor-page-sse.png","output_format":"png"}',
|
| 822 |
+
'',
|
| 823 |
+
'data: {"type":"done","client_request_id":"doctor-page-sse-request","images":[{"filename":"doctor-page-sse.png"}]}',
|
| 824 |
+
'',
|
| 825 |
+
''
|
| 826 |
+
].join('\n')
|
| 827 |
+
);
|
| 828 |
+
return;
|
| 829 |
+
}
|
| 830 |
if (request.url === '/api/agent/jobs/images/generate') {
|
| 831 |
response.writeHead(400, { 'content-type': 'application/json' });
|
| 832 |
response.end(
|
|
|
|
| 853 |
const body = parseJsonPayload(result.stdout, 'agent doctor');
|
| 854 |
assert.equal(body.service_base_url_source, 'user_provided');
|
| 855 |
assert.equal(body.interactive_confirmation_required, false);
|
| 856 |
+
assert.equal(body.summary.page_sse_real_smoke, 'passed');
|
| 857 |
assert.ok(hits.includes('/api/agent/images/generate'));
|
| 858 |
+
assert.ok(hits.includes('/api/images'));
|
| 859 |
}
|
| 860 |
);
|
| 861 |
});
|
scripts/first-run.mjs
CHANGED
|
@@ -317,6 +317,7 @@ function summarizeService(service) {
|
|
| 317 |
}
|
| 318 |
|
| 319 |
function summarizeCapabilitiesBody(body) {
|
|
|
|
| 320 |
return {
|
| 321 |
auth_required: body?.auth?.required === true,
|
| 322 |
auth_schemes: Array.isArray(body?.auth?.schemes) ? body.auth.schemes : [],
|
|
@@ -326,8 +327,12 @@ function summarizeCapabilitiesBody(body) {
|
|
| 326 |
image_storage_mode: body?.storage?.image_storage_mode,
|
| 327 |
agent_jobs_supported: body?.agent_jobs?.supported === true,
|
| 328 |
page_sse_supported: body?.agent_streaming?.page_sse?.supported === true,
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
};
|
| 332 |
}
|
| 333 |
|
|
@@ -412,6 +417,12 @@ export function formatFirstRunText(report) {
|
|
| 412 |
lines.push(
|
| 413 |
`- 页面 SSE 鉴权:${capability.page_sse_auth_required ? `需要 ${capability.page_sse_auth_form_field || 'passwordHash'}` : '不需要'}`
|
| 414 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
lines.push(`- 状态后端:${capability.state_backend || '未知'},图片存储:${capability.image_storage_mode || '未知'}`);
|
| 416 |
}
|
| 417 |
const runtime = report.service?.runtime || {};
|
|
@@ -444,6 +455,16 @@ function formatEndpoint(endpoint) {
|
|
| 444 |
return `失败${endpoint.status ? ` ${endpoint.status}` : ''}${endpoint.error ? ` ${endpoint.error}` : ''}`;
|
| 445 |
}
|
| 446 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
function formatAuthState(auth) {
|
| 448 |
if (!auth) return '未知';
|
| 449 |
if (auth.has_token) return '已加载 token';
|
|
|
|
| 317 |
}
|
| 318 |
|
| 319 |
function summarizeCapabilitiesBody(body) {
|
| 320 |
+
const responsesRequirement = body?.supported?.image_backend_requirements?.['responses-image-generation'];
|
| 321 |
return {
|
| 322 |
auth_required: body?.auth?.required === true,
|
| 323 |
auth_schemes: Array.isArray(body?.auth?.schemes) ? body.auth.schemes : [],
|
|
|
|
| 327 |
image_storage_mode: body?.storage?.image_storage_mode,
|
| 328 |
agent_jobs_supported: body?.agent_jobs?.supported === true,
|
| 329 |
page_sse_supported: body?.agent_streaming?.page_sse?.supported === true,
|
| 330 |
+
page_sse_declared_supported: body?.agent_streaming?.page_sse?.supported === true,
|
| 331 |
+
page_sse_real_smoke: 'not_run_by_first_run',
|
| 332 |
+
upstream_sse_declared_supported: body?.agent_streaming?.upstream_sse?.supported === true,
|
| 333 |
+
responses_image_backend_declared_supported: responsesRequirement?.supported === true,
|
| 334 |
+
responses_image_backend_enabled: responsesRequirement?.enabled === true,
|
| 335 |
+
responses_image_backend_real_smoke: 'not_run_by_first_run'
|
| 336 |
};
|
| 337 |
}
|
| 338 |
|
|
|
|
| 417 |
lines.push(
|
| 418 |
`- 页面 SSE 鉴权:${capability.page_sse_auth_required ? `需要 ${capability.page_sse_auth_form_field || 'passwordHash'}` : '不需要'}`
|
| 419 |
);
|
| 420 |
+
lines.push(
|
| 421 |
+
`- 页面 SSE:声明${capability.page_sse_declared_supported ? '支持' : '未支持'},实测=${formatSmokeState(capability.page_sse_real_smoke)}`
|
| 422 |
+
);
|
| 423 |
+
lines.push(
|
| 424 |
+
`- Responses 后端:声明${capability.responses_image_backend_declared_supported ? '支持' : '未支持'},启用=${capability.responses_image_backend_enabled ? '是' : '否'},实测=${formatSmokeState(capability.responses_image_backend_real_smoke)}`
|
| 425 |
+
);
|
| 426 |
lines.push(`- 状态后端:${capability.state_backend || '未知'},图片存储:${capability.image_storage_mode || '未知'}`);
|
| 427 |
}
|
| 428 |
const runtime = report.service?.runtime || {};
|
|
|
|
| 455 |
return `失败${endpoint.status ? ` ${endpoint.status}` : ''}${endpoint.error ? ` ${endpoint.error}` : ''}`;
|
| 456 |
}
|
| 457 |
|
| 458 |
+
function formatSmokeState(value) {
|
| 459 |
+
const states = {
|
| 460 |
+
not_run_by_first_run: '未执行真实 smoke',
|
| 461 |
+
skipped: '已跳过',
|
| 462 |
+
passed: '通过',
|
| 463 |
+
failed: '失败'
|
| 464 |
+
};
|
| 465 |
+
return states[value] || value || '未知';
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
function formatAuthState(auth) {
|
| 469 |
if (!auth) return '未知';
|
| 470 |
if (auth.has_token) return '已加载 token';
|
skills/gpt-image-playground-agent/SKILL.md
CHANGED
|
@@ -20,7 +20,8 @@ Agent API 是给自动化客户端使用的机器接口,不是自治 Agent 平
|
|
| 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、脱敏请求头、重试和下一步动作;不要再先手查 SQLite、Docker logs 或上游后台。
|
|
|
|
| 24 |
|
| 25 |
## 产品边界
|
| 26 |
|
|
@@ -32,12 +33,13 @@ Agent API 只作为自动化客户端接口,不作为首战场景或用户验
|
|
| 32 |
|
| 33 |
- 先读取 `GET /api/agent/capabilities` 的 `routing_rules`,按机器可读规则选择端点。
|
| 34 |
- 默认 WebP edit 使用页面端 `POST /api/images` form-data SSE 路径,因为 Agent edit 不接收输出格式字段。需要 Responses image_generation edit 时也必须使用页面 SSE,不要用 `--agent`。显式 `--agent` 才使用 `/api/agent/images/edit` Agent multipart 最终 JSON,输出格式固定为 Agent 契约;如果页面流式不可用或失败,先诊断结构化错误,再用新的 `Idempotency-Key` 显式决定是否用 Agent edit 对照。Agent edit 只是对照路径,不保证与页面 SSE 的输出格式和像素尺寸完全一致;尺寸敏感任务必须用 `--dimension-check` 或下载后校验。
|
|
|
|
| 35 |
- 复杂 UI 批量出图优先使用页面端 `POST /api/images` SSE 和 `scripts/batch-images.mjs`;不要手动并行启动多个单张脚本,因为这会绕过 manifest、`--resume`、`capacity_feedback` 和尺寸门禁。需要并发时显式设置 `--concurrency N` 或页面“并发批量”开关,并记录切换原因、失败清单和续跑锚点。
|
| 36 |
- 真实批量并发前先看 `GET /api/runtime-capabilities` 的 `channelQueue.capacityPerCredential` 和 `streamingBatch.recommendedConcurrency`。如果服务端建议并发为 `1`,或返回 `channel_capacity_queue_aborted` / `retry_after_seconds`,同一渠道任务保持 `--concurrency 1`,不要用多个 shell 进程绕过限流。
|
| 37 |
- 复杂 UI、长 prompt、高质量图生图遇到 5 分钟级超时、连接中断或上游 503 时,不要把失败归因到提示词质量;先读 `summary` 和诊断,再用新 key 显式尝试压缩 prompt 或改为 `quality=medium` 的对照请求,并记录这是稳定性取舍。
|
| 38 |
- 长图恢复或需要续跑锚点的生产请求优先使用页面端 `POST /api/images` SSE,保留局部进度和缺最终图诊断。
|
| 39 |
- 普通小图单次文生图使用 `/api/agent/images/generate`;`max_edge>2048` 的单次文生图默认优先走页面端 `/api/images` SSE,流式失败后先诊断,再显式选择 Agent JSON 或 job 路径,不自动回退。
|
| 40 |
-
- 单张文生图使用 `--responses-model`/`--gpt-model`、`--thinking`、`--prompt-optimization` 或 `--force-web` 时走页面端 `/api/images` SSE,因为这些是页面高级字段,不属于 Agent JSON schema。`--responses-model` 必须同时设置 `--image-backend responses-image-generation` 或兼容别名 `responses`;显式 `--agent`、`--job`、`stream_mode=non_stream` 或 `streaming_strategy=off` 会被脚本前置拒绝。
|
| 41 |
- 同一个已进入终态 `failed` 的 `Idempotency-Key` 只会回放失败;重新尝试必须先诊断原因,再创建新的业务操作和新的 key。
|
| 42 |
|
| 43 |
## 执行流程
|
|
@@ -55,7 +57,7 @@ Agent API 只作为自动化客户端接口,不作为首战场景或用户验
|
|
| 55 |
11. 默认使用 `response_mode: "path"`,只在用户明确需要图片内联数据时使用 `base64` 或 `both`。
|
| 56 |
12. 不要把页面端 `POST /api/images` 当成普通 Agent JSON 路径。它是页面表单和 SSE 路径,capabilities 会以 `agent_streaming.page_sse` 单独声明;仅在 `routing_rules` 命中高分辨率 edit、大图单次文生图、复杂 UI 批量、长图恢复、显式页面参数或明确诊断后切换。
|
| 57 |
13. 读取 `agent_jobs`。job 路径只在显式选择时使用;`max_edge>2048` 的单次文生图默认优先走页面端 `/api/images` SSE。
|
| 58 |
-
14. 处理失败时读取结构化 `error.code`、`error.retryable`、`error.diagnostics` 和 `Retry-After`。仅当 `retryable=true` 时等待后重试。
|
| 59 |
15. 返回结果时优先给出 `summary`、`content_url`、`metadata_url`、`absolute_content_url`、`absolute_metadata_url`、产物 ID、尺寸、格式和是否命中幂等缓存。回答“4K 非流式花了多久”时优先读 `summary.elapsed_ms`,服务端返回 timing 时也读 `summary.server_elapsed_ms`。
|
| 60 |
16. 需要查询页面请求后的人工反馈或日志摘要时,使用页面 SSE 的 `clientRequestId` 或脚本复用的 `Idempotency-Key` 调用 `scripts/diagnose-request.mjs --client-request-id ...`;不要直接调用 `/api/logs`。需要查询 Agent state 请求状态时,使用 `scripts/diagnose-request.mjs --agent-request-id ...` 或 `--idempotency-key ...`。
|
| 61 |
|
|
@@ -112,7 +114,7 @@ Authorization: Bearer <token>
|
|
| 112 |
- `scripts/diagnose-request.mjs`:按一个或多个页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断;支持读取批量 manifest 和 `--base-url`,不触发生图计费。
|
| 113 |
- `scripts/probe-upstream-image.mjs`:直接探测上游图片接口连通性。默认只检查 DNS、TLS 和 `/models`,必须添加 `--allow-billable` 才会真实调用 `/images/generations`。
|
| 114 |
|
| 115 |
-
生成、编辑和批量脚本的 dry-run 输出会包含 `verification_scope.mode=local_planning_only`,表示只验证了本地请求构造、参数归一化和静态路由规划;它不会读取远端 capabilities,不会验证远端鉴权、渠道容量或 manifest 写入。生成和编辑 dry-run 还会包含 `routing_guidance`,用于在真实计费前检查当前请求应走 Agent JSON、页面 SSE,或在页面流式失败后先诊断再手动选定后续路径。真实执行输出会包含 `summary`;成功摘要含 `ok=true`、`billable`、`request_id`、`idempotency_key`、`artifact_ids`、`content_urls`、`cached`、`elapsed_ms`、`server_elapsed_ms`、`transport`、`endpoint`、`route_mode`、`image_backend`、`stream_mode`、`streaming_strategy`、`selected_channel_id`、`upstream_host` 和脱敏 `request_headers`。失败摘要含 `transport_error_kind`、`retry_after_ms`、`cooldown_until`、`cooldown_target`、`retryable` 和 `next_action`。
|
| 116 |
所有生成、编辑、批量和探针脚本在 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 契约。
|
| 117 |
|
| 118 |
如果当前上下文位于仓库根目录,管理员侧优先使用顶层命令:
|
|
@@ -197,7 +199,7 @@ node "<skill-root>/scripts/batch-images.mjs" --base-url https://your-space.hf.sp
|
|
| 197 |
node "<skill-root>/scripts/batch-images.mjs" --base-url https://your-space.hf.space --allow-billable --input tasks.jsonl --manifest runs/product-set.manifest.jsonl --resume --dimension-check --max-attempts 2 --concurrency 3
|
| 198 |
```
|
| 199 |
|
| 200 |
-
`--manifest` 使用 JSONL append-only 记录每条任务的 `index`、`id`、`idempotency_key`、`attempt`、`status`、响应或错误以及机器可读 `summary`;`--resume` 会读取已成功记录并跳过同一 `id` 或 `idempotency_key`。dry-run 不写 manifest,输出会声明 `manifest_written=false`
|
| 201 |
|
| 202 |
批量 JSONL 字段按模式区分:`background` 只适用于 `generate`;`image_path`、`image_paths`、`mask_path` 只适用于 `edit`。默认 WebP edit 任务走页面 SSE;如需 Agent edit 固定输出,请拆成单张 `edit-image.mjs --agent`。`output_format`、`format`、`output_compression`、`moderation`、`image_backend`、`streaming_strategy`、`partial_images`、`responsesModel`/`gptModel`/`gpt_model`、`thinking`、`promptOptimization`/`prompt_optimization`、`force_web`/`forceWeb` 可用于页面 SSE 路径。edit 任务设置 `image_backend=responses-image-generation` 时会走页面 SSE;不要把它改成 Agent edit。`responsesModel` 必须同时设置 `image_backend=responses-image-generation` 或兼容值 `responses`。JSONL 字段名必须使用 `streaming_strategy`;`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段,会被脚本在真实请求前拒绝。PNG 搭配 `output_compression` 会在 dry-run 标记 normalization,真实请求不会发送压缩字段。`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`。脚本会在 dry-run 阶段显式拒绝跨模式字段、未知字段和无效路由控制字段。
|
| 203 |
|
|
|
|
| 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 |
|
| 26 |
## 产品边界
|
| 27 |
|
|
|
|
| 33 |
|
| 34 |
- 先读取 `GET /api/agent/capabilities` 的 `routing_rules`,按机器可读规则选择端点。
|
| 35 |
- 默认 WebP edit 使用页面端 `POST /api/images` form-data SSE 路径,因为 Agent edit 不接收输出格式字段。需要 Responses image_generation edit 时也必须使用页面 SSE,不要用 `--agent`。显式 `--agent` 才使用 `/api/agent/images/edit` Agent multipart 最终 JSON,输出格式固定为 Agent 契约;如果页面流式不可用或失败,先诊断结构化错误,再用新的 `Idempotency-Key` 显式决定是否用 Agent edit 对照。Agent edit 只是对照路径,不保证与页面 SSE 的输出格式和像素尺寸完全一致;尺寸敏感任务必须用 `--dimension-check` 或下载后校验。
|
| 36 |
+
- `capabilities` 里声明的 `page_sse_supported=true`、`agent_streaming.upstream_sse.supported=true` 只表示路径被声明支持,不表示当前渠道每次实测都能成功;如果页面 SSE 或 Responses 路径返回 `503`、断流,或 `summary` 里 `selected_channel_id`、`upstream_host` 为空,先诊断结构化错误,再用新的 `Idempotency-Key` 显式切换到 Agent JSON 或 job,不自动回退。
|
| 37 |
- 复杂 UI 批量出图优先使用页面端 `POST /api/images` SSE 和 `scripts/batch-images.mjs`;不要手动并行启动多个单张脚本,因为这会绕过 manifest、`--resume`、`capacity_feedback` 和尺寸门禁。需要并发时显式设置 `--concurrency N` 或页面“并发批量”开关,并记录切换原因、失败清单和续跑锚点。
|
| 38 |
- 真实批量并发前先看 `GET /api/runtime-capabilities` 的 `channelQueue.capacityPerCredential` 和 `streamingBatch.recommendedConcurrency`。如果服务端建议并发为 `1`,或返回 `channel_capacity_queue_aborted` / `retry_after_seconds`,同一渠道任务保持 `--concurrency 1`,不要用多个 shell 进程绕过限流。
|
| 39 |
- 复杂 UI、长 prompt、高质量图生图遇到 5 分钟级超时、连接中断或上游 503 时,不要把失败归因到提示词质量;先读 `summary` 和诊断,再用新 key 显式尝试压缩 prompt 或改为 `quality=medium` 的对照请求,并记录这是稳定性取舍。
|
| 40 |
- 长图恢复或需要续跑锚点的生产请求优先使用页面端 `POST /api/images` SSE,保留局部进度和缺最终图诊断。
|
| 41 |
- 普通小图单次文生图使用 `/api/agent/images/generate`;`max_edge>2048` 的单次文生图默认优先走页面端 `/api/images` SSE,流式失败后先诊断,再显式选择 Agent JSON 或 job 路径,不自动回退。
|
| 42 |
+
- 单张文生图使用 `--responses-model`/`--gpt-model`、`--thinking`、`--prompt-optimization` 或 `--force-web` 时走页面端 `/api/images` SSE,因为这些是页面高级字段,不属于 Agent JSON schema。`--responses-model` 覆盖本次请求的 Responses 顶层模型;未传时使用服务端 `OPENAI_RESPONSES_API_MODEL`。该字段只影响本项目 `responses-image-generation` 路径,不改变兼容上游自身 Images API 桥接层内部选择的模型。`--responses-model` 必须同时设置 `--image-backend responses-image-generation` 或兼容别名 `responses`;显式 `--agent`、`--job`、`stream_mode=non_stream` 或 `streaming_strategy=off` 会被脚本前置拒绝。
|
| 43 |
- 同一个已进入终态 `failed` 的 `Idempotency-Key` 只会回放失败;重新尝试必须先诊断原因,再创建新的业务操作和新的 key。
|
| 44 |
|
| 45 |
## 执行流程
|
|
|
|
| 57 |
11. 默认使用 `response_mode: "path"`,只在用户明确需要图片内联数据时使用 `base64` 或 `both`。
|
| 58 |
12. 不要把页面端 `POST /api/images` 当成普通 Agent JSON 路径。它是页面表单和 SSE 路径,capabilities 会以 `agent_streaming.page_sse` 单独声明;仅在 `routing_rules` 命中高分辨率 edit、大图单次文生图、复杂 UI 批量、长图恢复、显式页面参数或明确诊断后切换。
|
| 59 |
13. 读取 `agent_jobs`。job 路径只在显式选择时使用;`max_edge>2048` 的单次文生图默认优先走页面端 `/api/images` SSE。
|
| 60 |
+
14. 处理失败时读取结构化 `error.code`、`error.retryable`、`error.diagnostics` 和 `Retry-After`。仅当 `retryable=true` 时等待后重试。页面 SSE 返回 `503`、断流,或 `summary` 里的 `selected_channel_id`、`upstream_host` 为空时,先按结构化失败诊断,再用新 key 显式换路径,不要把它当成已自动回退成功。
|
| 61 |
15. 返回结果时优先给出 `summary`、`content_url`、`metadata_url`、`absolute_content_url`、`absolute_metadata_url`、产物 ID、尺寸、格式和是否命中幂等缓存。回答“4K 非流式花了多久”时优先读 `summary.elapsed_ms`,服务端返回 timing 时也读 `summary.server_elapsed_ms`。
|
| 62 |
16. 需要查询页面请求后的人工反馈或日志摘要时,使用页面 SSE 的 `clientRequestId` 或脚本复用的 `Idempotency-Key` 调用 `scripts/diagnose-request.mjs --client-request-id ...`;不要直接调用 `/api/logs`。需要查询 Agent state 请求状态时,使用 `scripts/diagnose-request.mjs --agent-request-id ...` 或 `--idempotency-key ...`。
|
| 63 |
|
|
|
|
| 114 |
- `scripts/diagnose-request.mjs`:按一个或多个页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断;支持读取批量 manifest 和 `--base-url`,不触发生图计费。
|
| 115 |
- `scripts/probe-upstream-image.mjs`:直接探测上游图片接口连通性。默认只检查 DNS、TLS 和 `/models`,必须添加 `--allow-billable` 才会真实调用 `/images/generations`。
|
| 116 |
|
| 117 |
+
生成、编辑和批量脚本的 dry-run 输出会包含 `verification_scope.mode=local_planning_only`,表示只验证了本地请求构造、参数归一化和静态路由规划;它不会读取远端 capabilities,不会验证远端鉴权、渠道容量或 manifest 写入。生成和编辑 dry-run 还会包含 `routing_guidance`,用于在真实计费前检查当前请求应走 Agent JSON、页面 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`、`selected_channel_id`、`upstream_host` 和脱敏 `request_headers`。失败摘要含 `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`,便于人工审查。
|
| 118 |
所有生成、编辑、批量和探针脚本在 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 契约。
|
| 119 |
|
| 120 |
如果当前上下文位于仓库根目录,管理员侧优先使用顶层命令:
|
|
|
|
| 199 |
node "<skill-root>/scripts/batch-images.mjs" --base-url https://your-space.hf.space --allow-billable --input tasks.jsonl --manifest runs/product-set.manifest.jsonl --resume --dimension-check --max-attempts 2 --concurrency 3
|
| 200 |
```
|
| 201 |
|
| 202 |
+
`--manifest` 使用 JSONL append-only 记录每条任务的 `index`、`id`、`idempotency_key`、`attempt`、`status`、响应或错误以及机器可读 `summary`;Agent JSON 失败时 manifest 也会记录只读诊断补采样得到的 `agent_failure_diagnostics`。`--resume` 会读取已成功记录并跳过同一 `id` 或 `idempotency_key`。dry-run 不写 manifest,输出会声明 `manifest_written=false`、`manifest_write_reason=dry_run` 和 `guardrails`;真实执行应复用 dry-run 中的同一个 `--ordered-prefix`,否则未显式写 `idempotency_key` 的任务会生成不同 key。`--dimension-check` 会读取响应里的 `b64_json` 或同 origin `content_url`,校验 PNG/JPEG/WebP 尺寸是否等于任务 `size`;通过时成功摘要写入 `image_dimensions` 和单图 `actual_dimensions`,失败时 `error.code=dimension_check_failed`、`validation_failure_kind=generated_artifact_failed_dimension_check`、产物 URL、`summary.expected_dimensions`、`summary.actual_dimensions`、`summary.dimension_check_failed=true` 会同时写入 manifest。固定尺寸任务没有开启时,dry-run 会在 `guardrails.dimension_check_recommended` 中提示。`--max-attempts` 会为第二次及以后尝试追加新的 attempt 级 idempotency key,避免复用终态失败 key;`--concurrency` 大于 `1` 时会先读取运行态并发建议,并发执行任务并按输入顺序输出结果。服务端 `recommendedConcurrency` 或 `channelQueue.capacityPerCredential` 小于请求值时,脚本会把有效并发降到建议值并在输出中写入 `capacity_feedback`;不要再另开多个单张脚本绕过这个限制。`failure_summary` 会区分 `validation_failure_count` 和 `request_failure_count`,避免把已生成但验收失败误判为上游调用失败。`--max-consecutive-failures` 会在连续失败达到阈值后跳过后续任务并输出 `failure_summary` 与 `resume_fix_list`,且只能与顺序执行的 `--concurrency 1` 同用。任务级 `sse_log_path` 会把页面 SSE 原始事件按 JSONL 追加保存;即使 fetch 或 SSE 收集阶段失败,也会记录 `request_started`、`request_failed`、`elapsed_ms`、`client_request_id` 和 `endpoint`,便于区分上游未给终图和解析/断流问题。
|
| 203 |
|
| 204 |
批量 JSONL 字段按模式区分:`background` 只适用于 `generate`;`image_path`、`image_paths`、`mask_path` 只适用于 `edit`。默认 WebP edit 任务走页面 SSE;如需 Agent edit 固定输出,请拆成单张 `edit-image.mjs --agent`。`output_format`、`format`、`output_compression`、`moderation`、`image_backend`、`streaming_strategy`、`partial_images`、`responsesModel`/`gptModel`/`gpt_model`、`thinking`、`promptOptimization`/`prompt_optimization`、`force_web`/`forceWeb` 可用于页面 SSE 路径。edit 任务设置 `image_backend=responses-image-generation` 时会走页面 SSE;不要把它改成 Agent edit。`responsesModel` 必须同时设置 `image_backend=responses-image-generation` 或兼容值 `responses`。JSONL 字段名必须使用 `streaming_strategy`;`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段,会被脚本在真实请求前拒绝。PNG 搭配 `output_compression` 会在 dry-run 标记 normalization,真实请求不会发送压缩字段。`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`。脚本会在 dry-run 阶段显式拒绝跨模式字段、未知字段和无效路由控制字段。
|
| 205 |
|
skills/gpt-image-playground-agent/references/api.md
CHANGED
|
@@ -47,7 +47,7 @@ npm run env:summary -- --file .env.local --container gpt-image-playground-custom
|
|
| 47 |
同一个 `Idempotency-Key` 如果已经进入终态 `failed`,再次调用 generate/edit 或 job result/status 只会回放该失败,且 `retryable=false`。需要重新尝试时应创建新的业务操作和新的 `Idempotency-Key`。
|
| 48 |
页面端 `/api/images` SSE 会把同一个业务 key 复用到 `clientRequestId`,因此脚本使用的 `Idempotency-Key` 不能超过 capabilities 中 `agent_streaming.page_sse.client_request_id.max_length` 声明的字符数;超长时会直接报错,不会静默截断。
|
| 49 |
脚本会在 dry-run 和真实请求前前置校验 `--size` 或 JSONL `size`。`gpt-image-2` 支持 `auto` 或任意正整数 `WIDTHxHEIGHT`;默认 OpenAI-compatible 上游的更严格尺寸边界由服务端 profile 或真实上游显式报错。非 `gpt-image-2` 模型只接受 `auto`、`1024x1024`、`1536x1024` 或 `1024x1536`。生成、页面编辑、批量和上游探针默认请求 `output_format=webp`、`output_compression=100`。
|
| 50 |
-
真实执行输出会包含机器可读 `summary`。成功摘要包含 `ok`、`billable`、`request_id`、`idempotency_key`、`artifact_ids`、`content_urls`、`absolute_content_urls`、`cached`、`started_at`、`completed_at`、`elapsed_ms`、`server_elapsed_ms`、`elapsed_source`、`elapsed_breakdown`、`transport`、`endpoint`、`route_mode`、`image_backend`、`stream_mode`、`streaming_strategy`、`selected_channel_id`、`upstream_host`、脱敏 `request_headers` 和 `next_action`。失败摘要也稳定包含空数组或 `null` 形式的产物、路由
|
| 51 |
|
| 52 |
生成脚本参数:
|
| 53 |
|
|
@@ -59,7 +59,7 @@ npm run env:summary -- --file .env.local --container gpt-image-playground-custom
|
|
| 59 |
- `--output-compression`:默认 `100`,仅适用于 `jpeg` 或 `webp`。
|
| 60 |
- `--response-mode`:默认 `path`。
|
| 61 |
- `--image-backend`:可选,显式选择 `images-api`、`images`、`responses` 或 `responses-image-generation`。
|
| 62 |
-
- `--responses-model` / `--gpt-model`:页面 SSE 专属字段,
|
| 63 |
- `--thinking`:页面 SSE 专属字段,可选值为 `minimal`、`none`、`low`、`medium`、`high` 或 `xhigh`。
|
| 64 |
- `--prompt-optimization`:页面 SSE 专属字段,必须是 `true` 或 `false`。
|
| 65 |
- `--force-web`:页面 SSE 专属字段,会发送为 form-data `force_web=true`。
|
|
@@ -115,7 +115,7 @@ npm run env:summary -- --file .env.local --container gpt-image-playground-custom
|
|
| 115 |
- `--manifest`:append-only JSONL manifest 路径,默认 `<input>.manifest.jsonl`。
|
| 116 |
- `--resume`:读取 manifest 中已 `succeeded` 的 `id` 或 `idempotency_key` 并跳过。
|
| 117 |
- `--ordered-prefix`:未显式提供 `idempotency_key` 时构造稳定有序 key 的前缀,默认 `batch`。
|
| 118 |
-
- `--dimension-check`:读取响应 `b64_json` 或同 origin `content_url`,校验 PNG/JPEG/WebP 尺寸等于任务 `size`。
|
| 119 |
- `--max-attempts`:失败任务最大尝试次数。第二次及后续尝试会追加新的 attempt 级 `Idempotency-Key`,避免复用终态失败 key。
|
| 120 |
- `--concurrency`:并发执行窗口,默认 `1`。大于 `1` 时会先读取 `/api/runtime-capabilities` 的 `streamingBatch.recommendedConcurrency` 和 `channelQueue.capacityPerCredential`,把有效并发限制到服务端建议值后按输入顺序输出结果;适合已确认渠道容量的批量生产。
|
| 121 |
- `--max-consecutive-failures`:顺序执行下的连续失败熔断阈值,默认 `0` 表示不熔断。只能与 `--concurrency 1` 同用。
|
|
@@ -123,7 +123,7 @@ npm run env:summary -- --file .env.local --container gpt-image-playground-custom
|
|
| 123 |
- `--dry-run`
|
| 124 |
- `--allow-billable`
|
| 125 |
|
| 126 |
-
批量 dry-run 不写 manifest,输出会声明 `manifest_written=false` 和 `
|
| 127 |
|
| 128 |
批量 JSONL 每行字段按 `mode` 区分。`background` 只适用于 `generate`;`image_path`、`image_paths`、`mask_path` 只适用于 `edit`。默认 WebP edit 任务走页面 SSE;如需 Agent edit 固定输出,请拆成单张 `edit-image.mjs --agent`。`output_format`、`format`、`output_compression`、`moderation`、`image_backend`、`streaming_strategy`、`partial_images`、`responsesModel`/`gptModel`/`gpt_model`、`thinking`、`promptOptimization`/`prompt_optimization`、`force_web`/`forceWeb` 可用于页面 SSE 路径。edit 任务设置 `image_backend=responses-image-generation` 时会走页面 SSE;不要把它改成 Agent edit。`responsesModel` 会选择页面 SSE 路径,且必须同时设置 `image_backend=responses-image-generation` 或兼容值 `responses`,因为 Agent JSON 不接收请求级 Responses 顶层模型。JSONL 字段名必须使用 `streaming_strategy`;`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段,会被脚本在真实请求前拒绝。PNG 搭配 `output_compression` 会在 dry-run 标记 normalization,真实请求不会发送压缩字段。`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`。脚本会在 dry-run 阶段显式拒绝跨模式字段、未知字段和无效路由控制字段,避免参数被真实接口忽略。
|
| 129 |
|
|
@@ -191,7 +191,7 @@ GET /api/agent/capabilities
|
|
| 191 |
- `agent_streaming.generate.mode`:当前为 `non_streaming_only`。
|
| 192 |
- `agent_streaming.edit.mode`:当前为 `non_streaming_only`。
|
| 193 |
- `agent_streaming.upstream_sse`:Agent generate/edit 内部消费上游 SSE 的能力,客户端响应仍是最终 `AgentImageResponse` JSON。
|
| 194 |
-
- `agent_streaming.upstream_sse.supported`:布尔值;当服务端支持 Agent 内部上游 SSE 消费时为 `true`,否则为 `false`。客户端只在为 `true` 时发送上游流式控制字段。
|
| 195 |
- `agent_streaming.upstream_sse.request_fields`:兼容旧客户端的字段合集,当前为 `image_backend`、`stream_mode`、`streaming_strategy`、`partial_images`。
|
| 196 |
- `agent_streaming.upstream_sse.request_fields_by_mode.generate`:generate 可发送的上游 SSE 控制字段,当前为 `image_backend`、`stream_mode`、`streaming_strategy`、`partial_images`。
|
| 197 |
- `agent_streaming.upstream_sse.request_fields_by_mode.edit`:edit 可发送的上游 SSE 控制字段,当前为 `stream_mode`、`streaming_strategy`、`partial_images`。
|
|
@@ -200,19 +200,22 @@ GET /api/agent/capabilities
|
|
| 200 |
- `agent_streaming.upstream_sse.streaming_strategies`:支持 `off`、`auto`、`openai-sse`、`newapi-keepalive-sse`、`responses-sse`、`force-sse`。
|
| 201 |
- `agent_streaming.upstream_sse.stream_modes`:支持 `auto`、`stream`、`non_stream`。
|
| 202 |
- `agent_streaming.upstream_sse.activation_strategies`:会真正向上游发送 `stream=true` 的策略,当前包含 `auto`、`openai-sse`、`newapi-keepalive-sse`、`responses-sse`、`force-sse`。
|
| 203 |
-
- `agent_streaming.page_sse`:页面端 `/api/images` 的 form-data SSE 能力,不代表 Agent generate/edit 支持流式。
|
| 204 |
- `agent_streaming.page_sse.auth`:页面 SSE 的独立表单鉴权。`APP_PASSWORD` 已配置时为 `required=true`、`schemes=["form-password-hash"]`、`form_field="passwordHash"`。
|
| 205 |
- `agent_streaming.page_sse.client_request_id`:页面 SSE 的请求 ID 契约。脚本会把 `Idempotency-Key` 写入 form-data `clientRequestId`,最大长度以 `max_length` 为准,当前为 `128`。
|
|
|
|
| 206 |
- `upstream_request_headers.default`:默认上游请求头摘要,包含 `user_agent_effective`、`has_extra_headers`、`allowed_header_names` 和 `configured_header_names`。
|
| 207 |
- `upstream_request_headers.channels`:每个服务端渠道的脱敏请求头摘要。该字段不包含 API key、Authorization 值、Matsca app secret 值或任意 header value。
|
| 208 |
- `routing_rules.high_resolution_edit`:`edit` 且最大边大于 `2048` 时默认优先使用页面端 `/api/images` SSE,页面流式有问题时显式回退。
|
| 209 |
- `routing_rules.complex_ui_batch`:复杂 UI 批量出图推荐使用页面端 `/api/images` SSE。
|
| 210 |
- `routing_rules.long_image_recovery`:长图恢复或续跑锚点场景推荐使用页面端 `/api/images` SSE。
|
| 211 |
- `routing_rules.agent_generate_small_smoke`:普通小图单次文生图默认使用 `/api/agent/images/generate`。
|
| 212 |
-
- `routing_rules.page_sse_large_generate`:`max_edge>2048` 的单次文生图推荐优先使用 `/api/images` SSE,失败后先诊断,再显式选择 `/api/agent/images/generate` 或 job 路径。
|
| 213 |
- `routing_rules.retry_recovery`:终态失败不会用同一 `Idempotency-Key` 重新执行,必须诊断后创建新的业务操作和新的 key。
|
| 214 |
- 批量 JSONL 路由控制字段:`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`;脚本会在 dry-run 阶段拒绝字符串布尔值和未知 transport。
|
| 215 |
- `GET /api/runtime-capabilities` 不属于 Agent capabilities。它是页面工作台读取的运行态能力摘要,用于展示流式默认值、图片上游传输配置、渠道健康、渠道队列、并发建议、Responses 后端 enablement 和缺失环境变量,不进入 Agent OpenAPI。
|
|
|
|
|
|
|
| 216 |
- `defaults.image_backend`:Agent generate 默认 `images-api`。
|
| 217 |
- `defaults.stream_mode`:Agent generate 默认 `auto`。auto 会先尝试内部上游 SSE;无法产出最终图时显式回退并暴露可观测标记。
|
| 218 |
- `defaults.streaming_strategy`:Agent generate 默认 `auto`。
|
|
@@ -673,7 +676,7 @@ node "<skill-root>/scripts/diagnose-request.mjs" --base-url https://your-space.h
|
|
| 673 |
}
|
| 674 |
```
|
| 675 |
|
| 676 |
-
`diagnostics` 只包含脱敏诊断字段和白名单响应头,不包含 API key、token、完整上游响应体或图片 base64。SDK/网络层只有 `Connection error.` 时,`transport_error` 会是 `true`,但不会伪造 `upstream_status`。
|
| 677 |
|
| 678 |
常见错误码:
|
| 679 |
|
|
|
|
| 47 |
同一个 `Idempotency-Key` 如果已经进入终态 `failed`,再次调用 generate/edit 或 job result/status 只会回放该失败,且 `retryable=false`。需要重新尝试时应创建新的业务操作和新的 `Idempotency-Key`。
|
| 48 |
页面端 `/api/images` SSE 会把同一个业务 key 复用到 `clientRequestId`,因此脚本使用的 `Idempotency-Key` 不能超过 capabilities 中 `agent_streaming.page_sse.client_request_id.max_length` 声明的字符数;超长时会直接报错,不会静默截断。
|
| 49 |
脚本会在 dry-run 和真实请求前前置校验 `--size` 或 JSONL `size`。`gpt-image-2` 支持 `auto` 或任意正整数 `WIDTHxHEIGHT`;默认 OpenAI-compatible 上游的更严格尺寸边界由服务端 profile 或真实上游显式报错。非 `gpt-image-2` 模型只接受 `auto`、`1024x1024`、`1536x1024` 或 `1024x1536`。生成、页面编辑、批量和上游探针默认请求 `output_format=webp`、`output_compression=100`。
|
| 50 |
+
真实执行输出会包含机器可读 `summary`。成功摘要包含 `ok`、`billable`、`request_id`、`idempotency_key`、`artifact_ids`、`content_urls`、`absolute_content_urls`、`image_dimensions`、`actual_dimensions`、`cached`、`started_at`、`completed_at`、`elapsed_ms`、`server_elapsed_ms`、`elapsed_source`、`elapsed_breakdown`、`transport`、`endpoint`、`route_mode`、`image_backend`、`stream_mode`、`streaming_strategy`、`selected_channel_id`、`upstream_host`、脱敏 `request_headers` 和 `next_action`。失败摘要也稳定包含空数组或 `null` 形式的产物、路由、渠道和尺寸字段,便于 subagent 按同一模板汇报;尺寸门禁失败属于“上游已生成但本地验收失败”,失败摘要会保留已生成产物的 `artifact_ids`、`content_urls`、`absolute_content_urls` 和 `image_dimensions`。失败摘要还包含 `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`。Agent JSON 失败时脚本会按幂等键只读查询 Agent state;若命中,会把 `request_id`、`selected_channel_id`、`upstream_host`、`transport_error_kind` 合并进首次失败摘要,并输出 `agent_failure_diagnostics`。回答耗时问题时优先读取 `summary.elapsed_ms`;需要区分脚本等待和上游耗时时读取 `summary.elapsed_breakdown`。
|
| 51 |
|
| 52 |
生成脚本参数:
|
| 53 |
|
|
|
|
| 59 |
- `--output-compression`:默认 `100`,仅适用于 `jpeg` 或 `webp`。
|
| 60 |
- `--response-mode`:默认 `path`。
|
| 61 |
- `--image-backend`:可选,显式选择 `images-api`、`images`、`responses` 或 `responses-image-generation`。
|
| 62 |
+
- `--responses-model` / `--gpt-model`:页面 SSE 专属字段,覆盖本次请求的 Responses 顶层模型;未传时使用服务端 `OPENAI_RESPONSES_API_MODEL`。必须同时设置 `--image-backend responses-image-generation` 或兼容别名 `responses`。该字段只影响本项目的 `responses-image-generation` 路径,不改变兼容上游自身 Images API 桥接层内部选择的模型。
|
| 63 |
- `--thinking`:页面 SSE 专属字段,可选值为 `minimal`、`none`、`low`、`medium`、`high` 或 `xhigh`。
|
| 64 |
- `--prompt-optimization`:页面 SSE 专属字段,必须是 `true` 或 `false`。
|
| 65 |
- `--force-web`:页面 SSE 专属字段,会发送为 form-data `force_web=true`。
|
|
|
|
| 115 |
- `--manifest`:append-only JSONL manifest 路径,默认 `<input>.manifest.jsonl`。
|
| 116 |
- `--resume`:读取 manifest 中已 `succeeded` 的 `id` 或 `idempotency_key` 并跳过。
|
| 117 |
- `--ordered-prefix`:未显式提供 `idempotency_key` 时构造稳定有序 key 的前缀,默认 `batch`。
|
| 118 |
+
- `--dimension-check`:读取响应 `b64_json` 或同 origin `content_url`,校验 PNG/JPEG/WebP 尺寸等于任务 `size`;通过时 summary 写入实际尺寸,失败时写入 `error.code=dimension_check_failed`、`validation_failure_kind=generated_artifact_failed_dimension_check`、产物 URL、`expected_dimensions` 和 `actual_dimensions`。这个失败表示上游已生成但本地验收未通过,不等于上游请求失败。
|
| 119 |
- `--max-attempts`:失败任务最大尝试次数。第二次及后续尝试会追加新的 attempt 级 `Idempotency-Key`,避免复用终态失败 key。
|
| 120 |
- `--concurrency`:并发执行窗口,默认 `1`。大于 `1` 时会先读取 `/api/runtime-capabilities` 的 `streamingBatch.recommendedConcurrency` 和 `channelQueue.capacityPerCredential`,把有效并发限制到服务端建议值后按输入顺序输出结果;适合已确认渠道容量的批量生产。
|
| 121 |
- `--max-consecutive-failures`:顺序执行下的连续失败熔断阈值,默认 `0` 表示不熔断。只能与 `--concurrency 1` 同用。
|
|
|
|
| 123 |
- `--dry-run`
|
| 124 |
- `--allow-billable`
|
| 125 |
|
| 126 |
+
批量 dry-run 不写 manifest,输出会声明 `manifest_written=false`、`manifest_write_reason=dry_run` 和 `guardrails`。`guardrails.ordered_prefix` 是本次 dry-run 用于自动生成幂等键的前缀,真实执行应复用同一个 `--ordered-prefix`;`guardrails.dimension_check_recommended=true` 表示输入包含固定尺寸但未启用 `--dimension-check`。只有真实执行时 manifest 才作为 append-only 续跑记录写入;Agent JSON 失败时 manifest 会同时保存增强后的 `summary` 和 `agent_failure_diagnostics`。尺寸门禁失败同样写入结构化 summary 和可审查产物 URL,避免只能从中文错误文本解析期望和实际尺寸。批量总摘要会输出 `failure_summary.validation_failure_count` 和 `failure_summary.request_failure_count`,用于区分“上游已生成但本地验收失败”和“请求未成功完成”。当 `validation_failure_count>0` 而 `request_failure_count=0` 时,要按验收失败处理,不能当成上游不可用。
|
| 127 |
|
| 128 |
批量 JSONL 每行字段按 `mode` 区分。`background` 只适用于 `generate`;`image_path`、`image_paths`、`mask_path` 只适用于 `edit`。默认 WebP edit 任务走页面 SSE;如需 Agent edit 固定输出,请拆成单张 `edit-image.mjs --agent`。`output_format`、`format`、`output_compression`、`moderation`、`image_backend`、`streaming_strategy`、`partial_images`、`responsesModel`/`gptModel`/`gpt_model`、`thinking`、`promptOptimization`/`prompt_optimization`、`force_web`/`forceWeb` 可用于页面 SSE 路径。edit 任务设置 `image_backend=responses-image-generation` 时会走页面 SSE;不要把它改成 Agent edit。`responsesModel` 会选择页面 SSE 路径,且必须同时设置 `image_backend=responses-image-generation` 或兼容值 `responses`,因为 Agent JSON 不接收请求级 Responses 顶层模型。JSONL 字段名必须使用 `streaming_strategy`;`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段,会被脚本在真实请求前拒绝。PNG 搭配 `output_compression` 会在 dry-run 标记 normalization,真实请求不会发送压缩字段。`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`。脚本会在 dry-run 阶段显式拒绝跨模式字段、未知字段和无效路由控制字段,避免参数被真实接口忽略。
|
| 129 |
|
|
|
|
| 191 |
- `agent_streaming.generate.mode`:当前为 `non_streaming_only`。
|
| 192 |
- `agent_streaming.edit.mode`:当前为 `non_streaming_only`。
|
| 193 |
- `agent_streaming.upstream_sse`:Agent generate/edit 内部消费上游 SSE 的能力,客户端响应仍是最终 `AgentImageResponse` JSON。
|
| 194 |
+
- `agent_streaming.upstream_sse.supported`:布尔值;当服务端支持 Agent 内部上游 SSE 消费时为 `true`,否则为 `false`。客户端只在为 `true` 时发送上游流式控制字段。这个字段只代表“声明支持”,不代表当前渠道每次实测都能成功。
|
| 195 |
- `agent_streaming.upstream_sse.request_fields`:兼容旧客户端的字段合集,当前为 `image_backend`、`stream_mode`、`streaming_strategy`、`partial_images`。
|
| 196 |
- `agent_streaming.upstream_sse.request_fields_by_mode.generate`:generate 可发送的上游 SSE 控制字段,当前为 `image_backend`、`stream_mode`、`streaming_strategy`、`partial_images`。
|
| 197 |
- `agent_streaming.upstream_sse.request_fields_by_mode.edit`:edit 可发送的上游 SSE 控制字段,当前为 `stream_mode`、`streaming_strategy`、`partial_images`。
|
|
|
|
| 200 |
- `agent_streaming.upstream_sse.streaming_strategies`:支持 `off`、`auto`、`openai-sse`、`newapi-keepalive-sse`、`responses-sse`、`force-sse`。
|
| 201 |
- `agent_streaming.upstream_sse.stream_modes`:支持 `auto`、`stream`、`non_stream`。
|
| 202 |
- `agent_streaming.upstream_sse.activation_strategies`:会真正向上游发送 `stream=true` 的策略,当前包含 `auto`、`openai-sse`、`newapi-keepalive-sse`、`responses-sse`、`force-sse`。
|
| 203 |
+
- `agent_streaming.page_sse`:页面端 `/api/images` 的 form-data SSE 能力,不代表 Agent generate/edit 支持流式。即使该字段为 `supported=true`,页面 SSE 仍可能在当前渠道返回 `503`、断流或没有选中渠道;这时应先诊断,再显式切换到 Agent JSON 或 job,不自动回退。
|
| 204 |
- `agent_streaming.page_sse.auth`:页面 SSE 的独立表单鉴权。`APP_PASSWORD` 已配置时为 `required=true`、`schemes=["form-password-hash"]`、`form_field="passwordHash"`。
|
| 205 |
- `agent_streaming.page_sse.client_request_id`:页面 SSE 的请求 ID 契约。脚本会把 `Idempotency-Key` 写入 form-data `clientRequestId`,最大长度以 `max_length` 为准,当前为 `128`。
|
| 206 |
+
- 页面 SSE 或 Responses 路径失败时,如果 `selected_channel_id`、`upstream_host` 为空,通常表示请求没有真正落到可执行渠道;先诊断结构化错误,再用新的 `Idempotency-Key` 显式改路由。
|
| 207 |
- `upstream_request_headers.default`:默认上游请求头摘要,包含 `user_agent_effective`、`has_extra_headers`、`allowed_header_names` 和 `configured_header_names`。
|
| 208 |
- `upstream_request_headers.channels`:每个服务端渠道的脱敏请求头摘要。该字段不包含 API key、Authorization 值、Matsca app secret 值或任意 header value。
|
| 209 |
- `routing_rules.high_resolution_edit`:`edit` 且最大边大于 `2048` 时默认优先使用页面端 `/api/images` SSE,页面流式有问题时显式回退。
|
| 210 |
- `routing_rules.complex_ui_batch`:复杂 UI 批量出图推荐使用页面端 `/api/images` SSE。
|
| 211 |
- `routing_rules.long_image_recovery`:长图恢复或续跑锚点场景推荐使用页面端 `/api/images` SSE。
|
| 212 |
- `routing_rules.agent_generate_small_smoke`:普通小图单次文生图默认使用 `/api/agent/images/generate`。
|
| 213 |
+
- `routing_rules.page_sse_large_generate`:`max_edge>2048` 的单次文生图推荐优先使用 `/api/images` SSE,失败后先诊断,再显式选择 `/api/agent/images/generate` 或 job 路径;不要把页面 SSE 的失败解释成自动回退到 Agent JSON 成功。
|
| 214 |
- `routing_rules.retry_recovery`:终态失败不会用同一 `Idempotency-Key` 重新执行,必须诊断后创建新的业务操作和新的 key。
|
| 215 |
- 批量 JSONL 路由控制字段:`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`;脚本会在 dry-run 阶段拒绝字符串布尔值和未知 transport。
|
| 216 |
- `GET /api/runtime-capabilities` 不属于 Agent capabilities。它是页面工作台读取的运行态能力摘要,用于展示流式默认值、图片上游传输配置、渠道健康、渠道队列、并发建议、Responses 后端 enablement 和缺失环境变量,不进入 Agent OpenAPI。
|
| 217 |
+
|
| 218 |
+
新增 probe、diagnostics 或健康摘要时,先把机器契约放进 capabilities、OpenAPI 或明确的 Agent 只读端点,再让脚本消费这些字段;不要让脚本自己拼 page API、runtime API 和 Agent API 的边界逻辑。
|
| 219 |
- `defaults.image_backend`:Agent generate 默认 `images-api`。
|
| 220 |
- `defaults.stream_mode`:Agent generate 默认 `auto`。auto 会先尝试内部上游 SSE;无法产出最终图时显式回退并暴露可观测标记。
|
| 221 |
- `defaults.streaming_strategy`:Agent generate 默认 `auto`。
|
|
|
|
| 676 |
}
|
| 677 |
```
|
| 678 |
|
| 679 |
+
`diagnostics` 只包含脱敏诊断字段和白名单响应头,不包含 API key、token、完整上游响应体或图片 base64。SDK/网络层只有 `Connection error.` 时,`transport_error` 会是 `true`,但不会伪造 `upstream_status`。如果页面 SSE 请求返回 `page_sse_failed`、`503`、断流,且 `summary.selected_channel_id` 与 `summary.upstream_host` 为空,按页面流式路径未跑通处理;先用 `diagnose-request.mjs` 读取结构化摘要,再用新的 `Idempotency-Key` 显式选择 Agent JSON 或 job,不自动回退。
|
| 680 |
|
| 681 |
常见错误码:
|
| 682 |
|
skills/gpt-image-playground-agent/scripts/batch-images.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
#!/usr/bin/env node
|
| 2 |
import { AGENT_ENDPOINTS } from './lib/agent-api-paths.mjs';
|
|
|
|
| 3 |
import {
|
| 4 |
errorMessage,
|
| 5 |
assertValidImageSizeForModel,
|
|
@@ -25,6 +26,7 @@ import {
|
|
| 25 |
postPageSse
|
| 26 |
} from './lib/page-sse-client.mjs';
|
| 27 |
import {
|
|
|
|
| 28 |
buildFailureSummary as buildScriptFailureSummary,
|
| 29 |
buildSuccessSummary,
|
| 30 |
completeScriptTiming,
|
|
@@ -57,6 +59,7 @@ const MAX_IDEMPOTENCY_KEY_LENGTH = 200;
|
|
| 57 |
const DEFAULT_BATCH_MAX_ATTEMPTS = 1;
|
| 58 |
const DEFAULT_MAX_CONSECUTIVE_FAILURES = 0;
|
| 59 |
const DEFAULT_BATCH_CONCURRENCY = 1;
|
|
|
|
| 60 |
const GENERATE_ONLY_FIELDS = [
|
| 61 |
'background'
|
| 62 |
];
|
|
@@ -79,6 +82,21 @@ const PAGE_ADVANCED_FIELDS = [
|
|
| 79 |
const EDIT_ONLY_FIELDS = ['image_path', 'image_paths', 'mask_path'];
|
| 80 |
const BOOLEAN_ROUTING_FIELDS = ['page_sse', 'complex_ui', 'long_image', 'resume_or_recover'];
|
| 81 |
const THINKING_VALUES = new Set(['minimal', 'none', 'low', 'medium', 'high', 'xhigh']);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
const TASK_FIELDS = new Set([
|
| 83 |
'id',
|
| 84 |
'mode',
|
|
@@ -180,6 +198,7 @@ if (!options.allowBillable || options.dryRun) {
|
|
| 180 |
max_attempts: options.maxAttempts,
|
| 181 |
max_consecutive_failures: options.maxConsecutiveFailures,
|
| 182 |
concurrency: options.concurrency,
|
|
|
|
| 183 |
tasks: planned.map((task) => {
|
| 184 |
const routing = buildTaskRouting(task);
|
| 185 |
return {
|
|
@@ -672,13 +691,13 @@ async function runTask(task) {
|
|
| 672 |
const routing = buildTaskRouting(task);
|
| 673 |
const taskTiming = startScriptTiming();
|
| 674 |
try {
|
| 675 |
-
|
| 676 |
routing.transport === 'page_sse'
|
| 677 |
? await postPageSseTask(task, routing)
|
| 678 |
: task.mode === 'edit'
|
| 679 |
? await postEditTask(task)
|
| 680 |
: await postGenerateTask(task);
|
| 681 |
-
if (options.dimensionCheck) await assertDimensions(task, response);
|
| 682 |
const summary = buildSuccessSummary({
|
| 683 |
result: response,
|
| 684 |
routing,
|
|
@@ -698,8 +717,27 @@ async function runTask(task) {
|
|
| 698 |
billable: failure.billable !== false,
|
| 699 |
nextAction: failure.next_step
|
| 700 |
});
|
| 701 |
-
const
|
| 702 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
return output;
|
| 704 |
}
|
| 705 |
}
|
|
@@ -816,6 +854,22 @@ function buildTaskFailureOutput(error, routing) {
|
|
| 816 |
next_step: error.pageSseFailure.next_step
|
| 817 |
};
|
| 818 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
return {
|
| 820 |
...(error?.billable === false ? { billable: false } : {}),
|
| 821 |
error: errorMessage(error),
|
|
@@ -824,6 +878,10 @@ function buildTaskFailureOutput(error, routing) {
|
|
| 824 |
};
|
| 825 |
}
|
| 826 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
function buildCircuitBreakerSkippedTask(task, consecutiveFailures) {
|
| 828 |
return {
|
| 829 |
status: 'skipped',
|
|
@@ -834,6 +892,22 @@ function buildCircuitBreakerSkippedTask(task, consecutiveFailures) {
|
|
| 834 |
};
|
| 835 |
}
|
| 836 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
function buildFailedTaskSummary(result, task) {
|
| 838 |
const error = normalizeFailureError(result.error);
|
| 839 |
return {
|
|
@@ -844,12 +918,21 @@ function buildFailedTaskSummary(result, task) {
|
|
| 844 |
route: result.routing?.transport,
|
| 845 |
endpoint: result.routing?.endpoint,
|
| 846 |
billable: result.billable !== false,
|
|
|
|
| 847 |
code: error.code,
|
| 848 |
message: error.message,
|
|
|
|
|
|
|
|
|
|
| 849 |
next_step: result.next_step || buildFailureNextStep(error)
|
| 850 |
};
|
| 851 |
}
|
| 852 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 853 |
function normalizeFailureError(error) {
|
| 854 |
if (error && typeof error === 'object') {
|
| 855 |
return {
|
|
@@ -871,6 +954,8 @@ function buildFailureSummary(failedTasks) {
|
|
| 871 |
count: failedTasks.length,
|
| 872 |
billable_count: failedTasks.filter((task) => task.billable).length,
|
| 873 |
non_billable_count: failedTasks.filter((task) => !task.billable).length,
|
|
|
|
|
|
|
| 874 |
tasks: failedTasks
|
| 875 |
};
|
| 876 |
}
|
|
@@ -1293,15 +1378,25 @@ function enrichImageUrls(result) {
|
|
| 1293 |
|
| 1294 |
async function assertDimensions(task, response) {
|
| 1295 |
const expected = parseExpectedSize(task.raw.size || (task.mode === 'generate' ? '1024x1024' : undefined));
|
| 1296 |
-
if (!expected) throw new
|
| 1297 |
-
if (!Array.isArray(response.images)) return;
|
|
|
|
|
|
|
| 1298 |
for (const image of response.images) {
|
| 1299 |
const bytes = await readImageBytes(image);
|
| 1300 |
const actual = readImageDimensions(bytes);
|
|
|
|
| 1301 |
if (actual.width !== expected.width || actual.height !== expected.height) {
|
| 1302 |
-
|
| 1303 |
}
|
| 1304 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1305 |
}
|
| 1306 |
|
| 1307 |
async function readImageBytes(image) {
|
|
|
|
| 1 |
#!/usr/bin/env node
|
| 2 |
import { AGENT_ENDPOINTS } from './lib/agent-api-paths.mjs';
|
| 3 |
+
import { enrichFailureWithAgentDiagnostics } from './lib/agent-diagnostics-summary.mjs';
|
| 4 |
import {
|
| 5 |
errorMessage,
|
| 6 |
assertValidImageSizeForModel,
|
|
|
|
| 26 |
postPageSse
|
| 27 |
} from './lib/page-sse-client.mjs';
|
| 28 |
import {
|
| 29 |
+
attachSummary,
|
| 30 |
buildFailureSummary as buildScriptFailureSummary,
|
| 31 |
buildSuccessSummary,
|
| 32 |
completeScriptTiming,
|
|
|
|
| 59 |
const DEFAULT_BATCH_MAX_ATTEMPTS = 1;
|
| 60 |
const DEFAULT_MAX_CONSECUTIVE_FAILURES = 0;
|
| 61 |
const DEFAULT_BATCH_CONCURRENCY = 1;
|
| 62 |
+
const FAILURE_KIND_DIMENSION_CHECK = 'generated_artifact_failed_dimension_check';
|
| 63 |
const GENERATE_ONLY_FIELDS = [
|
| 64 |
'background'
|
| 65 |
];
|
|
|
|
| 82 |
const EDIT_ONLY_FIELDS = ['image_path', 'image_paths', 'mask_path'];
|
| 83 |
const BOOLEAN_ROUTING_FIELDS = ['page_sse', 'complex_ui', 'long_image', 'resume_or_recover'];
|
| 84 |
const THINKING_VALUES = new Set(['minimal', 'none', 'low', 'medium', 'high', 'xhigh']);
|
| 85 |
+
|
| 86 |
+
class DimensionCheckError extends Error {
|
| 87 |
+
constructor(message, details = {}) {
|
| 88 |
+
super(message);
|
| 89 |
+
this.name = 'DimensionCheckError';
|
| 90 |
+
this.code = 'dimension_check_failed';
|
| 91 |
+
this.retryable = false;
|
| 92 |
+
this.billable = true;
|
| 93 |
+
this.nextStep = '确认当前渠道是否支持请求尺寸,或调整任务接受实际返回尺寸;重新执行必须使用新的 Idempotency-Key。';
|
| 94 |
+
this.expectedDimensions = details.expected;
|
| 95 |
+
this.actualDimensions = details.actual;
|
| 96 |
+
this.response = sanitizeResponse(details.response);
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
const TASK_FIELDS = new Set([
|
| 101 |
'id',
|
| 102 |
'mode',
|
|
|
|
| 198 |
max_attempts: options.maxAttempts,
|
| 199 |
max_consecutive_failures: options.maxConsecutiveFailures,
|
| 200 |
concurrency: options.concurrency,
|
| 201 |
+
guardrails: buildDryRunGuardrails(planned, options),
|
| 202 |
tasks: planned.map((task) => {
|
| 203 |
const routing = buildTaskRouting(task);
|
| 204 |
return {
|
|
|
|
| 691 |
const routing = buildTaskRouting(task);
|
| 692 |
const taskTiming = startScriptTiming();
|
| 693 |
try {
|
| 694 |
+
let response =
|
| 695 |
routing.transport === 'page_sse'
|
| 696 |
? await postPageSseTask(task, routing)
|
| 697 |
: task.mode === 'edit'
|
| 698 |
? await postEditTask(task)
|
| 699 |
: await postGenerateTask(task);
|
| 700 |
+
if (options.dimensionCheck) response = await assertDimensions(task, response);
|
| 701 |
const summary = buildSuccessSummary({
|
| 702 |
result: response,
|
| 703 |
routing,
|
|
|
|
| 717 |
billable: failure.billable !== false,
|
| 718 |
nextAction: failure.next_step
|
| 719 |
});
|
| 720 |
+
const failureOutput = { ok: false, status: 'failed', id: task.id, idempotency_key: task.idempotencyKey, ...failure, summary };
|
| 721 |
+
const enriched = shouldEnrichAgentFailure(failure)
|
| 722 |
+
? await enrichFailureWithAgentDiagnostics({
|
| 723 |
+
baseUrl,
|
| 724 |
+
authHeaders,
|
| 725 |
+
idempotencyKey: task.idempotencyKey,
|
| 726 |
+
failureOutput,
|
| 727 |
+
summary,
|
| 728 |
+
timeoutMs
|
| 729 |
+
})
|
| 730 |
+
: { failureOutput, summary };
|
| 731 |
+
const output = attachSummary(enriched.failureOutput, enriched.summary);
|
| 732 |
+
appendManifest(manifestPath, {
|
| 733 |
+
...baseManifestEntry(task),
|
| 734 |
+
status: 'failed',
|
| 735 |
+
...failure,
|
| 736 |
+
...(enriched.failureOutput.agent_failure_diagnostics
|
| 737 |
+
? { agent_failure_diagnostics: enriched.failureOutput.agent_failure_diagnostics }
|
| 738 |
+
: {}),
|
| 739 |
+
summary: enriched.summary
|
| 740 |
+
});
|
| 741 |
return output;
|
| 742 |
}
|
| 743 |
}
|
|
|
|
| 854 |
next_step: error.pageSseFailure.next_step
|
| 855 |
};
|
| 856 |
}
|
| 857 |
+
if (error?.code === 'dimension_check_failed') {
|
| 858 |
+
return {
|
| 859 |
+
billable: error.billable !== false,
|
| 860 |
+
error: {
|
| 861 |
+
code: error.code,
|
| 862 |
+
message: error.message,
|
| 863 |
+
retryable: error.retryable,
|
| 864 |
+
expected_dimensions: error.expectedDimensions,
|
| 865 |
+
actual_dimensions: error.actualDimensions
|
| 866 |
+
},
|
| 867 |
+
validation_failure_kind: FAILURE_KIND_DIMENSION_CHECK,
|
| 868 |
+
response: error.response,
|
| 869 |
+
routing,
|
| 870 |
+
...(typeof error?.nextStep === 'string' ? { next_step: error.nextStep } : {})
|
| 871 |
+
};
|
| 872 |
+
}
|
| 873 |
return {
|
| 874 |
...(error?.billable === false ? { billable: false } : {}),
|
| 875 |
error: errorMessage(error),
|
|
|
|
| 878 |
};
|
| 879 |
}
|
| 880 |
|
| 881 |
+
function shouldEnrichAgentFailure(failure) {
|
| 882 |
+
return failure.billable !== false && failure.routing?.transport === 'agent_json';
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
function buildCircuitBreakerSkippedTask(task, consecutiveFailures) {
|
| 886 |
return {
|
| 887 |
status: 'skipped',
|
|
|
|
| 892 |
};
|
| 893 |
}
|
| 894 |
|
| 895 |
+
function buildDryRunGuardrails(plannedTasks, parsedOptions) {
|
| 896 |
+
const hasExplicitFixedSize = plannedTasks.some((task) => {
|
| 897 |
+
const size = task.raw?.size;
|
| 898 |
+
return typeof size === 'string' && size !== 'auto';
|
| 899 |
+
});
|
| 900 |
+
return {
|
| 901 |
+
ordered_prefix: parsedOptions.orderedPrefix,
|
| 902 |
+
repeat_ordered_prefix_on_real_run: true,
|
| 903 |
+
dimension_check_recommended: hasExplicitFixedSize && !parsedOptions.dimensionCheck,
|
| 904 |
+
dimension_check_reason:
|
| 905 |
+
hasExplicitFixedSize && !parsedOptions.dimensionCheck
|
| 906 |
+
? '输入包含固定尺寸;真实上游可能返回非请求尺寸,尺寸敏感任务应添加 --dimension-check。'
|
| 907 |
+
: null
|
| 908 |
+
};
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
function buildFailedTaskSummary(result, task) {
|
| 912 |
const error = normalizeFailureError(result.error);
|
| 913 |
return {
|
|
|
|
| 918 |
route: result.routing?.transport,
|
| 919 |
endpoint: result.routing?.endpoint,
|
| 920 |
billable: result.billable !== false,
|
| 921 |
+
failure_kind: readFailureKind(result),
|
| 922 |
code: error.code,
|
| 923 |
message: error.message,
|
| 924 |
+
artifact_ids: Array.isArray(result.summary?.artifact_ids) ? result.summary.artifact_ids : [],
|
| 925 |
+
content_urls: Array.isArray(result.summary?.content_urls) ? result.summary.content_urls : [],
|
| 926 |
+
absolute_content_urls: Array.isArray(result.summary?.absolute_content_urls) ? result.summary.absolute_content_urls : [],
|
| 927 |
next_step: result.next_step || buildFailureNextStep(error)
|
| 928 |
};
|
| 929 |
}
|
| 930 |
|
| 931 |
+
function readFailureKind(result) {
|
| 932 |
+
if (typeof result?.validation_failure_kind === 'string') return result.validation_failure_kind;
|
| 933 |
+
return 'request_failed';
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
function normalizeFailureError(error) {
|
| 937 |
if (error && typeof error === 'object') {
|
| 938 |
return {
|
|
|
|
| 954 |
count: failedTasks.length,
|
| 955 |
billable_count: failedTasks.filter((task) => task.billable).length,
|
| 956 |
non_billable_count: failedTasks.filter((task) => !task.billable).length,
|
| 957 |
+
validation_failure_count: failedTasks.filter((task) => task.failure_kind === FAILURE_KIND_DIMENSION_CHECK).length,
|
| 958 |
+
request_failure_count: failedTasks.filter((task) => task.failure_kind !== FAILURE_KIND_DIMENSION_CHECK).length,
|
| 959 |
tasks: failedTasks
|
| 960 |
};
|
| 961 |
}
|
|
|
|
| 1378 |
|
| 1379 |
async function assertDimensions(task, response) {
|
| 1380 |
const expected = parseExpectedSize(task.raw.size || (task.mode === 'generate' ? '1024x1024' : undefined));
|
| 1381 |
+
if (!expected) throw new DimensionCheckError(`${task.id} --dimension-check 需要 size 为 WIDTHxHEIGHT。`);
|
| 1382 |
+
if (!Array.isArray(response.images)) return response;
|
| 1383 |
+
const images = [];
|
| 1384 |
+
let mismatch;
|
| 1385 |
for (const image of response.images) {
|
| 1386 |
const bytes = await readImageBytes(image);
|
| 1387 |
const actual = readImageDimensions(bytes);
|
| 1388 |
+
images.push({ ...image, dimensions: actual });
|
| 1389 |
if (actual.width !== expected.width || actual.height !== expected.height) {
|
| 1390 |
+
mismatch ??= actual;
|
| 1391 |
}
|
| 1392 |
}
|
| 1393 |
+
if (mismatch) {
|
| 1394 |
+
throw new DimensionCheckError(
|
| 1395 |
+
`${task.id} 尺寸校验失败:期望 ${expected.width}x${expected.height},实际 ${mismatch.width}x${mismatch.height}。`,
|
| 1396 |
+
{ expected, actual: mismatch, response: { ...response, images } }
|
| 1397 |
+
);
|
| 1398 |
+
}
|
| 1399 |
+
return { ...response, images };
|
| 1400 |
}
|
| 1401 |
|
| 1402 |
async function readImageBytes(image) {
|
skills/gpt-image-playground-agent/scripts/edit-image.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
| 33 |
normalizeImageBackendForPage,
|
| 34 |
postPageSse
|
| 35 |
} from './lib/page-sse-client.mjs';
|
|
|
|
| 36 |
|
| 37 |
const STREAM_MODES = new Set(['auto', 'stream', 'non_stream']);
|
| 38 |
const STREAMING_STRATEGIES = new Set([
|
|
@@ -668,6 +669,25 @@ if (routingGuidance.transport === 'page_sse') {
|
|
| 668 |
}
|
| 669 |
}
|
| 670 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
| 672 |
let response;
|
| 673 |
let result;
|
|
@@ -720,16 +740,7 @@ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
| 720 |
const failureOutput = { ...lastResult, retry_after: lastRetryAfter };
|
| 721 |
console.error(
|
| 722 |
JSON.stringify(
|
| 723 |
-
|
| 724 |
-
failureOutput,
|
| 725 |
-
buildFailureSummary({
|
| 726 |
-
errorBody: failureOutput,
|
| 727 |
-
routing: { transport: 'agent_json', endpoint: '/api/agent/images/edit' },
|
| 728 |
-
timing: completeScriptTiming(scriptTiming),
|
| 729 |
-
idempotencyKey,
|
| 730 |
-
billable: failureOutput?.billable !== false
|
| 731 |
-
})
|
| 732 |
-
),
|
| 733 |
null,
|
| 734 |
2
|
| 735 |
)
|
|
|
|
| 33 |
normalizeImageBackendForPage,
|
| 34 |
postPageSse
|
| 35 |
} from './lib/page-sse-client.mjs';
|
| 36 |
+
import { enrichFailureWithAgentDiagnostics } from './lib/agent-diagnostics-summary.mjs';
|
| 37 |
|
| 38 |
const STREAM_MODES = new Set(['auto', 'stream', 'non_stream']);
|
| 39 |
const STREAMING_STRATEGIES = new Set([
|
|
|
|
| 669 |
}
|
| 670 |
}
|
| 671 |
|
| 672 |
+
async function buildAgentFailureOutput(output, routing) {
|
| 673 |
+
const summary = buildFailureSummary({
|
| 674 |
+
errorBody: output,
|
| 675 |
+
routing,
|
| 676 |
+
timing: completeScriptTiming(scriptTiming),
|
| 677 |
+
idempotencyKey,
|
| 678 |
+
billable: output?.billable !== false
|
| 679 |
+
});
|
| 680 |
+
const enriched = await enrichFailureWithAgentDiagnostics({
|
| 681 |
+
baseUrl,
|
| 682 |
+
authHeaders,
|
| 683 |
+
idempotencyKey,
|
| 684 |
+
failureOutput: attachSummary(output, summary),
|
| 685 |
+
summary,
|
| 686 |
+
timeoutMs
|
| 687 |
+
});
|
| 688 |
+
return attachSummary(enriched.failureOutput, enriched.summary);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
| 692 |
let response;
|
| 693 |
let result;
|
|
|
|
| 740 |
const failureOutput = { ...lastResult, retry_after: lastRetryAfter };
|
| 741 |
console.error(
|
| 742 |
JSON.stringify(
|
| 743 |
+
await buildAgentFailureOutput(failureOutput, { transport: 'agent_json', endpoint: '/api/agent/images/edit' }),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
null,
|
| 745 |
2
|
| 746 |
)
|
skills/gpt-image-playground-agent/scripts/generate-image.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
#!/usr/bin/env node
|
| 2 |
import { AGENT_ENDPOINTS, buildAgentJobResultPath } from './lib/agent-api-paths.mjs';
|
|
|
|
| 3 |
import {
|
| 4 |
errorMessage,
|
| 5 |
assertValidImageSizeForModel,
|
|
@@ -615,7 +616,10 @@ async function runGenerateRequest(options = {}) {
|
|
| 615 |
|
| 616 |
console.error(
|
| 617 |
JSON.stringify(
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
| 619 |
null,
|
| 620 |
2
|
| 621 |
)
|
|
@@ -1102,6 +1106,19 @@ function buildFailureOutput(output, routing) {
|
|
| 1102 |
);
|
| 1103 |
}
|
| 1104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1105 |
function buildPageSseFailureStatus(error) {
|
| 1106 |
if (error && typeof error === 'object') {
|
| 1107 |
if (Number.isInteger(error.streamStatus)) return { status: error.streamStatus };
|
|
|
|
| 1 |
#!/usr/bin/env node
|
| 2 |
import { AGENT_ENDPOINTS, buildAgentJobResultPath } from './lib/agent-api-paths.mjs';
|
| 3 |
+
import { enrichFailureWithAgentDiagnostics } from './lib/agent-diagnostics-summary.mjs';
|
| 4 |
import {
|
| 5 |
errorMessage,
|
| 6 |
assertValidImageSizeForModel,
|
|
|
|
| 616 |
|
| 617 |
console.error(
|
| 618 |
JSON.stringify(
|
| 619 |
+
await buildAgentFailureOutput(
|
| 620 |
+
{ ...lastResult, retry_after: lastRetryAfter },
|
| 621 |
+
{ transport: 'agent_json', endpoint: AGENT_ENDPOINTS.generate }
|
| 622 |
+
),
|
| 623 |
null,
|
| 624 |
2
|
| 625 |
)
|
|
|
|
| 1106 |
);
|
| 1107 |
}
|
| 1108 |
|
| 1109 |
+
async function buildAgentFailureOutput(output, routing) {
|
| 1110 |
+
const failure = buildFailureOutput(output, routing);
|
| 1111 |
+
const enriched = await enrichFailureWithAgentDiagnostics({
|
| 1112 |
+
baseUrl,
|
| 1113 |
+
authHeaders,
|
| 1114 |
+
idempotencyKey,
|
| 1115 |
+
failureOutput: failure,
|
| 1116 |
+
summary: failure.summary,
|
| 1117 |
+
timeoutMs
|
| 1118 |
+
});
|
| 1119 |
+
return attachSummary(enriched.failureOutput, enriched.summary);
|
| 1120 |
+
}
|
| 1121 |
+
|
| 1122 |
function buildPageSseFailureStatus(error) {
|
| 1123 |
if (error && typeof error === 'object') {
|
| 1124 |
if (Number.isInteger(error.streamStatus)) return { status: error.streamStatus };
|
skills/gpt-image-playground-agent/scripts/lib/agent-diagnostics-summary.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { AGENT_ENDPOINTS } from './agent-api-paths.mjs';
|
| 2 |
+
|
| 3 |
+
const DEFAULT_AGENT_DIAGNOSTICS_TIMEOUT_MS = 5000;
|
| 4 |
+
|
| 5 |
+
export async function enrichFailureWithAgentDiagnostics({
|
| 6 |
+
baseUrl,
|
| 7 |
+
authHeaders,
|
| 8 |
+
idempotencyKey,
|
| 9 |
+
failureOutput,
|
| 10 |
+
summary,
|
| 11 |
+
timeoutMs,
|
| 12 |
+
diagnosticsTimeoutMs,
|
| 13 |
+
fetchFn = fetch
|
| 14 |
+
}) {
|
| 15 |
+
const diagnosticsResult = await fetchAgentDiagnosticsByIdempotencyKey({
|
| 16 |
+
baseUrl,
|
| 17 |
+
authHeaders,
|
| 18 |
+
idempotencyKey,
|
| 19 |
+
timeoutMs,
|
| 20 |
+
diagnosticsTimeoutMs,
|
| 21 |
+
fetchFn
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
const diagnostics = diagnosticsResult.diagnostics;
|
| 25 |
+
const agentFailureDiagnostics = buildAgentFailureDiagnostics(diagnosticsResult, diagnostics);
|
| 26 |
+
|
| 27 |
+
return {
|
| 28 |
+
failureOutput: {
|
| 29 |
+
...failureOutput,
|
| 30 |
+
...(agentFailureDiagnostics ? { agent_failure_diagnostics: agentFailureDiagnostics } : {})
|
| 31 |
+
},
|
| 32 |
+
summary: mergeDiagnosticsIntoSummary(summary, diagnosticsResult, diagnostics)
|
| 33 |
+
};
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
async function fetchAgentDiagnosticsByIdempotencyKey({
|
| 37 |
+
baseUrl,
|
| 38 |
+
authHeaders,
|
| 39 |
+
idempotencyKey,
|
| 40 |
+
timeoutMs,
|
| 41 |
+
diagnosticsTimeoutMs,
|
| 42 |
+
fetchFn
|
| 43 |
+
}) {
|
| 44 |
+
const url = buildAgentEndpointUrl(baseUrl, AGENT_ENDPOINTS.agent_request_diagnostics_lookup);
|
| 45 |
+
url.searchParams.set('idempotency_key', idempotencyKey);
|
| 46 |
+
const controller = new AbortController();
|
| 47 |
+
const timeout = setTimeout(() => controller.abort(), resolveDiagnosticsTimeoutMs(timeoutMs, diagnosticsTimeoutMs));
|
| 48 |
+
try {
|
| 49 |
+
const response = await fetchFn(url, {
|
| 50 |
+
headers: authHeaders(),
|
| 51 |
+
signal: controller.signal
|
| 52 |
+
});
|
| 53 |
+
const text = await response.text();
|
| 54 |
+
if (!isJsonResponse(response) && text) {
|
| 55 |
+
return buildUnavailableDiagnosticsResult('non_json_response', response.status);
|
| 56 |
+
}
|
| 57 |
+
const result = parseJson(text);
|
| 58 |
+
if (response.status === 404 && result?.found === false) {
|
| 59 |
+
return { checked: true, found: false };
|
| 60 |
+
}
|
| 61 |
+
if (!response.ok) {
|
| 62 |
+
return buildUnavailableDiagnosticsResult(`status_${response.status}`, response.status);
|
| 63 |
+
}
|
| 64 |
+
if (!result || typeof result !== 'object' || typeof result.found !== 'boolean') {
|
| 65 |
+
return buildUnavailableDiagnosticsResult('invalid_response', response.status);
|
| 66 |
+
}
|
| 67 |
+
if (result.found === true && !isObject(result.diagnostics)) {
|
| 68 |
+
return buildUnavailableDiagnosticsResult('invalid_response', response.status);
|
| 69 |
+
}
|
| 70 |
+
return {
|
| 71 |
+
checked: true,
|
| 72 |
+
found: result.found,
|
| 73 |
+
diagnostics: result.found ? result.diagnostics : null
|
| 74 |
+
};
|
| 75 |
+
} catch (error) {
|
| 76 |
+
return {
|
| 77 |
+
checked: true,
|
| 78 |
+
found: false,
|
| 79 |
+
unavailable_reason: readFetchErrorKind(error)
|
| 80 |
+
};
|
| 81 |
+
} finally {
|
| 82 |
+
clearTimeout(timeout);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function buildAgentEndpointUrl(baseUrl, endpoint) {
|
| 87 |
+
return new URL(`${String(baseUrl).replace(/\/+$/, '')}${endpoint}`);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function buildUnavailableDiagnosticsResult(unavailableReason, status) {
|
| 91 |
+
return {
|
| 92 |
+
checked: true,
|
| 93 |
+
found: false,
|
| 94 |
+
unavailable_reason: unavailableReason,
|
| 95 |
+
...(Number.isInteger(status) ? { http_status: status } : {})
|
| 96 |
+
};
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
function resolveDiagnosticsTimeoutMs(timeoutMs, diagnosticsTimeoutMs) {
|
| 100 |
+
return Math.min(
|
| 101 |
+
readPositiveInteger(timeoutMs) || DEFAULT_AGENT_DIAGNOSTICS_TIMEOUT_MS,
|
| 102 |
+
readPositiveInteger(diagnosticsTimeoutMs) || DEFAULT_AGENT_DIAGNOSTICS_TIMEOUT_MS
|
| 103 |
+
);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
function isJsonResponse(response) {
|
| 107 |
+
const contentType = typeof response?.headers?.get === 'function' ? response.headers.get('content-type') : '';
|
| 108 |
+
return String(contentType || '').toLowerCase().includes('application/json');
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function buildAgentFailureDiagnostics(result, diagnostics) {
|
| 112 |
+
if (!result?.checked) return undefined;
|
| 113 |
+
if (!result.found) {
|
| 114 |
+
return {
|
| 115 |
+
checked: true,
|
| 116 |
+
found: false,
|
| 117 |
+
unavailable_reason: result.unavailable_reason || null,
|
| 118 |
+
http_status: result.http_status ?? null
|
| 119 |
+
};
|
| 120 |
+
}
|
| 121 |
+
const request = isObject(diagnostics?.request) ? diagnostics.request : undefined;
|
| 122 |
+
const error = isObject(diagnostics?.error) ? diagnostics.error : undefined;
|
| 123 |
+
const errorDiagnostics = isObject(error?.diagnostics) ? error.diagnostics : undefined;
|
| 124 |
+
const response = isObject(diagnostics?.response) ? diagnostics.response : undefined;
|
| 125 |
+
const execution = isObject(response?.execution) ? response.execution : undefined;
|
| 126 |
+
return {
|
| 127 |
+
checked: true,
|
| 128 |
+
found: true,
|
| 129 |
+
request_id: readString(request?.request_id),
|
| 130 |
+
status: readString(request?.status),
|
| 131 |
+
error_code: readString(error?.code),
|
| 132 |
+
retryable: typeof error?.retryable === 'boolean' ? error.retryable : undefined,
|
| 133 |
+
selected_channel_id: readString(errorDiagnostics?.selected_channel_id) || readString(execution?.selected_channel_id),
|
| 134 |
+
upstream_host: readString(errorDiagnostics?.upstream_host) || readString(execution?.upstream_host),
|
| 135 |
+
transport_error_kind: readString(errorDiagnostics?.transport_error_kind)
|
| 136 |
+
};
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
function mergeDiagnosticsIntoSummary(summary, diagnosticsResult, diagnostics) {
|
| 140 |
+
if (!diagnosticsResult?.checked) return summary;
|
| 141 |
+
const request = isObject(diagnostics?.request) ? diagnostics.request : undefined;
|
| 142 |
+
const error = isObject(diagnostics?.error) ? diagnostics.error : undefined;
|
| 143 |
+
const errorDiagnostics = isObject(error?.diagnostics) ? error.diagnostics : undefined;
|
| 144 |
+
const response = isObject(diagnostics?.response) ? diagnostics.response : undefined;
|
| 145 |
+
const execution = isObject(response?.execution) ? response.execution : undefined;
|
| 146 |
+
const diagnosticsRetryable = typeof error?.retryable === 'boolean' ? error.retryable : undefined;
|
| 147 |
+
return {
|
| 148 |
+
...summary,
|
| 149 |
+
request_id: preferExistingString(summary.request_id, request?.request_id) || null,
|
| 150 |
+
selected_channel_id:
|
| 151 |
+
preferExistingString(summary.selected_channel_id, errorDiagnostics?.selected_channel_id, execution?.selected_channel_id) ||
|
| 152 |
+
null,
|
| 153 |
+
upstream_host: preferExistingString(summary.upstream_host, errorDiagnostics?.upstream_host, execution?.upstream_host) || null,
|
| 154 |
+
transport_error_kind: preferExistingString(summary.transport_error_kind, errorDiagnostics?.transport_error_kind) || null,
|
| 155 |
+
retryable: diagnosticsRetryable ?? summary.retryable,
|
| 156 |
+
agent_diagnostics_checked: true,
|
| 157 |
+
agent_diagnostics_found: diagnosticsResult.found === true,
|
| 158 |
+
agent_diagnostics_unavailable_reason: diagnosticsResult.found ? null : diagnosticsResult.unavailable_reason || null,
|
| 159 |
+
agent_diagnostics_http_status: diagnosticsResult.found ? null : diagnosticsResult.http_status ?? null,
|
| 160 |
+
next_action: readDiagnosticsNextAction(summary.next_action, diagnosticsRetryable)
|
| 161 |
+
};
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
function readDiagnosticsNextAction(currentNextAction, diagnosticsRetryable) {
|
| 165 |
+
if (diagnosticsRetryable === true) return 'retry_after_wait';
|
| 166 |
+
if (diagnosticsRetryable === false) return 'diagnose_then_new_idempotency_key';
|
| 167 |
+
return currentNextAction || 'diagnose_then_new_idempotency_key';
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
function preferExistingString(...values) {
|
| 171 |
+
for (const value of values) {
|
| 172 |
+
const stringValue = readString(value);
|
| 173 |
+
if (stringValue) return stringValue;
|
| 174 |
+
}
|
| 175 |
+
return undefined;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
function parseJson(text) {
|
| 179 |
+
try {
|
| 180 |
+
return text ? JSON.parse(text) : null;
|
| 181 |
+
} catch {
|
| 182 |
+
return null;
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
function readFetchErrorKind(error) {
|
| 187 |
+
const name = typeof error?.name === 'string' ? error.name : '';
|
| 188 |
+
if (name === 'AbortError') return 'diagnostics_timeout';
|
| 189 |
+
const causeCode = typeof error?.cause?.code === 'string' ? error.cause.code : '';
|
| 190 |
+
if (causeCode) return causeCode;
|
| 191 |
+
return 'diagnostics_fetch_failed';
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
function isObject(value) {
|
| 195 |
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
function readString(value) {
|
| 199 |
+
return typeof value === 'string' && value ? value : undefined;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
function readPositiveInteger(value) {
|
| 203 |
+
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
| 204 |
+
}
|
skills/gpt-image-playground-agent/scripts/lib/script-summary.mjs
CHANGED
|
@@ -17,6 +17,7 @@ export function buildSuccessSummary({ result, routing, timing, idempotencyKey, b
|
|
| 17 |
const serverTiming = readObject(result?.timing);
|
| 18 |
const execution = readObject(result?.execution);
|
| 19 |
const images = Array.isArray(result?.images) ? result.images : [];
|
|
|
|
| 20 |
const timingSummary = buildTimingSummary({ clientTiming: timing, serverTiming });
|
| 21 |
return stableSummary({
|
| 22 |
ok: true,
|
|
@@ -26,6 +27,8 @@ export function buildSuccessSummary({ result, routing, timing, idempotencyKey, b
|
|
| 26 |
artifact_ids: images.map((image) => image?.id).filter((value) => typeof value === 'string' && value),
|
| 27 |
content_urls: readImageUrls(images, ['content_url', 'path']),
|
| 28 |
absolute_content_urls: readImageUrls(images, ['absolute_content_url', 'absolute_path']),
|
|
|
|
|
|
|
| 29 |
cached: typeof result?.cached === 'boolean' ? result.cached : undefined,
|
| 30 |
started_at: timingSummary.started_at,
|
| 31 |
completed_at: timingSummary.completed_at,
|
|
@@ -51,15 +54,22 @@ export function buildFailureSummary({ errorBody, routing, timing, idempotencyKey
|
|
| 51 |
const error = readObject(errorBody?.error) || readObject(errorBody);
|
| 52 |
const errorMessage = readString(error?.message) || readString(errorBody?.error);
|
| 53 |
const diagnostics = readObject(error?.diagnostics);
|
|
|
|
|
|
|
|
|
|
| 54 |
const timingSummary = buildTimingSummary({ clientTiming: timing, diagnostics });
|
| 55 |
return stableSummary({
|
| 56 |
ok: false,
|
| 57 |
billable,
|
| 58 |
request_id: readString(error?.request_id) || null,
|
| 59 |
idempotency_key: idempotencyKey,
|
| 60 |
-
artifact_ids:
|
| 61 |
-
content_urls: [],
|
| 62 |
-
absolute_content_urls: [],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
started_at: timingSummary.started_at,
|
| 64 |
completed_at: timingSummary.completed_at,
|
| 65 |
elapsed_ms: timingSummary.elapsed_ms,
|
|
@@ -104,6 +114,10 @@ function stableSummary(value) {
|
|
| 104 |
artifact_ids: Array.isArray(value.artifact_ids) ? value.artifact_ids : [],
|
| 105 |
content_urls: Array.isArray(value.content_urls) ? value.content_urls : [],
|
| 106 |
absolute_content_urls: Array.isArray(value.absolute_content_urls) ? value.absolute_content_urls : [],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
route_mode: value.route_mode ?? null,
|
| 108 |
image_backend: value.image_backend ?? null,
|
| 109 |
stream_mode: value.stream_mode ?? null,
|
|
@@ -114,7 +128,11 @@ function stableSummary(value) {
|
|
| 114 |
retry_after_ms: value.retry_after_ms ?? null,
|
| 115 |
retry_after_seconds: value.retry_after_seconds ?? null,
|
| 116 |
cooldown_until: value.cooldown_until ?? null,
|
| 117 |
-
cooldown_target: value.cooldown_target ?? null
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
};
|
| 119 |
}
|
| 120 |
|
|
@@ -154,6 +172,32 @@ function readImageUrls(images, fields) {
|
|
| 154 |
.filter((value) => typeof value === 'string' && value);
|
| 155 |
}
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
function buildTimingSummary({ clientTiming, serverTiming, diagnostics }) {
|
| 158 |
const serverTimingElapsedMs = readNonNegativeNumber(serverTiming?.elapsed_ms);
|
| 159 |
const diagnosticsElapsedMs = readNonNegativeNumber(diagnostics?.elapsed_ms);
|
|
|
|
| 17 |
const serverTiming = readObject(result?.timing);
|
| 18 |
const execution = readObject(result?.execution);
|
| 19 |
const images = Array.isArray(result?.images) ? result.images : [];
|
| 20 |
+
const imageDimensions = readResponseImageDimensions(images);
|
| 21 |
const timingSummary = buildTimingSummary({ clientTiming: timing, serverTiming });
|
| 22 |
return stableSummary({
|
| 23 |
ok: true,
|
|
|
|
| 27 |
artifact_ids: images.map((image) => image?.id).filter((value) => typeof value === 'string' && value),
|
| 28 |
content_urls: readImageUrls(images, ['content_url', 'path']),
|
| 29 |
absolute_content_urls: readImageUrls(images, ['absolute_content_url', 'absolute_path']),
|
| 30 |
+
image_dimensions: imageDimensions,
|
| 31 |
+
actual_dimensions: imageDimensions.length === 1 ? imageDimensions[0] : null,
|
| 32 |
cached: typeof result?.cached === 'boolean' ? result.cached : undefined,
|
| 33 |
started_at: timingSummary.started_at,
|
| 34 |
completed_at: timingSummary.completed_at,
|
|
|
|
| 54 |
const error = readObject(errorBody?.error) || readObject(errorBody);
|
| 55 |
const errorMessage = readString(error?.message) || readString(errorBody?.error);
|
| 56 |
const diagnostics = readObject(error?.diagnostics);
|
| 57 |
+
const response = readObject(errorBody?.response);
|
| 58 |
+
const images = Array.isArray(response?.images) ? response.images : [];
|
| 59 |
+
const imageDimensions = readResponseImageDimensions(images);
|
| 60 |
const timingSummary = buildTimingSummary({ clientTiming: timing, diagnostics });
|
| 61 |
return stableSummary({
|
| 62 |
ok: false,
|
| 63 |
billable,
|
| 64 |
request_id: readString(error?.request_id) || null,
|
| 65 |
idempotency_key: idempotencyKey,
|
| 66 |
+
artifact_ids: images.map((image) => image?.id).filter((value) => typeof value === 'string' && value),
|
| 67 |
+
content_urls: readImageUrls(images, ['content_url', 'path']),
|
| 68 |
+
absolute_content_urls: readImageUrls(images, ['absolute_content_url', 'absolute_path']),
|
| 69 |
+
image_dimensions: imageDimensions,
|
| 70 |
+
expected_dimensions: readDimensionObject(error?.expected_dimensions),
|
| 71 |
+
actual_dimensions: readDimensionObject(error?.actual_dimensions),
|
| 72 |
+
dimension_check_failed: error?.code === 'dimension_check_failed' ? true : undefined,
|
| 73 |
started_at: timingSummary.started_at,
|
| 74 |
completed_at: timingSummary.completed_at,
|
| 75 |
elapsed_ms: timingSummary.elapsed_ms,
|
|
|
|
| 114 |
artifact_ids: Array.isArray(value.artifact_ids) ? value.artifact_ids : [],
|
| 115 |
content_urls: Array.isArray(value.content_urls) ? value.content_urls : [],
|
| 116 |
absolute_content_urls: Array.isArray(value.absolute_content_urls) ? value.absolute_content_urls : [],
|
| 117 |
+
image_dimensions: Array.isArray(value.image_dimensions) ? value.image_dimensions : [],
|
| 118 |
+
expected_dimensions: value.expected_dimensions ?? null,
|
| 119 |
+
actual_dimensions: value.actual_dimensions ?? null,
|
| 120 |
+
dimension_check_failed: value.dimension_check_failed ?? false,
|
| 121 |
route_mode: value.route_mode ?? null,
|
| 122 |
image_backend: value.image_backend ?? null,
|
| 123 |
stream_mode: value.stream_mode ?? null,
|
|
|
|
| 128 |
retry_after_ms: value.retry_after_ms ?? null,
|
| 129 |
retry_after_seconds: value.retry_after_seconds ?? null,
|
| 130 |
cooldown_until: value.cooldown_until ?? null,
|
| 131 |
+
cooldown_target: value.cooldown_target ?? null,
|
| 132 |
+
agent_diagnostics_checked: value.agent_diagnostics_checked ?? false,
|
| 133 |
+
agent_diagnostics_found: value.agent_diagnostics_found ?? false,
|
| 134 |
+
agent_diagnostics_unavailable_reason: value.agent_diagnostics_unavailable_reason ?? null,
|
| 135 |
+
agent_diagnostics_http_status: value.agent_diagnostics_http_status ?? null
|
| 136 |
};
|
| 137 |
}
|
| 138 |
|
|
|
|
| 172 |
.filter((value) => typeof value === 'string' && value);
|
| 173 |
}
|
| 174 |
|
| 175 |
+
function readResponseImageDimensions(images) {
|
| 176 |
+
return images
|
| 177 |
+
.map((image) => readDimensionObject(image?.dimensions) || readDimensionObject(image?.metadata?.dimensions) || readImageTopLevelDimensions(image))
|
| 178 |
+
.filter((value) => value !== undefined);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
function readImageTopLevelDimensions(image) {
|
| 182 |
+
if (!readObject(image)) return undefined;
|
| 183 |
+
if (!Object.prototype.hasOwnProperty.call(image, 'width')) return undefined;
|
| 184 |
+
if (!Object.prototype.hasOwnProperty.call(image, 'height')) return undefined;
|
| 185 |
+
return readDimensionObject(image);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
function readDimensionObject(value) {
|
| 189 |
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
| 190 |
+
const width = readPositiveInteger(value.width);
|
| 191 |
+
const height = readPositiveInteger(value.height);
|
| 192 |
+
if (width === undefined || height === undefined) return undefined;
|
| 193 |
+
return { width, height };
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
function readPositiveInteger(value) {
|
| 197 |
+
if (!Number.isInteger(value) || value <= 0) return undefined;
|
| 198 |
+
return value;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
function buildTimingSummary({ clientTiming, serverTiming, diagnostics }) {
|
| 202 |
const serverTimingElapsedMs = readNonNegativeNumber(serverTiming?.elapsed_ms);
|
| 203 |
const diagnosticsElapsedMs = readNonNegativeNumber(diagnostics?.elapsed_ms);
|
src/app/api/agent/images/edit/route.ts
CHANGED
|
@@ -36,7 +36,7 @@ export async function POST(request: NextRequest) {
|
|
| 36 |
requestHash
|
| 37 |
);
|
| 38 |
if (existingResult) {
|
| 39 |
-
return agentBeginResultResponse(existingResult, store);
|
| 40 |
}
|
| 41 |
const preparation = await prepareAgentEdit(formData, request.headers);
|
| 42 |
const beginResult = await store.beginRequest({
|
|
|
|
| 36 |
requestHash
|
| 37 |
);
|
| 38 |
if (existingResult) {
|
| 39 |
+
return await agentBeginResultResponse(existingResult, store);
|
| 40 |
}
|
| 41 |
const preparation = await prepareAgentEdit(formData, request.headers);
|
| 42 |
const beginResult = await store.beginRequest({
|
src/app/api/agent/images/generate/route.ts
CHANGED
|
@@ -34,7 +34,7 @@ export async function POST(request: NextRequest) {
|
|
| 34 |
requestHash
|
| 35 |
);
|
| 36 |
if (existingResult) {
|
| 37 |
-
return agentBeginResultResponse(existingResult, store);
|
| 38 |
}
|
| 39 |
const preparation = prepareAgentGenerate(imageRequest, request.headers);
|
| 40 |
const beginResult = await store.beginRequest({
|
|
|
|
| 34 |
requestHash
|
| 35 |
);
|
| 36 |
if (existingResult) {
|
| 37 |
+
return await agentBeginResultResponse(existingResult, store);
|
| 38 |
}
|
| 39 |
const preparation = prepareAgentGenerate(imageRequest, request.headers);
|
| 40 |
const beginResult = await store.beginRequest({
|