misonL commited on
Commit
d1b58b0
·
verified ·
1 Parent(s): 919485b

Deploy 863ecb4 to Docker Space

Browse files

Source: MisonL/gpt-image-playground-customer@863ecb4beb440a5c59e5a0034a32c0176a0853ba

.env.agent.local.example CHANGED
@@ -1,9 +1,18 @@
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=
 
1
  # Copy to .env.agent.local and keep the real file private.
2
+ # Agent CLI scripts automatically read this file from the current repo root.
3
+ # Shell environment variables still take precedence.
4
+ # Set GPT_IMAGE_AGENT_LOAD_ENV_FILE=0 to disable automatic loading.
 
5
 
6
+ # Use the deployment you intend to test. For Space or other public services,
7
+ # set the remote URL explicitly so subagents do not fall back to localhost.
8
  GPT_IMAGE_PLAYGROUND_URL=http://localhost:4783
9
+
10
+ # Agent JSON, Agent edit, job, artifact and diagnostics endpoints use this
11
+ # when the service is configured with AGENT_API_TOKEN.
12
  GPT_IMAGE_AGENT_TOKEN=
13
+
14
+ # Page SSE uses the page access-code hash as a form field named passwordHash.
15
+ # This is required for --page-sse, large generate defaults, Responses backend
16
+ # edit, and page-SSE batch tasks when the deployment has APP_PASSWORD enabled.
17
+ # GPT_IMAGE_AGENT_TOKEN alone does not satisfy page SSE form auth.
18
  GPT_IMAGE_APP_PASSWORD_HASH=
README.md CHANGED
@@ -35,7 +35,7 @@ npm run first-run -- --json --base-url https://your-space.hf.space
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
 
@@ -194,11 +194,13 @@ AI Agent 集成时优先调用 skill 内置脚本,而不是临时手写 fetch
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
 
@@ -306,8 +308,10 @@ node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
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
 
@@ -320,7 +324,7 @@ node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
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
 
 
35
  1. 运行 `npm install`。
36
  2. 启动服务:本地开发用 `npm run dev`,Docker 用 `docker compose up -d --build --remove-orphans`。
37
  3. 运行 `npm run first-run` 看中文摘要;如果要检查 Space 或内网服务,使用 `npm run first-run -- --base-url <url>`。
38
+ 4. 如果 Agent API 需要鉴权,复制 `.env.agent.local.example` 为 `.env.agent.local`填入本机私有 token。Agent CLI 会从当前仓库根目录自动读取该文件;shell 环境变量仍然优先
39
 
40
  推荐 Docker:
41
 
 
194
 
195
  交互式任务中,Agent 应先定位服务地址:用户明确提供 URL 时直接使用该 URL;否则先检查 `GPT_IMAGE_PLAYGROUND_URL`,再探测默认本地地址 `http://localhost:4783`。如果只发现环境变量或本地服务,先向用户确认是否使用;用户提供其他地址时,以用户提供的地址为准。非交互式任务无法确认时,按同一顺序自动选择,并在输出里说明地址来源。
196
 
197
+ 新环境或不确定服务地址时先运行 `npm run first-run`。它会只读报告 `service_base_url_source`、`interactive_confirmation_required`、服务可达性、当前进程是否拿到 Agent 鉴权,以及 `.env.agent.local` 是否存在私有鉴权配置。Agent CLI 会从当前仓库根目录自动读取 `.env.agent.local`,shell 环境变量仍然优先;如需禁用自动读取设置 `GPT_IMAGE_AGENT_LOAD_ENV_FILE=0`
198
 
199
  dry-run 只做本地请求构造和静态路由规划,不读取远端 capabilities,也不验证远端鉴权、渠道容量或 manifest 写入。脚本输出里的 `verification_scope.mode=local_planning_only` 表示还没有证明远端服务可执行;需要远端合同检查时使用 `--contract-check`,真实执行必须显式添加 `--allow-billable`。
200
 
201
+ subagent 或自动化任务要固定服务地址时,优先给脚本传 `--base-url`,不要只依赖默认 localhost。`generate-image.mjs`、`edit-image.mjs`、`batch-images.mjs`、`diagnose-request.mjs` 和 `npm run agent:doctor -- --base-url <url>` 都支持显式服务地址。首次配置 Agent 鉴权时复制 `.env.agent.local.example` 为 `.env.agent.local`填入本机私有 token;不要把 `.env.agent.local` 提交或粘到任务日志。
202
+
203
+ 公网部署常见有两层鉴权:`GPT_IMAGE_AGENT_TOKEN` 只用于 `/api/agent/*` Bearer 鉴权;页面 SSE `/api/images` 仍可能要求 `GPT_IMAGE_APP_PASSWORD_HASH` 作为 `passwordHash` 表单字段。使用 `--page-sse`、Responses backend edit、大图默认页面 SSE 或批量页面 SSE 前,先用 `npm run first-run -- --base-url <url> --json` 或 `npm run agent:doctor -- --base-url <url>` 检查 `page_sse_auth_available_to_process` / `page_sse_auth_ready`。
204
 
205
  1. 只读检查当前服务能力,不触发计费:
206
 
 
308
  | `service_base_url` / `verification_scope.service_base_url` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | 当前脚本准备访问的 Playground 服务地址。 |
309
  | `service_base_url_source` / `verification_scope.service_base_url_source` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | `user_provided` 表示用户或命令行明确指定;`GPT_IMAGE_PLAYGROUND_URL` 表示来自环境变量;`default_local_probe` 表示默认本地探测。 |
310
  | `interactive_confirmation_required` / `verification_scope.interactive_confirmation_required` | `first-run`、`agent:doctor`、诊断脚本为顶层;skill 脚本 dry-run 在 `verification_scope` 下 | 交互式任务中为 `true` 时,应先向用户确认是否使用该地址再发起真实请求。 |
311
+ | `agent_auth_process.has_token` | `first-run --json` | 当前进程是否已经拿到 `GPT_IMAGE_AGENT_TOKEN`。 |
312
+ | `page_sse_auth_available_to_process` | `first-run --json` | 目标服务要求页面 SSE `passwordHash` 时,当前进程是否已加载 `GPT_IMAGE_APP_PASSWORD_HASH`。 |
313
+ | `summary.page_sse_auth_ready` | `agent:doctor` | 页面 SSE 鉴权是否已满足;为 `false` 时不要运行 `--page-sse` 真实计费请求。 |
314
+ | `private_agent_env.exists` | `first-run --json` | 本机是否存在 `.env.agent.local` 私有配置;Agent CLI 默认从当前仓库根目录读取该文件。 |
315
  | `capabilities.ok` | `first-run --json`、`agent:doctor` | 目标地址是否返回 Agent capabilities;失败时先看 HTTP 状态、鉴权提示和服务地址。 |
316
  | `diagnostics_retention` | `diagnose-request.mjs` | 页面日志诊断的保留窗口;无匹配日志不等于请求一定没发生。 |
317
 
 
324
  | `GPT_IMAGE_APP_PASSWORD_HASH` | 使用页面访问码部署时的访问码哈希;页面 SSE 会作为 `passwordHash` 表单字段发送。 |
325
  | `GPT_IMAGE_AGENT_IDEMPOTENCY_KEY` | 跨脚本进程复用同一业务操作的幂等键。 |
326
 
327
+ Hugging Face Space Secrets 只能写入和列出名称,不能从 CLI 读回 secret 值。远端 Space 配置了 `AGENT_API_TOKEN` 后,本机 Agent 仍需要通过不入库的 shell 环境、keychain 或本地私有 env 文件注入 `GPT_IMAGE_AGENT_TOKEN`;如果 Space 同时配置了 `APP_PASSWORD`,页面 SSE 还需要 `GPT_IMAGE_APP_PASSWORD_HASH`。Agent CLI 默认读取当前仓库根目录的 `.env.agent.local`,shell 环境变量优先。不要把 token、访问码或哈希写进 README、任务 JSONL、manifest 或命令日志。仓库提供 `.env.agent.local.example` 作为私有本机配置模板。
328
 
329
  接口边界:
330
 
scripts/agent-doctor.mjs CHANGED
@@ -3,7 +3,10 @@
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));
@@ -60,6 +63,7 @@ agent:doctor 默认只读、非计费。真实 generate/edit smoke 必须显式
60
  }
61
 
