mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-01-30 06:12:06 +00:00
add presets
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user