From 569c8521f41f3e3d0ea3477d4324e6395ec39c6e Mon Sep 17 00:00:00 2001 From: dahetao <2732988078@qq.com> Date: Thu, 11 Dec 2025 11:13:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20debug=20=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E5=91=BD=E4=BB=A4=E8=A1=8C=E6=88=96=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F=E5=90=AF=E7=94=A8=E8=AF=A6=E7=BB=86=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 4 ++ src/api/client.js | 152 ++++++++++++++++++++++++++++++++++---------- src/server/index.js | 78 +++++++++++++++++++++-- src/utils/logger.js | 127 +++++++++++++++++++++++++++++++++++- 4 files changed, 323 insertions(+), 38 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3b2c2a4b..acf7e334 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,10 @@ services: - PANEL_PASSWORD=设置你的密码 - API_KEY=sk-text - IMAGE_BASE_URL=设置图像访问地址,如果是服务器部署有域名写域名,无域名写IP:8045,本地不写 + # DEBUG 调试日志级别(可选,默认关闭) + # - low: 显示客户端完整请求与响应 + # - high: 额外显示后端 API 请求与响应 + # - DEBUG=high volumes: - ./data:/app/data healthcheck: diff --git a/src/api/client.js b/src/api/client.js index 6f275668..9058d796 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -179,19 +179,19 @@ function detectEmbeddedError(body) { try { const parsed = typeof body === 'string' ? JSON.parse(body) : body; - + // 支持两种格式: // 1. { "error": { "code": 429, ... } } - 标准格式 // 2. { "code": 429, "status": "RESOURCE_EXHAUSTED", ... } - 直接格式 let errorObj = null; - + if (parsed?.error) { errorObj = parsed.error; } else if (parsed?.code || parsed?.status) { // 直接格式:{ "code": 429, "status": "RESOURCE_EXHAUSTED", "message": "..." } errorObj = parsed; } - + if (!errorObj) return null; const status = statusFromStatusText(errorObj.code || errorObj.status); @@ -348,9 +348,9 @@ function convertToToolCall(functionCall) { arguments: JSON.stringify(functionCall.args) } }; - } - - +} + + // 辅助函数:在保留原有结构的同时记录 thoughtSignature function convertToToolCallWithSignature(functionCall, thoughtSignature) { const toolCall = convertToToolCall(functionCall); @@ -438,14 +438,26 @@ export async function generateAssistantResponse(requestBody, token, callback) { const state = { toolCalls: [], usage: null, textAccumulator: { text: '', signature: null } }; let buffer = ''; // 缓冲区:处理跨 chunk 的不完整行 + let streamChunks = []; // 收集流式响应(用于 debug=high 日志) const processChunk = (chunk) => { buffer += chunk; + streamChunks.push(chunk); // 收集响应片段 const lines = buffer.split('\n'); buffer = lines.pop(); // 保留最后一行(可能不完整) lines.forEach(line => parseAndEmitStreamChunk(line, state, callback)); }; + // 记录后端请求 + const startTime = Date.now(); + log.backend({ + type: 'request', + url: config.api.url, + method: 'POST', + headers: buildHeaders(token), + body: requestBody + }); + try { await withRequesterFallback(async currentUseAxios => withRetry(async (currentToken) => { const headers = buildHeaders(currentToken); @@ -475,7 +487,22 @@ export async function generateAssistantResponse(requestBody, token, callback) { .onError(reject); }); }, token)); + + // 记录后端响应(成功) + log.backend({ + type: 'response', + status: 200, + durationMs: Date.now() - startTime, + body: streamChunks.join('') + }); } catch (error) { + // 记录后端响应(失败) + log.backend({ + type: 'response', + status: error?.status || 'Error', + durationMs: Date.now() - startTime, + body: error?.message || error + }); await handleApiError(error, token); } @@ -486,15 +513,28 @@ export async function getAvailableModels() { const token = await tokenManager.getToken(); if (!token) throw new Error('没有可用的token,请运行 npm run login 获取token'); + const headers = buildHeaders(token); + const requestBody = {}; + + // 记录后端请求 + const startTime = Date.now(); + log.backend({ + type: 'request', + url: config.api.modelsUrl, + method: 'POST', + headers, + body: requestBody + }); + try { const data = await withRequesterFallback(async currentUseAxios => withRetry(async (currentToken) => { - const headers = buildHeaders(currentToken); + const currentHeaders = buildHeaders(currentToken); if (currentUseAxios) { - return (await axios(buildAxiosConfig(config.api.modelsUrl, headers, {}))).data; + return (await axios(buildAxiosConfig(config.api.modelsUrl, currentHeaders, {}))).data; } - const response = await requester.antigravity_fetch(config.api.modelsUrl, buildRequesterConfig(headers, {})); + const response = await requester.antigravity_fetch(config.api.modelsUrl, buildRequesterConfig(currentHeaders, {})); const bodyText = await response.text(); const embeddedError = detectEmbeddedError(bodyText); @@ -510,6 +550,14 @@ export async function getAvailableModels() { return JSON.parse(bodyText); }, token)); + // 记录后端响应(成功) + log.backend({ + type: 'response', + status: 200, + durationMs: Date.now() - startTime, + body: data + }); + return { object: 'list', data: Object.keys(data.models).map(id => ({ @@ -520,39 +568,79 @@ export async function getAvailableModels() { })) }; } catch (error) { + // 记录后端响应(失败) + log.backend({ + type: 'response', + status: error?.status || 'Error', + durationMs: Date.now() - startTime, + body: error?.message || error + }); await handleApiError(error, token); } } // 内部复用的非流式请求封装,返回上游原始 JSON,方便不同上层按需解析 async function callNoStreamApi(requestBody, token) { - return await withRequesterFallback(async currentUseAxios => - withRetry(async (currentToken) => { - const headers = buildHeaders(currentToken); + const headers = buildHeaders(token); - if (currentUseAxios) { - return (await axios(buildAxiosConfig(config.api.noStreamUrl, headers, requestBody))).data; - } + // 记录后端请求 + const startTime = Date.now(); + log.backend({ + type: 'request', + url: config.api.noStreamUrl, + method: 'POST', + headers, + body: requestBody + }); - const response = await requester.antigravity_fetch( - config.api.noStreamUrl, - buildRequesterConfig(headers, requestBody) - ); - const bodyText = await response.text(); - const embeddedError = detectEmbeddedError(bodyText); + try { + const data = await withRequesterFallback(async currentUseAxios => + withRetry(async (currentToken) => { + const currentHeaders = buildHeaders(currentToken); - if (response.status !== 200 || embeddedError) { - throw { - status: embeddedError?.status ?? response.status, - message: embeddedError?.message ?? bodyText, - retryDelayMs: embeddedError?.retryDelayMs, - disableToken: embeddedError?.disableToken - }; - } + if (currentUseAxios) { + return (await axios(buildAxiosConfig(config.api.noStreamUrl, currentHeaders, requestBody))).data; + } - return JSON.parse(bodyText); - }, token) - ); + const response = await requester.antigravity_fetch( + config.api.noStreamUrl, + buildRequesterConfig(currentHeaders, requestBody) + ); + const bodyText = await response.text(); + const embeddedError = detectEmbeddedError(bodyText); + + if (response.status !== 200 || embeddedError) { + throw { + status: embeddedError?.status ?? response.status, + message: embeddedError?.message ?? bodyText, + retryDelayMs: embeddedError?.retryDelayMs, + disableToken: embeddedError?.disableToken + }; + } + + return JSON.parse(bodyText); + }, token) + ); + + // 记录后端响应(成功) + log.backend({ + type: 'response', + status: 200, + durationMs: Date.now() - startTime, + body: data + }); + + return data; + } catch (error) { + // 记录后端响应(失败) + log.backend({ + type: 'response', + status: error?.status || 'Error', + durationMs: Date.now() - startTime, + body: error?.message || error + }); + throw error; + } } export async function generateAssistantResponseNoStream(requestBody, token) { diff --git a/src/server/index.js b/src/server/index.js index 95daa501..b5ca7cd2 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -1426,8 +1426,8 @@ app.get('/admin/quota/all', requireApiKey, async (req, res) => { indexes && indexes.length > 0 ? indexes : accounts - .map((_, idx) => idx) - .filter(idx => accounts[idx]?.enable !== false); + .map((_, idx) => idx) + .filter(idx => accounts[idx]?.enable !== false); if (targetIndexes.length === 0) { return res.status(404).json({ error: '没有匹配的启用凭证' }); @@ -1566,6 +1566,23 @@ const createChatCompletionHandler = (resolveToken, options = {}) => async (req, } } }); + // 同时输出到控制台详细日志 + if (logger.detail) { + logger.detail({ + method: req.method, + path: req.originalUrl, + status, + durationMs: Date.now() - startedAt, + request: requestSnapshot, + response: { + status, + headers: res.getHeaders ? res.getHeaders() : undefined, + body: responseBodyForLog, + modelOutput: responseSummaryForLog + }, + error: success ? undefined : message + }); + } }; try { if (!messages) { @@ -1768,7 +1785,7 @@ app.post('/v1beta/models/:model\\:generateContent', async (req, res) => { let token = null; let responseBodyForLog = null; - const writeLog = ({ success, status, message }) => + const writeLog = ({ success, status, message }) => { appendLog({ timestamp: new Date().toISOString(), model, @@ -1788,6 +1805,23 @@ app.post('/v1beta/models/:model\\:generateContent', async (req, res) => { } } }); + // 同时输出到控制台详细日志 + if (logger.detail) { + logger.detail({ + method: req.method, + path: req.originalUrl, + status, + durationMs: Date.now() - startedAt, + request: requestSnapshot, + response: { + status, + headers: res.getHeaders ? res.getHeaders() : undefined, + body: responseBodyForLog + }, + error: success ? undefined : message + }); + } + }; try { const body = req.body || {}; @@ -1839,7 +1873,7 @@ app.post('/v1/messages/count_tokens', (req, res) => { const requestSnapshot = createRequestSnapshot(req); let responseBodyForLog = null; - const writeLog = ({ success, status, message }) => + const writeLog = ({ success, status, message }) => { appendLog({ timestamp: new Date().toISOString(), model: req.body?.model || 'unknown', @@ -1859,6 +1893,23 @@ app.post('/v1/messages/count_tokens', (req, res) => { } } }); + // 同时输出到控制台详细日志 + if (logger.detail) { + logger.detail({ + method: req.method, + path: req.originalUrl, + status, + durationMs: Date.now() - startedAt, + request: requestSnapshot, + response: { + status, + headers: res.getHeaders ? res.getHeaders() : undefined, + body: responseBodyForLog + }, + error: success ? undefined : message + }); + } + }; try { const result = countClaudeTokens(req.body || {}); @@ -1881,7 +1932,7 @@ app.post('/v1/messages', async (req, res) => { let openaiReq = null; let requestBody = null; - const writeLog = ({ success, status, message }) => + const writeLog = ({ success, status, message }) => { appendLog({ timestamp: new Date().toISOString(), model: openaiReq?.model || req.body?.model || 'unknown', @@ -1901,6 +1952,23 @@ app.post('/v1/messages', async (req, res) => { } } }); + // 同时输出到控制台详细日志 + if (logger.detail) { + logger.detail({ + method: req.method, + path: req.originalUrl, + status, + durationMs: Date.now() - startedAt, + request: requestSnapshot, + response: { + status, + headers: res.getHeaders ? res.getHeaders() : undefined, + body: responseBodyForLog + }, + error: success ? undefined : message + }); + } + }; try { openaiReq = mapClaudeToOpenAI(req.body || {}); diff --git a/src/utils/logger.js b/src/utils/logger.js index 31009b33..7a034a8d 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -20,11 +20,136 @@ function logRequest(method, path, status, duration, clientIP, userAgent) { console.log(`${colors.cyan}[${method}]${colors.reset} - ${path} ${statusColor}${status}${colors.reset} ${colors.gray}${duration}ms${colors.reset}${ipInfo}${uaInfo}`); } +const DebugLevel = { + OFF: 0, + LOW: 1, + HIGH: 2 +}; + +function getDebugLevel() { + // 1. 优先检查命令行参数 + const args = process.argv.slice(2); + const debugIndex = args.indexOf('-debug'); + + if (debugIndex !== -1) { + const nextArg = args[debugIndex + 1]; + if (nextArg === 'high') { + return DebugLevel.HIGH; + } + return DebugLevel.LOW; + } + + // 2. 检查环境变量 + const envDebug = process.env.DEBUG ? String(process.env.DEBUG).toLowerCase() : ''; + if (envDebug === 'high') { + return DebugLevel.HIGH; + } + if (['low', 'true', '1', 'on'].includes(envDebug)) { + return DebugLevel.LOW; + } + + return DebugLevel.OFF; +} + +const currentDebugLevel = getDebugLevel(); + +function logDetail(data) { + if (currentDebugLevel < DebugLevel.LOW) { + return; + } + + const { method, path, status, durationMs, request, response, error } = data; + const statusColor = status >= 500 ? colors.red : status >= 400 ? colors.yellow : colors.green; + + console.log('----------------------------------------------------'); + console.log(`${colors.cyan}[${method}]${colors.reset} ${path} ${statusColor}${status}${colors.reset} ${colors.gray}${durationMs}ms${colors.reset}`); + + if (error) { + console.log(`${colors.red}Error:${colors.reset} ${error}`); + } + + if (request) { + console.log(`${colors.cyan}Request Headers:${colors.reset}`); + console.log(JSON.stringify(request.headers || {}, null, 2)); + if (request.body) { + console.log(`${colors.cyan}Request Body:${colors.reset}`); + console.log(JSON.stringify(request.body, null, 2)); + } + } + + if (response) { + if (response.headers) { + // console.log(`${colors.green}Response Headers:${colors.reset}`); + // console.log(JSON.stringify(response.headers, null, 2)); + } + if (response.body || response.modelOutput) { + console.log(`${colors.green}Response Output:${colors.reset}`); + const out = response.modelOutput || response.body; + console.log(JSON.stringify(out, null, 2)); + } + } + console.log('----------------------------------------------------'); +} + +/** + * 记录后端 API 的请求和响应(仅 debug=high 时生效) + * @param {Object} data - 日志数据 + * @param {string} data.type - 'request' 或 'response' + * @param {string} data.url - 请求 URL + * @param {string} data.method - HTTP 方法 + * @param {Object} data.headers - 请求/响应头 + * @param {any} data.body - 请求/响应体 + * @param {number} data.status - 响应状态码(仅 response) + * @param {number} data.durationMs - 请求耗时(仅 response) + */ +function logBackend(data) { + if (currentDebugLevel < DebugLevel.HIGH) { + return; + } + + const { type, url, method, headers, body, status, durationMs } = data; + + console.log('==================== BACKEND ===================='); + + if (type === 'request') { + console.log(`${colors.yellow}[Backend Request]${colors.reset} ${colors.cyan}${method}${colors.reset} ${url}`); + if (headers) { + console.log(`${colors.yellow}Headers:${colors.reset}`); + // 隐藏敏感的 Authorization 头 + const safeHeaders = { ...headers }; + if (safeHeaders.Authorization) { + safeHeaders.Authorization = safeHeaders.Authorization.substring(0, 20) + '...[HIDDEN]'; + } + console.log(JSON.stringify(safeHeaders, null, 2)); + } + if (body) { + console.log(`${colors.yellow}Body:${colors.reset}`); + const bodyStr = typeof body === 'string' ? body : JSON.stringify(body, null, 2); + console.log(bodyStr); + } + } else if (type === 'response') { + const statusColor = status >= 500 ? colors.red : status >= 400 ? colors.yellow : colors.green; + console.log(`${colors.green}[Backend Response]${colors.reset} ${statusColor}${status}${colors.reset} ${colors.gray}${durationMs}ms${colors.reset}`); + if (body) { + console.log(`${colors.green}Body:${colors.reset}`); + const bodyStr = typeof body === 'string' ? body : JSON.stringify(body, null, 2); + console.log(bodyStr); + } + } + + console.log('=================================================='); +} + export const log = { info: (...args) => logMessage('info', ...args), warn: (...args) => logMessage('warn', ...args), error: (...args) => logMessage('error', ...args), - request: logRequest + request: logRequest, + detail: logDetail, + backend: logBackend, + level: currentDebugLevel, + DebugLevel }; export default log; +