50、【Agent】【OpenCode】本地代理增强版分析(超时机制实现)

【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除

背景

上篇 blog
【Agent】【OpenCode】本地代理增强版分析(setTimeout)

分析了如果请求没有得到响应,或者对于高并发场景,会有风险,提到从可靠性角度,本地代理可以添加 proxyReq.setTimeout,防止接收处理函数 proxyReq 收不到目标处理器的响应,导致内存泄漏,然后详细分析了 proxyReq.setTimeout 的函数原型,其中 setTimeout() 设置的是整个请求生命周期的超时,Node.js 会在底层 socket 上设置定时器,如果在指定时间内没有任何网络活动,会触发 timeout 事件,如果提供了回调函数,则执行回调函数,另外,触发 timeout 事件不会自动终止请求!需要用户手动调用 .destroy(),接着对 setTimeout 使用方式进行了举例,下面继续分析

OpenCode

综合前面 blog 分析,下面给出日志记录增强版 + 超时保护如下

javascript 复制代码
// dashscope-proxy.js (日志记录增强版 + 超时保护)

const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');

// 日志目录(可自定义)
const LOG_DIR = path.join(__dirname, 'logs');
if (!fs.existsSync(LOG_DIR)) {
  fs.mkdirSync(LOG_DIR, { recursive: true });
}

// 设置代理请求超时时间(毫秒)------ 建议 30~120 秒
const PROXY_TIMEOUT_MS = 60_000; // 60秒

const server = http.createServer((req, res) => {
  console.log(`📥 Received ${req.method} ${req.url}`);

  if (req.method === 'POST' && req.url === '/v1/chat/completions') {
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
      // 从原始请求中获取 Authorization 头
      const authHeader = req.headers['authorization'] || '';
      // 解析请求体
      let parsedBody = null;
      try {
        parsedBody = JSON.parse(body);
      } catch (e) {
        console.error('⚠️ Invalid JSON in request body');
        res.writeHead(400);
        res.end('Bad Request');
        return;
      }

      const timestamp = Date.now();
      const logEntry = {
        timestamp,
        request: parsedBody,
        response: null, // 占位
        error: null
      };

      const options = {
        hostname: 'dashscope.aliyuncs.com',
        port: 443,
        path: '/compatible-mode/v1/chat/completions',
        method: 'POST',
        headers: {
          'Authorization': authHeader, // 直接透传!
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(body)
        }
      };

      const proxyReq = https.request(options, (proxyRes) => {
        res.writeHead(proxyRes.statusCode, proxyRes.headers);

        let responseBody = '';
        proxyRes.on('data', (chunk) => {
          responseBody += chunk.toString();
          res.write(chunk); // 透传给客户端
        });

        proxyRes.on('end', () => {
          // 尝试解析响应(非流式)
          if (!parsedBody.stream) {
            try {
              logEntry.response = JSON.parse(responseBody);
            } catch (e) {
              logEntry.response = responseBody; // 原始字符串(如错误信息)
            }
          } else {
            // 流式响应:只记录原始 chunks(每行一个 data: {...})
            logEntry.response = responseBody;
          }

          // 保存日志到文件
          const logFile = path.join(LOG_DIR, `${timestamp}.json`);
          fs.writeFile(logFile, JSON.stringify(logEntry, null, 2), (err) => {
            if (err) console.error('Failed to write log file:', err.message);
            else console.log(`📝 Saved log to ${logFile}`);
          });

          // 结束客户端 HTTP 响应!
          res.end();
        });
      });

      // 设置超时
      proxyReq.setTimeout(PROXY_TIMEOUT_MS, () => {
        const timeoutMsg = `Proxy request timed out after ${PROXY_TIMEOUT_MS}ms`;
        console.warn(`⏰ ${timeoutMsg}`);

        // 记录超时错误
        logEntry.error = timeoutMsg;

        const logFile = path.join(LOG_DIR, `${timestamp}_timeout.json`);
        fs.writeFile(logFile, JSON.stringify(logEntry, null, 2), (err) => {
          if (err) console.error('Failed to write timeout log:', err.message);
        });

        // 必须 destroy!否则连接会挂起
        proxyReq.destroy();

        // 如果客户端还没收到响应,返回 504
        if (!res.headersSent) {
          res.writeHead(504, { 'Content-Type': 'text/plain' });
          res.end('Gateway Timeout');
        }
      });

      proxyReq.on('error', (e) => {
        const errorMsg = e.message || 'Unknown proxy error';
        logEntry.error = errorMsg;
        const logFile = path.join(LOG_DIR, `${timestamp}_error.json`);
        fs.writeFile(logFile, JSON.stringify(logEntry, null, 2), (err) => {
          if (err) console.error('Failed to write error log:', err.message);
        });
        if (!res.headersSent) {
          res.writeHead(502);
          res.end('Bad Gateway');
        }
      });

      proxyReq.write(body);
      proxyReq.end();
    });
    return;
  }

  res.writeHead(404);
  res.end('Not Found');
});

