【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
背景
上篇 blog
【Agent】【OpenCode】本地代理增强版分析(Unix时间戳)
继续分析了本地代理中,流式响应的处理就是直接存原始文本,因为流式响应本质是多行文本(SSE 格式),无法直接转成单个 JSON 对象,直接保存原始字符串就可以完整记录所有data事件,然后是保存日志文件,提到这里的文件名用时间戳来确保唯一性,用的是 Unix 时间戳:从 1970 年 1 月 1 日 00:00:00 UTC 开始,到当前时刻所经过的毫秒数,提到选择 1970 年 1 月 1 日,是计算机领域的历史约定,源于 Unix 操作系统的设计,1970s 年初,Unix 开发者需要一个统一的时间起点,这是个全球统一,时区无关的时间基准,然后分析了其数据溢出的可能性,分析了 JavaScript 的毫秒时间戳要到公元 28 万年才会超出安全整数范围,当前是公元 2026 年,不会发生整数溢出,下面继续分析
OpenCode
上篇 blog 分析了日志文件是用 Unix 时间戳命名,下面继续分析
logEntry写入日志文件后,其内容大致如下
{timestamp:1712345678901,request:{/* 客户端原始请求 */},response:{/* 解析后的响应或原始字符串 */},error:null// 如果有错误会在这里记录}最后是结束客户端响应
res.end();这里会通知 OpenCode 客户端,DashScope 服务器的响应已全部转发完毕,这是 HTTP 协议要求的最终结束信号,这里面有个关键细节,在proxyRes.on('data', ...)事件中,数据已经通过res.write(chunk)逐块转发了响应内容,这里res.end()只是关闭连接,不传输任何新数据
举个例子,OpenCode 客户端通过非流式进行请求
{"model":"qwen-plus","messages":[{"role":"user","content":"你好"}],"stream":false}此时代理会记录下日志文件内容
{"timestamp":1712345678901,"request":{"model":"qwen-plus","messages":[{"role":"user","content":"你好"}],"stream":false},"response":{"id":"chatcmpl-123","choices":[{"message":{"content":"你好!有什么问题吗?"}}]},"error":null}另外可以看到,本地代理并没有在data事件中直接结束响应,而是等到了end事件,有如下几个原因
- HTTP 要求:必须等待所有数据接收完毕后,才能结束响应
- 流式场景:需要持续转发 data 块直到结束,中途不能直接结束响应
- 错误处理:如果中途出错,比如网络中断,得走 error 分支而非 end 分支
OK,最后再解释一个点,无论是超时事件timeout监测,还是error事件监测,都对res.headerSent做了判断
这里是防止本地代理重复发送 HTTP 响应头部信息,不管是timeout事件,还是error事件,本地代理都可能已经向客户端发送了响应头信息,比如典型的请求转发流程如下
- OpenCode 客户端 → 本地代理
- 本地代理 → DashScope 服务器(通过
proxyReq) - DashScope 服务器返回响应头信息 → 本地代理立刻转发给 OpenCode 客户端(通过
res.writeHead(...)) - 之后,开始传输响应内容(通过
data事件)
此时关键点来了,一旦执行了res.writeHead(...)(在proxyRes的回调里),res.headersSent就变成了true,但如果在后续接收响应内容的过程中发生网络错误,比如连接中断,TLS 错误,目标服务器一直没响应,就会触发proxyReq.on('error')或者proxyReq.on('timeout'),而此时客户端已经收到了响应头(比如 200 OK),那么此时不能再调用res.writeHead(502)或者是res.writeHead(504),否则会抛出异常,Node.js 会 crash 崩溃(也可能 warning)
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog
【Agent】【OpenCode】代理日志解析(整体结构)