mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-01-29 22:02:05 +00:00
translate comment to english
This commit is contained in:
@@ -21,7 +21,7 @@ import {handlePresetCommand} from "./utils/preset";
|
||||
|
||||
const command = process.argv[2];
|
||||
|
||||
// 定义所有已知命令
|
||||
// Define all known commands
|
||||
const KNOWN_COMMANDS = [
|
||||
"start",
|
||||
"stop",
|
||||
@@ -95,34 +95,34 @@ async function waitForService(
|
||||
async function main() {
|
||||
const isRunning = await isServiceRunning()
|
||||
|
||||
// 如果命令不是已知命令,检查是否是 preset
|
||||
// If command is not a known command, check if it's a preset
|
||||
if (command && !KNOWN_COMMANDS.includes(command)) {
|
||||
const presetData: any = await readPresetFile(command);
|
||||
|
||||
if (presetData) {
|
||||
// 这是一个 preset,执行 code 命令
|
||||
const codeArgs = process.argv.slice(3); // 获取剩余参数
|
||||
// This is a preset, execute code command
|
||||
const codeArgs = process.argv.slice(3); // Get remaining arguments
|
||||
|
||||
// 检查 noServer 配置
|
||||
// Check noServer configuration
|
||||
const shouldStartServer = presetData.noServer !== true;
|
||||
|
||||
// 构建环境变量覆盖
|
||||
// Build environment variable overrides
|
||||
let envOverrides: Record<string, string> | undefined;
|
||||
|
||||
// 处理 provider 配置(支持新旧两种格式)
|
||||
// Handle provider configuration (supports both old and new formats)
|
||||
let provider: any = null;
|
||||
|
||||
// 旧格式:presetData.provider 是 provider 名称
|
||||
// Old format: presetData.provider is the provider name
|
||||
if (presetData.provider && typeof presetData.provider === 'string') {
|
||||
const config = await readConfigFile();
|
||||
provider = config.Providers?.find((p: any) => p.name === presetData.provider);
|
||||
}
|
||||
// 新格式:presetData.Providers 是 provider 数组
|
||||
// New format: presetData.Providers is an array of providers
|
||||
else if (presetData.Providers && presetData.Providers.length > 0) {
|
||||
provider = presetData.Providers[0];
|
||||
}
|
||||
|
||||
// 如果 noServer 不为 true,使用本地 server 的 baseurl
|
||||
// If noServer is not true, use local server baseurl
|
||||
if (shouldStartServer) {
|
||||
const config = await readConfigFile();
|
||||
const port = config.PORT || 3456;
|
||||
@@ -132,7 +132,7 @@ async function main() {
|
||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}/preset/${presetName}`,
|
||||
};
|
||||
} else if (provider) {
|
||||
// 处理 api_base_url,去掉 /v1/messages 后缀
|
||||
// Handle api_base_url, remove /v1/messages suffix
|
||||
if (provider.api_base_url) {
|
||||
let baseUrl = provider.api_base_url;
|
||||
if (baseUrl.endsWith('/v1/messages')) {
|
||||
@@ -146,7 +146,7 @@ async function main() {
|
||||
};
|
||||
}
|
||||
|
||||
// 处理 api_key
|
||||
// Handle api_key
|
||||
if (provider.api_key) {
|
||||
envOverrides = {
|
||||
...envOverrides,
|
||||
@@ -155,7 +155,7 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 构建 PresetConfig
|
||||
// Build PresetConfig
|
||||
const presetConfig: PresetConfig = {
|
||||
noServer: presetData.noServer,
|
||||
claudeCodeSettings: presetData.claudeCodeSettings,
|
||||
@@ -187,7 +187,7 @@ async function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// 服务已运行或不需要启动 server
|
||||
// Service is already running or no need to start server
|
||||
if (shouldStartServer && !isRunning) {
|
||||
console.error("Service is not running. Please start it first with `ccr start`");
|
||||
process.exit(1);
|
||||
@@ -196,7 +196,7 @@ async function main() {
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// 不是 preset 也不是已知命令
|
||||
// Not a preset nor a known command
|
||||
console.log(HELP_TEXT);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -232,7 +232,7 @@ async function main() {
|
||||
await showStatus();
|
||||
break;
|
||||
case "statusline":
|
||||
// 从stdin读取JSON输入
|
||||
// Read JSON input from stdin
|
||||
let inputData = "";
|
||||
process.stdin.setEncoding("utf-8");
|
||||
process.stdin.on("readable", () => {
|
||||
|
||||
@@ -30,17 +30,17 @@ export async function executeCodeCommand(
|
||||
const config = await readConfigFile();
|
||||
const env = await createEnvVariables();
|
||||
|
||||
// 应用环境变量覆盖(从 preset 的 provider 配置中获取)
|
||||
// Apply environment variable overrides (from preset's provider configuration)
|
||||
if (envOverrides) {
|
||||
Object.assign(env, envOverrides);
|
||||
}
|
||||
|
||||
// 构建 settingsFlag
|
||||
// Build settingsFlag
|
||||
let settingsFlag: ClaudeSettingsFlag = {
|
||||
env: env as ClaudeSettingsFlag['env']
|
||||
};
|
||||
|
||||
// 如果配置了 StatusLine,添加 statusLine
|
||||
// Add statusLine if StatusLine is configured
|
||||
if (config?.StatusLine?.enabled) {
|
||||
settingsFlag.statusLine = {
|
||||
type: "command",
|
||||
@@ -49,12 +49,12 @@ export async function executeCodeCommand(
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 preset 中有 claudeCodeSettings,合并到 settingsFlag 中
|
||||
// Merge claudeCodeSettings from preset into settingsFlag
|
||||
if (presetConfig?.claudeCodeSettings) {
|
||||
settingsFlag = {
|
||||
...settingsFlag,
|
||||
...presetConfig.claudeCodeSettings,
|
||||
// 深度合并 env
|
||||
// Deep merge env
|
||||
env: {
|
||||
...settingsFlag.env,
|
||||
...presetConfig.claudeCodeSettings.env,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* 预设导出功能 CLI 层
|
||||
* 负责处理 CLI 交互,核心逻辑在 shared 包中
|
||||
* Preset export functionality CLI layer
|
||||
* Handles CLI interactions, core logic is in shared package
|
||||
*/
|
||||
|
||||
import { input } from '@inquirer/prompts';
|
||||
import { readConfigFile } from '../index';
|
||||
import { exportPreset as exportPresetCore, ExportOptions } from '@CCR/shared';
|
||||
|
||||
// ANSI 颜色代码
|
||||
// ANSI color codes
|
||||
const RESET = "\x1B[0m";
|
||||
const GREEN = "\x1B[32m";
|
||||
const BOLDGREEN = "\x1B[1m\x1B[32m";
|
||||
@@ -15,9 +15,9 @@ const YELLOW = "\x1B[33m";
|
||||
const BOLDCYAN = "\x1B[1m\x1B[36m";
|
||||
|
||||
/**
|
||||
* 导出预设配置(CLI 版本,带交互)
|
||||
* @param presetName 预设名称
|
||||
* @param options 导出选项
|
||||
* Export preset configuration (CLI version, with interaction)
|
||||
* @param presetName Preset name
|
||||
* @param options Export options
|
||||
*/
|
||||
export async function exportPresetCli(
|
||||
presetName: string,
|
||||
@@ -28,10 +28,10 @@ export async function exportPresetCli(
|
||||
console.log(`${BOLDCYAN} Preset Export${RESET}`);
|
||||
console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\n`);
|
||||
|
||||
// 1. 读取当前配置
|
||||
// 1. Read current configuration
|
||||
const config = await readConfigFile();
|
||||
|
||||
// 2. 如果没有通过命令行提供,交互式询问元数据
|
||||
// 2. Interactively ask for metadata if not provided via command line
|
||||
if (!options.description) {
|
||||
try {
|
||||
options.description = await input({
|
||||
@@ -39,7 +39,7 @@ export async function exportPresetCli(
|
||||
default: '',
|
||||
});
|
||||
} catch {
|
||||
// 用户取消,使用默认值
|
||||
// User cancelled, use default value
|
||||
options.description = '';
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,10 @@ export async function exportPresetCli(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 调用核心导出功能
|
||||
// 3. Call core export functionality
|
||||
const result = await exportPresetCore(presetName, config, options);
|
||||
|
||||
// 4. 显示摘要
|
||||
// 4. Display summary
|
||||
console.log(`\n${BOLDGREEN}✓ Preset exported successfully${RESET}\n`);
|
||||
console.log(`${BOLDCYAN}Location:${RESET} ${result.outputPath}\n`);
|
||||
console.log(`${BOLDCYAN}Summary:${RESET}`);
|
||||
@@ -80,7 +80,7 @@ export async function exportPresetCli(
|
||||
console.log(` - Sensitive fields sanitized: ${YELLOW}${result.sanitizedCount}${RESET}`);
|
||||
}
|
||||
|
||||
// 显示元数据
|
||||
// Display metadata
|
||||
if (result.metadata.description) {
|
||||
console.log(`\n${BOLDCYAN}Description:${RESET} ${result.metadata.description}`);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export async function exportPresetCli(
|
||||
console.log(`${BOLDCYAN}Keywords:${RESET} ${result.metadata.keywords.join(', ')}`);
|
||||
}
|
||||
|
||||
// 显示分享提示
|
||||
// Display sharing tips
|
||||
console.log(`\n${BOLDCYAN}To share this preset:${RESET}`);
|
||||
console.log(` 1. Share the file: ${result.outputPath}`);
|
||||
console.log(` 2. Upload to GitHub Gist or your repository`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 动态配置 CLI 交互处理器
|
||||
* 处理各种输入类型的用户交互
|
||||
* Dynamic configuration CLI interaction handler
|
||||
* Handles user interactions for various input types
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -22,7 +22,7 @@ import password from '@inquirer/password';
|
||||
import checkbox from '@inquirer/checkbox';
|
||||
import editor from '@inquirer/editor';
|
||||
|
||||
// ANSI 颜色代码
|
||||
// ANSI color codes
|
||||
export const COLORS = {
|
||||
RESET: "\x1B[0m",
|
||||
GREEN: "\x1B[32m",
|
||||
@@ -34,41 +34,41 @@ export const COLORS = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 收集用户输入(支持动态配置)
|
||||
* Collect user input (supports dynamic configuration)
|
||||
*/
|
||||
export async function collectUserInputs(
|
||||
schema: RequiredInput[],
|
||||
presetConfig: PresetConfigSection,
|
||||
existingValues?: UserInputValues
|
||||
): Promise<UserInputValues> {
|
||||
// 按依赖关系排序
|
||||
// Sort by dependencies
|
||||
const sortedFields = sortFieldsByDependencies(schema);
|
||||
|
||||
// 初始化值
|
||||
// Initialize values
|
||||
const values: UserInputValues = { ...existingValues };
|
||||
|
||||
// 收集所有输入
|
||||
// Collect all inputs
|
||||
for (const field of sortedFields) {
|
||||
// 检查是否应该显示此字段
|
||||
// Check if this field should be displayed
|
||||
if (!shouldShowField(field, values)) {
|
||||
// 跳过,并清除该字段的值(如果之前存在)
|
||||
// Skip and clear the field value (if it existed before)
|
||||
delete values[field.id];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果已有值且不是初始收集,跳过
|
||||
// Skip if value already exists and not initial collection
|
||||
if (existingValues && field.id in existingValues) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取输入值
|
||||
// Get input value
|
||||
const value = await promptField(field, presetConfig, values);
|
||||
|
||||
// 验证
|
||||
// Validate
|
||||
const validation = validateInput(field, value);
|
||||
if (!validation.valid) {
|
||||
console.error(`${COLORS.YELLOW}Error:${COLORS.RESET} ${validation.error}`);
|
||||
// 对于必填字段,抛出错误
|
||||
// Throw error for required fields
|
||||
if (field.required !== false) {
|
||||
throw new Error(validation.error);
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export async function collectUserInputs(
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新收集受影响的字段(当某个字段值变化时)
|
||||
* Recollect affected fields (when a field value changes)
|
||||
*/
|
||||
export async function recollectAffectedFields(
|
||||
changedFieldId: string,
|
||||
@@ -95,24 +95,24 @@ export async function recollectAffectedFields(
|
||||
|
||||
const values = { ...currentValues };
|
||||
|
||||
// 对受影响的字段重新收集输入
|
||||
// Recollect input for affected fields
|
||||
for (const fieldId of affectedFields) {
|
||||
const field = sortedFields.find(f => f.id === fieldId);
|
||||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否应该显示
|
||||
// Check if should be displayed
|
||||
if (!shouldShowField(field, values)) {
|
||||
delete values[field.id];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 重新收集输入
|
||||
// Recollect input
|
||||
const value = await promptField(field, presetConfig, values);
|
||||
values[field.id] = value;
|
||||
|
||||
// 级联更新:如果这个字段的变化又影响了其他字段
|
||||
// Cascade update: if this field change affects other fields
|
||||
const newAffected = getAffectedFields(field.id, schema);
|
||||
for (const newAffectedId of newAffected) {
|
||||
if (!affectedFields.has(newAffectedId)) {
|
||||
@@ -125,7 +125,7 @@ export async function recollectAffectedFields(
|
||||
}
|
||||
|
||||
/**
|
||||
* 提示单个字段
|
||||
* Prompt for a single field
|
||||
*/
|
||||
async function promptField(
|
||||
field: RequiredInput,
|
||||
@@ -187,7 +187,7 @@ async function promptField(
|
||||
return field.defaultValue ?? [];
|
||||
}
|
||||
|
||||
// @inquirer/prompts 没有多选,使用 checkbox
|
||||
// @inquirer/prompts doesn't have multi-select, use checkbox
|
||||
return await checkbox({
|
||||
message,
|
||||
choices: options.map(opt => ({
|
||||
@@ -206,7 +206,7 @@ async function promptField(
|
||||
}
|
||||
|
||||
default:
|
||||
// 默认使用 input
|
||||
// Use input by default
|
||||
return await input({
|
||||
message,
|
||||
default: field.defaultValue,
|
||||
@@ -215,7 +215,7 @@ async function promptField(
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集敏感信息(兼容旧版)
|
||||
* Collect sensitive information (legacy compatible)
|
||||
*/
|
||||
export async function collectSensitiveInputs(
|
||||
schema: RequiredInput[],
|
||||
@@ -226,7 +226,7 @@ export async function collectSensitiveInputs(
|
||||
|
||||
const values = await collectUserInputs(schema, presetConfig, existingValues);
|
||||
|
||||
// 显示摘要
|
||||
// Display summary
|
||||
console.log(`${COLORS.GREEN}✓${COLORS.RESET} All required information collected\n`);
|
||||
|
||||
return values;
|
||||
|
||||
@@ -6,37 +6,37 @@ import { readFileSync } from "fs";
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
/**
|
||||
* 检查是否有新版本可用
|
||||
* @param currentVersion 当前版本
|
||||
* @returns 包含更新信息的对象
|
||||
* Check if new version is available
|
||||
* @param currentVersion Current version
|
||||
* @returns Object containing update information
|
||||
*/
|
||||
export async function checkForUpdates(currentVersion: string) {
|
||||
try {
|
||||
// 从npm registry获取最新版本信息
|
||||
// Get latest version info from npm registry
|
||||
const { stdout } = await execPromise("npm view @musistudio/claude-code-router version");
|
||||
const latestVersion = stdout.trim();
|
||||
|
||||
// 比较版本
|
||||
// Compare versions
|
||||
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
|
||||
|
||||
// 如果有更新,获取更新日志
|
||||
// If there is an update, get changelog
|
||||
let changelog = "";
|
||||
|
||||
return { hasUpdate, latestVersion, changelog };
|
||||
} catch (error) {
|
||||
console.error("Error checking for updates:", error);
|
||||
// 如果检查失败,假设没有更新
|
||||
// If check fails, assume no update
|
||||
return { hasUpdate: false, latestVersion: currentVersion, changelog: "" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行更新操作
|
||||
* @returns 更新结果
|
||||
* Perform update operation
|
||||
* @returns Update result
|
||||
*/
|
||||
export async function performUpdate() {
|
||||
try {
|
||||
// 执行npm update命令
|
||||
// Execute npm update command
|
||||
const { stdout, stderr } = await execPromise("npm update -g @musistudio/claude-code-router");
|
||||
|
||||
if (stderr) {
|
||||
@@ -59,9 +59,9 @@ export async function performUpdate() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个版本号
|
||||
* @param v1 版本号1
|
||||
* @param v2 版本号2
|
||||
* Compare two version numbers
|
||||
* @param v1 Version number 1
|
||||
* @param v2 Version number 2
|
||||
* @returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
||||
*/
|
||||
function compareVersions(v1: string, v2: string): number {
|
||||
|
||||
@@ -235,14 +235,14 @@ export class ForceReasoningTransformer implements Transformer {
|
||||
}
|
||||
} else if (fsmState === "FINAL") {
|
||||
if (currentContent.length > 0) {
|
||||
// 检查内容是否只包含换行符
|
||||
// Check if content contains only newlines
|
||||
const isOnlyNewlines = /^\s*$/.test(currentContent);
|
||||
|
||||
if (isOnlyNewlines) {
|
||||
// 如果只有换行符,添加到缓冲区但不发送
|
||||
// If only newlines, add to buffer but don't send
|
||||
finalBuffer += currentContent;
|
||||
} else {
|
||||
// 如果有非换行符内容,将缓冲区和新内容一起发送
|
||||
// If non-whitespace content, send buffer and new content together
|
||||
const finalPart = finalBuffer + currentContent;
|
||||
const newDelta = {
|
||||
...originalData.choices[0].delta,
|
||||
@@ -258,7 +258,7 @@ export class ForceReasoningTransformer implements Transformer {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify(finalChunk)}\n\n`)
|
||||
);
|
||||
// 发送后清空缓冲区
|
||||
// Clear buffer after sending
|
||||
finalBuffer = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ Examples:
|
||||
const encoder = new TextEncoder();
|
||||
let exitToolIndex = -1;
|
||||
let exitToolResponse = "";
|
||||
let buffer = ""; // 用于缓冲不完整的数据
|
||||
let buffer = ""; // Buffer for incomplete data
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
@@ -188,7 +188,7 @@ Examples:
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error processing line:", line, error);
|
||||
// 如果解析失败,直接传递原始行
|
||||
// If parsing fails, pass through the original line
|
||||
controller.enqueue(encoder.encode(line + "\n"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export function convertToOpenAI(
|
||||
request: UnifiedChatRequest
|
||||
): OpenAIChatRequest {
|
||||
const messages: OpenAIMessage[] = [];
|
||||
const toolResponsesQueue: Map<string, any> = new Map(); // 用于存储工具响应
|
||||
const toolResponsesQueue: Map<string, any> = new Map(); // For storing tool responses
|
||||
|
||||
request.messages.forEach((msg) => {
|
||||
if (msg.role === "tool" && msg.tool_call_id) {
|
||||
|
||||
@@ -91,7 +91,7 @@ const getProjectSpecificRouter = async (
|
||||
req: any,
|
||||
configService: ConfigService
|
||||
) => {
|
||||
// 检查是否有项目特定的配置
|
||||
// Check if there is project-specific configuration
|
||||
if (req.sessionId) {
|
||||
const project = await searchProjectBySession(req.sessionId);
|
||||
if (project) {
|
||||
@@ -102,7 +102,7 @@ const getProjectSpecificRouter = async (
|
||||
`${req.sessionId}.json`
|
||||
);
|
||||
|
||||
// 首先尝试读取sessionConfig文件
|
||||
// First try to read sessionConfig file
|
||||
try {
|
||||
const sessionConfig = JSON.parse(await readFile(sessionConfigPath, "utf8"));
|
||||
if (sessionConfig && sessionConfig.Router) {
|
||||
@@ -117,7 +117,7 @@ const getProjectSpecificRouter = async (
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return undefined; // 返回undefined表示使用原始配置
|
||||
return undefined; // Return undefined to use original configuration
|
||||
};
|
||||
|
||||
const getUseModel = async (
|
||||
@@ -256,9 +256,9 @@ export const router = async (req: any, _res: any, context: RouterContext) => {
|
||||
return;
|
||||
};
|
||||
|
||||
// 内存缓存,存储sessionId到项目名称的映射
|
||||
// null值表示之前已查找过但未找到项目
|
||||
// 使用LRU缓存,限制最大1000个条目
|
||||
// Memory cache for sessionId to project name mapping
|
||||
// null value indicates previously searched but not found
|
||||
// Uses LRU cache with max 1000 entries
|
||||
const sessionProjectCache = new LRUCache<string, string>({
|
||||
max: 1000,
|
||||
});
|
||||
@@ -266,7 +266,7 @@ const sessionProjectCache = new LRUCache<string, string>({
|
||||
export const searchProjectBySession = async (
|
||||
sessionId: string
|
||||
): Promise<string | null> => {
|
||||
// 首先检查缓存
|
||||
// Check cache first
|
||||
if (sessionProjectCache.has(sessionId)) {
|
||||
const result = sessionProjectCache.get(sessionId);
|
||||
if (!result || result === '') {
|
||||
@@ -279,14 +279,14 @@ export const searchProjectBySession = async (
|
||||
const dir = await opendir(CLAUDE_PROJECTS_DIR);
|
||||
const folderNames: string[] = [];
|
||||
|
||||
// 收集所有文件夹名称
|
||||
// Collect all folder names
|
||||
for await (const dirent of dir) {
|
||||
if (dirent.isDirectory()) {
|
||||
folderNames.push(dirent.name);
|
||||
}
|
||||
}
|
||||
|
||||
// 并发检查每个项目文件夹中是否存在sessionId.jsonl文件
|
||||
// Concurrently check each project folder for sessionId.jsonl file
|
||||
const checkPromises = folderNames.map(async (folderName) => {
|
||||
const sessionFilePath = join(
|
||||
CLAUDE_PROJECTS_DIR,
|
||||
@@ -297,28 +297,28 @@ export const searchProjectBySession = async (
|
||||
const fileStat = await stat(sessionFilePath);
|
||||
return fileStat.isFile() ? folderName : null;
|
||||
} catch {
|
||||
// 文件不存在,继续检查下一个
|
||||
// File does not exist, continue checking next
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(checkPromises);
|
||||
|
||||
// 返回第一个存在的项目目录名称
|
||||
// Return the first existing project directory name
|
||||
for (const result of results) {
|
||||
if (result) {
|
||||
// 缓存找到的结果
|
||||
// Cache the found result
|
||||
sessionProjectCache.set(sessionId, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存未找到的结果(null值表示之前已查找过但未找到项目)
|
||||
// Cache not found result (null value means previously searched but not found)
|
||||
sessionProjectCache.set(sessionId, '');
|
||||
return null; // 没有找到匹配的项目
|
||||
return null; // No matching project found
|
||||
} catch (error) {
|
||||
console.error("Error searching for project by session:", error);
|
||||
// 出错时也缓存null结果,避免重复出错
|
||||
// Cache null result on error to avoid repeated errors
|
||||
sessionProjectCache.set(sessionId, '');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@ import JSON5 from "json5";
|
||||
import { jsonrepair } from "jsonrepair";
|
||||
|
||||
/**
|
||||
* 解析工具调用参数的函数
|
||||
* Parse tool call arguments function
|
||||
* 先尝试标准JSON解析,然后JSON5解析,最后使用jsonrepair进行安全修复
|
||||
* First try standard JSON parsing, then JSON5 parsing, finally use jsonrepair for safe repair
|
||||
*
|
||||
* @param argsString - 需要解析的参数字符串 / Parameter string to parse
|
||||
* @returns 解析后的参数对象或安全的空对象 / Parsed parameter object or safe empty object
|
||||
*
|
||||
* @param argsString - Parameter string to parse
|
||||
* @returns Parsed parameter object or safe empty object
|
||||
*/
|
||||
export function parseToolArguments(argsString: string, logger?: any): string {
|
||||
// Handle empty or null input
|
||||
@@ -25,25 +23,25 @@ export function parseToolArguments(argsString: string, logger?: any): string {
|
||||
try {
|
||||
// Second attempt: JSON5 parsing for relaxed syntax
|
||||
const args = JSON5.parse(argsString);
|
||||
logger?.debug(`工具调用参数JSON5解析成功 / Tool arguments JSON5 parsing successful`);
|
||||
logger?.debug(`Tool arguments JSON5 parsing successful`);
|
||||
return JSON.stringify(args);
|
||||
} catch (json5Error: any) {
|
||||
try {
|
||||
// Third attempt: Safe JSON repair without code execution
|
||||
const repairedJson = jsonrepair(argsString);
|
||||
logger?.debug(`工具调用参数安全修复成功 / Tool arguments safely repaired`);
|
||||
logger?.debug(`Tool arguments safely repaired`);
|
||||
return repairedJson;
|
||||
} catch (repairError: any) {
|
||||
// All parsing attempts failed - log errors and return safe fallback
|
||||
logger?.error(
|
||||
`JSON解析失败 / JSON parsing failed: ${jsonError.message}. ` +
|
||||
`JSON5解析失败 / JSON5 parsing failed: ${json5Error.message}. ` +
|
||||
`JSON修复失败 / JSON repair failed: ${repairError.message}. ` +
|
||||
`输入数据 / Input data: ${JSON.stringify(argsString)}`
|
||||
`JSON parsing failed: ${jsonError.message}. ` +
|
||||
`JSON5 parsing failed: ${json5Error.message}. ` +
|
||||
`JSON repair failed: ${repairError.message}. ` +
|
||||
`Input data: ${JSON.stringify(argsString)}`
|
||||
);
|
||||
|
||||
// Return safe empty object as fallback instead of potentially malformed input
|
||||
logger?.debug(`返回安全的空对象作为后备方案 / Returning safe empty object as fallback`);
|
||||
logger?.debug(`Returning safe empty object as fallback`);
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { UnifiedChatRequest, UnifiedMessage, UnifiedTool } from "../types/llm";
|
||||
|
||||
// Vertex Claude消息接口
|
||||
// Vertex Claude message interface
|
||||
interface ClaudeMessage {
|
||||
role: "user" | "assistant";
|
||||
content: Array<{
|
||||
@@ -14,7 +14,7 @@ interface ClaudeMessage {
|
||||
}>;
|
||||
}
|
||||
|
||||
// Vertex Claude工具接口
|
||||
// Vertex Claude tool interface
|
||||
interface ClaudeTool {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -27,7 +27,7 @@ interface ClaudeTool {
|
||||
};
|
||||
}
|
||||
|
||||
// Vertex Claude请求接口
|
||||
// Vertex Claude request interface
|
||||
interface VertexClaudeRequest {
|
||||
anthropic_version: "vertex-2023-10-16";
|
||||
messages: ClaudeMessage[];
|
||||
@@ -40,7 +40,7 @@ interface VertexClaudeRequest {
|
||||
tool_choice?: "auto" | "none" | { type: "tool"; name: string };
|
||||
}
|
||||
|
||||
// Vertex Claude响应接口
|
||||
// Vertex Claude response interface
|
||||
interface VertexClaudeResponse {
|
||||
content: Array<{
|
||||
type: "text";
|
||||
@@ -76,7 +76,7 @@ export function buildRequestBody(
|
||||
const content: ClaudeMessage["content"] = [];
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
// 保留所有字符串内容,即使是空字符串,因为可能包含重要信息
|
||||
// Keep all string content, even empty strings, as it may contain important information
|
||||
content.push({
|
||||
type: "text",
|
||||
text: message.content,
|
||||
@@ -84,13 +84,13 @@ export function buildRequestBody(
|
||||
} else if (Array.isArray(message.content)) {
|
||||
message.content.forEach((item) => {
|
||||
if (item.type === "text") {
|
||||
// 保留所有文本内容,即使是空字符串
|
||||
// Keep all text content, even empty strings
|
||||
content.push({
|
||||
type: "text",
|
||||
text: item.text || "",
|
||||
});
|
||||
} else if (item.type === "image_url") {
|
||||
// 处理图片内容
|
||||
// Handle image content
|
||||
content.push({
|
||||
type: "image",
|
||||
source: {
|
||||
@@ -103,7 +103,7 @@ export function buildRequestBody(
|
||||
});
|
||||
}
|
||||
|
||||
// 只跳过完全空的非最后一条消息(没有内容和工具调用)
|
||||
// Only skip completely empty non-last messages (no content and no tool calls)
|
||||
if (
|
||||
!isLastMessage &&
|
||||
content.length === 0 &&
|
||||
@@ -113,7 +113,7 @@ export function buildRequestBody(
|
||||
continue;
|
||||
}
|
||||
|
||||
// 对于最后一条 assistant 消息,如果没有内容但有工具调用,则添加空内容
|
||||
// For last assistant message, add empty content if no content but has tool calls
|
||||
if (
|
||||
isLastMessage &&
|
||||
isAssistantMessage &&
|
||||
@@ -140,7 +140,7 @@ export function buildRequestBody(
|
||||
...(request.temperature && { temperature: request.temperature }),
|
||||
};
|
||||
|
||||
// 处理工具定义
|
||||
// Handle tool definitions
|
||||
if (request.tools && request.tools.length > 0) {
|
||||
requestBody.tools = request.tools.map((tool: UnifiedTool) => ({
|
||||
name: tool.function.name,
|
||||
@@ -149,12 +149,12 @@ export function buildRequestBody(
|
||||
}));
|
||||
}
|
||||
|
||||
// 处理工具选择
|
||||
// Handle tool choice
|
||||
if (request.tool_choice) {
|
||||
if (request.tool_choice === "auto" || request.tool_choice === "none") {
|
||||
requestBody.tool_choice = request.tool_choice;
|
||||
} else if (typeof request.tool_choice === "string") {
|
||||
// 如果 tool_choice 是字符串,假设是工具名称
|
||||
// If tool_choice is a string, assume it's the tool name
|
||||
requestBody.tool_choice = {
|
||||
type: "tool",
|
||||
name: request.tool_choice,
|
||||
@@ -206,7 +206,7 @@ export function transformRequestOut(
|
||||
stream: vertexRequest.stream,
|
||||
};
|
||||
|
||||
// 处理工具定义
|
||||
// Handle tool definitions
|
||||
if (vertexRequest.tools && vertexRequest.tools.length > 0) {
|
||||
result.tools = vertexRequest.tools.map((tool) => ({
|
||||
type: "function" as const,
|
||||
@@ -224,7 +224,7 @@ export function transformRequestOut(
|
||||
}));
|
||||
}
|
||||
|
||||
// 处理工具选择
|
||||
// Handle tool choice
|
||||
if (vertexRequest.tool_choice) {
|
||||
if (typeof vertexRequest.tool_choice === "string") {
|
||||
result.tool_choice = vertexRequest.tool_choice;
|
||||
@@ -244,7 +244,7 @@ export async function transformResponseOut(
|
||||
if (response.headers.get("Content-Type")?.includes("application/json")) {
|
||||
const jsonResponse = (await response.json()) as VertexClaudeResponse;
|
||||
|
||||
// 处理工具调用
|
||||
// Handle tool calls
|
||||
let tool_calls = undefined;
|
||||
if (jsonResponse.tool_use && jsonResponse.tool_use.length > 0) {
|
||||
tool_calls = jsonResponse.tool_use.map((tool) => ({
|
||||
@@ -257,7 +257,7 @@ export async function transformResponseOut(
|
||||
}));
|
||||
}
|
||||
|
||||
// 转换为OpenAI格式的响应
|
||||
// Convert to OpenAI format response
|
||||
const res = {
|
||||
id: jsonResponse.id,
|
||||
choices: [
|
||||
@@ -288,7 +288,7 @@ export async function transformResponseOut(
|
||||
headers: response.headers,
|
||||
});
|
||||
} else if (response.headers.get("Content-Type")?.includes("stream")) {
|
||||
// 处理流式响应
|
||||
// Handle streaming response
|
||||
if (!response.body) {
|
||||
return response;
|
||||
}
|
||||
@@ -307,12 +307,12 @@ export async function transformResponseOut(
|
||||
try {
|
||||
const chunk = JSON.parse(chunkStr);
|
||||
|
||||
// 处理 Anthropic 原生格式的流式响应
|
||||
// Handle Anthropic native format streaming response
|
||||
if (
|
||||
chunk.type === "content_block_delta" &&
|
||||
chunk.delta?.type === "text_delta"
|
||||
) {
|
||||
// 这是 Anthropic 原生格式,需要转换为 OpenAI 格式
|
||||
// This is Anthropic native format, need to convert to OpenAI format
|
||||
const res = {
|
||||
choices: [
|
||||
{
|
||||
@@ -345,7 +345,7 @@ export async function transformResponseOut(
|
||||
chunk.type === "content_block_delta" &&
|
||||
chunk.delta?.type === "input_json_delta"
|
||||
) {
|
||||
// 处理工具调用的参数增量
|
||||
// Handle tool call argument delta
|
||||
const res = {
|
||||
choices: [
|
||||
{
|
||||
@@ -384,7 +384,7 @@ export async function transformResponseOut(
|
||||
chunk.type === "content_block_start" &&
|
||||
chunk.content_block?.type === "tool_use"
|
||||
) {
|
||||
// 处理工具调用开始
|
||||
// Handle tool call start
|
||||
const res = {
|
||||
choices: [
|
||||
{
|
||||
@@ -423,7 +423,7 @@ export async function transformResponseOut(
|
||||
encoder.encode(`data: ${JSON.stringify(res)}\n\n`)
|
||||
);
|
||||
} else if (chunk.type === "message_delta") {
|
||||
// 处理消息结束
|
||||
// Handle message end
|
||||
const res = {
|
||||
choices: [
|
||||
{
|
||||
@@ -457,10 +457,10 @@ export async function transformResponseOut(
|
||||
encoder.encode(`data: ${JSON.stringify(res)}\n\n`)
|
||||
);
|
||||
} else if (chunk.type === "message_stop") {
|
||||
// 发送结束标记
|
||||
// Send end marker
|
||||
controller.enqueue(encoder.encode(`data: [DONE]\n\n`));
|
||||
} else {
|
||||
// 处理其他格式的响应(保持原有逻辑作为后备)
|
||||
// Handle other format responses (keep original logic as fallback)
|
||||
const res = {
|
||||
choices: [
|
||||
{
|
||||
|
||||
@@ -5,25 +5,25 @@ export class AgentsManager {
|
||||
private agents: Map<string, IAgent> = new Map();
|
||||
|
||||
/**
|
||||
* 注册一个agent
|
||||
* @param agent 要注册的agent实例
|
||||
* @param isDefault 是否设为默认agent
|
||||
* Register an agent
|
||||
* @param agent The agent instance to register
|
||||
* @param isDefault Whether to set as default agent
|
||||
*/
|
||||
registerAgent(agent: IAgent): void {
|
||||
this.agents.set(agent.name, agent);
|
||||
}
|
||||
/**
|
||||
* 根据名称查找agent
|
||||
* @param name agent名称
|
||||
* @returns 找到的agent实例,未找到返回undefined
|
||||
* Find agent by name
|
||||
* @param name Agent name
|
||||
* @returns Found agent instance, undefined if not found
|
||||
*/
|
||||
getAgent(name: string): IAgent | undefined {
|
||||
return this.agents.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的agents
|
||||
* @returns 所有agent实例的数组
|
||||
* Get all registered agents
|
||||
* @returns Array of all agent instances
|
||||
*/
|
||||
getAllAgents(): IAgent[] {
|
||||
return Array.from(this.agents.values());
|
||||
@@ -31,8 +31,8 @@ export class AgentsManager {
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有agent的工具
|
||||
* @returns 工具数组
|
||||
* Get all agent tools
|
||||
* @returns Array of tools
|
||||
*/
|
||||
getAllTools(): any[] {
|
||||
const allTools: any[] = [];
|
||||
|
||||
@@ -94,13 +94,13 @@ async function getServer(options: RunOptions = {}) {
|
||||
|
||||
let loggerConfig: any;
|
||||
|
||||
// 如果外部传入了 logger 配置,使用外部的
|
||||
// Use external logger configuration if provided
|
||||
if (options.logger !== undefined) {
|
||||
loggerConfig = options.logger;
|
||||
} else {
|
||||
// 如果没有传入,并且 config.LOG !== false,则启用 logger
|
||||
// Enable logger if not provided and config.LOG !== false
|
||||
if (config.LOG !== false) {
|
||||
// 将 config.LOG 设为 true(如果它还未设置)
|
||||
// Set config.LOG to true (if not already set)
|
||||
if (config.LOG === undefined) {
|
||||
config.LOG = true;
|
||||
}
|
||||
@@ -166,7 +166,7 @@ async function getServer(options: RunOptions = {}) {
|
||||
|
||||
for (const agent of agentsManager.getAllAgents()) {
|
||||
if (agent.shouldHandle(req, config)) {
|
||||
// 设置agent标识
|
||||
// Set agent identifier
|
||||
useAgents.push(agent.name)
|
||||
|
||||
// change request body
|
||||
@@ -209,10 +209,10 @@ async function getServer(options: RunOptions = {}) {
|
||||
let currentToolId = ''
|
||||
const toolMessages: any[] = []
|
||||
const assistantMessages: any[] = []
|
||||
// 存储Anthropic格式的消息体,区分文本和工具类型
|
||||
// Store Anthropic format message body, distinguishing text and tool types
|
||||
return done(null, rewriteStream(eventStream, async (data, controller) => {
|
||||
try {
|
||||
// 检测工具调用开始
|
||||
// Detect tool call start
|
||||
if (data.event === 'content_block_start' && data?.data?.content_block?.name) {
|
||||
const agent = req.agents.find((name: string) => agentsManager.getAgent(name)?.tools.get(data.data.content_block.name))
|
||||
if (agent) {
|
||||
@@ -224,13 +224,13 @@ async function getServer(options: RunOptions = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// 收集工具参数
|
||||
// Collect tool arguments
|
||||
if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data?.delta?.type === 'input_json_delta') {
|
||||
currentToolArgs += data.data?.delta?.partial_json;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 工具调用完成,处理agent调用
|
||||
// Tool call completed, handle agent invocation
|
||||
if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data.type === 'content_block_stop') {
|
||||
try {
|
||||
const args = JSON5.parse(currentToolArgs);
|
||||
@@ -293,7 +293,7 @@ async function getServer(options: RunOptions = {}) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查流是否仍然可写
|
||||
// Check if stream is still writable
|
||||
if (!controller.desiredSize) {
|
||||
break;
|
||||
}
|
||||
@@ -301,7 +301,7 @@ async function getServer(options: RunOptions = {}) {
|
||||
controller.enqueue(eventData)
|
||||
}catch (readError: any) {
|
||||
if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||
abortController.abort(); // 中止所有相关操作
|
||||
abortController.abort(); // Abort all related operations
|
||||
break;
|
||||
}
|
||||
throw readError;
|
||||
@@ -314,13 +314,13 @@ async function getServer(options: RunOptions = {}) {
|
||||
}catch (error: any) {
|
||||
console.error('Unexpected error in stream processing:', error);
|
||||
|
||||
// 处理流提前关闭的错误
|
||||
// Handle premature stream closure error
|
||||
if (error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||
abortController.abort();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 其他错误仍然抛出
|
||||
// Re-throw other errors
|
||||
throw error;
|
||||
}
|
||||
}).pipeThrough(new SSESerializerTransform()))
|
||||
@@ -405,7 +405,7 @@ export type { RunOptions };
|
||||
export type { IAgent, ITool } from "./agents/type";
|
||||
export { initDir, initConfig, readConfigFile, writeConfigFile, backupConfigFile } from "./utils";
|
||||
|
||||
// 如果是直接运行此文件,则启动服务
|
||||
// Start service if this file is run directly
|
||||
if (require.main === module) {
|
||||
run().catch((error) => {
|
||||
console.error('Failed to start server:', error);
|
||||
|
||||
@@ -8,7 +8,7 @@ export class SSEParserTransform extends TransformStream<string, any> {
|
||||
this.buffer += chunk;
|
||||
const lines = this.buffer.split('\n');
|
||||
|
||||
// 保留最后一行(可能不完整)
|
||||
// Keep last line (may be incomplete)
|
||||
this.buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -19,14 +19,14 @@ export class SSEParserTransform extends TransformStream<string, any> {
|
||||
}
|
||||
},
|
||||
flush: (controller) => {
|
||||
// 处理缓冲区中剩余的内容
|
||||
// Process remaining content in buffer
|
||||
if (this.buffer.trim()) {
|
||||
const events: any[] = [];
|
||||
this.processLine(this.buffer.trim(), events);
|
||||
events.forEach(event => controller.enqueue(event));
|
||||
}
|
||||
|
||||
// 推送最后一个事件(如果有)
|
||||
// Push last event (if any)
|
||||
if (Object.keys(this.currentEvent).length > 0) {
|
||||
controller.enqueue(this.currentEvent);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**rewriteStream
|
||||
* 读取源readablestream,返回一个新的readablestream,由processor对源数据进行处理后将返回的新值推送到新的stream,如果没有返回值则不推送
|
||||
* Read source readablestream and return a new readablestream, processor processes source data and pushes returned new value to new stream, no push if no return value
|
||||
* @param stream
|
||||
* @param processor
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 预设导出核心功能
|
||||
* 注意:这个模块不包含 CLI 交互逻辑,交互逻辑由调用者提供
|
||||
* Preset export core functionality
|
||||
* Note: This module does not contain CLI interaction logic, interaction logic is provided by the caller
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
@@ -12,7 +12,7 @@ import { PresetFile, PresetMetadata, ManifestFile } from './types';
|
||||
import { HOME_DIR } from '../constants';
|
||||
|
||||
/**
|
||||
* 导出选项
|
||||
* Export options
|
||||
*/
|
||||
export interface ExportOptions {
|
||||
output?: string;
|
||||
@@ -23,7 +23,7 @@ export interface ExportOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出结果
|
||||
* Export result
|
||||
*/
|
||||
export interface ExportResult {
|
||||
outputPath: string;
|
||||
@@ -34,11 +34,11 @@ export interface ExportResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 manifest 对象
|
||||
* @param presetName 预设名称
|
||||
* @param config 配置对象
|
||||
* @param sanitizedConfig 脱敏后的配置
|
||||
* @param options 导出选项
|
||||
* Create manifest object
|
||||
* @param presetName Preset name
|
||||
* @param config Configuration object
|
||||
* @param sanitizedConfig Sanitized configuration
|
||||
* @param options Export options
|
||||
*/
|
||||
export function createManifest(
|
||||
presetName: string,
|
||||
@@ -63,18 +63,18 @@ export function createManifest(
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出预设配置
|
||||
* @param presetName 预设名称
|
||||
* @param config 当前配置
|
||||
* @param options 导出选项
|
||||
* @returns 导出结果
|
||||
* Export preset configuration
|
||||
* @param presetName Preset name
|
||||
* @param config Current configuration
|
||||
* @param options Export options
|
||||
* @returns Export result
|
||||
*/
|
||||
export async function exportPreset(
|
||||
presetName: string,
|
||||
config: any,
|
||||
options: ExportOptions = {}
|
||||
): Promise<ExportResult> {
|
||||
// 1. 收集元数据
|
||||
// 1. Collect metadata
|
||||
const metadata: PresetMetadata = {
|
||||
name: presetName,
|
||||
version: '1.0.0',
|
||||
@@ -83,28 +83,28 @@ export async function exportPreset(
|
||||
keywords: options.tags ? options.tags.split(',').map(t => t.trim()) : undefined,
|
||||
};
|
||||
|
||||
// 2. 脱敏配置
|
||||
// 2. Sanitize configuration
|
||||
const { sanitizedConfig, requiredInputs, sanitizedCount } = await sanitizeConfig(config);
|
||||
|
||||
// 3. 生成manifest.json(扁平化结构)
|
||||
// 3. Generate manifest.json (flattened structure)
|
||||
const manifest: ManifestFile = {
|
||||
...metadata,
|
||||
...sanitizedConfig,
|
||||
requiredInputs: options.includeSensitive ? undefined : requiredInputs,
|
||||
};
|
||||
|
||||
// 4. 确定输出路径
|
||||
// 4. Determine output path
|
||||
const presetsDir = path.join(HOME_DIR, 'presets');
|
||||
|
||||
// 确保预设目录存在
|
||||
// Ensure presets directory exists
|
||||
await fs.mkdir(presetsDir, { recursive: true });
|
||||
|
||||
const outputPath = options.output || path.join(presetsDir, `${presetName}.ccrsets`);
|
||||
|
||||
// 5. 创建压缩包
|
||||
// 5. Create archive
|
||||
const output = fsSync.createWriteStream(outputPath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 9 } // 最高压缩级别
|
||||
zlib: { level: 9 } // Highest compression level
|
||||
});
|
||||
|
||||
return new Promise<ExportResult>((resolve, reject) => {
|
||||
@@ -122,13 +122,13 @@ export async function exportPreset(
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// 连接输出流
|
||||
// Connect output stream
|
||||
archive.pipe(output);
|
||||
|
||||
// 添加manifest.json到压缩包
|
||||
// Add manifest.json to archive
|
||||
archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' });
|
||||
|
||||
// 完成压缩
|
||||
// Finalize archive
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* 配置合并策略
|
||||
* Configuration merge strategies
|
||||
*/
|
||||
|
||||
import { MergeStrategy, ProviderConfig, RouterConfig, TransformerConfig } from './types';
|
||||
|
||||
/**
|
||||
* 合并 Provider 配置
|
||||
* 如果 provider 已存在则直接覆盖,否则添加
|
||||
* Merge Provider configuration
|
||||
* Overwrite if provider exists, otherwise add
|
||||
*/
|
||||
function mergeProviders(
|
||||
existing: ProviderConfig[],
|
||||
@@ -18,10 +18,10 @@ function mergeProviders(
|
||||
for (const provider of incoming) {
|
||||
const existingIndex = existingNames.get(provider.name);
|
||||
if (existingIndex !== undefined) {
|
||||
// Provider 已存在,直接覆盖
|
||||
// Provider exists, overwrite directly
|
||||
result[existingIndex] = provider;
|
||||
} else {
|
||||
// 新 Provider,直接添加
|
||||
// New provider, add directly
|
||||
result.push(provider);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ function mergeProviders(
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 Router 配置
|
||||
* Merge Router configuration
|
||||
*/
|
||||
async function mergeRouter(
|
||||
existing: RouterConfig,
|
||||
@@ -48,10 +48,10 @@ async function mergeRouter(
|
||||
const existingValue = result[key];
|
||||
|
||||
if (existingValue === undefined || existingValue === null) {
|
||||
// 现有配置中没有这个路由规则,直接添加
|
||||
// No such routing rule in existing config, add directly
|
||||
result[key] = value;
|
||||
} else {
|
||||
// 存在冲突
|
||||
// Conflict exists
|
||||
if (strategy === MergeStrategy.ASK && onRouterConflict) {
|
||||
const shouldOverwrite = await onRouterConflict(key, existingValue, value);
|
||||
if (shouldOverwrite) {
|
||||
@@ -60,10 +60,10 @@ async function mergeRouter(
|
||||
} else if (strategy === MergeStrategy.OVERWRITE) {
|
||||
result[key] = value;
|
||||
} else if (strategy === MergeStrategy.MERGE) {
|
||||
// 对于 Router,merge 策略等同于 skip,保留现有值
|
||||
// 或者可以询问用户
|
||||
// For Router, merge strategy equals skip, keep existing value
|
||||
// Or can ask user
|
||||
}
|
||||
// skip 策略:保留现有值,不做任何操作
|
||||
// skip strategy: keep existing value, do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ async function mergeRouter(
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 Transformer 配置
|
||||
* Merge Transformer configuration
|
||||
*/
|
||||
async function mergeTransformers(
|
||||
existing: TransformerConfig[],
|
||||
@@ -87,33 +87,33 @@ async function mergeTransformers(
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Transformer 合并逻辑:按路径匹配
|
||||
// Transformer merge logic: match by path
|
||||
const result = [...existing];
|
||||
const existingPaths = new Set(existing.map(t => t.path));
|
||||
|
||||
for (const transformer of incoming) {
|
||||
if (!transformer.path) {
|
||||
// 没有 path 的 transformer,直接添加
|
||||
// Transformer without path, add directly
|
||||
result.push(transformer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingPaths.has(transformer.path)) {
|
||||
// 已存在相同 path 的 transformer
|
||||
// Transformer with same path already exists
|
||||
if (strategy === MergeStrategy.ASK && onTransformerConflict) {
|
||||
const action = await onTransformerConflict(transformer.path);
|
||||
if (action === 'overwrite') {
|
||||
const index = result.findIndex(t => t.path === transformer.path);
|
||||
result[index] = transformer;
|
||||
}
|
||||
// keep 和 skip 都不做操作
|
||||
// keep and skip do nothing
|
||||
} else if (strategy === MergeStrategy.OVERWRITE) {
|
||||
const index = result.findIndex(t => t.path === transformer.path);
|
||||
result[index] = transformer;
|
||||
}
|
||||
// merge 和 skip 策略:保留现有
|
||||
// merge and skip strategies: keep existing
|
||||
} else {
|
||||
// 新 transformer,直接添加
|
||||
// New transformer, add directly
|
||||
result.push(transformer);
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ async function mergeTransformers(
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并其他顶级配置
|
||||
* Merge other top-level configurations
|
||||
*/
|
||||
async function mergeOtherConfig(
|
||||
existing: any,
|
||||
@@ -145,10 +145,10 @@ async function mergeOtherConfig(
|
||||
const existingValue = result[key];
|
||||
|
||||
if (existingValue === undefined || existingValue === null) {
|
||||
// 现有配置中没有这个字段,直接添加
|
||||
// No such field in existing config, add directly
|
||||
result[key] = value;
|
||||
} else {
|
||||
// 存在冲突
|
||||
// Conflict exists
|
||||
if (strategy === MergeStrategy.ASK && onConfigConflict) {
|
||||
const shouldOverwrite = await onConfigConflict(key);
|
||||
if (shouldOverwrite) {
|
||||
@@ -157,7 +157,7 @@ async function mergeOtherConfig(
|
||||
} else if (strategy === MergeStrategy.OVERWRITE) {
|
||||
result[key] = value;
|
||||
}
|
||||
// merge 和 skip 策略:保留现有值
|
||||
// merge and skip strategies: keep existing值
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ async function mergeOtherConfig(
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并交互回调接口
|
||||
* Merge interaction callback interface
|
||||
*/
|
||||
export interface MergeCallbacks {
|
||||
onRouterConflict?: (key: string, existingValue: any, newValue: any) => Promise<boolean>;
|
||||
@@ -174,12 +174,12 @@ export interface MergeCallbacks {
|
||||
}
|
||||
|
||||
/**
|
||||
* 主配置合并函数
|
||||
* @param baseConfig 基础配置(现有配置)
|
||||
* @param presetConfig 预设配置
|
||||
* @param strategy 合并策略
|
||||
* @param callbacks 交互式回调函数
|
||||
* @returns 合并后的配置
|
||||
* Main configuration merge function
|
||||
* @param baseConfig Base configuration (existing configuration)
|
||||
* @param presetConfig Preset configuration
|
||||
* @param strategy Merge strategy
|
||||
* @param callbacks Interactive callback functions
|
||||
* @returns Merged configuration
|
||||
*/
|
||||
export async function mergeConfig(
|
||||
baseConfig: any,
|
||||
@@ -189,7 +189,7 @@ export async function mergeConfig(
|
||||
): Promise<any> {
|
||||
const result = { ...baseConfig };
|
||||
|
||||
// 合并 Providers
|
||||
// Merge Providers
|
||||
if (presetConfig.Providers) {
|
||||
result.Providers = mergeProviders(
|
||||
result.Providers || [],
|
||||
@@ -197,7 +197,7 @@ export async function mergeConfig(
|
||||
);
|
||||
}
|
||||
|
||||
// 合并 Router
|
||||
// Merge Router
|
||||
if (presetConfig.Router) {
|
||||
result.Router = await mergeRouter(
|
||||
result.Router || {},
|
||||
@@ -207,7 +207,7 @@ export async function mergeConfig(
|
||||
);
|
||||
}
|
||||
|
||||
// 合并 transformers
|
||||
// Merge transformers
|
||||
if (presetConfig.transformers) {
|
||||
result.transformers = await mergeTransformers(
|
||||
result.transformers || [],
|
||||
@@ -217,7 +217,7 @@ export async function mergeConfig(
|
||||
);
|
||||
}
|
||||
|
||||
// 合并其他配置
|
||||
// Merge other configurations
|
||||
const otherConfig = await mergeOtherConfig(
|
||||
result,
|
||||
presetConfig,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* 敏感字段识别和脱敏功能
|
||||
* Sensitive field identification and sanitization functionality
|
||||
*/
|
||||
|
||||
import { RequiredInput, SanitizeResult } from './types';
|
||||
|
||||
// 敏感字段模式列表
|
||||
// Sensitive field pattern list
|
||||
const SENSITIVE_PATTERNS = [
|
||||
'api_key', 'apikey', 'apiKey', 'APIKEY',
|
||||
'api_secret', 'apisecret', 'apiSecret',
|
||||
@@ -15,11 +15,11 @@ const SENSITIVE_PATTERNS = [
|
||||
'access_key', 'accessKey',
|
||||
];
|
||||
|
||||
// 环境变量占位符正则
|
||||
// Environment variable placeholder regex
|
||||
const ENV_VAR_REGEX = /^\$\{?[A-Z_][A-Z0-9_]*\}?$/;
|
||||
|
||||
/**
|
||||
* 检查字段名是否为敏感字段
|
||||
* Check if field name is sensitive
|
||||
*/
|
||||
function isSensitiveField(fieldName: string): boolean {
|
||||
const lowerFieldName = fieldName.toLowerCase();
|
||||
@@ -29,22 +29,22 @@ function isSensitiveField(fieldName: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成环境变量名称
|
||||
* @param fieldType 字段类型 (provider, transformer, global)
|
||||
* @param entityName 实体名称 (如 provider name)
|
||||
* @param fieldName 字段名称
|
||||
* Generate environment variable name
|
||||
* @param fieldType Field type (provider, transformer, global)
|
||||
* @param entityName Entity name (e.g., provider name)
|
||||
* @param fieldName Field name
|
||||
*/
|
||||
export function generateEnvVarName(
|
||||
fieldType: 'provider' | 'transformer' | 'global',
|
||||
entityName: string,
|
||||
fieldName: string
|
||||
): string {
|
||||
// 生成大写的环境变量名
|
||||
// 例如: DEEPSEEK_API_KEY, CUSTOM_TRANSFORMER_SECRET
|
||||
// Generate uppercase environment variable name
|
||||
// e.g., DEEPSEEK_API_KEY, CUSTOM_TRANSFORMER_SECRET
|
||||
const prefix = entityName.toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
||||
const field = fieldName.toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
||||
|
||||
// 如果前缀和字段名相同(如 API_KEY),避免重复
|
||||
// If prefix and field name are the same (e.g., API_KEY), avoid duplication
|
||||
if (prefix === field) {
|
||||
return prefix;
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export function generateEnvVarName(
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否已经是环境变量占位符
|
||||
* Check if value is already an environment variable placeholder
|
||||
*/
|
||||
function isEnvPlaceholder(value: any): boolean {
|
||||
if (typeof value !== 'string') {
|
||||
@@ -63,19 +63,19 @@ function isEnvPlaceholder(value: any): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从环境变量占位符中提取变量名
|
||||
* @param value 环境变量值(如 $VAR 或 ${VAR})
|
||||
* Extract variable name from environment variable placeholder
|
||||
* @param value Environment variable value (e.g., $VAR or ${VAR})
|
||||
*/
|
||||
function extractEnvVarName(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
|
||||
// 匹配 ${VAR_NAME} 格式
|
||||
// Match ${VAR_NAME} format
|
||||
const bracedMatch = trimmed.match(/^\$\{([A-Z_][A-Z0-9_]*)\}$/);
|
||||
if (bracedMatch) {
|
||||
return bracedMatch[1];
|
||||
}
|
||||
|
||||
// 匹配 $VAR_NAME 格式
|
||||
// Match $VAR_NAME format
|
||||
const unbracedMatch = trimmed.match(/^\$([A-Z_][A-Z0-9_]*)$/);
|
||||
if (unbracedMatch) {
|
||||
return unbracedMatch[1];
|
||||
@@ -85,11 +85,11 @@ function extractEnvVarName(value: string): string | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历对象,识别和脱敏敏感字段
|
||||
* @param config 配置对象
|
||||
* @param path 当前字段路径
|
||||
* @param requiredInputs 必需输入数组(累积)
|
||||
* @param sanitizedCount 脱敏字段计数
|
||||
* Recursively traverse object to identify and sanitize sensitive fields
|
||||
* @param config Configuration object
|
||||
* @param path Current field path
|
||||
* @param requiredInputs Required inputs array (cumulative)
|
||||
* @param sanitizedCount Sanitized field count
|
||||
*/
|
||||
function sanitizeObject(
|
||||
config: any,
|
||||
@@ -121,12 +121,12 @@ function sanitizeObject(
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
const currentPath = path ? `${path}.${key}` : key;
|
||||
|
||||
// 检查是否是敏感字段
|
||||
// Check if this is a sensitive field
|
||||
if (isSensitiveField(key) && typeof value === 'string') {
|
||||
// 如果值已经是环境变量,保持不变
|
||||
// If value is already an environment variable, keep unchanged
|
||||
if (isEnvPlaceholder(value)) {
|
||||
sanitizedObj[key] = value;
|
||||
// 仍然需要记录为必需输入,但使用已有环境变量
|
||||
// Still need to record as required input, but use existing environment variable
|
||||
const envVarName = extractEnvVarName(value);
|
||||
if (envVarName && !requiredInputs.some(input => input.id === currentPath)) {
|
||||
requiredInputs.push({
|
||||
@@ -136,19 +136,19 @@ function sanitizeObject(
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 脱敏:替换为环境变量占位符
|
||||
// 尝试从路径中推断实体名称
|
||||
// Sanitize: replace with environment variable placeholder
|
||||
// Try to infer entity name from path
|
||||
let entityName = 'CONFIG';
|
||||
const pathParts = currentPath.split('.');
|
||||
|
||||
// 如果路径包含 Providers 或 transformers,尝试提取实体名称
|
||||
// If path contains Providers or transformers, try to extract entity name
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
if (pathParts[i] === 'Providers' || pathParts[i] === 'transformers') {
|
||||
// 查找 name 字段
|
||||
// Find name field
|
||||
if (i + 1 < pathParts.length && pathParts[i + 1].match(/^\d+$/)) {
|
||||
// 这是数组索引,查找同级的 name 字段
|
||||
// This is array index, find name field at same level
|
||||
const parentPath = pathParts.slice(0, i + 2).join('.');
|
||||
// 在当前上下文中查找 name
|
||||
// Find name in current context
|
||||
const context = config;
|
||||
if (context.name) {
|
||||
entityName = context.name;
|
||||
@@ -161,7 +161,7 @@ function sanitizeObject(
|
||||
const envVarName = generateEnvVarName('global', entityName, key);
|
||||
sanitizedObj[key] = `\${${envVarName}}`;
|
||||
|
||||
// 记录为必需输入
|
||||
// Record as required input
|
||||
requiredInputs.push({
|
||||
id: currentPath,
|
||||
prompt: `Enter ${key}`,
|
||||
@@ -171,13 +171,13 @@ function sanitizeObject(
|
||||
sanitizedCount++;
|
||||
}
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// 递归处理嵌套对象
|
||||
// Recursively process nested objects
|
||||
const result = sanitizeObject(value, currentPath, requiredInputs, sanitizedCount);
|
||||
sanitizedObj[key] = result.sanitized;
|
||||
requiredInputs = result.requiredInputs;
|
||||
sanitizedCount = result.count;
|
||||
} else {
|
||||
// 保留原始值
|
||||
// Keep original value
|
||||
sanitizedObj[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -186,12 +186,12 @@ function sanitizeObject(
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏配置对象
|
||||
* @param config 原始配置
|
||||
* @returns 脱敏结果
|
||||
* Sanitize configuration object
|
||||
* @param config Original configuration
|
||||
* @returns Sanitization result
|
||||
*/
|
||||
export async function sanitizeConfig(config: any): Promise<SanitizeResult> {
|
||||
// 深拷贝配置,避免修改原始对象
|
||||
// Deep copy configuration to avoid modifying original object
|
||||
const configCopy = JSON.parse(JSON.stringify(config));
|
||||
|
||||
const result = sanitizeObject(configCopy);
|
||||
@@ -204,10 +204,10 @@ export async function sanitizeConfig(config: any): Promise<SanitizeResult> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充敏感信息到配置中
|
||||
* @param config 预设配置(包含环境变量占位符)
|
||||
* @param inputs 用户输入的敏感信息
|
||||
* @returns 填充后的配置
|
||||
* Fill sensitive information into configuration
|
||||
* @param config Preset configuration (containing environment variable placeholders)
|
||||
* @param inputs User input sensitive information
|
||||
* @returns Filled configuration
|
||||
*/
|
||||
export function fillSensitiveInputs(config: any, inputs: Record<string, string>): any {
|
||||
const configCopy = JSON.parse(JSON.stringify(config));
|
||||
@@ -228,7 +228,7 @@ export function fillSensitiveInputs(config: any, inputs: Record<string, string>)
|
||||
const currentPath = path ? `${path}.${key}` : key;
|
||||
|
||||
if (typeof value === 'string' && isEnvPlaceholder(value)) {
|
||||
// 查找是否有用户输入
|
||||
// Check if there is user input
|
||||
const input = inputs[currentPath];
|
||||
if (input) {
|
||||
result[key] = input;
|
||||
|
||||
Reference in New Issue
Block a user