Spaces:
Running
Running
Deploy 96f3dcf to Docker Space
Browse filesSource: MisonL/gpt-image-playground-customer@96f3dcf3b00cdc76478bfb8c3de537c0af3b636a
- .env.agent.local.example +9 -0
- .gitignore +1 -0
- README.md +73 -3
- package.json +2 -0
- scripts/agent-doctor.mjs +44 -24
- scripts/agent-skill-scripts.test.mjs +148 -0
- scripts/cleanup-docker-fixtures.mjs +174 -0
- scripts/command-center-utils.mjs +6 -0
- scripts/command-center.test.mjs +387 -6
- scripts/first-run.mjs +504 -0
- scripts/status.mjs +2 -0
- skills/gpt-image-playground-agent/SKILL.md +52 -33
- skills/gpt-image-playground-agent/agents/openai.yaml +2 -2
- skills/gpt-image-playground-agent/references/api.md +28 -2
- skills/gpt-image-playground-agent/scripts/batch-images.mjs +24 -3
- skills/gpt-image-playground-agent/scripts/convert-image-format.mjs +13 -1
- skills/gpt-image-playground-agent/scripts/diagnose-request.mjs +12 -4
- skills/gpt-image-playground-agent/scripts/edit-image.mjs +22 -3
- skills/gpt-image-playground-agent/scripts/generate-image.mjs +22 -3
- skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs +23 -0
.env.agent.local.example
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copy to .env.agent.local and keep the real file private.
|
| 2 |
+
# Load it into your shell before running protected Agent scripts.
|
| 3 |
+
#
|
| 4 |
+
# Example:
|
| 5 |
+
# set -a; source .env.agent.local; set +a
|
| 6 |
+
|
| 7 |
+
GPT_IMAGE_PLAYGROUND_URL=http://localhost:4783
|
| 8 |
+
GPT_IMAGE_AGENT_TOKEN=
|
| 9 |
+
GPT_IMAGE_APP_PASSWORD_HASH=
|
.gitignore
CHANGED
|
@@ -36,6 +36,7 @@ dev-server*.log
|
|
| 36 |
.env*
|
| 37 |
!.env.example
|
| 38 |
!.env.agent.example
|
|
|
|
| 39 |
!.env.real-smoke.example
|
| 40 |
|
| 41 |
# vercel
|
|
|
|
| 36 |
.env*
|
| 37 |
!.env.example
|
| 38 |
!.env.agent.example
|
| 39 |
+
!.env.agent.local.example
|
| 40 |
!.env.real-smoke.example
|
| 41 |
|
| 42 |
# vercel
|
README.md
CHANGED
|
@@ -17,6 +17,26 @@ app_port: 4783
|
|
| 17 |
|
| 18 |
## 快速开始
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
推荐 Docker:
|
| 21 |
|
| 22 |
```bash
|
|
@@ -172,12 +192,20 @@ node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
| 172 |
|
| 173 |
AI Agent 集成时优先调用 skill 内置脚本,而不是临时手写 fetch、curl 或表单提交逻辑。脚本会先读取 capabilities,自动处理鉴权、幂等键、路由选择、超时、产物 URL 和结构化失败摘要。
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
1. 只读检查当前服务能力,不触发计费:
|
| 176 |
|
| 177 |
```bash
|
| 178 |
-
GPT_IMAGE_PLAYGROUND_URL=http://localhost:4783 \
|
| 179 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
| 180 |
--contract-check \
|
|
|
|
| 181 |
"capability check"
|
| 182 |
```
|
| 183 |
|
|
@@ -185,6 +213,7 @@ node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
| 185 |
|
| 186 |
```bash
|
| 187 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
|
|
| 188 |
--size 1024x1024 \
|
| 189 |
--quality high \
|
| 190 |
--response-mode path \
|
|
@@ -196,6 +225,7 @@ node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
| 196 |
|
| 197 |
```bash
|
| 198 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
|
|
| 199 |
--allow-billable \
|
| 200 |
--timeout-ms 420000 \
|
| 201 |
--size 1024x1024 \
|
|
@@ -209,6 +239,7 @@ node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
|
| 209 |
|
| 210 |
```bash
|
| 211 |
node skills/gpt-image-playground-agent/scripts/edit-image.mjs \
|
|
|
|
| 212 |
--image ./source.png \
|
| 213 |
--format webp \
|
| 214 |
--output-compression 100 \
|
|
@@ -226,6 +257,7 @@ node skills/gpt-image-playground-agent/scripts/edit-image.mjs \
|
|
| 226 |
|
| 227 |
```bash
|
| 228 |
node skills/gpt-image-playground-agent/scripts/batch-images.mjs \
|
|
|
|
| 229 |
--input tasks.jsonl \
|
| 230 |
--manifest runs/product-set.manifest.jsonl \
|
| 231 |
--resume \
|
|
@@ -247,6 +279,7 @@ npm run env:summary -- --file .env.local --container gpt-image-playground-custom
|
|
| 247 |
|
| 248 |
```bash
|
| 249 |
node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
|
|
|
| 250 |
--idempotency-key agent-demo-generate-001
|
| 251 |
```
|
| 252 |
|
|
@@ -254,18 +287,41 @@ node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
|
| 254 |
|
| 255 |
```bash
|
| 256 |
node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
|
|
|
| 257 |
--client-request-id agent-demo-edit-001
|
| 258 |
```
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
常用环境变量:
|
| 261 |
|
| 262 |
| 变量 | 用途 |
|
| 263 |
| --- | --- |
|
| 264 |
-
| `GPT_IMAGE_PLAYGROUND_URL` | 指向本机、内网或公网部署地址;默认尝试 `http://localhost:4783`。 |
|
| 265 |
| `GPT_IMAGE_AGENT_TOKEN` | Agent Bearer token,对应服务端 `AGENT_API_TOKEN`。 |
|
| 266 |
| `GPT_IMAGE_APP_PASSWORD_HASH` | 使用页面访问码部署时的访问码哈希;页面 SSE 会作为 `passwordHash` 表单字段发送。 |
|
| 267 |
| `GPT_IMAGE_AGENT_IDEMPOTENCY_KEY` | 跨脚本进程复用同一业务操作的幂等键。 |
|
| 268 |
|
|
|
|
|
|
|
| 269 |
接口边界:
|
| 270 |
|
| 271 |
- `/api/agent/*` 是自动化机器契约,返回最终 JSON,不向客户端返回 SSE。
|
|
@@ -303,6 +359,18 @@ docker compose up -d --build --remove-orphans
|
|
| 303 |
npm run deploy:local
|
| 304 |
```
|
| 305 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
常见部署模式:
|
| 307 |
|
| 308 |
| 模式 | 命令或配置 | 适用场景 |
|
|
@@ -332,9 +400,11 @@ Hugging Face Space 免费层部署见 [docs/deployment/huggingface-space-free.md
|
|
| 332 |
| `npm run lint:scripts` | 检查仓库脚本和 skill 脚本语法。 |
|
| 333 |
| `npm run version:check` | 检查版本、README badge 和 CHANGELOG 口径。 |
|
| 334 |
| `npm run verify` | 运行提交前基线。 |
|
|
|
|
|
|
|
| 335 |
| `npm run status` | 只读查看 git、Node、部署目标和 Agent 摘要。 |
|
| 336 |
| `npm run doctor` | 运行本机和部署诊断。 |
|
| 337 |
-
| `npm run agent:doctor` | 非计费 Agent 分层诊断。 |
|
| 338 |
| `npm run deploy:space` | 上传干净 git HEAD 到固定 HF Space。 |
|
| 339 |
|
| 340 |
真实上游 smoke 默认不会触发计费;需要真实生图时必须显式传入 `--allow-billable`。
|
|
|
|
| 17 |
|
| 18 |
## 快速开始
|
| 19 |
|
| 20 |
+
第一次配置或换机器后,先跑只读就绪检查。它不会写配置、不会输出密钥、不会触发真实生图:
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
npm run first-run
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
该命令默认输出中文摘要;给 Agent 或脚本消费时加 `--json`。它会检查 Node、依赖、`.env.local` / `.env.agent.local` 摘要、默认本地服务 `http://localhost:4783`、Agent capabilities 和下一步动作。检查公网或 Space 服务时显式传地址:
|
| 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 后加载到 shell,再运行 `npm run first-run` 或 skill 脚本。
|
| 39 |
+
|
| 40 |
推荐 Docker:
|
| 41 |
|
| 42 |
```bash
|
|
|
|
| 192 |
|
| 193 |
AI Agent 集成时优先调用 skill 内置脚本,而不是临时手写 fetch、curl 或表单提交逻辑。脚本会先读取 capabilities,自动处理鉴权、幂等键、路由选择、超时、产物 URL 和结构化失败摘要。
|
| 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` 是否存在私有鉴权配置;如果 token 只在私有 env 文件中,先把它加载到 shell,再运行 skill 脚本。
|
| 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 后加载到 shell;不要把 `.env.agent.local` 提交或粘到任务日志。
|
| 202 |
+
|
| 203 |
1. 只读检查当前服务能力,不触发计费:
|
| 204 |
|
| 205 |
```bash
|
|
|
|
| 206 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
| 207 |
--contract-check \
|
| 208 |
+
--base-url http://localhost:4783 \
|
| 209 |
"capability check"
|
| 210 |
```
|
| 211 |
|
|
|
|
| 213 |
|
| 214 |
```bash
|
| 215 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
| 216 |
+
--base-url http://localhost:4783 \
|
| 217 |
--size 1024x1024 \
|
| 218 |
--quality high \
|
| 219 |
--response-mode path \
|
|
|
|
| 225 |
|
| 226 |
```bash
|
| 227 |
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
| 228 |
+
--base-url http://localhost:4783 \
|
| 229 |
--allow-billable \
|
| 230 |
--timeout-ms 420000 \
|
| 231 |
--size 1024x1024 \
|
|
|
|
| 239 |
|
| 240 |
```bash
|
| 241 |
node skills/gpt-image-playground-agent/scripts/edit-image.mjs \
|
| 242 |
+
--base-url http://localhost:4783 \
|
| 243 |
--image ./source.png \
|
| 244 |
--format webp \
|
| 245 |
--output-compression 100 \
|
|
|
|
| 257 |
|
| 258 |
```bash
|
| 259 |
node skills/gpt-image-playground-agent/scripts/batch-images.mjs \
|
| 260 |
+
--base-url http://localhost:4783 \
|
| 261 |
--input tasks.jsonl \
|
| 262 |
--manifest runs/product-set.manifest.jsonl \
|
| 263 |
--resume \
|
|
|
|
| 279 |
|
| 280 |
```bash
|
| 281 |
node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
| 282 |
+
--base-url http://localhost:4783 \
|
| 283 |
--idempotency-key agent-demo-generate-001
|
| 284 |
```
|
| 285 |
|
|
|
|
| 287 |
|
| 288 |
```bash
|
| 289 |
node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
| 290 |
+
--base-url http://localhost:4783 \
|
| 291 |
--client-request-id agent-demo-edit-001
|
| 292 |
```
|
| 293 |
|
| 294 |
+
远��� Space、云服务或内网服务必须显式固定目标地址,避免误查本机默认服务:
|
| 295 |
+
|
| 296 |
+
```bash
|
| 297 |
+
node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
|
| 298 |
+
--base-url https://your-space.hf.space \
|
| 299 |
+
--idempotency-key agent-demo-generate-001
|
| 300 |
+
```
|
| 301 |
+
|
| 302 |
+
首次配置和诊断输出字段速查:
|
| 303 |
+
|
| 304 |
+
| 字段 | 出现位置 | 判断口径 |
|
| 305 |
+
| --- | --- | --- |
|
| 306 |
+
| `service_base_url` / `verification_scope.service_base_url` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | 当前脚本准备访问的 Playground 服务地址。 |
|
| 307 |
+
| `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` 表示默认本地探测。 |
|
| 308 |
+
| `interactive_confirmation_required` / `verification_scope.interactive_confirmation_required` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | 交互式任务中为 `true` 时,应先向用户确认是否使用该地址再发起真实请求。 |
|
| 309 |
+
| `agent_auth_process.has_token` | `first-run --json` | 当前 shell 是否已经拿到 `GPT_IMAGE_AGENT_TOKEN`。 |
|
| 310 |
+
| `private_agent_env.exists` | `first-run --json` | 本机是否存在 `.env.agent.local` 私有配置;存在不代表当前 shell 已加载。 |
|
| 311 |
+
| `capabilities.ok` | `first-run --json`、`agent:doctor` | 目标地址是否返回 Agent capabilities;失败时先看 HTTP 状态、鉴权提示和服务地址。 |
|
| 312 |
+
| `diagnostics_retention` | `diagnose-request.mjs` | 页面日志诊断的保留窗口;无匹配日志不等于请求一定没发生。 |
|
| 313 |
+
|
| 314 |
常用环境变量:
|
| 315 |
|
| 316 |
| 变量 | 用途 |
|
| 317 |
| --- | --- |
|
| 318 |
+
| `GPT_IMAGE_PLAYGROUND_URL` | 指向本机、内网或公网部署地址;未设置时脚本默认尝试 `http://localhost:4783`。交互式任务中,自动发现到本地服务后应先向用户确认。 |
|
| 319 |
| `GPT_IMAGE_AGENT_TOKEN` | Agent Bearer token,对应服务端 `AGENT_API_TOKEN`。 |
|
| 320 |
| `GPT_IMAGE_APP_PASSWORD_HASH` | 使用页面访问码部署时的访问码哈希;页面 SSE 会作为 `passwordHash` 表单字段发送。 |
|
| 321 |
| `GPT_IMAGE_AGENT_IDEMPOTENCY_KEY` | 跨脚本进程复用同一业务操作的幂等键。 |
|
| 322 |
|
| 323 |
+
Hugging Face Space Secrets 只能写入和列出名称,不能从 CLI 读回 secret 值。远端 Space 配置了 `AGENT_API_TOKEN` 后,本机 Agent 仍需要通过不入库的 shell 环境、keychain 或本地私有 env 文件注入 `GPT_IMAGE_AGENT_TOKEN`;不要把 token 写进 README、任务 JSONL、manifest 或命令日志。仓库提供 `.env.agent.local.example` 作为私有本机配置模板。
|
| 324 |
+
|
| 325 |
接口边界:
|
| 326 |
|
| 327 |
- `/api/agent/*` 是自动化机器契约,返回最终 JSON,不向客户端返回 SSE。
|
|
|
|
| 359 |
npm run deploy:local
|
| 360 |
```
|
| 361 |
|
| 362 |
+
本仓库的 Compose 服务只挂载 `generated-images/`。不要用 `docker run -v "$PWD:/workspace"` 启动本地图片上游 fixture;这会把 `.git/`、`node_modules/` 和 `.next/` 暴露给 Docker Desktop 文件共享层,可能触发文件事件风暴。本地 fixture gate 使用进程内服务:
|
| 363 |
+
|
| 364 |
+
```bash
|
| 365 |
+
npm run smoke:image-upstream-local
|
| 366 |
+
```
|
| 367 |
+
|
| 368 |
+
如本机遗留了整仓挂载的 fixture 容器,可执行:
|
| 369 |
+
|
| 370 |
+
```bash
|
| 371 |
+
npm run docker:cleanup-fixtures
|
| 372 |
+
```
|
| 373 |
+
|
| 374 |
常见部署模式:
|
| 375 |
|
| 376 |
| 模式 | 命令或配置 | 适用场景 |
|
|
|
|
| 400 |
| `npm run lint:scripts` | 检查仓库脚本和 skill 脚本语法。 |
|
| 401 |
| `npm run version:check` | 检查版本、README badge 和 CHANGELOG 口径。 |
|
| 402 |
| `npm run verify` | 运行提交前基线。 |
|
| 403 |
+
| `npm run docker:cleanup-fixtures` | 清理遗留的整仓挂载 Docker fixture 容器。 |
|
| 404 |
+
| `npm run first-run` | 首次配置就绪检查,默认中文摘要;加 `-- --json` 输出机器可读 JSON。 |
|
| 405 |
| `npm run status` | 只读查看 git、Node、部署目标和 Agent 摘要。 |
|
| 406 |
| `npm run doctor` | 运行本机和部署诊断。 |
|
| 407 |
+
| `npm run agent:doctor` | 非计费 Agent 分层诊断;支持 `-- --base-url <url>`。 |
|
| 408 |
| `npm run deploy:space` | 上传干净 git HEAD 到固定 HF Space。 |
|
| 409 |
|
| 410 |
真实上游 smoke 默认不会触发计费;需要真实生图时必须显式传入 `--allow-billable`。
|
package.json
CHANGED
|
@@ -12,12 +12,14 @@
|
|
| 12 |
"test:scripts": "node --test \"scripts/**/*.test.mjs\"",
|
| 13 |
"test:postgres": "node scripts/test-postgres-live.mjs",
|
| 14 |
"version:check": "node scripts/check-version-metadata.mjs",
|
|
|
|
| 15 |
"doctor": "node scripts/doctor.mjs",
|
| 16 |
"status": "node scripts/status.mjs",
|
| 17 |
"env:summary": "node scripts/env-summary.mjs",
|
| 18 |
"verify": "node scripts/verify.mjs",
|
| 19 |
"deploy:local": "node scripts/deploy-local.mjs",
|
| 20 |
"deploy:space": "npm run deploy:hf-space",
|
|
|
|
| 21 |
"agent:doctor": "node scripts/agent-doctor.mjs",
|
| 22 |
"deploy:hf-space": "node scripts/deploy-hf-space.mjs",
|
| 23 |
"doctor:hf-space": "node scripts/doctor-hf-space.mjs",
|
|
|
|
| 12 |
"test:scripts": "node --test \"scripts/**/*.test.mjs\"",
|
| 13 |
"test:postgres": "node scripts/test-postgres-live.mjs",
|
| 14 |
"version:check": "node scripts/check-version-metadata.mjs",
|
| 15 |
+
"first-run": "node scripts/first-run.mjs",
|
| 16 |
"doctor": "node scripts/doctor.mjs",
|
| 17 |
"status": "node scripts/status.mjs",
|
| 18 |
"env:summary": "node scripts/env-summary.mjs",
|
| 19 |
"verify": "node scripts/verify.mjs",
|
| 20 |
"deploy:local": "node scripts/deploy-local.mjs",
|
| 21 |
"deploy:space": "npm run deploy:hf-space",
|
| 22 |
+
"docker:cleanup-fixtures": "node scripts/cleanup-docker-fixtures.mjs",
|
| 23 |
"agent:doctor": "node scripts/agent-doctor.mjs",
|
| 24 |
"deploy:hf-space": "node scripts/deploy-hf-space.mjs",
|
| 25 |
"doctor:hf-space": "node scripts/doctor-hf-space.mjs",
|
scripts/agent-doctor.mjs
CHANGED
|
@@ -2,22 +2,30 @@
|
|
| 2 |
|
| 3 |
import { fileURLToPath } from 'node:url';
|
| 4 |
|
| 5 |
-
import { isMainModule, parseJsonPayload, pickFailureOutput, printJson, runCommand } from './command-center-utils.mjs';
|
|
|
|
| 6 |
|
| 7 |
const GENERATE_SCRIPT = fileURLToPath(new URL('../skills/gpt-image-playground-agent/scripts/generate-image.mjs', import.meta.url));
|
| 8 |
const EDIT_SCRIPT = fileURLToPath(new URL('../skills/gpt-image-playground-agent/scripts/edit-image.mjs', import.meta.url));
|
| 9 |
const AGENT_DOCTOR_TIMEOUT_MS = 75_000;
|
| 10 |
-
const DEFAULT_BASE_URL = 'http://localhost:4783';
|
| 11 |
|
| 12 |
export function buildAgentDoctorArgs() {
|
| 13 |
return [GENERATE_SCRIPT, '--contract-check', '--timeout-ms', '60000', 'contract check'];
|
| 14 |
}
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
function parseArgs(argv) {
|
| 17 |
const parsed = {
|
| 18 |
help: false,
|
| 19 |
allowBillable: false,
|
| 20 |
timeoutMs: 60_000,
|
|
|
|
| 21 |
editImage: undefined
|
| 22 |
};
|
| 23 |
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -25,23 +33,30 @@ function parseArgs(argv) {
|
|
| 25 |
if (arg === '--help' || arg === '-h') parsed.help = true;
|
| 26 |
else if (arg === '--allow-billable') parsed.allowBillable = true;
|
| 27 |
else if (arg === '--timeout-ms') parsed.timeoutMs = readPositiveInteger(readOptionValue(argv, (index += 1), arg), '--timeout-ms');
|
|
|
|
| 28 |
else if (arg === '--edit-image') parsed.editImage = readOptionValue(argv, (index += 1), arg);
|
| 29 |
-
else throw new Error(`
|
| 30 |
}
|
| 31 |
return parsed;
|
| 32 |
}
|
| 33 |
|
| 34 |
function printHelp() {
|
| 35 |
-
console.log(`
|
| 36 |
npm run agent:doctor
|
| 37 |
-
npm run agent:doctor -- --allow-billable --edit-image /path/to/reference.png
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
}
|
| 46 |
|
| 47 |
async function main() {
|
|
@@ -51,11 +66,12 @@ async function main() {
|
|
| 51 |
return;
|
| 52 |
}
|
| 53 |
|
| 54 |
-
const
|
| 55 |
-
const
|
|
|
|
| 56 |
const capabilities = await readJsonLayer('capabilities', `${baseUrl}/api/agent/capabilities`, options.timeoutMs);
|
| 57 |
const runtime = await readJsonLayer('runtime', `${baseUrl}/api/runtime-capabilities`, options.timeoutMs);
|
| 58 |
-
const smoke = options.allowBillable ? runBillableSmoke(options) : buildSkippedSmoke(options);
|
| 59 |
const layers = buildLayers({ capabilities, runtime, contract, smoke });
|
| 60 |
|
| 61 |
printJson({
|
|
@@ -63,14 +79,17 @@ async function main() {
|
|
| 63 |
command: 'agent:doctor',
|
| 64 |
billable: options.allowBillable,
|
| 65 |
base_url: redactBaseUrl(baseUrl),
|
|
|
|
|
|
|
|
|
|
| 66 |
layers,
|
| 67 |
summary: buildSummary({ capabilities, runtime, contract, smoke })
|
| 68 |
});
|
| 69 |
if (layers.some((layer) => !layer.ok && !layer.skipped)) process.exit(1);
|
| 70 |
}
|
| 71 |
|
| 72 |
-
function runContractCheck() {
|
| 73 |
-
const result = runCommand(process.execPath,
|
| 74 |
env: { ...process.env, GPT_IMAGE_AGENT_CONTRACT_CHECK: '1' },
|
| 75 |
timeoutMs: AGENT_DOCTOR_TIMEOUT_MS
|
| 76 |
});
|
|
@@ -120,10 +139,12 @@ function buildSkippedSmoke(options) {
|
|
| 120 |
};
|
| 121 |
}
|
| 122 |
|
| 123 |
-
function runBillableSmoke(options) {
|
| 124 |
const checks = [
|
| 125 |
runSmokeCommand('generate_1k', [
|
| 126 |
GENERATE_SCRIPT,
|
|
|
|
|
|
|
| 127 |
'--allow-billable',
|
| 128 |
'--agent',
|
| 129 |
'--timeout-ms',
|
|
@@ -141,6 +162,8 @@ function runBillableSmoke(options) {
|
|
| 141 |
checks.push(
|
| 142 |
runSmokeCommand('edit_1k', [
|
| 143 |
EDIT_SCRIPT,
|
|
|
|
|
|
|
| 144 |
'--allow-billable',
|
| 145 |
'--agent',
|
| 146 |
'--timeout-ms',
|
|
@@ -158,6 +181,8 @@ function runBillableSmoke(options) {
|
|
| 158 |
checks.push(
|
| 159 |
runSmokeCommand('page_sse_edit_2k', [
|
| 160 |
EDIT_SCRIPT,
|
|
|
|
|
|
|
| 161 |
'--allow-billable',
|
| 162 |
'--page-sse',
|
| 163 |
'--timeout-ms',
|
|
@@ -296,14 +321,14 @@ function authHeaders() {
|
|
| 296 |
|
| 297 |
function readOptionValue(argv, index, name) {
|
| 298 |
const value = argv[index];
|
| 299 |
-
if (!value || value.startsWith('--')) throw new Error(`${name}
|
| 300 |
return value;
|
| 301 |
}
|
| 302 |
|
| 303 |
function readPositiveInteger(value, name) {
|
| 304 |
-
if (!/^\d+$/.test(String(value))) throw new Error(`${name}
|
| 305 |
const parsed = Number(value);
|
| 306 |
-
if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${name}
|
| 307 |
return parsed;
|
| 308 |
}
|
| 309 |
|
|
@@ -319,11 +344,6 @@ function normalizeBaseUrl(value) {
|
|
| 319 |
return normalized;
|
| 320 |
}
|
| 321 |
|
| 322 |
-
function redactBaseUrl(value) {
|
| 323 |
-
const parsed = new URL(value);
|
| 324 |
-
return `${parsed.protocol}//${parsed.host}`;
|
| 325 |
-
}
|
| 326 |
-
|
| 327 |
function safePathname(url) {
|
| 328 |
try {
|
| 329 |
return new URL(url).pathname;
|
|
|
|
| 2 |
|
| 3 |
import { fileURLToPath } from 'node:url';
|
| 4 |
|
| 5 |
+
import { isMainModule, parseJsonPayload, pickFailureOutput, printJson, redactBaseUrl, runCommand } from './command-center-utils.mjs';
|
| 6 |
+
import { resolvePlaygroundBaseUrl } from '../skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs';
|
| 7 |
|
| 8 |
const GENERATE_SCRIPT = fileURLToPath(new URL('../skills/gpt-image-playground-agent/scripts/generate-image.mjs', import.meta.url));
|
| 9 |
const EDIT_SCRIPT = fileURLToPath(new URL('../skills/gpt-image-playground-agent/scripts/edit-image.mjs', import.meta.url));
|
| 10 |
const AGENT_DOCTOR_TIMEOUT_MS = 75_000;
|
|
|
|
| 11 |
|
| 12 |
export function buildAgentDoctorArgs() {
|
| 13 |
return [GENERATE_SCRIPT, '--contract-check', '--timeout-ms', '60000', 'contract check'];
|
| 14 |
}
|
| 15 |
|
| 16 |
+
export function buildAgentDoctorContractArgs(baseUrl) {
|
| 17 |
+
const args = [GENERATE_SCRIPT, '--contract-check', '--timeout-ms', '60000'];
|
| 18 |
+
if (baseUrl) args.push('--base-url', baseUrl);
|
| 19 |
+
args.push('contract check');
|
| 20 |
+
return args;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
function parseArgs(argv) {
|
| 24 |
const parsed = {
|
| 25 |
help: false,
|
| 26 |
allowBillable: false,
|
| 27 |
timeoutMs: 60_000,
|
| 28 |
+
baseUrl: undefined,
|
| 29 |
editImage: undefined
|
| 30 |
};
|
| 31 |
for (let index = 0; index < argv.length; index += 1) {
|
|
|
|
| 33 |
if (arg === '--help' || arg === '-h') parsed.help = true;
|
| 34 |
else if (arg === '--allow-billable') parsed.allowBillable = true;
|
| 35 |
else if (arg === '--timeout-ms') parsed.timeoutMs = readPositiveInteger(readOptionValue(argv, (index += 1), arg), '--timeout-ms');
|
| 36 |
+
else if (arg === '--base-url') parsed.baseUrl = readOptionValue(argv, (index += 1), arg);
|
| 37 |
else if (arg === '--edit-image') parsed.editImage = readOptionValue(argv, (index += 1), arg);
|
| 38 |
+
else throw new Error(`未知参数:${arg}`);
|
| 39 |
}
|
| 40 |
return parsed;
|
| 41 |
}
|
| 42 |
|
| 43 |
function printHelp() {
|
| 44 |
+
console.log(`用法:
|
| 45 |
npm run agent:doctor
|
| 46 |
+
npm run agent:doctor -- --base-url https://your-space.hf.space --allow-billable --edit-image /path/to/reference.png
|
| 47 |
+
|
| 48 |
+
环境变量:
|
| 49 |
+
GPT_IMAGE_PLAYGROUND_URL 服务基础地址,默认 http://localhost:4783。
|
| 50 |
+
GPT_IMAGE_AGENT_TOKEN capabilities 需要 bearer 鉴权时使用。
|
| 51 |
+
GPT_IMAGE_APP_PASSWORD_HASH capabilities 需要页面密码鉴权时使用。
|
| 52 |
|
| 53 |
+
选项:
|
| 54 |
+
--base-url 显式服务地址,优先于 GPT_IMAGE_PLAYGROUND_URL 和本地探测。
|
| 55 |
+
--timeout-ms HTTP 探测和 smoke 超时,默认 60000。
|
| 56 |
+
--edit-image 真实 edit smoke 使用的参考图。
|
| 57 |
+
--allow-billable 显式允许真实 1K/2K smoke 请求。
|
| 58 |
|
| 59 |
+
agent:doctor 默认只读、非计费。真实 generate/edit smoke 必须显式添加 --allow-billable。`);
|
| 60 |
}
|
| 61 |
|
| 62 |
async function main() {
|
|
|
|
| 66 |
return;
|
| 67 |
}
|
| 68 |
|
| 69 |
+
const baseUrlInfo = resolvePlaygroundBaseUrl(options.baseUrl, process.env);
|
| 70 |
+
const baseUrl = baseUrlInfo.baseUrl;
|
| 71 |
+
const contract = runContractCheck(baseUrl);
|
| 72 |
const capabilities = await readJsonLayer('capabilities', `${baseUrl}/api/agent/capabilities`, options.timeoutMs);
|
| 73 |
const runtime = await readJsonLayer('runtime', `${baseUrl}/api/runtime-capabilities`, options.timeoutMs);
|
| 74 |
+
const smoke = options.allowBillable ? runBillableSmoke(options, baseUrl) : buildSkippedSmoke(options);
|
| 75 |
const layers = buildLayers({ capabilities, runtime, contract, smoke });
|
| 76 |
|
| 77 |
printJson({
|
|
|
|
| 79 |
command: 'agent:doctor',
|
| 80 |
billable: options.allowBillable,
|
| 81 |
base_url: redactBaseUrl(baseUrl),
|
| 82 |
+
service_base_url: redactBaseUrl(baseUrl),
|
| 83 |
+
service_base_url_source: baseUrlInfo.source,
|
| 84 |
+
interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
|
| 85 |
layers,
|
| 86 |
summary: buildSummary({ capabilities, runtime, contract, smoke })
|
| 87 |
});
|
| 88 |
if (layers.some((layer) => !layer.ok && !layer.skipped)) process.exit(1);
|
| 89 |
}
|
| 90 |
|
| 91 |
+
function runContractCheck(baseUrl) {
|
| 92 |
+
const result = runCommand(process.execPath, buildAgentDoctorContractArgs(baseUrl), {
|
| 93 |
env: { ...process.env, GPT_IMAGE_AGENT_CONTRACT_CHECK: '1' },
|
| 94 |
timeoutMs: AGENT_DOCTOR_TIMEOUT_MS
|
| 95 |
});
|
|
|
|
| 139 |
};
|
| 140 |
}
|
| 141 |
|
| 142 |
+
function runBillableSmoke(options, baseUrl) {
|
| 143 |
const checks = [
|
| 144 |
runSmokeCommand('generate_1k', [
|
| 145 |
GENERATE_SCRIPT,
|
| 146 |
+
'--base-url',
|
| 147 |
+
baseUrl,
|
| 148 |
'--allow-billable',
|
| 149 |
'--agent',
|
| 150 |
'--timeout-ms',
|
|
|
|
| 162 |
checks.push(
|
| 163 |
runSmokeCommand('edit_1k', [
|
| 164 |
EDIT_SCRIPT,
|
| 165 |
+
'--base-url',
|
| 166 |
+
baseUrl,
|
| 167 |
'--allow-billable',
|
| 168 |
'--agent',
|
| 169 |
'--timeout-ms',
|
|
|
|
| 181 |
checks.push(
|
| 182 |
runSmokeCommand('page_sse_edit_2k', [
|
| 183 |
EDIT_SCRIPT,
|
| 184 |
+
'--base-url',
|
| 185 |
+
baseUrl,
|
| 186 |
'--allow-billable',
|
| 187 |
'--page-sse',
|
| 188 |
'--timeout-ms',
|
|
|
|
| 321 |
|
| 322 |
function readOptionValue(argv, index, name) {
|
| 323 |
const value = argv[index];
|
| 324 |
+
if (!value || value.startsWith('--')) throw new Error(`${name} 需要参数值。`);
|
| 325 |
return value;
|
| 326 |
}
|
| 327 |
|
| 328 |
function readPositiveInteger(value, name) {
|
| 329 |
+
if (!/^\d+$/.test(String(value))) throw new Error(`${name} 必须是正整数。`);
|
| 330 |
const parsed = Number(value);
|
| 331 |
+
if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${name} 必须是正整数。`);
|
| 332 |
return parsed;
|
| 333 |
}
|
| 334 |
|
|
|
|
| 344 |
return normalized;
|
| 345 |
}
|
| 346 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
function safePathname(url) {
|
| 348 |
try {
|
| 349 |
return new URL(url).pathname;
|
scripts/agent-skill-scripts.test.mjs
CHANGED
|
@@ -155,6 +155,46 @@ describe('Agent skill script argument validation', () => {
|
|
| 155 |
assert.equal(result.stdout.trim(), '');
|
| 156 |
});
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
it('rejects upstream probe base URLs with embedded credentials before network checks', () => {
|
| 159 |
const result = runSkillScript('probe-upstream-image.mjs', [
|
| 160 |
'--base-url',
|
|
@@ -240,6 +280,10 @@ describe('Agent skill script argument validation', () => {
|
|
| 240 |
const body = JSON.parse(result.stdout);
|
| 241 |
assert.equal(body.dry_run, true);
|
| 242 |
assert.equal(body.billable, false);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
assert.equal(body.request.output_format, 'webp');
|
| 244 |
assert.equal(body.request.output_compression, 100);
|
| 245 |
assert.equal(result.stderr.trim(), '');
|
|
@@ -310,6 +354,10 @@ describe('Agent skill script argument validation', () => {
|
|
| 310 |
assert.equal(body.request.stream_mode, 'auto');
|
| 311 |
assert.equal(body.request.streaming_strategy, 'force-sse');
|
| 312 |
assert.equal(body.request.partial_images, 2);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
assert.equal(result.stderr.trim(), '');
|
| 314 |
});
|
| 315 |
|
|
@@ -2500,6 +2548,29 @@ describe('Agent skill script argument validation', () => {
|
|
| 2500 |
assert.deepEqual(matches, []);
|
| 2501 |
});
|
| 2502 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2503 |
it('tells agents to use bundled scripts instead of ad hoc API callers', () => {
|
| 2504 |
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
|
| 2505 |
const openAiYaml = readFileSync(join(skillRoot, 'agents/openai.yaml'), 'utf8');
|
|
@@ -2550,12 +2621,26 @@ describe('Agent skill script argument validation', () => {
|
|
| 2550 |
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
|
| 2551 |
const apiReference = readFileSync(join(skillRoot, 'references/api.md'), 'utf8');
|
| 2552 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2553 |
assert.match(readmeText, /不要手动并行启动多个单张脚本/);
|
| 2554 |
assert.match(readmeText, /streamingBatch\.recommendedConcurrency/);
|
| 2555 |
assert.match(readmeText, /channelQueue\.capacityPerCredential/);
|
| 2556 |
assert.match(readmeText, /Agent edit 输出格式和尺寸可能与页面 SSE 不完全一致/);
|
| 2557 |
assert.match(readmeText, /不要直接输出 `\.env\.local`、`\.env\*\.local`、secret 文件或原始 `docker inspect \.Config\.Env`/);
|
| 2558 |
assert.match(readmeText, /npm run env:summary/);
|
|
|
|
|
|
|
| 2559 |
|
| 2560 |
assert.match(skillText, /不要手动并行启动多个单张脚本/);
|
| 2561 |
assert.match(skillText, /capacity_feedback/);
|
|
@@ -2564,6 +2649,9 @@ describe('Agent skill script argument validation', () => {
|
|
| 2564 |
assert.match(skillText, /复杂 UI、长 prompt、高质量图生图遇到 5 分钟级超时/);
|
| 2565 |
assert.match(skillText, /Codex 会话日志会持久保存命令输出/);
|
| 2566 |
assert.match(skillText, /npm run env:summary/);
|
|
|
|
|
|
|
|
|
|
| 2567 |
|
| 2568 |
assert.match(apiReference, /不要手动并行启动多个单张脚本来绕过 `capacity_feedback`/);
|
| 2569 |
assert.match(apiReference, /streamingBatch\.recommendedConcurrency/);
|
|
@@ -2571,6 +2659,9 @@ describe('Agent skill script argument validation', () => {
|
|
| 2571 |
assert.match(apiReference, /尺寸敏感任务必须使用批量 `--dimension-check` 或下载后校验/);
|
| 2572 |
assert.match(apiReference, /channel_capacity_queue_aborted/);
|
| 2573 |
assert.match(apiReference, /npm run env:summary/);
|
|
|
|
|
|
|
|
|
|
| 2574 |
});
|
| 2575 |
|
| 2576 |
it('keeps WebUI page APIs out of the Agent OpenAPI contract', () => {
|
|
@@ -2647,6 +2738,24 @@ describe('Agent skill script argument validation', () => {
|
|
| 2647 |
assert.match(result.stderr, /用法:generate-image\.mjs/);
|
| 2648 |
assert.equal(result.stdout.trim(), '');
|
| 2649 |
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /ERR_MODULE_NOT_FOUND|src\/lib/);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2650 |
} finally {
|
| 2651 |
rmSync(tempRoot, { recursive: true, force: true });
|
| 2652 |
}
|
|
@@ -2683,6 +2792,11 @@ describe('Agent skill script argument validation', () => {
|
|
| 2683 |
const body = JSON.parse(result.stdout);
|
| 2684 |
assert.equal(body.dry_run, true);
|
| 2685 |
assert.equal(body.billable, false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2686 |
assert.equal(body.total, 2);
|
| 2687 |
assert.equal(body.concurrency, 1);
|
| 2688 |
assert.equal(body.tasks[0].endpoint, '/api/agent/images/generate');
|
|
@@ -5022,6 +5136,40 @@ describe('Agent skill script argument validation', () => {
|
|
| 5022 |
);
|
| 5023 |
});
|
| 5024 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5025 |
it('reports missing Agent state diagnostics as found false without failing the script', async () => {
|
| 5026 |
await withServer(
|
| 5027 |
async (request, response) => {
|
|
|
|
| 155 |
assert.equal(result.stdout.trim(), '');
|
| 156 |
});
|
| 157 |
|
| 158 |
+
it('lets explicit base-url override the environment service URL in dry-run output', () => {
|
| 159 |
+
const generateResult = runSkillScript(
|
| 160 |
+
'generate-image.mjs',
|
| 161 |
+
['--base-url', 'https://space.example.test/', '--size', '1024x1024', 'prompt'],
|
| 162 |
+
{ GPT_IMAGE_PLAYGROUND_URL: 'http://localhost:4783' }
|
| 163 |
+
);
|
| 164 |
+
assert.equal(generateResult.status, 0);
|
| 165 |
+
const generateBody = JSON.parse(generateResult.stdout);
|
| 166 |
+
assert.equal(generateBody.verification_scope.service_base_url, 'https://space.example.test');
|
| 167 |
+
assert.equal(generateBody.verification_scope.service_base_url_source, 'user_provided');
|
| 168 |
+
assert.equal(generateBody.verification_scope.interactive_confirmation_required, false);
|
| 169 |
+
|
| 170 |
+
const editResult = runSkillScript(
|
| 171 |
+
'edit-image.mjs',
|
| 172 |
+
['--base-url', 'https://space.example.test/', '--image', '/tmp/source.png', 'prompt'],
|
| 173 |
+
{ GPT_IMAGE_PLAYGROUND_URL: 'http://localhost:4783' }
|
| 174 |
+
);
|
| 175 |
+
assert.equal(editResult.status, 0);
|
| 176 |
+
const editBody = JSON.parse(editResult.stdout);
|
| 177 |
+
assert.equal(editBody.verification_scope.service_base_url, 'https://space.example.test');
|
| 178 |
+
assert.equal(editBody.verification_scope.service_base_url_source, 'user_provided');
|
| 179 |
+
|
| 180 |
+
const tempRoot = mkdtempSync(join(tmpdir(), 'batch-base-url-'));
|
| 181 |
+
try {
|
| 182 |
+
const inputPath = join(tempRoot, 'tasks.jsonl');
|
| 183 |
+
writeFileSync(inputPath, JSON.stringify({ id: 'first', prompt: 'prompt' }));
|
| 184 |
+
const batchResult = runSkillScript(
|
| 185 |
+
'batch-images.mjs',
|
| 186 |
+
['--base-url', 'https://space.example.test/', '--input', inputPath],
|
| 187 |
+
{ GPT_IMAGE_PLAYGROUND_URL: 'http://localhost:4783' }
|
| 188 |
+
);
|
| 189 |
+
assert.equal(batchResult.status, 0);
|
| 190 |
+
const batchBody = JSON.parse(batchResult.stdout);
|
| 191 |
+
assert.equal(batchBody.verification_scope.service_base_url, 'https://space.example.test');
|
| 192 |
+
assert.equal(batchBody.verification_scope.service_base_url_source, 'user_provided');
|
| 193 |
+
} finally {
|
| 194 |
+
rmSync(tempRoot, { recursive: true, force: true });
|
| 195 |
+
}
|
| 196 |
+
});
|
| 197 |
+
|
| 198 |
it('rejects upstream probe base URLs with embedded credentials before network checks', () => {
|
| 199 |
const result = runSkillScript('probe-upstream-image.mjs', [
|
| 200 |
'--base-url',
|
|
|
|
| 280 |
const body = JSON.parse(result.stdout);
|
| 281 |
assert.equal(body.dry_run, true);
|
| 282 |
assert.equal(body.billable, false);
|
| 283 |
+
assert.equal(body.verification_scope.mode, 'local_planning_only');
|
| 284 |
+
assert.equal(body.verification_scope.remote_capabilities_verified, false);
|
| 285 |
+
assert.equal(body.verification_scope.runtime_capacity_verified, false);
|
| 286 |
+
assert.equal(body.verification_scope.auth_verified, false);
|
| 287 |
assert.equal(body.request.output_format, 'webp');
|
| 288 |
assert.equal(body.request.output_compression, 100);
|
| 289 |
assert.equal(result.stderr.trim(), '');
|
|
|
|
| 354 |
assert.equal(body.request.stream_mode, 'auto');
|
| 355 |
assert.equal(body.request.streaming_strategy, 'force-sse');
|
| 356 |
assert.equal(body.request.partial_images, 2);
|
| 357 |
+
assert.equal(body.verification_scope.mode, 'local_planning_only');
|
| 358 |
+
assert.equal(body.verification_scope.remote_capabilities_verified, false);
|
| 359 |
+
assert.equal(body.verification_scope.runtime_capacity_verified, false);
|
| 360 |
+
assert.equal(body.verification_scope.auth_verified, false);
|
| 361 |
assert.equal(result.stderr.trim(), '');
|
| 362 |
});
|
| 363 |
|
|
|
|
| 2548 |
assert.deepEqual(matches, []);
|
| 2549 |
});
|
| 2550 |
|
| 2551 |
+
it('keeps skill frontmatter valid without requiring Python YAML tooling', () => {
|
| 2552 |
+
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
|
| 2553 |
+
const match = skillText.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
| 2554 |
+
assert.ok(match, 'SKILL.md must start with YAML frontmatter');
|
| 2555 |
+
|
| 2556 |
+
const frontmatter = Object.fromEntries(
|
| 2557 |
+
match[1]
|
| 2558 |
+
.split(/\r?\n/)
|
| 2559 |
+
.filter(Boolean)
|
| 2560 |
+
.map((line) => {
|
| 2561 |
+
const separator = line.indexOf(':');
|
| 2562 |
+
assert.ok(separator > 0, `invalid frontmatter line: ${line}`);
|
| 2563 |
+
return [line.slice(0, separator).trim(), line.slice(separator + 1).trim()];
|
| 2564 |
+
})
|
| 2565 |
+
);
|
| 2566 |
+
|
| 2567 |
+
assert.deepEqual(Object.keys(frontmatter).sort(), ['description', 'name']);
|
| 2568 |
+
assert.match(frontmatter.name, /^[a-z0-9-]+$/);
|
| 2569 |
+
assert.ok(frontmatter.description.length > 0);
|
| 2570 |
+
assert.ok(frontmatter.description.length <= 1024);
|
| 2571 |
+
assert.doesNotMatch(frontmatter.description, /[<>]/);
|
| 2572 |
+
});
|
| 2573 |
+
|
| 2574 |
it('tells agents to use bundled scripts instead of ad hoc API callers', () => {
|
| 2575 |
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
|
| 2576 |
const openAiYaml = readFileSync(join(skillRoot, 'agents/openai.yaml'), 'utf8');
|
|
|
|
| 2621 |
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
|
| 2622 |
const apiReference = readFileSync(join(skillRoot, 'references/api.md'), 'utf8');
|
| 2623 |
|
| 2624 |
+
assert.match(readmeText, /npm run first-run/);
|
| 2625 |
+
assert.match(readmeText, /npm run first-run -- --json/);
|
| 2626 |
+
assert.match(readmeText, /\.env\.agent\.local\.example/);
|
| 2627 |
+
assert.match(readmeText, /--base-url http:\/\/localhost:4783/);
|
| 2628 |
+
assert.match(skillText, /npm run first-run/);
|
| 2629 |
+
assert.match(skillText, /-- --json/);
|
| 2630 |
+
assert.match(skillText, /--base-url <url>/);
|
| 2631 |
+
assert.match(skillText, /\.env\.agent\.local\.example/);
|
| 2632 |
+
assert.match(apiReference, /npm run first-run/);
|
| 2633 |
+
assert.match(apiReference, /npm run first-run -- --json/);
|
| 2634 |
+
assert.match(apiReference, /--base-url <url>/);
|
| 2635 |
+
assert.match(apiReference, /\.env\.agent\.local\.example/);
|
| 2636 |
assert.match(readmeText, /不要手动并行启动多个单张脚本/);
|
| 2637 |
assert.match(readmeText, /streamingBatch\.recommendedConcurrency/);
|
| 2638 |
assert.match(readmeText, /channelQueue\.capacityPerCredential/);
|
| 2639 |
assert.match(readmeText, /Agent edit 输出格式和尺寸可能与页面 SSE 不完全一致/);
|
| 2640 |
assert.match(readmeText, /不要直接输出 `\.env\.local`、`\.env\*\.local`、secret 文件或原始 `docker inspect \.Config\.Env`/);
|
| 2641 |
assert.match(readmeText, /npm run env:summary/);
|
| 2642 |
+
assert.match(readmeText, /verification_scope\.mode=local_planning_only/);
|
| 2643 |
+
assert.match(readmeText, /Hugging Face Space Secrets 只能写入和列出名称/);
|
| 2644 |
|
| 2645 |
assert.match(skillText, /不要手动并行启动多个单张脚本/);
|
| 2646 |
assert.match(skillText, /capacity_feedback/);
|
|
|
|
| 2649 |
assert.match(skillText, /复杂 UI、长 prompt、高质量图生图遇到 5 分钟级超时/);
|
| 2650 |
assert.match(skillText, /Codex 会话日志会持久保存命令输出/);
|
| 2651 |
assert.match(skillText, /npm run env:summary/);
|
| 2652 |
+
assert.match(skillText, /verification_scope\.mode=local_planning_only/);
|
| 2653 |
+
assert.match(skillText, /manifest_written=false/);
|
| 2654 |
+
assert.match(skillText, /Hugging Face Space Secrets 只能写入和列出名称/);
|
| 2655 |
|
| 2656 |
assert.match(apiReference, /不要手动并行启动多个单张脚本来绕过 `capacity_feedback`/);
|
| 2657 |
assert.match(apiReference, /streamingBatch\.recommendedConcurrency/);
|
|
|
|
| 2659 |
assert.match(apiReference, /尺寸敏感任务必须使用批量 `--dimension-check` 或下载后校验/);
|
| 2660 |
assert.match(apiReference, /channel_capacity_queue_aborted/);
|
| 2661 |
assert.match(apiReference, /npm run env:summary/);
|
| 2662 |
+
assert.match(apiReference, /verification_scope\.mode=local_planning_only/);
|
| 2663 |
+
assert.match(apiReference, /manifest_written=false/);
|
| 2664 |
+
assert.match(apiReference, /Hugging Face Space Secrets 只能写入和列出名称/);
|
| 2665 |
});
|
| 2666 |
|
| 2667 |
it('keeps WebUI page APIs out of the Agent OpenAPI contract', () => {
|
|
|
|
| 2738 |
assert.match(result.stderr, /用法:generate-image\.mjs/);
|
| 2739 |
assert.equal(result.stdout.trim(), '');
|
| 2740 |
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /ERR_MODULE_NOT_FOUND|src\/lib/);
|
| 2741 |
+
|
| 2742 |
+
const convertHelp = spawnSync(
|
| 2743 |
+
process.execPath,
|
| 2744 |
+
[join(copiedSkillRoot, 'scripts/convert-image-format.mjs'), '--help'],
|
| 2745 |
+
{
|
| 2746 |
+
cwd: tmpdir(),
|
| 2747 |
+
encoding: 'utf8',
|
| 2748 |
+
env: {
|
| 2749 |
+
...process.env,
|
| 2750 |
+
NODE_PATH: ''
|
| 2751 |
+
}
|
| 2752 |
+
}
|
| 2753 |
+
);
|
| 2754 |
+
|
| 2755 |
+
assert.equal(convertHelp.status, 0);
|
| 2756 |
+
assert.match(convertHelp.stderr, /用法:convert-image-format\.mjs/);
|
| 2757 |
+
assert.equal(convertHelp.stdout.trim(), '');
|
| 2758 |
+
assert.doesNotMatch(`${convertHelp.stdout}\n${convertHelp.stderr}`, /ERR_MODULE_NOT_FOUND|sharp/);
|
| 2759 |
} finally {
|
| 2760 |
rmSync(tempRoot, { recursive: true, force: true });
|
| 2761 |
}
|
|
|
|
| 2792 |
const body = JSON.parse(result.stdout);
|
| 2793 |
assert.equal(body.dry_run, true);
|
| 2794 |
assert.equal(body.billable, false);
|
| 2795 |
+
assert.equal(body.verification_scope.mode, 'local_planning_only');
|
| 2796 |
+
assert.equal(body.verification_scope.remote_capabilities_verified, false);
|
| 2797 |
+
assert.equal(body.verification_scope.runtime_capacity_verified, false);
|
| 2798 |
+
assert.equal(body.manifest_written, false);
|
| 2799 |
+
assert.equal(body.manifest_write_reason, 'dry_run');
|
| 2800 |
assert.equal(body.total, 2);
|
| 2801 |
assert.equal(body.concurrency, 1);
|
| 2802 |
assert.equal(body.tasks[0].endpoint, '/api/agent/images/generate');
|
|
|
|
| 5136 |
);
|
| 5137 |
});
|
| 5138 |
|
| 5139 |
+
it('lets diagnose-request explicit base-url override the environment service URL', async () => {
|
| 5140 |
+
await withServer(
|
| 5141 |
+
async (request, response) => {
|
| 5142 |
+
if (request.url === '/api/agent/capabilities') {
|
| 5143 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 5144 |
+
response.end(JSON.stringify({ endpoints: {} }));
|
| 5145 |
+
return;
|
| 5146 |
+
}
|
| 5147 |
+
if (request.url === '/api/agent/diagnostics/requests/req_base_url') {
|
| 5148 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 5149 |
+
response.end(JSON.stringify({ found: false }));
|
| 5150 |
+
return;
|
| 5151 |
+
}
|
| 5152 |
+
response.writeHead(404, { 'content-type': 'application/json' });
|
| 5153 |
+
response.end(JSON.stringify({ error: 'missing' }));
|
| 5154 |
+
},
|
| 5155 |
+
async (baseUrl) => {
|
| 5156 |
+
const result = await runSkillScriptAsync(
|
| 5157 |
+
'diagnose-request.mjs',
|
| 5158 |
+
['--base-url', baseUrl, '--agent-request-id', 'req_base_url'],
|
| 5159 |
+
{ GPT_IMAGE_PLAYGROUND_URL: 'http://127.0.0.1:9' }
|
| 5160 |
+
);
|
| 5161 |
+
|
| 5162 |
+
assert.equal(result.status, 0);
|
| 5163 |
+
assert.equal(result.stderr.trim(), '');
|
| 5164 |
+
const body = JSON.parse(result.stdout);
|
| 5165 |
+
assert.equal(body.service_base_url, baseUrl);
|
| 5166 |
+
assert.equal(body.service_base_url_source, 'user_provided');
|
| 5167 |
+
assert.equal(body.interactive_confirmation_required, false);
|
| 5168 |
+
assert.equal(body.agent_found, false);
|
| 5169 |
+
}
|
| 5170 |
+
);
|
| 5171 |
+
});
|
| 5172 |
+
|
| 5173 |
it('reports missing Agent state diagnostics as found false without failing the script', async () => {
|
| 5174 |
await withServer(
|
| 5175 |
async (request, response) => {
|
scripts/cleanup-docker-fixtures.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
import { dirname, resolve } from 'node:path';
|
| 4 |
+
import { fileURLToPath } from 'node:url';
|
| 5 |
+
|
| 6 |
+
import { isMainModule, pickFailureOutput, printJson, runCommand } from './command-center-utils.mjs';
|
| 7 |
+
|
| 8 |
+
const LEGACY_FIXTURE_CONTAINER = 'gipc-local-image-fixture';
|
| 9 |
+
const LEGACY_FIXTURE_DESTINATION = '/workspace';
|
| 10 |
+
const DOCKER_TIMEOUT_MS = 30_000;
|
| 11 |
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
| 12 |
+
|
| 13 |
+
export function hasLegacyFixtureRepoMount(mounts, repoRoot = REPO_ROOT) {
|
| 14 |
+
const resolvedRepoRoot = resolve(repoRoot);
|
| 15 |
+
return mounts.some(
|
| 16 |
+
(mount) =>
|
| 17 |
+
mount?.Type === 'bind' &&
|
| 18 |
+
typeof mount.Source === 'string' &&
|
| 19 |
+
resolve(mount.Source) === resolvedRepoRoot &&
|
| 20 |
+
mount.Destination === LEGACY_FIXTURE_DESTINATION
|
| 21 |
+
);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function parseDockerInspectContainer(output) {
|
| 25 |
+
const parsed = JSON.parse(output);
|
| 26 |
+
if (!Array.isArray(parsed) || !parsed[0] || typeof parsed[0] !== 'object') {
|
| 27 |
+
throw new Error('docker inspect did not return a container object');
|
| 28 |
+
}
|
| 29 |
+
return parsed[0];
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
export function summarizeDockerMounts(mounts) {
|
| 33 |
+
return mounts.map((mount) => ({
|
| 34 |
+
type: mount.Type,
|
| 35 |
+
source: mount.Source,
|
| 36 |
+
destination: mount.Destination,
|
| 37 |
+
mode: mount.Mode,
|
| 38 |
+
writable: mount.RW
|
| 39 |
+
}));
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function parseArgs(argv) {
|
| 43 |
+
const options = {
|
| 44 |
+
dryRun: false,
|
| 45 |
+
help: false,
|
| 46 |
+
containerName: LEGACY_FIXTURE_CONTAINER
|
| 47 |
+
};
|
| 48 |
+
for (let index = 0; index < argv.length; index += 1) {
|
| 49 |
+
const arg = argv[index];
|
| 50 |
+
if (arg === '--dry-run') {
|
| 51 |
+
options.dryRun = true;
|
| 52 |
+
continue;
|
| 53 |
+
}
|
| 54 |
+
if (arg === '--help' || arg === '-h') {
|
| 55 |
+
options.help = true;
|
| 56 |
+
continue;
|
| 57 |
+
}
|
| 58 |
+
if (arg === '--container') {
|
| 59 |
+
const value = argv[index + 1];
|
| 60 |
+
if (!value) throw new Error('--container requires a value');
|
| 61 |
+
options.containerName = value;
|
| 62 |
+
index += 1;
|
| 63 |
+
continue;
|
| 64 |
+
}
|
| 65 |
+
throw new Error(`Unknown option: ${arg}`);
|
| 66 |
+
}
|
| 67 |
+
return options;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function printHelp() {
|
| 71 |
+
console.log(`用法:npm run docker:cleanup-fixtures -- [--dry-run] [--container <name>]
|
| 72 |
+
|
| 73 |
+
清理遗留的本地图片上游 Docker fixture 容器。该脚本只会删除同时满足以下条件的容器:
|
| 74 |
+
|
| 75 |
+
- 容器名匹配,默认 ${LEGACY_FIXTURE_CONTAINER}
|
| 76 |
+
- 存在 ${REPO_ROOT} -> ${LEGACY_FIXTURE_DESTINATION} 的整仓 bind mount
|
| 77 |
+
|
| 78 |
+
推荐使用 npm run smoke:image-upstream-local 启动本地 fixture。不要用 docker run -v "$PWD:${LEGACY_FIXTURE_DESTINATION}" 启动 fixture。`);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
function isMissingContainer(result) {
|
| 82 |
+
const output = `${result.stdout}\n${result.stderr}`;
|
| 83 |
+
return /No such object|No such container/i.test(output);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
export function buildAbsentReport(containerName) {
|
| 87 |
+
return {
|
| 88 |
+
ok: true,
|
| 89 |
+
command: 'docker:cleanup-fixtures',
|
| 90 |
+
container: containerName,
|
| 91 |
+
present: false,
|
| 92 |
+
unsafe_repo_mount: false,
|
| 93 |
+
removed: false
|
| 94 |
+
};
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
export function buildSkippedReport(containerName, mounts) {
|
| 98 |
+
return {
|
| 99 |
+
ok: true,
|
| 100 |
+
command: 'docker:cleanup-fixtures',
|
| 101 |
+
container: containerName,
|
| 102 |
+
present: true,
|
| 103 |
+
unsafe_repo_mount: false,
|
| 104 |
+
removed: false,
|
| 105 |
+
mounts: summarizeDockerMounts(mounts)
|
| 106 |
+
};
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
function cleanupLegacyFixtureContainer(options) {
|
| 110 |
+
const inspect = runCommand('docker', ['inspect', options.containerName], { timeoutMs: DOCKER_TIMEOUT_MS });
|
| 111 |
+
if (!inspect.ok) {
|
| 112 |
+
if (isMissingContainer(inspect)) return buildAbsentReport(options.containerName);
|
| 113 |
+
return {
|
| 114 |
+
ok: false,
|
| 115 |
+
command: 'docker:cleanup-fixtures',
|
| 116 |
+
phase: 'inspect',
|
| 117 |
+
container: options.containerName,
|
| 118 |
+
error: pickFailureOutput(inspect)
|
| 119 |
+
};
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
let container;
|
| 123 |
+
try {
|
| 124 |
+
container = parseDockerInspectContainer(inspect.stdout);
|
| 125 |
+
} catch (error) {
|
| 126 |
+
return {
|
| 127 |
+
ok: false,
|
| 128 |
+
command: 'docker:cleanup-fixtures',
|
| 129 |
+
phase: 'parse-inspect',
|
| 130 |
+
container: options.containerName,
|
| 131 |
+
error: error instanceof Error ? error.message : String(error)
|
| 132 |
+
};
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
const mounts = Array.isArray(container.Mounts) ? container.Mounts : [];
|
| 136 |
+
if (!hasLegacyFixtureRepoMount(mounts, REPO_ROOT)) return buildSkippedReport(options.containerName, mounts);
|
| 137 |
+
if (options.dryRun) {
|
| 138 |
+
return {
|
| 139 |
+
ok: true,
|
| 140 |
+
command: 'docker:cleanup-fixtures',
|
| 141 |
+
container: options.containerName,
|
| 142 |
+
present: true,
|
| 143 |
+
unsafe_repo_mount: true,
|
| 144 |
+
removed: false,
|
| 145 |
+
dry_run: true,
|
| 146 |
+
mounts: summarizeDockerMounts(mounts)
|
| 147 |
+
};
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
const remove = runCommand('docker', ['rm', '-f', options.containerName], { timeoutMs: DOCKER_TIMEOUT_MS });
|
| 151 |
+
return {
|
| 152 |
+
ok: remove.ok,
|
| 153 |
+
command: 'docker:cleanup-fixtures',
|
| 154 |
+
container: options.containerName,
|
| 155 |
+
present: true,
|
| 156 |
+
unsafe_repo_mount: true,
|
| 157 |
+
removed: remove.ok,
|
| 158 |
+
mounts: summarizeDockerMounts(mounts),
|
| 159 |
+
...(remove.ok ? {} : { phase: 'remove', error: pickFailureOutput(remove) })
|
| 160 |
+
};
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
function main() {
|
| 164 |
+
const options = parseArgs(process.argv.slice(2));
|
| 165 |
+
if (options.help) {
|
| 166 |
+
printHelp();
|
| 167 |
+
return;
|
| 168 |
+
}
|
| 169 |
+
const report = cleanupLegacyFixtureContainer(options);
|
| 170 |
+
printJson(report);
|
| 171 |
+
if (!report.ok) process.exitCode = 1;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
if (isMainModule(import.meta.url, process.argv[1])) main();
|
scripts/command-center-utils.mjs
CHANGED
|
@@ -80,6 +80,12 @@ export function parseJsonPayload(output, label = 'command') {
|
|
| 80 |
throw new Error(`${label} returned invalid JSON: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
| 81 |
}
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
export async function fetchJsonWithTimeout(url, options = {}) {
|
| 84 |
const timeoutMs = options.timeoutMs || DEFAULT_FETCH_TIMEOUT_MS;
|
| 85 |
const pathname = safeUrlPathname(url);
|
|
|
|
| 80 |
throw new Error(`${label} returned invalid JSON: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
| 81 |
}
|
| 82 |
|
| 83 |
+
export function redactBaseUrl(value) {
|
| 84 |
+
const parsed = new URL(value);
|
| 85 |
+
const path = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, '');
|
| 86 |
+
return `${parsed.protocol}//${parsed.host}${path}`;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
export async function fetchJsonWithTimeout(url, options = {}) {
|
| 90 |
const timeoutMs = options.timeoutMs || DEFAULT_FETCH_TIMEOUT_MS;
|
| 91 |
const pathname = safeUrlPathname(url);
|
scripts/command-center.test.mjs
CHANGED
|
@@ -6,8 +6,16 @@ import os from 'node:os';
|
|
| 6 |
import path from 'node:path';
|
| 7 |
import { describe, it } from 'node:test';
|
| 8 |
|
| 9 |
-
import { buildAgentDoctorArgs } from './agent-doctor.mjs';
|
| 10 |
import { fetchJsonWithTimeout, parseJsonPayload, pickFailureOutput, runCommand } from './command-center-utils.mjs';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
import {
|
| 12 |
buildAdminCommands,
|
| 13 |
buildImageUpstreamRealSmokeStatus,
|
|
@@ -21,12 +29,14 @@ import { buildVerifyPlan } from './verify.mjs';
|
|
| 21 |
describe('Command center scripts', () => {
|
| 22 |
it('exposes a small stable administrator command set', () => {
|
| 23 |
assert.deepEqual(buildAdminCommands(), {
|
|
|
|
| 24 |
doctor: 'npm run doctor',
|
| 25 |
status: 'npm run status',
|
| 26 |
env_summary: 'npm run env:summary',
|
| 27 |
verify: 'npm run verify',
|
| 28 |
deploy_local: 'npm run deploy:local',
|
| 29 |
deploy_space: 'npm run deploy:space',
|
|
|
|
| 30 |
agent_doctor: 'npm run agent:doctor',
|
| 31 |
hf_space_doctor: 'npm run doctor:hf-space',
|
| 32 |
hf_space_smoke: 'npm run smoke:hf-space'
|
|
@@ -322,6 +332,77 @@ describe('Command center scripts', () => {
|
|
| 322 |
});
|
| 323 |
});
|
| 324 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
it('fails local memory deploy probes when the overlay did not take effect', () => {
|
| 326 |
assert.doesNotThrow(() => assertLocalProbeMatchesMode({ stateBackend: 'sqlite', imageStorageMode: 'fs' }));
|
| 327 |
assert.doesNotThrow(() =>
|
|
@@ -339,10 +420,188 @@ describe('Command center scripts', () => {
|
|
| 339 |
assert.deepEqual(args.slice(1), ['--contract-check', '--timeout-ms', '60000', 'contract check']);
|
| 340 |
});
|
| 341 |
|
| 342 |
-
it('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
await withServer(
|
| 344 |
(request, response) => {
|
| 345 |
if (request.url === '/api/agent/capabilities') {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
response.writeHead(200, { 'content-type': 'application/json' });
|
| 347 |
response.end(
|
| 348 |
JSON.stringify({
|
|
@@ -370,7 +629,7 @@ describe('Command center scripts', () => {
|
|
| 370 |
);
|
| 371 |
return;
|
| 372 |
}
|
| 373 |
-
if (request.url === '/api/runtime-capabilities') {
|
| 374 |
response.writeHead(200, { 'content-type': 'application/json' });
|
| 375 |
response.end(
|
| 376 |
JSON.stringify({
|
|
@@ -384,7 +643,12 @@ describe('Command center scripts', () => {
|
|
| 384 |
);
|
| 385 |
return;
|
| 386 |
}
|
| 387 |
-
if (request.url === '/api/agent/images/generate') {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
response.writeHead(400, { 'content-type': 'application/json' });
|
| 389 |
response.end(
|
| 390 |
JSON.stringify({
|
|
@@ -397,7 +661,7 @@ describe('Command center scripts', () => {
|
|
| 397 |
);
|
| 398 |
return;
|
| 399 |
}
|
| 400 |
-
if (request.url === '/api/agent/jobs/images/generate') {
|
| 401 |
response.writeHead(400, { 'content-type': 'application/json' });
|
| 402 |
response.end(
|
| 403 |
JSON.stringify({
|
|
@@ -414,8 +678,9 @@ describe('Command center scripts', () => {
|
|
| 414 |
response.end(JSON.stringify({ error: 'missing' }));
|
| 415 |
},
|
| 416 |
async (baseUrl) => {
|
|
|
|
| 417 |
const result = await runNodeCommandAsync(['scripts/agent-doctor.mjs'], {
|
| 418 |
-
env: { ...process.env, GPT_IMAGE_PLAYGROUND_URL:
|
| 419 |
timeoutMs: 15_000
|
| 420 |
});
|
| 421 |
|
|
@@ -430,10 +695,126 @@ describe('Command center scripts', () => {
|
|
| 430 |
assert.equal(body.summary.billable_smoke, 'skipped');
|
| 431 |
assert.equal(body.layers.find((layer) => layer.name === 'billable_smoke').skipped, true);
|
| 432 |
assert.equal(body.layers.find((layer) => layer.name === 'capabilities').executable_routing_rules, true);
|
|
|
|
|
|
|
|
|
|
| 433 |
}
|
| 434 |
);
|
| 435 |
});
|
| 436 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
it('preserves raw child output for command consumers', () => {
|
| 438 |
const result = runCommand(process.execPath, ['-e', 'process.stdout.write(" M README.md\\n")']);
|
| 439 |
|
|
|
|
| 6 |
import path from 'node:path';
|
| 7 |
import { describe, it } from 'node:test';
|
| 8 |
|
| 9 |
+
import { buildAgentDoctorArgs, buildAgentDoctorContractArgs } from './agent-doctor.mjs';
|
| 10 |
import { fetchJsonWithTimeout, parseJsonPayload, pickFailureOutput, runCommand } from './command-center-utils.mjs';
|
| 11 |
+
import {
|
| 12 |
+
buildAbsentReport,
|
| 13 |
+
buildSkippedReport,
|
| 14 |
+
hasLegacyFixtureRepoMount,
|
| 15 |
+
parseDockerInspectContainer,
|
| 16 |
+
summarizeDockerMounts
|
| 17 |
+
} from './cleanup-docker-fixtures.mjs';
|
| 18 |
+
import { buildFirstRunReport, formatFirstRunText } from './first-run.mjs';
|
| 19 |
import {
|
| 20 |
buildAdminCommands,
|
| 21 |
buildImageUpstreamRealSmokeStatus,
|
|
|
|
| 29 |
describe('Command center scripts', () => {
|
| 30 |
it('exposes a small stable administrator command set', () => {
|
| 31 |
assert.deepEqual(buildAdminCommands(), {
|
| 32 |
+
first_run: 'npm run first-run',
|
| 33 |
doctor: 'npm run doctor',
|
| 34 |
status: 'npm run status',
|
| 35 |
env_summary: 'npm run env:summary',
|
| 36 |
verify: 'npm run verify',
|
| 37 |
deploy_local: 'npm run deploy:local',
|
| 38 |
deploy_space: 'npm run deploy:space',
|
| 39 |
+
docker_cleanup_fixtures: 'npm run docker:cleanup-fixtures',
|
| 40 |
agent_doctor: 'npm run agent:doctor',
|
| 41 |
hf_space_doctor: 'npm run doctor:hf-space',
|
| 42 |
hf_space_smoke: 'npm run smoke:hf-space'
|
|
|
|
| 332 |
});
|
| 333 |
});
|
| 334 |
|
| 335 |
+
it('detects only the legacy fixture whole-repository Docker mount', () => {
|
| 336 |
+
const repoRoot = '/Volumes/Work/code/gpt-image-playground-customer';
|
| 337 |
+
assert.equal(
|
| 338 |
+
hasLegacyFixtureRepoMount(
|
| 339 |
+
[
|
| 340 |
+
{
|
| 341 |
+
Type: 'bind',
|
| 342 |
+
Source: repoRoot,
|
| 343 |
+
Destination: '/workspace',
|
| 344 |
+
Mode: 'ro',
|
| 345 |
+
RW: false
|
| 346 |
+
}
|
| 347 |
+
],
|
| 348 |
+
repoRoot
|
| 349 |
+
),
|
| 350 |
+
true
|
| 351 |
+
);
|
| 352 |
+
assert.equal(
|
| 353 |
+
hasLegacyFixtureRepoMount(
|
| 354 |
+
[
|
| 355 |
+
{
|
| 356 |
+
Type: 'bind',
|
| 357 |
+
Source: `${repoRoot}/generated-images`,
|
| 358 |
+
Destination: '/app/generated-images',
|
| 359 |
+
Mode: '',
|
| 360 |
+
RW: true
|
| 361 |
+
}
|
| 362 |
+
],
|
| 363 |
+
repoRoot
|
| 364 |
+
),
|
| 365 |
+
false
|
| 366 |
+
);
|
| 367 |
+
});
|
| 368 |
+
|
| 369 |
+
it('parses and summarizes Docker fixture cleanup inspection data', () => {
|
| 370 |
+
const container = parseDockerInspectContainer(
|
| 371 |
+
JSON.stringify([
|
| 372 |
+
{
|
| 373 |
+
Mounts: [
|
| 374 |
+
{
|
| 375 |
+
Type: 'bind',
|
| 376 |
+
Source: '/repo',
|
| 377 |
+
Destination: '/workspace',
|
| 378 |
+
Mode: 'ro',
|
| 379 |
+
RW: false
|
| 380 |
+
}
|
| 381 |
+
]
|
| 382 |
+
}
|
| 383 |
+
])
|
| 384 |
+
);
|
| 385 |
+
|
| 386 |
+
assert.deepEqual(summarizeDockerMounts(container.Mounts), [
|
| 387 |
+
{
|
| 388 |
+
type: 'bind',
|
| 389 |
+
source: '/repo',
|
| 390 |
+
destination: '/workspace',
|
| 391 |
+
mode: 'ro',
|
| 392 |
+
writable: false
|
| 393 |
+
}
|
| 394 |
+
]);
|
| 395 |
+
assert.deepEqual(buildAbsentReport('gipc-local-image-fixture'), {
|
| 396 |
+
ok: true,
|
| 397 |
+
command: 'docker:cleanup-fixtures',
|
| 398 |
+
container: 'gipc-local-image-fixture',
|
| 399 |
+
present: false,
|
| 400 |
+
unsafe_repo_mount: false,
|
| 401 |
+
removed: false
|
| 402 |
+
});
|
| 403 |
+
assert.equal(buildSkippedReport('custom', container.Mounts).removed, false);
|
| 404 |
+
});
|
| 405 |
+
|
| 406 |
it('fails local memory deploy probes when the overlay did not take effect', () => {
|
| 407 |
assert.doesNotThrow(() => assertLocalProbeMatchesMode({ stateBackend: 'sqlite', imageStorageMode: 'fs' }));
|
| 408 |
assert.doesNotThrow(() =>
|
|
|
|
| 420 |
assert.deepEqual(args.slice(1), ['--contract-check', '--timeout-ms', '60000', 'contract check']);
|
| 421 |
});
|
| 422 |
|
| 423 |
+
it('routes agent:doctor contract check through an explicit base URL', () => {
|
| 424 |
+
const args = buildAgentDoctorContractArgs('https://space.example.test/');
|
| 425 |
+
assert.match(args[0], /generate-image\.mjs$/);
|
| 426 |
+
assert.deepEqual(args.slice(1), [
|
| 427 |
+
'--contract-check',
|
| 428 |
+
'--timeout-ms',
|
| 429 |
+
'60000',
|
| 430 |
+
'--base-url',
|
| 431 |
+
'https://space.example.test/',
|
| 432 |
+
'contract check'
|
| 433 |
+
]);
|
| 434 |
+
});
|
| 435 |
+
|
| 436 |
+
it('reports first-run readiness without exposing secrets', async () => {
|
| 437 |
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'first-run-'));
|
| 438 |
+
await writeFile(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'demo', version: '1.0.0' }));
|
| 439 |
+
await writeFile(path.join(tempDir, 'package-lock.json'), '{}');
|
| 440 |
+
await writeFile(path.join(tempDir, '.env.agent.local'), 'GPT_IMAGE_AGENT_TOKEN=file-secret\n');
|
| 441 |
await withServer(
|
| 442 |
(request, response) => {
|
| 443 |
if (request.url === '/api/agent/capabilities') {
|
| 444 |
+
assert.equal(request.headers.authorization, 'Bearer shell-secret');
|
| 445 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 446 |
+
response.end(
|
| 447 |
+
JSON.stringify({
|
| 448 |
+
auth: { schemes: ['bearer'] },
|
| 449 |
+
defaults: { state_backend: 'memory' },
|
| 450 |
+
supported: { image_backend_requirements: {} }
|
| 451 |
+
})
|
| 452 |
+
);
|
| 453 |
+
return;
|
| 454 |
+
}
|
| 455 |
+
if (request.url === '/api/runtime-capabilities') {
|
| 456 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 457 |
+
response.end(JSON.stringify({ streaming: { defaultMode: 'auto' } }));
|
| 458 |
+
return;
|
| 459 |
+
}
|
| 460 |
+
response.writeHead(404, { 'content-type': 'application/json' });
|
| 461 |
+
response.end(JSON.stringify({ error: 'missing' }));
|
| 462 |
+
},
|
| 463 |
+
async (baseUrl) => {
|
| 464 |
+
try {
|
| 465 |
+
const report = await buildFirstRunReport(
|
| 466 |
+
{
|
| 467 |
+
cwd: tempDir,
|
| 468 |
+
baseUrl,
|
| 469 |
+
envFiles: ['.env.agent.local'],
|
| 470 |
+
timeoutMs: 1000
|
| 471 |
+
},
|
| 472 |
+
{ GPT_IMAGE_AGENT_TOKEN: 'shell-secret' }
|
| 473 |
+
);
|
| 474 |
+
|
| 475 |
+
assert.equal(report.command, 'first-run');
|
| 476 |
+
assert.equal(report.billable, false);
|
| 477 |
+
assert.equal(report.service_base_url, baseUrl);
|
| 478 |
+
assert.equal(report.service_base_url_source, 'user_provided');
|
| 479 |
+
assert.equal(report.interactive_confirmation_required, false);
|
| 480 |
+
assert.deepEqual(report.agent_auth_process, {
|
| 481 |
+
has_token: true,
|
| 482 |
+
has_password_hash: false,
|
| 483 |
+
has_any_auth: true
|
| 484 |
+
});
|
| 485 |
+
assert.deepEqual(report.private_agent_env, {
|
| 486 |
+
exists: true,
|
| 487 |
+
has_token: true,
|
| 488 |
+
has_password_hash: false
|
| 489 |
+
});
|
| 490 |
+
assert.equal(report.service.ok, true);
|
| 491 |
+
assert.equal(report.checks.find((check) => check.name === 'agent_auth_available_to_process').ok, true);
|
| 492 |
+
assert.equal(
|
| 493 |
+
report.checks.find((check) => check.name === 'agent_auth_available_to_process')
|
| 494 |
+
.auth_in_private_env_file,
|
| 495 |
+
true
|
| 496 |
+
);
|
| 497 |
+
assert.doesNotMatch(JSON.stringify(report), /shell-secret|file-secret/);
|
| 498 |
+
assert.match(JSON.stringify(report.next_actions), /agent:doctor/);
|
| 499 |
+
} finally {
|
| 500 |
+
await rm(tempDir, { recursive: true, force: true });
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
);
|
| 504 |
+
});
|
| 505 |
+
|
| 506 |
+
it('formats first-run as human-readable text by default', async () => {
|
| 507 |
+
const report = await buildFirstRunReport({ cwd: process.cwd(), baseUrl: 'not a url', envFiles: [] }, {});
|
| 508 |
+
const text = formatFirstRunText(report);
|
| 509 |
+
|
| 510 |
+
assert.match(text, /^首次配置检查:需要处理/m);
|
| 511 |
+
assert.match(text, /服务地址:无效/);
|
| 512 |
+
assert.match(text, /当前进程鉴权:未加载/);
|
| 513 |
+
assert.match(text, /私有 Agent env:不存在/);
|
| 514 |
+
assert.match(text, /下一步:/);
|
| 515 |
+
assert.doesNotMatch(text, /^\{/);
|
| 516 |
+
});
|
| 517 |
+
|
| 518 |
+
it('preserves path-prefixed service URLs in first-run reports', async () => {
|
| 519 |
+
// This test only checks local URL normalization in the report; .test is reserved and should not resolve.
|
| 520 |
+
const report = await buildFirstRunReport(
|
| 521 |
+
{
|
| 522 |
+
cwd: process.cwd(),
|
| 523 |
+
baseUrl: 'https://space.example.test/proxy/',
|
| 524 |
+
envFiles: [],
|
| 525 |
+
timeoutMs: 5
|
| 526 |
+
},
|
| 527 |
+
{}
|
| 528 |
+
);
|
| 529 |
+
|
| 530 |
+
assert.equal(report.service_base_url, 'https://space.example.test/proxy');
|
| 531 |
+
});
|
| 532 |
+
|
| 533 |
+
it('prints localized first-run help and option errors', async () => {
|
| 534 |
+
const help = await runNodeCommandAsync(['scripts/first-run.mjs', '--help'], {
|
| 535 |
+
env: process.env,
|
| 536 |
+
timeoutMs: 5_000
|
| 537 |
+
});
|
| 538 |
+
assert.equal(help.ok, true);
|
| 539 |
+
assert.match(help.stdout, /用法:/);
|
| 540 |
+
assert.match(help.stdout, /输出机器可读 JSON/);
|
| 541 |
+
assert.doesNotMatch(help.stdout, /Usage:/);
|
| 542 |
+
|
| 543 |
+
const invalid = await runNodeCommandAsync(['scripts/first-run.mjs', '--missing-option'], {
|
| 544 |
+
env: process.env,
|
| 545 |
+
timeoutMs: 5_000
|
| 546 |
+
});
|
| 547 |
+
assert.equal(invalid.ok, false);
|
| 548 |
+
assert.match(invalid.stdout, /"ok": false/);
|
| 549 |
+
assert.match(invalid.stdout, /未知参数:--missing-option/);
|
| 550 |
+
});
|
| 551 |
+
|
| 552 |
+
it('keeps first-run local discovery explicit when no URL is provided', async () => {
|
| 553 |
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'first-run-default-'));
|
| 554 |
+
try {
|
| 555 |
+
await writeFile(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'demo', version: '1.0.0' }));
|
| 556 |
+
|
| 557 |
+
const report = await buildFirstRunReport({ cwd: tempDir, envFiles: [], timeoutMs: 20 }, {});
|
| 558 |
+
|
| 559 |
+
assert.equal(report.service_base_url_source, 'default_local_probe');
|
| 560 |
+
assert.equal(report.interactive_confirmation_required, true);
|
| 561 |
+
assert.equal(report.ok, false);
|
| 562 |
+
assert.match(JSON.stringify(report.next_actions), /确认探测到的服务地址/);
|
| 563 |
+
} finally {
|
| 564 |
+
await rm(tempDir, { recursive: true, force: true });
|
| 565 |
+
}
|
| 566 |
+
});
|
| 567 |
+
|
| 568 |
+
it('points first-run at auth setup when the service rejects capabilities with 401', async () => {
|
| 569 |
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'first-run-auth-'));
|
| 570 |
+
await writeFile(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'demo', version: '1.0.0' }));
|
| 571 |
+
await withServer(
|
| 572 |
+
(_request, response) => {
|
| 573 |
+
response.writeHead(401, { 'content-type': 'application/json' });
|
| 574 |
+
response.end(JSON.stringify({ error: 'unauthorized' }));
|
| 575 |
+
},
|
| 576 |
+
async (baseUrl) => {
|
| 577 |
+
try {
|
| 578 |
+
const report = await buildFirstRunReport(
|
| 579 |
+
{
|
| 580 |
+
cwd: tempDir,
|
| 581 |
+
baseUrl: `${baseUrl}/`,
|
| 582 |
+
envFiles: [],
|
| 583 |
+
timeoutMs: 1000
|
| 584 |
+
},
|
| 585 |
+
{}
|
| 586 |
+
);
|
| 587 |
+
|
| 588 |
+
assert.equal(report.service_base_url, baseUrl);
|
| 589 |
+
assert.equal(report.checks.find((check) => check.name === 'agent_auth_available_to_process').skipped, false);
|
| 590 |
+
assert.equal(report.checks.find((check) => check.name === 'service_reachable').status, 401);
|
| 591 |
+
assert.equal('runtime' in report.service, false);
|
| 592 |
+
assert.match(JSON.stringify(report.next_actions), /GPT_IMAGE_AGENT_TOKEN/);
|
| 593 |
+
assert.doesNotMatch(JSON.stringify(report.next_actions), /Start the service/);
|
| 594 |
+
} finally {
|
| 595 |
+
await rm(tempDir, { recursive: true, force: true });
|
| 596 |
+
}
|
| 597 |
+
}
|
| 598 |
+
);
|
| 599 |
+
});
|
| 600 |
+
|
| 601 |
+
it('reports layered agent:doctor diagnostics without billable smoke by default', async () => {
|
| 602 |
+
await withServer(
|
| 603 |
+
(request, response) => {
|
| 604 |
+
if (request.url === '/proxy/api/agent/capabilities') {
|
| 605 |
response.writeHead(200, { 'content-type': 'application/json' });
|
| 606 |
response.end(
|
| 607 |
JSON.stringify({
|
|
|
|
| 629 |
);
|
| 630 |
return;
|
| 631 |
}
|
| 632 |
+
if (request.url === '/proxy/api/runtime-capabilities') {
|
| 633 |
response.writeHead(200, { 'content-type': 'application/json' });
|
| 634 |
response.end(
|
| 635 |
JSON.stringify({
|
|
|
|
| 643 |
);
|
| 644 |
return;
|
| 645 |
}
|
| 646 |
+
if (request.url === '/proxy/api/agent/images/generate') {
|
| 647 |
+
if (request.headers['idempotency-key']) {
|
| 648 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 649 |
+
response.end(JSON.stringify({ images: [{ id: 'doctor-generate', filename: 'doctor.png' }] }));
|
| 650 |
+
return;
|
| 651 |
+
}
|
| 652 |
response.writeHead(400, { 'content-type': 'application/json' });
|
| 653 |
response.end(
|
| 654 |
JSON.stringify({
|
|
|
|
| 661 |
);
|
| 662 |
return;
|
| 663 |
}
|
| 664 |
+
if (request.url === '/proxy/api/agent/jobs/images/generate') {
|
| 665 |
response.writeHead(400, { 'content-type': 'application/json' });
|
| 666 |
response.end(
|
| 667 |
JSON.stringify({
|
|
|
|
| 678 |
response.end(JSON.stringify({ error: 'missing' }));
|
| 679 |
},
|
| 680 |
async (baseUrl) => {
|
| 681 |
+
const prefixedBaseUrl = `${baseUrl}/proxy`;
|
| 682 |
const result = await runNodeCommandAsync(['scripts/agent-doctor.mjs'], {
|
| 683 |
+
env: { ...process.env, GPT_IMAGE_PLAYGROUND_URL: `${prefixedBaseUrl}/` },
|
| 684 |
timeoutMs: 15_000
|
| 685 |
});
|
| 686 |
|
|
|
|
| 695 |
assert.equal(body.summary.billable_smoke, 'skipped');
|
| 696 |
assert.equal(body.layers.find((layer) => layer.name === 'billable_smoke').skipped, true);
|
| 697 |
assert.equal(body.layers.find((layer) => layer.name === 'capabilities').executable_routing_rules, true);
|
| 698 |
+
assert.equal(body.service_base_url, prefixedBaseUrl);
|
| 699 |
+
assert.equal(body.service_base_url_source, 'GPT_IMAGE_PLAYGROUND_URL');
|
| 700 |
+
assert.equal(body.interactive_confirmation_required, true);
|
| 701 |
}
|
| 702 |
);
|
| 703 |
});
|
| 704 |
|
| 705 |
+
it('uses an explicit base URL for agent:doctor billable smoke checks', async () => {
|
| 706 |
+
const hits = [];
|
| 707 |
+
await withServer(
|
| 708 |
+
(request, response) => {
|
| 709 |
+
hits.push(request.url);
|
| 710 |
+
if (request.url === '/api/agent/capabilities') {
|
| 711 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 712 |
+
response.end(
|
| 713 |
+
JSON.stringify({
|
| 714 |
+
defaults: { state_backend: 'memory' },
|
| 715 |
+
storage: { image_storage_mode: 'indexeddb', postgres_configured: false },
|
| 716 |
+
agent_streaming: { page_sse: { supported: true } },
|
| 717 |
+
agent_jobs: { supported: true },
|
| 718 |
+
routing_rules: {
|
| 719 |
+
high_resolution_edit: {
|
| 720 |
+
conditions: { operation: 'edit', max_edge: { operator: 'gt', value: 2048 } }
|
| 721 |
+
}
|
| 722 |
+
},
|
| 723 |
+
supported: {
|
| 724 |
+
image_backend_requirements: {
|
| 725 |
+
'responses-image-generation': {
|
| 726 |
+
supported: true,
|
| 727 |
+
enabled: true,
|
| 728 |
+
missing_env: []
|
| 729 |
+
}
|
| 730 |
+
}
|
| 731 |
+
}
|
| 732 |
+
})
|
| 733 |
+
);
|
| 734 |
+
return;
|
| 735 |
+
}
|
| 736 |
+
if (request.url === '/api/runtime-capabilities') {
|
| 737 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 738 |
+
response.end(
|
| 739 |
+
JSON.stringify({
|
| 740 |
+
streaming: {
|
| 741 |
+
defaultMode: 'auto',
|
| 742 |
+
unavailableMarkScope: 'channel+backend+strategy+operation'
|
| 743 |
+
},
|
| 744 |
+
streamingBatch: { enabled: true },
|
| 745 |
+
responsesImageBackend: { enabled: true, mode: 'experimental' }
|
| 746 |
+
})
|
| 747 |
+
);
|
| 748 |
+
return;
|
| 749 |
+
}
|
| 750 |
+
if (request.url === '/api/agent/images/generate') {
|
| 751 |
+
if (request.headers['idempotency-key']) {
|
| 752 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 753 |
+
response.end(JSON.stringify({ images: [{ id: 'doctor-generate', filename: 'doctor.png' }] }));
|
| 754 |
+
} else {
|
| 755 |
+
response.writeHead(400, { 'content-type': 'application/json' });
|
| 756 |
+
response.end(
|
| 757 |
+
JSON.stringify({
|
| 758 |
+
error: {
|
| 759 |
+
code: 'idempotency_key_required',
|
| 760 |
+
message: 'missing key',
|
| 761 |
+
retryable: false
|
| 762 |
+
}
|
| 763 |
+
})
|
| 764 |
+
);
|
| 765 |
+
}
|
| 766 |
+
return;
|
| 767 |
+
}
|
| 768 |
+
if (request.url === '/api/agent/jobs/images/generate') {
|
| 769 |
+
response.writeHead(400, { 'content-type': 'application/json' });
|
| 770 |
+
response.end(
|
| 771 |
+
JSON.stringify({
|
| 772 |
+
error: {
|
| 773 |
+
code: 'idempotency_key_required',
|
| 774 |
+
message: 'missing key',
|
| 775 |
+
retryable: false
|
| 776 |
+
}
|
| 777 |
+
})
|
| 778 |
+
);
|
| 779 |
+
return;
|
| 780 |
+
}
|
| 781 |
+
response.writeHead(404, { 'content-type': 'application/json' });
|
| 782 |
+
response.end(JSON.stringify({ error: 'missing' }));
|
| 783 |
+
},
|
| 784 |
+
async (baseUrl) => {
|
| 785 |
+
const result = await runNodeCommandAsync(
|
| 786 |
+
['scripts/agent-doctor.mjs', '--base-url', baseUrl, '--allow-billable'],
|
| 787 |
+
{ timeoutMs: 15_000 }
|
| 788 |
+
);
|
| 789 |
+
|
| 790 |
+
assert.equal(result.ok, true);
|
| 791 |
+
const body = parseJsonPayload(result.stdout, 'agent doctor');
|
| 792 |
+
assert.equal(body.service_base_url_source, 'user_provided');
|
| 793 |
+
assert.equal(body.interactive_confirmation_required, false);
|
| 794 |
+
assert.ok(hits.includes('/api/agent/images/generate'));
|
| 795 |
+
}
|
| 796 |
+
);
|
| 797 |
+
});
|
| 798 |
+
|
| 799 |
+
it('prints localized agent:doctor help and option errors', async () => {
|
| 800 |
+
const help = await runNodeCommandAsync(['scripts/agent-doctor.mjs', '--help'], {
|
| 801 |
+
env: process.env,
|
| 802 |
+
timeoutMs: 5_000
|
| 803 |
+
});
|
| 804 |
+
assert.equal(help.ok, true);
|
| 805 |
+
assert.match(help.stdout, /用法:/);
|
| 806 |
+
assert.match(help.stdout, /真实 generate\/edit smoke/);
|
| 807 |
+
assert.doesNotMatch(help.stdout, /Usage:/);
|
| 808 |
+
|
| 809 |
+
const invalid = await runNodeCommandAsync(['scripts/agent-doctor.mjs', '--bad-option'], {
|
| 810 |
+
env: process.env,
|
| 811 |
+
timeoutMs: 5_000
|
| 812 |
+
});
|
| 813 |
+
assert.equal(invalid.ok, false);
|
| 814 |
+
assert.match(invalid.stdout, /"ok": false/);
|
| 815 |
+
assert.match(invalid.stdout, /未知参数:--bad-option/);
|
| 816 |
+
});
|
| 817 |
+
|
| 818 |
it('preserves raw child output for command consumers', () => {
|
| 819 |
const result = runCommand(process.execPath, ['-e', 'process.stdout.write(" M README.md\\n")']);
|
| 820 |
|
scripts/first-run.mjs
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
import { existsSync, readFileSync } from 'node:fs';
|
| 4 |
+
import { join } from 'node:path';
|
| 5 |
+
|
| 6 |
+
import { isMainModule, printJson, redactBaseUrl } from './command-center-utils.mjs';
|
| 7 |
+
import { summarizeEnvFile } from './env-summary.mjs';
|
| 8 |
+
import { resolvePlaygroundBaseUrl } from '../skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs';
|
| 9 |
+
|
| 10 |
+
const DEFAULT_TIMEOUT_MS = 3000;
|
| 11 |
+
const DEFAULT_ENV_FILES = ['.env.local', '.env.agent.local'];
|
| 12 |
+
|
| 13 |
+
function parseArgs(argv) {
|
| 14 |
+
const parsed = {
|
| 15 |
+
help: false,
|
| 16 |
+
json: false,
|
| 17 |
+
baseUrl: undefined,
|
| 18 |
+
timeoutMs: DEFAULT_TIMEOUT_MS
|
| 19 |
+
};
|
| 20 |
+
for (let index = 0; index < argv.length; index += 1) {
|
| 21 |
+
const arg = argv[index];
|
| 22 |
+
if (arg === '--help' || arg === '-h') parsed.help = true;
|
| 23 |
+
else if (arg === '--json') parsed.json = true;
|
| 24 |
+
else if (arg === '--base-url') parsed.baseUrl = readOptionValue(argv, (index += 1), arg);
|
| 25 |
+
else if (arg === '--timeout-ms') parsed.timeoutMs = readPositiveInteger(readOptionValue(argv, (index += 1), arg), arg);
|
| 26 |
+
else throw new Error(`未知参数:${arg}`);
|
| 27 |
+
}
|
| 28 |
+
return parsed;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function printHelp() {
|
| 32 |
+
console.log(`用法:
|
| 33 |
+
npm run first-run
|
| 34 |
+
npm run first-run -- --json
|
| 35 |
+
npm run first-run -- --base-url https://your-space.hf.space
|
| 36 |
+
|
| 37 |
+
选项:
|
| 38 |
+
--json 输出机器可读 JSON。
|
| 39 |
+
--base-url 显式服务地址,优先于 GPT_IMAGE_PLAYGROUND_URL 和本地探测。
|
| 40 |
+
--timeout-ms HTTP 探测超时,默认 3000。
|
| 41 |
+
|
| 42 |
+
first-run 只读、非计费,不写 env 文件或 secret。`);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
export async function buildFirstRunReport(options = {}, env = process.env) {
|
| 46 |
+
const cwd = options.cwd || process.cwd();
|
| 47 |
+
const envFiles = options.envFiles || DEFAULT_ENV_FILES;
|
| 48 |
+
const base = resolveFirstRunBaseUrl(options.baseUrl, env);
|
| 49 |
+
const envSummary = envFiles.map((filePath) => summarizeEnvFile(join(cwd, filePath)));
|
| 50 |
+
const validationError = readBaseUrlValidationError(base);
|
| 51 |
+
const service = validationError
|
| 52 |
+
? { ok: false, skipped: true, error: validationError }
|
| 53 |
+
: await probeService(base.baseUrl, {
|
| 54 |
+
timeoutMs: options.timeoutMs || DEFAULT_TIMEOUT_MS,
|
| 55 |
+
headers: authHeaders(env)
|
| 56 |
+
});
|
| 57 |
+
const agentAuth = readAgentAuthState(env, envSummary);
|
| 58 |
+
const checks = buildChecks({
|
| 59 |
+
cwd,
|
| 60 |
+
env,
|
| 61 |
+
envSummary,
|
| 62 |
+
service,
|
| 63 |
+
base,
|
| 64 |
+
validationError
|
| 65 |
+
});
|
| 66 |
+
const nextActions = buildNextActions({ checks, base, service, env, envSummary, validationError });
|
| 67 |
+
return {
|
| 68 |
+
ok: checks.every((check) => check.ok || check.skipped),
|
| 69 |
+
command: 'first-run',
|
| 70 |
+
billable: false,
|
| 71 |
+
service_base_url: validationError ? undefined : redactBaseUrl(base.baseUrl),
|
| 72 |
+
service_base_url_source: base.source,
|
| 73 |
+
interactive_confirmation_required: base.interactive_confirmation_required,
|
| 74 |
+
agent_auth_process: agentAuth.process,
|
| 75 |
+
private_agent_env: agentAuth.privateEnv,
|
| 76 |
+
checks,
|
| 77 |
+
env_sources: envSummary.map((source) => summarizeEnvSource(source, cwd)),
|
| 78 |
+
service: summarizeService(service),
|
| 79 |
+
next_actions: nextActions
|
| 80 |
+
};
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
async function probeService(baseUrl, options) {
|
| 84 |
+
const capabilities = await readJsonEndpoint(`${baseUrl}/api/agent/capabilities`, options);
|
| 85 |
+
const runtime = capabilities.ok ? await readJsonEndpoint(`${baseUrl}/api/runtime-capabilities`, options) : undefined;
|
| 86 |
+
return {
|
| 87 |
+
ok: capabilities.ok && (!runtime || runtime.ok),
|
| 88 |
+
capabilities,
|
| 89 |
+
...(runtime ? { runtime } : {})
|
| 90 |
+
};
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
async function readJsonEndpoint(url, options) {
|
| 94 |
+
const controller = new AbortController();
|
| 95 |
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
| 96 |
+
try {
|
| 97 |
+
const response = await fetch(url, { headers: options.headers, signal: controller.signal });
|
| 98 |
+
const text = await response.text();
|
| 99 |
+
if (!response.ok) {
|
| 100 |
+
return {
|
| 101 |
+
ok: false,
|
| 102 |
+
status: response.status,
|
| 103 |
+
endpoint: safeEndpoint(url),
|
| 104 |
+
error: `HTTP ${response.status}`
|
| 105 |
+
};
|
| 106 |
+
}
|
| 107 |
+
try {
|
| 108 |
+
return {
|
| 109 |
+
ok: true,
|
| 110 |
+
status: response.status,
|
| 111 |
+
endpoint: safeEndpoint(url),
|
| 112 |
+
body: JSON.parse(text)
|
| 113 |
+
};
|
| 114 |
+
} catch {
|
| 115 |
+
return {
|
| 116 |
+
ok: false,
|
| 117 |
+
status: response.status,
|
| 118 |
+
endpoint: safeEndpoint(url),
|
| 119 |
+
error: 'response_not_json'
|
| 120 |
+
};
|
| 121 |
+
}
|
| 122 |
+
} catch (error) {
|
| 123 |
+
return {
|
| 124 |
+
ok: false,
|
| 125 |
+
endpoint: safeEndpoint(url),
|
| 126 |
+
error: error?.name === 'AbortError' ? `timeout_after_${options.timeoutMs}ms` : 'request_failed'
|
| 127 |
+
};
|
| 128 |
+
} finally {
|
| 129 |
+
clearTimeout(timer);
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
function buildChecks({ cwd, env, envSummary, service, base, validationError }) {
|
| 134 |
+
const node = readNodeCheck();
|
| 135 |
+
const packageJson = readPackageJson(cwd);
|
| 136 |
+
const packageLock = existsSync(join(cwd, 'package-lock.json'));
|
| 137 |
+
const nodeModules = existsSync(join(cwd, 'node_modules'));
|
| 138 |
+
const currentToken = Boolean(env.GPT_IMAGE_AGENT_TOKEN || env.GPT_IMAGE_APP_PASSWORD_HASH);
|
| 139 |
+
const fileToken = envSummary.some((source) =>
|
| 140 |
+
['GPT_IMAGE_AGENT_TOKEN', 'GPT_IMAGE_APP_PASSWORD_HASH'].some((name) => sourceHasSetVariable(source, name))
|
| 141 |
+
);
|
| 142 |
+
return [
|
| 143 |
+
{ name: 'node_version', ok: node.ok, current: node.current, required: '>=20.0.0' },
|
| 144 |
+
{ name: 'package_lock', ok: packageLock },
|
| 145 |
+
{
|
| 146 |
+
name: 'dependencies_installed',
|
| 147 |
+
ok: nodeModules,
|
| 148 |
+
skipped: false,
|
| 149 |
+
hint: nodeModules ? undefined : 'run npm install'
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
name: 'env_files',
|
| 153 |
+
ok: envSummary.some((source) => source.exists),
|
| 154 |
+
files: envSummary.map((source) => ({ path: source.path.replace(`${cwd}/`, ''), exists: source.exists }))
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
name: 'agent_auth_available_to_process',
|
| 158 |
+
ok: currentToken,
|
| 159 |
+
skipped: !requiresAgentAuth(service),
|
| 160 |
+
auth_in_private_env_file: fileToken
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
name: 'service_base_url_valid',
|
| 164 |
+
ok: !validationError,
|
| 165 |
+
source: base.source,
|
| 166 |
+
...(validationError ? { error: validationError } : {})
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
name: 'service_reachable',
|
| 170 |
+
ok: service.ok,
|
| 171 |
+
endpoint: '/api/agent/capabilities',
|
| 172 |
+
...(service.capabilities?.status ? { status: service.capabilities.status } : {}),
|
| 173 |
+
...(service.capabilities?.error ? { error: service.capabilities.error } : {})
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
name: 'agent_capabilities_contract',
|
| 177 |
+
ok: isCapabilitiesBody(service.capabilities?.body),
|
| 178 |
+
skipped: !service.capabilities?.ok
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
name: 'runtime_capabilities_contract',
|
| 182 |
+
ok: Boolean(service.runtime?.ok),
|
| 183 |
+
skipped: !service.capabilities?.ok
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
name: 'package_metadata',
|
| 187 |
+
ok: Boolean(packageJson.name && packageJson.version),
|
| 188 |
+
package: packageJson.name,
|
| 189 |
+
version: packageJson.version
|
| 190 |
+
}
|
| 191 |
+
];
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
function readAgentAuthState(env, envSummary) {
|
| 195 |
+
const hasToken = Boolean(env.GPT_IMAGE_AGENT_TOKEN);
|
| 196 |
+
const hasPasswordHash = Boolean(env.GPT_IMAGE_APP_PASSWORD_HASH);
|
| 197 |
+
const privateSource = envSummary.find((source) => source.path.endsWith('.env.agent.local'));
|
| 198 |
+
return {
|
| 199 |
+
process: {
|
| 200 |
+
has_token: hasToken,
|
| 201 |
+
has_password_hash: hasPasswordHash,
|
| 202 |
+
has_any_auth: hasToken || hasPasswordHash
|
| 203 |
+
},
|
| 204 |
+
privateEnv: {
|
| 205 |
+
exists: privateSource?.exists === true,
|
| 206 |
+
has_token: privateSource ? sourceHasSetVariable(privateSource, 'GPT_IMAGE_AGENT_TOKEN') : false,
|
| 207 |
+
has_password_hash: privateSource ? sourceHasSetVariable(privateSource, 'GPT_IMAGE_APP_PASSWORD_HASH') : false
|
| 208 |
+
}
|
| 209 |
+
};
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
function buildNextActions({ checks, base, service, env, envSummary, validationError }) {
|
| 213 |
+
const actions = [];
|
| 214 |
+
const hasCurrentAuth = Boolean(env.GPT_IMAGE_AGENT_TOKEN || env.GPT_IMAGE_APP_PASSWORD_HASH);
|
| 215 |
+
const hasFileAuth = envSummary.some((source) =>
|
| 216 |
+
['GPT_IMAGE_AGENT_TOKEN', 'GPT_IMAGE_APP_PASSWORD_HASH'].some((name) => sourceHasSetVariable(source, name))
|
| 217 |
+
);
|
| 218 |
+
if (!findCheck(checks, 'node_version').ok) actions.push('安装 Node.js 20 或更新版本。');
|
| 219 |
+
if (!findCheck(checks, 'dependencies_installed').ok) actions.push('运行 npm install。');
|
| 220 |
+
if (!findCheck(checks, 'env_files').ok) {
|
| 221 |
+
actions.push('复制 .env.example 为 .env.local,或在页面设置里配置默认上游。');
|
| 222 |
+
}
|
| 223 |
+
if (validationError) {
|
| 224 |
+
actions.push('把 GPT_IMAGE_PLAYGROUND_URL 或 --base-url 设为不含凭据、查询参数或片段的 http/https 地址。');
|
| 225 |
+
return actions;
|
| 226 |
+
}
|
| 227 |
+
if (!hasCurrentAuth && requiresAgentAuth(service) && !hasFileAuth) {
|
| 228 |
+
actions.push(
|
| 229 |
+
'在仓库外导出 GPT_IMAGE_AGENT_TOKEN 或 GPT_IMAGE_APP_PASSWORD_HASH,再运行受保护的 Agent 脚本。'
|
| 230 |
+
);
|
| 231 |
+
}
|
| 232 |
+
if (!service.ok && !requiresAgentAuth(service)) {
|
| 233 |
+
actions.push('先用 npm run dev 或 docker compose up -d --build --remove-orphans 启动服务,再重新运行 npm run first-run。');
|
| 234 |
+
}
|
| 235 |
+
if (base.interactive_confirmation_required) {
|
| 236 |
+
actions.push('在交互式 Agent 任务里,先和用户确认探测到的服务地址,再发真实请求。');
|
| 237 |
+
}
|
| 238 |
+
if (!hasCurrentAuth && hasFileAuth) {
|
| 239 |
+
actions.push('先把私有 Agent env 文件加载到 shell,再运行 Agent 脚本。');
|
| 240 |
+
}
|
| 241 |
+
if (service.ok && hasCurrentAuth) {
|
| 242 |
+
actions.push('运行 npm run agent:doctor 做完整的非计费 Agent 合同检查。');
|
| 243 |
+
}
|
| 244 |
+
return actions;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
function requiresAgentAuth(service) {
|
| 248 |
+
if (service.capabilities?.status === 401 || service.capabilities?.status === 403) return true;
|
| 249 |
+
const schemes = service.capabilities?.body?.auth?.schemes;
|
| 250 |
+
return Array.isArray(schemes) && schemes.length > 0;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
function sourceHasSetVariable(source, name) {
|
| 254 |
+
return source.exists && source.variables.some((item) => item.name === name && item.set === true);
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
function summarizeEnvSource(source, cwd) {
|
| 258 |
+
return {
|
| 259 |
+
path: source.path.startsWith(`${cwd}/`) ? source.path.slice(cwd.length + 1) : source.path,
|
| 260 |
+
exists: source.exists,
|
| 261 |
+
variable_count: source.variable_count || 0,
|
| 262 |
+
configured: source.variables
|
| 263 |
+
.filter((item) => item.set)
|
| 264 |
+
.map((item) => ({
|
| 265 |
+
name: item.name,
|
| 266 |
+
sensitive: item.sensitive,
|
| 267 |
+
value_kind: item.value_kind
|
| 268 |
+
}))
|
| 269 |
+
};
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
function summarizeService(service) {
|
| 273 |
+
const capabilities = service.capabilities || {};
|
| 274 |
+
const runtime = service.runtime;
|
| 275 |
+
return {
|
| 276 |
+
ok: service.ok,
|
| 277 |
+
capabilities: {
|
| 278 |
+
ok: capabilities.ok === true,
|
| 279 |
+
endpoint: capabilities.endpoint,
|
| 280 |
+
...(capabilities.status ? { status: capabilities.status } : {}),
|
| 281 |
+
...(capabilities.error ? { error: capabilities.error } : {}),
|
| 282 |
+
...(capabilities.ok ? summarizeCapabilitiesBody(capabilities.body) : {})
|
| 283 |
+
},
|
| 284 |
+
...(runtime
|
| 285 |
+
? {
|
| 286 |
+
runtime: {
|
| 287 |
+
ok: runtime.ok === true,
|
| 288 |
+
endpoint: runtime.endpoint,
|
| 289 |
+
...(runtime.status ? { status: runtime.status } : {}),
|
| 290 |
+
...(runtime.error ? { error: runtime.error } : {}),
|
| 291 |
+
...(runtime.ok ? summarizeRuntimeBody(runtime.body) : {})
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
: {})
|
| 295 |
+
};
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
function summarizeCapabilitiesBody(body) {
|
| 299 |
+
return {
|
| 300 |
+
auth_required: body?.auth?.required === true,
|
| 301 |
+
auth_schemes: Array.isArray(body?.auth?.schemes) ? body.auth.schemes : [],
|
| 302 |
+
state_backend: body?.defaults?.state_backend,
|
| 303 |
+
image_storage_mode: body?.storage?.image_storage_mode,
|
| 304 |
+
agent_jobs_supported: body?.agent_jobs?.supported === true,
|
| 305 |
+
page_sse_supported: body?.agent_streaming?.page_sse?.supported === true,
|
| 306 |
+
responses_image_backend_enabled:
|
| 307 |
+
body?.supported?.image_backend_requirements?.['responses-image-generation']?.enabled === true
|
| 308 |
+
};
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
function summarizeRuntimeBody(body) {
|
| 312 |
+
return {
|
| 313 |
+
default_streaming_strategy: body?.streaming?.defaultStrategy,
|
| 314 |
+
streaming_batch_enabled: body?.streamingBatch?.enabled === true,
|
| 315 |
+
recommended_concurrency: body?.streamingBatch?.recommendedConcurrency,
|
| 316 |
+
channel_capacity_per_credential: body?.channelQueue?.capacityPerCredential,
|
| 317 |
+
responses_image_backend_enabled: body?.responsesImageBackend?.enabled === true
|
| 318 |
+
};
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
function findCheck(checks, name) {
|
| 322 |
+
return checks.find((check) => check.name === name) || { ok: false };
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
function readNodeCheck() {
|
| 326 |
+
const match = process.version.match(/^v(\d+)\./);
|
| 327 |
+
const major = match ? Number(match[1]) : 0;
|
| 328 |
+
return { ok: major >= 20, current: process.version };
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
function readPackageJson(cwd) {
|
| 332 |
+
try {
|
| 333 |
+
return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
| 334 |
+
} catch {
|
| 335 |
+
return {};
|
| 336 |
+
}
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
function isCapabilitiesBody(body) {
|
| 340 |
+
return Boolean(body && typeof body === 'object' && body.supported && body.defaults);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
function authHeaders(env) {
|
| 344 |
+
if (env.GPT_IMAGE_AGENT_TOKEN) return { Authorization: `Bearer ${env.GPT_IMAGE_AGENT_TOKEN}` };
|
| 345 |
+
if (env.GPT_IMAGE_APP_PASSWORD_HASH) return { 'X-App-Password-Hash': env.GPT_IMAGE_APP_PASSWORD_HASH };
|
| 346 |
+
return {};
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
function readBaseUrlValidationError(base) {
|
| 350 |
+
return base.error;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
function resolveFirstRunBaseUrl(explicitBaseUrl, env) {
|
| 354 |
+
try {
|
| 355 |
+
return resolvePlaygroundBaseUrl(explicitBaseUrl, env);
|
| 356 |
+
} catch (error) {
|
| 357 |
+
return {
|
| 358 |
+
baseUrl: '',
|
| 359 |
+
source: explicitBaseUrl ? 'user_provided' : env.GPT_IMAGE_PLAYGROUND_URL ? 'GPT_IMAGE_PLAYGROUND_URL' : 'default_local_probe',
|
| 360 |
+
interactive_confirmation_required: explicitBaseUrl ? false : true,
|
| 361 |
+
error: error instanceof Error ? error.message : String(error)
|
| 362 |
+
};
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
export function formatFirstRunText(report) {
|
| 367 |
+
const lines = [];
|
| 368 |
+
lines.push(`首次配置检查:${report.ok ? '通过' : '需要处理'}`);
|
| 369 |
+
if (report.service_base_url) {
|
| 370 |
+
lines.push(`服务地址:${report.service_base_url}(${formatServiceSource(report.service_base_url_source)})`);
|
| 371 |
+
} else {
|
| 372 |
+
lines.push(`服务地址:无效(${readCheckError(report, 'service_base_url_valid') || 'unknown'})`);
|
| 373 |
+
}
|
| 374 |
+
lines.push(`交互式确认:${report.interactive_confirmation_required ? '需要' : '不需要'}`);
|
| 375 |
+
lines.push(`当前进程鉴权:${formatAuthState(report.agent_auth_process)}`);
|
| 376 |
+
lines.push(`私有 Agent env:${formatPrivateEnvState(report.private_agent_env)}`);
|
| 377 |
+
lines.push('');
|
| 378 |
+
lines.push('检查项:');
|
| 379 |
+
for (const check of report.checks) {
|
| 380 |
+
lines.push(`- ${formatCheckLabel(check.name)}:${formatCheckStatus(check)}`);
|
| 381 |
+
}
|
| 382 |
+
lines.push('');
|
| 383 |
+
lines.push('服务摘要:');
|
| 384 |
+
lines.push(`- 能力:${formatEndpoint(report.service?.capabilities)}`);
|
| 385 |
+
lines.push(`- 运行时:${formatEndpoint(report.service?.runtime)}`);
|
| 386 |
+
const capability = report.service?.capabilities || {};
|
| 387 |
+
if (capability.ok) {
|
| 388 |
+
lines.push(`- 鉴权:${capability.auth_required ? capability.auth_schemes.join(',') || '需要' : '不需要'}`);
|
| 389 |
+
lines.push(`- 状态后端:${capability.state_backend || '未知'},图片存储:${capability.image_storage_mode || '未知'}`);
|
| 390 |
+
}
|
| 391 |
+
const runtime = report.service?.runtime || {};
|
| 392 |
+
if (runtime.ok) {
|
| 393 |
+
lines.push(
|
| 394 |
+
`- 并发建议:推荐=${runtime.recommended_concurrency ?? '未知'},每凭证容量=${runtime.channel_capacity_per_credential ?? '未知'}`
|
| 395 |
+
);
|
| 396 |
+
}
|
| 397 |
+
lines.push('');
|
| 398 |
+
lines.push('下一步:');
|
| 399 |
+
if (report.next_actions.length === 0) {
|
| 400 |
+
lines.push('- 无');
|
| 401 |
+
} else {
|
| 402 |
+
for (const action of report.next_actions) lines.push(`- ${action}`);
|
| 403 |
+
}
|
| 404 |
+
return `${lines.join('\n')}\n`;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
function formatCheckStatus(check) {
|
| 408 |
+
if (check.skipped) return '已跳过';
|
| 409 |
+
if (check.ok) return '通过';
|
| 410 |
+
if (check.status) return `失败(${check.status})`;
|
| 411 |
+
if (check.error) return `失败(${check.error})`;
|
| 412 |
+
return '失败';
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
function formatEndpoint(endpoint) {
|
| 416 |
+
if (!endpoint) return '已跳过';
|
| 417 |
+
if (endpoint.ok) return `通过 ${endpoint.status || ''}`.trim();
|
| 418 |
+
return `失败${endpoint.status ? ` ${endpoint.status}` : ''}${endpoint.error ? ` ${endpoint.error}` : ''}`;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
function formatAuthState(auth) {
|
| 422 |
+
if (!auth) return '未知';
|
| 423 |
+
if (auth.has_token) return '已加载 token';
|
| 424 |
+
if (auth.has_password_hash) return '已加载访问码哈希';
|
| 425 |
+
return '未加载';
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
function formatPrivateEnvState(privateEnv) {
|
| 429 |
+
if (!privateEnv?.exists) return '不存在';
|
| 430 |
+
if (privateEnv.has_token) return '存在,含 token';
|
| 431 |
+
if (privateEnv.has_password_hash) return '存在,含访问码哈希';
|
| 432 |
+
return '存在,未发现 Agent 鉴权变量';
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
function readCheckError(report, name) {
|
| 436 |
+
const check = report.checks.find((item) => item.name === name);
|
| 437 |
+
return check?.error;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
function formatServiceSource(source) {
|
| 441 |
+
if (source === 'user_provided') return '用户提供';
|
| 442 |
+
if (source === 'GPT_IMAGE_PLAYGROUND_URL') return '环境变量 GPT_IMAGE_PLAYGROUND_URL';
|
| 443 |
+
if (source === 'default_local_probe') return '默认本地探测';
|
| 444 |
+
return source || '未知来源';
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
function formatCheckLabel(name) {
|
| 448 |
+
const labels = {
|
| 449 |
+
node_version: 'Node.js 版本',
|
| 450 |
+
package_lock: 'package-lock.json',
|
| 451 |
+
dependencies_installed: '依赖是否已安装',
|
| 452 |
+
env_files: '环境文件',
|
| 453 |
+
agent_auth_available_to_process: 'Agent 鉴权可用',
|
| 454 |
+
service_base_url_valid: '服务地址合法',
|
| 455 |
+
service_reachable: '服务可达',
|
| 456 |
+
agent_capabilities_contract: 'Agent capabilities 合同',
|
| 457 |
+
runtime_capabilities_contract: 'runtime capabilities 合同',
|
| 458 |
+
package_metadata: '包信息'
|
| 459 |
+
};
|
| 460 |
+
return labels[name] || name;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
function safeEndpoint(url) {
|
| 464 |
+
try {
|
| 465 |
+
return new URL(url).pathname;
|
| 466 |
+
} catch {
|
| 467 |
+
return String(url);
|
| 468 |
+
}
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
function readOptionValue(argv, index, name) {
|
| 472 |
+
const value = argv[index];
|
| 473 |
+
if (!value || value.startsWith('--')) throw new Error(`${name} 需要参数值。`);
|
| 474 |
+
return value;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
function readPositiveInteger(value, name) {
|
| 478 |
+
if (!/^\d+$/.test(String(value))) throw new Error(`${name} 必须是正整数。`);
|
| 479 |
+
const parsed = Number(value);
|
| 480 |
+
if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${name} 必须是正整数。`);
|
| 481 |
+
return parsed;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
async function main() {
|
| 485 |
+
const options = parseArgs(process.argv.slice(2));
|
| 486 |
+
if (options.help) {
|
| 487 |
+
printHelp();
|
| 488 |
+
return;
|
| 489 |
+
}
|
| 490 |
+
const report = await buildFirstRunReport(options);
|
| 491 |
+
if (options.json) {
|
| 492 |
+
printJson(report);
|
| 493 |
+
} else {
|
| 494 |
+
process.stdout.write(formatFirstRunText(report));
|
| 495 |
+
}
|
| 496 |
+
if (!report.ok) process.exit(1);
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
try {
|
| 500 |
+
if (isMainModule(import.meta.url, process.argv[1])) await main();
|
| 501 |
+
} catch (error) {
|
| 502 |
+
printJson({ ok: false, command: 'first-run', billable: false, error: error instanceof Error ? error.message : String(error) });
|
| 503 |
+
process.exit(1);
|
| 504 |
+
}
|
scripts/status.mjs
CHANGED
|
@@ -28,12 +28,14 @@ const STATUS_ENV_FILES = [
|
|
| 28 |
|
| 29 |
export function buildAdminCommands() {
|
| 30 |
return {
|
|
|
|
| 31 |
doctor: 'npm run doctor',
|
| 32 |
status: 'npm run status',
|
| 33 |
env_summary: 'npm run env:summary',
|
| 34 |
verify: 'npm run verify',
|
| 35 |
deploy_local: 'npm run deploy:local',
|
| 36 |
deploy_space: 'npm run deploy:space',
|
|
|
|
| 37 |
agent_doctor: 'npm run agent:doctor',
|
| 38 |
hf_space_doctor: 'npm run doctor:hf-space',
|
| 39 |
hf_space_smoke: 'npm run smoke:hf-space'
|
|
|
|
| 28 |
|
| 29 |
export function buildAdminCommands() {
|
| 30 |
return {
|
| 31 |
+
first_run: 'npm run first-run',
|
| 32 |
doctor: 'npm run doctor',
|
| 33 |
status: 'npm run status',
|
| 34 |
env_summary: 'npm run env:summary',
|
| 35 |
verify: 'npm run verify',
|
| 36 |
deploy_local: 'npm run deploy:local',
|
| 37 |
deploy_space: 'npm run deploy:space',
|
| 38 |
+
docker_cleanup_fixtures: 'npm run docker:cleanup-fixtures',
|
| 39 |
agent_doctor: 'npm run agent:doctor',
|
| 40 |
hf_space_doctor: 'npm run doctor:hf-space',
|
| 41 |
hf_space_smoke: 'npm run smoke:hf-space'
|
skills/gpt-image-playground-agent/SKILL.md
CHANGED
|
@@ -43,18 +43,21 @@ Agent API 只作为自动化客户端接口,不作为首战场景或用户验
|
|
| 43 |
## 执行流程
|
| 44 |
|
| 45 |
1. 先按任务类型选择内置脚本,不要从零写 API 调用代码。
|
| 46 |
-
2. 定位服务基础地址。
|
| 47 |
-
3.
|
| 48 |
-
4.
|
| 49 |
-
5.
|
| 50 |
-
6.
|
| 51 |
-
7.
|
| 52 |
-
8.
|
| 53 |
-
9.
|
| 54 |
-
10.
|
| 55 |
-
11.
|
| 56 |
-
12.
|
| 57 |
-
13.
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
## 鉴权
|
| 60 |
|
|
@@ -74,7 +77,8 @@ Authorization: Bearer <token>
|
|
| 74 |
- 排查环境配置时不要直接输出 `.env.local`、`.env*.local`、secret 文件或原始 `docker inspect .Config.Env`。Codex 会话日志会持久保存命令输出;优先运行仓库脚本 `npm run env:summary`,或在命令中先把 `API_KEY`、`TOKEN`、`PASSWORD`、`SECRET` 值替换为 `<redacted>`。
|
| 75 |
- Skill 必须保持自包含和可迁移:脚本、示例和说明不得写入本机绝对路径或仓库绝对路径;运行脚本时以当前已安装 Skill 目录为根解析 `scripts/`,不要依赖某台机器上的 checkout 位置。
|
| 76 |
- Skill 必须兼容 Windows、Linux 和 macOS:脚本只用 Node.js 20+、跨平台 `node:` 标准库和 `package.json` 声明依赖;文档示例用 `node "<skill-root>/scripts/..."`,不依赖 bash、sh、chmod、可执行位、POSIX inline env 或反斜杠续行。
|
| 77 |
-
- 不要把 `localhost:4783` 当作唯一部署位置;它只是无明确地址时的探测默认值。
|
|
|
|
| 78 |
- 不要在模型上下文中展开大体积 base64,除非用户明确要求。
|
| 79 |
- 不要把 `error.message` 当成唯一判断依据;稳定分支以 `error.code` 和 HTTP 状态为准。
|
| 80 |
- 不要在没有 `Idempotency-Key` 的情况下调用生成或编辑接口。
|
|
@@ -105,46 +109,59 @@ Authorization: Bearer <token>
|
|
| 105 |
- `scripts/edit-image.mjs`:multipart 编辑调用。默认 dry-run,不消耗额度;必须添加 `--allow-billable` 才会真实编辑。
|
| 106 |
- `scripts/batch-images.mjs`:JSONL 批量 generate/edit 调用。默认 dry-run,不消耗额度;必须添加 `--allow-billable` 才会真实执行,支持 append-only manifest、`--resume`、`--ordered-prefix`、`--dimension-check`、`--max-attempts`、`--concurrency` 和顺序执行下的 `--max-consecutive-failures`。`--concurrency` 默认 `1`,大于 `1` 时并发执行并按输入顺序输出结果。
|
| 107 |
- `scripts/convert-image-format.mjs`:本地 PNG/JPEG/WebP 互转。默认输出 WebP,质量 `100`;JPEG 会把透明背景铺成白色,PNG/WebP 保留透明。
|
| 108 |
-
- `scripts/diagnose-request.mjs`:按一个或多个页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断;支持读取批量 manifest,不触发生图计费。
|
| 109 |
- `scripts/probe-upstream-image.mjs`:直接探测上游图片接口连通性。默认只检查 DNS、TLS 和 `/models`,必须添加 `--allow-billable` 才会真实调用 `/images/generations`。
|
| 110 |
|
| 111 |
-
生成
|
| 112 |
所有生成、编辑、批量和探针脚本在 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 契约。
|
| 113 |
|
| 114 |
如果当前上下文位于仓库根目录,管理员侧优先使用顶层命令:
|
| 115 |
|
|
|
|
| 116 |
- `npm run status`:只读查看 git、Space 目标、Agent API、Skill 入口和独立真实图片上游 smoke 配置摘要;会自动读取 `.env.real-smoke.local`,不输出 URL 或 API Key。
|
| 117 |
- `npm run doctor`:统一诊断本机与 HF Space 配置,不写 Secret。
|
| 118 |
- `npm run verify`:运行提交前基线;需要真实 PostgreSQL gate 时加 `-- --postgres`。
|
| 119 |
- `npm run deploy:local`:重建本地 Docker 服务并探测真实 HTTP 端点;加 `-- --memory` 会断言 memory/indexeddb overlay 生效。
|
| 120 |
- `npm run deploy:space`:部署干净 git HEAD 到固定 Space,并做只读公网验证。
|
| 121 |
-
- `npm run agent:doctor`:执行非计费分层诊断,覆盖 capabilities、Agent contract、runtime backend、state backend 和 Responses/GPT2Image readiness;真实 1K/2K smoke 必须显式加 `-- --allow-billable`。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
生成脚本常用参数:
|
| 124 |
|
| 125 |
```text
|
| 126 |
-
node "<skill-root>/scripts/generate-image.mjs" --size 2048x2048 --quality high --response-mode path --idempotency-key stable-operation-key "a product photo of a ceramic mug"
|
| 127 |
```
|
| 128 |
|
| 129 |
常用 preset 可先 dry-run 展开真实参数,不触发计费:
|
| 130 |
|
| 131 |
```text
|
| 132 |
-
node "<skill-root>/scripts/generate-image.mjs" --preset 1k-smoke-agent "a product photo of a ceramic mug"
|
| 133 |
-
node "<skill-root>/scripts/generate-image.mjs" --preset 4k-agent-nonstream "a cinematic landscape"
|
| 134 |
-
node "<skill-root>/scripts/generate-image.mjs" --preset 4k-page-sse "a cinematic landscape"
|
| 135 |
-
node "<skill-root>/scripts/generate-image.mjs" --preset 4k-upstream-sse-newapi "a cinematic landscape"
|
| 136 |
```
|
| 137 |
|
| 138 |
启用 Agent 内部上游 SSE 时,必须显式传策略字段;脚本仍只输出最终 JSON:
|
| 139 |
|
| 140 |
```text
|
| 141 |
-
node "<skill-root>/scripts/generate-image.mjs" --allow-billable --image-backend images-api --stream-mode auto --streaming-strategy newapi-keepalive-sse --partial-images 2 --size 3840x2160 --quality high "a product photo of a ceramic mug"
|
| 142 |
```
|
| 143 |
|
| 144 |
真实生图必须显式开启:
|
| 145 |
|
| 146 |
```text
|
| 147 |
-
node "<skill-root>/scripts/generate-image.mjs" --allow-billable --timeout-ms 420000 --size 2048x2048 "a product photo of a ceramic mug"
|
| 148 |
```
|
| 149 |
|
| 150 |
本地格式转换不触发生图计费:
|
|
@@ -168,17 +185,17 @@ generate 或页面 SSE 请求包含 `image_backend` 时,`partial_images` 必
|
|
| 168 |
默认 dry-run 只解析 JSONL、生成稳定幂等键并输出计划,不请求服务:
|
| 169 |
|
| 170 |
```text
|
| 171 |
-
node "<skill-root>/scripts/batch-images.mjs" --input tasks.jsonl --ordered-prefix product-set
|
| 172 |
```
|
| 173 |
|
| 174 |
真实批量执行必须显式允许计费。需要并发时添加 `--concurrency N`;需要严格连续失败熔断时保持 `--concurrency 1`:
|
| 175 |
|
| 176 |
```text
|
| 177 |
-
node "<skill-root>/scripts/batch-images.mjs" --allow-billable --input tasks.jsonl --manifest runs/product-set.manifest.jsonl --resume --dimension-check --max-attempts 2 --max-consecutive-failures 3
|
| 178 |
-
node "<skill-root>/scripts/batch-images.mjs" --allow-billable --input tasks.jsonl --manifest runs/product-set.manifest.jsonl --resume --dimension-check --max-attempts 2 --concurrency 3
|
| 179 |
```
|
| 180 |
|
| 181 |
-
`--manifest` 使用 JSONL append-only 记录每条任务的 `index`、`id`、`idempotency_key`、`attempt`、`status`、响应或错误以及机器可读 `summary`;`--resume` 会读取已成功记录并跳过同一 `id` 或 `idempotency_key`。`--dimension-check` 会读取响应里的 `b64_json` 或同 origin `content_url`,校验 PNG/JPEG/WebP 尺寸是否等于任务 `size`。`--max-attempts` 会为第二次及以后尝试追加新的 attempt 级 idempotency key,避免复用终态失败 key;`--concurrency` 大于 `1` 时会先读取运行态并发建议,并发执行任务并按输入顺序输出结果。服务端 `recommendedConcurrency` 或 `channelQueue.capacityPerCredential` 小于请求值时,脚本会把有效并发降到建议值并在输出中写入 `capacity_feedback`;不要再另开多个单张脚本绕过这个限制。`--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`,便于区分上游未给终图和解析/断流问题。
|
| 182 |
|
| 183 |
批量 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 阶段显式拒绝跨模式字段、未知字段和无效路由控制字段。
|
| 184 |
|
|
@@ -193,11 +210,11 @@ node "<skill-root>/scripts/probe-upstream-image.mjs" --base-url https://api.open
|
|
| 193 |
页面请求反馈和日志诊断:
|
| 194 |
|
| 195 |
```text
|
| 196 |
-
node "<skill-root>/scripts/diagnose-request.mjs" --client-request-id stable-operation-key --filename output.png
|
| 197 |
-
node "<skill-root>/scripts/diagnose-request.mjs" --manifest runs/product-set.manifest.jsonl --filename output.png
|
| 198 |
-
node "<skill-root>/scripts/diagnose-request.mjs" --manifest runs/product-set.manifest.jsonl --output runs/diagnosis.json
|
| 199 |
-
node "<skill-root>/scripts/diagnose-request.mjs" --agent-request-id req_abc
|
| 200 |
-
node "<skill-root>/scripts/diagnose-request.mjs" --idempotency-key stable-operation-key
|
| 201 |
```
|
| 202 |
|
| 203 |
调用前按当前系统和 shell 设置 `OPENAI_API_KEY` 或 `GPT_IMAGE_UPSTREAM_API_KEY`,不要把 key 写进命令历史或文档。
|
|
@@ -208,7 +225,7 @@ node "<skill-root>/scripts/diagnose-request.mjs" --idempotency-key stable-operat
|
|
| 208 |
|
| 209 |
脚本读取以下环境变量:
|
| 210 |
|
| 211 |
-
- `GPT_IMAGE_PLAYGROUND_URL`:服务基础地址,可指向本机、局域网、云服务器或域名;脚本未设置时默认尝试 `http://localhost:4783`。
|
| 212 |
- `GPT_IMAGE_AGENT_TOKEN`:Bearer token。
|
| 213 |
- `GPT_IMAGE_APP_PASSWORD_HASH`:使用 `APP_PASSWORD` 访问码部署时,Agent 端点发送为 `X-App-Password-Hash`,页面 SSE 发送为 form-data `passwordHash`。
|
| 214 |
- `GPT_IMAGE_AGENT_IDEMPOTENCY_KEY`:跨脚本进程恢复同一操作时复用的幂等键;也供 `diagnose-request.mjs` 按 Agent 幂等键查询 state。脚本不会自动重试已终态失败的 key。
|
|
@@ -217,6 +234,8 @@ node "<skill-root>/scripts/diagnose-request.mjs" --idempotency-key stable-operat
|
|
| 217 |
- `GPT_IMAGE_AGENT_MAX_ATTEMPTS`:最大尝试次数,默认 `3`。
|
| 218 |
- `GPT_IMAGE_AGENT_CONTRACT_CHECK=1`:只检查 capabilities 和错误契约,不触发真实生图或编辑。
|
| 219 |
|
|
|
|
|
|
|
| 220 |
上游请求头由服务端统一生成。默认 `User-Agent` 是 `gpt-image-playground/<package-version>`;可用 `OPENAI_UPSTREAM_USER_AGENT` 或 `UPSTREAM_USER_AGENT` 覆盖全局 UA,也可用 `OPENAI_CHANNEL_N_USER_AGENT` 和 `OPENAI_CHANNEL_N_UPSTREAM_HEADERS_JSON` 覆盖单渠道安全 header。`Authorization`、`Accept`、`Content-Type`、`Content-Length` 和 `Host` 等协议头不可由 extra headers 覆盖;capabilities、status 和 diagnostics 只暴露 `user_agent_effective`、`has_extra_headers`、`allowed_header_names` 和 `configured_header_names`,不暴露 secret 值。
|
| 221 |
|
| 222 |
`GPT_IMAGE_PLAYGROUND_URL` 必须是无凭据、无查询参数、无片段的 `http`/`https` 绝对 base URL。不要把 token、访问码或其他 Secret 放进 URL。生成脚本轮询 job result 时只会携带鉴权头访问同 origin URL,避免异常服务返回外部 `result_url` 后泄露 Bearer token 或访问码哈希。
|
|
|
|
| 43 |
## 执行流程
|
| 44 |
|
| 45 |
1. 先按任务类型选择内置脚本,不要从零写 API 调用代码。
|
| 46 |
+
2. 定位服务基础地址。用户明确提供 URL 时直接使用该 URL;否则先检查 `GPT_IMAGE_PLAYGROUND_URL`,再探测默认本地地址 `http://localhost:4783`。
|
| 47 |
+
3. 交互式任务中,如果只发现环境变量或本地默认地址,先把发现到的地址、服务可达性和鉴权需求告诉用户,并确认是否使用它;不要把自动发现到的本地服务直接当成用户意图。如果用户随后提供其他服务地址,以用户提供的地址为准。
|
| 48 |
+
4. 非交互式任务无法向用户确认时,按“用户提供 URL > `GPT_IMAGE_PLAYGROUND_URL` > 默认本地探测地址”的顺序执行,并在输出里标明服务地址来源和是否只是自动发现。
|
| 49 |
+
5. 位于仓库根目录且用户是首次配置、换机器、服务地址不确定或 token 不确定时,先运行 `npm run first-run`。该命令只读、非计费、不写 env 文件,默认输出中文摘要;`-- --json` 输出机器可读 JSON。它会报告 `service_base_url_source`、`interactive_confirmation_required`、服务可达性、当前进程是否拿到 Agent 鉴权,以及 `.env.agent.local` 是否存在私有鉴权配置;如果 token 只在私有 env 文件中,先加载到 shell,再运行 skill 脚本。
|
| 50 |
+
6. 让脚本请求 `GET /api/agent/capabilities`。如果所选地址不可达、404、不是 JSON 或不是 Agent capabilities 响应,交互式任务中向用户询问实际部署地址、端口、域名和是否需要鉴权;非交互式任务中显式失败并输出下一步动作。
|
| 51 |
+
7. 读取 capabilities 中的认证方式、模型、模型级限制、`image_transport`、`routing_rules`、Agent 流式边界、页面 SSE 鉴权、后端 runtime enablement、状态后端和端点路径;不要硬编码假设部署方式。
|
| 52 |
+
8. 为每个业务操作生成稳定的 `Idempotency-Key`。网络中断、运行中轮询或非终态重试复用原 key;同一 key 已进入 `failed` 终态后不再用于触发新执行,必须先诊断原因,再创建新的业务操作和新的 key。
|
| 53 |
+
9. 文生图使用 `POST /api/agent/images/generate`,请求体为 JSON。该 Agent 端点对外始终返回最终 `AgentImageResponse` JSON;如 capabilities 声明 `agent_streaming.upstream_sse.supported=true`,可通过 `image_backend`、`stream_mode`、`streaming_strategy`、`partial_images` 控制内部上游 SSE 消费。不要把 `responsesModel`、`thinking`、`promptOptimization` 或 `force_web` 发送到 Agent JSON;这些字段需要页面端 `/api/images` form-data 路径。
|
| 54 |
+
10. 图片编辑若走 Agent edit,使用 `POST /api/agent/images/edit`,请求体为 `multipart/form-data`,源图字段必须使用从 `image_0` 开始的连续字段,最大数量以 capabilities 的 `limits.upload_images.max` 为准;跳号、超过当前 profile 上限、`image_01` 或 `image_foo` 会被显式拒绝。该 Agent 端点同样是非流式端点;上游 SSE 字段按 `agent_streaming.upstream_sse.request_fields_by_mode.edit` 发送,不要给 Agent edit 传 `image_backend`。需要 `image_backend=responses-image-generation` 或页面表单字段 `image_streaming_strategy=responses-sse` 的 edit,一律走页面端 `/api/images` form-data SSE;脚本参数仍写作 `--streaming-strategy responses-sse`。
|
| 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 |
|
| 62 |
## 鉴权
|
| 63 |
|
|
|
|
| 77 |
- 排查环境配置时不要直接输出 `.env.local`、`.env*.local`、secret 文件或原始 `docker inspect .Config.Env`。Codex 会话日志会持久保存命令输出;优先运行仓库脚本 `npm run env:summary`,或在命令中先把 `API_KEY`、`TOKEN`、`PASSWORD`、`SECRET` 值替换为 `<redacted>`。
|
| 78 |
- Skill 必须保持自包含和可迁移:脚本、示例和说明不得写入本机绝对路径或仓库绝对路径;运行脚本时以当前已安装 Skill 目录为根解析 `scripts/`,不要依赖某台机器上的 checkout 位置。
|
| 79 |
- Skill 必须兼容 Windows、Linux 和 macOS:脚本只用 Node.js 20+、跨平台 `node:` 标准库和 `package.json` 声明依赖;文档示例用 `node "<skill-root>/scripts/..."`,不依赖 bash、sh、chmod、可执行位、POSIX inline env 或反斜杠续行。
|
| 80 |
+
- 不要把 `localhost:4783` 当作唯一部署位置;它只是无明确地址时的探测默认值。交互式任务中,探测到本地服务后先请用户确认是否使用;用户提供其他地址时,以用户地址为准。
|
| 81 |
+
- subagent 或自动化任务如果用户指定 Space、云服务或内网服务,调用 `generate-image.mjs`、`edit-image.mjs`、`batch-images.mjs`、`diagnose-request.mjs` 或 `npm run agent:doctor -- --base-url <url>` 时显式传服务地址;不要依赖默认 localhost。
|
| 82 |
- 不要在模型上下文中展开大体积 base64,除非用户明确要求。
|
| 83 |
- 不要把 `error.message` 当成唯一判断依据;稳定分支以 `error.code` 和 HTTP 状态为准。
|
| 84 |
- 不要在没有 `Idempotency-Key` 的情况下调用生成或编辑接口。
|
|
|
|
| 109 |
- `scripts/edit-image.mjs`:multipart 编辑调用。默认 dry-run,不消耗额度;必须添加 `--allow-billable` 才会真实编辑。
|
| 110 |
- `scripts/batch-images.mjs`:JSONL 批量 generate/edit 调用。默认 dry-run,不消耗额度;必须添加 `--allow-billable` 才会真实执行,支持 append-only manifest、`--resume`、`--ordered-prefix`、`--dimension-check`、`--max-attempts`、`--concurrency` 和顺序执行下的 `--max-consecutive-failures`。`--concurrency` 默认 `1`,大于 `1` 时并发执行并按输入顺序输出结果。
|
| 111 |
- `scripts/convert-image-format.mjs`:本地 PNG/JPEG/WebP 互转。默认输出 WebP,质量 `100`;JPEG 会把透明背景铺成白色,PNG/WebP 保留透明。
|
| 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 |
如果当前上下文位于仓库根目录,管理员侧优先使用顶层命令:
|
| 119 |
|
| 120 |
+
- `npm run first-run`:首次配置就绪检查,只读、非计费、不写 env 文件;默认输出中文摘要,加 `-- --json` 输出机器可读 JSON;用于确认 Node、依赖、服务地址、Agent capabilities、当前进程鉴权和下一步动作。
|
| 121 |
- `npm run status`:只读查看 git、Space 目标、Agent API、Skill 入口和独立真实图片上游 smoke 配置摘要;会自动读取 `.env.real-smoke.local`,不输出 URL 或 API Key。
|
| 122 |
- `npm run doctor`:统一诊断本机与 HF Space 配置,不写 Secret。
|
| 123 |
- `npm run verify`:运行提交前基线;需要真实 PostgreSQL gate 时加 `-- --postgres`。
|
| 124 |
- `npm run deploy:local`:重建本地 Docker 服务并探测真实 HTTP 端点;加 `-- --memory` 会断言 memory/indexeddb overlay 生效。
|
| 125 |
- `npm run deploy:space`:部署干净 git HEAD 到固定 Space,并做只读公网验证。
|
| 126 |
+
- `npm run agent:doctor`:执行非计费分层诊断,覆盖 capabilities、Agent contract、runtime backend、state backend 和 Responses/GPT2Image readiness;支持 `-- --base-url <url>`;真实 1K/2K smoke 必须显式加 `-- --allow-billable`。
|
| 127 |
+
|
| 128 |
+
首次配置和诊断输出字段速查:
|
| 129 |
+
|
| 130 |
+
| 字段 | 出现位置 | 判断口径 |
|
| 131 |
+
| --- | --- | --- |
|
| 132 |
+
| `service_base_url` / `verification_scope.service_base_url` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | 当前脚本准备访问的 Playground 服务地址。 |
|
| 133 |
+
| `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` 表示默认本地探测。 |
|
| 134 |
+
| `interactive_confirmation_required` / `verification_scope.interactive_confirmation_required` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | 交互式任务中为 `true` 时,应先向用户确认是否使用该地址再发起真实请求。 |
|
| 135 |
+
| `agent_auth_process.has_token` | `first-run --json` | 当前 shell 是否已经拿到 `GPT_IMAGE_AGENT_TOKEN`。 |
|
| 136 |
+
| `private_agent_env.exists` | `first-run --json` | 本机是否存在 `.env.agent.local` 私有配置;存在不代表当前 shell 已加载。 |
|
| 137 |
+
| `capabilities.ok` | `first-run --json`、`agent:doctor` | 目标地址是否返回 Agent capabilities;失败时先看 HTTP 状态、鉴权提示和服务地址。 |
|
| 138 |
+
| `diagnostics_retention` | `diagnose-request.mjs` | 页面日志诊断的保留窗口;无匹配日志不等于请求一定没发生。 |
|
| 139 |
|
| 140 |
生成脚本常用参数:
|
| 141 |
|
| 142 |
```text
|
| 143 |
+
node "<skill-root>/scripts/generate-image.mjs" --base-url https://your-space.hf.space --size 2048x2048 --quality high --response-mode path --idempotency-key stable-operation-key "a product photo of a ceramic mug"
|
| 144 |
```
|
| 145 |
|
| 146 |
常用 preset 可先 dry-run 展开真实参数,不触发计费:
|
| 147 |
|
| 148 |
```text
|
| 149 |
+
node "<skill-root>/scripts/generate-image.mjs" --base-url https://your-space.hf.space --preset 1k-smoke-agent "a product photo of a ceramic mug"
|
| 150 |
+
node "<skill-root>/scripts/generate-image.mjs" --base-url https://your-space.hf.space --preset 4k-agent-nonstream "a cinematic landscape"
|
| 151 |
+
node "<skill-root>/scripts/generate-image.mjs" --base-url https://your-space.hf.space --preset 4k-page-sse "a cinematic landscape"
|
| 152 |
+
node "<skill-root>/scripts/generate-image.mjs" --base-url https://your-space.hf.space --preset 4k-upstream-sse-newapi "a cinematic landscape"
|
| 153 |
```
|
| 154 |
|
| 155 |
启用 Agent 内部上游 SSE 时,必须显式传策略字段;脚本仍只输出最终 JSON:
|
| 156 |
|
| 157 |
```text
|
| 158 |
+
node "<skill-root>/scripts/generate-image.mjs" --base-url https://your-space.hf.space --allow-billable --image-backend images-api --stream-mode auto --streaming-strategy newapi-keepalive-sse --partial-images 2 --size 3840x2160 --quality high "a product photo of a ceramic mug"
|
| 159 |
```
|
| 160 |
|
| 161 |
真实生图必须显式开启:
|
| 162 |
|
| 163 |
```text
|
| 164 |
+
node "<skill-root>/scripts/generate-image.mjs" --base-url https://your-space.hf.space --allow-billable --timeout-ms 420000 --size 2048x2048 "a product photo of a ceramic mug"
|
| 165 |
```
|
| 166 |
|
| 167 |
本地格式转换不触发生图计费:
|
|
|
|
| 185 |
默认 dry-run 只解析 JSONL、生成稳定幂等键并输出计划,不请求服务:
|
| 186 |
|
| 187 |
```text
|
| 188 |
+
node "<skill-root>/scripts/batch-images.mjs" --base-url https://your-space.hf.space --input tasks.jsonl --ordered-prefix product-set
|
| 189 |
```
|
| 190 |
|
| 191 |
真实批量执行必须显式允许计费。需要并发时添加 `--concurrency N`;需要严格连续失败熔断时保持 `--concurrency 1`:
|
| 192 |
|
| 193 |
```text
|
| 194 |
+
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 --max-consecutive-failures 3
|
| 195 |
+
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
|
| 196 |
```
|
| 197 |
|
| 198 |
+
`--manifest` 使用 JSONL append-only 记录每条任务的 `index`、`id`、`idempotency_key`、`attempt`、`status`、响应或错误以及机器可读 `summary`;`--resume` 会读取已成功记录并跳过同一 `id` 或 `idempotency_key`。dry-run 不写 manifest,输出会声明 `manifest_written=false` 和 `manifest_write_reason=dry_run`。`--dimension-check` 会读取响应里的 `b64_json` 或同 origin `content_url`,校验 PNG/JPEG/WebP 尺寸是否等于任务 `size`。`--max-attempts` 会为第二次及以后尝试追加新的 attempt 级 idempotency key,避免复用终态失败 key;`--concurrency` 大于 `1` 时会先读取运行态并发建议,并发执行任务并按输入顺序输出结果。服务端 `recommendedConcurrency` 或 `channelQueue.capacityPerCredential` 小于请求值时,脚本会把有效并发降到建议值并在输出中写入 `capacity_feedback`;不要再另开多个单张脚本绕过这个限制。`--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`,便于区分上游未给终图和解析/断流问题。
|
| 199 |
|
| 200 |
批量 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 阶段显式拒绝跨模式字段、未知字段和无效路由控制字段。
|
| 201 |
|
|
|
|
| 210 |
页面请求反馈和日志诊断:
|
| 211 |
|
| 212 |
```text
|
| 213 |
+
node "<skill-root>/scripts/diagnose-request.mjs" --base-url https://your-space.hf.space --client-request-id stable-operation-key --filename output.png
|
| 214 |
+
node "<skill-root>/scripts/diagnose-request.mjs" --base-url https://your-space.hf.space --manifest runs/product-set.manifest.jsonl --filename output.png
|
| 215 |
+
node "<skill-root>/scripts/diagnose-request.mjs" --base-url https://your-space.hf.space --manifest runs/product-set.manifest.jsonl --output runs/diagnosis.json
|
| 216 |
+
node "<skill-root>/scripts/diagnose-request.mjs" --base-url https://your-space.hf.space --agent-request-id req_abc
|
| 217 |
+
node "<skill-root>/scripts/diagnose-request.mjs" --base-url https://your-space.hf.space --idempotency-key stable-operation-key
|
| 218 |
```
|
| 219 |
|
| 220 |
调用前按当前系统和 shell 设置 `OPENAI_API_KEY` 或 `GPT_IMAGE_UPSTREAM_API_KEY`,不要把 key 写进命令历史或文档。
|
|
|
|
| 225 |
|
| 226 |
脚本读取以下环境变量:
|
| 227 |
|
| 228 |
+
- `GPT_IMAGE_PLAYGROUND_URL`:服务基础地址,可指向本机、局域网、云服务器或域名;脚本未设置时默认尝试 `http://localhost:4783`。脚本参数 `--base-url` 优先级高于该环境变量。
|
| 229 |
- `GPT_IMAGE_AGENT_TOKEN`:Bearer token。
|
| 230 |
- `GPT_IMAGE_APP_PASSWORD_HASH`:使用 `APP_PASSWORD` 访问码部署时,Agent 端点发送为 `X-App-Password-Hash`,页面 SSE 发送为 form-data `passwordHash`。
|
| 231 |
- `GPT_IMAGE_AGENT_IDEMPOTENCY_KEY`:跨脚本进程恢复同一操作时复用的幂等键;也供 `diagnose-request.mjs` 按 Agent 幂等键查询 state。脚本不会自动重试已终态失败的 key。
|
|
|
|
| 234 |
- `GPT_IMAGE_AGENT_MAX_ATTEMPTS`:最大尝试次数,默认 `3`。
|
| 235 |
- `GPT_IMAGE_AGENT_CONTRACT_CHECK=1`:只检查 capabilities 和错误契约,不触发真实生图或编辑。
|
| 236 |
|
| 237 |
+
Hugging Face Space Secrets 只能写入和列出名称,不能从 CLI 读回 secret 值。远端配置 `AGENT_API_TOKEN` 后,本机 Agent 仍必须通过不入库的 shell 环境、keychain 或本地私有 env 文件注入 `GPT_IMAGE_AGENT_TOKEN`;不要把 token 写进仓库、README、任务 JSONL、manifest、命令参数或日志。仓库根目录的 `.env.agent.local.example` 只作私有本机配置模板,真实 `.env.agent.local` 不入库。
|
| 238 |
+
|
| 239 |
上游请求头由服务端统一生成。默认 `User-Agent` 是 `gpt-image-playground/<package-version>`;可用 `OPENAI_UPSTREAM_USER_AGENT` 或 `UPSTREAM_USER_AGENT` 覆盖全局 UA,也可用 `OPENAI_CHANNEL_N_USER_AGENT` 和 `OPENAI_CHANNEL_N_UPSTREAM_HEADERS_JSON` 覆盖单渠道安全 header。`Authorization`、`Accept`、`Content-Type`、`Content-Length` 和 `Host` 等协议头不可由 extra headers 覆盖;capabilities、status 和 diagnostics 只暴露 `user_agent_effective`、`has_extra_headers`、`allowed_header_names` 和 `configured_header_names`,不暴露 secret 值。
|
| 240 |
|
| 241 |
`GPT_IMAGE_PLAYGROUND_URL` 必须是无凭据、无查询参数、无片段的 `http`/`https` 绝对 base URL。不要把 token、访问码或其他 Secret 放进 URL。生成脚本轮询 job result 时只会携带鉴权头访问同 origin URL,避免异常服务返回外部 `result_url` 后泄露 Bearer token 或访问码哈希。
|
skills/gpt-image-playground-agent/agents/openai.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
interface:
|
| 2 |
display_name: "GPT Image Agent API"
|
| 3 |
-
short_description: "使用内置脚本调用图片 Agent API"
|
| 4 |
-
default_prompt: "使用 $gpt-image-playground-agent 先选择并运行内置脚本,不要临时编写 API 调用脚本。"
|
|
|
|
| 1 |
interface:
|
| 2 |
display_name: "GPT Image Agent API"
|
| 3 |
+
short_description: "使用内置脚本调用图片 Agent API,并确认目标服务地址"
|
| 4 |
+
default_prompt: "使用 $gpt-image-playground-agent 先选择并运行内置脚本;运行前先定位服务地址,用户提供 URL 时显式传 --base-url,自动发现本地或环境变量地址时先确认,不要临时编写 API 调用脚本。"
|
skills/gpt-image-playground-agent/references/api.md
CHANGED
|
@@ -26,13 +26,17 @@ Agent API 是给自动化客户端使用的机器接口,不是自治 Agent 平
|
|
| 26 |
- `scripts/edit-image.mjs`:multipart 编辑调用。
|
| 27 |
- `scripts/batch-images.mjs`:JSONL 批量 generate/edit 调用。
|
| 28 |
- `scripts/convert-image-format.mjs`:本地 PNG/JPEG/WebP 互转。
|
| 29 |
-
- `scripts/diagnose-request.mjs`:按页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断。
|
| 30 |
- `scripts/probe-upstream-image.mjs`:上游图片接口连通性探针。
|
| 31 |
|
| 32 |
-
生成、编辑和批量脚本默认只做 dry-run,不触发真实生图或编辑。必须显式添加 `--allow-billable` 才会按 capabilities 路由规则调用 `/api/agent/images/generate`、`/api/agent/images/edit`、`/api/agent/jobs/images/generate` 或页面端 `/api/images` SSE。
|
| 33 |
上游探针默认只检查 DNS、TLS 和 `/models`,必须显式添加 `--allow-billable` 才会调用上游 `/images/generations`。
|
| 34 |
脚本支持 `GPT_IMAGE_AGENT_CONTRACT_CHECK=1` 或 `--contract-check` 做只读契约检查,不触发真实生图或编辑。
|
|
|
|
|
|
|
| 35 |
Agent 端点鉴权以 capabilities 的 `auth.schemes` 为准。配置 `AGENT_API_TOKEN` 时只接受 Bearer token;只有未配置 `AGENT_API_TOKEN` 且配置了 `APP_PASSWORD` 时,Agent 端点才接受访问码哈希 `GPT_IMAGE_APP_PASSWORD_HASH`。页面端 `/api/images` SSE 另看 `agent_streaming.page_sse.auth`;当其声明 `required=true` 时,form-data 必须包含 `passwordHash`。
|
|
|
|
|
|
|
| 36 |
排查环境配置时不要直接输出 `.env.local`、`.env*.local`、secret 文件或原始 `docker inspect .Config.Env`。Codex 会话日志会持久保存命令输出;优先运行仓库脚本 `npm run env:summary`,或在命令中先把 `API_KEY`、`TOKEN`、`PASSWORD`、`SECRET` 值替换为 `<redacted>`。
|
| 37 |
|
| 38 |
```text
|
|
@@ -119,6 +123,8 @@ npm run env:summary -- --file .env.local --container gpt-image-playground-custom
|
|
| 119 |
- `--dry-run`
|
| 120 |
- `--allow-billable`
|
| 121 |
|
|
|
|
|
|
|
| 122 |
批量 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 阶段显式拒绝跨模式字段、未知字段和无效路由控制字段,避免参数被真实接口忽略。
|
| 123 |
|
| 124 |
Responses edit JSONL 正例:
|
|
@@ -523,8 +529,28 @@ node "<skill-root>/scripts/diagnose-request.mjs" --agent-request-id req_abc
|
|
| 523 |
node "<skill-root>/scripts/diagnose-request.mjs" --idempotency-key stable-operation-key
|
| 524 |
```
|
| 525 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
脚本输出会包含 `diagnostics_retention`。当某个请求的 `matched_log_count=0` 时,该请求会额外包含 `diagnostics_note`,说明无匹配日志的保留窗口边界。
|
| 527 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
单条 Agent state 诊断响应示例:
|
| 529 |
|
| 530 |
```json
|
|
|
|
| 26 |
- `scripts/edit-image.mjs`:multipart 编辑调用。
|
| 27 |
- `scripts/batch-images.mjs`:JSONL 批量 generate/edit 调用。
|
| 28 |
- `scripts/convert-image-format.mjs`:本地 PNG/JPEG/WebP 互转。
|
| 29 |
+
- `scripts/diagnose-request.mjs`:按页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断,支持 `--base-url` 固定目标服务。
|
| 30 |
- `scripts/probe-upstream-image.mjs`:上游图片接口连通性探针。
|
| 31 |
|
| 32 |
+
生成、编辑和批量脚本默认只做 dry-run,不触发真实生图或编辑。dry-run 输出的 `verification_scope.mode=local_planning_only` 表示只完成本地请求构造、参数归一化和静态路由规划;它不会读取远端 capabilities,不会验证远端鉴权、渠道容量或 manifest 写入。必须显式添加 `--allow-billable` 才会按 capabilities 路由规则调用 `/api/agent/images/generate`、`/api/agent/images/edit`、`/api/agent/jobs/images/generate` 或页面端 `/api/images` SSE。
|
| 33 |
上游探针默认只检查 DNS、TLS 和 `/models`,必须显式添加 `--allow-billable` 才会调用上游 `/images/generations`。
|
| 34 |
脚本支持 `GPT_IMAGE_AGENT_CONTRACT_CHECK=1` 或 `--contract-check` 做只读契约检查,不触发真实生图或编辑。
|
| 35 |
+
位于仓库根目录且是首次配置、换机器、服务地址不确定或 token 不确定时,先运行 `npm run first-run`。它只读、非计费、不写 env 文件,默认输出中文摘要,并报告 `service_base_url_source`、`interactive_confirmation_required`、服务可达性、当前进程鉴权和下一步动作。
|
| 36 |
+
自动化消费时使用 `npm run first-run -- --json`。
|
| 37 |
Agent 端点鉴权以 capabilities 的 `auth.schemes` 为准。配置 `AGENT_API_TOKEN` 时只接受 Bearer token;只有未配置 `AGENT_API_TOKEN` 且配置了 `APP_PASSWORD` 时,Agent 端点才接受访问码哈希 `GPT_IMAGE_APP_PASSWORD_HASH`。页面端 `/api/images` SSE 另看 `agent_streaming.page_sse.auth`;当其声明 `required=true` 时,form-data 必须包含 `passwordHash`。
|
| 38 |
+
subagent 或自动化任务如果用户指定 Space、云服务或内网服务,调用 `generate-image.mjs`、`edit-image.mjs`、`batch-images.mjs`、`diagnose-request.mjs` 或 `npm run agent:doctor -- --base-url <url>` 时显式传服务地址;不要依赖默认 localhost。
|
| 39 |
+
Hugging Face Space Secrets 只能写入和列出名称,不能从 CLI 读回 secret 值。远端配置 `AGENT_API_TOKEN` 后,本机 Agent 仍必须通过不入库的 shell 环境、keychain 或本地私有 env 文件注入 `GPT_IMAGE_AGENT_TOKEN`;不要把 token 写进仓库、README、任务 JSONL、manifest、命令参数或日志。仓库根目录的 `.env.agent.local.example` 只作私有本机配置模板,真实 `.env.agent.local` 不入库。
|
| 40 |
排查环境配置时不要直接输出 `.env.local`、`.env*.local`、secret 文件或原始 `docker inspect .Config.Env`。Codex 会话日志会持久保存命令输出;优先运行仓库脚本 `npm run env:summary`,或在命令中先把 `API_KEY`、`TOKEN`、`PASSWORD`、`SECRET` 值替换为 `<redacted>`。
|
| 41 |
|
| 42 |
```text
|
|
|
|
| 123 |
- `--dry-run`
|
| 124 |
- `--allow-billable`
|
| 125 |
|
| 126 |
+
批量 dry-run 不写 manifest,输出会声明 `manifest_written=false` 和 `manifest_write_reason=dry_run`。只有真实执行时 manifest 才作为 append-only 续跑记录写入。
|
| 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 |
|
| 130 |
Responses edit JSONL 正例:
|
|
|
|
| 529 |
node "<skill-root>/scripts/diagnose-request.mjs" --idempotency-key stable-operation-key
|
| 530 |
```
|
| 531 |
|
| 532 |
+
固定服务地址时加 `--base-url`:
|
| 533 |
+
|
| 534 |
+
```text
|
| 535 |
+
node "<skill-root>/scripts/diagnose-request.mjs" --base-url https://your-space.hf.space --idempotency-key stable-operation-key
|
| 536 |
+
```
|
| 537 |
+
|
| 538 |
+
远程 Space、云服务或内网服务必须显式传 `--base-url`,不要让诊断脚本误查默认本地服务。
|
| 539 |
+
|
| 540 |
脚本输出会包含 `diagnostics_retention`。当某个请求的 `matched_log_count=0` 时,该请求会额外包含 `diagnostics_note`,说明无匹配日志的保留窗口边界。
|
| 541 |
|
| 542 |
+
首次配置和诊断输出字段速查:
|
| 543 |
+
|
| 544 |
+
| 字段 | 出现位置 | 判断口径 |
|
| 545 |
+
| --- | --- | --- |
|
| 546 |
+
| `service_base_url` / `verification_scope.service_base_url` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | 当前脚本准备访问的 Playground 服务地址。 |
|
| 547 |
+
| `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` 表示默认本地探测。 |
|
| 548 |
+
| `interactive_confirmation_required` / `verification_scope.interactive_confirmation_required` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | 交互式任务中为 `true` 时,应先向用户确认是否使用该地址再发起真实请求。 |
|
| 549 |
+
| `agent_auth_process.has_token` | `first-run --json` | 当前 shell 是否已经拿到 `GPT_IMAGE_AGENT_TOKEN`。 |
|
| 550 |
+
| `private_agent_env.exists` | `first-run --json` | 本机是否存在 `.env.agent.local` 私有配置;存在不代表当前 shell 已加载。 |
|
| 551 |
+
| `capabilities.ok` | `first-run --json`、`agent:doctor` | 目标地址是否返回 Agent capabilities;失败时先看 HTTP 状态、鉴权提示和服务地址。 |
|
| 552 |
+
| `diagnostics_retention` | `diagnose-request.mjs` | 页面日志诊断的保留窗口;无匹配日志不等于请求一定没发生。 |
|
| 553 |
+
|
| 554 |
单条 Agent state 诊断响应示例:
|
| 555 |
|
| 556 |
```json
|
skills/gpt-image-playground-agent/scripts/batch-images.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import { AGENT_ENDPOINTS } from './lib/agent-api-paths.mjs';
|
|
| 3 |
import {
|
| 4 |
errorMessage,
|
| 5 |
assertValidImageSizeForModel,
|
| 6 |
-
normalizeBaseUrl,
|
| 7 |
normalizeOutputFormat,
|
| 8 |
parseImageSizeValue,
|
| 9 |
readCapabilitiesImageTransportTimeoutMs,
|
|
@@ -11,6 +10,7 @@ import {
|
|
| 11 |
readMaxImageEdge,
|
| 12 |
readOptionValue,
|
| 13 |
readPartialImages,
|
|
|
|
| 14 |
resolveSameOriginUrl,
|
| 15 |
validateAgentEditRequestAgainstCapabilities,
|
| 16 |
validateAgentGenerateRequestAgainstCapabilities
|
|
@@ -118,6 +118,7 @@ if (options.help) {
|
|
| 118 |
}
|
| 119 |
|
| 120 |
let baseUrl;
|
|
|
|
| 121 |
let tasks;
|
| 122 |
let timeoutMs;
|
| 123 |
let capabilities;
|
|
@@ -126,7 +127,8 @@ let runtimeCapabilities;
|
|
| 126 |
let runtimeCapabilitiesPromise;
|
| 127 |
try {
|
| 128 |
if (!options.input) throw new Error('--input 需要 JSONL 文件路径。');
|
| 129 |
-
|
|
|
|
| 130 |
timeoutMs = readConfiguredPositiveInteger(options.timeoutMs, '--timeout-ms', 420000);
|
| 131 |
options.maxAttempts = readConfiguredPositiveInteger(
|
| 132 |
options.maxAttempts ?? DEFAULT_BATCH_MAX_ATTEMPTS,
|
|
@@ -167,8 +169,11 @@ if (!options.allowBillable || options.dryRun) {
|
|
| 167 |
ok: true,
|
| 168 |
billable: false,
|
| 169 |
dry_run: true,
|
|
|
|
| 170 |
input: options.input,
|
| 171 |
manifest: manifestPath,
|
|
|
|
|
|
|
| 172 |
total: planned.length,
|
| 173 |
max_attempts: options.maxAttempts,
|
| 174 |
max_consecutive_failures: options.maxConsecutiveFailures,
|
|
@@ -235,6 +240,7 @@ function parseArgs(argv) {
|
|
| 235 |
maxAttempts: undefined,
|
| 236 |
maxConsecutiveFailures: undefined,
|
| 237 |
concurrency: undefined,
|
|
|
|
| 238 |
allowBillable: false,
|
| 239 |
dryRun: false,
|
| 240 |
resume: false,
|
|
@@ -252,6 +258,7 @@ function parseArgs(argv) {
|
|
| 252 |
else if (arg === '--manifest') parsed.manifest = readOptionValue(argv, (index += 1), arg);
|
| 253 |
else if (arg === '--ordered-prefix') parsed.orderedPrefix = readOptionValue(argv, (index += 1), arg);
|
| 254 |
else if (arg === '--timeout-ms') parsed.timeoutMs = readOptionValue(argv, (index += 1), arg);
|
|
|
|
| 255 |
else if (arg === '--max-attempts') parsed.maxAttempts = readOptionValue(argv, (index += 1), arg);
|
| 256 |
else if (arg === '--max-consecutive-failures') {
|
| 257 |
parsed.maxConsecutiveFailures = readOptionValue(argv, (index += 1), arg);
|
|
@@ -264,6 +271,20 @@ function parseArgs(argv) {
|
|
| 264 |
return parsed;
|
| 265 |
}
|
| 266 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
function readJsonlTasks(filePath) {
|
| 268 |
return fs
|
| 269 |
.readFileSync(filePath, 'utf8')
|
|
@@ -1402,5 +1423,5 @@ function mimeTypeForPath(filePath) {
|
|
| 1402 |
function printUsage() {
|
| 1403 |
console.error('用法:batch-images.mjs --input tasks.jsonl [options]');
|
| 1404 |
console.error('默认只输出 dry-run;添加 --allow-billable 才会按 routing rules 逐行真实请求 Agent API 或页面 SSE。');
|
| 1405 |
-
console.error('常用参数:--manifest --resume --ordered-prefix --dimension-check --max-attempts --max-consecutive-failures --concurrency --timeout-ms --dry-run --allow-billable');
|
| 1406 |
}
|
|
|
|
| 3 |
import {
|
| 4 |
errorMessage,
|
| 5 |
assertValidImageSizeForModel,
|
|
|
|
| 6 |
normalizeOutputFormat,
|
| 7 |
parseImageSizeValue,
|
| 8 |
readCapabilitiesImageTransportTimeoutMs,
|
|
|
|
| 10 |
readMaxImageEdge,
|
| 11 |
readOptionValue,
|
| 12 |
readPartialImages,
|
| 13 |
+
resolvePlaygroundBaseUrl,
|
| 14 |
resolveSameOriginUrl,
|
| 15 |
validateAgentEditRequestAgainstCapabilities,
|
| 16 |
validateAgentGenerateRequestAgainstCapabilities
|
|
|
|
| 118 |
}
|
| 119 |
|
| 120 |
let baseUrl;
|
| 121 |
+
let baseUrlInfo;
|
| 122 |
let tasks;
|
| 123 |
let timeoutMs;
|
| 124 |
let capabilities;
|
|
|
|
| 127 |
let runtimeCapabilitiesPromise;
|
| 128 |
try {
|
| 129 |
if (!options.input) throw new Error('--input 需要 JSONL 文件路径。');
|
| 130 |
+
baseUrlInfo = resolvePlaygroundBaseUrl(options.baseUrl, process.env);
|
| 131 |
+
baseUrl = baseUrlInfo.baseUrl;
|
| 132 |
timeoutMs = readConfiguredPositiveInteger(options.timeoutMs, '--timeout-ms', 420000);
|
| 133 |
options.maxAttempts = readConfiguredPositiveInteger(
|
| 134 |
options.maxAttempts ?? DEFAULT_BATCH_MAX_ATTEMPTS,
|
|
|
|
| 169 |
ok: true,
|
| 170 |
billable: false,
|
| 171 |
dry_run: true,
|
| 172 |
+
verification_scope: buildDryRunVerificationScope(),
|
| 173 |
input: options.input,
|
| 174 |
manifest: manifestPath,
|
| 175 |
+
manifest_written: false,
|
| 176 |
+
manifest_write_reason: 'dry_run',
|
| 177 |
total: planned.length,
|
| 178 |
max_attempts: options.maxAttempts,
|
| 179 |
max_consecutive_failures: options.maxConsecutiveFailures,
|
|
|
|
| 240 |
maxAttempts: undefined,
|
| 241 |
maxConsecutiveFailures: undefined,
|
| 242 |
concurrency: undefined,
|
| 243 |
+
baseUrl: undefined,
|
| 244 |
allowBillable: false,
|
| 245 |
dryRun: false,
|
| 246 |
resume: false,
|
|
|
|
| 258 |
else if (arg === '--manifest') parsed.manifest = readOptionValue(argv, (index += 1), arg);
|
| 259 |
else if (arg === '--ordered-prefix') parsed.orderedPrefix = readOptionValue(argv, (index += 1), arg);
|
| 260 |
else if (arg === '--timeout-ms') parsed.timeoutMs = readOptionValue(argv, (index += 1), arg);
|
| 261 |
+
else if (arg === '--base-url') parsed.baseUrl = readOptionValue(argv, (index += 1), arg);
|
| 262 |
else if (arg === '--max-attempts') parsed.maxAttempts = readOptionValue(argv, (index += 1), arg);
|
| 263 |
else if (arg === '--max-consecutive-failures') {
|
| 264 |
parsed.maxConsecutiveFailures = readOptionValue(argv, (index += 1), arg);
|
|
|
|
| 271 |
return parsed;
|
| 272 |
}
|
| 273 |
|
| 274 |
+
function buildDryRunVerificationScope() {
|
| 275 |
+
return {
|
| 276 |
+
mode: 'local_planning_only',
|
| 277 |
+
service_base_url: baseUrl,
|
| 278 |
+
service_base_url_source: baseUrlInfo.source,
|
| 279 |
+
interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
|
| 280 |
+
remote_capabilities_verified: false,
|
| 281 |
+
runtime_capacity_verified: false,
|
| 282 |
+
auth_verified: false,
|
| 283 |
+
billable_request_sent: false,
|
| 284 |
+
note: 'Dry-run validates JSONL parsing, idempotency keys, request previews and static routing only; run with --allow-billable to verify remote capabilities, capacity and auth.'
|
| 285 |
+
};
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
function readJsonlTasks(filePath) {
|
| 289 |
return fs
|
| 290 |
.readFileSync(filePath, 'utf8')
|
|
|
|
| 1423 |
function printUsage() {
|
| 1424 |
console.error('用法:batch-images.mjs --input tasks.jsonl [options]');
|
| 1425 |
console.error('默认只输出 dry-run;添加 --allow-billable 才会按 routing rules 逐行真实请求 Agent API 或页面 SSE。');
|
| 1426 |
+
console.error('常用参数:--manifest --resume --ordered-prefix --dimension-check --max-attempts --max-consecutive-failures --concurrency --timeout-ms --base-url --dry-run --allow-billable');
|
| 1427 |
}
|
skills/gpt-image-playground-agent/scripts/convert-image-format.mjs
CHANGED
|
@@ -2,7 +2,6 @@
|
|
| 2 |
import { errorMessage, normalizeOutputFormat, readConfiguredPositiveInteger, readOptionValue } from './lib/script-utils.mjs';
|
| 3 |
import fs from 'node:fs/promises';
|
| 4 |
import path from 'node:path';
|
| 5 |
-
import sharp from 'sharp';
|
| 6 |
|
| 7 |
const OUTPUT_FORMATS = new Set(['png', 'jpeg', 'webp']);
|
| 8 |
const DEFAULT_OUTPUT_FORMAT = 'webp';
|
|
@@ -100,6 +99,7 @@ async function buildConversionPlan(parsed) {
|
|
| 100 |
|
| 101 |
async function convertImage(plan) {
|
| 102 |
await fs.mkdir(path.dirname(plan.output.path), { recursive: true });
|
|
|
|
| 103 |
let pipeline = sharp(plan.input.path, { animated: false });
|
| 104 |
if (plan.output.format === 'jpeg') {
|
| 105 |
pipeline = pipeline.flatten({ background: '#ffffff' }).jpeg({ quality: plan.output.quality });
|
|
@@ -122,6 +122,18 @@ async function convertImage(plan) {
|
|
| 122 |
};
|
| 123 |
}
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
async function readInputFileStat(inputPath) {
|
| 126 |
let stat;
|
| 127 |
try {
|
|
|
|
| 2 |
import { errorMessage, normalizeOutputFormat, readConfiguredPositiveInteger, readOptionValue } from './lib/script-utils.mjs';
|
| 3 |
import fs from 'node:fs/promises';
|
| 4 |
import path from 'node:path';
|
|
|
|
| 5 |
|
| 6 |
const OUTPUT_FORMATS = new Set(['png', 'jpeg', 'webp']);
|
| 7 |
const DEFAULT_OUTPUT_FORMAT = 'webp';
|
|
|
|
| 99 |
|
| 100 |
async function convertImage(plan) {
|
| 101 |
await fs.mkdir(path.dirname(plan.output.path), { recursive: true });
|
| 102 |
+
const sharp = await loadSharp();
|
| 103 |
let pipeline = sharp(plan.input.path, { animated: false });
|
| 104 |
if (plan.output.format === 'jpeg') {
|
| 105 |
pipeline = pipeline.flatten({ background: '#ffffff' }).jpeg({ quality: plan.output.quality });
|
|
|
|
| 122 |
};
|
| 123 |
}
|
| 124 |
|
| 125 |
+
async function loadSharp() {
|
| 126 |
+
try {
|
| 127 |
+
const module = await import('sharp');
|
| 128 |
+
return module.default;
|
| 129 |
+
} catch (error) {
|
| 130 |
+
if (error?.code === 'ERR_MODULE_NOT_FOUND') {
|
| 131 |
+
throw new Error('缺少 sharp 依赖,无法执行图片格式转换;请在包含本 skill 的项目中安装依赖后重试。');
|
| 132 |
+
}
|
| 133 |
+
throw error;
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
async function readInputFileStat(inputPath) {
|
| 138 |
let stat;
|
| 139 |
try {
|
skills/gpt-image-playground-agent/scripts/diagnose-request.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
| 3 |
import { dirname } from 'node:path';
|
| 4 |
import { AGENT_ENDPOINTS } from './lib/agent-api-paths.mjs';
|
| 5 |
-
import { errorMessage,
|
| 6 |
|
| 7 |
const MAX_CLIENT_REQUEST_IDS = 50;
|
| 8 |
const token = process.env.GPT_IMAGE_AGENT_TOKEN || '';
|
|
@@ -24,8 +24,10 @@ if (options.help) {
|
|
| 24 |
let baseUrl;
|
| 25 |
let clientRequestIds;
|
| 26 |
let agentLookups;
|
|
|
|
| 27 |
try {
|
| 28 |
-
|
|
|
|
| 29 |
clientRequestIds = readClientRequestIds(options);
|
| 30 |
agentLookups = readAgentLookups(options);
|
| 31 |
if (!clientRequestIds.length && !agentLookups.length) {
|
|
@@ -46,6 +48,9 @@ try {
|
|
| 46 |
const body = {
|
| 47 |
ok: true,
|
| 48 |
billable: false,
|
|
|
|
|
|
|
|
|
|
| 49 |
page_request_count: requests.length,
|
| 50 |
agent_request_count: agentRequests.length,
|
| 51 |
request_count: requests.length,
|
|
@@ -84,6 +89,7 @@ function parseArgs(argv) {
|
|
| 84 |
filenames: [],
|
| 85 |
manifests: [],
|
| 86 |
output: '',
|
|
|
|
| 87 |
help: false
|
| 88 |
};
|
| 89 |
const envClientRequestId = process.env.GPT_IMAGE_AGENT_CLIENT_REQUEST_ID?.trim();
|
|
@@ -95,7 +101,9 @@ function parseArgs(argv) {
|
|
| 95 |
|
| 96 |
for (let index = 0; index < argv.length; index += 1) {
|
| 97 |
const arg = argv[index];
|
| 98 |
-
if (arg === '--
|
|
|
|
|
|
|
| 99 |
parsed.clientRequestIds.push(readOptionValue(argv, (index += 1), arg));
|
| 100 |
} else if (arg === '--agent-request-id') {
|
| 101 |
parsed.agentRequestIds.push(readOptionValue(argv, (index += 1), arg));
|
|
@@ -487,6 +495,6 @@ function writeOutputFile(filepath, text) {
|
|
| 487 |
}
|
| 488 |
|
| 489 |
function printUsage() {
|
| 490 |
-
console.error('用法:diagnose-request.mjs [--client-request-id <id>] [--agent-request-id <id>] [--idempotency-key <key>] [--manifest <jsonl>] [--filename <name>] [--output <json>]');
|
| 491 |
console.error('只读查询页面反馈/日志诊断或 Agent state 请求诊断,不触发生图计费。');
|
| 492 |
}
|
|
|
|
| 2 |
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
| 3 |
import { dirname } from 'node:path';
|
| 4 |
import { AGENT_ENDPOINTS } from './lib/agent-api-paths.mjs';
|
| 5 |
+
import { errorMessage, readOptionValue, resolvePlaygroundBaseUrl } from './lib/script-utils.mjs';
|
| 6 |
|
| 7 |
const MAX_CLIENT_REQUEST_IDS = 50;
|
| 8 |
const token = process.env.GPT_IMAGE_AGENT_TOKEN || '';
|
|
|
|
| 24 |
let baseUrl;
|
| 25 |
let clientRequestIds;
|
| 26 |
let agentLookups;
|
| 27 |
+
let baseUrlInfo;
|
| 28 |
try {
|
| 29 |
+
baseUrlInfo = resolvePlaygroundBaseUrl(options.baseUrl, process.env);
|
| 30 |
+
baseUrl = baseUrlInfo.baseUrl;
|
| 31 |
clientRequestIds = readClientRequestIds(options);
|
| 32 |
agentLookups = readAgentLookups(options);
|
| 33 |
if (!clientRequestIds.length && !agentLookups.length) {
|
|
|
|
| 48 |
const body = {
|
| 49 |
ok: true,
|
| 50 |
billable: false,
|
| 51 |
+
service_base_url: baseUrl,
|
| 52 |
+
service_base_url_source: baseUrlInfo.source,
|
| 53 |
+
interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
|
| 54 |
page_request_count: requests.length,
|
| 55 |
agent_request_count: agentRequests.length,
|
| 56 |
request_count: requests.length,
|
|
|
|
| 89 |
filenames: [],
|
| 90 |
manifests: [],
|
| 91 |
output: '',
|
| 92 |
+
baseUrl: undefined,
|
| 93 |
help: false
|
| 94 |
};
|
| 95 |
const envClientRequestId = process.env.GPT_IMAGE_AGENT_CLIENT_REQUEST_ID?.trim();
|
|
|
|
| 101 |
|
| 102 |
for (let index = 0; index < argv.length; index += 1) {
|
| 103 |
const arg = argv[index];
|
| 104 |
+
if (arg === '--base-url') {
|
| 105 |
+
parsed.baseUrl = readOptionValue(argv, (index += 1), arg);
|
| 106 |
+
} else if (arg === '--client-request-id' || arg === '--request-id') {
|
| 107 |
parsed.clientRequestIds.push(readOptionValue(argv, (index += 1), arg));
|
| 108 |
} else if (arg === '--agent-request-id') {
|
| 109 |
parsed.agentRequestIds.push(readOptionValue(argv, (index += 1), arg));
|
|
|
|
| 495 |
}
|
| 496 |
|
| 497 |
function printUsage() {
|
| 498 |
+
console.error('用法:diagnose-request.mjs [--base-url <url>] [--client-request-id <id>] [--agent-request-id <id>] [--idempotency-key <key>] [--manifest <jsonl>] [--filename <name>] [--output <json>]');
|
| 499 |
console.error('只读查询页面反馈/日志诊断或 Agent state 请求诊断,不触发生图计费。');
|
| 500 |
}
|
skills/gpt-image-playground-agent/scripts/edit-image.mjs
CHANGED
|
@@ -5,7 +5,6 @@ import path from 'node:path';
|
|
| 5 |
import {
|
| 6 |
errorMessage,
|
| 7 |
assertValidImageSizeForModel,
|
| 8 |
-
normalizeBaseUrl,
|
| 9 |
normalizeOutputFormat,
|
| 10 |
parseRetryAfterValue,
|
| 11 |
readCapabilitiesImageTransportTimeoutMs,
|
|
@@ -13,6 +12,7 @@ import {
|
|
| 13 |
readMaxImageEdge,
|
| 14 |
readOptionValue,
|
| 15 |
readPartialImages,
|
|
|
|
| 16 |
sleep,
|
| 17 |
validateAgentEditRequestAgainstCapabilities
|
| 18 |
} from './lib/script-utils.mjs';
|
|
@@ -95,8 +95,10 @@ if ((!imagePath || !prompt) && !contractCheck) {
|
|
| 95 |
}
|
| 96 |
|
| 97 |
let baseUrl;
|
|
|
|
| 98 |
try {
|
| 99 |
-
|
|
|
|
| 100 |
} catch (error) {
|
| 101 |
console.error(errorMessage(error));
|
| 102 |
process.exit(2);
|
|
@@ -110,6 +112,7 @@ if (options.dryRun || (!contractCheck && !options.allowBillable)) {
|
|
| 110 |
ok: true,
|
| 111 |
billable: false,
|
| 112 |
dry_run: true,
|
|
|
|
| 113 |
endpoint: `${baseUrl}${routingGuidance.recommended_endpoint}`,
|
| 114 |
routing_guidance: routingGuidance,
|
| 115 |
idempotency_key: idempotencyKey,
|
|
@@ -179,6 +182,7 @@ function parseArgs(argv) {
|
|
| 179 |
forceWeb: undefined,
|
| 180 |
sseLogPath: undefined,
|
| 181 |
timeoutMs: undefined,
|
|
|
|
| 182 |
idempotencyKey: undefined,
|
| 183 |
imagePath: undefined,
|
| 184 |
imagePathSource: undefined,
|
|
@@ -215,6 +219,7 @@ function parseArgs(argv) {
|
|
| 215 |
else if (arg === '--force-web') parsed.forceWeb = true;
|
| 216 |
else if (arg === '--sse-log') parsed.sseLogPath = readOptionValue(argv, (index += 1), arg);
|
| 217 |
else if (arg === '--timeout-ms') parsed.timeoutMs = readOptionValue(argv, (index += 1), arg);
|
|
|
|
| 218 |
else if (arg === '--idempotency-key') parsed.idempotencyKey = readOptionValue(argv, (index += 1), arg);
|
| 219 |
else if (arg === '--image') {
|
| 220 |
if (parsed.imagePathSource === 'option') throw new Error('--image 只能设置一次。');
|
|
@@ -243,6 +248,20 @@ function absoluteUrl(value) {
|
|
| 243 |
return new URL(value, `${baseUrl}/`).toString();
|
| 244 |
}
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
function buildEditRoutingGuidance(parsed) {
|
| 247 |
if (parsed.routeMode === 'agent') {
|
| 248 |
assertNoPageOnlyEditOptions(parsed, 'Agent edit');
|
|
@@ -448,7 +467,7 @@ async function fetchWithTimeout(url, init) {
|
|
| 448 |
function printUsage() {
|
| 449 |
console.error('用法:edit-image.mjs [options] <image-path> <prompt> 或 edit-image.mjs --image <path> [options] <prompt>');
|
| 450 |
console.error('默认只输出 dry-run;添加 --allow-billable 才会真实编辑图片。');
|
| 451 |
-
console.error('常用参数:--image --model --size --quality --response-mode --format --output-compression --moderation --image-backend --responses-model --thinking --prompt-optimization --force-web --stream-mode --streaming-strategy --partial-images --sse-log --timeout-ms --idempotency-key --page-sse --agent --dry-run --allow-billable');
|
| 452 |
console.error('契约检查:GPT_IMAGE_AGENT_CONTRACT_CHECK=1 edit-image.mjs 或 edit-image.mjs --contract-check');
|
| 453 |
}
|
| 454 |
|
|
|
|
| 5 |
import {
|
| 6 |
errorMessage,
|
| 7 |
assertValidImageSizeForModel,
|
|
|
|
| 8 |
normalizeOutputFormat,
|
| 9 |
parseRetryAfterValue,
|
| 10 |
readCapabilitiesImageTransportTimeoutMs,
|
|
|
|
| 12 |
readMaxImageEdge,
|
| 13 |
readOptionValue,
|
| 14 |
readPartialImages,
|
| 15 |
+
resolvePlaygroundBaseUrl,
|
| 16 |
sleep,
|
| 17 |
validateAgentEditRequestAgainstCapabilities
|
| 18 |
} from './lib/script-utils.mjs';
|
|
|
|
| 95 |
}
|
| 96 |
|
| 97 |
let baseUrl;
|
| 98 |
+
let baseUrlInfo;
|
| 99 |
try {
|
| 100 |
+
baseUrlInfo = resolvePlaygroundBaseUrl(options.baseUrl, process.env);
|
| 101 |
+
baseUrl = baseUrlInfo.baseUrl;
|
| 102 |
} catch (error) {
|
| 103 |
console.error(errorMessage(error));
|
| 104 |
process.exit(2);
|
|
|
|
| 112 |
ok: true,
|
| 113 |
billable: false,
|
| 114 |
dry_run: true,
|
| 115 |
+
verification_scope: buildDryRunVerificationScope(),
|
| 116 |
endpoint: `${baseUrl}${routingGuidance.recommended_endpoint}`,
|
| 117 |
routing_guidance: routingGuidance,
|
| 118 |
idempotency_key: idempotencyKey,
|
|
|
|
| 182 |
forceWeb: undefined,
|
| 183 |
sseLogPath: undefined,
|
| 184 |
timeoutMs: undefined,
|
| 185 |
+
baseUrl: undefined,
|
| 186 |
idempotencyKey: undefined,
|
| 187 |
imagePath: undefined,
|
| 188 |
imagePathSource: undefined,
|
|
|
|
| 219 |
else if (arg === '--force-web') parsed.forceWeb = true;
|
| 220 |
else if (arg === '--sse-log') parsed.sseLogPath = readOptionValue(argv, (index += 1), arg);
|
| 221 |
else if (arg === '--timeout-ms') parsed.timeoutMs = readOptionValue(argv, (index += 1), arg);
|
| 222 |
+
else if (arg === '--base-url') parsed.baseUrl = readOptionValue(argv, (index += 1), arg);
|
| 223 |
else if (arg === '--idempotency-key') parsed.idempotencyKey = readOptionValue(argv, (index += 1), arg);
|
| 224 |
else if (arg === '--image') {
|
| 225 |
if (parsed.imagePathSource === 'option') throw new Error('--image 只能设置一次。');
|
|
|
|
| 248 |
return new URL(value, `${baseUrl}/`).toString();
|
| 249 |
}
|
| 250 |
|
| 251 |
+
function buildDryRunVerificationScope() {
|
| 252 |
+
return {
|
| 253 |
+
mode: 'local_planning_only',
|
| 254 |
+
service_base_url: baseUrl,
|
| 255 |
+
service_base_url_source: baseUrlInfo.source,
|
| 256 |
+
interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
|
| 257 |
+
remote_capabilities_verified: false,
|
| 258 |
+
runtime_capacity_verified: false,
|
| 259 |
+
auth_verified: false,
|
| 260 |
+
billable_request_sent: false,
|
| 261 |
+
note: 'Dry-run validates local request construction and routing guidance only; run --contract-check or --allow-billable to verify the remote service.'
|
| 262 |
+
};
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
function buildEditRoutingGuidance(parsed) {
|
| 266 |
if (parsed.routeMode === 'agent') {
|
| 267 |
assertNoPageOnlyEditOptions(parsed, 'Agent edit');
|
|
|
|
| 467 |
function printUsage() {
|
| 468 |
console.error('用法:edit-image.mjs [options] <image-path> <prompt> 或 edit-image.mjs --image <path> [options] <prompt>');
|
| 469 |
console.error('默认只输出 dry-run;添加 --allow-billable 才会真实编辑图片。');
|
| 470 |
+
console.error('常用参数:--image --model --size --quality --response-mode --format --output-compression --moderation --image-backend --responses-model --thinking --prompt-optimization --force-web --stream-mode --streaming-strategy --partial-images --sse-log --timeout-ms --base-url --idempotency-key --page-sse --agent --dry-run --allow-billable');
|
| 471 |
console.error('契约检查:GPT_IMAGE_AGENT_CONTRACT_CHECK=1 edit-image.mjs 或 edit-image.mjs --contract-check');
|
| 472 |
}
|
| 473 |
|
skills/gpt-image-playground-agent/scripts/generate-image.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import { AGENT_ENDPOINTS, buildAgentJobResultPath } from './lib/agent-api-paths.
|
|
| 3 |
import {
|
| 4 |
errorMessage,
|
| 5 |
assertValidImageSizeForModel,
|
| 6 |
-
normalizeBaseUrl,
|
| 7 |
normalizeOutputFormat,
|
| 8 |
parseRetryAfterValue,
|
| 9 |
readCapabilitiesImageTransportTimeoutMs,
|
|
@@ -11,6 +10,7 @@ import {
|
|
| 11 |
readMaxImageEdge,
|
| 12 |
readOptionValue,
|
| 13 |
readPartialImages,
|
|
|
|
| 14 |
resolveSameOriginUrl,
|
| 15 |
sleep,
|
| 16 |
validateAgentGenerateRequestAgainstCapabilities
|
|
@@ -120,8 +120,10 @@ if (!isNonBillableDryRun(options, contractCheck) && !prompt && !contractCheck) {
|
|
| 120 |
}
|
| 121 |
|
| 122 |
let baseUrl;
|
|
|
|
| 123 |
try {
|
| 124 |
-
|
|
|
|
| 125 |
} catch (error) {
|
| 126 |
console.error(errorMessage(error));
|
| 127 |
process.exit(2);
|
|
@@ -134,6 +136,7 @@ if (isNonBillableDryRun(options, contractCheck)) {
|
|
| 134 |
ok: true,
|
| 135 |
billable: false,
|
| 136 |
dry_run: true,
|
|
|
|
| 137 |
endpoint: dryRunEndpoint(requestBody, options.routeMode),
|
| 138 |
route_mode: options.routeMode,
|
| 139 |
routing_guidance: buildGenerateRoutingGuidance(requestBody, options.routeMode),
|
|
@@ -228,6 +231,7 @@ function parseArgs(argv) {
|
|
| 228 |
partialImages: undefined,
|
| 229 |
sseLogPath: undefined,
|
| 230 |
timeoutMs: undefined,
|
|
|
|
| 231 |
promptFile: undefined,
|
| 232 |
idempotencyKey: undefined,
|
| 233 |
routeMode: 'auto',
|
|
@@ -263,6 +267,7 @@ function parseArgs(argv) {
|
|
| 263 |
else if (arg === '--partial-images') parsed.partialImages = readOptionValue(expandedArgv, (index += 1), arg);
|
| 264 |
else if (arg === '--sse-log') parsed.sseLogPath = readOptionValue(expandedArgv, (index += 1), arg);
|
| 265 |
else if (arg === '--timeout-ms') parsed.timeoutMs = readOptionValue(expandedArgv, (index += 1), arg);
|
|
|
|
| 266 |
else if (arg === '--prompt-file') parsed.promptFile = readOptionValue(expandedArgv, (index += 1), arg);
|
| 267 |
else if (arg === '--idempotency-key') parsed.idempotencyKey = readOptionValue(expandedArgv, (index += 1), arg);
|
| 268 |
else if (arg.startsWith('--')) throw new Error(`未知参数:${arg}`);
|
|
@@ -473,6 +478,20 @@ function enrichImageUrls(result) {
|
|
| 473 |
};
|
| 474 |
}
|
| 475 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
function dryRunEndpoint(body, routeMode) {
|
| 477 |
if (routeMode === 'job') return `${baseUrl}${AGENT_ENDPOINTS.create_generate_job}`;
|
| 478 |
if (routeMode === 'agent') return `${baseUrl}${AGENT_ENDPOINTS.generate}`;
|
|
@@ -1368,7 +1387,7 @@ function printUsage() {
|
|
| 1368 |
console.error('用法:generate-image.mjs [options] <prompt>');
|
| 1369 |
console.error('默认只输出 dry-run;添加 --allow-billable 才会真实生图。');
|
| 1370 |
console.error(
|
| 1371 |
-
'常用参数:--model --size --quality --n --format --output-compression --response-mode --image-backend --responses-model --gpt-model --thinking --prompt-optimization --force-web --stream-mode --streaming-strategy --partial-images --sse-log --timeout-ms --prompt-file --idempotency-key --page-sse --agent --job --no-job(兼容别名)'
|
| 1372 |
);
|
| 1373 |
console.error(
|
| 1374 |
'契约检查:GPT_IMAGE_AGENT_CONTRACT_CHECK=1 generate-image.mjs 或 generate-image.mjs --contract-check'
|
|
|
|
| 3 |
import {
|
| 4 |
errorMessage,
|
| 5 |
assertValidImageSizeForModel,
|
|
|
|
| 6 |
normalizeOutputFormat,
|
| 7 |
parseRetryAfterValue,
|
| 8 |
readCapabilitiesImageTransportTimeoutMs,
|
|
|
|
| 10 |
readMaxImageEdge,
|
| 11 |
readOptionValue,
|
| 12 |
readPartialImages,
|
| 13 |
+
resolvePlaygroundBaseUrl,
|
| 14 |
resolveSameOriginUrl,
|
| 15 |
sleep,
|
| 16 |
validateAgentGenerateRequestAgainstCapabilities
|
|
|
|
| 120 |
}
|
| 121 |
|
| 122 |
let baseUrl;
|
| 123 |
+
let baseUrlInfo;
|
| 124 |
try {
|
| 125 |
+
baseUrlInfo = resolvePlaygroundBaseUrl(options.baseUrl, process.env);
|
| 126 |
+
baseUrl = baseUrlInfo.baseUrl;
|
| 127 |
} catch (error) {
|
| 128 |
console.error(errorMessage(error));
|
| 129 |
process.exit(2);
|
|
|
|
| 136 |
ok: true,
|
| 137 |
billable: false,
|
| 138 |
dry_run: true,
|
| 139 |
+
verification_scope: buildDryRunVerificationScope(),
|
| 140 |
endpoint: dryRunEndpoint(requestBody, options.routeMode),
|
| 141 |
route_mode: options.routeMode,
|
| 142 |
routing_guidance: buildGenerateRoutingGuidance(requestBody, options.routeMode),
|
|
|
|
| 231 |
partialImages: undefined,
|
| 232 |
sseLogPath: undefined,
|
| 233 |
timeoutMs: undefined,
|
| 234 |
+
baseUrl: undefined,
|
| 235 |
promptFile: undefined,
|
| 236 |
idempotencyKey: undefined,
|
| 237 |
routeMode: 'auto',
|
|
|
|
| 267 |
else if (arg === '--partial-images') parsed.partialImages = readOptionValue(expandedArgv, (index += 1), arg);
|
| 268 |
else if (arg === '--sse-log') parsed.sseLogPath = readOptionValue(expandedArgv, (index += 1), arg);
|
| 269 |
else if (arg === '--timeout-ms') parsed.timeoutMs = readOptionValue(expandedArgv, (index += 1), arg);
|
| 270 |
+
else if (arg === '--base-url') parsed.baseUrl = readOptionValue(expandedArgv, (index += 1), arg);
|
| 271 |
else if (arg === '--prompt-file') parsed.promptFile = readOptionValue(expandedArgv, (index += 1), arg);
|
| 272 |
else if (arg === '--idempotency-key') parsed.idempotencyKey = readOptionValue(expandedArgv, (index += 1), arg);
|
| 273 |
else if (arg.startsWith('--')) throw new Error(`未知参数:${arg}`);
|
|
|
|
| 478 |
};
|
| 479 |
}
|
| 480 |
|
| 481 |
+
function buildDryRunVerificationScope() {
|
| 482 |
+
return {
|
| 483 |
+
mode: 'local_planning_only',
|
| 484 |
+
service_base_url: baseUrl,
|
| 485 |
+
service_base_url_source: baseUrlInfo.source,
|
| 486 |
+
interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
|
| 487 |
+
remote_capabilities_verified: false,
|
| 488 |
+
runtime_capacity_verified: false,
|
| 489 |
+
auth_verified: false,
|
| 490 |
+
billable_request_sent: false,
|
| 491 |
+
note: 'Dry-run validates local request construction and routing guidance only; run --contract-check or --allow-billable to verify the remote service.'
|
| 492 |
+
};
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
function dryRunEndpoint(body, routeMode) {
|
| 496 |
if (routeMode === 'job') return `${baseUrl}${AGENT_ENDPOINTS.create_generate_job}`;
|
| 497 |
if (routeMode === 'agent') return `${baseUrl}${AGENT_ENDPOINTS.generate}`;
|
|
|
|
| 1387 |
console.error('用法:generate-image.mjs [options] <prompt>');
|
| 1388 |
console.error('默认只输出 dry-run;添加 --allow-billable 才会真实生图。');
|
| 1389 |
console.error(
|
| 1390 |
+
'常用参数:--model --size --quality --n --format --output-compression --response-mode --image-backend --responses-model --gpt-model --thinking --prompt-optimization --force-web --stream-mode --streaming-strategy --partial-images --sse-log --timeout-ms --base-url --prompt-file --idempotency-key --page-sse --agent --job --no-job(兼容别名)'
|
| 1391 |
);
|
| 1392 |
console.error(
|
| 1393 |
'契约检查:GPT_IMAGE_AGENT_CONTRACT_CHECK=1 generate-image.mjs 或 generate-image.mjs --contract-check'
|
skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs
CHANGED
|
@@ -2,6 +2,7 @@ const MAX_RETRY_AFTER_SECONDS = 60;
|
|
| 2 |
const DIGITS_PATTERN = /^\d+$/;
|
| 3 |
const IMAGE_SIZE_PATTERN = /^(\d+)x(\d+)$/;
|
| 4 |
const LEGACY_IMAGE_SIZES = new Set(['auto', '1024x1024', '1536x1024', '1024x1536']);
|
|
|
|
| 5 |
|
| 6 |
export function readOptionValue(argv, index, name) {
|
| 7 |
const value = argv[index];
|
|
@@ -110,6 +111,28 @@ export function normalizeBaseUrl(value) {
|
|
| 110 |
return normalized;
|
| 111 |
}
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
export function normalizeOutputFormat(value) {
|
| 114 |
return value.toLowerCase() === 'jpg' ? 'jpeg' : value.toLowerCase();
|
| 115 |
}
|
|
|
|
| 2 |
const DIGITS_PATTERN = /^\d+$/;
|
| 3 |
const IMAGE_SIZE_PATTERN = /^(\d+)x(\d+)$/;
|
| 4 |
const LEGACY_IMAGE_SIZES = new Set(['auto', '1024x1024', '1536x1024', '1024x1536']);
|
| 5 |
+
export const DEFAULT_PLAYGROUND_BASE_URL = 'http://localhost:4783';
|
| 6 |
|
| 7 |
export function readOptionValue(argv, index, name) {
|
| 8 |
const value = argv[index];
|
|
|
|
| 111 |
return normalized;
|
| 112 |
}
|
| 113 |
|
| 114 |
+
export function resolvePlaygroundBaseUrl(explicitBaseUrl, env = process.env) {
|
| 115 |
+
if (explicitBaseUrl) {
|
| 116 |
+
return {
|
| 117 |
+
baseUrl: normalizeBaseUrl(explicitBaseUrl),
|
| 118 |
+
source: 'user_provided',
|
| 119 |
+
interactive_confirmation_required: false
|
| 120 |
+
};
|
| 121 |
+
}
|
| 122 |
+
if (env.GPT_IMAGE_PLAYGROUND_URL) {
|
| 123 |
+
return {
|
| 124 |
+
baseUrl: normalizeBaseUrl(env.GPT_IMAGE_PLAYGROUND_URL),
|
| 125 |
+
source: 'GPT_IMAGE_PLAYGROUND_URL',
|
| 126 |
+
interactive_confirmation_required: true
|
| 127 |
+
};
|
| 128 |
+
}
|
| 129 |
+
return {
|
| 130 |
+
baseUrl: DEFAULT_PLAYGROUND_BASE_URL,
|
| 131 |
+
source: 'default_local_probe',
|
| 132 |
+
interactive_confirmation_required: true
|
| 133 |
+
};
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
export function normalizeOutputFormat(value) {
|
| 137 |
return value.toLowerCase() === 'jpg' ? 'jpeg' : value.toLowerCase();
|
| 138 |
}
|