| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889 |
- #!/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 'thread/fork': {
- if (!threads.has(params.threadId)) return send({ id, error: { code: -32000, message: 'thread not found' } })
- const tid = 'f-' + (threads.size + 1); threads.add(tid)
- globalThis.lastConfig = params.config
- globalThis.forks = [...(globalThis.forks ?? []), params.threadId]
- return reply({ thread: { id: tid }, model: params.model })
- }
- 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 } })
- }
- })
|