diff --git a/README.md b/README.md index a86e4a9..e4198ab 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,7 @@ Transformers allow you to modify the request and response payloads to ensure com - `enhancetool`: Adds a layer of error tolerance to the tool call parameters returned by the LLM (this will cause the tool call information to no longer be streamed). - `cleancache`: Clears the `cache_control` field from requests. - `vertex-gemini`: Handles the Gemini API using Vertex authentication. +- `qwen-cli` (experimental): Unofficial support for qwen3-coder-plus model via Qwen CLI [qwen-cli.js](https://gist.github.com/musistudio/f5a67841ced39912fd99e42200d5ca8b). **Custom Transformers:** @@ -506,5 +507,10 @@ A huge thank you to all our sponsors for their generous support! - @\*亿 - @\*辉 - @JACK +- @\*光 +- @W\*l +- [@kesku](https://github.com/kesku) +- @水\*丫 +- @二吉吉 (If your name is masked, please contact me via my homepage email to update it with your GitHub username.) diff --git a/README_zh.md b/README_zh.md index b8598f7..cf63815 100644 --- a/README_zh.md +++ b/README_zh.md @@ -284,6 +284,7 @@ Transformers 允许您修改请求和响应负载,以确保与不同提供商 - `enhancetool`: 对 LLM 返回的工具调用参数增加一层容错处理(这会导致不再流式返回工具调用信息)。 - `cleancache`: 清除请求中的 `cache_control` 字段。 - `vertex-gemini`: 处理使用 vertex 鉴权的 gemini api。 +- `qwen-cli` (实验性): 通过 Qwen CLI [qwen-cli.js](https://gist.github.com/musistudio/f5a67841ced39912fd99e42200d5ca8b) 对 qwen3-coder-plus 的非官方支持。 **自定义 Transformer:** @@ -497,6 +498,11 @@ jobs: - @\*亿 - @\*辉 - @JACK +- @\*光 +- @W\*l +- [@kesku](https://github.com/kesku) +- @水\*丫 +- @二吉吉 (如果您的名字被屏蔽,请通过我的主页电子邮件与我联系,以便使用您的 GitHub 用户名进行更新。) diff --git a/package.json b/package.json index f8227b9..db4a637 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@musistudio/claude-code-router", - "version": "1.0.33", + "version": "1.0.34", "description": "Use Claude Code without an Anthropics account and route it to another LLM provider", "bin": { "ccr": "./dist/cli.js" @@ -20,10 +20,11 @@ "license": "MIT", "dependencies": { "@fastify/static": "^8.2.0", - "@musistudio/llms": "v1.0.19", + "@musistudio/llms": "^1.0.21", "dotenv": "^16.4.7", "json5": "^2.2.3", "openurl": "^1.1.1", + "pino-rotating-file-stream": "^0.0.2", "tiktoken": "^1.0.21", "uuid": "^11.1.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fe5c38..524c57b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^8.2.0 version: 8.2.0 '@musistudio/llms': - specifier: v1.0.19 - version: 1.0.19(ws@8.18.3)(zod@3.25.67) + specifier: ^1.0.21 + version: 1.0.21(ws@8.18.3)(zod@3.25.67) dotenv: specifier: ^16.4.7 version: 16.6.1 @@ -23,6 +23,9 @@ importers: openurl: specifier: ^1.1.1 version: 1.1.1 + pino-rotating-file-stream: + specifier: ^0.0.2 + version: 0.0.2 tiktoken: specifier: ^1.0.21 version: 1.0.21 @@ -257,8 +260,8 @@ packages: resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} - '@musistudio/llms@1.0.19': - resolution: {integrity: sha512-+U29ZxqXUQJBur5kE9d3e0mC19H0uetwxYvMpWCF4lBtXb2syBPIop2KeolBP+5/vSUz8M45HFd8yKFfVDEO3A==} + '@musistudio/llms@1.0.21': + resolution: {integrity: sha512-oRSs9U0o13HCNaw+fesLnJv75t+AUSKn37LxJOlO1yNWYucjfB76vo3y3rK47wztxv2rmSumrlbu2FzjRG4YuQ==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -707,6 +710,9 @@ packages: pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + pino-rotating-file-stream@0.0.2: + resolution: {integrity: sha512-knF+ReDBMQMB7gzBfuFpUmCrXpRen6YYh5Q9Ymmj//dDHeH4QEMwAV7VoGEEM+30s7VHqfbabazs9wxkMO2BIQ==} + pino-std-serializers@7.0.0: resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} @@ -757,6 +763,10 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rotating-file-stream@3.2.6: + resolution: {integrity: sha512-r8yShzMWUvWXkRzbOXDM1fEaMpc3qo2PzK7bBH/0p0Nl/uz8Mud/Y+0XTQxe3kbSnDF7qBH2tSe83WDKA7o3ww==} + engines: {node: '>=14.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1102,7 +1112,7 @@ snapshots: '@lukeed/ms@2.0.2': {} - '@musistudio/llms@1.0.19(ws@8.18.3)(zod@3.25.67)': + '@musistudio/llms@1.0.21(ws@8.18.3)(zod@3.25.67)': dependencies: '@anthropic-ai/sdk': 0.54.0 '@fastify/cors': 11.0.1 @@ -1604,6 +1614,10 @@ snapshots: dependencies: split2: 4.2.0 + pino-rotating-file-stream@0.0.2: + dependencies: + rotating-file-stream: 3.2.6 + pino-std-serializers@7.0.0: {} pino@9.7.0: @@ -1653,6 +1667,8 @@ snapshots: rfdc@1.4.1: {} + rotating-file-stream@3.2.6: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 diff --git a/src/index.ts b/src/index.ts index eba09b5..827f2c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ import { existsSync } from "fs"; import { writeFile } from "fs/promises"; import { homedir } from "os"; -import { join } from "path"; -import { initConfig, initDir } from "./utils"; +import path, { join } from "path"; +import { initConfig, initDir, cleanupLogFiles } from "./utils"; import { createServer } from "./server"; import { router } from "./utils/router"; import { apiKeyAuth } from "./middleware/auth"; @@ -12,6 +12,8 @@ import { savePid, } from "./utils/processCheck"; import { CONFIG_FILE } from "./constants"; +import createWriteStream from "pino-rotating-file-stream"; +import { HOME_DIR } from "./constants"; async function initializeClaudeConfig() { const homeDir = homedir(); @@ -46,14 +48,14 @@ async function run(options: RunOptions = {}) { await initializeClaudeConfig(); await initDir(); + // Clean up old log files, keeping only the 10 most recent ones + await cleanupLogFiles(); const config = await initConfig(); let HOST = config.HOST; if (config.HOST && !config.APIKEY) { HOST = "127.0.0.1"; - console.warn( - "⚠️ API key is not set. HOST is forced to 127.0.0.1." - ); + console.warn("⚠️ API key is not set. HOST is forced to 127.0.0.1."); } const port = config.PORT || 3456; @@ -73,12 +75,15 @@ async function run(options: RunOptions = {}) { cleanupPidFile(); process.exit(0); }); - console.log(HOST) + console.log(HOST); // Use port from environment variable if set (for background process) const servicePort = process.env.SERVICE_PORT ? parseInt(process.env.SERVICE_PORT) : port; + + const startTime = new Date().toISOString(); + const server = createServer({ jsonPath: CONFIG_FILE, initialConfig: { @@ -92,6 +97,15 @@ async function run(options: RunOptions = {}) { "claude-code-router.log" ), }, + logger: { + level: "debug", + stream: createWriteStream({ + path: HOME_DIR, + filename: `./logs/ccr-${startTime}.log`, + maxFiles: 3, + interval: "1d", + }), + }, }); // Add async preHandler hook for authentication server.addHook("preHandler", async (req, reply) => { @@ -105,8 +119,8 @@ async function run(options: RunOptions = {}) { }); }); server.addHook("preHandler", async (req, reply) => { - if(req.url.startsWith("/v1/messages")) { - router(req, reply, config) + if (req.url.startsWith("/v1/messages")) { + router(req, reply, config); } }); server.start(); diff --git a/src/utils/index.ts b/src/utils/index.ts index d47cf21..afc79db 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -9,6 +9,7 @@ import { PLUGINS_DIR, } from "../constants"; import { getSystemUUID, generateTempAPIKey, getTempAPIKey } from "./systemUUID"; +import { cleanupLogFiles } from "./logCleanup"; const ensureDir = async (dir_path: string) => { try { @@ -139,3 +140,6 @@ export const initConfig = async () => { // 导出系统UUID相关函数 export { getSystemUUID, generateTempAPIKey, getTempAPIKey }; + +// 导出日志清理函数 +export { cleanupLogFiles }; diff --git a/src/utils/logCleanup.ts b/src/utils/logCleanup.ts new file mode 100644 index 0000000..1e21219 --- /dev/null +++ b/src/utils/logCleanup.ts @@ -0,0 +1,44 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { HOME_DIR } from "../constants"; + +/** + * Cleans up old log files, keeping only the most recent ones + * @param maxFiles - Maximum number of log files to keep (default: 9) + */ +export async function cleanupLogFiles(maxFiles: number = 9): Promise { + try { + const logsDir = path.join(HOME_DIR, "logs"); + + // Check if logs directory exists + try { + await fs.access(logsDir); + } catch { + // Logs directory doesn't exist, nothing to clean up + return; + } + + // Read all files in the logs directory + const files = await fs.readdir(logsDir); + + // Filter for log files (files starting with 'ccr-' and ending with '.log') + const logFiles = files + .filter(file => file.startsWith('ccr-') && file.endsWith('.log')) + .sort() + .reverse(); // Sort in descending order (newest first) + + // Delete files exceeding the maxFiles limit + if (logFiles.length > maxFiles) { + for (let i = maxFiles; i < logFiles.length; i++) { + const filePath = path.join(logsDir, logFiles[i]); + try { + await fs.unlink(filePath); + } catch (error) { + console.warn(`Failed to delete log file ${filePath}:`, error); + } + } + } + } catch (error) { + console.warn("Failed to clean up log files:", error); + } +}