mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-01-30 06:12:06 +00:00
support docker
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
@@ -14,3 +14,4 @@ blog
|
||||
config.json
|
||||
ui
|
||||
scripts
|
||||
packages
|
||||
|
||||
192
CLAUDE.md
192
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 "<your prompt>"
|
||||
```
|
||||
- **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/<project-id>/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:
|
||||
```
|
||||
<CCR-SUBAGENT-MODEL>provider,model</CCR-SUBAGENT-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`
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
RUN npm install -g @musistudio/claude-code-router
|
||||
|
||||
EXPOSE 3456
|
||||
|
||||
CMD ["ccr", "start"]
|
||||
@@ -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
|
||||
21
package.json
21
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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.");
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<void>((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) {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{/* 更新版本按钮 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => checkForUpdates(true)}
|
||||
disabled={isCheckingUpdate}
|
||||
className="transition-all-ease hover:scale-110 relative"
|
||||
>
|
||||
<div className="relative">
|
||||
<CircleArrowUp className="h-5 w-5" />
|
||||
{isNewVersionAvailable && !isCheckingUpdate && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-white"></div>
|
||||
)}
|
||||
</div>
|
||||
{isCheckingUpdate && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
|
||||
{/* 更新版本按钮 - 仅当更新功能可用时显示 */}
|
||||
{isUpdateFeatureAvailable && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => checkForUpdates(true)}
|
||||
disabled={isCheckingUpdate}
|
||||
className="transition-all-ease hover:scale-110 relative"
|
||||
>
|
||||
<div className="relative">
|
||||
<CircleArrowUp className="h-5 w-5" />
|
||||
{isNewVersionAvailable && !isCheckingUpdate && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-white"></div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
{isCheckingUpdate && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={saveConfig} variant="outline" className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{t('app.save')}
|
||||
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -23,15 +23,15 @@ importers:
|
||||
|
||||
packages/cli:
|
||||
dependencies:
|
||||
'@CCR/server':
|
||||
specifier: workspace:*
|
||||
version: link:../server
|
||||
'@CCR/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
'@inquirer/prompts':
|
||||
specifier: ^5.0.0
|
||||
version: 5.5.0
|
||||
'@musistudio/claude-code-router-server':
|
||||
specifier: workspace:*
|
||||
version: link:../server
|
||||
'@musistudio/claude-code-router-shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
find-process:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
@@ -57,12 +57,12 @@ importers:
|
||||
|
||||
packages/server:
|
||||
dependencies:
|
||||
'@CCR/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
'@fastify/static':
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0
|
||||
'@musistudio/claude-code-router-shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
'@musistudio/llms':
|
||||
specifier: ^1.0.51
|
||||
version: 1.0.51(ws@8.18.3)
|
||||
|
||||
@@ -75,7 +75,20 @@ try {
|
||||
console.warn('⚠ Warning: index.html not found in UI dist, skipping...');
|
||||
}
|
||||
|
||||
console.log('CLI build completed successfully!');
|
||||
// Step 7: Copy CLI dist to project root
|
||||
console.log('\nCopying CLI dist to project root...');
|
||||
const rootDistDir = path.join(rootDir, 'dist');
|
||||
|
||||
// Remove existing dist directory in root if it exists
|
||||
if (fs.existsSync(rootDistDir)) {
|
||||
fs.rmSync(rootDistDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Copy CLI dist to root
|
||||
fs.cpSync(cliDistDir, rootDistDir, { recursive: true });
|
||||
console.log('✓ CLI dist copied to project root successfully!');
|
||||
|
||||
console.log('\nCLI build completed successfully!');
|
||||
console.log('\nCLI dist contents:');
|
||||
const files = fs.readdirSync(cliDistDir);
|
||||
files.forEach(file => {
|
||||
|
||||
@@ -7,18 +7,27 @@ const fs = require('fs');
|
||||
console.log('Building Server package...');
|
||||
|
||||
try {
|
||||
const serverDir = path.join(__dirname, '../packages/server');
|
||||
|
||||
// Create dist directory
|
||||
const distDir = path.join(__dirname, '../packages/server/dist');
|
||||
const distDir = path.join(serverDir, 'dist');
|
||||
if (!fs.existsSync(distDir)) {
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate type declaration files
|
||||
console.log('Generating type declaration files...');
|
||||
execSync('tsc --emitDeclarationOnly', {
|
||||
stdio: 'inherit',
|
||||
cwd: serverDir
|
||||
});
|
||||
|
||||
// Build the server application
|
||||
console.log('Building server application...');
|
||||
// 使用 minify 和 tree-shaking 优化体积
|
||||
execSync('esbuild src/index.ts --bundle --platform=node --minify --tree-shaking=true --outfile=dist/index.js', {
|
||||
stdio: 'inherit',
|
||||
cwd: path.join(__dirname, '../packages/server')
|
||||
cwd: serverDir
|
||||
});
|
||||
|
||||
// Copy the tiktoken WASM file
|
||||
|
||||
151
scripts/release.sh
Executable file
151
scripts/release.sh
Executable file
@@ -0,0 +1,151 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# 发布脚本
|
||||
# - CLI 包作为 @CCR/cli npm 包发布
|
||||
# - Server 包发布为 Docker 镜像
|
||||
|
||||
VERSION=$(node -p "require('../packages/cli/package.json').version")
|
||||
IMAGE_NAME="ccr/router"
|
||||
IMAGE_TAG="${VERSION}"
|
||||
LATEST_TAG="latest"
|
||||
|
||||
echo "========================================="
|
||||
echo "发布 Claude Code Router v${VERSION}"
|
||||
echo "========================================="
|
||||
|
||||
# 获取发布类型参数
|
||||
PUBLISH_TYPE="${1:-all}"
|
||||
|
||||
case "$PUBLISH_TYPE" in
|
||||
npm)
|
||||
echo "仅发布 npm 包..."
|
||||
;;
|
||||
docker)
|
||||
echo "仅发布 Docker 镜像..."
|
||||
;;
|
||||
all)
|
||||
echo "发布 npm 包和 Docker 镜像..."
|
||||
;;
|
||||
*)
|
||||
echo "用法: $0 [npm|docker|all]"
|
||||
echo " npm - 仅发布到 npm"
|
||||
echo " docker - 仅发布到 Docker Hub"
|
||||
echo " all - 发布到 npm 和 Docker Hub (默认)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# ===========================
|
||||
# 发布 npm 包
|
||||
# ===========================
|
||||
publish_npm() {
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "发布 npm 包 @CCR/cli"
|
||||
echo "========================================="
|
||||
|
||||
# 检查是否已登录 npm
|
||||
if ! npm whoami &>/dev/null; then
|
||||
echo "错误: 未登录 npm,请先运行: npm login"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 备份原始 package.json
|
||||
CLI_DIR="../packages/cli"
|
||||
BACKUP_DIR="../packages/cli/.backup"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp "$CLI_DIR/package.json" "$BACKUP_DIR/package.json.bak"
|
||||
|
||||
# 创建临时的发布用 package.json
|
||||
node -e "
|
||||
const pkg = require('../packages/cli/package.json');
|
||||
pkg.name = '@CCR/cli';
|
||||
delete pkg.scripts;
|
||||
pkg.files = ['dist/*', 'README.md', 'LICENSE'];
|
||||
pkg.dependencies = {};
|
||||
// 移除 workspace 依赖
|
||||
delete pkg.dependencies['@CCR/shared'];
|
||||
delete pkg.dependencies['@CCR/server'];
|
||||
pkg.dependencies['@musistudio/llms'] = require('../packages/server/package.json').dependencies['@musistudio/llms'];
|
||||
pkg.peerDependencies = {
|
||||
'node': '>=18.0.0'
|
||||
};
|
||||
pkg.engines = {
|
||||
'node': '>=18.0.0'
|
||||
};
|
||||
require('fs').writeFileSync('../packages/cli/package.publish.json', JSON.stringify(pkg, null, 2));
|
||||
"
|
||||
|
||||
# 使用发布版本的 package.json
|
||||
mv "$CLI_DIR/package.json" "$BACKUP_DIR/package.json.original"
|
||||
mv "$CLI_DIR/package.publish.json" "$CLI_DIR/package.json"
|
||||
|
||||
# 复制 README 和 LICENSE
|
||||
cp ../README.md "$CLI_DIR/"
|
||||
cp ../LICENSE "$CLI_DIR/" 2>/dev/null || echo "LICENSE 文件不存在,跳过..."
|
||||
|
||||
# 发布到 npm
|
||||
cd "$CLI_DIR"
|
||||
echo "执行 npm publish..."
|
||||
npm publish --access public
|
||||
|
||||
# 恢复原始 package.json
|
||||
mv "$BACKUP_DIR/package.json.original" "$CLI_DIR/package.json"
|
||||
|
||||
echo ""
|
||||
echo "✅ npm 包发布成功!"
|
||||
echo " 包名: @CCR/cli@${VERSION}"
|
||||
}
|
||||
|
||||
# ===========================
|
||||
# 发布 Docker 镜像
|
||||
# ===========================
|
||||
publish_docker() {
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "发布 Docker 镜像"
|
||||
echo "========================================="
|
||||
|
||||
# 检查是否已登录 Docker
|
||||
if ! docker info &>/dev/null; then
|
||||
echo "错误: Docker 未运行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 构建 Docker 镜像
|
||||
echo "构建 Docker 镜像 ${IMAGE_NAME}:${IMAGE_TAG}..."
|
||||
docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" -f ../packages/server/Dockerfile ..
|
||||
|
||||
# 标记为 latest
|
||||
echo "标记为 latest..."
|
||||
docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_NAME}:${LATEST_TAG}"
|
||||
|
||||
# 推送到 Docker Hub
|
||||
echo "推送 ${IMAGE_NAME}:${IMAGE_TAG}..."
|
||||
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
echo "推送 ${IMAGE_NAME}:${LATEST_TAG}..."
|
||||
docker push "${IMAGE_NAME}:${LATEST_TAG}"
|
||||
|
||||
echo ""
|
||||
echo "✅ Docker 镜像发布成功!"
|
||||
echo " 镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
echo " 镜像: ${IMAGE_NAME}:latest"
|
||||
}
|
||||
|
||||
# ===========================
|
||||
# 执行发布
|
||||
# ===========================
|
||||
if [ "$PUBLISH_TYPE" = "npm" ] || [ "$PUBLISH_TYPE" = "all" ]; then
|
||||
publish_npm
|
||||
fi
|
||||
|
||||
if [ "$PUBLISH_TYPE" = "docker" ] || [ "$PUBLISH_TYPE" = "all" ]; then
|
||||
publish_docker
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "🎉 发布完成!"
|
||||
echo "========================================="
|
||||
Reference in New Issue
Block a user