62
  async function main() {
 
63
  const options = parseArgs(process.argv.slice(2));
64
  if (options.help) {
65
  printHelp();
@@ -262,6 +266,15 @@ function buildLayers({ capabilities, runtime, contract, smoke }) {
262
  function summarizeCapabilities(body) {
263
  return {
264
  page_sse: body?.agent_streaming?.page_sse?.supported === true,
 
 
 
 
 
 
 
 
 
265
  agent_jobs: body?.agent_jobs?.supported === true,
266
  routing_rules: Boolean(body?.routing_rules),
267
  executable_routing_rules: Boolean(body?.routing_rules?.high_resolution_edit?.conditions)
@@ -304,6 +317,10 @@ function buildSummary({ capabilities, runtime, contract, smoke }) {
304
  contract_check: contract.ok ? 'ok' : 'failed',
305
  runtime: runtime.ok ? 'ok' : 'failed',
306
  state_backend: capabilities.ok ? capabilities.body?.defaults?.state_backend : 'unknown',
 
 
 
 
307
  responses_gpt2image_ready:
308
  capabilities.ok && runtime.ok
309
  ? capabilities.body?.supported?.image_backend_requirements?.['responses-image-generation']?.enabled === true &&
 
3
  import { fileURLToPath } from 'node:url';
4
 
5
  import { isMainModule, parseJsonPayload, pickFailureOutput, printJson, redactBaseUrl, runCommand } from './command-center-utils.mjs';
6
+ import {
7
+ loadPrivateAgentEnvFile,
8
+ resolvePlaygroundBaseUrl
9
+ } from '../skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs';
10
 
11
  const GENERATE_SCRIPT = fileURLToPath(new URL('../skills/gpt-image-playground-agent/scripts/generate-image.mjs', import.meta.url));
12
  const EDIT_SCRIPT = fileURLToPath(new URL('../skills/gpt-image-playground-agent/scripts/edit-image.mjs', import.meta.url));
 
63
  }
64
 
65
  async function main() {
66
+ loadPrivateAgentEnvFile();
67
  const options = parseArgs(process.argv.slice(2));
68
  if (options.help) {
69
  printHelp();
 
266
  function summarizeCapabilities(body) {
267
  return {
268
  page_sse: body?.agent_streaming?.page_sse?.supported === true,
269
+ page_sse_auth_required: body?.agent_streaming?.page_sse?.auth?.required === true,
270
+ page_sse_auth_ready:
271
+ body?.agent_streaming?.page_sse?.auth?.required === true
272
+ ? Boolean(process.env.GPT_IMAGE_APP_PASSWORD_HASH)
273
+ : true,
274
+ page_sse_auth_next_action:
275
+ body?.agent_streaming?.page_sse?.auth?.required === true && !process.env.GPT_IMAGE_APP_PASSWORD_HASH
276
+ ? 'Set GPT_IMAGE_APP_PASSWORD_HASH in private local env before using --page-sse.'
277
+ : undefined,
278
  agent_jobs: body?.agent_jobs?.supported === true,
279
  routing_rules: Boolean(body?.routing_rules),
280
  executable_routing_rules: Boolean(body?.routing_rules?.high_resolution_edit?.conditions)
 
317
  contract_check: contract.ok ? 'ok' : 'failed',
318
  runtime: runtime.ok ? 'ok' : 'failed',
319
  state_backend: capabilities.ok ? capabilities.body?.defaults?.state_backend : 'unknown',
320
+ page_sse_auth_ready:
321
+ capabilities.ok && capabilities.body?.agent_streaming?.page_sse?.auth?.required === true
322
+ ? Boolean(process.env.GPT_IMAGE_APP_PASSWORD_HASH)
323
+ : capabilities.ok,
324
  responses_gpt2image_ready:
325
  capabilities.ok && runtime.ok
326
  ? capabilities.body?.supported?.image_backend_requirements?.['responses-image-generation']?.enabled === true &&
scripts/agent-skill-scripts.test.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
  import { AGENT_ENDPOINTS } from '../src/lib/agent-api-paths.mjs';
7
  import assert from 'node:assert/strict';
8
  import { spawn, spawnSync } from 'node:child_process';
9
- import { cpSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
10
  import { createServer } from 'node:http';
11
  import { tmpdir } from 'node:os';
12
  import { join } from 'node:path';
@@ -126,6 +126,144 @@ describe('Agent skill script argument validation', () => {
126
  assert.equal(result.stdout.trim(), '');
127
  });
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  it('inherits longer image transport timeout from capabilities by default', () => {
130
  assert.equal(
131
  readCapabilitiesImageTransportTimeoutMs(
@@ -918,6 +1056,28 @@ describe('Agent skill script argument validation', () => {
918
  const body = JSON.parse(result.stderr);
919
  assert.equal(body.error.code, 'page_sse_auth_required');
920
  assert.match(body.error.message, /GPT_IMAGE_APP_PASSWORD_HASH/);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
921
  assert.deepEqual(
922
  requests.map((item) => `${item.method} ${item.url}`),
923
  ['GET /api/agent/capabilities']
@@ -1457,7 +1617,8 @@ describe('Agent skill script argument validation', () => {
1457
  content_url: '/api/agent/artifacts/artifact-off/content',
1458
  metadata_url: '/api/agent/artifacts/artifact-off'
1459
  }
1460
- ]
 
1461
  })
1462
  );
1463
  return;
@@ -1496,7 +1657,13 @@ describe('Agent skill script argument validation', () => {
1496
  assert.equal(body.summary.idempotency_key.startsWith('agent-generate-'), true);
1497
  assert.equal(body.summary.transport, 'agent_json');
1498
  assert.equal(body.summary.endpoint, '/api/agent/images/generate');
 
 
 
1499
  assert.equal(typeof body.summary.elapsed_ms, 'number');
 
 
 
1500
  assert.equal(body.summary.next_action, 'done');
1501
  assert.deepEqual(
1502
  requests.map((item) => `${item.method} ${item.url}`),
@@ -1620,11 +1787,22 @@ describe('Agent skill script argument validation', () => {
1620
  const body = JSON.parse(result.stdout);
1621
  assert.equal(body.images[0].filename, 'small-page.png');
1622
  assert.equal(body.images[0].absolute_path, `${baseUrl}/api/image/small-page.png`);
1623
- assert.deepEqual(body.routing, { transport: 'page_sse', endpoint: '/api/images' });
 
 
 
 
 
 
 
 
1624
  assert.equal(body.summary.ok, true);
1625
  assert.equal(body.summary.billable, true);
1626
  assert.equal(body.summary.transport, 'page_sse');
1627
  assert.equal(body.summary.endpoint, '/api/images');
 
 
 
1628
  assert.equal(typeof body.summary.elapsed_ms, 'number');
1629
  assert.deepEqual(
1630
  requests.map((item) => `${item.method} ${item.url}`),
@@ -2721,6 +2899,26 @@ describe('Agent skill script argument validation', () => {
2721
  const copiedSkillRoot = join(tempRoot, 'gpt-image-playground-agent');
2722
  try {
2723
  cpSync(skillRoot, copiedSkillRoot, { recursive: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2724
  const result = spawnSync(
2725
  process.execPath,
2726
  [join(copiedSkillRoot, 'scripts/generate-image.mjs'), '--help'],
@@ -5443,14 +5641,34 @@ describe('Agent skill script argument validation', () => {
5443
  });
5444
  });
5445
 
5446
- function runSkillScript(filename, args, env = {}) {
 
 
5447
  return spawnSync(process.execPath, [join(skillScriptsRoot, filename), ...args], {
5448
- cwd: repoRoot,
5449
  encoding: 'utf8',
5450
- env: { ...process.env, ...env }
5451
  });
5452
  }
5453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5454
  function listTextFiles(root) {
5455
  const result = [];
5456
  for (const name of readdirSync(root)) {
 
6
  import { AGENT_ENDPOINTS } from '../src/lib/agent-api-paths.mjs';
7
  import assert from 'node:assert/strict';
8
  import { spawn, spawnSync } from 'node:child_process';
9
+ import { cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
10
  import { createServer } from 'node:http';
11
  import { tmpdir } from 'node:os';
12
  import { join } from 'node:path';
 
126
  assert.equal(result.stdout.trim(), '');
127
  });
128
 
129
+ it('loads private agent env files for CLI scripts without overriding shell env', () => {
130
+ const tempRoot = mkdtempSync(join(tmpdir(), 'agent-env-load-'));
131
+ try {
132
+ mkdirSync(join(tempRoot, '.git'));
133
+ writeFileSync(
134
+ join(tempRoot, '.env.agent.local'),
135
+ [
136
+ 'GPT_IMAGE_PLAYGROUND_URL=https://file-space.example.test',
137
+ 'GPT_IMAGE_AGENT_MAX_ATTEMPTS=2',
138
+ 'GPT_IMAGE_AGENT_TOKEN=file-token',
139
+ 'GPT_IMAGE_APP_PASSWORD_HASH=file-hash',
140
+ 'GPT_IMAGE_UPSTREAM_BASE_URL=https://upstream.example.test/v1',
141
+ 'GPT_IMAGE_UPSTREAM_API_KEY=upstream-secret'
142
+ ].join('\n')
143
+ );
144
+
145
+ const loaded = runSkillScript('generate-image.mjs', ['prompt'], {}, { cwd: tempRoot, loadPrivateAgentEnv: true });
146
+ assert.equal(loaded.status, 0);
147
+ const loadedBody = JSON.parse(loaded.stdout);
148
+ assert.equal(loadedBody.verification_scope.service_base_url, 'https://file-space.example.test');
149
+ assert.equal(loadedBody.verification_scope.service_base_url_source, 'GPT_IMAGE_PLAYGROUND_URL');
150
+ assert.equal(loadedBody.verification_scope.interactive_confirmation_required, true);
151
+ assert.doesNotMatch(loaded.stdout, /file-token|file-hash|upstream-secret/);
152
+ assert.equal(loaded.stderr.trim(), '');
153
+
154
+ const fromNestedCwd = runSkillScript(
155
+ 'generate-image.mjs',
156
+ ['prompt'],
157
+ {},
158
+ { cwd: join(tempRoot, 'nested', 'scripts'), loadPrivateAgentEnv: true, createCwd: true }
159
+ );
160
+ assert.equal(fromNestedCwd.status, 0);
161
+ const nestedBody = JSON.parse(fromNestedCwd.stdout);
162
+ assert.equal(nestedBody.verification_scope.service_base_url, 'https://file-space.example.test');
163
+
164
+ const parentRoot = mkdtempSync(join(tmpdir(), 'agent-env-parent-'));
165
+ try {
166
+ writeFileSync(
167
+ join(parentRoot, '.env.agent.local'),
168
+ 'GPT_IMAGE_PLAYGROUND_URL=https://parent-space.example.test'
169
+ );
170
+ const childRoot = join(parentRoot, 'child-repo');
171
+ mkdirSync(join(childRoot, '.git'), { recursive: true });
172
+ const bounded = runSkillScript(
173
+ 'generate-image.mjs',
174
+ ['prompt'],
175
+ {},
176
+ { cwd: join(childRoot, 'nested'), loadPrivateAgentEnv: true, createCwd: true }
177
+ );
178
+ assert.equal(bounded.status, 0);
179
+ const boundedBody = JSON.parse(bounded.stdout);
180
+ assert.equal(boundedBody.verification_scope.service_base_url, 'http://localhost:4783');
181
+
182
+ const projectCopyRoot = join(parentRoot, 'project-copy');
183
+ mkdirSync(join(projectCopyRoot, 'skills/gpt-image-playground-agent'), { recursive: true });
184
+ mkdirSync(join(projectCopyRoot, 'nested'), { recursive: true });
185
+ writeFileSync(join(projectCopyRoot, 'package.json'), JSON.stringify({ name: 'gpt-image-playground' }));
186
+ writeFileSync(join(projectCopyRoot, 'skills/gpt-image-playground-agent/SKILL.md'), '# skill\n');
187
+ const projectBounded = runSkillScript(
188
+ 'generate-image.mjs',
189
+ ['prompt'],
190
+ {},
191
+ { cwd: join(projectCopyRoot, 'nested'), loadPrivateAgentEnv: true }
192
+ );
193
+ assert.equal(projectBounded.status, 0);
194
+ const projectBoundedBody = JSON.parse(projectBounded.stdout);
195
+ assert.equal(projectBoundedBody.verification_scope.service_base_url, 'http://localhost:4783');
196
+
197
+ writeFileSync(
198
+ join(projectCopyRoot, '.env.agent.local'),
199
+ 'GPT_IMAGE_PLAYGROUND_URL=https://project-copy.example.test'
200
+ );
201
+ const projectLoaded = runSkillScript(
202
+ 'generate-image.mjs',
203
+ ['prompt'],
204
+ {},
205
+ { cwd: join(projectCopyRoot, 'nested'), loadPrivateAgentEnv: true }
206
+ );
207
+ assert.equal(projectLoaded.status, 0);
208
+ const projectLoadedBody = JSON.parse(projectLoaded.stdout);
209
+ assert.equal(projectLoadedBody.verification_scope.service_base_url, 'https://project-copy.example.test');
210
+
211
+ const projectSkillScriptsLoaded = runSkillScript(
212
+ 'generate-image.mjs',
213
+ ['prompt'],
214
+ {},
215
+ {
216
+ cwd: join(projectCopyRoot, 'skills/gpt-image-playground-agent/scripts'),
217
+ loadPrivateAgentEnv: true,
218
+ createCwd: true
219
+ }
220
+ );
221
+ assert.equal(projectSkillScriptsLoaded.status, 0);
222
+ const projectSkillScriptsLoadedBody = JSON.parse(projectSkillScriptsLoaded.stdout);
223
+ assert.equal(
224
+ projectSkillScriptsLoadedBody.verification_scope.service_base_url,
225
+ 'https://project-copy.example.test'
226
+ );
227
+ } finally {
228
+ rmSync(parentRoot, { recursive: true, force: true });
229
+ }
230
+
231
+ const shellWins = runSkillScript(
232
+ 'generate-image.mjs',
233
+ ['prompt'],
234
+ { GPT_IMAGE_PLAYGROUND_URL: 'https://shell-space.example.test' },
235
+ { cwd: tempRoot, loadPrivateAgentEnv: true }
236
+ );
237
+ assert.equal(shellWins.status, 0);
238
+ const shellBody = JSON.parse(shellWins.stdout);
239
+ assert.equal(shellBody.verification_scope.service_base_url, 'https://shell-space.example.test');
240
+
241
+ const disabled = runSkillScript(
242
+ 'generate-image.mjs',
243
+ ['prompt'],
244
+ { GPT_IMAGE_AGENT_LOAD_ENV_FILE: '0' },
245
+ { cwd: tempRoot }
246
+ );
247
+ assert.equal(disabled.status, 0);
248
+ const disabledBody = JSON.parse(disabled.stdout);
249
+ assert.equal(disabledBody.verification_scope.service_base_url, 'http://localhost:4783');
250
+
251
+ const probe = runSkillScript(
252
+ 'probe-upstream-image.mjs',
253
+ ['--timeout-ms', '1'],
254
+ {},
255
+ { cwd: tempRoot, loadPrivateAgentEnv: true }
256
+ );
257
+ assert.notEqual(probe.status, 2);
258
+ const probeBody = JSON.parse(probe.stdout);
259
+ assert.equal(probeBody.base_url, 'https://upstream.example.test/v1');
260
+ assert.equal(probeBody.api_key_configured, true);
261
+ assert.doesNotMatch(probe.stdout, /upstream-secret/);
262
+ } finally {
263
+ rmSync(tempRoot, { recursive: true, force: true });
264
+ }
265
+ });
266
+
267
  it('inherits longer image transport timeout from capabilities by default', () => {
268
  assert.equal(
269
  readCapabilitiesImageTransportTimeoutMs(
 
1056
  const body = JSON.parse(result.stderr);
1057
  assert.equal(body.error.code, 'page_sse_auth_required');
1058
  assert.match(body.error.message, /GPT_IMAGE_APP_PASSWORD_HASH/);
1059
+ assert.match(body.error.message, /\.env\.agent\.local/);
1060
+ assert.match(body.error.message, /GPT_IMAGE_AGENT_TOKEN/);
1061
+ assert.equal(body.summary.ok, false);
1062
+ assert.equal(body.summary.billable, false);
1063
+ assert.equal(body.summary.request_id, null);
1064
+ assert.equal(body.summary.idempotency_key.startsWith('agent-generate-'), true);
1065
+ assert.deepEqual(body.summary.artifact_ids, []);
1066
+ assert.deepEqual(body.summary.content_urls, []);
1067
+ assert.deepEqual(body.summary.absolute_content_urls, []);
1068
+ assert.equal(body.summary.route_mode, 'page_sse');
1069
+ assert.equal(body.summary.image_backend, null);
1070
+ assert.equal(body.summary.stream_mode, null);
1071
+ assert.equal(body.summary.streaming_strategy, null);
1072
+ assert.equal(body.summary.selected_channel_id, null);
1073
+ assert.equal(body.summary.upstream_host, null);
1074
+ assert.equal(body.summary.transport_error_kind, null);
1075
+ assert.equal(body.summary.retry_after_ms, null);
1076
+ assert.equal(body.summary.retry_after_seconds, null);
1077
+ assert.equal(body.summary.cooldown_until, null);
1078
+ assert.equal(body.summary.cooldown_target, null);
1079
+ assert.equal(body.summary.elapsed_source, 'client_script');
1080
+ assert.equal(typeof body.summary.elapsed_breakdown.client_script_ms, 'number');
1081
  assert.deepEqual(
1082
  requests.map((item) => `${item.method} ${item.url}`),
1083
  ['GET /api/agent/capabilities']
 
1617
  content_url: '/api/agent/artifacts/artifact-off/content',
1618
  metadata_url: '/api/agent/artifacts/artifact-off'
1619
  }
1620
+ ],
1621
+ timing: { server_elapsed_ms: 4321 }
1622
  })
1623
  );
1624
  return;
 
1657
  assert.equal(body.summary.idempotency_key.startsWith('agent-generate-'), true);
1658
  assert.equal(body.summary.transport, 'agent_json');
1659
  assert.equal(body.summary.endpoint, '/api/agent/images/generate');
1660
+ assert.equal(body.summary.route_mode, 'agent');
1661
+ assert.deepEqual(body.summary.content_urls, ['/api/agent/artifacts/artifact-off/content']);
1662
+ assert.deepEqual(body.summary.absolute_content_urls, [`${baseUrl}/api/agent/artifacts/artifact-off/content`]);
1663
  assert.equal(typeof body.summary.elapsed_ms, 'number');
1664
+ assert.equal(body.summary.elapsed_source, 'client_script');
1665
+ assert.equal(body.summary.server_elapsed_ms, 4321);
1666
+ assert.equal(body.summary.elapsed_breakdown.upstream_or_server_ms, 4321);
1667
  assert.equal(body.summary.next_action, 'done');
1668
  assert.deepEqual(
1669
  requests.map((item) => `${item.method} ${item.url}`),
 
1787
  const body = JSON.parse(result.stdout);
1788
  assert.equal(body.images[0].filename, 'small-page.png');
1789
  assert.equal(body.images[0].absolute_path, `${baseUrl}/api/image/small-page.png`);
1790
+ assert.deepEqual(body.routing, {
1791
+ transport: 'page_sse',
1792
+ endpoint: '/api/images',
1793
+ route_mode: 'page_sse',
1794
+ fallback_endpoint: '/api/agent/images/generate',
1795
+ fallback_mode: 'manual_after_diagnosis',
1796
+ stream_mode: null,
1797
+ streaming_strategy: null
1798
+ });
1799
  assert.equal(body.summary.ok, true);
1800
  assert.equal(body.summary.billable, true);
1801
  assert.equal(body.summary.transport, 'page_sse');
1802
  assert.equal(body.summary.endpoint, '/api/images');
1803
+ assert.equal(body.summary.route_mode, 'page_sse');
1804
+ assert.deepEqual(body.summary.content_urls, ['/api/image/small-page.png']);
1805
+ assert.deepEqual(body.summary.absolute_content_urls, [`${baseUrl}/api/image/small-page.png`]);
1806
  assert.equal(typeof body.summary.elapsed_ms, 'number');
1807
  assert.deepEqual(
1808
  requests.map((item) => `${item.method} ${item.url}`),
 
2899
  const copiedSkillRoot = join(tempRoot, 'gpt-image-playground-agent');
2900
  try {
2901
  cpSync(skillRoot, copiedSkillRoot, { recursive: true });
2902
+ writeFileSync(
2903
+ join(tempRoot, '.env.agent.local'),
2904
+ 'GPT_IMAGE_PLAYGROUND_URL=https://parent-space.example.test'
2905
+ );
2906
+ const standaloneRun = spawnSync(
2907
+ process.execPath,
2908
+ [join(copiedSkillRoot, 'scripts/generate-image.mjs'), 'prompt'],
2909
+ {
2910
+ cwd: join(copiedSkillRoot, 'scripts'),
2911
+ encoding: 'utf8',
2912
+ env: {
2913
+ ...buildIsolatedSkillScriptEnv({ loadPrivateAgentEnv: true }),
2914
+ GPT_IMAGE_AGENT_LOAD_ENV_FILE: '1'
2915
+ }
2916
+ }
2917
+ );
2918
+ assert.equal(standaloneRun.status, 0);
2919
+ const standaloneBody = JSON.parse(standaloneRun.stdout);
2920
+ assert.equal(standaloneBody.verification_scope.service_base_url, 'http://localhost:4783');
2921
+
2922
  const result = spawnSync(
2923
  process.execPath,
2924
  [join(copiedSkillRoot, 'scripts/generate-image.mjs'), '--help'],
 
5641
  });
5642
  });
5643
 
5644
+ function runSkillScript(filename, args, env = {}, options = {}) {
5645
+ const baseEnv = buildIsolatedSkillScriptEnv({ loadPrivateAgentEnv: options.loadPrivateAgentEnv });
5646
+ if (options.createCwd) mkdirSync(options.cwd, { recursive: true });
5647
  return spawnSync(process.execPath, [join(skillScriptsRoot, filename), ...args], {
5648
+ cwd: options.cwd || repoRoot,
5649
  encoding: 'utf8',
5650
+ env: { ...baseEnv, ...env }
5651
  });
5652
  }
5653
 
5654
+ function buildIsolatedSkillScriptEnv(options = {}) {
5655
+ const keepNames = [
5656
+ 'HOME',
5657
+ 'PATH',
5658
+ 'SystemRoot',
5659
+ 'TEMP',
5660
+ 'TMP',
5661
+ 'TMPDIR',
5662
+ 'USERPROFILE'
5663
+ ];
5664
+ const isolated = {};
5665
+ for (const name of keepNames) {
5666
+ if (process.env[name] !== undefined) isolated[name] = process.env[name];
5667
+ }
5668
+ if (!options.loadPrivateAgentEnv) isolated.GPT_IMAGE_AGENT_LOAD_ENV_FILE = '0';
5669
+ return isolated;
5670
+ }
5671
+
5672
  function listTextFiles(root) {
5673
  const result = [];
5674
  for (const name of readdirSync(root)) {
scripts/command-center.test.mjs CHANGED
@@ -447,6 +447,15 @@ describe('Command center scripts', () => {
447
  JSON.stringify({
448
  auth: { schemes: ['bearer'] },
449
  defaults: { state_backend: 'memory' },
 
 
 
 
 
 
 
 
 
450
  supported: { image_backend_requirements: {} }
451
  })
452
  );
@@ -489,6 +498,15 @@ describe('Command center scripts', () => {
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,
@@ -496,6 +514,8 @@ describe('Command center scripts', () => {
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
  }
@@ -504,7 +524,10 @@ describe('Command center scripts', () => {
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);
@@ -608,7 +631,10 @@ describe('Command center scripts', () => {
608
  defaults: { state_backend: 'memory' },
609
  storage: { image_storage_mode: 'indexeddb', postgres_configured: false },
610
  agent_streaming: {
611
- page_sse: { supported: true }
 
 
 
612
  },
613
  agent_jobs: { supported: true },
614
  routing_rules: {
@@ -691,10 +717,17 @@ describe('Command center scripts', () => {
691
  assert.equal(body.summary.capabilities, 'ok');
692
  assert.equal(body.summary.runtime, 'ok');
693
  assert.equal(body.summary.state_backend, 'memory');
 
694
  assert.equal(body.summary.responses_gpt2image_ready, true);
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);
@@ -942,7 +975,7 @@ function runNodeCommandAsync(args, options = {}) {
942
  return new Promise((resolve) => {
943
  const child = spawn(process.execPath, args, {
944
  cwd: process.cwd(),
945
- env: options.env,
946
  stdio: ['ignore', 'pipe', 'pipe']
947
  });
948
  let stdout = '';
 
447
  JSON.stringify({
448
  auth: { schemes: ['bearer'] },
449
  defaults: { state_backend: 'memory' },
450
+ agent_streaming: {
451
+ page_sse: {
452
+ supported: true,
453
+ auth: {
454
+ required: true,
455
+ form_field: 'passwordHash'
456
+ }
457
+ }
458
+ },
459
  supported: { image_backend_requirements: {} }
460
  })
461
  );
 
498
  });
499
  assert.equal(report.service.ok, true);
500
  assert.equal(report.checks.find((check) => check.name === 'agent_auth_available_to_process').ok, true);
501
+ assert.equal(report.checks.find((check) => check.name === 'page_sse_auth_available_to_process').ok, false);
502
+ assert.equal(report.checks.find((check) => check.name === 'page_sse_auth_available_to_process').skipped, false);
503
+ assert.equal(
504
+ report.checks.find((check) => check.name === 'page_sse_auth_available_to_process')
505
+ .auth_in_private_env_file,
506
+ false
507
+ );
508
+ assert.equal(report.service.capabilities.page_sse_auth_required, true);
509
+ assert.equal(report.service.capabilities.page_sse_auth_form_field, 'passwordHash');
510
  assert.equal(
511
  report.checks.find((check) => check.name === 'agent_auth_available_to_process')
512
  .auth_in_private_env_file,
 
514
  );
515
  assert.doesNotMatch(JSON.stringify(report), /shell-secret|file-secret/);
516
  assert.match(JSON.stringify(report.next_actions), /agent:doctor/);
517
+ assert.match(JSON.stringify(report.next_actions), /GPT_IMAGE_APP_PASSWORD_HASH/);
518
+ assert.match(JSON.stringify(report.next_actions), /GPT_IMAGE_AGENT_TOKEN/);
519
  } finally {
520
  await rm(tempDir, { recursive: true, force: true });
521
  }
 
524
  });
525
 
526
  it('formats first-run as human-readable text by default', async () => {
527
+ const report = await buildFirstRunReport(
528
+ { cwd: process.cwd(), baseUrl: 'not a url', envFiles: [] },
529
+ { GPT_IMAGE_AGENT_LOAD_ENV_FILE: '0' }
530
+ );
531
  const text = formatFirstRunText(report);
532
 
533
  assert.match(text, /^首次配置检查:需要处理/m);
 
631
  defaults: { state_backend: 'memory' },
632
  storage: { image_storage_mode: 'indexeddb', postgres_configured: false },
633
  agent_streaming: {
634
+ page_sse: {
635
+ supported: true,
636
+ auth: { required: true, form_field: 'passwordHash' }
637
+ }
638
  },
639
  agent_jobs: { supported: true },
640
  routing_rules: {
 
717
  assert.equal(body.summary.capabilities, 'ok');
718
  assert.equal(body.summary.runtime, 'ok');
719
  assert.equal(body.summary.state_backend, 'memory');
720
+ assert.equal(body.summary.page_sse_auth_ready, false);
721
  assert.equal(body.summary.responses_gpt2image_ready, true);
722
  assert.equal(body.summary.billable_smoke, 'skipped');
723
  assert.equal(body.layers.find((layer) => layer.name === 'billable_smoke').skipped, true);
724
  assert.equal(body.layers.find((layer) => layer.name === 'capabilities').executable_routing_rules, true);
725
+ assert.equal(body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_required, true);
726
+ assert.equal(body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_ready, false);
727
+ assert.match(
728
+ body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_next_action,
729
+ /GPT_IMAGE_APP_PASSWORD_HASH/
730
+ );
731
  assert.equal(body.service_base_url, prefixedBaseUrl);
732
  assert.equal(body.service_base_url_source, 'GPT_IMAGE_PLAYGROUND_URL');
733
  assert.equal(body.interactive_confirmation_required, true);
 
975
  return new Promise((resolve) => {
976
  const child = spawn(process.execPath, args, {
977
  cwd: process.cwd(),
978
+ env: { GPT_IMAGE_AGENT_LOAD_ENV_FILE: '0', ...options.env },
979
  stdio: ['ignore', 'pipe', 'pipe']
980
  });
981
  let stdout = '';
scripts/first-run.mjs CHANGED
@@ -5,7 +5,10 @@ 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'];
@@ -45,6 +48,7 @@ first-run 只读、非计费,不写 env 文件或 secret。`);
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);
@@ -159,6 +163,14 @@ function buildChecks({ cwd, env, envSummary, service, base, validationError }) {
159
  skipped: !requiresAgentAuth(service),
160
  auth_in_private_env_file: fileToken
161
  },
 
 
 
 
 
 
 
 
162
  {
163
  name: 'service_base_url_valid',
164
  ok: !validationError,
@@ -229,6 +241,11 @@ function buildNextActions({ checks, base, service, env, envSummary, validationEr
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
  }
@@ -236,7 +253,7 @@ function buildNextActions({ checks, base, service, env, envSummary, validationEr
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 合同检查。');
@@ -250,6 +267,10 @@ function requiresAgentAuth(service) {
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
  }
@@ -299,6 +320,8 @@ 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,
@@ -386,6 +409,9 @@ export function formatFirstRunText(report) {
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 || {};
@@ -451,6 +477,7 @@ function formatCheckLabel(name) {
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 合同',
 
5
 
6
  import { isMainModule, printJson, redactBaseUrl } from './command-center-utils.mjs';
7
  import { summarizeEnvFile } from './env-summary.mjs';
8
+ import {
9
+ loadPrivateAgentEnvFile,
10
+ resolvePlaygroundBaseUrl
11
+ } from '../skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs';
12
 
13
  const DEFAULT_TIMEOUT_MS = 3000;
14
  const DEFAULT_ENV_FILES = ['.env.local', '.env.agent.local'];
 
48
  export async function buildFirstRunReport(options = {}, env = process.env) {
49
  const cwd = options.cwd || process.cwd();
50
  const envFiles = options.envFiles || DEFAULT_ENV_FILES;
51
+ loadPrivateAgentEnvFile({ cwd, env });
52
  const base = resolveFirstRunBaseUrl(options.baseUrl, env);
53
  const envSummary = envFiles.map((filePath) => summarizeEnvFile(join(cwd, filePath)));
54
  const validationError = readBaseUrlValidationError(base);
 
163
  skipped: !requiresAgentAuth(service),
164
  auth_in_private_env_file: fileToken
165
  },
166
+ {
167
+ name: 'page_sse_auth_available_to_process',
168
+ ok: Boolean(env.GPT_IMAGE_APP_PASSWORD_HASH),
169
+ skipped: !requiresPageSsePasswordHash(service),
170
+ auth_in_private_env_file: envSummary.some((source) =>
171
+ sourceHasSetVariable(source, 'GPT_IMAGE_APP_PASSWORD_HASH')
172
+ )
173
+ },
174
  {
175
  name: 'service_base_url_valid',
176
  ok: !validationError,
 
241
  '在仓库外导出 GPT_IMAGE_AGENT_TOKEN 或 GPT_IMAGE_APP_PASSWORD_HASH,再运行受保护的 Agent 脚本。'
242
  );
243
  }
244
+ if (requiresPageSsePasswordHash(service) && !env.GPT_IMAGE_APP_PASSWORD_HASH) {
245
+ actions.push(
246
+ '如果要使用页面 SSE、Responses backend edit 或 --page-sse,请在本机私有 .env.agent.local 中设置 GPT_IMAGE_APP_PASSWORD_HASH;Agent 脚本会自动读取该文件,GPT_IMAGE_AGENT_TOKEN 只覆盖 Agent JSON 鉴权。'
247
+ );
248
+ }
249
  if (!service.ok && !requiresAgentAuth(service)) {
250
  actions.push('先用 npm run dev 或 docker compose up -d --build --remove-orphans 启动服务,再重新运行 npm run first-run。');
251
  }
 
253
  actions.push('在交互式 Agent 任务里,先和用户确认探测到的服务地址,再发真实请求。');
254
  }
255
  if (!hasCurrentAuth && hasFileAuth) {
256
+ actions.push('Agent 脚本会自动读取 .env.agent.local;如果仍提示缺少鉴权,请确认文件位于当前仓库根目录且变量名正确。');
257
  }
258
  if (service.ok && hasCurrentAuth) {
259
  actions.push('运行 npm run agent:doctor 做完整的非计费 Agent 合同检查。');
 
267
  return Array.isArray(schemes) && schemes.length > 0;
268
  }
269
 
270
+ function requiresPageSsePasswordHash(service) {
271
+ return service.capabilities?.body?.agent_streaming?.page_sse?.auth?.required === true;
272
+ }
273
+
274
  function sourceHasSetVariable(source, name) {
275
  return source.exists && source.variables.some((item) => item.name === name && item.set === true);
276
  }
 
320
  return {
321
  auth_required: body?.auth?.required === true,
322
  auth_schemes: Array.isArray(body?.auth?.schemes) ? body.auth.schemes : [],
323
+ page_sse_auth_required: body?.agent_streaming?.page_sse?.auth?.required === true,
324
+ page_sse_auth_form_field: body?.agent_streaming?.page_sse?.auth?.form_field,
325
  state_backend: body?.defaults?.state_backend,
326
  image_storage_mode: body?.storage?.image_storage_mode,
327
  agent_jobs_supported: body?.agent_jobs?.supported === true,
 
409
  const capability = report.service?.capabilities || {};
410
  if (capability.ok) {
411
  lines.push(`- 鉴权:${capability.auth_required ? capability.auth_schemes.join(',') || '需要' : '不需要'}`);
412
+ lines.push(
413
+ `- 页面 SSE 鉴权:${capability.page_sse_auth_required ? `需要 ${capability.page_sse_auth_form_field || 'passwordHash'}` : '不需要'}`
414
+ );
415
  lines.push(`- 状态后端:${capability.state_backend || '未知'},图片存储:${capability.image_storage_mode || '未知'}`);
416
  }
417
  const runtime = report.service?.runtime || {};
 
477
  dependencies_installed: '依赖是否已安装',
478
  env_files: '环境文件',
479
  agent_auth_available_to_process: 'Agent 鉴权可用',
480
+ page_sse_auth_available_to_process: '页面 SSE 鉴权可用',
481
  service_base_url_valid: '服务地址合法',
482
  service_reachable: '服务可达',
483
  agent_capabilities_contract: 'Agent capabilities 合同',
skills/gpt-image-playground-agent/SKILL.md CHANGED
@@ -20,7 +20,7 @@ Agent API 是给自动化客户端使用的机器接口,不是自治 Agent 平
20
  - 不要临时编写 Node/Python/shell 脚本、curl 命令或手写 fetch/FormData 来重复实现这些脚本已经覆盖的 API 调用。
21
  - 只有在内置脚本缺少用户明确需要的能力时,才修改或扩展 `scripts/` 内的预置脚本,并同步补测试;不要在仓库外留下 ad hoc 调用脚本。
22
  - 先用 dry-run 或 `--contract-check` 检查请求、路由和鉴权;只有用户明确允许真实计费时才加 `--allow-billable`。
23
- - 真实调用成功或失败后,优先读取脚本输出的 `summary`。它是面向 Agent 的机器摘要,包含 `billable`、请求 ID、幂等键、产物 URL、耗时、路由、渠道、上游 host、脱敏请求头、重试和下一步动作;不要再先手查 SQLite、Docker logs 或上游后台。
24
 
25
  ## 产品边界
26
 
@@ -46,7 +46,7 @@ Agent 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。
@@ -69,7 +69,7 @@ Authorization: Bearer <token>
69
 
70
  此时 Agent 端点只接受 Bearer token,不会回退到访问码哈希。如果未配置 `AGENT_API_TOKEN` 但配置了页面访问码 `APP_PASSWORD`,Agent 端点发送 `X-App-Password-Hash`。下载或删除产物时必须复用 capabilities 声明的同一 Agent 鉴权方式。
71
 
72
- 页面端 `/api/images` SSE 是独立页面契约,读取 `agent_streaming.page_sse.auth`。当该字段声明 `required=true` 时,必须在 form-data 中发送 `passwordHash`;脚本侧对应环境变量是 `GPT_IMAGE_APP_PASSWORD_HASH`。即使 `auth.schemes` 只返回 `bearer`,混合配置下 page SSE 仍可能需要这个表单访问码哈希。页面 SSE 还会把同一业务 key 写入 form-data `clientRequestId`,长度不得超过 `agent_streaming.page_sse.client_request_id.max_length`。
73
 
74
  ## 调用约束
75
 
@@ -132,8 +132,10 @@ Authorization: Bearer <token>
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
 
@@ -227,14 +229,14 @@ node "<skill-root>/scripts/diagnose-request.mjs" --base-url https://your-space.h
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。
232
  - `GPT_IMAGE_AGENT_CLIENT_REQUEST_ID`:供 `diagnose-request.mjs` 读取的页面请求 ID;页面 SSE 路径通常等于脚本使用的 `Idempotency-Key`,多个 ID 可重复传 `--client-request-id`。
233
  - `GPT_IMAGE_AGENT_REQUEST_ID`:供 `diagnose-request.mjs` 读取 Agent state 请求诊断的 Agent `request_id`。
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
 
 
20
  - 不要临时编写 Node/Python/shell 脚本、curl 命令或手写 fetch/FormData 来重复实现这些脚本已经覆盖的 API 调用。
21
  - 只有在内置脚本缺少用户明确需要的能力时,才修改或扩展 `scripts/` 内的预置脚本,并同步补测试;不要在仓库外留下 ad hoc 调用脚本。
22
  - 先用 dry-run 或 `--contract-check` 检查请求、路由和鉴权;只有用户明确允许真实计费时才加 `--allow-billable`。
23
+ - 真实调用成功或失败后,优先读取脚本输出的 `summary`。它是面向 Agent 的机器摘要,包含 `billable`、请求 ID、幂等键、产物 URL、耗时、耗时拆分、路由、渠道、上游 host、脱敏请求头、重试和下一步动作;不要再先手查 SQLite、Docker logs 或上游后台。
24
 
25
  ## 产品边界
26
 
 
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 鉴权、页面 SSE 鉴权是否可用,以及 `.env.agent.local` 是否存在私有鉴权配置。Agent CLI 默认从当前仓库根目录自动读取 `.env.agent.local`,shell 环境变量优先;如需禁用自动读取设置 `GPT_IMAGE_AGENT_LOAD_ENV_FILE=0`
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。
 
69
 
70
  此时 Agent 端点只接受 Bearer token,不会回退到访问码哈希。如果未配置 `AGENT_API_TOKEN` 但配置了页面访问码 `APP_PASSWORD`,Agent 端点发送 `X-App-Password-Hash`。下载或删除产物时必须复用 capabilities 声明的同一 Agent 鉴权方式。
71
 
72
+ 页面端 `/api/images` SSE 是独立页面契约,读取 `agent_streaming.page_sse.auth`。当该字段声明 `required=true` 时,必须在 form-data 中发送 `passwordHash`;脚本侧对应环境变量是 `GPT_IMAGE_APP_PASSWORD_HASH`。即使 `auth.schemes` 只返回 `bearer`,混合配置下 page SSE 仍可能需要这个表单访问码哈希;`GPT_IMAGE_AGENT_TOKEN` 不能替代页面 SSE 表单鉴权。页面 SSE 还会把同一业务 key 写入 form-data `clientRequestId`,长度不得超过 `agent_streaming.page_sse.client_request_id.max_length`。
73
 
74
  ## 调用约束
75
 
 
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` | 当前进程是否已经拿到 `GPT_IMAGE_AGENT_TOKEN`。 |
136
+ | `page_sse_auth_available_to_process` | `first-run --json` | 目标服务要求页面 SSE `passwordHash` 时,当前进程是否已加载 `GPT_IMAGE_APP_PASSWORD_HASH`。 |
137
+ | `summary.page_sse_auth_ready` | `agent:doctor` | 页面 SSE 鉴权是否已满足;为 `false` 时不要运行 `--page-sse` 真实计费请求。 |
138
+ | `private_agent_env.exists` | `first-run --json` | 本机是否存在 `.env.agent.local` 私有配置;Agent CLI 默认从当前仓库根目录读取该文件。 |
139
  | `capabilities.ok` | `first-run --json`、`agent:doctor` | 目标地址是否返回 Agent capabilities;失败时先看 HTTP 状态、鉴权提示和服务地址。 |
140
  | `diagnostics_retention` | `diagnose-request.mjs` | 页面日志诊断的保留窗口;无匹配日志不等于请求一定没发生。 |
141
 
 
229
 
230
  - `GPT_IMAGE_PLAYGROUND_URL`:服务基础地址,可指向本机、局域网、云服务器或域名;脚本未设置时默认尝试 `http://localhost:4783`。脚本参数 `--base-url` 优先级高于该环境变量。
231
  - `GPT_IMAGE_AGENT_TOKEN`:Bearer token。
232
+ - `GPT_IMAGE_APP_PASSWORD_HASH`:使用 `APP_PASSWORD` 访问码部署时,Agent 端点发送为 `X-App-Password-Hash`,页面 SSE 发送为 form-data `passwordHash`。公网 Space 同时配置 `AGENT_API_TOKEN` 和 `APP_PASSWORD` 时,Agent JSON 需要 `GPT_IMAGE_AGENT_TOKEN`,页面 SSE 仍需要这个哈希。
233
  - `GPT_IMAGE_AGENT_IDEMPOTENCY_KEY`:跨脚本进程恢复同一操作时复用的幂等键;也供 `diagnose-request.mjs` 按 Agent 幂等键查询 state。脚本不会自动重试已终态失败的 key。
234
  - `GPT_IMAGE_AGENT_CLIENT_REQUEST_ID`:供 `diagnose-request.mjs` 读取的页面请求 ID;页面 SSE 路径通常等于脚本使用的 `Idempotency-Key`,多个 ID 可重复传 `--client-request-id`。
235
  - `GPT_IMAGE_AGENT_REQUEST_ID`:供 `diagnose-request.mjs` 读取 Agent state 请求诊断的 Agent `request_id`。
236
  - `GPT_IMAGE_AGENT_MAX_ATTEMPTS`:最大尝试次数,默认 `3`。
237
  - `GPT_IMAGE_AGENT_CONTRACT_CHECK=1`:只检查 capabilities 和错误契约,不触发真实生图或编辑。
238
 
239
+ Hugging Face Space Secrets 只能写入和列出名称,不能从 CLI 读回 secret 值。远端配置 `AGENT_API_TOKEN` 后,本机 Agent 仍必须通过不入库的 shell 环境、keychain 或本地私有 env 文件注入 `GPT_IMAGE_AGENT_TOKEN`;Agent CLI 默认读取当前仓库根目录的 `.env.agent.local`,shell 环境变量优先。不要把 token 写进仓库、README、任务 JSONL、manifest、命令参数或日志。仓库根目录的 `.env.agent.local.example` 只作私有本机配置模板,真实 `.env.agent.local` 不入库。
240
 
241
  上游请求���由服务端统一生成。默认 `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 值。
242
 
skills/gpt-image-playground-agent/references/api.md CHANGED
@@ -32,11 +32,11 @@ Agent API 是给自动化客户端使用的机器接口,不是自治 Agent 平
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
@@ -47,7 +47,7 @@ npm run env:summary -- --file .env.local --container gpt-image-playground-custom
47
  同一个 `Idempotency-Key` 如果已经进入终态 `failed`,再次调用 generate/edit 或 job result/status 只会回放该失败,且 `retryable=false`。需要重新尝试时应创建新的业务操作和新的 `Idempotency-Key`。
48
  页面端 `/api/images` SSE 会把同一个业务 key 复用到 `clientRequestId`,因此脚本使用的 `Idempotency-Key` 不能超过 capabilities 中 `agent_streaming.page_sse.client_request_id.max_length` 声明的字符数;超长时会直接报错,不会静默截断。
49
  脚本会在 dry-run 和真实请求前前置校验 `--size` 或 JSONL `size`。`gpt-image-2` 支持 `auto` 或任意正整数 `WIDTHxHEIGHT`;默认 OpenAI-compatible 上游的更严格尺寸边界由服务端 profile 或真实上游显式报错。非 `gpt-image-2` 模型只接受 `auto`、`1024x1024`、`1536x1024` 或 `1024x1536`。生成、页面编辑、批量和上游探针默认请求 `output_format=webp`、`output_compression=100`。
50
- 真实执行输出会包含机器可读 `summary`。成功摘要包含 `ok`、`billable`、`request_id`、`idempotency_key`、`artifact_ids`、`content_urls`、`cached`、`started_at`、`completed_at`、`elapsed_ms`、`server_elapsed_ms`、`transport`、`endpoint`、`route_mode`、`image_backend`、`stream_mode`、`streaming_strategy`、`selected_channel_id`、`upstream_host`、脱敏 `request_headers` 和 `next_action`。失败摘要包含 `transport_error_kind`、`retry_after_ms`、`cooldown_until`、`cooldown_target`、`retryable` 和 `next_action`。回答耗时问题时优先读取 `summary.elapsed_ms`;服务端返回 timing 时读取 `summary.server_elapsed_ms`。
51
 
52
  生成脚本参数:
53
 
@@ -546,8 +546,10 @@ node "<skill-root>/scripts/diagnose-request.mjs" --base-url https://your-space.h
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
 
 
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`、服务可达性、当前进程鉴权、页面 SSE 鉴权和下一步动作。
36
  自动化消费时使用 `npm run first-run -- --json`。
37
+ Agent 端点鉴权以 capabilities 的 `auth.schemes` 为准。配置 `AGENT_API_TOKEN` 时只接受 Bearer token;只有未配置 `AGENT_API_TOKEN` 且配置了 `APP_PASSWORD` 时,Agent 端点才接受访问码哈希 `GPT_IMAGE_APP_PASSWORD_HASH`。页面端 `/api/images` SSE 另看 `agent_streaming.page_sse.auth`;当其声明 `required=true` 时,form-data 必须包含 `passwordHash`。`GPT_IMAGE_AGENT_TOKEN` 不能替代页面 SSE 表单鉴权。
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`;Agent CLI 默认读取当前仓库根目录的 `.env.agent.local`,shell 环境变量优先。不要把 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
 
47
  同一个 `Idempotency-Key` 如果已经进入终态 `failed`,再次调用 generate/edit 或 job result/status 只会回放该失败,且 `retryable=false`。需要重新尝试时应创建新的业务操作和新的 `Idempotency-Key`。
48
  页面端 `/api/images` SSE 会把同一个业务 key 复用到 `clientRequestId`,因此脚本使用的 `Idempotency-Key` 不能超过 capabilities 中 `agent_streaming.page_sse.client_request_id.max_length` 声明的字符数;超长时会直接报错,不会静默截断。
49
  脚本会在 dry-run 和真实请求前前置校验 `--size` 或 JSONL `size`。`gpt-image-2` 支持 `auto` 或任意正整数 `WIDTHxHEIGHT`;默认 OpenAI-compatible 上游的更严格尺寸边界由服务端 profile 或真实上游显式报错。非 `gpt-image-2` 模型只接受 `auto`、`1024x1024`、`1536x1024` 或 `1024x1536`。生成、页面编辑、批量和上游探针默认请求 `output_format=webp`、`output_compression=100`。
50
+ 真实执行输出会包含机器可读 `summary`。成功摘要包含 `ok`、`billable`、`request_id`、`idempotency_key`、`artifact_ids`、`content_urls`、`absolute_content_urls`、`cached`、`started_at`、`completed_at`、`elapsed_ms`、`server_elapsed_ms`、`elapsed_source`、`elapsed_breakdown`、`transport`、`endpoint`、`route_mode`、`image_backend`、`stream_mode`、`streaming_strategy`、`selected_channel_id`、`upstream_host`、脱敏 `request_headers` 和 `next_action`。失败摘要也稳定包含空数组或 `null` 形式的产物、路由和渠道字段,便于 subagent 按同一模板汇报。失败摘要还包含 `transport_error_kind`、`retry_after_ms`、`cooldown_until`、`cooldown_target`、`retryable` 和 `next_action`。回答耗时问题时优先读取 `summary.elapsed_ms`;需要区分脚本等待和上游耗时时读取 `summary.elapsed_breakdown`。
51
 
52
  生成脚本参数:
53
 
 
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` | 当前进程是否已经拿到 `GPT_IMAGE_AGENT_TOKEN`。 |
550
+ | `page_sse_auth_available_to_process` | `first-run --json` | 目标服务要求页面 SSE `passwordHash` 时,当前进程是否已加载 `GPT_IMAGE_APP_PASSWORD_HASH`。 |
551
+ | `summary.page_sse_auth_ready` | `agent:doctor` | 页面 SSE 鉴权是否已满足;为 `false` 时不要运行 `--page-sse` 真实计费请求。 |
552
+ | `private_agent_env.exists` | `first-run --json` | 本机是否存在 `.env.agent.local` 私有配置;Agent CLI 默认从当前仓库根目录读取该文件。 |
553
  | `capabilities.ok` | `first-run --json`、`agent:doctor` | 目标地址是否返回 Agent capabilities;失败时先看 HTTP 状态、鉴权提示和服务地址。 |
554
  | `diagnostics_retention` | `diagnose-request.mjs` | 页面日志诊断的保留窗口;无匹配日志不等于请求一定没发生。 |
555
 
skills/gpt-image-playground-agent/scripts/batch-images.mjs CHANGED
@@ -10,6 +10,7 @@ import {
10
  readMaxImageEdge,
11
  readOptionValue,
12
  readPartialImages,
 
13
  resolvePlaygroundBaseUrl,
14
  resolveSameOriginUrl,
15
  validateAgentEditRequestAgainstCapabilities,
@@ -101,6 +102,7 @@ const TASK_FIELDS = new Set([
101
  ...EDIT_ONLY_FIELDS
102
  ]);
103
 
 
104
  const token = process.env.GPT_IMAGE_AGENT_TOKEN || '';
105
  const passwordHash = process.env.GPT_IMAGE_APP_PASSWORD_HASH || '';
106
 
 
10
  readMaxImageEdge,
11
  readOptionValue,
12
  readPartialImages,
13
+ loadPrivateAgentEnvFile,
14
  resolvePlaygroundBaseUrl,
15
  resolveSameOriginUrl,
16
  validateAgentEditRequestAgainstCapabilities,
 
102
  ...EDIT_ONLY_FIELDS
103
  ]);
104
 
105
+ loadPrivateAgentEnvFile();
106
  const token = process.env.GPT_IMAGE_AGENT_TOKEN || '';
107
  const passwordHash = process.env.GPT_IMAGE_APP_PASSWORD_HASH || '';
108
 
skills/gpt-image-playground-agent/scripts/diagnose-request.mjs CHANGED
@@ -2,9 +2,10 @@
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 || '';
9
  const passwordHash = process.env.GPT_IMAGE_APP_PASSWORD_HASH || '';
10
 
 
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, loadPrivateAgentEnvFile, readOptionValue, resolvePlaygroundBaseUrl } from './lib/script-utils.mjs';
6
 
7
  const MAX_CLIENT_REQUEST_IDS = 50;
8
+ loadPrivateAgentEnvFile();
9
  const token = process.env.GPT_IMAGE_AGENT_TOKEN || '';
10
  const passwordHash = process.env.GPT_IMAGE_APP_PASSWORD_HASH || '';
11
 
skills/gpt-image-playground-agent/scripts/edit-image.mjs CHANGED
@@ -12,6 +12,7 @@ import {
12
  readMaxImageEdge,
13
  readOptionValue,
14
  readPartialImages,
 
15
  resolvePlaygroundBaseUrl,
16
  sleep,
17
  validateAgentEditRequestAgainstCapabilities
@@ -49,6 +50,7 @@ const THINKING_VALUES = new Set(['minimal', 'none', 'low', 'medium', 'high', 'xh
49
  const DEFAULT_PAGE_OUTPUT_FORMAT = 'webp';
50
  const DEFAULT_PAGE_OUTPUT_COMPRESSION = 100;
51
 
 
52
  const token = process.env.GPT_IMAGE_AGENT_TOKEN || '';
53
  const passwordHash = process.env.GPT_IMAGE_APP_PASSWORD_HASH || '';
54
  const contractCheck = process.env.GPT_IMAGE_AGENT_CONTRACT_CHECK === '1' || process.argv.includes('--contract-check');
 
12
  readMaxImageEdge,
13
  readOptionValue,
14
  readPartialImages,
15
+ loadPrivateAgentEnvFile,
16
  resolvePlaygroundBaseUrl,
17
  sleep,
18
  validateAgentEditRequestAgainstCapabilities
 
50
  const DEFAULT_PAGE_OUTPUT_FORMAT = 'webp';
51
  const DEFAULT_PAGE_OUTPUT_COMPRESSION = 100;
52
 
53
+ loadPrivateAgentEnvFile();
54
  const token = process.env.GPT_IMAGE_AGENT_TOKEN || '';
55
  const passwordHash = process.env.GPT_IMAGE_APP_PASSWORD_HASH || '';
56
  const contractCheck = process.env.GPT_IMAGE_AGENT_CONTRACT_CHECK === '1' || process.argv.includes('--contract-check');
skills/gpt-image-playground-agent/scripts/generate-image.mjs CHANGED
@@ -10,6 +10,7 @@ import {
10
  readMaxImageEdge,
11
  readOptionValue,
12
  readPartialImages,
 
13
  resolvePlaygroundBaseUrl,
14
  resolveSameOriginUrl,
15
  sleep,
@@ -63,6 +64,7 @@ const GENERATE_PRESETS = {
63
  '2'
64
  ]
65
  };
 
66
  const token = process.env.GPT_IMAGE_AGENT_TOKEN || '';
67
  const passwordHash = process.env.GPT_IMAGE_APP_PASSWORD_HASH || '';
68
  const contractCheck = process.env.GPT_IMAGE_AGENT_CONTRACT_CHECK === '1' || process.argv.includes('--contract-check');
@@ -187,8 +189,9 @@ try {
187
  console.log(
188
  JSON.stringify(
189
  buildSuccessOutput(formatPageSseOutput(result), {
190
- transport: 'page_sse',
191
- endpoint: PAGE_SSE_ENDPOINT
 
192
  }, completeScriptTiming(scriptTiming)),
193
  null,
194
  2
@@ -696,7 +699,7 @@ function assertPageSseReady(capabilitiesValue) {
696
  if (pageSse?.auth?.required === true && !passwordHash) {
697
  throw createScriptError(
698
  'page_sse_auth_required',
699
- '页面 SSE 路径需要表单字段 passwordHash;请设置 GPT_IMAGE_APP_PASSWORD_HASH 后重试。'
700
  );
701
  }
702
  }
@@ -1000,13 +1003,23 @@ function buildPageSseFailureOutput(error, timing = completeScriptTiming(scriptTi
1000
 
1001
  function buildPageSseRouting(fallbackMode) {
1002
  return {
1003
- transport: 'page_sse',
1004
- endpoint: PAGE_SSE_ENDPOINT,
1005
  fallback_endpoint: AGENT_ENDPOINTS.generate,
1006
  fallback_mode: fallbackMode
1007
  };
1008
  }
1009
 
 
 
 
 
 
 
 
 
 
 
 
1010
  function buildPageSseScriptFailure(error, diagnostics, timing) {
1011
  const output = {
1012
  ok: false,
 
10
  readMaxImageEdge,
11
  readOptionValue,
12
  readPartialImages,
13
+ loadPrivateAgentEnvFile,
14
  resolvePlaygroundBaseUrl,
15
  resolveSameOriginUrl,
16
  sleep,
 
64
  '2'
65
  ]
66
  };
67
+ loadPrivateAgentEnvFile();
68
  const token = process.env.GPT_IMAGE_AGENT_TOKEN || '';
69
  const passwordHash = process.env.GPT_IMAGE_APP_PASSWORD_HASH || '';
70
  const contractCheck = process.env.GPT_IMAGE_AGENT_CONTRACT_CHECK === '1' || process.argv.includes('--contract-check');
 
189
  console.log(
190
  JSON.stringify(
191
  buildSuccessOutput(formatPageSseOutput(result), {
192
+ ...buildPageSseSummaryRouting(),
193
+ fallback_endpoint: AGENT_ENDPOINTS.generate,
194
+ fallback_mode: 'manual_after_diagnosis'
195
  }, completeScriptTiming(scriptTiming)),
196
  null,
197
  2
 
699
  if (pageSse?.auth?.required === true && !passwordHash) {
700
  throw createScriptError(
701
  'page_sse_auth_required',
702
+ '页面 SSE 路径需要表单字段 passwordHash;请在本机私有 .env.agent.local 中设置 GPT_IMAGE_APP_PASSWORD_HASH,或导出该环境变量后重试。只有 GPT_IMAGE_AGENT_TOKEN 不能用于页面 SSE 表单鉴权。'
703
  );
704
  }
705
  }
 
1003
 
1004
  function buildPageSseRouting(fallbackMode) {
1005
  return {
1006
+ ...buildPageSseSummaryRouting(),
 
1007
  fallback_endpoint: AGENT_ENDPOINTS.generate,
1008
  fallback_mode: fallbackMode
1009
  };
1010
  }
1011
 
1012
+ function buildPageSseSummaryRouting() {
1013
+ return {
1014
+ transport: 'page_sse',
1015
+ endpoint: PAGE_SSE_ENDPOINT,
1016
+ route_mode: 'page_sse',
1017
+ image_backend: normalizeImageBackendForPage(requestBody?.image_backend),
1018
+ stream_mode: requestBody?.stream_mode || null,
1019
+ streaming_strategy: requestBody?.streaming_strategy || null
1020
+ };
1021
+ }
1022
+
1023
  function buildPageSseScriptFailure(error, diagnostics, timing) {
1024
  const output = {
1025
  ok: false,
skills/gpt-image-playground-agent/scripts/lib/page-sse-client.mjs CHANGED
@@ -21,7 +21,7 @@ export function assertPageSseReady({ capabilities, passwordHash, idempotencyKey
21
  if (pageSse?.auth?.required === true && !passwordHash) {
22
  throw createPageSseScriptError(
23
  'page_sse_auth_required',
24
- '页面 SSE 路径需要表单字段 passwordHash;请设置 GPT_IMAGE_APP_PASSWORD_HASH 后重试。'
25
  );
26
  }
27
  const maxLength = readPageSseClientRequestIdMaxLength(capabilities);
 
21
  if (pageSse?.auth?.required === true && !passwordHash) {
22
  throw createPageSseScriptError(
23
  'page_sse_auth_required',
24
+ '页面 SSE 路径需要表单字段 passwordHash;请在本机私有 .env.agent.local 中设置 GPT_IMAGE_APP_PASSWORD_HASH,或导出该环境变量后重试。只有 GPT_IMAGE_AGENT_TOKEN 不能用于页面 SSE 表单鉴权。'
25
  );
26
  }
27
  const maxLength = readPageSseClientRequestIdMaxLength(capabilities);
skills/gpt-image-playground-agent/scripts/lib/script-summary.mjs CHANGED
@@ -17,29 +17,30 @@ export function buildSuccessSummary({ result, routing, timing, idempotencyKey, b
17
  const serverTiming = readObject(result?.timing);
18
  const execution = readObject(result?.execution);
19
  const images = Array.isArray(result?.images) ? result.images : [];
20
- const contentUrls = images
21
- .map((image) => image?.absolute_content_url || image?.content_url || image?.absolute_path || image?.path)
22
- .filter((value) => typeof value === 'string' && value);
23
- return compactObject({
24
  ok: true,
25
  billable,
26
- request_id: readString(result?.request_id),
27
  idempotency_key: readString(result?.idempotency_key) || idempotencyKey,
28
  artifact_ids: images.map((image) => image?.id).filter((value) => typeof value === 'string' && value),
29
- content_urls: contentUrls,
 
30
  cached: typeof result?.cached === 'boolean' ? result.cached : undefined,
31
- started_at: readString(serverTiming?.started_at) || timing?.started_at,
32
- completed_at: readString(serverTiming?.completed_at) || timing?.completed_at,
33
- elapsed_ms: readNonNegativeNumber(serverTiming?.elapsed_ms) ?? timing?.elapsed_ms,
34
- server_elapsed_ms: readNonNegativeNumber(serverTiming?.server_elapsed_ms),
 
 
35
  transport: readString(execution?.transport) || readString(routing?.transport),
36
  endpoint: readString(execution?.endpoint) || readString(routing?.endpoint),
37
- route_mode: readString(execution?.route_mode),
38
- image_backend: readString(execution?.image_backend),
39
- stream_mode: readString(execution?.stream_mode),
40
- streaming_strategy: readString(execution?.streaming_strategy),
41
- selected_channel_id: readString(execution?.selected_channel_id),
42
- upstream_host: readString(execution?.upstream_host),
43
  request_headers: readObject(execution?.request_headers),
44
  retryable: false,
45
  next_action: nextAction || 'done'
@@ -50,18 +51,29 @@ export function buildFailureSummary({ errorBody, routing, timing, idempotencyKey
50
  const error = readObject(errorBody?.error) || readObject(errorBody);
51
  const errorMessage = readString(error?.message) || readString(errorBody?.error);
52
  const diagnostics = readObject(error?.diagnostics);
53
- return compactObject({
 
54
  ok: false,
55
  billable,
56
- request_id: readString(error?.request_id),
57
  idempotency_key: idempotencyKey,
58
- started_at: timing?.started_at,
59
- completed_at: timing?.completed_at,
60
- elapsed_ms: timing?.elapsed_ms,
 
 
 
 
 
 
61
  transport: readString(routing?.transport),
62
  endpoint: readString(routing?.endpoint),
63
- selected_channel_id: readString(diagnostics?.selected_channel_id),
64
- upstream_host: readString(diagnostics?.upstream_host),
 
 
 
 
65
  transport_error_kind: readString(diagnostics?.transport_error_kind),
66
  retryable: typeof error?.retryable === 'boolean' ? error.retryable : undefined,
67
  retry_after_seconds: readNonNegativeNumber(errorBody?.retry_after ?? diagnostics?.retry_after_seconds),
@@ -84,6 +96,28 @@ function buildFailureNextAction(error) {
84
  return 'diagnose_then_new_idempotency_key';
85
  }
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  function compactObject(value) {
88
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
89
  }
@@ -100,3 +134,51 @@ function readNonNegativeNumber(value) {
100
  if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return undefined;
101
  return Math.round(value);
102
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  const serverTiming = readObject(result?.timing);
18
  const execution = readObject(result?.execution);
19
  const images = Array.isArray(result?.images) ? result.images : [];
20
+ const timingSummary = buildTimingSummary({ clientTiming: timing, serverTiming });
21
+ return stableSummary({
 
 
22
  ok: true,
23
  billable,
24
+ request_id: readString(result?.request_id) || null,
25
  idempotency_key: readString(result?.idempotency_key) || idempotencyKey,
26
  artifact_ids: images.map((image) => image?.id).filter((value) => typeof value === 'string' && value),
27
+ content_urls: readImageUrls(images, ['content_url', 'path']),
28
+ absolute_content_urls: readImageUrls(images, ['absolute_content_url', 'absolute_path']),
29
  cached: typeof result?.cached === 'boolean' ? result.cached : undefined,
30
+ started_at: timingSummary.started_at,
31
+ completed_at: timingSummary.completed_at,
32
+ elapsed_ms: timingSummary.elapsed_ms,
33
+ server_elapsed_ms: timingSummary.server_elapsed_ms,
34
+ elapsed_source: timingSummary.elapsed_source,
35
+ elapsed_breakdown: timingSummary.elapsed_breakdown,
36
  transport: readString(execution?.transport) || readString(routing?.transport),
37
  endpoint: readString(execution?.endpoint) || readString(routing?.endpoint),
38
+ route_mode: readString(execution?.route_mode) || readRouteMode(routing),
39
+ image_backend: readString(execution?.image_backend) || readString(routing?.image_backend) || null,
40
+ stream_mode: readString(execution?.stream_mode) || readString(routing?.stream_mode) || null,
41
+ streaming_strategy: readString(execution?.streaming_strategy) || readString(routing?.streaming_strategy) || null,
42
+ selected_channel_id: readString(execution?.selected_channel_id) || null,
43
+ upstream_host: readString(execution?.upstream_host) || null,
44
  request_headers: readObject(execution?.request_headers),
45
  retryable: false,
46
  next_action: nextAction || 'done'
 
51
  const error = readObject(errorBody?.error) || readObject(errorBody);
52
  const errorMessage = readString(error?.message) || readString(errorBody?.error);
53
  const diagnostics = readObject(error?.diagnostics);
54
+ const timingSummary = buildTimingSummary({ clientTiming: timing, diagnostics });
55
+ return stableSummary({
56
  ok: false,
57
  billable,
58
+ request_id: readString(error?.request_id) || null,
59
  idempotency_key: idempotencyKey,
60
+ artifact_ids: [],
61
+ content_urls: [],
62
+ absolute_content_urls: [],
63
+ started_at: timingSummary.started_at,
64
+ completed_at: timingSummary.completed_at,
65
+ elapsed_ms: timingSummary.elapsed_ms,
66
+ server_elapsed_ms: timingSummary.server_elapsed_ms,
67
+ elapsed_source: timingSummary.elapsed_source,
68
+ elapsed_breakdown: timingSummary.elapsed_breakdown,
69
  transport: readString(routing?.transport),
70
  endpoint: readString(routing?.endpoint),
71
+ route_mode: readRouteMode(routing),
72
+ image_backend: readString(routing?.image_backend) || null,
73
+ stream_mode: readString(routing?.stream_mode) || null,
74
+ streaming_strategy: readString(routing?.streaming_strategy) || null,
75
+ selected_channel_id: readString(diagnostics?.selected_channel_id) || null,
76
+ upstream_host: readString(diagnostics?.upstream_host) || null,
77
  transport_error_kind: readString(diagnostics?.transport_error_kind),
78
  retryable: typeof error?.retryable === 'boolean' ? error.retryable : undefined,
79
  retry_after_seconds: readNonNegativeNumber(errorBody?.retry_after ?? diagnostics?.retry_after_seconds),
 
96
  return 'diagnose_then_new_idempotency_key';
97
  }
98
 
99
+ function stableSummary(value) {
100
+ return {
101
+ ...value,
102
+ request_id: value.request_id ?? null,
103
+ idempotency_key: value.idempotency_key ?? null,
104
+ artifact_ids: Array.isArray(value.artifact_ids) ? value.artifact_ids : [],
105
+ content_urls: Array.isArray(value.content_urls) ? value.content_urls : [],
106
+ absolute_content_urls: Array.isArray(value.absolute_content_urls) ? value.absolute_content_urls : [],
107
+ route_mode: value.route_mode ?? null,
108
+ image_backend: value.image_backend ?? null,
109
+ stream_mode: value.stream_mode ?? null,
110
+ streaming_strategy: value.streaming_strategy ?? null,
111
+ selected_channel_id: value.selected_channel_id ?? null,
112
+ upstream_host: value.upstream_host ?? null,
113
+ transport_error_kind: value.transport_error_kind ?? null,
114
+ retry_after_ms: value.retry_after_ms ?? null,
115
+ retry_after_seconds: value.retry_after_seconds ?? null,
116
+ cooldown_until: value.cooldown_until ?? null,
117
+ cooldown_target: value.cooldown_target ?? null
118
+ };
119
+ }
120
+
121
  function compactObject(value) {
122
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
123
  }
 
134
  if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return undefined;
135
  return Math.round(value);
136
  }
137
+
138
+ function readRouteMode(routing) {
139
+ if (typeof routing?.route_mode === 'string' && routing.route_mode) return routing.route_mode;
140
+ if (routing?.transport === 'page_sse') return 'page_sse';
141
+ if (routing?.transport === 'agent_job_polling') return 'job';
142
+ if (routing?.transport === 'agent_json') return 'agent';
143
+ return undefined;
144
+ }
145
+
146
+ function readImageUrls(images, fields) {
147
+ return images
148
+ .map((image) => {
149
+ for (const field of fields) {
150
+ if (typeof image?.[field] === 'string' && image[field]) return image[field];
151
+ }
152
+ return undefined;
153
+ })
154
+ .filter((value) => typeof value === 'string' && value);
155
+ }
156
+
157
+ function buildTimingSummary({ clientTiming, serverTiming, diagnostics }) {
158
+ const serverTimingElapsedMs = readNonNegativeNumber(serverTiming?.elapsed_ms);
159
+ const diagnosticsElapsedMs = readNonNegativeNumber(diagnostics?.elapsed_ms);
160
+ const serverElapsedMs = readNonNegativeNumber(serverTiming?.server_elapsed_ms ?? serverTimingElapsedMs ?? diagnosticsElapsedMs);
161
+ const clientElapsedMs = readNonNegativeNumber(clientTiming?.elapsed_ms);
162
+ const elapsedMs = serverTimingElapsedMs ?? diagnosticsElapsedMs ?? clientElapsedMs;
163
+ const elapsedSource =
164
+ serverTimingElapsedMs !== undefined || diagnosticsElapsedMs !== undefined ? 'server_timing' : 'client_script';
165
+ const summary = compactObject({
166
+ started_at: readString(serverTiming?.started_at) || clientTiming?.started_at,
167
+ completed_at: readString(serverTiming?.completed_at) || clientTiming?.completed_at,
168
+ elapsed_ms: elapsedMs,
169
+ server_elapsed_ms: serverElapsedMs,
170
+ elapsed_source: elapsedSource,
171
+ elapsed_breakdown: buildElapsedBreakdown(clientElapsedMs, serverElapsedMs)
172
+ });
173
+ return summary;
174
+ }
175
+
176
+ function buildElapsedBreakdown(clientElapsedMs, serverElapsedMs) {
177
+ if (clientElapsedMs === undefined && serverElapsedMs === undefined) return undefined;
178
+ return compactObject({
179
+ client_script_ms: clientElapsedMs,
180
+ upstream_or_server_ms: serverElapsedMs,
181
+ client_overhead_ms:
182
+ clientElapsedMs !== undefined && serverElapsedMs !== undefined ? Math.max(0, clientElapsedMs - serverElapsedMs) : undefined
183
+ });
184
+ }
skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs CHANGED
@@ -1,8 +1,39 @@
 
 
 
1
  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
  export const DEFAULT_PLAYGROUND_BASE_URL = 'http://localhost:4783';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  export function readOptionValue(argv, index, name) {
8
  const value = argv[index];
@@ -194,6 +225,75 @@ export function errorMessage(error) {
194
  return error instanceof Error ? error.message : String(error);
195
  }
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  function assertPositiveIntegerDimensions(width, height, label) {
198
  if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
199
  throw new Error(`${label} 的宽度和高度必须是正数。`);
 
1
+ import { dirname, join, resolve } from 'node:path';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+
4
  const MAX_RETRY_AFTER_SECONDS = 60;
5
  const DIGITS_PATTERN = /^\d+$/;
6
  const IMAGE_SIZE_PATTERN = /^(\d+)x(\d+)$/;
7
  const LEGACY_IMAGE_SIZES = new Set(['auto', '1024x1024', '1536x1024', '1024x1536']);
8
  export const DEFAULT_PLAYGROUND_BASE_URL = 'http://localhost:4783';
9
+ const DEFAULT_PRIVATE_AGENT_ENV_FILE = '.env.agent.local';
10
+ const PRIVATE_AGENT_ENV_PREFIX = 'GPT_IMAGE_';
11
+ const DISABLE_PRIVATE_AGENT_ENV_VALUES = new Set(['0', 'false', 'no']);
12
+
13
+ export function loadPrivateAgentEnvFile(options = {}) {
14
+ const env = options.env || process.env;
15
+ if (isPrivateAgentEnvLoadingDisabled(env)) {
16
+ return { loaded: false, skipped: true, reason: 'disabled_by_env' };
17
+ }
18
+ const cwd = options.cwd || process.cwd();
19
+ const filePath = options.filePath || findPrivateAgentEnvFile(cwd);
20
+ if (!existsSync(filePath)) {
21
+ return { loaded: false, skipped: true, reason: 'file_not_found', path: filePath };
22
+ }
23
+ const entries = parsePrivateAgentEnvContent(readFileSync(filePath, 'utf8'));
24
+ const appliedNames = [];
25
+ for (const { name, value } of entries) {
26
+ if (!name.startsWith(PRIVATE_AGENT_ENV_PREFIX)) continue;
27
+ if (env[name] !== undefined) continue;
28
+ env[name] = value;
29
+ appliedNames.push(name);
30
+ }
31
+ return {
32
+ loaded: true,
33
+ path: filePath,
34
+ applied_names: appliedNames
35
+ };
36
+ }
37
 
38
  export function readOptionValue(argv, index, name) {
39
  const value = argv[index];
 
225
  return error instanceof Error ? error.message : String(error);
226
  }
227
 
228
+ function isPrivateAgentEnvLoadingDisabled(env) {
229
+ return DISABLE_PRIVATE_AGENT_ENV_VALUES.has(String(env.GPT_IMAGE_AGENT_LOAD_ENV_FILE || '').trim().toLowerCase());
230
+ }
231
+
232
+ function findPrivateAgentEnvFile(cwd) {
233
+ const start = resolve(cwd);
234
+ let current = start;
235
+ while (true) {
236
+ const candidate = join(current, DEFAULT_PRIVATE_AGENT_ENV_FILE);
237
+ if (existsSync(candidate)) return candidate;
238
+ if (isPrivateAgentEnvSearchBoundary(current) || dirname(current) === current) {
239
+ return join(start, DEFAULT_PRIVATE_AGENT_ENV_FILE);
240
+ }
241
+ current = dirname(current);
242
+ }
243
+ }
244
+
245
+ function isPrivateAgentEnvSearchBoundary(directory) {
246
+ return existsSync(join(directory, '.git')) || isPlaygroundProjectRoot(directory) || isStandaloneSkillRoot(directory);
247
+ }
248
+
249
+ function isPlaygroundProjectRoot(directory) {
250
+ return existsSync(join(directory, 'package.json')) && existsSync(join(directory, 'skills/gpt-image-playground-agent/SKILL.md'));
251
+ }
252
+
253
+ function isStandaloneSkillRoot(directory) {
254
+ return (
255
+ existsSync(join(directory, 'SKILL.md')) &&
256
+ existsSync(join(directory, 'scripts')) &&
257
+ !isPlaygroundProjectRoot(dirname(dirname(directory)))
258
+ );
259
+ }
260
+
261
+ function parsePrivateAgentEnvContent(content) {
262
+ const entries = [];
263
+ for (const line of content.split(/\r?\n/)) {
264
+ const parsed = parsePrivateAgentEnvLine(line);
265
+ if (parsed) entries.push(parsed);
266
+ }
267
+ return entries;
268
+ }
269
+
270
+ function parsePrivateAgentEnvLine(line) {
271
+ const trimmed = line.trim();
272
+ if (!trimmed || trimmed.startsWith('#')) return undefined;
273
+ const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
274
+ if (!match) return undefined;
275
+ return { name: match[1], value: parsePrivateAgentEnvValue(match[2].trim()) };
276
+ }
277
+
278
+ function parsePrivateAgentEnvValue(value) {
279
+ if (value.length < 2) return value;
280
+ if (value.startsWith('"') || value.startsWith("'")) return parseQuotedPrivateAgentEnvValue(value);
281
+ return stripPrivateAgentEnvComment(value).trim();
282
+ }
283
+
284
+ function parseQuotedPrivateAgentEnvValue(value) {
285
+ const quote = value[0];
286
+ const closeIndex = value.indexOf(quote, 1);
287
+ if (closeIndex < 0) return value.slice(1);
288
+ return value.slice(1, closeIndex);
289
+ }
290
+
291
+ function stripPrivateAgentEnvComment(value) {
292
+ const index = value.search(/\s#/);
293
+ if (index < 0) return value;
294
+ return value.slice(0, index);
295
+ }
296
+
297
  function assertPositiveIntegerDimensions(width, height, label) {
298
  if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
299
  throw new Error(`${label} 的宽度和高度必须是正数。`);
skills/gpt-image-playground-agent/scripts/probe-upstream-image.mjs CHANGED
@@ -4,6 +4,7 @@ import tls from 'node:tls';
4
  import {
5
  errorMessage,
6
  assertValidImageSizeForModel,
 
7
  normalizeBaseUrl,
8
  normalizeOutputFormat,
9
  readConfiguredPositiveInteger,
@@ -16,6 +17,7 @@ const DEFAULT_USER_AGENT = 'gpt-image-playground/probe';
16
  const OUTPUT_FORMATS = new Set(['png', 'jpeg', 'webp']);
17
  const DEFAULT_OUTPUT_FORMAT = 'webp';
18
  const DEFAULT_OUTPUT_COMPRESSION = 100;
 
19
  const scriptTiming = startScriptTiming();
20
  let options;
21
  try {
 
4
  import {
5
  errorMessage,
6
  assertValidImageSizeForModel,
7
+ loadPrivateAgentEnvFile,
8
  normalizeBaseUrl,
9
  normalizeOutputFormat,
10
  readConfiguredPositiveInteger,
 
17
  const OUTPUT_FORMATS = new Set(['png', 'jpeg', 'webp']);
18
  const DEFAULT_OUTPUT_FORMAT = 'webp';
19
  const DEFAULT_OUTPUT_COMPRESSION = 100;
20
+ loadPrivateAgentEnvFile();
21
  const scriptTiming = startScriptTiming();
22
  let options;
23
  try {