add presets

This commit is contained in:
musistudio
2025-12-26 16:48:32 +08:00
parent 983297b781
commit 085ee97cdc
5 changed files with 182 additions and 21 deletions

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import { run } from "./utils"; import { run } from "./utils";
import { showStatus } from "./utils/status"; import { showStatus } from "./utils/status";
import { executeCodeCommand } from "./utils/codeCommand"; import { executeCodeCommand, PresetConfig } from "./utils/codeCommand";
import { import {
cleanupPidFile, cleanupPidFile,
isServiceRunning, isServiceRunning,
@@ -9,6 +9,7 @@ import {
} from "./utils/processCheck"; } from "./utils/processCheck";
import { runModelSelector } from "./utils/modelSelector"; import { runModelSelector } from "./utils/modelSelector";
import { activateCommand } from "./utils/activateCommand"; import { activateCommand } from "./utils/activateCommand";
import { readConfigFile } from "./utils";
import { version } from "../package.json"; import { version } from "../package.json";
import { spawn, exec } from "child_process"; import { spawn, exec } from "child_process";
import { PID_FILE, REFERENCE_COUNT_FILE } from "@CCR/shared"; 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 command = process.argv[2];
// 定义所有已知命令
const KNOWN_COMMANDS = [
"start",
"stop",
"restart",
"status",
"statusline",
"code",
"model",
"activate",
"env",
"ui",
"-v",
"version",
"-h",
"help",
];
const HELP_TEXT = ` const HELP_TEXT = `
Usage: ccr [command] Usage: ccr [command] [preset-name]
Commands: Commands:
start Start server start Start server
@@ -34,9 +53,13 @@ Commands:
-v, version Show version information -v, version Show version information
-h, help Show help information -h, help Show help information
Example: Presets:
Any preset-name defined in ~/.claude-code-router/presets/*.ccrsets
Examples:
ccr start ccr start
ccr code "Write a Hello World" ccr code "Write a Hello World"
ccr my-preset "Write a Hello World" # Use preset configuration
ccr model ccr model
eval "$(ccr activate)" # Set environment variables globally eval "$(ccr activate)" # Set environment variables globally
ccr ui ccr ui
@@ -64,6 +87,96 @@ async function waitForService(
async function main() { async function main() {
const isRunning = await isServiceRunning() 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<string, string> | 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) { switch (command) {
case "start": case "start":
await run(); await run();
@@ -132,27 +245,14 @@ async function main() {
stdio: "ignore", stdio: "ignore",
}); });
// let errorMessage = "";
// startProcess.stderr?.on("data", (data) => {
// errorMessage += data.toString();
// });
startProcess.on("error", (error) => { startProcess.on("error", (error) => {
console.error("Failed to start service:", error.message); console.error("Failed to start service:", error.message);
process.exit(1); process.exit(1);
}); });
// startProcess.on("close", (code) => {
// if (code !== 0 && errorMessage) {
// console.error("Failed to start service:", errorMessage.trim());
// process.exit(1);
// }
// });
startProcess.unref(); startProcess.unref();
if (await waitForService()) { if (await waitForService()) {
// Join all code arguments into a single string to preserve spaces within quotes
const codeArgs = process.argv.slice(3); const codeArgs = process.argv.slice(3);
executeCodeCommand(codeArgs); executeCodeCommand(codeArgs);
} else { } else {
@@ -162,7 +262,6 @@ async function main() {
process.exit(1); process.exit(1);
} }
} else { } else {
// Join all code arguments into a single string to preserve spaces within quotes
const codeArgs = process.argv.slice(3); const codeArgs = process.argv.slice(3);
executeCodeCommand(codeArgs); executeCodeCommand(codeArgs);
} }

View File

@@ -9,14 +9,38 @@ import { quote } from 'shell-quote';
import minimist from "minimist"; import minimist from "minimist";
import { createEnvVariables } from "./createEnvVariables"; import { createEnvVariables } from "./createEnvVariables";
export interface PresetConfig {
noServer?: boolean;
claudeCodeSettings?: {
env?: Record<string, any>;
statusLine?: any;
[key: string]: any;
};
provider?: string;
router?: Record<string, any>;
[key: string]: any;
}
export async function executeCodeCommand(args: string[] = []) { export async function executeCodeCommand(
args: string[] = [],
presetConfig?: PresetConfig | null,
envOverrides?: Record<string, string>
) {
// Set environment variables using shared function // Set environment variables using shared function
const config = await readConfigFile(); const config = await readConfigFile();
const env = await createEnvVariables(); const env = await createEnvVariables();
const settingsFlag: ClaudeSettingsFlag = {
// 应用环境变量覆盖(从 preset 的 provider 配置中获取)
if (envOverrides) {
Object.assign(env, envOverrides);
}
// 构建 settingsFlag
let settingsFlag: ClaudeSettingsFlag = {
env: env as ClaudeSettingsFlag['env'] env: env as ClaudeSettingsFlag['env']
}; };
// 如果配置了 StatusLine添加 statusLine
if (config?.StatusLine?.enabled) { if (config?.StatusLine?.enabled) {
settingsFlag.statusLine = { settingsFlag.statusLine = {
type: "command", type: "command",
@@ -24,7 +48,22 @@ export async function executeCodeCommand(args: string[] = []) {
padding: 0, 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)}`)); args.push('--settings', getSettingsPath(`${JSON.stringify(settingsFlag)}`));
console.log(args)
// Non-interactive mode for automation environments // Non-interactive mode for automation environments
if (config.NON_INTERACTIVE_MODE) { if (config.NON_INTERACTIVE_MODE) {

View File

@@ -11,7 +11,6 @@ export const createEnvVariables = async (): Promise<Record<string, string | unde
return { return {
ANTHROPIC_AUTH_TOKEN: apiKey, ANTHROPIC_AUTH_TOKEN: apiKey,
ANTHROPIC_API_KEY: "",
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`, ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
NO_PROXY: "127.0.0.1", NO_PROXY: "127.0.0.1",
DISABLE_TELEMETRY: "true", DISABLE_TELEMETRY: "true",

View File

@@ -7,6 +7,7 @@ import {
CONFIG_FILE, CONFIG_FILE,
HOME_DIR, PID_FILE, HOME_DIR, PID_FILE,
PLUGINS_DIR, PLUGINS_DIR,
PRESETS_DIR,
REFERENCE_COUNT_FILE, REFERENCE_COUNT_FILE,
} from "@CCR/shared"; } from "@CCR/shared";
import { getServer } from "@CCR/server"; import { getServer } from "@CCR/server";
@@ -47,6 +48,7 @@ const ensureDir = async (dir_path: string) => {
export const initDir = async () => { export const initDir = async () => {
await ensureDir(HOME_DIR); await ensureDir(HOME_DIR);
await ensureDir(PLUGINS_DIR); await ensureDir(PLUGINS_DIR);
await ensureDir(PRESETS_DIR);
await ensureDir(path.join(HOME_DIR, "logs")); await ensureDir(path.join(HOME_DIR, "logs"));
}; };
@@ -272,3 +274,23 @@ export const getSettingsPath = (content: string): string => {
return tempFilePath; return tempFilePath;
} }
/**
* 读取 preset 配置文件
* @param name preset 名称
* @returns preset 配置对象,如果文件不存在则返回 null
*/
export const readPresetFile = async (name: string): Promise<any | null> => {
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;
}
}

View File

@@ -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 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 PID_FILE = path.join(HOME_DIR, '.claude-code-router.pid');
export const REFERENCE_COUNT_FILE = path.join(os.tmpdir(), "claude-code-reference-count.txt"); export const REFERENCE_COUNT_FILE = path.join(os.tmpdir(), "claude-code-reference-count.txt");