From 085ee97cdc45838d657fb46cbc1152c23ad66a3a Mon Sep 17 00:00:00 2001 From: musistudio Date: Fri, 26 Dec 2025 16:48:32 +0800 Subject: [PATCH] add presets --- packages/cli/src/cli.ts | 133 ++++++++++++++++--- packages/cli/src/utils/codeCommand.ts | 43 +++++- packages/cli/src/utils/createEnvVariables.ts | 3 +- packages/cli/src/utils/index.ts | 22 +++ packages/shared/src/constants.ts | 2 + 5 files changed, 182 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index ba65920..43aa644 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { run } from "./utils"; import { showStatus } from "./utils/status"; -import { executeCodeCommand } from "./utils/codeCommand"; +import { executeCodeCommand, PresetConfig } from "./utils/codeCommand"; import { cleanupPidFile, isServiceRunning, @@ -9,6 +9,7 @@ import { } from "./utils/processCheck"; import { runModelSelector } from "./utils/modelSelector"; import { activateCommand } from "./utils/activateCommand"; +import { readConfigFile } from "./utils"; import { version } from "../package.json"; import { spawn, exec } from "child_process"; import { PID_FILE, REFERENCE_COUNT_FILE } from "@CCR/shared"; @@ -18,8 +19,26 @@ import { parseStatusLineData, StatusLineInput } from "./utils/statusline"; const command = process.argv[2]; +// 定义所有已知命令 +const KNOWN_COMMANDS = [ + "start", + "stop", + "restart", + "status", + "statusline", + "code", + "model", + "activate", + "env", + "ui", + "-v", + "version", + "-h", + "help", +]; + const HELP_TEXT = ` -Usage: ccr [command] +Usage: ccr [command] [preset-name] Commands: start Start server @@ -34,9 +53,13 @@ Commands: -v, version Show version information -h, help Show help information -Example: +Presets: + Any preset-name defined in ~/.claude-code-router/presets/*.ccrsets + +Examples: ccr start ccr code "Write a Hello World" + ccr my-preset "Write a Hello World" # Use preset configuration ccr model eval "$(ccr activate)" # Set environment variables globally ccr ui @@ -64,6 +87,96 @@ async function waitForService( async function main() { const isRunning = await isServiceRunning() + + // 如果命令不是已知命令,检查是否是 preset + if (command && !KNOWN_COMMANDS.includes(command)) { + const { readPresetFile } = await import("./utils"); + const presetConfig: PresetConfig | null = await readPresetFile(command); + + if (presetConfig) { + // 这是一个 preset,执行 code 命令 + const codeArgs = process.argv.slice(3); // 获取剩余参数 + + // 检查 noServer 配置 + const shouldStartServer = presetConfig.noServer !== true; + + // 处理 provider 配置,构建环境变量覆盖 + let envOverrides: Record | undefined; + if (presetConfig.provider) { + const config = await readConfigFile(); + const providerName = presetConfig.provider; + const provider = config.Providers?.find((p: any) => p.name === providerName); + + if (provider) { + // 处理 api_base_url,去掉 /v1/messages 后缀 + if (provider.api_base_url) { + let baseUrl = provider.api_base_url; + if (baseUrl.endsWith('/v1/messages')) { + baseUrl = baseUrl.slice(0, -'/v1/messages'.length); + } else if (baseUrl.endsWith('/')) { + baseUrl = baseUrl.slice(0, -1); + } + envOverrides = { + ...envOverrides, + ANTHROPIC_BASE_URL: baseUrl, + }; + } + + // 处理 api_key + if (provider.api_key) { + envOverrides = { + ...envOverrides, + ANTHROPIC_AUTH_TOKEN: provider.api_key, + }; + } + } else { + console.error(`Provider "${providerName}" not found in config`); + process.exit(1); + } + } + + // TODO: 处理 router 配置 + // 如果 preset 中有 router 配置且需要启动 server,可能需要临时修改配置文件 + + if (shouldStartServer && !isRunning) { + console.log("Service not running, starting service..."); + const cliPath = 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.message); + process.exit(1); + }); + + startProcess.unref(); + + if (await waitForService()) { + executeCodeCommand(codeArgs, presetConfig, envOverrides); + } else { + console.error( + "Service startup timeout, please manually run `ccr start` to start the service" + ); + process.exit(1); + } + } else { + // 服务已运行或不需要启动 server + if (shouldStartServer && !isRunning) { + console.error("Service is not running. Please start it first with `ccr start`"); + process.exit(1); + } + executeCodeCommand(codeArgs, presetConfig, envOverrides); + } + return; + } else { + // 不是 preset 也不是已知命令 + console.log(HELP_TEXT); + process.exit(1); + } + } + switch (command) { case "start": await run(); @@ -132,27 +245,14 @@ async function main() { stdio: "ignore", }); - // let errorMessage = ""; - // startProcess.stderr?.on("data", (data) => { - // errorMessage += data.toString(); - // }); - startProcess.on("error", (error) => { console.error("Failed to start service:", error.message); process.exit(1); }); - // startProcess.on("close", (code) => { - // if (code !== 0 && errorMessage) { - // console.error("Failed to start service:", errorMessage.trim()); - // process.exit(1); - // } - // }); - startProcess.unref(); if (await waitForService()) { - // Join all code arguments into a single string to preserve spaces within quotes const codeArgs = process.argv.slice(3); executeCodeCommand(codeArgs); } else { @@ -162,7 +262,6 @@ async function main() { process.exit(1); } } else { - // Join all code arguments into a single string to preserve spaces within quotes const codeArgs = process.argv.slice(3); executeCodeCommand(codeArgs); } diff --git a/packages/cli/src/utils/codeCommand.ts b/packages/cli/src/utils/codeCommand.ts index 3a0cd62..198137c 100644 --- a/packages/cli/src/utils/codeCommand.ts +++ b/packages/cli/src/utils/codeCommand.ts @@ -9,14 +9,38 @@ import { quote } from 'shell-quote'; import minimist from "minimist"; import { createEnvVariables } from "./createEnvVariables"; +export interface PresetConfig { + noServer?: boolean; + claudeCodeSettings?: { + env?: Record; + statusLine?: any; + [key: string]: any; + }; + provider?: string; + router?: Record; + [key: string]: any; +} -export async function executeCodeCommand(args: string[] = []) { +export async function executeCodeCommand( + args: string[] = [], + presetConfig?: PresetConfig | null, + envOverrides?: Record +) { // Set environment variables using shared function const config = await readConfigFile(); const env = await createEnvVariables(); - const settingsFlag: ClaudeSettingsFlag = { + + // 应用环境变量覆盖(从 preset 的 provider 配置中获取) + if (envOverrides) { + Object.assign(env, envOverrides); + } + + // 构建 settingsFlag + let settingsFlag: ClaudeSettingsFlag = { env: env as ClaudeSettingsFlag['env'] }; + + // 如果配置了 StatusLine,添加 statusLine if (config?.StatusLine?.enabled) { settingsFlag.statusLine = { type: "command", @@ -24,7 +48,22 @@ export async function executeCodeCommand(args: string[] = []) { padding: 0, } } + + // 如果 preset 中有 claudeCodeSettings,合并到 settingsFlag 中 + if (presetConfig?.claudeCodeSettings) { + settingsFlag = { + ...settingsFlag, + ...presetConfig.claudeCodeSettings, + // 深度合并 env + env: { + ...settingsFlag.env, + ...presetConfig.claudeCodeSettings.env, + } as ClaudeSettingsFlag['env'] + }; + } + args.push('--settings', getSettingsPath(`${JSON.stringify(settingsFlag)}`)); + console.log(args) // Non-interactive mode for automation environments if (config.NON_INTERACTIVE_MODE) { diff --git a/packages/cli/src/utils/createEnvVariables.ts b/packages/cli/src/utils/createEnvVariables.ts index 437c0aa..01ee7ca 100644 --- a/packages/cli/src/utils/createEnvVariables.ts +++ b/packages/cli/src/utils/createEnvVariables.ts @@ -11,7 +11,6 @@ export const createEnvVariables = async (): Promise { export const initDir = async () => { await ensureDir(HOME_DIR); await ensureDir(PLUGINS_DIR); + await ensureDir(PRESETS_DIR); await ensureDir(path.join(HOME_DIR, "logs")); }; @@ -272,3 +274,23 @@ export const getSettingsPath = (content: string): string => { return tempFilePath; } + +/** + * 读取 preset 配置文件 + * @param name preset 名称 + * @returns preset 配置对象,如果文件不存在则返回 null + */ +export const readPresetFile = async (name: string): Promise => { + try { + const presetFile = path.join(PRESETS_DIR, `${name}.ccrsets`); + const content = await fs.readFile(presetFile, 'utf-8'); + return JSON5.parse(content); + } catch (error: any) { + if (error.code === 'ENOENT') { + console.error(`Preset file not found: ${name}.ccrsets`); + } else { + console.error(`Failed to read preset file: ${error.message}`); + } + return null; + } +} diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 4af2fda..f94c8fd 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -7,6 +7,8 @@ export const CONFIG_FILE = path.join(HOME_DIR, "config.json"); export const PLUGINS_DIR = path.join(HOME_DIR, "plugins"); +export const PRESETS_DIR = path.join(HOME_DIR, "presets"); + export const PID_FILE = path.join(HOME_DIR, '.claude-code-router.pid'); export const REFERENCE_COUNT_FILE = path.join(os.tmpdir(), "claude-code-reference-count.txt");