
如何用 Dual Output 让外部程序订阅 Qwen Code 交互会话的结构化事件流【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-codeDual Output 是 Qwen Code 交互 TUI 的一个旁路sidecar模式Qwen Code 在stdout上照常渲染 TUI 的同时把结构化的 JSON 事件流写到另一条独立通道上供外部程序——IDE 扩展、Web 前端、CI 流水线、自动化脚本——订阅。它还提供一条反向通道外部程序向一个被 TUI 监视的文件写入 JSONL 命令就可以像人坐在键盘前一样提交 prompt、响应工具权限请求。整个功能完全可选不带任何 Dual Output 参数时TUI 行为与之前完全一致没有额外 I/O。本文以「让一个外部 Node/Shell 程序订阅会话事件流并反向注入命令」为目标走完从选通道、启动、验收到处理异常的全程。前提是本机已安装并可以使用qwen命令行README 中给出的安装方式之一npm install -g qwen-code/qwen-codelatest需要 Node.js 22 或更高版本。先选输出通道--json-fd还是--json-fileDual Output 的三个参数如下参数类型用途--json-fd n数字n 3把 JSON 事件写到文件描述符n。调用方必须通过 spawn 的stdio配置或 shell 重定向提供该 fd--json-file path路径把 JSON 事件写到文件。路径可以是普通文件、FIFO命名管道或/dev/fd/N--input-file path路径监视该文件接收外部程序写入的 JSONL 命令--json-fd与--json-file互斥fd 0、1、2 会被拒绝防止破坏 TUI 自己的输出。两者怎么选取决于你的外部程序如何托管 TUI嵌入方式使用child_process.spawn 普通stdio--json-fdnode-pty/bun-pty/ 任何 PTY 宿主--json-fileShell 重定向 / 手动管道测试两者皆可CI 日志收集普通文件退出后读取--json-file同主机上追求最低延迟--json-file FIFO文档给出的判断规则很直接如果你需要 TUI 正确渲染就需要 PTY因此需要用--json-file。原因是 PTY 封装层node-pty等的 API 不接受stdio数组且底层forkpty(3)/login_tty会在exec前主动关闭父进程中所有 3的 fd——额外的 fd 无法被继承。而文件路径只是普通 CLI 参数能穿过任何 spawn 模型。--json-fd适合那种干脆丢弃 stdout 的纯程序化包装器。最短主路径普通文件 两个终端启动时同时打开输出与输入两条通道touch /tmp/qwen-events.jsonl /tmp/qwen-input.jsonl qwen \ --json-file /tmp/qwen-events.jsonl \ --input-file /tmp/qwen-input.jsonl在第二个终端里 tail 事件流确认通道已建立tail -f /tmp/qwen-events.jsonl事件通道上收到的第一个事件永远是system/session_startbridge 构造时发出用它把通道与 session id 关联起来再等待其它事件{ type: system, subtype: session_start }看到这条就说明订阅链路已通。之后在第三个终端向运行中的 TUI 注入一条 promptecho {type:submit,text:Explain this repo} /tmp/qwen-input.jsonl这条 prompt 会以用户亲键输入的方式出现在 TUI 中流式响应同步镜像到/tmp/qwen-events.jsonl。这就是「外部程序订阅 反向控制」的最小闭环。可选分支用 FIFO 降低事件输出延迟FIFO 没有磁盘 I/O当读写双方都在同一台主机上时延迟更低mkfifo /tmp/qwen-events.jsonl touch /tmp/qwen-input.jsonl qwen \ --json-file /tmp/qwen-events.jsonl \ --input-file /tmp/qwen-input.jsonl # TUI 立即可启动 —— 不需要先启动 reader # 第二个终端随时连接 cat /tmp/qwen-events.jsonlbridge 以O_RDWR | O_NONBLOCK打开 FIFO所以没有 reader 时也不会阻塞事件先缓存在内核管道缓冲区。两个注意点--input-file只接受普通文件不接受 FIFO——watcher 依赖stat.size检测新数据而 FIFO 的 size 恒为 0。如果 FIFO 始终没有 reader内部缓冲区超过 1 MB 后 bridge 自动禁用TUI 继续正常运行。理解订阅到的事件流schema 与握手事件以 JSON Lines 输出每行一个对象schema 与非交互模式--output-formatstream-json相同且includePartialMessages恒为开启。协议版本 2 会把文本型tool_result.content在 JSON 序列化后限制在 65,536 个 UTF-8 字节以内超限值会变成确定性的头/尾预览——这是字段限制不是整个 JSONL 帧的大小限制。按生命周期你会依次看到这几类事件以下为文档给出的结构示例// 会话生命周期第一个事件用于关联 session id { type: system, subtype: session_start, uuid: ..., session_id: ..., data: { session_id: ..., cwd: /path/to/cwd } } // 进行中的 assistant 回合的流式事件 { type: stream_event, event: { type: message_start, message: { ... } }, ... } { type: stream_event, event: { type: content_block_delta, index: 0, delta: { type: text_delta, text: Hello } }, ... } { type: stream_event, event: { type: message_stop }, ... } // 完成的消息 { type: user, message: { role: user, content: [...] }, ... } { type: assistant, message: { role: assistant, content: [...], usage: { ... } }, ... } // 权限控制平面仅在工具需要审批时出现 { type: control_request, request_id: ..., request: { subtype: can_use_tool, tool_name: run_shell_command, tool_use_id: ..., input: { command: rm -rf /tmp/x }, permission_suggestions: null, blocked_path: null } } { type: control_response, response: { subtype: success, request_id: ..., response: { allowed: true } } }control_response无论审批决定是在 TUI 的原生确认界面做出的还是由外部confirmation_response做出的都会发出——所有观察者都能看到最终结果。订阅端还应把system/session_end当作干净退出信号如果 TUI 在session_end之前崩溃输出流会直接关闭下一次写入时表现为EPIPE两种路径都要处理。反向通道两种输入命令--input-file接受两种命令形态// 向 prompt 队列提交一条用户消息 { type: submit, text: What does this function do? } // 响应一条挂起的 control_request { type: confirmation_response, request_id: ..., allowed: true }行为上有几条对订阅端很关键的规则submit进入队列。如果 TUI 正在响应中命令会在 TUI 回到空闲状态时自动重试。confirmation_response立即分发、从不排队因为工具调用是阻塞的响应必须直达底层的onConfirm处理器。哪一侧先批准工具哪一侧生效另一侧迟到的响应会被无害地丢弃。无法解析为 JSON 的行会被记录日志并跳过不会让 watcher 停止。远程审批工具调用时一个可用的演练流程文档 POC 3# 终端 A —— 只观察 control_request mkfifo /tmp/qwen-out.jsonl touch /tmp/qwen-in.jsonl (cat /tmp/qwen-out.jsonl \ | jq -c select(.type control_request)) # 终端 B qwen --json-file /tmp/qwen-out.jsonl --input-file /tmp/qwen-in.jsonl # 让 Qwen 做一件需要审批的事例如run ls -la /tmp。 # 终端 A 会出现 control_request复制其中的 request_id然后在第三个终端 echo {type:confirmation_response,request_id:paste-id,allowed:true} \ /tmp/qwen-in.jsonl # TUI 的确认提示消失工具开始执行注意命令中的paste-id需要替换为你从control_request事件里复制到的实际request_id。如果回了一个未知request_idbridge 会在输出通道上发出一条control_response供消费者记录或重试{ type: control_response, response: { subtype: error, request_id: ..., error: unknown request_id (already resolved, cancelled, or never issued) } }程序化订阅Node 宿主进程示例对「父进程 spawn Qwen Code、tail 事件、按自己节奏注入 prompt」这一最真实的形态文档给出了两种 spawn 写法。fd 方式child_process.spawn不经过 PTYimport { spawn } from node:child_process; import { openSync } from node:fs; const eventsFd openSync(/tmp/qwen-events.jsonl, w); const child spawn( qwen, [--json-fd, 3, --input-file, /tmp/qwen-input.jsonl], { stdio: [inherit, inherit, inherit, eventsFd] }, );此时 TUI 仍持有用户终端的 stdio 0/1/2嵌入方在 fd 3 背后的文件上读结构化事件向/tmp/qwen-input.jsonl追加 JSONL 行来推命令。PTY 方式node-ptyTUI 需要正确渲染时import { spawn } from node-pty; const pty spawn( qwen, [ --json-file, /tmp/qwen-events.jsonl, --input-file, /tmp/qwen-input.jsonl, ], { cols: 120, rows: 40 }, );子进程自己打开事件文件写入嵌入方用fs.watch 增量读取 tail 同一路径。事件处理的骨架取自文档的demo-embedder.ts示例运行方式npx tsx demo-embedder.tsrl.on(line, (line) { if (!line.trim()) return; const ev JSON.parse(line); if (ev.type system ev.subtype session_start) { // 旧版本 Qwen Code 不保证发出 protocol_version先做特性探测 const v ev.data?.protocol_version ?? 0; if (ev.data?.supported_events?.includes(control_request)) { console.log([embedder] permission control-plane available); } } if (ev.type assistant) { console.log([embedder] assistant turn ended, tokens , ev.message.usage?.output_tokens); } if (ev.type system ev.subtype session_end) { console.log([embedder] session ended cleanly); } });// 2 秒后注入一条 prompt如同用户键入 setTimeout(() { appendFileSync( input, JSON.stringify({ type: submit, text: hello from embedder }) \n, ); }, 2000);完整的可运行 demoPOC 1–7都在 dual-output 文档 中从「只看事件流」到「失败演练」逐级递进可直接复制执行。settings.json 配置长驻嵌入方对长期运行的嵌入方每次启动都穿 CLI 参数并不方便。同一套通道可以写进settings.json的顶层dualOutput键// ~/.qwen/settings.json用户级 // 或 workspace/.qwen/settings.json工作区级 { dualOutput: { jsonFile: /tmp/qwen-events.jsonl, inputFile: /tmp/qwen-input.jsonl, }, }优先级规则CLI 参数优先于 settings命令行传了--json-file /foo就会覆盖 settings 里的dualOutput.jsonFile。--json-fd没有 settings 等价项——fd 传递是 spawn 时机的问题无法静态声明。参数和 settings 都没有时Dual Output 保持关闭。dualOutput配置项带requiresRestart: true见 settingsSchemabridge 在启动时构造一次改动只在下次启动 Qwen Code 时生效。失败模式与延迟边界排查订阅端问题时按文档列出的失败模式对号坏 fd传给--json-fd的 fd 未打开或是 0/1/2——TUI 在stderr打印警告如Warning: dual output disabled — fd 9999 not openDual Output 不启用TUI 照常启动。坏路径--json-file的文件打不开——同样是stderr警告 无 Dual OutputTUI 照常启动。消费端断开通道另一端 reader 消失EPIPE时bridge 静默自禁用TUI 继续运行不重试。FIFO 缓冲溢出无 reader 的 FIFO 上事件先在内核管道Linux 约 64 KB和 Node.js WriteStream 中缓冲管道写满或内部缓冲超过 1 MB 后 bridge 自禁用并关闭 fd。这种情况下不会发出session_end——消费端应把「没有session_end的流关闭」视为异常终止。适配器异常事件发射过程中的任何异常都会被捕获、记录并禁用 bridgeDual Output 故障永远不会把 TUI 打崩。延迟方面--input-file用fs.watchFile以 500 ms 间隔轮询所以远程submit的最坏往返延迟约半秒——这是为了跨平台与文件系统包括 macOS / 网络挂载可移植而有意为之。输出通道没有轮询事件随 TUI 发出同步写入。多会话隔离时文档建议把每会话的文件路径放在$XDG_RUNTIME_DIR下或放在一个mkdtemp出来、权限0700的目录里。验证清单一次订阅链路算打通需要依次看到事件通道第一行是{ type: system, subtype: session_start }通过--input-file注入submit后TUI 出现该 prompt事件流中跟随assistant回合可带usage需要审批的操作触发control_request外部confirmation_response生效后通道出现control_response会话正常结束时收到system/session_end。完整协议细节、POC 脚本与更多嵌入场景IDE 扩展、浏览器 Chat 前端、CI 观察者、多 agent 编排、可观测性看板参见 docs/users/features/dual-output.md。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考