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

相关推荐
花千树-01018 小时前
MCP 协议通信详解:从握手到工具调用的完整流程
ai·langchain·aigc·agent·ai agent·mcp
Rubin智造社18 小时前
安全先行·自主编程|Claude Code Opus 4.7深度解读:AI开发进入合规量产时代
人工智能·anthropic·claude opus 4.7·mythos preview·xhigh努力等级·/ultrareview命令·自主开发ai
xinlianyq18 小时前
全球 AI 芯片格局生变:英伟达主导训练,国产算力崛起推理
人工智能
ShineWinsu18 小时前
AI训练硬件指南:GPU算力梯队与任务匹配框架
人工智能
范桂飓18 小时前
精选 Skills 清单
人工智能
码农的日常搅屎棍19 小时前
AIAgent开发新选择:OpenHarness极简入门指南
人工智能
花千树-01019 小时前
内存(Memory)基础:ConversationBuffer、Summary Memory 等
agent·ai agent·上下文·长短期记忆·ai memory·ai 记忆压缩
AC赳赳老秦19 小时前
OpenClaw生成博客封面图+标题,适配CSDN视觉搜索,提升点击量
运维·人工智能·python·自动化·php·deepseek·openclaw
萝卜小白19 小时前
算法实习Day04-MinerU2.5-pro
人工智能·算法·机器学习
geneculture19 小时前
从人际间性到人机间性:进入人机互助新时代——兼论融智学视域下人类认知第二次大飞跃的理论奠基与实践场域
人工智能·融智学的重要应用·哲学与科学统一性·融智时代(杂志)·人际间性·人机间性·人际间文性