#!/usr/bin/env node import { loadPrivateAgentEnvFile, resolvePlaygroundBaseUrl } from '../skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs'; import { isMainModule, printJson, redactBaseUrl } from './command-center-utils.mjs'; import { inspectDependencyInstallation, LOCAL_DEPENDENCY_INSTALL_COMMAND } from './dependency-installation.mjs'; import { summarizeEnvFile } from './env-summary.mjs'; import { inspectNpmInstallPolicy } from './npm-install-policy.mjs'; import { isSupportedNodeVersion, MIN_NODE_VERSION_RANGE } from './node-version.mjs'; import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; const DEFAULT_TIMEOUT_MS = 3000; const DEFAULT_ENV_FILES = ['.env.local', '.env.agent.local']; function parseArgs(argv) { const parsed = { help: false, json: false, baseUrl: undefined, timeoutMs: DEFAULT_TIMEOUT_MS }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--help' || arg === '-h') parsed.help = true; else if (arg === '--json') parsed.json = true; else if (arg === '--base-url') parsed.baseUrl = readOptionValue(argv, (index += 1), arg); else if (arg === '--timeout-ms') parsed.timeoutMs = readPositiveInteger(readOptionValue(argv, (index += 1), arg), arg); else throw new Error(`未知参数:${arg}`); } return parsed; } function printHelp() { console.log(`用法: npm run first-run npm run first-run -- --json npm run first-run -- --base-url https://your-space.hf.space 选项: --json 输出机器可读 JSON。 --base-url 显式服务地址,优先于 GPT_IMAGE_PLAYGROUND_URL 和本地探测。 --timeout-ms HTTP 探测超时,默认 3000。 first-run 只读、非计费,不写 env 文件或 secret。`); } export async function buildFirstRunReport(options = {}, env = process.env) { const cwd = options.cwd || process.cwd(); const envFiles = options.envFiles || DEFAULT_ENV_FILES; loadPrivateAgentEnvFile({ cwd, env }); const base = resolveFirstRunBaseUrl(options.baseUrl, env); const envSummary = envFiles.map((filePath) => summarizeEnvFile(join(cwd, filePath))); const npmInstallPolicy = options.npmInstallPolicy || inspectNpmInstallPolicy(); const validationError = readBaseUrlValidationError(base); const service = validationError ? { ok: false, skipped: true, error: validationError } : await probeService(base.baseUrl, { timeoutMs: options.timeoutMs || DEFAULT_TIMEOUT_MS, headers: authHeaders(env) }); const agentAuth = readAgentAuthState(env, envSummary, service); const checks = buildChecks({ cwd, env, envSummary, service, base, validationError, npmInstallPolicy }); const nextActions = buildNextActions({ checks, base, service, env, envSummary, validationError }); return { ok: checks.every((check) => check.ok || check.skipped), command: 'first-run', billable: false, service_base_url: validationError ? undefined : redactBaseUrl(base.baseUrl), service_base_url_source: base.source, interactive_confirmation_required: base.interactive_confirmation_required, agent_auth_process: agentAuth.process, agent_auth_service: agentAuth.service, private_agent_env: agentAuth.privateEnv, checks, env_sources: envSummary.map((source) => summarizeEnvSource(source, cwd)), service: summarizeService(service), next_actions: nextActions }; } async function probeService(baseUrl, options) { const capabilities = await readJsonEndpoint(`${baseUrl}/api/agent/capabilities`, options); const runtime = capabilities.ok ? await readJsonEndpoint(`${baseUrl}/api/runtime-capabilities`, options) : undefined; return { ok: capabilities.ok && (!runtime || runtime.ok), capabilities, ...(runtime ? { runtime } : {}) }; } async function readJsonEndpoint(url, options) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), options.timeoutMs); try { const response = await fetch(url, { headers: options.headers, signal: controller.signal }); const text = await response.text(); if (!response.ok) { return { ok: false, status: response.status, endpoint: safeEndpoint(url), error: `HTTP ${response.status}` }; } try { return { ok: true, status: response.status, endpoint: safeEndpoint(url), body: JSON.parse(text) }; } catch { return { ok: false, status: response.status, endpoint: safeEndpoint(url), error: 'response_not_json' }; } } catch (error) { return { ok: false, endpoint: safeEndpoint(url), error: error?.name === 'AbortError' ? `timeout_after_${options.timeoutMs}ms` : 'request_failed' }; } finally { clearTimeout(timer); } } function buildChecks({ cwd, env, envSummary, service, base, validationError, npmInstallPolicy }) { const node = readNodeCheck(); const packageJson = readPackageJson(cwd); const packageLock = existsSync(join(cwd, 'package-lock.json')); const dependencyInstallation = inspectDependencyInstallation(cwd); const agentAuthSchemes = readAgentAuthSchemes(service); const currentAgentAuth = hasAnyConfiguredAgentAuth(env, agentAuthSchemes); const fileAgentAuth = envSummary.some((source) => hasAnyConfiguredAgentAuthSource(source, agentAuthSchemes)); return [ { name: 'node_version', ok: node.ok, current: node.current, required: MIN_NODE_VERSION_RANGE }, { name: 'npm_install_policy', ok: npmInstallPolicy.ok, ...(npmInstallPolicy.ok ? {} : { reason: npmInstallPolicy.reason, ...(npmInstallPolicy.error ? { error: npmInstallPolicy.error } : {}) }) }, { name: 'package_lock', ok: packageLock }, { name: 'dependencies_installed', ok: dependencyInstallation.ok, skipped: false, ...(dependencyInstallation.ok ? {} : summarizeDependencyFailure(dependencyInstallation)), hint: dependencyInstallation.ok ? undefined : `运行 ${LOCAL_DEPENDENCY_INSTALL_COMMAND}。` }, { name: 'env_files', ok: envSummary.some((source) => source.exists), files: envSummary.map((source) => ({ path: source.path.replace(`${cwd}/`, ''), exists: source.exists })) }, { name: 'agent_auth_available_to_process', ok: currentAgentAuth, skipped: !requiresAgentAuth(service), auth_in_private_env_file: fileAgentAuth, required_schemes: agentAuthSchemes }, { name: 'page_sse_auth_available_to_process', ok: Boolean(env.GPT_IMAGE_APP_PASSWORD_HASH), skipped: !requiresPageSsePasswordHash(service), auth_in_private_env_file: envSummary.some((source) => sourceHasSetVariable(source, 'GPT_IMAGE_APP_PASSWORD_HASH') ) }, { name: 'service_base_url_valid', ok: !validationError, source: base.source, ...(validationError ? { error: validationError } : {}) }, { name: 'service_reachable', ok: service.ok, endpoint: '/api/agent/capabilities', ...(service.capabilities?.status ? { status: service.capabilities.status } : {}), ...(service.capabilities?.error ? { error: service.capabilities.error } : {}) }, { name: 'agent_capabilities_contract', ok: isCapabilitiesBody(service.capabilities?.body), skipped: !service.capabilities?.ok }, { name: 'runtime_capabilities_contract', ok: Boolean(service.runtime?.ok), skipped: !service.capabilities?.ok }, { name: 'package_metadata', ok: Boolean(packageJson.name && packageJson.version), package: packageJson.name, version: packageJson.version } ]; } function readAgentAuthState(env, envSummary, service) { const hasToken = Boolean(env.GPT_IMAGE_AGENT_TOKEN); const hasPasswordHash = Boolean(env.GPT_IMAGE_APP_PASSWORD_HASH); const privateSource = envSummary.find((source) => source.path.endsWith('.env.agent.local')); const requiredSchemes = readAgentAuthSchemes(service); const serviceRequiresAuth = requiresAgentAuth(service); return { process: { has_token: hasToken, has_password_hash: hasPasswordHash, has_any_auth: hasToken || hasPasswordHash }, service: { required: serviceRequiresAuth, required_schemes: requiredSchemes, ready: serviceRequiresAuth ? hasAnyConfiguredAgentAuth(env, requiredSchemes) : false, has_token: requiredSchemes.includes('bearer') ? hasToken : false, has_password_hash: requiredSchemes.includes('x-app-password-hash') ? hasPasswordHash : false, has_any_auth: serviceRequiresAuth ? hasAnyConfiguredAgentAuth(env, requiredSchemes) : false }, privateEnv: { exists: privateSource?.exists === true, has_token: privateSource ? sourceHasSetVariable(privateSource, 'GPT_IMAGE_AGENT_TOKEN') : false, has_password_hash: privateSource ? sourceHasSetVariable(privateSource, 'GPT_IMAGE_APP_PASSWORD_HASH') : false } }; } function buildNextActions({ checks, base, service, env, envSummary, validationError }) { const actions = []; const agentAuthSchemes = readAgentAuthSchemes(service); const hasCurrentAgentAuth = hasAnyConfiguredAgentAuth(env, agentAuthSchemes); const hasFileAgentAuth = envSummary.some((source) => hasAnyConfiguredAgentAuthSource(source, agentAuthSchemes)); if (!findCheck(checks, 'node_version').ok) actions.push(`安装 Node.js ${MIN_NODE_VERSION_RANGE} 或更新版本。`); if (!findCheck(checks, 'npm_install_policy').ok) { actions.push('升级 npm 到支持 --strict-allow-scripts 的版本后重新运行。'); } if (!findCheck(checks, 'dependencies_installed').ok) actions.push(`运行 ${LOCAL_DEPENDENCY_INSTALL_COMMAND}。`); if (!findCheck(checks, 'env_files').ok) { actions.push('复制 .env.example 为 .env.local,或在页面设置里配置默认上游。'); } if (validationError) { actions.push('把 GPT_IMAGE_PLAYGROUND_URL 或 --base-url 设为不含凭据、查询参数或片段的 http/https 地址。'); return actions; } if (!hasCurrentAgentAuth && requiresAgentAuth(service) && !hasFileAgentAuth) { actions.push(buildAgentAuthAction(agentAuthSchemes)); } if (requiresPageSsePasswordHash(service) && !env.GPT_IMAGE_APP_PASSWORD_HASH) { actions.push( '如果要使用页面 SSE、Responses backend edit 或 --page-sse,请在本机私有 .env.agent.local 中设置 GPT_IMAGE_APP_PASSWORD_HASH;Agent 脚本会自动读取该文件,GPT_IMAGE_AGENT_TOKEN 只覆盖 Agent JSON 鉴权。' ); } if (!service.ok && !requiresAgentAuth(service)) { actions.push( '先用 npm run dev 或 docker compose up -d --build --remove-orphans 启动服务,再重新运行 npm run first-run。' ); } if (base.interactive_confirmation_required) { actions.push('在交互式 Agent 任务里,先和用户确认探测到的服务地址,再发真实请求。'); } if (!hasCurrentAgentAuth && hasFileAgentAuth) { actions.push( 'Agent 脚本会自动读取 .env.agent.local;如果仍提示缺少鉴权,请确认文件位于当前仓库根目录且变量名正确。' ); } if (service.ok && hasCurrentAgentAuth) { actions.push('运行 npm run agent:doctor 做完整的非计费 Agent 合同检查。'); } return actions; } function requiresAgentAuth(service) { if (service.capabilities?.status === 401 || service.capabilities?.status === 403) return true; const schemes = service.capabilities?.body?.auth?.schemes; return Array.isArray(schemes) && schemes.length > 0; } function requiresPageSsePasswordHash(service) { return service.capabilities?.body?.agent_streaming?.page_sse?.auth?.required === true; } function readAgentAuthSchemes(service) { const schemes = service.capabilities?.body?.auth?.schemes; if (!Array.isArray(schemes)) return []; return schemes.filter((scheme) => typeof scheme === 'string'); } function hasAnyConfiguredAgentAuth(env, schemes) { if (schemes.includes('bearer')) return Boolean(env.GPT_IMAGE_AGENT_TOKEN); if (schemes.includes('x-app-password-hash')) return Boolean(env.GPT_IMAGE_APP_PASSWORD_HASH); return Boolean(env.GPT_IMAGE_AGENT_TOKEN || env.GPT_IMAGE_APP_PASSWORD_HASH); } function hasAnyConfiguredAgentAuthSource(source, schemes) { if (schemes.includes('bearer')) return sourceHasSetVariable(source, 'GPT_IMAGE_AGENT_TOKEN'); if (schemes.includes('x-app-password-hash')) return sourceHasSetVariable(source, 'GPT_IMAGE_APP_PASSWORD_HASH'); return ( sourceHasSetVariable(source, 'GPT_IMAGE_AGENT_TOKEN') || sourceHasSetVariable(source, 'GPT_IMAGE_APP_PASSWORD_HASH') ); } function buildAgentAuthAction(schemes) { if (schemes.includes('bearer')) { return '在仓库外导出 GPT_IMAGE_AGENT_TOKEN,再运行受保护的 Agent 脚本。'; } if (schemes.includes('x-app-password-hash')) { return '在仓库外导出 GPT_IMAGE_APP_PASSWORD_HASH,再运行受保护的 Agent 脚本。'; } return '在仓库外导出 GPT_IMAGE_AGENT_TOKEN 或 GPT_IMAGE_APP_PASSWORD_HASH,再运行受保护的 Agent 脚本。'; } function sourceHasSetVariable(source, name) { return source.exists && source.variables.some((item) => item.name === name && item.set === true); } function summarizeEnvSource(source, cwd) { return { path: source.path.startsWith(`${cwd}/`) ? source.path.slice(cwd.length + 1) : source.path, exists: source.exists, variable_count: source.variable_count || 0, configured: source.variables .filter((item) => item.set) .map((item) => ({ name: item.name, sensitive: item.sensitive, value_kind: item.value_kind })) }; } function summarizeService(service) { const capabilities = service.capabilities || {}; const runtime = service.runtime; return { ok: service.ok, capabilities: { ok: capabilities.ok === true, endpoint: capabilities.endpoint, ...(capabilities.status ? { status: capabilities.status } : {}), ...(capabilities.error ? { error: capabilities.error } : {}), ...(capabilities.ok ? summarizeCapabilitiesBody(capabilities.body) : {}) }, ...(runtime ? { runtime: { ok: runtime.ok === true, endpoint: runtime.endpoint, ...(runtime.status ? { status: runtime.status } : {}), ...(runtime.error ? { error: runtime.error } : {}), ...(runtime.ok ? summarizeRuntimeBody(runtime.body) : {}) } } : {}) }; } function summarizeCapabilitiesBody(body) { const responsesRequirement = body?.supported?.image_backend_requirements?.['responses-image-generation']; return { auth_required: body?.auth?.required === true, auth_schemes: Array.isArray(body?.auth?.schemes) ? body.auth.schemes : [], page_sse_auth_required: body?.agent_streaming?.page_sse?.auth?.required === true, page_sse_auth_form_field: body?.agent_streaming?.page_sse?.auth?.form_field, state_backend: body?.defaults?.state_backend, image_storage_mode: body?.storage?.image_storage_mode, agent_jobs_supported: body?.agent_jobs?.supported === true, page_sse_supported: body?.agent_streaming?.page_sse?.supported === true, page_sse_declared_supported: body?.agent_streaming?.page_sse?.supported === true, page_sse_real_smoke: 'not_run_by_first_run', page_sse_real_smoke_status: buildNotRunSmokeStatus('first-run is non-billable and does not call /api/images'), upstream_sse_declared_supported: body?.agent_streaming?.upstream_sse?.supported === true, responses_image_backend_declared_supported: responsesRequirement?.supported === true, responses_image_backend_enabled: responsesRequirement?.enabled === true, responses_image_backend_real_smoke: 'not_run_by_first_run', responses_image_backend_real_smoke_status: buildNotRunSmokeStatus( 'first-run is non-billable and does not call Responses image generation' ) }; } function summarizeRuntimeBody(body) { return { default_streaming_strategy: body?.streaming?.defaultStrategy, streaming_batch_enabled: body?.streamingBatch?.enabled === true, recommended_concurrency: body?.streamingBatch?.recommendedConcurrency, channel_capacity_per_credential: body?.channelQueue?.capacityPerCredential, responses_image_backend_enabled: body?.responsesImageBackend?.enabled === true }; } function findCheck(checks, name) { return checks.find((check) => check.name === name) || { ok: false }; } function readNodeCheck() { return { ok: isSupportedNodeVersion(), current: process.version }; } function summarizeDependencyFailure(state) { return { reason: state.reason, ...(state.missingPackages?.length ? { missing_packages: state.missingPackages } : {}), ...(state.invalidPackages?.length ? { invalid_packages: state.invalidPackages } : {}), ...(state.nameMismatches?.length ? { name_mismatches: state.nameMismatches } : {}), ...(state.versionMismatches?.length ? { version_mismatches: state.versionMismatches } : {}), ...(state.rootLockMismatches?.length ? { root_lock_mismatches: state.rootLockMismatches } : {}), ...(state.hiddenLockMismatches?.length ? { hidden_lock_mismatches: state.hiddenLockMismatches } : {}) }; } function readPackageJson(cwd) { try { return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); } catch { return {}; } } function isCapabilitiesBody(body) { return Boolean(body && typeof body === 'object' && body.supported && body.defaults); } function authHeaders(env) { if (env.GPT_IMAGE_AGENT_TOKEN) return { Authorization: `Bearer ${env.GPT_IMAGE_AGENT_TOKEN}` }; if (env.GPT_IMAGE_APP_PASSWORD_HASH) return { 'X-App-Password-Hash': env.GPT_IMAGE_APP_PASSWORD_HASH }; return {}; } function readBaseUrlValidationError(base) { return base.error; } function resolveFirstRunBaseUrl(explicitBaseUrl, env) { try { return resolvePlaygroundBaseUrl(explicitBaseUrl, env); } catch (error) { return { baseUrl: '', source: explicitBaseUrl ? 'user_provided' : env.GPT_IMAGE_PLAYGROUND_URL ? 'GPT_IMAGE_PLAYGROUND_URL' : 'default_local_probe', interactive_confirmation_required: explicitBaseUrl ? false : true, error: error instanceof Error ? error.message : String(error) }; } } export function formatFirstRunText(report) { const lines = []; lines.push(`首次配置检查:${report.ok ? '通过' : '需要处理'}`); if (report.service_base_url) { lines.push(`服务地址:${report.service_base_url}(${formatServiceSource(report.service_base_url_source)})`); } else { lines.push(`服务地址:无效(${readCheckError(report, 'service_base_url_valid') || 'unknown'})`); } lines.push(`交互式确认:${report.interactive_confirmation_required ? '需要' : '不需要'}`); lines.push(`当前进程鉴权:${formatAuthState(report.agent_auth_process)}`); lines.push(`服务鉴权:${formatServiceAuthState(report.agent_auth_service)}`); lines.push(`私有 Agent env:${formatPrivateEnvState(report.private_agent_env)}`); lines.push(''); lines.push('检查项:'); for (const check of report.checks) { lines.push(`- ${formatCheckLabel(check.name)}:${formatCheckStatus(check)}`); } lines.push(''); lines.push('服务摘要:'); lines.push(`- 能力:${formatEndpoint(report.service?.capabilities)}`); lines.push(`- 运行时:${formatEndpoint(report.service?.runtime)}`); const capability = report.service?.capabilities || {}; if (capability.ok) { lines.push(`- 鉴权:${capability.auth_required ? capability.auth_schemes.join(',') || '需要' : '不需要'}`); lines.push( `- 页面 SSE 鉴权:${capability.page_sse_auth_required ? `需要 ${capability.page_sse_auth_form_field || 'passwordHash'}` : '不需要'}` ); lines.push( `- 页面 SSE:声明${capability.page_sse_declared_supported ? '支持' : '未支持'},实测=${formatSmokeState(capability.page_sse_real_smoke)}` ); lines.push( `- Responses 后端:声明${capability.responses_image_backend_declared_supported ? '支持' : '未支持'},启用=${capability.responses_image_backend_enabled ? '是' : '否'},实测=${formatSmokeState(capability.responses_image_backend_real_smoke)}` ); lines.push( `- 状态后端:${capability.state_backend || '未知'},图片存储:${capability.image_storage_mode || '未知'}` ); } const runtime = report.service?.runtime || {}; if (runtime.ok) { lines.push( `- 并发建议:推荐=${runtime.recommended_concurrency ?? '未知'},每凭证容量=${runtime.channel_capacity_per_credential ?? '未知'}` ); } lines.push(''); lines.push('下一步:'); if (report.next_actions.length === 0) { lines.push('- 无'); } else { for (const action of report.next_actions) lines.push(`- ${action}`); } return `${lines.join('\n')}\n`; } function formatCheckStatus(check) { if (check.skipped) return '已跳过'; if (check.ok) return '通过'; if (check.status) return `失败(${check.status})`; if (check.error) return `失败(${check.error})`; return '失败'; } function formatEndpoint(endpoint) { if (!endpoint) return '已跳过'; if (endpoint.ok) return `通过 ${endpoint.status || ''}`.trim(); return `失败${endpoint.status ? ` ${endpoint.status}` : ''}${endpoint.error ? ` ${endpoint.error}` : ''}`; } function formatSmokeState(value) { const states = { not_run_by_first_run: '未执行真实 smoke', skipped: '已跳过', passed: '通过', failed: '失败' }; return states[value] || value || '未知'; } function buildNotRunSmokeStatus(reason) { return { state: 'not_run', billable: false, reason }; } function formatAuthState(auth) { if (!auth) return '未知'; if (auth.has_token && auth.has_password_hash) return '已加载 token + 访问码哈希'; if (auth.has_token) return '已加载 token'; if (auth.has_password_hash) return '已加载访问码哈希'; return '未加载'; } function formatServiceAuthState(auth) { if (!auth) return '未知'; if (auth.required === true) return auth.ready ? '已满足当前服务鉴权要求' : '未满足当前服务鉴权要求'; return '不需要'; } function formatPrivateEnvState(privateEnv) { if (!privateEnv?.exists) return '不存在'; if (privateEnv.has_token) return '存在,含 token'; if (privateEnv.has_password_hash) return '存在,含访问码哈希'; return '存在,未发现 Agent 鉴权变量'; } function readCheckError(report, name) { const check = report.checks.find((item) => item.name === name); return check?.error; } function formatServiceSource(source) { if (source === 'user_provided') return '用户提供'; if (source === 'GPT_IMAGE_PLAYGROUND_URL') return '环境变量 GPT_IMAGE_PLAYGROUND_URL'; if (source === 'default_local_probe') return '默认本地探测'; return source || '未知来源'; } function formatCheckLabel(name) { const labels = { node_version: 'Node.js 版本', npm_install_policy: 'npm 安装脚本策略', package_lock: 'package-lock.json', dependencies_installed: '依赖是否已安装', env_files: '环境文件', agent_auth_available_to_process: 'Agent 鉴权可用', page_sse_auth_available_to_process: '页面 SSE 鉴权可用', service_base_url_valid: '服务地址合法', service_reachable: '服务可达', agent_capabilities_contract: 'Agent capabilities 合同', runtime_capabilities_contract: 'runtime capabilities 合同', package_metadata: '包信息' }; return labels[name] || name; } function safeEndpoint(url) { try { return new URL(url).pathname; } catch { return String(url); } } function readOptionValue(argv, index, name) { const value = argv[index]; if (!value || value.startsWith('--')) throw new Error(`${name} 需要参数值。`); return value; } function readPositiveInteger(value, name) { if (!/^\d+$/.test(String(value))) throw new Error(`${name} 必须是正整数。`); const parsed = Number(value); if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${name} 必须是正整数。`); return parsed; } async function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { printHelp(); return; } const report = await buildFirstRunReport(options); if (options.json) { printJson(report); } else { process.stdout.write(formatFirstRunText(report)); } if (!report.ok) process.exit(1); } try { if (isMainModule(import.meta.url, process.argv[1])) await main(); } catch (error) { printJson({ ok: false, command: 'first-run', billable: false, error: error instanceof Error ? error.message : String(error) }); process.exit(1); }