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时间戳)

相关推荐
奔跑中的小象5 小时前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南5 小时前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang5 小时前
LangGraph 状态机实战
人工智能
刹那芳华19925 小时前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作5 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门5 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜5 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟6 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能
天国梦6 小时前
自习室智能化升级避坑指南:天学网AI智习室方案实测与选型建议
大数据·人工智能
阿里云云原生6 小时前
可用性从 99.9% 跃升至 99.995%:畅捷通如何用 AI 重塑运维底座?
运维·网络·人工智能