feat: 集成阿里云日志服务(SLS)
- 添加 aliyun-log SDK 依赖 - 新增 sls-logger.js 模块,支持批量日志上报、静默降级 - 在四个 API 处理函数中集成请求日志记录 - 更新 .env.example 添加 SLS 配置示例
This commit is contained in:
@@ -5,3 +5,10 @@ FACTORY_API_KEY=your_factory_api_key_here
|
|||||||
|
|
||||||
# 方式2:使用refresh token自动刷新(次优先级)
|
# 方式2:使用refresh token自动刷新(次优先级)
|
||||||
DROID_REFRESH_KEY=your_refresh_token_here
|
DROID_REFRESH_KEY=your_refresh_token_here
|
||||||
|
|
||||||
|
# 阿里云日志服务配置
|
||||||
|
ALIYUN_ACCESS_KEY_ID=your_access_key_id
|
||||||
|
ALIYUN_ACCESS_KEY_SECRET=your_access_key_secret
|
||||||
|
ALIYUN_SLS_ENDPOINT=cn-hangzhou.log.aliyuncs.com
|
||||||
|
ALIYUN_SLS_PROJECT=your_project_name
|
||||||
|
ALIYUN_SLS_LOGSTORE=your_logstore_name
|
||||||
|
|||||||
173
package-lock.json
generated
173
package-lock.json
generated
@@ -1,19 +1,99 @@
|
|||||||
{
|
{
|
||||||
"name": "droid2api",
|
"name": "droid2api",
|
||||||
"version": "1.3.5",
|
"version": "1.3.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "droid2api",
|
"name": "droid2api",
|
||||||
"version": "1.3.5",
|
"version": "1.3.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"aliyun-log": "github:aliyun/aliyun-log-nodejs-sdk",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"https-proxy-agent": "^7.0.2",
|
"https-proxy-agent": "^7.0.2",
|
||||||
"node-fetch": "^3.3.2"
|
"node-fetch": "^3.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@protobufjs/aspromise": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/base64": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/codegen": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/eventemitter": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/fetch": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.1",
|
||||||
|
"@protobufjs/inquire": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/float": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/inquire": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/path": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/pool": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/utf8": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@types/long": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
|
||||||
|
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||||
@@ -36,6 +116,18 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/aliyun-log": {
|
||||||
|
"name": "@alicloud/log",
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "git+ssh://git@github.com/aliyun/aliyun-log-nodejs-sdk.git#f5c2ab9cf5e0c7d3edd2fa1a15fc7f0a9946cd05",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^2.6.8",
|
||||||
|
"httpx": "^2.1.2",
|
||||||
|
"kitx": "^1.2.1",
|
||||||
|
"protobufjs": "^6.8.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
@@ -502,6 +594,39 @@
|
|||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/httpx": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/httpx/-/httpx-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"debug": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/httpx/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/httpx/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
@@ -529,6 +654,18 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kitx": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/kitx/-/kitx-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-fhBqFlXd0GkKTB+8ayLfpzPUw+LHxZlPAukPNBD1Om7JMeInT+/PxCAf1yLagvD+VKoyWhXtJR68xQkX/a0wOQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -690,6 +827,32 @@
|
|||||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/protobufjs": {
|
||||||
|
"version": "6.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
|
||||||
|
"integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
|
"@protobufjs/base64": "^1.1.2",
|
||||||
|
"@protobufjs/codegen": "^2.0.4",
|
||||||
|
"@protobufjs/eventemitter": "^1.1.0",
|
||||||
|
"@protobufjs/fetch": "^1.1.0",
|
||||||
|
"@protobufjs/float": "^1.0.2",
|
||||||
|
"@protobufjs/inquire": "^1.1.0",
|
||||||
|
"@protobufjs/path": "^1.1.2",
|
||||||
|
"@protobufjs/pool": "^1.1.0",
|
||||||
|
"@protobufjs/utf8": "^1.1.0",
|
||||||
|
"@types/long": "^4.0.1",
|
||||||
|
"@types/node": ">=13.7.0",
|
||||||
|
"long": "^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pbjs": "bin/pbjs",
|
||||||
|
"pbts": "bin/pbts"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -931,6 +1094,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
|||||||
@@ -8,12 +8,17 @@
|
|||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "node server.js"
|
"dev": "node server.js"
|
||||||
},
|
},
|
||||||
"keywords": ["openai", "api", "proxy"],
|
"keywords": [
|
||||||
|
"openai",
|
||||||
|
"api",
|
||||||
|
"proxy"
|
||||||
|
],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"https-proxy-agent": "^7.0.2",
|
"https-proxy-agent": "^7.0.2",
|
||||||
"node-fetch": "^3.3.2"
|
"node-fetch": "^3.3.2",
|
||||||
|
"aliyun-log": "github:aliyun/aliyun-log-nodejs-sdk"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
25
routes.js
25
routes.js
@@ -9,6 +9,7 @@ import { AnthropicResponseTransformer } from './transformers/response-anthropic.
|
|||||||
import { OpenAIResponseTransformer } from './transformers/response-openai.js';
|
import { OpenAIResponseTransformer } from './transformers/response-openai.js';
|
||||||
import { getApiKey } from './auth.js';
|
import { getApiKey } from './auth.js';
|
||||||
import { getNextProxyAgent } from './proxy-manager.js';
|
import { getNextProxyAgent } from './proxy-manager.js';
|
||||||
|
import { logRequest as slsLogRequest } from './sls-logger.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ router.get('/v1/models', (req, res) => {
|
|||||||
// 标准 OpenAI 聊天补全处理函数(带格式转换)
|
// 标准 OpenAI 聊天补全处理函数(带格式转换)
|
||||||
async function handleChatCompletions(req, res) {
|
async function handleChatCompletions(req, res) {
|
||||||
logInfo('POST /v1/chat/completions');
|
logInfo('POST /v1/chat/completions');
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const openaiRequest = req.body;
|
const openaiRequest = req.body;
|
||||||
@@ -187,6 +189,7 @@ async function handleChatCompletions(req, res) {
|
|||||||
}
|
}
|
||||||
res.end();
|
res.end();
|
||||||
logInfo('Stream forwarded (common type)');
|
logInfo('Stream forwarded (common type)');
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
logError('Stream error', streamError);
|
logError('Stream error', streamError);
|
||||||
res.end();
|
res.end();
|
||||||
@@ -206,6 +209,7 @@ async function handleChatCompletions(req, res) {
|
|||||||
}
|
}
|
||||||
res.end();
|
res.end();
|
||||||
logInfo('Stream completed');
|
logInfo('Stream completed');
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
logError('Stream error', streamError);
|
logError('Stream error', streamError);
|
||||||
res.end();
|
res.end();
|
||||||
@@ -228,10 +232,12 @@ async function handleChatCompletions(req, res) {
|
|||||||
logResponse(200, null, data);
|
logResponse(200, null, data);
|
||||||
res.json(data);
|
res.json(data);
|
||||||
}
|
}
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Error in /v1/chat/completions', error);
|
logError('Error in /v1/chat/completions', error);
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: 'Internal server error',
|
error: 'Internal server error',
|
||||||
message: error.message
|
message: error.message
|
||||||
@@ -242,6 +248,7 @@ async function handleChatCompletions(req, res) {
|
|||||||
// 直接转发 OpenAI 请求(不做格式转换)
|
// 直接转发 OpenAI 请求(不做格式转换)
|
||||||
async function handleDirectResponses(req, res) {
|
async function handleDirectResponses(req, res) {
|
||||||
logInfo('POST /v1/responses');
|
logInfo('POST /v1/responses');
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const openaiRequest = req.body;
|
const openaiRequest = req.body;
|
||||||
@@ -340,8 +347,8 @@ async function handleDirectResponses(req, res) {
|
|||||||
if (claudeCodeTools.includes(tool.name)) return false;
|
if (claudeCodeTools.includes(tool.name)) return false;
|
||||||
// 过滤所有 mcp__ 开头的工具和 MCP 相关工具
|
// 过滤所有 mcp__ 开头的工具和 MCP 相关工具
|
||||||
return !tool.name.startsWith('mcp__') &&
|
return !tool.name.startsWith('mcp__') &&
|
||||||
!tool.name.includes('Mcp') &&
|
!tool.name.includes('Mcp') &&
|
||||||
!tool.name.includes('MCP');
|
!tool.name.includes('MCP');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,6 +394,7 @@ async function handleDirectResponses(req, res) {
|
|||||||
}
|
}
|
||||||
res.end();
|
res.end();
|
||||||
logInfo('Stream forwarded successfully');
|
logInfo('Stream forwarded successfully');
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/responses', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
logError('Stream error', streamError);
|
logError('Stream error', streamError);
|
||||||
res.end();
|
res.end();
|
||||||
@@ -396,10 +404,12 @@ async function handleDirectResponses(req, res) {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
logResponse(200, null, data);
|
logResponse(200, null, data);
|
||||||
res.json(data);
|
res.json(data);
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/responses', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Error in /v1/responses', error);
|
logError('Error in /v1/responses', error);
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/responses', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: 'Internal server error',
|
error: 'Internal server error',
|
||||||
message: error.message
|
message: error.message
|
||||||
@@ -410,6 +420,7 @@ async function handleDirectResponses(req, res) {
|
|||||||
// 直接转发 Anthropic 请求(不做格式转换)
|
// 直接转发 Anthropic 请求(不做格式转换)
|
||||||
async function handleDirectMessages(req, res) {
|
async function handleDirectMessages(req, res) {
|
||||||
logInfo('POST /v1/messages');
|
logInfo('POST /v1/messages');
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const anthropicRequest = req.body;
|
const anthropicRequest = req.body;
|
||||||
@@ -575,8 +586,8 @@ async function handleDirectMessages(req, res) {
|
|||||||
if (claudeCodeTools.includes(tool.name)) return false;
|
if (claudeCodeTools.includes(tool.name)) return false;
|
||||||
// 过滤所有 mcp__ 开头的工具和 MCP 相关工具
|
// 过滤所有 mcp__ 开头的工具和 MCP 相关工具
|
||||||
return !tool.name.startsWith('mcp__') &&
|
return !tool.name.startsWith('mcp__') &&
|
||||||
!tool.name.includes('Mcp') &&
|
!tool.name.includes('Mcp') &&
|
||||||
!tool.name.includes('MCP');
|
!tool.name.includes('MCP');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,6 +631,7 @@ async function handleDirectMessages(req, res) {
|
|||||||
}
|
}
|
||||||
res.end();
|
res.end();
|
||||||
logInfo('Stream forwarded successfully');
|
logInfo('Stream forwarded successfully');
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/messages', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
logError('Stream error', streamError);
|
logError('Stream error', streamError);
|
||||||
res.end();
|
res.end();
|
||||||
@@ -629,10 +641,12 @@ async function handleDirectMessages(req, res) {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
logResponse(200, null, data);
|
logResponse(200, null, data);
|
||||||
res.json(data);
|
res.json(data);
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/messages', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Error in /v1/messages', error);
|
logError('Error in /v1/messages', error);
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/messages', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: 'Internal server error',
|
error: 'Internal server error',
|
||||||
message: error.message
|
message: error.message
|
||||||
@@ -643,6 +657,7 @@ async function handleDirectMessages(req, res) {
|
|||||||
// 处理 Anthropic count_tokens 请求
|
// 处理 Anthropic count_tokens 请求
|
||||||
async function handleCountTokens(req, res) {
|
async function handleCountTokens(req, res) {
|
||||||
logInfo('POST /v1/messages/count_tokens');
|
logInfo('POST /v1/messages/count_tokens');
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const anthropicRequest = req.body;
|
const anthropicRequest = req.body;
|
||||||
@@ -728,9 +743,11 @@ async function handleCountTokens(req, res) {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
logResponse(200, null, data);
|
logResponse(200, null, data);
|
||||||
res.json(data);
|
res.json(data);
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/messages/count_tokens', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Error in /v1/messages/count_tokens', error);
|
logError('Error in /v1/messages/count_tokens', error);
|
||||||
|
slsLogRequest({ method: 'POST', endpoint: '/v1/messages/count_tokens', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: 'Internal server error',
|
error: 'Internal server error',
|
||||||
message: error.message
|
message: error.message
|
||||||
|
|||||||
149
sls-logger.js
Normal file
149
sls-logger.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* 阿里云日志服务(SLS)日志模块
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* - 将 API 请求/响应日志上报到阿里云 SLS
|
||||||
|
* - 批量上报,减少 API 调用
|
||||||
|
* - 环境变量缺失时静默降级
|
||||||
|
*/
|
||||||
|
|
||||||
|
import aliyunLog from 'aliyun-log';
|
||||||
|
const { Client, PutLogsRequest, LogItem, LogContent } = aliyunLog;
|
||||||
|
|
||||||
|
// SLS 配置
|
||||||
|
const SLS_CONFIG = {
|
||||||
|
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
|
||||||
|
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
|
||||||
|
endpoint: process.env.ALIYUN_SLS_ENDPOINT,
|
||||||
|
project: process.env.ALIYUN_SLS_PROJECT,
|
||||||
|
logstore: process.env.ALIYUN_SLS_LOGSTORE
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查配置是否完整
|
||||||
|
const isConfigured = Object.values(SLS_CONFIG).every(v => v);
|
||||||
|
|
||||||
|
let client = null;
|
||||||
|
let logQueue = [];
|
||||||
|
const BATCH_SIZE = 10;
|
||||||
|
const FLUSH_INTERVAL_MS = 5000;
|
||||||
|
|
||||||
|
// 初始化 SLS Client
|
||||||
|
function initClient() {
|
||||||
|
if (!isConfigured) {
|
||||||
|
console.warn('[SLS] 阿里云日志服务未配置,日志将仅输出到控制台');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
client = new Client({
|
||||||
|
accessKeyId: SLS_CONFIG.accessKeyId,
|
||||||
|
accessKeySecret: SLS_CONFIG.accessKeySecret,
|
||||||
|
endpoint: SLS_CONFIG.endpoint
|
||||||
|
});
|
||||||
|
console.log('[SLS] 阿里云日志服务客户端初始化成功');
|
||||||
|
return client;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SLS] 初始化失败:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新日志队列
|
||||||
|
async function flushLogs() {
|
||||||
|
if (!client || logQueue.length === 0) return;
|
||||||
|
|
||||||
|
const logsToSend = logQueue.splice(0, BATCH_SIZE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const logItems = logsToSend.map(log => {
|
||||||
|
const contents = Object.entries(log).map(([key, value]) => {
|
||||||
|
return new LogContent({
|
||||||
|
key,
|
||||||
|
value: String(value ?? '')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return new LogItem({
|
||||||
|
time: Math.floor(Date.now() / 1000),
|
||||||
|
contents
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = new PutLogsRequest({
|
||||||
|
projectName: SLS_CONFIG.project,
|
||||||
|
logStoreName: SLS_CONFIG.logstore,
|
||||||
|
logGroup: {
|
||||||
|
logs: logItems
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.putLogs(request);
|
||||||
|
console.log(`[SLS] 成功上报 ${logsToSend.length} 条日志`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SLS] 日志上报失败:', error.message);
|
||||||
|
// 失败的日志重新入队(可选:限制重试次数)
|
||||||
|
logQueue.unshift(...logsToSend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定时刷新
|
||||||
|
let flushTimer = null;
|
||||||
|
function startFlushTimer() {
|
||||||
|
if (flushTimer || !isConfigured) return;
|
||||||
|
flushTimer = setInterval(flushLogs, FLUSH_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 API 请求日志
|
||||||
|
* @param {Object} logData - 日志数据
|
||||||
|
* @param {string} logData.method - HTTP 方法
|
||||||
|
* @param {string} logData.endpoint - 请求路径
|
||||||
|
* @param {string} logData.model - 模型 ID
|
||||||
|
* @param {number} logData.status - 响应状态码
|
||||||
|
* @param {number} logData.duration_ms - 请求耗时
|
||||||
|
* @param {number} [logData.input_tokens] - 输入 Token 数
|
||||||
|
* @param {number} [logData.output_tokens] - 输出 Token 数
|
||||||
|
* @param {string} [logData.error] - 错误信息
|
||||||
|
*/
|
||||||
|
export function logRequest(logData) {
|
||||||
|
const enrichedLog = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
...logData
|
||||||
|
};
|
||||||
|
|
||||||
|
// 始终输出到控制台
|
||||||
|
console.log('[SLS]', JSON.stringify(enrichedLog));
|
||||||
|
|
||||||
|
if (!isConfigured) return;
|
||||||
|
|
||||||
|
logQueue.push(enrichedLog);
|
||||||
|
|
||||||
|
// 队列满时立即刷新
|
||||||
|
if (logQueue.length >= BATCH_SIZE) {
|
||||||
|
flushLogs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优雅关闭,刷新剩余日志
|
||||||
|
*/
|
||||||
|
export async function shutdown() {
|
||||||
|
if (flushTimer) {
|
||||||
|
clearInterval(flushTimer);
|
||||||
|
flushTimer = null;
|
||||||
|
}
|
||||||
|
await flushLogs();
|
||||||
|
console.log('[SLS] 已关闭');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
initClient();
|
||||||
|
startFlushTimer();
|
||||||
|
|
||||||
|
// 进程退出时优雅关闭
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
|
process.on('SIGINT', shutdown);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
logRequest,
|
||||||
|
shutdown
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user