mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-02-03 07:10:51 +08:00
move llms to core package
This commit is contained in:
@@ -12,10 +12,12 @@ 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";
|
||||
import {PID_FILE, readPresetFile, REFERENCE_COUNT_FILE} from "@CCR/shared";
|
||||
import fs, { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { parseStatusLineData, StatusLineInput } from "./utils/statusline";
|
||||
import {handlePresetCommand} from "./utils/preset";
|
||||
|
||||
|
||||
const command = process.argv[2];
|
||||
|
||||
@@ -95,7 +97,6 @@ async function main() {
|
||||
|
||||
// 如果命令不是已知命令,检查是否是 preset
|
||||
if (command && !KNOWN_COMMANDS.includes(command)) {
|
||||
const { readPresetFile } = await import("./utils");
|
||||
const presetData: any = await readPresetFile(command);
|
||||
|
||||
if (presetData) {
|
||||
@@ -248,7 +249,6 @@ async function main() {
|
||||
await runModelSelector();
|
||||
break;
|
||||
case "preset":
|
||||
const { handlePresetCommand } = await import("./utils/preset");
|
||||
await handlePresetCommand(process.argv.slice(3));
|
||||
break;
|
||||
case "activate":
|
||||
|
||||
@@ -105,20 +105,11 @@ export async function applyPresetCli(
|
||||
|
||||
console.log(`${BOLDCYAN}Validating preset...${RESET} ${GREEN}✓${RESET}`);
|
||||
|
||||
// 检查是否已经配置过(通过检查manifest中是否已有敏感信息)
|
||||
const presetDir = getPresetDir(presetName);
|
||||
|
||||
try {
|
||||
const existingManifest = await readManifestFromDir(presetDir);
|
||||
// 检查是否已经配置了敏感信息(例如api_key)
|
||||
const hasSecrets = existingManifest.Providers?.some((p: any) => p.api_key && p.api_key !== '');
|
||||
if (hasSecrets) {
|
||||
console.log(`\n${GREEN}✓${RESET} Preset already configured`);
|
||||
console.log(`${DIM}You can use this preset with: ccr ${presetName}${RESET}\n`);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// manifest不存在,继续配置流程
|
||||
// 检查是否需要配置
|
||||
if (preset.schema && preset.schema.length > 0) {
|
||||
console.log(`\n${BOLDCYAN}Configuration required:${RESET} ${preset.schema.length} field(s)\n`);
|
||||
} else {
|
||||
console.log(`\n${DIM}No configuration required for this preset${RESET}\n`);
|
||||
}
|
||||
|
||||
// 收集用户输入
|
||||
|
||||
@@ -14,8 +14,13 @@ import {
|
||||
getDefaultValue,
|
||||
sortFieldsByDependencies,
|
||||
getAffectedFields,
|
||||
} from '@musistudio/claude-code-router-shared';
|
||||
import { input, confirm, select, password } from '@inquirer/prompts';
|
||||
} from '@CCR/shared';
|
||||
import input from '@inquirer/input';
|
||||
import confirm from '@inquirer/confirm';
|
||||
import select from '@inquirer/select';
|
||||
import password from '@inquirer/password';
|
||||
import checkbox from '@inquirer/checkbox';
|
||||
import editor from '@inquirer/editor';
|
||||
|
||||
// ANSI 颜色代码
|
||||
export const COLORS = {
|
||||
@@ -183,7 +188,6 @@ async function promptField(
|
||||
}
|
||||
|
||||
// @inquirer/prompts 没有多选,使用 checkbox
|
||||
const { checkbox } = await import('@inquirer/prompts');
|
||||
return await checkbox({
|
||||
message,
|
||||
choices: options.map(opt => ({
|
||||
@@ -195,7 +199,6 @@ async function promptField(
|
||||
}
|
||||
|
||||
case InputType.EDITOR: {
|
||||
const { editor } = await import('@inquirer/prompts');
|
||||
return await editor({
|
||||
message,
|
||||
default: field.defaultValue,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"dependencies": {
|
||||
"@fastify/multipart": "^9.0.0",
|
||||
"@fastify/static": "^8.2.0",
|
||||
"@musistudio/llms": "^1.0.51",
|
||||
"@musistudio/llms": "workspace:*",
|
||||
"adm-zip": "^0.5.16",
|
||||
"dotenv": "^16.4.7",
|
||||
"json5": "^2.2.3",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, writeFileSync, unlinkSync } from "fs";
|
||||
import { existsSync } from "fs";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
@@ -6,7 +6,7 @@ import { initConfig, initDir } from "./utils";
|
||||
import { createServer } from "./server";
|
||||
import { router } from "./utils/router";
|
||||
import { apiKeyAuth } from "./middleware/auth";
|
||||
import { CONFIG_FILE, HOME_DIR } from "@CCR/shared";
|
||||
import {CONFIG_FILE, HOME_DIR, listPresets} from "@CCR/shared";
|
||||
import { createStream } from 'rotating-file-stream';
|
||||
import { sessionUsageCache } from "./utils/cache";
|
||||
import {SSEParserTransform} from "./utils/SSEParser.transform";
|
||||
@@ -16,7 +16,6 @@ import JSON5 from "json5";
|
||||
import { IAgent, ITool } from "./agents/type";
|
||||
import agentsManager from "./agents";
|
||||
import { EventEmitter } from "node:events";
|
||||
import {spawn} from "child_process";
|
||||
|
||||
const event = new EventEmitter()
|
||||
|
||||
@@ -121,6 +120,8 @@ async function getServer(options: RunOptions = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const presets = await listPresets();
|
||||
|
||||
const serverInstance = await createServer({
|
||||
jsonPath: CONFIG_FILE,
|
||||
initialConfig: {
|
||||
@@ -137,6 +138,11 @@ async function getServer(options: RunOptions = {}) {
|
||||
logger: loggerConfig,
|
||||
});
|
||||
|
||||
presets.forEach(preset => {
|
||||
console.log(preset.name, preset.config);
|
||||
serverInstance.registerNamespace(preset.name, preset.config);
|
||||
})
|
||||
|
||||
// Add async preHandler hook for authentication
|
||||
serverInstance.addHook("preHandler", async (req: any, reply: any) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
readManifestFromDir,
|
||||
manifestToPresetFile,
|
||||
extractPreset,
|
||||
validatePreset,
|
||||
loadPreset,
|
||||
saveManifest,
|
||||
isPresetInstalled,
|
||||
downloadPresetToTemp,
|
||||
@@ -20,16 +18,15 @@ import {
|
||||
type PresetFile,
|
||||
type ManifestFile,
|
||||
type PresetMetadata,
|
||||
MergeStrategy
|
||||
} from "@CCR/shared";
|
||||
import fastifyMultipart from "@fastify/multipart";
|
||||
import AdmZip from "adm-zip";
|
||||
|
||||
export const createServer = async (config: any): Promise<any> => {
|
||||
const server = new Server(config);
|
||||
const app = server.app;
|
||||
|
||||
// Register multipart plugin for file uploads (dynamic import)
|
||||
const fastifyMultipart = await import('@fastify/multipart');
|
||||
app.register(fastifyMultipart.default, {
|
||||
app.register(fastifyMultipart, {
|
||||
limits: {
|
||||
fileSize: 50 * 1024 * 1024, // 50MB
|
||||
},
|
||||
@@ -171,8 +168,6 @@ export const createServer = async (config: any): Promise<any> => {
|
||||
}
|
||||
});
|
||||
|
||||
// ========== Preset 相关 API ==========
|
||||
|
||||
// 获取预设列表
|
||||
app.get("/api/presets", async (req: any, reply: any) => {
|
||||
try {
|
||||
@@ -435,8 +430,13 @@ export const createServer = async (config: any): Promise<any> => {
|
||||
}
|
||||
|
||||
// 解析 GitHub 仓库 URL
|
||||
// 支持格式: https://github.com/owner/repo 或 https://github.com/owner/repo.git
|
||||
const githubRepoMatch = repo.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
||||
// 支持格式:
|
||||
// - owner/repo (简短格式,来自市场)
|
||||
// - github.com/owner/repo
|
||||
// - https://github.com/owner/repo
|
||||
// - https://github.com/owner/repo.git
|
||||
// - git@github.com:owner/repo.git
|
||||
const githubRepoMatch = repo.match(/(?:github\.com[:/]|^)([^/]+)\/([^/\s#]+?)(?:\.git)?$/);
|
||||
if (!githubRepoMatch) {
|
||||
reply.status(400).send({ error: "Invalid GitHub repository URL" });
|
||||
return;
|
||||
@@ -484,7 +484,6 @@ export const createServer = async (config: any): Promise<any> => {
|
||||
|
||||
// 辅助函数:从 ZIP 加载预设
|
||||
async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {
|
||||
const AdmZip = (await import('adm-zip')).default;
|
||||
const zip = new AdmZip(zipFile);
|
||||
|
||||
// 首先尝试在根目录查找 manifest.json
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './preset/install';
|
||||
export * from './preset/export';
|
||||
export * from './preset/readPreset';
|
||||
export * from './preset/schema';
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as fsSync from 'fs';
|
||||
import * as path from 'path';
|
||||
import JSON5 from 'json5';
|
||||
import AdmZip from 'adm-zip';
|
||||
import { PresetFile, MergeStrategy, RequiredInput, ManifestFile } from './types';
|
||||
import { HOME_DIR } from '../constants';
|
||||
import { PresetFile, MergeStrategy, RequiredInput, ManifestFile, PresetInfo } from './types';
|
||||
import { HOME_DIR, PRESETS_DIR } from '../constants';
|
||||
|
||||
/**
|
||||
* 获取预设目录的完整路径
|
||||
@@ -48,6 +47,58 @@ export async function extractPreset(sourceZip: string, targetDir: string): Promi
|
||||
|
||||
// 解压文件
|
||||
const zip = new AdmZip(sourceZip);
|
||||
const entries = zip.getEntries();
|
||||
|
||||
// 检测是否有单一的根目录(GitHub ZIP 文件通常有这个特征)
|
||||
if (entries.length > 0) {
|
||||
// 获取所有顶层目录
|
||||
const rootDirs = new Set<string>();
|
||||
for (const entry of entries) {
|
||||
const parts = entry.entryName.split('/');
|
||||
if (parts.length > 1) {
|
||||
rootDirs.add(parts[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果只有一个根目录,则去除它
|
||||
if (rootDirs.size === 1) {
|
||||
const singleRoot = Array.from(rootDirs)[0];
|
||||
|
||||
// 检查 manifest.json 是否在根目录下
|
||||
const hasManifestInRoot = entries.some(e =>
|
||||
e.entryName === 'manifest.json' || e.entryName.startsWith(`${singleRoot}/manifest.json`)
|
||||
);
|
||||
|
||||
if (hasManifestInRoot) {
|
||||
// 将所有文件从根目录下提取出来
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 去除根目录前缀
|
||||
let newPath = entry.entryName;
|
||||
if (newPath.startsWith(`${singleRoot}/`)) {
|
||||
newPath = newPath.substring(singleRoot.length + 1);
|
||||
}
|
||||
|
||||
// 跳过根目录本身
|
||||
if (newPath === '' || newPath === singleRoot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 提取文件
|
||||
const targetPath = path.join(targetDir, newPath);
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
await fs.writeFile(targetPath, entry.getData());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有单一的根目录,直接解压
|
||||
zip.extractAllTo(targetDir, true);
|
||||
}
|
||||
|
||||
@@ -65,11 +116,11 @@ export async function readManifestFromDir(presetDir: string): Promise<ManifestFi
|
||||
* 将manifest转换为PresetFile格式
|
||||
*/
|
||||
export function manifestToPresetFile(manifest: ManifestFile): PresetFile {
|
||||
const { Providers, Router, PORT, HOST, API_TIMEOUT_MS, PROXY_URL, LOG, LOG_LEVEL, StatusLine, NON_INTERACTIVE_MODE, requiredInputs, ...metadata } = manifest;
|
||||
const { Providers, Router, StatusLine, NON_INTERACTIVE_MODE, schema, ...metadata } = manifest;
|
||||
return {
|
||||
metadata,
|
||||
config: { Providers, Router, PORT, HOST, API_TIMEOUT_MS, PROXY_URL, LOG, LOG_LEVEL, StatusLine, NON_INTERACTIVE_MODE },
|
||||
requiredInputs,
|
||||
config: { Providers, Router, StatusLine, NON_INTERACTIVE_MODE },
|
||||
schema,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -237,3 +288,55 @@ export async function isPresetInstalled(presetName: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有已安装的预设
|
||||
* @returns PresetInfo 数组
|
||||
*/
|
||||
export async function listPresets(): Promise<PresetInfo[]> {
|
||||
const presetsDir = PRESETS_DIR;
|
||||
const presets: PresetInfo[] = [];
|
||||
|
||||
try {
|
||||
await fs.access(presetsDir);
|
||||
} catch {
|
||||
return presets;
|
||||
}
|
||||
|
||||
// 读取目录下的所有子目录
|
||||
const entries = await fs.readdir(presetsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const presetName = entry.name;
|
||||
const presetDir = path.join(presetsDir, presetName);
|
||||
const manifestPath = path.join(presetDir, 'manifest.json');
|
||||
|
||||
try {
|
||||
// 检查 manifest.json 是否存在
|
||||
await fs.access(manifestPath);
|
||||
|
||||
// 读取 manifest.json
|
||||
const content = await fs.readFile(manifestPath, 'utf-8');
|
||||
const manifest = JSON5.parse(content) as ManifestFile;
|
||||
|
||||
// 获取目录创建时间
|
||||
const stats = await fs.stat(presetDir);
|
||||
|
||||
presets.push({
|
||||
name: manifest.name || presetName,
|
||||
version: manifest.version,
|
||||
description: manifest.description,
|
||||
author: manifest.author,
|
||||
config: manifestToPresetFile(manifest).config,
|
||||
});
|
||||
} catch {
|
||||
// 忽略无效的预设目录(没有 manifest.json 或读取失败)
|
||||
// 可以选择跳过或者添加到列表中标记为错误
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
@@ -2,72 +2,24 @@
|
||||
* 配置合并策略
|
||||
*/
|
||||
|
||||
import { MergeStrategy, ProviderConfig, RouterConfig, TransformerConfig, ProviderConflictAction } from './types';
|
||||
import { MergeStrategy, ProviderConfig, RouterConfig, TransformerConfig } from './types';
|
||||
|
||||
/**
|
||||
* 合并 Provider 配置
|
||||
* 如果 provider 已存在则直接覆盖,否则添加
|
||||
*/
|
||||
async function mergeProviders(
|
||||
function mergeProviders(
|
||||
existing: ProviderConfig[],
|
||||
incoming: ProviderConfig[],
|
||||
strategy: MergeStrategy,
|
||||
onProviderConflict?: (providerName: string) => Promise<ProviderConflictAction>
|
||||
): Promise<ProviderConfig[]> {
|
||||
incoming: ProviderConfig[]
|
||||
): ProviderConfig[] {
|
||||
const result = [...existing];
|
||||
const existingNames = new Set(existing.map(p => p.name));
|
||||
const existingNames = new Map(existing.map(p => [p.name, result.findIndex(x => x.name === p.name)]));
|
||||
|
||||
for (const provider of incoming) {
|
||||
if (existingNames.has(provider.name)) {
|
||||
// Provider 已存在,需要处理冲突
|
||||
let action: ProviderConflictAction;
|
||||
|
||||
if (strategy === MergeStrategy.ASK && onProviderConflict) {
|
||||
action = await onProviderConflict(provider.name);
|
||||
} else if (strategy === MergeStrategy.OVERWRITE) {
|
||||
action = 'overwrite';
|
||||
} else if (strategy === MergeStrategy.MERGE) {
|
||||
action = 'merge';
|
||||
} else {
|
||||
action = 'skip';
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'keep':
|
||||
// 保留现有,不做任何操作
|
||||
break;
|
||||
case 'overwrite':
|
||||
const index = result.findIndex(p => p.name === provider.name);
|
||||
result[index] = provider;
|
||||
break;
|
||||
case 'merge':
|
||||
const existingProvider = result.find(p => p.name === provider.name)!;
|
||||
// 合并模型列表,去重
|
||||
const mergedModels = [...new Set([
|
||||
...existingProvider.models,
|
||||
...provider.models,
|
||||
])];
|
||||
existingProvider.models = mergedModels;
|
||||
|
||||
// 合并 transformer 配置
|
||||
if (provider.transformer) {
|
||||
if (!existingProvider.transformer) {
|
||||
existingProvider.transformer = provider.transformer;
|
||||
} else {
|
||||
// 合并 transformer.use
|
||||
if (provider.transformer.use && existingProvider.transformer.use) {
|
||||
const mergedTransformers = [...new Set([
|
||||
...existingProvider.transformer.use,
|
||||
...provider.transformer.use,
|
||||
])];
|
||||
existingProvider.transformer.use = mergedTransformers as any;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'skip':
|
||||
// 跳过,不做任何操作
|
||||
break;
|
||||
}
|
||||
const existingIndex = existingNames.get(provider.name);
|
||||
if (existingIndex !== undefined) {
|
||||
// Provider 已存在,直接覆盖
|
||||
result[existingIndex] = provider;
|
||||
} else {
|
||||
// 新 Provider,直接添加
|
||||
result.push(provider);
|
||||
@@ -216,7 +168,6 @@ async function mergeOtherConfig(
|
||||
* 合并交互回调接口
|
||||
*/
|
||||
export interface MergeCallbacks {
|
||||
onProviderConflict?: (providerName: string) => Promise<ProviderConflictAction>;
|
||||
onRouterConflict?: (key: string, existingValue: any, newValue: any) => Promise<boolean>;
|
||||
onTransformerConflict?: (transformerPath: string) => Promise<'keep' | 'overwrite' | 'skip'>;
|
||||
onConfigConflict?: (key: string) => Promise<boolean>;
|
||||
@@ -240,11 +191,9 @@ export async function mergeConfig(
|
||||
|
||||
// 合并 Providers
|
||||
if (presetConfig.Providers) {
|
||||
result.Providers = await mergeProviders(
|
||||
result.Providers = mergeProviders(
|
||||
result.Providers || [],
|
||||
presetConfig.Providers,
|
||||
strategy,
|
||||
callbacks?.onProviderConflict
|
||||
presetConfig.Providers
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -128,9 +128,9 @@ function sanitizeObject(
|
||||
sanitizedObj[key] = value;
|
||||
// 仍然需要记录为必需输入,但使用已有环境变量
|
||||
const envVarName = extractEnvVarName(value);
|
||||
if (envVarName && !requiredInputs.some(input => input.field === currentPath)) {
|
||||
if (envVarName && !requiredInputs.some(input => input.id === currentPath)) {
|
||||
requiredInputs.push({
|
||||
field: currentPath,
|
||||
id: currentPath,
|
||||
prompt: `Enter ${key}`,
|
||||
placeholder: envVarName,
|
||||
});
|
||||
@@ -163,7 +163,7 @@ function sanitizeObject(
|
||||
|
||||
// 记录为必需输入
|
||||
requiredInputs.push({
|
||||
field: currentPath,
|
||||
id: currentPath,
|
||||
prompt: `Enter ${key}`,
|
||||
placeholder: envVarName,
|
||||
});
|
||||
|
||||
@@ -140,12 +140,6 @@ export interface PresetConfigSection {
|
||||
Providers?: ProviderConfig[];
|
||||
Router?: RouterConfig;
|
||||
transformers?: TransformerConfig[];
|
||||
PORT?: number;
|
||||
HOST?: string;
|
||||
API_TIMEOUT_MS?: number;
|
||||
PROXY_URL?: string;
|
||||
LOG?: boolean;
|
||||
LOG_LEVEL?: string;
|
||||
StatusLine?: any;
|
||||
NON_INTERACTIVE_MODE?: boolean;
|
||||
[key: string]: any;
|
||||
@@ -243,5 +237,11 @@ export interface SanitizeResult {
|
||||
sanitizedCount: number;
|
||||
}
|
||||
|
||||
// Provider 冲突处理动作
|
||||
export type ProviderConflictAction = 'keep' | 'overwrite' | 'merge' | 'skip';
|
||||
// Preset 信息(用于列表展示)
|
||||
export interface PresetInfo {
|
||||
name: string; // 预设名称
|
||||
version?: string; // 版本号
|
||||
description?: string; // 描述
|
||||
author?: string; // 作者
|
||||
config: PresetConfigSection;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
|
||||
@@ -144,10 +144,45 @@ export function Presets() {
|
||||
const handleInstallFromMarket = async (preset: MarketPreset) => {
|
||||
try {
|
||||
setInstallingFromMarket(preset.id);
|
||||
|
||||
// 第一步:安装预设(解压到目录)
|
||||
await api.installPresetFromGitHub(preset.repo, preset.name);
|
||||
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
||||
setMarketDialogOpen(false);
|
||||
await loadPresets();
|
||||
|
||||
// 第二步:获取预设详情(检查是否需要配置)
|
||||
try {
|
||||
const detail = await api.getPreset(preset.name);
|
||||
const presetDetail: PresetDetail = { ...preset, ...detail };
|
||||
|
||||
// 检查是否需要配置
|
||||
if (detail.schema && detail.schema.length > 0) {
|
||||
// 需要配置,打开配置对话框
|
||||
setSelectedPreset(presetDetail);
|
||||
|
||||
// 初始化默认值
|
||||
const initialValues: Record<string, any> = {};
|
||||
for (const input of detail.schema) {
|
||||
initialValues[input.id] = input.defaultValue ?? '';
|
||||
}
|
||||
setSecrets(initialValues);
|
||||
|
||||
// 关闭市场对话框,打开详情对话框
|
||||
setMarketDialogOpen(false);
|
||||
setDetailDialogOpen(true);
|
||||
|
||||
setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' });
|
||||
} else {
|
||||
// 不需要配置,直接完成
|
||||
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
||||
setMarketDialogOpen(false);
|
||||
await loadPresets();
|
||||
}
|
||||
} catch (error) {
|
||||
// 获取详情失败,但安装成功了,刷新列表
|
||||
console.error('Failed to get preset details after installation:', error);
|
||||
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
||||
setMarketDialogOpen(false);
|
||||
await loadPresets();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to install preset:', error);
|
||||
setToast({ message: t('presets.preset_install_failed', { error: error.message }), type: 'error' });
|
||||
@@ -214,21 +249,81 @@ export function Presets() {
|
||||
try {
|
||||
setIsInstalling(true);
|
||||
|
||||
if (installMethod === 'url' && installUrl) {
|
||||
await api.installPresetFromUrl(installUrl, installName || undefined);
|
||||
} else if (installMethod === 'file' && installFile) {
|
||||
await api.uploadPresetFile(installFile, installName || undefined);
|
||||
} else {
|
||||
setToast({ message: t('presets.please_provide_file_or_url'), type: 'warning' });
|
||||
// 验证输入
|
||||
if (installMethod === 'url' && !installUrl) {
|
||||
setToast({ message: t('presets.please_provide_url'), type: 'warning' });
|
||||
return;
|
||||
}
|
||||
if (installMethod === 'file' && !installFile) {
|
||||
setToast({ message: t('presets.please_provide_file'), type: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
||||
setInstallDialogOpen(false);
|
||||
setInstallUrl('');
|
||||
setInstallFile(null);
|
||||
setInstallName('');
|
||||
await loadPresets();
|
||||
// 确定预设名称
|
||||
const presetName = installName || (
|
||||
installMethod === 'file'
|
||||
? installFile!.name.replace('.ccrsets', '')
|
||||
: installUrl!.split('/').pop()!.replace('.ccrsets', '')
|
||||
);
|
||||
|
||||
// 第一步:安装预设(解压到目录)
|
||||
if (installMethod === 'url' && installUrl) {
|
||||
await api.installPresetFromUrl(installUrl, presetName);
|
||||
} else if (installMethod === 'file' && installFile) {
|
||||
await api.uploadPresetFile(installFile, presetName);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// 第二步:获取预设详情(检查是否需要配置)
|
||||
try {
|
||||
const detail = await api.getPreset(presetName);
|
||||
|
||||
// 检查是否需要配置
|
||||
if (detail.schema && detail.schema.length > 0) {
|
||||
// 需要配置,打开配置对话框
|
||||
setSelectedPreset({
|
||||
id: presetName,
|
||||
name: presetName,
|
||||
version: detail.version || '1.0.0',
|
||||
installed: true,
|
||||
...detail
|
||||
});
|
||||
|
||||
// 初始化默认值
|
||||
const initialValues: Record<string, any> = {};
|
||||
for (const input of detail.schema) {
|
||||
initialValues[input.id] = input.defaultValue ?? '';
|
||||
}
|
||||
setSecrets(initialValues);
|
||||
|
||||
// 关闭安装对话框,打开详情对话框
|
||||
setInstallDialogOpen(false);
|
||||
setInstallUrl('');
|
||||
setInstallFile(null);
|
||||
setInstallName('');
|
||||
setDetailDialogOpen(true);
|
||||
|
||||
setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' });
|
||||
} else {
|
||||
// 不需要配置,直接完成
|
||||
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
||||
setInstallDialogOpen(false);
|
||||
setInstallUrl('');
|
||||
setInstallFile(null);
|
||||
setInstallName('');
|
||||
await loadPresets();
|
||||
}
|
||||
} catch (error) {
|
||||
// 获取详情失败,但安装成功了,刷新列表
|
||||
console.error('Failed to get preset details after installation:', error);
|
||||
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
||||
setInstallDialogOpen(false);
|
||||
setInstallUrl('');
|
||||
setInstallFile(null);
|
||||
setInstallName('');
|
||||
await loadPresets();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to install preset:', error);
|
||||
setToast({ message: t('presets.preset_install_failed', { error: error.message }), type: 'error' });
|
||||
@@ -262,6 +357,8 @@ export function Presets() {
|
||||
setToast({ message: t('presets.preset_applied'), type: 'success' });
|
||||
setDetailDialogOpen(false);
|
||||
setSecrets({});
|
||||
// 刷新预设列表
|
||||
await loadPresets();
|
||||
} catch (error: any) {
|
||||
console.error('Failed to apply preset:', error);
|
||||
setToast({ message: t('presets.preset_apply_failed', { error: error.message }), type: 'error' });
|
||||
@@ -443,7 +540,7 @@ export function Presets() {
|
||||
)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto py-4">
|
||||
<div className="flex-1 overflow-y-auto py-4 px-2">
|
||||
{selectedPreset?.description && (
|
||||
<p className="text-gray-700 mb-4">{selectedPreset.description}</p>
|
||||
)}
|
||||
|
||||
@@ -87,7 +87,7 @@ export function DynamicConfigForm({
|
||||
const visible = new Set<string>();
|
||||
|
||||
for (const field of schema) {
|
||||
if (shouldShowField(field, values)) {
|
||||
if (shouldShowField(field)) {
|
||||
visible.add(field.id);
|
||||
}
|
||||
}
|
||||
@@ -334,7 +334,7 @@ export function DynamicConfigForm({
|
||||
{field.type === 'select' && (
|
||||
<Select
|
||||
value={values[field.id] || ''}
|
||||
onValueChange={(value) => updateValue(field.id, value)}
|
||||
onValueChange={(value: string) => updateValue(field.id, value)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<SelectTrigger id={`field-${field.id}`}>
|
||||
@@ -367,9 +367,9 @@ export function DynamicConfigForm({
|
||||
<Checkbox
|
||||
id={`field-${field.id}-${option.value}`}
|
||||
checked={Array.isArray(values[field.id]) && values[field.id].includes(option.value)}
|
||||
onCheckedChange={(checked) => {
|
||||
onCheckedChange={(checked: boolean | 'indeterminate') => {
|
||||
const current = Array.isArray(values[field.id]) ? values[field.id] : [];
|
||||
if (checked) {
|
||||
if (checked === true) {
|
||||
updateValue(field.id, [...current, option.value]);
|
||||
} else {
|
||||
updateValue(field.id, current.filter((v: any) => v !== option.value));
|
||||
@@ -397,7 +397,7 @@ export function DynamicConfigForm({
|
||||
<Checkbox
|
||||
id={`field-${field.id}`}
|
||||
checked={values[field.id] || false}
|
||||
onCheckedChange={(checked) => updateValue(field.id, checked)}
|
||||
onCheckedChange={(checked: boolean | 'indeterminate') => updateValue(field.id, checked)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Label htmlFor={`field-${field.id}`} className="text-sm font-normal cursor-pointer">
|
||||
@@ -412,7 +412,7 @@ export function DynamicConfigForm({
|
||||
id={`field-${field.id}`}
|
||||
placeholder={field.placeholder}
|
||||
value={values[field.id] || ''}
|
||||
onChange={(e) => updateValue(field.id, e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => updateValue(field.id, e.target.value)}
|
||||
rows={field.rows || 5}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
@@ -45,7 +45,7 @@ class ApiClient {
|
||||
localStorage.removeItem('apiKey');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update temp API key
|
||||
setTempApiKey(tempApiKey: string | null) {
|
||||
this.tempApiKey = tempApiKey;
|
||||
@@ -56,25 +56,25 @@ class ApiClient {
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
|
||||
// Use temp API key if available, otherwise use regular API key
|
||||
if (this.tempApiKey) {
|
||||
headers['X-Temp-API-Key'] = this.tempApiKey;
|
||||
} else if (this.apiKey) {
|
||||
headers['X-API-Key'] = this.apiKey;
|
||||
}
|
||||
|
||||
|
||||
if (contentType) {
|
||||
headers['Content-Type'] = contentType;
|
||||
}
|
||||
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Generic fetch wrapper with base URL and authentication
|
||||
private async apiFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
|
||||
|
||||
const config: RequestInit = {
|
||||
...options,
|
||||
headers: {
|
||||
@@ -82,10 +82,10 @@ class ApiClient {
|
||||
...options.headers,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
|
||||
|
||||
// Handle 401 Unauthorized responses
|
||||
if (response.status === 401) {
|
||||
// Remove API key when it's invalid
|
||||
@@ -101,11 +101,11 @@ class ApiClient {
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
if (response.status === 204) {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
|
||||
const text = await response.text();
|
||||
return text ? JSON.parse(text) : ({} as T);
|
||||
|
||||
@@ -139,9 +139,10 @@ class ApiClient {
|
||||
}
|
||||
|
||||
// DELETE request
|
||||
async delete<T>(endpoint: string): Promise<T> {
|
||||
async delete<T>(endpoint: string, body?: any): Promise<T> {
|
||||
return this.apiFetch<T>(endpoint, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(body || {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,87 +151,87 @@ class ApiClient {
|
||||
async getConfig(): Promise<Config> {
|
||||
return this.get<Config>('/config');
|
||||
}
|
||||
|
||||
|
||||
// Update entire configuration
|
||||
async updateConfig(config: Config): Promise<Config> {
|
||||
return this.post<Config>('/config', config);
|
||||
}
|
||||
|
||||
|
||||
// Get providers
|
||||
async getProviders(): Promise<Provider[]> {
|
||||
return this.get<Provider[]>('/api/providers');
|
||||
}
|
||||
|
||||
|
||||
// Add a new provider
|
||||
async addProvider(provider: Provider): Promise<Provider> {
|
||||
return this.post<Provider>('/api/providers', provider);
|
||||
}
|
||||
|
||||
|
||||
// Update a provider
|
||||
async updateProvider(index: number, provider: Provider): Promise<Provider> {
|
||||
return this.post<Provider>(`/api/providers/${index}`, provider);
|
||||
}
|
||||
|
||||
|
||||
// Delete a provider
|
||||
async deleteProvider(index: number): Promise<void> {
|
||||
return this.delete<void>(`/api/providers/${index}`);
|
||||
}
|
||||
|
||||
|
||||
// Get transformers
|
||||
async getTransformers(): Promise<Transformer[]> {
|
||||
return this.get<Transformer[]>('/api/transformers');
|
||||
}
|
||||
|
||||
|
||||
// Add a new transformer
|
||||
async addTransformer(transformer: Transformer): Promise<Transformer> {
|
||||
return this.post<Transformer>('/api/transformers', transformer);
|
||||
}
|
||||
|
||||
|
||||
// Update a transformer
|
||||
async updateTransformer(index: number, transformer: Transformer): Promise<Transformer> {
|
||||
return this.post<Transformer>(`/api/transformers/${index}`, transformer);
|
||||
}
|
||||
|
||||
|
||||
// Delete a transformer
|
||||
async deleteTransformer(index: number): Promise<void> {
|
||||
return this.delete<void>(`/api/transformers/${index}`);
|
||||
}
|
||||
|
||||
|
||||
// Get configuration (new endpoint)
|
||||
async getConfigNew(): Promise<Config> {
|
||||
return this.get<Config>('/config');
|
||||
}
|
||||
|
||||
|
||||
// Save configuration (new endpoint)
|
||||
async saveConfig(config: Config): Promise<unknown> {
|
||||
return this.post<Config>('/config', config);
|
||||
}
|
||||
|
||||
|
||||
// Restart service
|
||||
async restartService(): Promise<unknown> {
|
||||
return this.post<void>('/restart', {});
|
||||
}
|
||||
|
||||
|
||||
// Check for updates
|
||||
async checkForUpdates(): Promise<{ hasUpdate: boolean; latestVersion?: string; changelog?: string }> {
|
||||
return this.get<{ hasUpdate: boolean; latestVersion?: string; changelog?: string }>('/update/check');
|
||||
}
|
||||
|
||||
|
||||
// Perform update
|
||||
async performUpdate(): Promise<{ success: boolean; message: string }> {
|
||||
return this.post<{ success: boolean; message: string }>('/api/update/perform', {});
|
||||
}
|
||||
|
||||
|
||||
// Get log files list
|
||||
async getLogFiles(): Promise<Array<{ name: string; path: string; size: number; lastModified: string }>> {
|
||||
return this.get<Array<{ name: string; path: string; size: number; lastModified: string }>>('/logs/files');
|
||||
}
|
||||
|
||||
|
||||
// Get logs from specific file
|
||||
async getLogs(filePath: string): Promise<string[]> {
|
||||
return this.get<string[]>(`/logs?file=${encodeURIComponent(filePath)}`);
|
||||
}
|
||||
|
||||
|
||||
// Clear logs from specific file
|
||||
async clearLogs(filePath: string): Promise<void> {
|
||||
return this.delete<void>(`/logs?file=${encodeURIComponent(filePath)}`);
|
||||
@@ -300,7 +301,7 @@ class ApiClient {
|
||||
|
||||
// Delete preset
|
||||
async deletePreset(name: string): Promise<any> {
|
||||
return this.delete<any>(`/presets/${encodeURIComponent(name)}`);
|
||||
return this.delete<any>(`/presets/${encodeURIComponent(name)}`, {});
|
||||
}
|
||||
|
||||
// Get market presets
|
||||
@@ -318,4 +319,4 @@ class ApiClient {
|
||||
export const api = new ApiClient();
|
||||
|
||||
// Export the class for creating custom instances
|
||||
export default ApiClient;
|
||||
export default ApiClient;
|
||||
|
||||
@@ -282,6 +282,9 @@
|
||||
"load_presets_failed": "Failed to load presets",
|
||||
"load_preset_details_failed": "Failed to load preset details",
|
||||
"please_fill_field": "Please fill in {{field}}",
|
||||
"load_market_failed": "Failed to load market presets"
|
||||
"load_market_failed": "Failed to load market presets",
|
||||
"preset_installed_config_required": "Preset installed, please complete configuration",
|
||||
"please_provide_file": "Please provide a preset file",
|
||||
"please_provide_url": "Please provide a preset URL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +282,9 @@
|
||||
"load_presets_failed": "加载预设失败",
|
||||
"load_preset_details_failed": "加载预设详情失败",
|
||||
"please_fill_field": "请填写 {{field}}",
|
||||
"load_market_failed": "加载市场预设失败"
|
||||
"load_market_failed": "加载市场预设失败",
|
||||
"preset_installed_config_required": "预设已安装,请完成配置",
|
||||
"please_provide_file": "请提供预设文件",
|
||||
"please_provide_url": "请提供预设 URL"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user