support to lib

This commit is contained in:
musistudio
2025-10-02 21:46:44 +08:00
parent 07e92468f2
commit 0c5a1fd58c
5 changed files with 269 additions and 267 deletions
+8 -6
View File
@@ -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);
}
}
-1
View File
@@ -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: "",
+15 -259
View File
@@ -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();
}
+244 -1
View File
@@ -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;
};
+2
View File
@@ -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}` : ""),
[],