fixtures-fake-codex.mjs 5.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. #!/usr/bin/env node
  2. // 模拟 codex app-server 的 JSON-RPC(每行一个 JSON),用于测试 CodexProvider
  3. import readline from 'node:readline'
  4. const send = m => process.stdout.write(JSON.stringify(m) + '\n')
  5. const pending = new Map()
  6. let sid = 1000
  7. const ask = (method, params) => new Promise(r => { const id = ++sid; pending.set(id, r); send({ id, method, params }) })
  8. const notify = (method, params) => send({ method, params })
  9. const threads = new Set()
  10. let loggedIn = false
  11. readline.createInterface({ input: process.stdin }).on('line', async line => {
  12. const msg = JSON.parse(line)
  13. if (msg.id !== undefined && !msg.method) { pending.get(msg.id)?.(msg.result); pending.delete(msg.id); return }
  14. const { id, method, params } = msg
  15. const reply = result => send({ id, result })
  16. switch (method) {
  17. case 'initialize': return reply({ userAgent: 'agent_server/9.9.9 (test)', codexHome: process.env.CODEX_HOME })
  18. case 'initialized': return
  19. case 'account/read': return reply({ account: loggedIn ? { type: 'chatgpt', email: 'me@example.com', planType: 'plus' } : null, requiresOpenaiAuth: true })
  20. case 'model/list': return reply({ data: [{ model: 'gpt-test', hidden: false }, { model: 'gpt-hidden', hidden: true }], nextCursor: null })
  21. case 'account/login/start':
  22. setTimeout(() => { loggedIn = true; notify('account/login/completed', { loginId: 'L1', success: true, error: null }) }, 100)
  23. return reply({ type: 'chatgptDeviceCode', loginId: 'L1', verificationUrl: 'https://auth.example.com/device', userCode: 'ABCD-1234' })
  24. case 'thread/start': {
  25. const tid = 't-' + (threads.size + 1); threads.add(tid)
  26. globalThis.lastConfig = params.config
  27. return reply({ thread: { id: tid }, model: params.model })
  28. }
  29. case 'thread/resume':
  30. if (!threads.has(params.threadId)) return send({ id, error: { code: -32000, message: 'thread not found' } })
  31. return reply({ thread: { id: params.threadId } })
  32. case 'turn/interrupt': return reply({})
  33. case 'skills/extraRoots/set': globalThis.extraRoots = params.extraRoots; return reply({})
  34. case 'skills/list': return reply({ data: [{ cwd: params.cwds[0], errors: [], skills: [
  35. { name: 'imagegen', description: '', scope: 'system', path: '/sys/imagegen/SKILL.md', enabled: true, pluginId: null },
  36. { name: 'skill-installer', description: '', scope: 'system', path: '/sys/skill-installer/SKILL.md', enabled: true, pluginId: null },
  37. ] }] })
  38. case 'turn/start': {
  39. const threadId = params.threadId, turnId = 'turn-1'
  40. reply({ turn: { id: turnId, status: 'inProgress' } })
  41. const text = params.input[0].text
  42. // 回显收到的权限配置,便于测试断言
  43. notify('item/completed', { threadId, turnId, item: { type: 'agentMessage', id: 'cfg', text: JSON.stringify(globalThis.lastConfig) } })
  44. if (text.includes('命令')) {
  45. notify('item/started', { threadId, turnId, item: { type: 'commandExecution', id: 'c1', command: 'ls -la', cwd: '/w', status: 'inProgress', aggregatedOutput: null, exitCode: null } })
  46. const r = await ask('item/commandExecution/requestApproval', { threadId, turnId, itemId: 'c1', command: 'ls -la', cwd: '/w', reason: '需要越过沙箱' })
  47. const ok = r.decision === 'accept' || r.decision === 'acceptForSession'
  48. 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 } })
  49. }
  50. if (text.includes('联网')) {
  51. const r = await ask('item/commandExecution/requestApproval', { threadId, turnId, itemId: 'c2', command: 'curl x', networkApprovalContext: { host: 'x' } })
  52. notify('item/completed', { threadId, turnId, item: { type: 'agentMessage', id: 'net', text: 'network:' + r.decision } })
  53. }
  54. if (text.includes('提问')) {
  55. 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: '' }] }] })
  56. notify('item/completed', { threadId, turnId, item: { type: 'agentMessage', id: 'ans', text: 'answer:' + JSON.stringify(r.answers) } })
  57. }
  58. if (text.includes('等待')) return // 不结束,等中断
  59. notify('item/agentMessage/delta', { threadId, turnId, itemId: 'm1', delta: '完' })
  60. notify('item/agentMessage/delta', { threadId, turnId, itemId: 'm1', delta: '成' })
  61. notify('item/completed', { threadId, turnId, item: { type: 'agentMessage', id: 'm1', text: '完成' } })
  62. const b = (i, o) => ({ totalTokens: i + o, inputTokens: i, cachedInputTokens: 0, cacheWriteInputTokens: 0, outputTokens: o, reasoningOutputTokens: 0 })
  63. notify('thread/tokenUsage/updated', { threadId, turnId, tokenUsage: { total: b(150, 30), last: b(50, 10), modelContextWindow: null } })
  64. notify('thread/tokenUsage/updated', { threadId, turnId, tokenUsage: { total: b(220, 45), last: b(70, 15), modelContextWindow: null } })
  65. notify('turn/completed', { threadId, turn: { id: turnId, status: 'completed', error: null } })
  66. return
  67. }
  68. default: return send({ id, error: { code: -32601, message: 'unknown ' + method } })
  69. }
  70. })