#!/usr/bin/env node // 模拟 codex app-server 的 JSON-RPC(每行一个 JSON),用于测试 CodexProvider import readline from 'node:readline' const send = m => process.stdout.write(JSON.stringify(m) + '\n') const pending = new Map() let sid = 1000 const ask = (method, params) => new Promise(r => { const id = ++sid; pending.set(id, r); send({ id, method, params }) }) const notify = (method, params) => send({ method, params }) const threads = new Set() let loggedIn = false readline.createInterface({ input: process.stdin }).on('line', async line => { const msg = JSON.parse(line) if (msg.id !== undefined && !msg.method) { pending.get(msg.id)?.(msg.result); pending.delete(msg.id); return } const { id, method, params } = msg const reply = result => send({ id, result }) switch (method) { case 'initialize': return reply({ userAgent: 'agent_server/9.9.9 (test)', codexHome: process.env.CODEX_HOME }) case 'initialized': return case 'account/read': return reply({ account: loggedIn ? { type: 'chatgpt', email: 'me@example.com', planType: 'plus' } : null, requiresOpenaiAuth: true }) case 'model/list': return reply({ data: [{ model: 'gpt-test', hidden: false }, { model: 'gpt-hidden', hidden: true }], nextCursor: null }) case 'account/login/start': setTimeout(() => { loggedIn = true; notify('account/login/completed', { loginId: 'L1', success: true, error: null }) }, 100) return reply({ type: 'chatgptDeviceCode', loginId: 'L1', verificationUrl: 'https://auth.example.com/device', userCode: 'ABCD-1234' }) case 'thread/start': { const tid = 't-' + (threads.size + 1); threads.add(tid) globalThis.lastConfig = params.config return reply({ thread: { id: tid }, model: params.model }) } case 'thread/resume': if (!threads.has(params.threadId)) return send({ id, error: { code: -32000, message: 'thread not found' } }) return reply({ thread: { id: params.threadId } }) case 'turn/interrupt': return reply({}) case 'skills/extraRoots/set': globalThis.extraRoots = params.extraRoots; return reply({}) case 'skills/list': return reply({ data: [{ cwd: params.cwds[0], errors: [], skills: [ { name: 'imagegen', description: '', scope: 'system', path: '/sys/imagegen/SKILL.md', enabled: true, pluginId: null }, { name: 'skill-installer', description: '', scope: 'system', path: '/sys/skill-installer/SKILL.md', enabled: true, pluginId: null }, { name: 'google-drive:google-docs', description: '', scope: 'user', path: '/plugins/google-drive/skills/google-docs/SKILL.md', enabled: true, pluginId: 'google-drive@openai-curated-remote' }, ] }] }) case 'plugin/list': case 'plugin/installed': { const p = (name, installed, extra = {}) => ({ id: `${name}@openai-curated-remote`, name, installed, enabled: installed, installPolicy: 'AVAILABLE', availability: 'AVAILABLE', source: { type: 'remote' }, interface: { displayName: name.toUpperCase(), shortDescription: `${name} desc`, category: 'Productivity', developerName: 'OpenAI' }, ...extra }) const plugins = method === 'plugin/installed' ? [p('gmail', globalThis.gmail ?? true)] : [p('gmail', globalThis.gmail ?? true), p('notion', false, { interface: { displayName: 'Notion', shortDescription: 'docs', category: 'Docs' } }), p('blocked', false, { availability: 'DISABLED_BY_ADMIN' })] return reply({ marketplaces: [{ name: 'openai-curated-remote', path: null, interface: null, plugins }, { name: 'other', path: '/x', interface: null, plugins: [p('other-plugin', false)] }], marketplaceLoadErrors: [], featuredPluginIds: [] }) } case 'plugin/install': globalThis.installed = params; return reply({ authPolicy: 'ON_USE', appsNeedingAuth: [{ id: 'connector_n', name: 'Notion', description: null, installUrl: 'https://chatgpt.com/apps/notion', category: null }] }) case 'plugin/uninstall': globalThis.gmail = false; return reply({}) case 'turn/start': { const threadId = params.threadId, turnId = 'turn-1' reply({ turn: { id: turnId, status: 'inProgress' } }) const text = params.input[0].text // 回显收到的权限配置,便于测试断言 notify('item/completed', { threadId, turnId, item: { type: 'agentMessage', id: 'cfg', text: JSON.stringify(globalThis.lastConfig) } }) if (text.includes('命令')) { notify('item/started', { threadId, turnId, item: { type: 'commandExecution', id: 'c1', command: 'ls -la', cwd: '/w', status: 'inProgress', aggregatedOutput: null, exitCode: null } }) const r = await ask('item/commandExecution/requestApproval', { threadId, turnId, itemId: 'c1', command: 'ls -la', cwd: '/w', reason: '需要越过沙箱' }) const ok = r.decision === 'accept' || r.decision === 'acceptForSession' notify('item/completed', { threadId, turnId, item: { type: 'commandExecution', id: 'c1', command: 'ls -la', cwd: '/w', status: ok ? 'completed' : 'declined', aggregatedOutput: ok ? 'a.csv' : '', exitCode: ok ? 0 : null } }) } if (text.includes('联网')) { const r = await ask('item/commandExecution/requestApproval', { threadId, turnId, itemId: 'c2', command: 'curl x', networkApprovalContext: { host: 'x' } }) notify('item/completed', { threadId, turnId, item: { type: 'agentMessage', id: 'net', text: 'network:' + r.decision } }) } if (text.includes('提问')) { const r = await ask('item/tool/requestUserInput', { threadId, turnId, itemId: 'q1', isBlocking: true, autoResolutionMs: null, questions: [{ id: 'chart', header: '图表', question: '用哪种图?', isOther: true, isSecret: false, options: [{ label: '柱状图', description: '' }, { label: '折线图', description: '' }] }] }) notify('item/completed', { threadId, turnId, item: { type: 'agentMessage', id: 'ans', text: 'answer:' + JSON.stringify(r.answers) } }) } if (text.includes('应用')) { const r = await ask('mcpServer/elicitation/request', { threadId, turnId, serverName: 'codex_apps', mode: 'form', _meta: null, message: '允许调用 Gmail 的 search_emails?', requestedSchema: { type: 'object', properties: {} } }) notify('item/completed', { threadId, turnId, item: { type: 'agentMessage', id: 'app', text: 'app:' + r.action } }) } if (text.includes('等待')) return // 不结束,等中断 notify('item/agentMessage/delta', { threadId, turnId, itemId: 'm1', delta: '完' }) notify('item/agentMessage/delta', { threadId, turnId, itemId: 'm1', delta: '成' }) notify('item/completed', { threadId, turnId, item: { type: 'agentMessage', id: 'm1', text: '完成' } }) const b = (i, o) => ({ totalTokens: i + o, inputTokens: i, cachedInputTokens: 0, cacheWriteInputTokens: 0, outputTokens: o, reasoningOutputTokens: 0 }) notify('thread/tokenUsage/updated', { threadId, turnId, tokenUsage: { total: b(150, 30), last: b(50, 10), modelContextWindow: null } }) notify('thread/tokenUsage/updated', { threadId, turnId, tokenUsage: { total: b(220, 45), last: b(70, 15), modelContextWindow: null } }) notify('turn/completed', { threadId, turn: { id: turnId, status: 'completed', error: null } }) return } default: return send({ id, error: { code: -32601, message: 'unknown ' + method } }) } })