Spaces:
Running
Running
Deploy 863ecb4 to Docker Space
Browse filesSource: MisonL/gpt-image-playground-customer@863ecb4beb440a5c59e5a0034a32c0176a0853ba
- .env.agent.local.example +13 -4
- README.md +10 -6
- scripts/agent-doctor.mjs +18 -1
- scripts/agent-skill-scripts.test.mjs +224 -6
- scripts/command-center.test.mjs +36 -3
- scripts/first-run.mjs +29 -2
- skills/gpt-image-playground-agent/SKILL.md +9 -7
- skills/gpt-image-playground-agent/references/api.md +8 -6
- skills/gpt-image-playground-agent/scripts/batch-images.mjs +2 -0
- skills/gpt-image-playground-agent/scripts/diagnose-request.mjs +2 -1
- skills/gpt-image-playground-agent/scripts/edit-image.mjs +2 -0
- skills/gpt-image-playground-agent/scripts/generate-image.mjs +18 -5
- skills/gpt-image-playground-agent/scripts/lib/page-sse-client.mjs +1 -1
- skills/gpt-image-playground-agent/scripts/lib/script-summary.mjs +105 -23
- skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs +100 -0
- skills/gpt-image-playground-agent/scripts/probe-upstream-image.mjs +2 -0
.env.agent.local.example
CHANGED
|
@@ -1,9 +1,18 @@
|
|
| 1 |
# Copy to .env.agent.local and keep the real file private.
|
| 2 |
-
#
|
| 3 |
-
#
|
| 4 |
-
#
|
| 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`
|
| 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` 是否存在私有鉴权配置
|
| 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`
|
|
|
|
|
|
|
| 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` | 当前
|
| 310 |
-
| `
|
|
|
|
|
|
|
| 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
|
| 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 {
|
|
|
|
|
|
|
|
|
|
| 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, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: { ...
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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: {
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
|
|
|
| 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('
|
| 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` 是否存在私有鉴权配置
|
| 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` | 当前
|
| 136 |
-
| `
|
|
|
|
|
|
|
| 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`;
|
| 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` | 当前
|
| 550 |
-
| `
|
|
|
|
|
|
|
| 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 |
-
|
| 191 |
-
|
|
|
|
| 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 |
-
|
| 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
|
| 21 |
-
|
| 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:
|
|
|
|
| 30 |
cached: typeof result?.cached === 'boolean' ? result.cached : undefined,
|
| 31 |
-
started_at:
|
| 32 |
-
completed_at:
|
| 33 |
-
elapsed_ms:
|
| 34 |
-
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 |
-
|
|
|
|
| 54 |
ok: false,
|
| 55 |
billable,
|
| 56 |
-
request_id: readString(error?.request_id),
|
| 57 |
idempotency_key: idempotencyKey,
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
transport: readString(routing?.transport),
|
| 62 |
endpoint: readString(routing?.endpoint),
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|