From 0c5a1fd58c3c198a13e5c378bfbbec793331d684 Mon Sep 17 00:00:00 2001 From: musistudio Date: Thu, 2 Oct 2025 21:46:44 +0800 Subject: [PATCH] support to lib --- scripts/build.js | 14 +- src/constants.ts | 1 - src/index.ts | 274 +++------------------------------------ src/server.ts | 245 +++++++++++++++++++++++++++++++++- src/utils/codeCommand.ts | 2 + 5 files changed, 269 insertions(+), 267 deletions(-) diff --git a/scripts/build.js b/scripts/build.js index 5786b9fd..f50a0576 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -9,12 +9,14 @@ console.log('Building Claude Code Router...'); try { // Build the main CLI application console.log('Building CLI application...'); - execSync('esbuild src/cli.ts --bundle --platform=node --outfile=dist/cli.js', { stdio: 'inherit' }); - + execSync('esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --minify', { stdio: 'inherit' }); + execSync('esbuild src/cli.ts --bundle --platform=node --outfile=dist/cli.js --external:./index --minify', { stdio: 'inherit' }); + + // Copy the tiktoken WASM file console.log('Copying tiktoken WASM file...'); execSync('shx cp node_modules/tiktoken/tiktoken_bg.wasm dist/tiktoken_bg.wasm', { stdio: 'inherit' }); - + // Build the UI console.log('Building UI...'); // Check if node_modules exists in ui directory, if not install dependencies @@ -23,13 +25,13 @@ try { execSync('cd ui && npm install', { stdio: 'inherit' }); } execSync('cd ui && npm run build', { stdio: 'inherit' }); - + // Copy the built UI index.html to dist console.log('Copying UI build artifacts...'); execSync('shx cp ui/dist/index.html dist/index.html', { stdio: 'inherit' }); - + console.log('Build completed successfully!'); } catch (error) { console.error('Build failed:', error.message); process.exit(1); -} \ No newline at end of file +} diff --git a/src/constants.ts b/src/constants.ts index 5b7f3bf0..d62ca5fc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,7 +11,6 @@ export const PID_FILE = path.join(HOME_DIR, '.claude-code-router.pid'); export const REFERENCE_COUNT_FILE = path.join(os.tmpdir(), "claude-code-reference-count.txt"); - export const DEFAULT_CONFIG = { LOG: false, OPENAI_API_KEY: "", diff --git a/src/index.ts b/src/index.ts index d1032900..17b150ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,9 @@ import { existsSync } from "fs"; import { writeFile } from "fs/promises"; import { homedir } from "os"; -import path, { join } from "path"; +import { join } from "path"; import { initConfig, initDir, cleanupLogFiles } from "./utils"; import { createServer } from "./server"; -import { router } from "./utils/router"; -import { apiKeyAuth } from "./middleware/auth"; import { cleanupPidFile, isServiceRunning, @@ -14,16 +12,7 @@ import { import { CONFIG_FILE } from "./constants"; import { createStream } from 'rotating-file-stream'; import { HOME_DIR } from "./constants"; -import { sessionUsageCache } from "./utils/cache"; -import {SSEParserTransform} from "./utils/SSEParser.transform"; -import {SSESerializerTransform} from "./utils/SSESerializer.transform"; -import {rewriteStream} from "./utils/rewriteStream"; -import JSON5 from "json5"; -import { IAgent } from "./agents/type"; -import agentsManager from "./agents"; -import { EventEmitter } from "node:events"; - -const event = new EventEmitter() +export { isServiceRunning } from "./utils/processCheck"; async function initializeClaudeConfig() { const homeDir = homedir(); @@ -34,11 +23,9 @@ async function initializeClaudeConfig() { () => Math.random().toString(16)[2] ).join(""); const configContent = { - numStartups: 184, autoUpdaterStatus: "enabled", userID, hasCompletedOnboarding: true, - lastOnboardingVersion: "1.0.17", projects: {}, }; await writeFile(configPath, JSON.stringify(configContent, null, 2)); @@ -49,21 +36,13 @@ interface RunOptions { port?: number; } -async function run(options: RunOptions = {}) { - // Check if service is already running - const isRunning = await isServiceRunning() - if (isRunning) { - console.log("✅ Service is already running in the background."); - return; - } - +export async function getServer(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 || "127.0.0.1"; if (config.HOST && !config.APIKEY) { @@ -71,7 +50,7 @@ async function run(options: RunOptions = {}) { console.warn("⚠️ API key is not set. HOST is forced to 127.0.0.1."); } - const port = config.PORT || 3456; + const port = options.port || config.PORT || 3456; // Save the PID of the background process savePid(process.pid); @@ -136,7 +115,7 @@ async function run(options: RunOptions = {}) { ), }, logger: loggerConfig, - }); + }, config); // Add global error handlers to prevent the service from crashing process.on("uncaughtException", (err) => { @@ -146,239 +125,16 @@ async function run(options: RunOptions = {}) { process.on("unhandledRejection", (reason, promise) => { server.logger.error("Unhandled rejection at:", promise, "reason:", reason); }); - // Add async preHandler hook for authentication - server.addHook("preHandler", async (req, reply) => { - return new Promise((resolve, reject) => { - const done = (err?: Error) => { - if (err) reject(err); - else resolve(); - }; - // Call the async auth function - apiKeyAuth(config)(req, reply, done).catch(reject); - }); - }); - server.addHook("preHandler", async (req, reply) => { - if (req.url.startsWith("/v1/messages")) { - const useAgents = [] - for (const agent of agentsManager.getAllAgents()) { - if (agent.shouldHandle(req, config)) { - // 设置agent标识 - useAgents.push(agent.name) - - // change request body - agent.reqHandler(req, config); - - // append agent tools - if (agent.tools.size) { - if (!req.body?.tools?.length) { - req.body.tools = [] - } - req.body.tools.unshift(...Array.from(agent.tools.values()).map(item => { - return { - name: item.name, - description: item.description, - input_schema: item.input_schema - } - })) - } - } - } - - if (useAgents.length) { - req.agents = useAgents; - } - await router(req, reply, { - config, - event - }); - } - }); - server.addHook("onError", async (request, reply, error) => { - event.emit('onError', request, reply, error); - }) - server.addHook("onSend", (req, reply, payload, done) => { - if (req.sessionId && req.url.startsWith("/v1/messages")) { - if (payload instanceof ReadableStream) { - if (req.agents) { - const abortController = new AbortController(); - const eventStream = payload.pipeThrough(new SSEParserTransform()) - let currentAgent: undefined | IAgent; - let currentToolIndex = -1 - let currentToolName = '' - let currentToolArgs = '' - let currentToolId = '' - const toolMessages: any[] = [] - const assistantMessages: any[] = [] - // 存储Anthropic格式的消息体,区分文本和工具类型 - return done(null, rewriteStream(eventStream, async (data, controller) => { - try { - // 检测工具调用开始 - if (data.event === 'content_block_start' && data?.data?.content_block?.name) { - const agent = req.agents.find((name: string) => agentsManager.getAgent(name)?.tools.get(data.data.content_block.name)) - if (agent) { - currentAgent = agentsManager.getAgent(agent) - currentToolIndex = data.data.index - currentToolName = data.data.content_block.name - currentToolId = data.data.content_block.id - return undefined; - } - } - - // 收集工具参数 - if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data?.delta?.type === 'input_json_delta') { - currentToolArgs += data.data?.delta?.partial_json; - return undefined; - } - - // 工具调用完成,处理agent调用 - if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data.type === 'content_block_stop') { - try { - const args = JSON5.parse(currentToolArgs); - assistantMessages.push({ - type: "tool_use", - id: currentToolId, - name: currentToolName, - input: args - }) - const toolResult = await currentAgent?.tools.get(currentToolName)?.handler(args, { - req, - config - }); - toolMessages.push({ - "tool_use_id": currentToolId, - "type": "tool_result", - "content": toolResult - }) - currentAgent = undefined - currentToolIndex = -1 - currentToolName = '' - currentToolArgs = '' - currentToolId = '' - } catch (e) { - console.log(e); - } - return undefined; - } - - if (data.event === 'message_delta' && toolMessages.length) { - req.body.messages.push({ - role: 'assistant', - content: assistantMessages - }) - req.body.messages.push({ - role: 'user', - content: toolMessages - }) - const response = await fetch(`http://127.0.0.1:${config.PORT}/v1/messages`, { - method: "POST", - headers: { - 'x-api-key': config.APIKEY, - 'content-type': 'application/json', - }, - body: JSON.stringify(req.body), - }) - if (!response.ok) { - return undefined; - } - const stream = response.body!.pipeThrough(new SSEParserTransform()) - const reader = stream.getReader() - while (true) { - try { - const {value, done} = await reader.read(); - if (done) { - break; - } - if (['message_start', 'message_stop'].includes(value.event)) { - continue - } - - // 检查流是否仍然可写 - if (!controller.desiredSize) { - break; - } - - controller.enqueue(value) - }catch (readError: any) { - if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') { - abortController.abort(); // 中止所有相关操作 - break; - } - throw readError; - } - - } - return undefined - } - return data - }catch (error: any) { - console.error('Unexpected error in stream processing:', error); - - // 处理流提前关闭的错误 - if (error.code === 'ERR_STREAM_PREMATURE_CLOSE') { - abortController.abort(); - return undefined; - } - - // 其他错误仍然抛出 - throw error; - } - }).pipeThrough(new SSESerializerTransform())) - } - - const [originalStream, clonedStream] = payload.tee(); - const read = async (stream: ReadableStream) => { - const reader = stream.getReader(); - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - // Process the value if needed - const dataStr = new TextDecoder().decode(value); - if (!dataStr.startsWith("event: message_delta")) { - continue; - } - const str = dataStr.slice(27); - try { - const message = JSON.parse(str); - sessionUsageCache.put(req.sessionId, message.usage); - } catch {} - } - } catch (readError: any) { - if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') { - console.error('Background read stream closed prematurely'); - } else { - console.error('Error in background stream reading:', readError); - } - } finally { - reader.releaseLock(); - } - } - read(clonedStream); - return done(null, originalStream) - } - sessionUsageCache.put(req.sessionId, payload.usage); - if (typeof payload ==='object') { - if (payload.error) { - return done(payload.error, null) - } else { - return done(payload, null) - } - } - } - if (typeof payload ==='object' && payload.error) { - return done(payload.error, null) - } - done(null, payload) - }); - server.addHook("onSend", async (req, reply, payload) => { - event.emit('onSend', req, reply, payload); - return payload; - }) - - - server.start(); + return server; } -export { run }; -// run(); +export async function run(options: RunOptions = {}) { + const isRunning = await isServiceRunning() + if (isRunning) { + console.log("✅ Service is already running in the background."); + return; + } + const server = await getServer(options); + server.start(); +} diff --git a/src/server.ts b/src/server.ts index 74839131..32eda060 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,8 +5,21 @@ import { join } from "path"; import fastifyStatic from "@fastify/static"; import { readdirSync, statSync, readFileSync, writeFileSync, existsSync } from "fs"; import { homedir } from "os"; +import { router } from "./utils/router"; +import { apiKeyAuth } from "./middleware/auth"; +import { sessionUsageCache } from "./utils/cache"; +import {SSEParserTransform} from "./utils/SSEParser.transform"; +import {SSESerializerTransform} from "./utils/SSESerializer.transform"; +import {rewriteStream} from "./utils/rewriteStream"; +import JSON5 from "json5"; +import { IAgent } from "./agents/type"; +import agentsManager from "./agents"; +import { EventEmitter } from "node:events"; -export const createServer = (config: any): Server => { + +const event = new EventEmitter(); + +export const createServer = (config: any, ccrConfig): Server => { const server = new Server(config); // Add endpoint to read config.json with access control @@ -191,5 +204,235 @@ export const createServer = (config: any): Server => { } }); + // Add async preHandler hook for authentication + server.addHook("preHandler", async (req, reply) => { + return new Promise((resolve, reject) => { + const done = (err?: Error) => { + if (err) reject(err); + else resolve(); + }; + // Call the async auth function + apiKeyAuth(config)(req, reply, done).catch(reject); + }); + }); + server.addHook("preHandler", async (req, reply) => { + if (req.url.startsWith("/v1/messages")) { + const useAgents = [] + + for (const agent of agentsManager.getAllAgents()) { + if (agent.shouldHandle(req, config)) { + // 设置agent标识 + useAgents.push(agent.name) + + // change request body + agent.reqHandler(req, config); + + // append agent tools + if (agent.tools.size) { + if (!req.body?.tools?.length) { + req.body.tools = [] + } + req.body.tools.unshift(...Array.from(agent.tools.values()).map(item => { + return { + name: item.name, + description: item.description, + input_schema: item.input_schema + } + })) + } + } + } + + if (useAgents.length) { + req.agents = useAgents; + } + await router(req, reply, { + config, + event + }); + } + }); + server.addHook("onError", async (request, reply, error) => { + event.emit('onError', request, reply, error); + }) + server.addHook("onSend", (req, reply, payload, done) => { + if (req.sessionId && req.url.startsWith("/v1/messages")) { + if (payload instanceof ReadableStream) { + if (req.agents) { + const abortController = new AbortController(); + const eventStream = payload.pipeThrough(new SSEParserTransform()) + let currentAgent: undefined | IAgent; + let currentToolIndex = -1 + let currentToolName = '' + let currentToolArgs = '' + let currentToolId = '' + const toolMessages: any[] = [] + const assistantMessages: any[] = [] + // 存储Anthropic格式的消息体,区分文本和工具类型 + return done(null, rewriteStream(eventStream, async (data, controller) => { + try { + // 检测工具调用开始 + if (data.event === 'content_block_start' && data?.data?.content_block?.name) { + const agent = req.agents.find((name: string) => agentsManager.getAgent(name)?.tools.get(data.data.content_block.name)) + if (agent) { + currentAgent = agentsManager.getAgent(agent) + currentToolIndex = data.data.index + currentToolName = data.data.content_block.name + currentToolId = data.data.content_block.id + return undefined; + } + } + + // 收集工具参数 + if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data?.delta?.type === 'input_json_delta') { + currentToolArgs += data.data?.delta?.partial_json; + return undefined; + } + + // 工具调用完成,处理agent调用 + if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data.type === 'content_block_stop') { + try { + const args = JSON5.parse(currentToolArgs); + assistantMessages.push({ + type: "tool_use", + id: currentToolId, + name: currentToolName, + input: args + }) + const toolResult = await currentAgent?.tools.get(currentToolName)?.handler(args, { + req, + config + }); + toolMessages.push({ + "tool_use_id": currentToolId, + "type": "tool_result", + "content": toolResult + }) + currentAgent = undefined + currentToolIndex = -1 + currentToolName = '' + currentToolArgs = '' + currentToolId = '' + } catch (e) { + console.log(e); + } + return undefined; + } + + if (data.event === 'message_delta' && toolMessages.length) { + req.body.messages.push({ + role: 'assistant', + content: assistantMessages + }) + req.body.messages.push({ + role: 'user', + content: toolMessages + }) + const response = await fetch(`http://127.0.0.1:${config.PORT}/v1/messages`, { + method: "POST", + headers: { + 'x-api-key': config.APIKEY, + 'content-type': 'application/json', + }, + body: JSON.stringify(req.body), + }) + if (!response.ok) { + return undefined; + } + const stream = response.body!.pipeThrough(new SSEParserTransform()) + const reader = stream.getReader() + while (true) { + try { + const {value, done} = await reader.read(); + if (done) { + break; + } + if (['message_start', 'message_stop'].includes(value.event)) { + continue + } + + // 检查流是否仍然可写 + if (!controller.desiredSize) { + break; + } + + controller.enqueue(value) + }catch (readError: any) { + if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') { + abortController.abort(); // 中止所有相关操作 + break; + } + throw readError; + } + + } + return undefined + } + return data + }catch (error: any) { + console.error('Unexpected error in stream processing:', error); + + // 处理流提前关闭的错误 + if (error.code === 'ERR_STREAM_PREMATURE_CLOSE') { + abortController.abort(); + return undefined; + } + + // 其他错误仍然抛出 + throw error; + } + }).pipeThrough(new SSESerializerTransform())) + } + + const [originalStream, clonedStream] = payload.tee(); + const read = async (stream: ReadableStream) => { + const reader = stream.getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + // Process the value if needed + const dataStr = new TextDecoder().decode(value); + if (!dataStr.startsWith("event: message_delta")) { + continue; + } + const str = dataStr.slice(27); + try { + const message = JSON.parse(str); + sessionUsageCache.put(req.sessionId, message.usage); + } catch {} + } + } catch (readError: any) { + if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') { + console.error('Background read stream closed prematurely'); + } else { + console.error('Error in background stream reading:', readError); + } + } finally { + reader.releaseLock(); + } + } + read(clonedStream); + return done(null, originalStream) + } + sessionUsageCache.put(req.sessionId, payload.usage); + if (typeof payload ==='object') { + if (payload.error) { + return done(payload.error, null) + } else { + return done(payload, null) + } + } + } + if (typeof payload ==='object' && payload.error) { + return done(payload.error, null) + } + done(null, payload) + }); + server.addHook("onSend", async (req, reply, payload) => { + event.emit('onSend', req, reply, payload); + return payload; + }) + return server; }; diff --git a/src/utils/codeCommand.ts b/src/utils/codeCommand.ts index 3eb11849..bd04b4fe 100644 --- a/src/utils/codeCommand.ts +++ b/src/utils/codeCommand.ts @@ -56,6 +56,8 @@ export async function executeCodeCommand(args: string[] = []) { const stdioConfig: StdioOptions = config.NON_INTERACTIVE_MODE ? ["pipe", "inherit", "inherit"] // Pipe stdin for non-interactive : "inherit"; // Default inherited behavior + + console.log(claudePath + (joinedArgs ? ` ${joinedArgs}` : "")) const claudeProcess = spawn( claudePath + (joinedArgs ? ` ${joinedArgs}` : ""), [],