support docker

This commit is contained in:
musistudio
2025-12-25 23:00:24 +08:00
parent 6a20b2021d
commit cd7454d7fb
25 changed files with 541 additions and 206 deletions

View File

@@ -1,2 +0,0 @@
node_modules
npm-debug.log

View File

@@ -14,3 +14,4 @@ blog
config.json
ui
scripts
packages

192
CLAUDE.md
View File

@@ -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`

View File

@@ -1,7 +0,0 @@
FROM node:20-alpine
RUN npm install -g @musistudio/claude-code-router
EXPOSE 3456
CMD ["ccr", "start"]

View File

@@ -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

View File

@@ -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",

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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.");
};

View File

@@ -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
}
}
}
}

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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"),

View File

@@ -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 => {

View File

@@ -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

View File

@@ -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",

View File

@@ -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: "",

View File

@@ -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",

View File

@@ -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,7 +308,8 @@ function App() {
</div>
</PopoverContent>
</Popover>
{/* 更新版本按钮 */}
{/* 更新版本按钮 - 仅当更新功能可用时显示 */}
{isUpdateFeatureAvailable && (
<Button
variant="ghost"
size="icon"
@@ -326,6 +329,7 @@ function App() {
</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
View File

@@ -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)

View File

@@ -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 => {

View 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
View 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 "========================================="