server.listen(2048, '127.0.0.1', () => {
  console.log('✅ Qwen-Plus proxy with full OpenCode & Ollama compatibility running on http://127.0.0.1:2048');
  console.log(`⏱️  Proxy timeout set to ${PROXY_TIMEOUT_MS / 1000}s`);
});

与之前 blog 【Agent】【OpenCode】本地代理增强(日志记录) 相比,这里的代理加入了 setTimeout() 超时机制,防止代理请求长时间挂起(比如网络卡住,DashScope 无响应等),超时时主动终止请求并记录错误日志,避免内存泄漏或连接堆积

简单总结下与之前仅日志记录版本的不同点

  • proxyReq.setTimeout(...):设置 60 秒超时(可调)
  • 超时后自动执行 proxyReq.destroy(),主动断开连接,释放资源
  • 超时日志单独保存,文件名带后缀 _timeout.json,便于排查
  • 超时时,本地代理返回 504 Gateway Timeout,符合 HTTP 规范
  • 防止重复响应,本地代理先检查 !res.headersSent 再写头部信息
  • 控制台提示超时,方便调试

OK,下面继续分析,首先看 proxyRes.on('end', ...) 部分

可以对比之前 blog 【Agent】【OpenCode】本地代理(脚本实现) 中这里的实现,可以看到明显内容多了不少

相比于之前的 .pipe() 自动转发(无法获取中间数据),日志记录需要缓存所有的 chunk 块,所以只能手动拼接 ,在这里,proxyRes.on('end', ...) 是整个代理服务器中处理 DashScope 响应结束(end 事件)的核心逻辑,代理会在收到完整的 DashScope 响应后,智能解析响应内容(区分流式和非流式),保存好结构化日志,并向 OpenCode 客户端发送最终响应结束信号,下面详细分析下

  • parsedBody.streamparsedBody 是 OpenCode 客户端(注意不是 DashScope 服务器)发来的原始请求解析出的 JS 对象,如果请求中没有设置 stream: true,说明 OpenCode 客户端期望的是完整 JSON 响应(非流式),否则就是 SSE 流式响应(每行 data: {...} 格式)

非流式

javascript 复制代码
{"choices": [{"message": {"content": "Hello"}}]}

流式

javascript 复制代码
data: {"choices": [{"delta": {"content": "H"}}]}\n\n
data: {"choices": [{"delta": {"content": "i"}}]}\n\n
...

然后是非流式响应的处理,代理会尝试先解析 JSON 格式的字符串

javascript 复制代码
try {
  logEntry.response = JSON.parse(responseBody);
} catch (e) {
  logEntry.response = responseBody; // 保留原始字符串(如错误信息)
}

成功时,将响应存为结构化对象,方便后续分析日志,而失败时,则保留原始字符串,方便分析错误原因,比如 DashScope 返回 401 Unauthorized 时,可能返回 HTML 错误页,而非 JSON


OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog
【Agent】【OpenCode】本地代理增强版分析(Unix时间戳)

相关推荐
youcans_15 小时前
【HALCON机器视觉实战】专栏介绍
图像处理·人工智能·计算机视觉·halcon
火山引擎开发者社区15 小时前
火山引擎 veRoCE 获权威认证:IANA 官方为 veRoCE 分配专属 UDP 端口号 4794
人工智能
囫囵吞桃15 小时前
Agent出现LLM因为历史工具调用消息而误解工具调用方式的问题
llm·agent
飘落的数码折腾日记15 小时前
你的AI Agent可能正在“叛变“ | 5类真实威胁与四层防御
人工智能
放羊郎15 小时前
基于ORB-SLAM2算法的优化工作
人工智能·算法·计算机视觉
AI袋鼠帝16 小时前
字节的技术决心,都藏在这个动作里
人工智能
AI袋鼠帝16 小时前
企微又偷偷进化AI,并开始不对劲了..
人工智能
工业机器人销售服务16 小时前
2026 年,探索专业伯朗特机器人的奇妙世界
人工智能·机器人
摆烂大大王16 小时前
AI 日报|2026年5月9日:四部门力推AI与能源双向赋能,AI终端国标出台,中国大模型融资潮涌
人工智能
萑澈16 小时前
编程能力强和多模态模型的模型后训练
人工智能·深度学习·机器学习