diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 93f1361..0000000 --- a/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -npm-debug.log diff --git a/.npmignore b/.npmignore index 185d20a..350d538 100644 --- a/.npmignore +++ b/.npmignore @@ -13,4 +13,5 @@ docs blog config.json ui -scripts \ No newline at end of file +scripts +packages diff --git a/CLAUDE.md b/CLAUDE.md index f901d48..a1f4e10 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,43 +2,163 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Commands +## Project Overview -- **Build the project**: - ```bash - npm run build - ``` -- **Start the router server**: - ```bash - ccr start - ``` -- **Stop the router server**: - ```bash - ccr stop - ``` -- **Check the server status**: - ```bash - ccr status - ``` -- **Run Claude Code through the router**: - ```bash - ccr code "" - ``` -- **Release a new version**: - ```bash - npm run release - ``` +Claude Code Router is a tool that routes Claude Code requests to different LLM providers. It uses a Monorepo architecture with four main packages: -## Architecture +- **cli** (`@musistudio/claude-code-router-cli`): Command-line tool providing the `ccr` command +- **server** (`@musistudio/claude-code-router-server`): Core server handling API routing and transformations +- **shared** (`@musistudio/claude-code-router-shared`): Shared constants and utilities +- **ui** (`@musistudio/claude-code-router-ui`): Web management interface (React + Vite) -This project is a TypeScript-based router for Claude Code requests. It allows routing requests to different large language models (LLMs) from various providers based on custom rules. +## Build Commands -- **Entry Point**: The main command-line interface logic is in `src/cli.ts`. It handles parsing commands like `start`, `stop`, and `code`. -- **Server**: The `ccr start` command launches a server that listens for requests from Claude Code. The server logic is initiated from `src/index.ts`. -- **Configuration**: The router is configured via a JSON file located at `~/.claude-code-router/config.json`. This file defines API providers, routing rules, and custom transformers. An example can be found in `config.example.json`. -- **Routing**: The core routing logic determines which LLM provider and model to use for a given request. It supports default routes for different scenarios (`default`, `background`, `think`, `longContext`, `webSearch`) and can be extended with a custom JavaScript router file. The router logic is likely in `src/utils/router.ts`. -- **Providers and Transformers**: The application supports multiple LLM providers. Transformers adapt the request and response formats for different provider APIs. -- **Claude Code Integration**: When a user runs `ccr code`, the command is forwarded to the running router service. The service then processes the request, applies routing rules, and sends it to the configured LLM. If the service isn't running, `ccr code` will attempt to start it automatically. -- **Dependencies**: The project is built with `esbuild`. It has a key local dependency `@musistudio/llms`, which probably contains the core logic for interacting with different LLM APIs. -- `@musistudio/llms` is implemented based on `fastify` and exposes `fastify`'s hook and middleware interfaces, allowing direct use of `server.addHook`. -- 无论如何你都不能自动提交git +### Build all packages +```bash +pnpm build +``` + +### Build individual packages +```bash +pnpm build:cli # Build CLI +pnpm build:server # Build Server +pnpm build:ui # Build UI +``` + +### Development mode +```bash +pnpm dev:cli # Develop CLI (ts-node) +pnpm dev:server # Develop Server (ts-node) +pnpm dev:ui # Develop UI (Vite) +``` + +### Publish +```bash +pnpm release # Build and publish all packages +``` + +## Core Architecture + +### 1. Routing System (packages/server/src/utils/router.ts) + +The routing logic determines which model a request should be sent to: + +- **Default routing**: Uses `Router.default` configuration +- **Project-level routing**: Checks `~/.claude/projects//claude-code-router.json` +- **Custom routing**: Loads custom JavaScript router function via `CUSTOM_ROUTER_PATH` +- **Built-in scenario routing**: + - `background`: Background tasks (typically lightweight models) + - `think`: Thinking-intensive tasks (Plan Mode) + - `longContext`: Long context (exceeds `longContextThreshold` tokens) + - `webSearch`: Web search tasks + - `image`: Image-related tasks + +Token calculation uses `tiktoken` (cl100k_base) to estimate request size. + +### 2. Transformer System + +The project uses the `@musistudio/llms` package (external dependency) to handle request/response transformations. Transformers adapt to different provider API differences: + +- Built-in transformers: `anthropic`, `deepseek`, `gemini`, `openrouter`, `groq`, `maxtoken`, `tooluse`, `reasoning`, `enhancetool`, etc. +- Custom transformers: Load external plugins via `transformers` array in `config.json` + +Transformer configuration supports: +- Global application (provider level) +- Model-specific application +- Option passing (e.g., `max_tokens` parameter for `maxtoken`) + +### 3. Agent System (packages/server/src/agents/) + +Agents are pluggable feature modules that can: +- Detect whether to handle a request (`shouldHandle`) +- Modify requests (`reqHandler`) +- Provide custom tools (`tools`) + +Built-in agents: +- **imageAgent**: Handles image-related tasks + +Agent tool call flow: +1. Detect and mark agents in `preHandler` hook +2. Add agent tools to the request +3. Intercept tool call events in `onSend` hook +4. Execute agent tool and initiate new LLM request +5. Stream results back + +### 4. SSE Stream Processing + +The server uses custom Transform streams to handle Server-Sent Events: +- `SSEParserTransform`: Parses SSE text stream into event objects +- `SSESerializerTransform`: Serializes event objects into SSE text stream +- `rewriteStream`: Intercepts and modifies stream data (for agent tool calls) + +### 5. Configuration Management + +Configuration file location: `~/.claude-code-router/config.json` + +Key features: +- Supports environment variable interpolation (`$VAR_NAME` or `${VAR_NAME}`) +- JSON5 format (supports comments) +- Automatic backups (keeps last 3 backups) +- Hot reload requires service restart (`ccr restart`) + +Configuration validation: +- If `Providers` are configured, both `HOST` and `APIKEY` must be set +- Otherwise listens on `0.0.0.0` without authentication + +### 6. Logging System + +Two separate logging systems: + +**Server-level logs** (pino): +- Location: `~/.claude-code-router/logs/ccr-*.log` +- Content: HTTP requests, API calls, server events +- Configuration: `LOG_LEVEL` (fatal/error/warn/info/debug/trace) + +**Application-level logs**: +- Location: `~/.claude-code-router/claude-code-router.log` +- Content: Routing decisions, business logic events + +## CLI Commands + +```bash +ccr start # Start server +ccr stop # Stop server +ccr restart # Restart server +ccr status # Show status +ccr code # Execute claude command +ccr model # Interactive model selection and configuration +ccr activate # Output shell environment variables (for integration) +ccr ui # Open Web UI +ccr statusline # Integrated statusline (reads JSON from stdin) +``` + +## Subagent Routing + +Use special tags in subagent prompts to specify models: +``` +provider,model +Please help me analyze this code... +``` + +## Dependencies + +``` +cli → server → shared +server → @musistudio/llms (core routing and transformation logic) +ui (standalone frontend application) +``` + +## Development Notes + +1. **Node.js version**: Requires >= 18.0.0 +2. **Package manager**: Uses pnpm (monorepo depends on workspace protocol) +3. **TypeScript**: All packages use TypeScript, but UI package is ESM module +4. **Build tools**: + - cli/server/shared: esbuild + - ui: Vite + TypeScript +5. **@musistudio/llms**: This is an external dependency package providing the core server framework and transformer functionality, type definitions in `packages/server/src/types.d.ts` + +## Configuration Example Locations + +- Main configuration example: Complete example in README.md +- Custom router example: `custom-router.example.js` diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 36a6d15..0000000 --- a/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM node:20-alpine - -RUN npm install -g @musistudio/claude-code-router - -EXPOSE 3456 - -CMD ["ccr", "start"] diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 9773de1..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: "3.8" - -services: - claude-code-router: - build: . - ports: - - "3456:3456" - volumes: - - ~/.claude-code-router:/root/.claude-code-router - restart: unless-stopped diff --git a/package.json b/package.json index 2b7e9a8..e4b54c0 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,22 @@ { "name": "@musistudio/claude-code-router", - "version": "1.0.73", + "version": "2.0.0", "description": "Use Claude Code without an Anthropics account and route it to another LLM provider", "private": true, "scripts": { "build": "node scripts/build.js", - "build:cli": "pnpm --filter @musistudio/claude-code-router-cli build", - "build:server": "pnpm --filter @musistudio/claude-code-router-server build", - "build:ui": "pnpm --filter @musistudio/claude-code-router-ui build", - "release": "pnpm build && pnpm publish -r", - "dev:cli": "pnpm --filter @musistudio/claude-code-router-cli dev", - "dev:server": "pnpm --filter @musistudio/claude-code-router-server dev", - "dev:ui": "pnpm --filter @musistudio/claude-code-router-ui dev" + "build:cli": "pnpm --filter @CCR/cli build", + "build:server": "pnpm --filter @CCR/server build", + "build:ui": "pnpm --filter @CCR/ui build", + "release": "pnpm build && bash scripts/release.sh all", + "release:npm": "bash scripts/release.sh npm", + "release:docker": "bash scripts/release.sh docker", + "dev:cli": "pnpm --filter @CCR/cli dev", + "dev:server": "pnpm --filter @CCR/server dev", + "dev:ui": "pnpm --filter @CCR/ui dev" + }, + "bin": { + "ccr": "dist/cli.js" }, "keywords": [ "claude", diff --git a/packages/cli/package.json b/packages/cli/package.json index e71b914..99dcc4e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { - "name": "@musistudio/claude-code-router-cli", - "version": "1.0.73", + "name": "@CCR/cli", + "version": "2.0.0", "description": "CLI for Claude Code Router", "bin": { "ccr": "dist/cli.js" @@ -18,9 +18,9 @@ "author": "musistudio", "license": "MIT", "dependencies": { - "@musistudio/claude-code-router-shared": "workspace:*", + "@CCR/shared": "workspace:*", "@inquirer/prompts": "^5.0.0", - "@musistudio/claude-code-router-server": "workspace:*", + "@CCR/server": "workspace:*", "find-process": "^2.0.0", "minimist": "^1.2.8", "openurl": "^1.1.1" diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index f83650f..ba65920 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,8 +1,5 @@ #!/usr/bin/env node -// @ts-ignore - server package is built separately -import { run } from "@musistudio/claude-code-router-server"; -// @ts-ignore - server package is built separately -import { parseStatusLineData, type StatusLineInput } from "@musistudio/claude-code-router-server"; +import { run } from "./utils"; import { showStatus } from "./utils/status"; import { executeCodeCommand } from "./utils/codeCommand"; import { @@ -10,13 +7,14 @@ import { isServiceRunning, getServiceInfo, } from "./utils/processCheck"; -import { runModelSelector } from "./utils/modelSelector"; // ADD THIS LINE +import { runModelSelector } from "./utils/modelSelector"; import { activateCommand } from "./utils/activateCommand"; import { version } from "../package.json"; import { spawn, exec } from "child_process"; -import { PID_FILE, REFERENCE_COUNT_FILE } from "@musistudio/claude-code-router-shared"; +import { PID_FILE, REFERENCE_COUNT_FILE } from "@CCR/shared"; import fs, { existsSync, readFileSync } from "fs"; import { join } from "path"; +import { parseStatusLineData, StatusLineInput } from "./utils/statusline"; const command = process.argv[2]; @@ -68,7 +66,7 @@ async function main() { const isRunning = await isServiceRunning() switch (command) { case "start": - run(); + await run(); break; case "stop": try { @@ -337,4 +335,4 @@ async function main() { } } -main().catch(console.error); \ No newline at end of file +main().catch(console.error); diff --git a/packages/cli/src/utils/codeCommand.ts b/packages/cli/src/utils/codeCommand.ts index aa551db..8a9ee13 100644 --- a/packages/cli/src/utils/codeCommand.ts +++ b/packages/cli/src/utils/codeCommand.ts @@ -1,10 +1,9 @@ import { spawn, type StdioOptions } from "child_process"; import { readConfigFile } from "."; -// @ts-ignore - server package is built separately -import { closeService } from "@musistudio/claude-code-router-server"; import { decrementReferenceCount, incrementReferenceCount, + closeService, } from "./processCheck"; import { quote } from 'shell-quote'; import minimist from "minimist"; diff --git a/packages/cli/src/utils/index.ts b/packages/cli/src/utils/index.ts index d031e61..4a6c312 100644 --- a/packages/cli/src/utils/index.ts +++ b/packages/cli/src/utils/index.ts @@ -4,12 +4,18 @@ import JSON5 from "json5"; import path from "node:path"; import { CONFIG_FILE, - DEFAULT_CONFIG, - HOME_DIR, + HOME_DIR, PID_FILE, PLUGINS_DIR, -} from "@musistudio/claude-code-router-shared"; -// @ts-ignore - server package is built separately -import { cleanupLogFiles } from "@musistudio/claude-code-router-server"; + REFERENCE_COUNT_FILE, +} from "@CCR/shared"; +import { getServer } from "@CCR/server"; +import { writeFileSync, existsSync, readFileSync } from "fs"; +import { checkForUpdates, performUpdate } from "./update"; +import { version } from "../../package.json"; +import { spawn } from "child_process"; +import { cleanupPidFile } from "./processCheck"; +import fastifyStatic from "@fastify/static"; +import {join} from "path"; // Function to interpolate environment variables in config values const interpolateEnvVars = (obj: any): any => { @@ -174,8 +180,66 @@ export const initConfig = async () => { return config; }; -// 导出日志清理函数 -export { cleanupLogFiles }; +export const run = async (args: string[] = []) => { + const server = await getServer(); + // Save the PID of the background process + writeFileSync(PID_FILE, process.pid.toString()); -// 导出更新功能 -export { checkForUpdates, performUpdate } from "./update"; + // server.app.post('/api/update/perform', async () => { + // return await performUpdate(); + // }) + // + // server.app.get('/api/update/check', async () => { + // return await checkForUpdates(version); + // }) + + server.app.post("/api/restart", async () => { + setTimeout(async () => { + spawn("ccr", ["restart"], { + detached: true, + stdio: "ignore", + }).unref(); + }, 100); + + return { success: true, message: "Service restart initiated" } + }); + + // await server.start() to ensure it starts successfully and keep process alive + await server.start(); +} + +export const restartService = async () => { + // Stop the service if it's running + try { + const pid = parseInt(readFileSync(PID_FILE, "utf-8")); + process.kill(pid); + cleanupPidFile(); + if (existsSync(REFERENCE_COUNT_FILE)) { + try { + await fs.unlink(REFERENCE_COUNT_FILE); + } catch (e) { + // Ignore cleanup errors + } + } + console.log("claude code router service has been stopped."); + } catch (e) { + console.log("Service was not running or failed to stop."); + cleanupPidFile(); + } + + // Start the service again in the background + console.log("Starting claude code router service..."); + const cliPath = path.join(__dirname, "../cli.js"); + const startProcess = spawn("node", [cliPath, "start"], { + detached: true, + stdio: "ignore", + }); + + startProcess.on("error", (error) => { + console.error("Failed to start service:", error); + throw error; + }); + + startProcess.unref(); + console.log("✅ Service started successfully in the background."); +}; diff --git a/packages/cli/src/utils/processCheck.ts b/packages/cli/src/utils/processCheck.ts index bcd45e1..002e6be 100644 --- a/packages/cli/src/utils/processCheck.ts +++ b/packages/cli/src/utils/processCheck.ts @@ -1,5 +1,5 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { PID_FILE, REFERENCE_COUNT_FILE } from '@musistudio/claude-code-router-shared'; +import { PID_FILE, REFERENCE_COUNT_FILE } from '@CCR/shared'; import { readConfigFile } from '.'; import find from 'find-process'; import { execSync } from 'child_process'; // 引入 execSync 来执行命令行 @@ -134,3 +134,21 @@ export async function getServiceInfo() { referenceCount: getReferenceCount() }; } + +export async function closeService() { + // Check reference count + const referenceCount = getReferenceCount(); + + // Only stop the service if reference count is 0 + if (referenceCount === 0) { + const pid = getServicePid(); + if (pid && await isServiceRunning()) { + try { + // Kill the service process + process.kill(pid, 'SIGTERM'); + } catch (e) { + // Ignore kill errors + } + } + } +} diff --git a/packages/cli/src/utils/statusline.ts b/packages/cli/src/utils/statusline.ts index 78a3724..8bbe89b 100644 --- a/packages/cli/src/utils/statusline.ts +++ b/packages/cli/src/utils/statusline.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { execSync } from "child_process"; -import { CONFIG_FILE } from "@musistudio/claude-code-router-shared"; +import { CONFIG_FILE } from "@CCR/shared"; import JSON5 from "json5"; export interface StatusLineModuleConfig { diff --git a/packages/server/package.json b/packages/server/package.json index 1970083..b69e1dd 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,8 +1,9 @@ { - "name": "@musistudio/claude-code-router-server", - "version": "1.0.73", + "name": "@CCR/server", + "version": "2.0.0", "description": "Server for Claude Code Router", "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "build": "node ../../scripts/build-server.js", "dev": "ts-node src/index.ts" @@ -16,7 +17,7 @@ "author": "musistudio", "license": "MIT", "dependencies": { - "@musistudio/claude-code-router-shared": "workspace:*", + "@CCR/shared": "workspace:*", "@fastify/static": "^8.2.0", "@musistudio/llms": "^1.0.51", "dotenv": "^16.4.7", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b6c53a2..f56e878 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -6,16 +6,17 @@ import { initConfig, initDir } from "./utils"; import { createServer } from "./server"; import { router } from "./utils/router"; import { apiKeyAuth } from "./middleware/auth"; -import { PID_FILE, CONFIG_FILE, HOME_DIR } from "@musistudio/claude-code-router-shared"; +import { CONFIG_FILE, HOME_DIR } from "@CCR/shared"; import { createStream } from 'rotating-file-stream'; 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 { IAgent, ITool } from "./agents/type"; import agentsManager from "./agents"; import { EventEmitter } from "node:events"; +import {spawn} from "child_process"; const event = new EventEmitter() @@ -44,14 +45,7 @@ interface RunOptions { logger?: any; } -async function run(options: RunOptions = {}) { - // Check if service is already running - const isRunning = existsSync(PID_FILE); - if (isRunning) { - console.log("✅ Service is already running in the background."); - return; - } - +async function getServer(options: RunOptions = {}) { await initializeClaudeConfig(); await initDir(); const config = await initConfig(); @@ -63,13 +57,10 @@ async function run(options: RunOptions = {}) { let HOST = config.HOST || "127.0.0.1"; if (hasProviders) { - // When providers are configured, require both HOST and APIKEY - if (!config.HOST || !config.APIKEY) { - console.error("❌ Both HOST and APIKEY must be configured when Providers are set."); - console.error(" Please add HOST and APIKEY to your config file."); - process.exit(1); - } HOST = config.HOST; + if (!config.APIKEY) { + HOST = "127.0.0.1"; + } } else { // When no providers are configured, listen on 0.0.0.0 without authentication HOST = "0.0.0.0"; @@ -78,35 +69,6 @@ async function run(options: RunOptions = {}) { const port = config.PORT || 3456; - // Save the PID of the background process - writeFileSync(PID_FILE, process.pid.toString()); - - // Handle SIGINT (Ctrl+C) to clean up PID file - process.on("SIGINT", () => { - console.log("Received SIGINT, cleaning up..."); - if (existsSync(PID_FILE)) { - try { - unlinkSync(PID_FILE); - } catch (e) { - // Ignore cleanup errors - } - } - process.exit(0); - }); - - // Handle SIGTERM to clean up PID file - process.on("SIGTERM", () => { - if (existsSync(PID_FILE)) { - try { - const fs = require('fs'); - fs.unlinkSync(PID_FILE); - } catch (e) { - // Ignore cleanup errors - } - } - process.exit(0); - }); - // Use port from environment variable if set (for background process) const servicePort = process.env.SERVICE_PORT ? parseInt(process.env.SERVICE_PORT) @@ -159,7 +121,7 @@ async function run(options: RunOptions = {}) { } } - const server = createServer({ + const serverInstance = createServer({ jsonPath: CONFIG_FILE, initialConfig: { // ...config, @@ -175,16 +137,8 @@ async function run(options: RunOptions = {}) { logger: loggerConfig, }); - // Add global error handlers to prevent the service from crashing - process.on("uncaughtException", (err) => { - server.logger.error("Uncaught exception:", err); - }); - - 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: any, reply: any) => { + serverInstance.addHook("preHandler", async (req: any, reply: any) => { return new Promise((resolve, reject) => { const done = (err?: Error) => { if (err) reject(err); @@ -194,7 +148,7 @@ async function run(options: RunOptions = {}) { apiKeyAuth(config)(req, reply, done).catch(reject); }); }); - server.addHook("preHandler", async (req: any, reply: any) => { + serverInstance.addHook("preHandler", async (req: any, reply: any) => { if (req.url.startsWith("/v1/messages") && !req.url.startsWith("/v1/messages/count_tokens")) { const useAgents = [] @@ -231,10 +185,10 @@ async function run(options: RunOptions = {}) { }); } }); - server.addHook("onError", async (request: any, reply: any, error: any) => { + serverInstance.addHook("onError", async (request: any, reply: any, error: any) => { event.emit('onError', request, reply, error); }) - server.addHook("onSend", (req: any, reply: any, payload: any, done: any) => { + serverInstance.addHook("onSend", (req: any, reply: any, payload: any, done: any) => { if (req.sessionId && req.url.startsWith("/v1/messages") && !req.url.startsWith("/v1/messages/count_tokens")) { if (payload instanceof ReadableStream) { if (req.agents) { @@ -409,16 +363,39 @@ async function run(options: RunOptions = {}) { } done(null, payload) }); - server.addHook("onSend", async (req: any, reply: any, payload: any) => { + serverInstance.addHook("onSend", async (req: any, reply: any, payload: any) => { event.emit('onSend', req, reply, payload); return payload; - }) + }); + // Add global error handlers to prevent the service from crashing + process.on("uncaughtException", (err) => { + serverInstance.logger.error("Uncaught exception:", err); + }); - server.start(); + process.on("unhandledRejection", (reason, promise) => { + serverInstance.logger.error("Unhandled rejection at:", promise, "reason:", reason); + }); + + return serverInstance; } -export { run }; +async function run() { + const server = await getServer(); + server.app.post("/api/restart", async () => { + setTimeout(async () => { + process.exit(0); + }, 100); + + return { success: true, message: "Service restart initiated" } + }); + await server.start(); +} + +export { getServer }; +export type { RunOptions }; +export type { IAgent, ITool } from "./agents/type"; +export { initDir, initConfig, readConfigFile, writeConfigFile, backupConfigFile } from "./utils"; // 如果是直接运行此文件,则启动服务 if (require.main === module) { diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index a9f005e..416d644 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -5,6 +5,7 @@ import fastifyStatic from "@fastify/static"; import { readdirSync, statSync, readFileSync, writeFileSync, existsSync } from "fs"; import { homedir } from "os"; import {calculateTokenCount} from "./utils/router"; +import { fork, spawn } from "child_process"; export const createServer = (config: any): any => { const server = new Server(config); @@ -46,20 +47,6 @@ export const createServer = (config: any): any => { return { success: true, message: "Config saved successfully" }; }); - // Add endpoint to restart the service with access control - server.app.post("/api/restart", async (req: any, reply: any) => { - reply.send({ success: true, message: "Service restart initiated" }); - - // Restart the service after a short delay to allow response to be sent - setTimeout(() => { - const { spawn } = require("child_process"); - spawn(process.execPath, [process.argv[1], "restart"], { - detached: true, - stdio: "ignore", - }); - }, 1000); - }); - // Register static file serving with caching server.app.register(fastifyStatic, { root: join(__dirname, "..", "dist"), diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 945aaa4..9e839a4 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -7,7 +7,7 @@ import { DEFAULT_CONFIG, HOME_DIR, PLUGINS_DIR, -} from "@musistudio/claude-code-router-shared"; +} from "@CCR/shared"; // Function to interpolate environment variables in config values const interpolateEnvVars = (obj: any): any => { diff --git a/packages/server/src/utils/router.ts b/packages/server/src/utils/router.ts index 7e77d23..5de5a9f 100644 --- a/packages/server/src/utils/router.ts +++ b/packages/server/src/utils/router.ts @@ -3,7 +3,7 @@ import { sessionUsageCache, Usage } from "./cache"; import { readFile, access } from "fs/promises"; import { opendir, stat } from "fs/promises"; import { join } from "path"; -import { CLAUDE_PROJECTS_DIR, HOME_DIR } from "@musistudio/claude-code-router-shared"; +import { CLAUDE_PROJECTS_DIR, HOME_DIR } from "@CCR/shared"; import { LRUCache } from "lru-cache"; // Types from @anthropic-ai/sdk diff --git a/packages/shared/package.json b/packages/shared/package.json index 9ae6171..48da9b0 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { - "name": "@musistudio/claude-code-router-shared", - "version": "1.0.73", + "name": "@CCR/shared", + "version": "2.0.0", "description": "Shared utilities and constants for Claude Code Router", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index a978e22..4af2fda 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -15,7 +15,14 @@ export const REFERENCE_COUNT_FILE = path.join(os.tmpdir(), "claude-code-referenc export const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects"); -export const DEFAULT_CONFIG = { +export interface DefaultConfig { + LOG: boolean; + OPENAI_API_KEY: string; + OPENAI_BASE_URL: string; + OPENAI_MODEL: string; +} + +export const DEFAULT_CONFIG: DefaultConfig = { LOG: false, OPENAI_API_KEY: "", OPENAI_BASE_URL: "", diff --git a/packages/ui/package.json b/packages/ui/package.json index abda17b..44299ad 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,7 @@ { - "name": "@musistudio/claude-code-router-ui", + "name": "@CCR/ui", "private": true, - "version": "1.0.73", + "version": "2.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index b641680..356884b 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -42,6 +42,7 @@ function App() { const [newVersionInfo, setNewVersionInfo] = useState<{ version: string; changelog: string } | null>(null); const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); const [hasCheckedUpdate, setHasCheckedUpdate] = useState(false); + const [isUpdateFeatureAvailable, setIsUpdateFeatureAvailable] = useState(true); const hasAutoCheckedUpdate = useRef(false); const saveConfig = async () => { @@ -155,6 +156,7 @@ function App() { setHasCheckedUpdate(true); } catch (error) { console.error('Failed to check for updates:', error); + setIsUpdateFeatureAvailable(false); if (showDialog) { setToast({ message: t('app.update_check_failed') + ': ' + (error as Error).message, type: 'error' }); } @@ -306,26 +308,28 @@ function App() { - {/* 更新版本按钮 */} - + {isCheckingUpdate && ( +
+
+
+ )} + + )}