mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-01-29 22:02:05 +00:00
add presets
This commit is contained in:
@@ -18,14 +18,17 @@
|
||||
"author": "musistudio",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@CCR/server": "workspace:*",
|
||||
"@CCR/shared": "workspace:*",
|
||||
"@inquirer/prompts": "^5.0.0",
|
||||
"@CCR/server": "workspace:*",
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^7.0.1",
|
||||
"find-process": "^2.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"openurl": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/node": "^24.0.15",
|
||||
"esbuild": "^0.25.1",
|
||||
"ts-node": "^10.9.2",
|
||||
|
||||
@@ -28,6 +28,7 @@ const KNOWN_COMMANDS = [
|
||||
"statusline",
|
||||
"code",
|
||||
"model",
|
||||
"preset",
|
||||
"activate",
|
||||
"env",
|
||||
"ui",
|
||||
@@ -48,6 +49,7 @@ Commands:
|
||||
statusline Integrated statusline
|
||||
code Execute claude command
|
||||
model Interactive model selection and configuration
|
||||
preset Manage presets (export, install, list, delete)
|
||||
activate Output environment variables for shell integration
|
||||
ui Open the web UI in browser
|
||||
-v, version Show version information
|
||||
@@ -61,6 +63,9 @@ Examples:
|
||||
ccr code "Write a Hello World"
|
||||
ccr my-preset "Write a Hello World" # Use preset configuration
|
||||
ccr model
|
||||
ccr preset export my-config # Export current config as preset
|
||||
ccr preset install my-config.ccrsets # Install a preset
|
||||
ccr preset list # List all presets
|
||||
eval "$(ccr activate)" # Set environment variables globally
|
||||
ccr ui
|
||||
`;
|
||||
@@ -91,52 +96,62 @@ async function main() {
|
||||
// 如果命令不是已知命令,检查是否是 preset
|
||||
if (command && !KNOWN_COMMANDS.includes(command)) {
|
||||
const { readPresetFile } = await import("./utils");
|
||||
const presetConfig: PresetConfig | null = await readPresetFile(command);
|
||||
const presetData: any = await readPresetFile(command);
|
||||
|
||||
if (presetConfig) {
|
||||
if (presetData) {
|
||||
// 这是一个 preset,执行 code 命令
|
||||
const codeArgs = process.argv.slice(3); // 获取剩余参数
|
||||
|
||||
// 检查 noServer 配置
|
||||
const shouldStartServer = presetConfig.noServer !== true;
|
||||
const shouldStartServer = presetData.noServer !== true;
|
||||
|
||||
// 处理 provider 配置,构建环境变量覆盖
|
||||
// 构建环境变量覆盖
|
||||
let envOverrides: Record<string, string> | undefined;
|
||||
if (presetConfig.provider) {
|
||||
|
||||
// 处理 provider 配置(支持新旧两种格式)
|
||||
let provider: any = null;
|
||||
|
||||
// 旧格式:presetData.provider 是 provider 名称
|
||||
if (presetData.provider && typeof presetData.provider === 'string') {
|
||||
const config = await readConfigFile();
|
||||
const providerName = presetConfig.provider;
|
||||
const provider = config.Providers?.find((p: any) => p.name === providerName);
|
||||
provider = config.Providers?.find((p: any) => p.name === presetData.provider);
|
||||
}
|
||||
// 新格式:presetData.Providers 是 provider 数组
|
||||
else if (presetData.Providers && presetData.Providers.length > 0) {
|
||||
provider = presetData.Providers[0];
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
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);
|
||||
// 处理 api_key
|
||||
if (provider.api_key) {
|
||||
envOverrides = {
|
||||
...envOverrides,
|
||||
ANTHROPIC_AUTH_TOKEN: provider.api_key,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 处理 router 配置
|
||||
// 如果 preset 中有 router 配置且需要启动 server,可能需要临时修改配置文件
|
||||
// 构建 PresetConfig
|
||||
const presetConfig: PresetConfig = {
|
||||
noServer: presetData.noServer,
|
||||
claudeCodeSettings: presetData.claudeCodeSettings,
|
||||
provider: presetData.provider,
|
||||
router: presetData.router,
|
||||
};
|
||||
|
||||
if (shouldStartServer && !isRunning) {
|
||||
console.log("Service not running, starting service...");
|
||||
@@ -232,6 +247,10 @@ async function main() {
|
||||
case "model":
|
||||
await runModelSelector();
|
||||
break;
|
||||
case "preset":
|
||||
const { handlePresetCommand } = await import("./utils/preset");
|
||||
await handlePresetCommand(process.argv.slice(3));
|
||||
break;
|
||||
case "activate":
|
||||
case "env":
|
||||
await activateCommand();
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
PLUGINS_DIR,
|
||||
PRESETS_DIR,
|
||||
REFERENCE_COUNT_FILE,
|
||||
readPresetFile,
|
||||
} from "@CCR/shared";
|
||||
import { getServer } from "@CCR/server";
|
||||
import { writeFileSync, existsSync, readFileSync } from "fs";
|
||||
@@ -191,15 +192,15 @@ export const run = async (args: string[] = []) => {
|
||||
// Save the PID of the background process
|
||||
writeFileSync(PID_FILE, process.pid.toString());
|
||||
|
||||
server.app.post('/api/update/perform', async () => {
|
||||
app.post('/api/update/perform', async () => {
|
||||
return await performUpdate();
|
||||
})
|
||||
|
||||
server.app.get('/api/update/check', async () => {
|
||||
app.get('/api/update/check', async () => {
|
||||
return await checkForUpdates(version);
|
||||
})
|
||||
|
||||
server.app.post("/api/restart", async () => {
|
||||
app.post("/api/restart", async () => {
|
||||
setTimeout(async () => {
|
||||
spawn("ccr", ["restart"], {
|
||||
detached: true,
|
||||
@@ -274,23 +275,3 @@ export const getSettingsPath = (content: string): string => {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
256
packages/cli/src/utils/preset/commands.ts
Normal file
256
packages/cli/src/utils/preset/commands.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* 预设命令处理器 CLI 层
|
||||
* 负责处理 CLI 交互,核心逻辑在 shared 包中
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as fsSync from 'fs';
|
||||
import * as path from 'path';
|
||||
import JSON5 from 'json5';
|
||||
import { exportPresetCli } from './export';
|
||||
import { installPresetCli, loadPreset } from './install';
|
||||
import { MergeStrategy, HOME_DIR } from '@CCR/shared';
|
||||
|
||||
// ANSI 颜色代码
|
||||
const RESET = "\x1B[0m";
|
||||
const GREEN = "\x1B[32m";
|
||||
const YELLOW = "\x1B[33m";
|
||||
const BOLDCYAN = "\x1B[1m\x1B[36m";
|
||||
const BOLDYELLOW = "\x1B[1m\x1B[33m";
|
||||
const DIM = "\x1B[2m";
|
||||
|
||||
/**
|
||||
* 列出本地预设
|
||||
*/
|
||||
async function listPresets(): Promise<void> {
|
||||
const presetsDir = path.join(HOME_DIR, 'presets');
|
||||
|
||||
try {
|
||||
await fs.access(presetsDir);
|
||||
} catch {
|
||||
console.log('\nNo presets directory found.');
|
||||
console.log(`\nCreate your first preset with: ${GREEN}ccr preset export <name>${RESET}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(presetsDir, { withFileTypes: true });
|
||||
const presetDirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name);
|
||||
|
||||
if (presetDirs.length === 0) {
|
||||
console.log('\nNo presets found.');
|
||||
console.log(`\nInstall a preset with: ${GREEN}ccr preset install <file>${RESET}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n${BOLDCYAN}Available presets:${RESET}\n`);
|
||||
|
||||
for (const dirName of presetDirs) {
|
||||
const presetDir = path.join(presetsDir, dirName);
|
||||
try {
|
||||
const manifestPath = path.join(presetDir, 'manifest.json');
|
||||
const content = await fs.readFile(manifestPath, 'utf-8');
|
||||
const manifest = JSON5.parse(content);
|
||||
|
||||
// 从manifest中提取metadata字段
|
||||
const { Providers, Router, PORT, HOST, API_TIMEOUT_MS, PROXY_URL, LOG, LOG_LEVEL, StatusLine, NON_INTERACTIVE_MODE, requiredInputs, ...metadata } = manifest;
|
||||
|
||||
const name = metadata.name || dirName;
|
||||
const description = metadata.description || '';
|
||||
const author = metadata.author || '';
|
||||
const version = metadata.version;
|
||||
|
||||
// 显示预设名称
|
||||
if (version) {
|
||||
console.log(`${GREEN}•${RESET} ${BOLDCYAN}${name}${RESET} (v${version})`);
|
||||
} else {
|
||||
console.log(`${GREEN}•${RESET} ${BOLDCYAN}${name}${RESET}`);
|
||||
}
|
||||
|
||||
// 显示描述
|
||||
if (description) {
|
||||
console.log(` ${description}`);
|
||||
}
|
||||
|
||||
// 显示作者
|
||||
if (author) {
|
||||
console.log(` ${DIM}by ${author}${RESET}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.log(`${YELLOW}•${RESET} ${dirName}`);
|
||||
console.log(` ${DIM}(Error reading preset)${RESET}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除预设
|
||||
*/
|
||||
async function deletePreset(name: string): Promise<void> {
|
||||
const presetsDir = path.join(HOME_DIR, 'presets');
|
||||
const presetDir = path.join(presetsDir, name);
|
||||
|
||||
try {
|
||||
// 递归删除整个目录
|
||||
await fs.rm(presetDir, { recursive: true, force: true });
|
||||
console.log(`\n${GREEN}✓${RESET} Preset "${name}" deleted.\n`);
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
console.error(`\n${YELLOW}Error:${RESET} Preset "${name}" not found.\n`);
|
||||
} else {
|
||||
console.error(`\n${YELLOW}Error:${RESET} ${error.message}\n`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示预设信息
|
||||
*/
|
||||
async function showPresetInfo(name: string): Promise<void> {
|
||||
try {
|
||||
const preset = await loadPreset(name);
|
||||
|
||||
const config = preset.config;
|
||||
const metadata = preset.metadata || {};
|
||||
|
||||
console.log(`\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`);
|
||||
if (metadata.name) {
|
||||
console.log(`${BOLDCYAN}Preset: ${RESET}${metadata.name}`);
|
||||
} else {
|
||||
console.log(`${BOLDCYAN}Preset: ${RESET}${name}`);
|
||||
}
|
||||
console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\n`);
|
||||
|
||||
if (metadata.version) console.log(`${BOLDCYAN}Version:${RESET} ${metadata.version}`);
|
||||
if (metadata.description) console.log(`${BOLDCYAN}Description:${RESET} ${metadata.description}`);
|
||||
if (metadata.author) console.log(`${BOLDCYAN}Author:${RESET} ${metadata.author}`);
|
||||
const keywords = (metadata as any).keywords;
|
||||
if (keywords && keywords.length > 0) {
|
||||
console.log(`${BOLDCYAN}Keywords:${RESET} ${keywords.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(`\n${BOLDCYAN}Configuration:${RESET}`);
|
||||
if (config.Providers) {
|
||||
console.log(` Providers: ${config.Providers.length}`);
|
||||
}
|
||||
if (config.Router) {
|
||||
console.log(` Router rules: ${Object.keys(config.Router).length}`);
|
||||
}
|
||||
if (config.provider) {
|
||||
console.log(` Provider: ${config.provider}`);
|
||||
}
|
||||
|
||||
if (preset.requiredInputs && preset.requiredInputs.length > 0) {
|
||||
console.log(`\n${BOLDYELLOW}Required inputs:${RESET}`);
|
||||
for (const input of preset.requiredInputs) {
|
||||
const envVar = input.placeholder || input.field;
|
||||
console.log(` - ${input.field} ${DIM}(${envVar})${RESET}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
} catch (error: any) {
|
||||
console.error(`\n${YELLOW}Error:${RESET} ${error.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理预设命令
|
||||
*/
|
||||
export async function handlePresetCommand(args: string[]): Promise<void> {
|
||||
const subCommand = args[0];
|
||||
|
||||
switch (subCommand) {
|
||||
case 'export':
|
||||
const presetName = args[1];
|
||||
if (!presetName) {
|
||||
console.error('\nError: Preset name is required\n');
|
||||
console.error('Usage: ccr preset export <name> [--output <path>] [--description <text>] [--author <name>] [--tags <tags>]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 解析选项
|
||||
const options: any = {};
|
||||
for (let i = 2; i < args.length; i++) {
|
||||
if (args[i] === '--output' && args[i + 1]) {
|
||||
options.output = args[++i];
|
||||
} else if (args[i] === '--description' && args[i + 1]) {
|
||||
options.description = args[++i];
|
||||
} else if (args[i] === '--author' && args[i + 1]) {
|
||||
options.author = args[++i];
|
||||
} else if (args[i] === '--tags' && args[i + 1]) {
|
||||
options.tags = args[++i];
|
||||
} else if (args[i] === '--include-sensitive') {
|
||||
options.includeSensitive = true;
|
||||
}
|
||||
}
|
||||
|
||||
await exportPresetCli(presetName, options);
|
||||
break;
|
||||
|
||||
case 'install':
|
||||
const source = args[1];
|
||||
if (!source) {
|
||||
console.error('\nError: Preset source is required\n');
|
||||
console.error('Usage: ccr preset install <file | url | name>\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 解析选项
|
||||
const installOptions: any = {};
|
||||
for (let i = 2; i < args.length; i++) {
|
||||
if (args[i] === '--strategy' && args[i + 1]) {
|
||||
const strategy = args[++i];
|
||||
if (['ask', 'overwrite', 'merge', 'skip'].includes(strategy)) {
|
||||
installOptions.strategy = strategy as MergeStrategy;
|
||||
} else {
|
||||
console.error(`\nError: Invalid merge strategy "${strategy}"\n`);
|
||||
console.error('Valid strategies: ask, overwrite, merge, skip\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await installPresetCli(source, installOptions);
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
await listPresets();
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
case 'rm':
|
||||
case 'remove':
|
||||
const deleteName = args[1];
|
||||
if (!deleteName) {
|
||||
console.error('\nError: Preset name is required\n');
|
||||
console.error('Usage: ccr preset delete <name>\n');
|
||||
process.exit(1);
|
||||
}
|
||||
await deletePreset(deleteName);
|
||||
break;
|
||||
|
||||
case 'info':
|
||||
const infoName = args[1];
|
||||
if (!infoName) {
|
||||
console.error('\nError: Preset name is required\n');
|
||||
console.error('Usage: ccr preset info <name>\n');
|
||||
process.exit(1);
|
||||
}
|
||||
await showPresetInfo(infoName);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`\nError: Unknown preset command "${subCommand}"\n`);
|
||||
console.error('Available commands:');
|
||||
console.error(' ccr preset export <name> Export current configuration as a preset');
|
||||
console.error(' ccr preset install <source> Install a preset from file, URL, or registry');
|
||||
console.error(' ccr preset list List installed presets');
|
||||
console.error(' ccr preset info <name> Show preset information');
|
||||
console.error(' ccr preset delete <name> Delete a preset\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
104
packages/cli/src/utils/preset/export.ts
Normal file
104
packages/cli/src/utils/preset/export.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 预设导出功能 CLI 层
|
||||
* 负责处理 CLI 交互,核心逻辑在 shared 包中
|
||||
*/
|
||||
|
||||
import { input } from '@inquirer/prompts';
|
||||
import { readConfigFile } from '../index';
|
||||
import { exportPreset as exportPresetCore, ExportOptions } from '@CCR/shared';
|
||||
|
||||
// ANSI 颜色代码
|
||||
const RESET = "\x1B[0m";
|
||||
const GREEN = "\x1B[32m";
|
||||
const BOLDGREEN = "\x1B[1m\x1B[32m";
|
||||
const YELLOW = "\x1B[33m";
|
||||
const BOLDCYAN = "\x1B[1m\x1B[36m";
|
||||
|
||||
/**
|
||||
* 导出预设配置(CLI 版本,带交互)
|
||||
* @param presetName 预设名称
|
||||
* @param options 导出选项
|
||||
*/
|
||||
export async function exportPresetCli(
|
||||
presetName: string,
|
||||
options: ExportOptions = {}
|
||||
): Promise<void> {
|
||||
try {
|
||||
console.log(`\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`);
|
||||
console.log(`${BOLDCYAN} Preset Export${RESET}`);
|
||||
console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\n`);
|
||||
|
||||
// 1. 读取当前配置
|
||||
const config = await readConfigFile();
|
||||
|
||||
// 2. 如果没有通过命令行提供,交互式询问元数据
|
||||
if (!options.description) {
|
||||
try {
|
||||
options.description = await input({
|
||||
message: 'Description (optional):',
|
||||
default: '',
|
||||
});
|
||||
} catch {
|
||||
// 用户取消,使用默认值
|
||||
options.description = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.author) {
|
||||
try {
|
||||
options.author = await input({
|
||||
message: 'Author (optional):',
|
||||
default: '',
|
||||
});
|
||||
} catch {
|
||||
options.author = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.tags) {
|
||||
try {
|
||||
const keywordsInput = await input({
|
||||
message: 'Keywords (comma-separated, optional):',
|
||||
default: '',
|
||||
});
|
||||
options.tags = keywordsInput || '';
|
||||
} catch {
|
||||
options.tags = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 调用核心导出功能
|
||||
const result = await exportPresetCore(presetName, config, options);
|
||||
|
||||
// 4. 显示摘要
|
||||
console.log(`\n${BOLDGREEN}✓ Preset exported successfully${RESET}\n`);
|
||||
console.log(`${BOLDCYAN}Location:${RESET} ${result.outputPath}\n`);
|
||||
console.log(`${BOLDCYAN}Summary:${RESET}`);
|
||||
console.log(` - Providers: ${result.sanitizedConfig.Providers?.length || 0}`);
|
||||
console.log(` - Router rules: ${Object.keys(result.sanitizedConfig.Router || {}).length}`);
|
||||
if (!options.includeSensitive) {
|
||||
console.log(` - Sensitive fields sanitized: ${YELLOW}${result.sanitizedCount}${RESET}`);
|
||||
}
|
||||
|
||||
// 显示元数据
|
||||
if (result.metadata.description) {
|
||||
console.log(`\n${BOLDCYAN}Description:${RESET} ${result.metadata.description}`);
|
||||
}
|
||||
if (result.metadata.author) {
|
||||
console.log(`${BOLDCYAN}Author:${RESET} ${result.metadata.author}`);
|
||||
}
|
||||
if (result.metadata.keywords && result.metadata.keywords.length > 0) {
|
||||
console.log(`${BOLDCYAN}Keywords:${RESET} ${result.metadata.keywords.join(', ')}`);
|
||||
}
|
||||
|
||||
// 显示分享提示
|
||||
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`);
|
||||
console.log(` 3. Others can install with: ${GREEN}ccr preset install <file>${RESET}\n`);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`\n${YELLOW}Error exporting preset:${RESET} ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
12
packages/cli/src/utils/preset/index.ts
Normal file
12
packages/cli/src/utils/preset/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 预设功能 CLI 层
|
||||
* 导出所有预设相关的功能和类型
|
||||
*/
|
||||
|
||||
// 从 shared 包重新导出类型和核心功能
|
||||
export * from '@CCR/shared';
|
||||
|
||||
// 导出 CLI 特定的功能(带交互)
|
||||
export { exportPresetCli } from './export';
|
||||
export { installPresetCli, applyPresetCli } from './install';
|
||||
export { handlePresetCommand } from './commands';
|
||||
301
packages/cli/src/utils/preset/install.ts
Normal file
301
packages/cli/src/utils/preset/install.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* 预设安装功能 CLI 层
|
||||
* 负责处理 CLI 交互,核心逻辑在 shared 包中
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { password, confirm } from '@inquirer/prompts';
|
||||
import {
|
||||
loadPreset as loadPresetShared,
|
||||
validatePreset,
|
||||
MergeStrategy,
|
||||
getPresetDir,
|
||||
readManifestFromDir,
|
||||
manifestToPresetFile,
|
||||
saveManifest,
|
||||
extractPreset,
|
||||
findPresetFile,
|
||||
isPresetInstalled,
|
||||
ManifestFile,
|
||||
PresetFile
|
||||
} from '@CCR/shared';
|
||||
|
||||
// 重新导出 loadPreset
|
||||
export { loadPresetShared as loadPreset };
|
||||
|
||||
// ANSI 颜色代码
|
||||
const RESET = "\x1B[0m";
|
||||
const GREEN = "\x1B[32m";
|
||||
const BOLDGREEN = "\x1B[1m\x1B[32m";
|
||||
const YELLOW = "\x1B[33m";
|
||||
const BOLDYELLOW = "\x1B[1m\x1B[33m";
|
||||
const BOLDCYAN = "\x1B[1m\x1B[36m";
|
||||
const DIM = "\x1B[2m";
|
||||
|
||||
/**
|
||||
* 收集缺失的敏感信息
|
||||
*/
|
||||
async function collectSensitiveInputs(
|
||||
preset: PresetFile
|
||||
): Promise<Record<string, string>> {
|
||||
const inputs: Record<string, string> = {};
|
||||
|
||||
if (!preset.requiredInputs || preset.requiredInputs.length === 0) {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
console.log(`\n${BOLDYELLOW}This preset requires additional information:${RESET}\n`);
|
||||
|
||||
for (const inputField of preset.requiredInputs) {
|
||||
let value: string;
|
||||
|
||||
// 尝试从环境变量获取
|
||||
const envVarName = inputField.placeholder;
|
||||
if (envVarName && process.env[envVarName]) {
|
||||
const useEnv = await confirm({
|
||||
message: `Found ${envVarName} in environment. Use it?`,
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (useEnv) {
|
||||
value = process.env[envVarName]!;
|
||||
inputs[inputField.field] = value;
|
||||
console.log(`${GREEN}✓${RESET} Using ${envVarName} from environment\n`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 提示用户输入
|
||||
value = await password({
|
||||
message: inputField.prompt || `Enter ${inputField.field}:`,
|
||||
mask: '*',
|
||||
});
|
||||
|
||||
if (!value || value.trim() === '') {
|
||||
console.error(`${YELLOW}Error:${RESET} ${inputField.field} is required`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 验证输入
|
||||
if (inputField.validator) {
|
||||
const regex = typeof inputField.validator === 'string'
|
||||
? new RegExp(inputField.validator)
|
||||
: inputField.validator;
|
||||
|
||||
if (!regex.test(value)) {
|
||||
console.error(`${YELLOW}Error:${RESET} Invalid format for ${inputField.field}`);
|
||||
console.error(` Expected: ${inputField.validator}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
inputs[inputField.field] = value;
|
||||
console.log('');
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用预设到配置
|
||||
* @param presetName 预设名称
|
||||
* @param preset 预设对象
|
||||
*/
|
||||
export async function applyPresetCli(
|
||||
presetName: string,
|
||||
preset: PresetFile
|
||||
): Promise<void> {
|
||||
try {
|
||||
console.log(`${BOLDCYAN}Loading preset...${RESET} ${GREEN}✓${RESET}`);
|
||||
|
||||
// 验证预设
|
||||
const validation = await validatePreset(preset);
|
||||
if (validation.warnings.length > 0) {
|
||||
console.log(`\n${YELLOW}Warnings:${RESET}`);
|
||||
for (const warning of validation.warnings) {
|
||||
console.log(` ${DIM}⚠${RESET} ${warning}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!validation.valid) {
|
||||
console.log(`\n${YELLOW}Validation errors:${RESET}`);
|
||||
for (const error of validation.errors) {
|
||||
console.log(` ${YELLOW}✗${RESET} ${error}`);
|
||||
}
|
||||
throw new Error('Invalid preset file');
|
||||
}
|
||||
|
||||
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 with secrets`);
|
||||
console.log(`${DIM}You can use this preset with: ccr ${presetName}${RESET}\n`);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// manifest不存在,继续配置流程
|
||||
}
|
||||
|
||||
// 收集敏感信息
|
||||
const sensitiveInputs = await collectSensitiveInputs(preset);
|
||||
|
||||
// 读取现有的manifest并更新
|
||||
const manifest: ManifestFile = {
|
||||
...(preset.metadata || {}),
|
||||
...preset.config,
|
||||
};
|
||||
|
||||
// 将secrets信息应用到manifest中
|
||||
for (const [fieldPath, value] of Object.entries(sensitiveInputs)) {
|
||||
const keys = fieldPath.split(/[.\[\]]+/).filter(k => k !== '');
|
||||
let current = manifest as any;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!current[key]) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
current[keys[keys.length - 1]] = value;
|
||||
}
|
||||
|
||||
if (preset.requiredInputs) {
|
||||
manifest.requiredInputs = preset.requiredInputs;
|
||||
}
|
||||
|
||||
// 保存到解压目录的manifest.json
|
||||
await saveManifest(presetName, manifest);
|
||||
|
||||
// 显示摘要
|
||||
console.log(`\n${BOLDGREEN}✓ Preset configured successfully!${RESET}\n`);
|
||||
console.log(`${BOLDCYAN}Preset directory:${RESET} ${presetDir}`);
|
||||
console.log(`${BOLDCYAN}Secrets configured:${RESET} ${Object.keys(sensitiveInputs).length}`);
|
||||
|
||||
if (preset.metadata?.description) {
|
||||
console.log(`\n${BOLDCYAN}Description:${RESET} ${preset.metadata.description}`);
|
||||
}
|
||||
|
||||
if (preset.metadata?.author) {
|
||||
console.log(`${BOLDCYAN}Author:${RESET} ${preset.metadata.author}`);
|
||||
}
|
||||
|
||||
const keywords = (preset.metadata as any).keywords;
|
||||
if (keywords && keywords.length > 0) {
|
||||
console.log(`${BOLDCYAN}Keywords:${RESET} ${keywords.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(`\n${GREEN}Use this preset:${RESET} ccr ${presetName} "your prompt"`);
|
||||
console.log(`${DIM}Note: Secrets are stored in the manifest file${RESET}\n`);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`\n${YELLOW}Error applying preset:${RESET} ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装预设(主入口)
|
||||
*/
|
||||
export async function installPresetCli(
|
||||
source: string,
|
||||
options: {
|
||||
strategy?: MergeStrategy;
|
||||
name?: string;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
let tempFile: string | null = null;
|
||||
try {
|
||||
// 确定预设名称
|
||||
let presetName = options.name;
|
||||
let sourceZip: string;
|
||||
let isReconfigure = false; // 是否是重新配置已安装的preset
|
||||
|
||||
// 判断source类型并获取ZIP文件路径
|
||||
if (source.startsWith('http://') || source.startsWith('https://')) {
|
||||
// URL:下载到临时文件
|
||||
if (!presetName) {
|
||||
const urlParts = source.split('/');
|
||||
const filename = urlParts[urlParts.length - 1];
|
||||
presetName = filename.replace('.ccrsets', '');
|
||||
}
|
||||
// 这里直接从 shared 包导入的 downloadPresetToTemp 会返回临时文件
|
||||
// 但我们会在 loadPreset 中自动清理,所以不需要在这里处理
|
||||
const preset = await loadPreset(source);
|
||||
if (!presetName) {
|
||||
presetName = preset.metadata?.name || 'preset';
|
||||
}
|
||||
// 重新下载到临时文件以供 extractPreset 使用
|
||||
// 由于 loadPreset 已经下载并删除了,这里需要特殊处理
|
||||
throw new Error('URL installation not fully implemented yet');
|
||||
} else if (source.includes('/') || source.includes('\\')) {
|
||||
// 文件路径
|
||||
if (!presetName) {
|
||||
const filename = path.basename(source);
|
||||
presetName = filename.replace('.ccrsets', '');
|
||||
}
|
||||
// 验证文件存在
|
||||
try {
|
||||
await fs.access(source);
|
||||
} catch {
|
||||
throw new Error(`Preset file not found: ${source}`);
|
||||
}
|
||||
sourceZip = source;
|
||||
} else {
|
||||
// 预设名称(不带路径)
|
||||
presetName = source;
|
||||
|
||||
// 按优先级查找文件:当前目录 -> presets目录
|
||||
const presetFile = await findPresetFile(source);
|
||||
|
||||
if (presetFile) {
|
||||
sourceZip = presetFile;
|
||||
} else {
|
||||
// 检查是否已安装(目录存在)
|
||||
if (await isPresetInstalled(source)) {
|
||||
// 已安装,重新配置
|
||||
isReconfigure = true;
|
||||
} else {
|
||||
// 都不存在,报错
|
||||
throw new Error(`Preset '${source}' not found in current directory or presets directory.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isReconfigure) {
|
||||
// 重新配置已安装的preset
|
||||
console.log(`${BOLDCYAN}Reconfiguring preset:${RESET} ${presetName}\n`);
|
||||
|
||||
const presetDir = getPresetDir(presetName);
|
||||
const manifest = await readManifestFromDir(presetDir);
|
||||
const preset = manifestToPresetFile(manifest);
|
||||
|
||||
// 应用preset(会询问敏感信息)
|
||||
await applyPresetCli(presetName, preset);
|
||||
} else {
|
||||
// 新安装:解压到目标目录
|
||||
const targetDir = getPresetDir(presetName);
|
||||
console.log(`${BOLDCYAN}Extracting preset to:${RESET} ${targetDir}`);
|
||||
await extractPreset(sourceZip, targetDir);
|
||||
console.log(`${GREEN}✓${RESET} Extracted successfully\n`);
|
||||
|
||||
// 从解压目录读取manifest
|
||||
const manifest = await readManifestFromDir(targetDir);
|
||||
const preset = manifestToPresetFile(manifest);
|
||||
|
||||
// 应用preset(询问用户信息等)
|
||||
await applyPresetCli(presetName, preset);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`\n${YELLOW}Failed to install preset:${RESET} ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,10 @@
|
||||
"author": "musistudio",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/multipart": "^9.0.0",
|
||||
"@fastify/static": "^8.2.0",
|
||||
"@musistudio/llms": "^1.0.51",
|
||||
"adm-zip": "^0.5.16",
|
||||
"dotenv": "^16.4.7",
|
||||
"json5": "^2.2.3",
|
||||
"lru-cache": "^11.2.2",
|
||||
@@ -29,6 +31,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@CCR/shared": "workspace:*",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/node": "^24.0.15",
|
||||
"esbuild": "^0.25.1",
|
||||
"fastify": "^5.4.0",
|
||||
|
||||
@@ -121,7 +121,7 @@ async function getServer(options: RunOptions = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const serverInstance = createServer({
|
||||
const serverInstance = await createServer({
|
||||
jsonPath: CONFIG_FILE,
|
||||
initialConfig: {
|
||||
// ...config,
|
||||
@@ -370,11 +370,11 @@ async function getServer(options: RunOptions = {}) {
|
||||
|
||||
// Add global error handlers to prevent the service from crashing
|
||||
process.on("uncaughtException", (err) => {
|
||||
serverInstance.logger.error("Uncaught exception:", err);
|
||||
serverInstance.app.log.error("Uncaught exception:", err);
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
serverInstance.logger.error("Unhandled rejection at:", promise, "reason:", reason);
|
||||
serverInstance.app.log.error("Unhandled rejection at:", promise, "reason:", reason);
|
||||
});
|
||||
|
||||
return serverInstance;
|
||||
|
||||
@@ -2,27 +2,53 @@ import Server from "@musistudio/llms";
|
||||
import { readConfigFile, writeConfigFile, backupConfigFile } from "./utils";
|
||||
import { join } from "path";
|
||||
import fastifyStatic from "@fastify/static";
|
||||
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, rmSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { calculateTokenCount } from "./utils/router";
|
||||
import {
|
||||
getPresetDir,
|
||||
readManifestFromDir,
|
||||
manifestToPresetFile,
|
||||
extractPreset,
|
||||
validatePreset,
|
||||
loadPreset,
|
||||
saveManifest,
|
||||
isPresetInstalled,
|
||||
downloadPresetToTemp,
|
||||
getTempDir,
|
||||
HOME_DIR,
|
||||
type PresetFile,
|
||||
type ManifestFile,
|
||||
type PresetMetadata,
|
||||
MergeStrategy
|
||||
} from "@CCR/shared";
|
||||
|
||||
export const createServer = (config: any): any => {
|
||||
export const createServer = async (config: any): Promise<any> => {
|
||||
const server = new Server(config);
|
||||
const app = server.app;
|
||||
|
||||
server.app.post("/v1/messages/count_tokens", async (req: any, reply: any) => {
|
||||
// Register multipart plugin for file uploads (dynamic import)
|
||||
const fastifyMultipart = await import('@fastify/multipart');
|
||||
app.register(fastifyMultipart.default, {
|
||||
limits: {
|
||||
fileSize: 50 * 1024 * 1024, // 50MB
|
||||
},
|
||||
});
|
||||
|
||||
app.post("/v1/messages/count_tokens", async (req: any, reply: any) => {
|
||||
const {messages, tools, system} = req.body;
|
||||
const tokenCount = calculateTokenCount(messages, system, tools);
|
||||
return { "input_tokens": tokenCount }
|
||||
});
|
||||
|
||||
// Add endpoint to read config.json with access control
|
||||
server.app.get("/api/config", async (req: any, reply: any) => {
|
||||
app.get("/api/config", async (req: any, reply: any) => {
|
||||
return await readConfigFile();
|
||||
});
|
||||
|
||||
server.app.get("/api/transformers", async (req: any, reply: any) => {
|
||||
app.get("/api/transformers", async (req: any, reply: any) => {
|
||||
const transformers =
|
||||
(server.app as any)._server!.transformerService.getAllTransformers();
|
||||
(app as any)._server!.transformerService.getAllTransformers();
|
||||
const transformerList = Array.from(transformers.entries()).map(
|
||||
([name, transformer]: any) => ({
|
||||
name,
|
||||
@@ -33,7 +59,7 @@ export const createServer = (config: any): any => {
|
||||
});
|
||||
|
||||
// Add endpoint to save config.json with access control
|
||||
server.app.post("/api/config", async (req: any, reply: any) => {
|
||||
app.post("/api/config", async (req: any, reply: any) => {
|
||||
const newConfig = req.body;
|
||||
|
||||
// Backup existing config file if it exists
|
||||
@@ -47,19 +73,19 @@ export const createServer = (config: any): any => {
|
||||
});
|
||||
|
||||
// Register static file serving with caching
|
||||
server.app.register(fastifyStatic, {
|
||||
app.register(fastifyStatic, {
|
||||
root: join(__dirname, "..", "dist"),
|
||||
prefix: "/ui/",
|
||||
maxAge: "1h",
|
||||
});
|
||||
|
||||
// Redirect /ui to /ui/ for proper static file serving
|
||||
server.app.get("/ui", async (_: any, reply: any) => {
|
||||
app.get("/ui", async (_: any, reply: any) => {
|
||||
return reply.redirect("/ui/");
|
||||
});
|
||||
|
||||
// 获取日志文件列表端点
|
||||
server.app.get("/api/logs/files", async (req: any, reply: any) => {
|
||||
app.get("/api/logs/files", async (req: any, reply: any) => {
|
||||
try {
|
||||
const logDir = join(homedir(), ".claude-code-router", "logs");
|
||||
const logFiles: Array<{ name: string; path: string; size: number; lastModified: string }> = [];
|
||||
@@ -93,7 +119,7 @@ export const createServer = (config: any): any => {
|
||||
});
|
||||
|
||||
// 获取日志内容端点
|
||||
server.app.get("/api/logs", async (req: any, reply: any) => {
|
||||
app.get("/api/logs", async (req: any, reply: any) => {
|
||||
try {
|
||||
const filePath = (req.query as any).file as string;
|
||||
let logFilePath: string;
|
||||
@@ -121,7 +147,7 @@ export const createServer = (config: any): any => {
|
||||
});
|
||||
|
||||
// 清除日志内容端点
|
||||
server.app.delete("/api/logs", async (req: any, reply: any) => {
|
||||
app.delete("/api/logs", async (req: any, reply: any) => {
|
||||
try {
|
||||
const filePath = (req.query as any).file as string;
|
||||
let logFilePath: string;
|
||||
@@ -145,5 +171,252 @@ export const createServer = (config: any): any => {
|
||||
}
|
||||
});
|
||||
|
||||
// ========== Preset 相关 API ==========
|
||||
|
||||
// 获取预设列表
|
||||
app.get("/api/presets", async (req: any, reply: any) => {
|
||||
try {
|
||||
const presetsDir = join(HOME_DIR, "presets");
|
||||
|
||||
if (!existsSync(presetsDir)) {
|
||||
return { presets: [] };
|
||||
}
|
||||
|
||||
const entries = readdirSync(presetsDir, { withFileTypes: true });
|
||||
const presetDirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name);
|
||||
|
||||
const presets: Array<PresetMetadata & { installed: boolean; id: string }> = [];
|
||||
|
||||
for (const dirName of presetDirs) {
|
||||
const presetDir = join(presetsDir, dirName);
|
||||
try {
|
||||
const manifestPath = join(presetDir, "manifest.json");
|
||||
const content = readFileSync(manifestPath, 'utf-8');
|
||||
const manifest = JSON.parse(content);
|
||||
|
||||
// 提取 metadata 字段
|
||||
const { Providers, Router, PORT, HOST, API_TIMEOUT_MS, PROXY_URL, LOG, LOG_LEVEL, StatusLine, NON_INTERACTIVE_MODE, requiredInputs, ...metadata } = manifest;
|
||||
|
||||
presets.push({
|
||||
id: dirName, // 目录名作为唯一标识
|
||||
name: metadata.name || dirName,
|
||||
version: metadata.version || '1.0.0',
|
||||
description: metadata.description,
|
||||
author: metadata.author,
|
||||
homepage: metadata.homepage,
|
||||
repository: metadata.repository,
|
||||
license: metadata.license,
|
||||
keywords: metadata.keywords,
|
||||
ccrVersion: metadata.ccrVersion,
|
||||
source: metadata.source,
|
||||
sourceType: metadata.sourceType,
|
||||
checksum: metadata.checksum,
|
||||
installed: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to read preset ${dirName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return { presets };
|
||||
} catch (error) {
|
||||
console.error("Failed to get presets:", error);
|
||||
reply.status(500).send({ error: "Failed to get presets" });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取预设详情
|
||||
app.get("/api/presets/:name", async (req: any, reply: any) => {
|
||||
try {
|
||||
const { name } = req.params;
|
||||
const presetDir = getPresetDir(name);
|
||||
|
||||
if (!existsSync(presetDir)) {
|
||||
reply.status(404).send({ error: "Preset not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const manifest = await readManifestFromDir(presetDir);
|
||||
const preset = manifestToPresetFile(manifest);
|
||||
|
||||
return preset;
|
||||
} catch (error: any) {
|
||||
console.error("Failed to get preset:", error);
|
||||
reply.status(500).send({ error: error.message || "Failed to get preset" });
|
||||
}
|
||||
});
|
||||
|
||||
// 上传并安装预设(支持文件上传)
|
||||
app.post("/api/presets/install", async (req: any, reply: any) => {
|
||||
try {
|
||||
const { source, name, url } = req.body;
|
||||
|
||||
// 如果提供了 URL,从 URL 下载
|
||||
if (url) {
|
||||
const tempFile = await downloadPresetToTemp(url);
|
||||
const preset = await loadPresetFromZip(tempFile);
|
||||
|
||||
// 确定预设名称
|
||||
const presetName = name || preset.metadata?.name || `preset-${Date.now()}`;
|
||||
|
||||
// 检查是否已安装
|
||||
if (await isPresetInstalled(presetName)) {
|
||||
reply.status(409).send({ error: "Preset already installed" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 解压到目标目录
|
||||
const targetDir = getPresetDir(presetName);
|
||||
await extractPreset(tempFile, targetDir);
|
||||
|
||||
// 清理临时文件
|
||||
unlinkSync(tempFile);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
presetName,
|
||||
preset: {
|
||||
...preset.metadata,
|
||||
installed: true,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 如果没有 URL,需要处理文件上传(使用 multipart/form-data)
|
||||
// 这部分需要在客户端使用 FormData 上传
|
||||
reply.status(400).send({ error: "Please provide a URL or upload a file" });
|
||||
} catch (error: any) {
|
||||
console.error("Failed to install preset:", error);
|
||||
reply.status(500).send({ error: error.message || "Failed to install preset" });
|
||||
}
|
||||
});
|
||||
|
||||
// 上传预设文件(multipart/form-data)
|
||||
app.post("/api/presets/upload", async (req: any, reply: any) => {
|
||||
try {
|
||||
const data = await req.file();
|
||||
if (!data) {
|
||||
reply.status(400).send({ error: "No file uploaded" });
|
||||
return;
|
||||
}
|
||||
|
||||
const tempDir = getTempDir();
|
||||
mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
const tempFile = join(tempDir, `preset-${Date.now()}.ccrsets`);
|
||||
|
||||
// 保存上传的文件到临时位置
|
||||
const buffer = await data.toBuffer();
|
||||
writeFileSync(tempFile, buffer);
|
||||
|
||||
// 加载预设
|
||||
const preset = await loadPresetFromZip(tempFile);
|
||||
|
||||
// 确定预设名称
|
||||
const presetName = data.fields.name?.value || preset.metadata?.name || `preset-${Date.now()}`;
|
||||
|
||||
// 检查是否已安装
|
||||
if (await isPresetInstalled(presetName)) {
|
||||
unlinkSync(tempFile);
|
||||
reply.status(409).send({ error: "Preset already installed" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 解压到目标目录
|
||||
const targetDir = getPresetDir(presetName);
|
||||
await extractPreset(tempFile, targetDir);
|
||||
|
||||
// 清理临时文件
|
||||
unlinkSync(tempFile);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
presetName,
|
||||
preset: {
|
||||
...preset.metadata,
|
||||
installed: true,
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("Failed to upload preset:", error);
|
||||
reply.status(500).send({ error: error.message || "Failed to upload preset" });
|
||||
}
|
||||
});
|
||||
|
||||
// 应用预设(配置敏感信息)
|
||||
app.post("/api/presets/:name/apply", async (req: any, reply: any) => {
|
||||
try {
|
||||
const { name } = req.params;
|
||||
const { secrets } = req.body;
|
||||
|
||||
const presetDir = getPresetDir(name);
|
||||
|
||||
if (!existsSync(presetDir)) {
|
||||
reply.status(404).send({ error: "Preset not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取现有 manifest
|
||||
const manifest = await readManifestFromDir(presetDir);
|
||||
|
||||
// 将 secrets 信息应用到 manifest 中
|
||||
if (secrets) {
|
||||
for (const [fieldPath, value] of Object.entries(secrets)) {
|
||||
const keys = fieldPath.split(/[.\[\]]+/).filter(k => k !== '');
|
||||
let current = manifest as any;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!current[key]) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
current[keys[keys.length - 1]] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存更新后的 manifest
|
||||
await saveManifest(name, manifest);
|
||||
|
||||
return { success: true, message: "Preset applied successfully" };
|
||||
} catch (error: any) {
|
||||
console.error("Failed to apply preset:", error);
|
||||
reply.status(500).send({ error: error.message || "Failed to apply preset" });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除预设
|
||||
app.delete("/api/presets/:name", async (req: any, reply: any) => {
|
||||
try {
|
||||
const { name } = req.params;
|
||||
const presetDir = getPresetDir(name);
|
||||
|
||||
if (!existsSync(presetDir)) {
|
||||
reply.status(404).send({ error: "Preset not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 递归删除整个目录
|
||||
rmSync(presetDir, { recursive: true, force: true });
|
||||
|
||||
return { success: true, message: "Preset deleted successfully" };
|
||||
} catch (error: any) {
|
||||
console.error("Failed to delete preset:", error);
|
||||
reply.status(500).send({ error: error.message || "Failed to delete preset" });
|
||||
}
|
||||
});
|
||||
|
||||
// 辅助函数:从 ZIP 加载预设
|
||||
async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {
|
||||
const AdmZip = (await import('adm-zip')).default;
|
||||
const zip = new AdmZip(zipFile);
|
||||
const entry = zip.getEntry('manifest.json');
|
||||
if (!entry) {
|
||||
throw new Error('Invalid preset file: manifest.json not found');
|
||||
}
|
||||
const manifest = JSON.parse(entry.getData().toString('utf-8')) as ManifestFile;
|
||||
return manifestToPresetFile(manifest);
|
||||
}
|
||||
|
||||
return server;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,14 @@
|
||||
],
|
||||
"author": "musistudio",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^7.0.1",
|
||||
"json5": "^2.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/node": "^24.0.15",
|
||||
"esbuild": "^0.25.1",
|
||||
"typescript": "^5.8.2"
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
export * from "./constants";
|
||||
|
||||
// Export preset-related functionality
|
||||
export * from './preset/types';
|
||||
export * from './preset/sensitiveFields';
|
||||
export * from './preset/merge';
|
||||
export * from './preset/install';
|
||||
export * from './preset/export';
|
||||
export * from './preset/readPreset';
|
||||
|
||||
134
packages/shared/src/preset/export.ts
Normal file
134
packages/shared/src/preset/export.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 预设导出核心功能
|
||||
* 注意:这个模块不包含 CLI 交互逻辑,交互逻辑由调用者提供
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as fsSync from 'fs';
|
||||
import * as path from 'path';
|
||||
import archiver from 'archiver';
|
||||
import { sanitizeConfig } from './sensitiveFields';
|
||||
import { PresetFile, PresetMetadata, ManifestFile } from './types';
|
||||
import { HOME_DIR } from '../constants';
|
||||
|
||||
/**
|
||||
* 导出选项
|
||||
*/
|
||||
export interface ExportOptions {
|
||||
output?: string;
|
||||
includeSensitive?: boolean;
|
||||
description?: string;
|
||||
author?: string;
|
||||
tags?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出结果
|
||||
*/
|
||||
export interface ExportResult {
|
||||
outputPath: string;
|
||||
sanitizedConfig: any;
|
||||
metadata: PresetMetadata;
|
||||
requiredInputs: any[];
|
||||
sanitizedCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 manifest 对象
|
||||
* @param presetName 预设名称
|
||||
* @param config 配置对象
|
||||
* @param sanitizedConfig 脱敏后的配置
|
||||
* @param options 导出选项
|
||||
*/
|
||||
export function createManifest(
|
||||
presetName: string,
|
||||
config: any,
|
||||
sanitizedConfig: any,
|
||||
options: ExportOptions,
|
||||
requiredInputs: any[] = []
|
||||
): ManifestFile {
|
||||
const metadata: PresetMetadata = {
|
||||
name: presetName,
|
||||
version: '1.0.0',
|
||||
description: options.description,
|
||||
author: options.author,
|
||||
keywords: options.tags ? options.tags.split(',').map(t => t.trim()) : undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
...sanitizedConfig,
|
||||
requiredInputs: options.includeSensitive ? undefined : requiredInputs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出预设配置
|
||||
* @param presetName 预设名称
|
||||
* @param config 当前配置
|
||||
* @param options 导出选项
|
||||
* @returns 导出结果
|
||||
*/
|
||||
export async function exportPreset(
|
||||
presetName: string,
|
||||
config: any,
|
||||
options: ExportOptions = {}
|
||||
): Promise<ExportResult> {
|
||||
// 1. 收集元数据
|
||||
const metadata: PresetMetadata = {
|
||||
name: presetName,
|
||||
version: '1.0.0',
|
||||
description: options.description,
|
||||
author: options.author,
|
||||
keywords: options.tags ? options.tags.split(',').map(t => t.trim()) : undefined,
|
||||
};
|
||||
|
||||
// 2. 脱敏配置
|
||||
const { sanitizedConfig, requiredInputs, sanitizedCount } = await sanitizeConfig(config);
|
||||
|
||||
// 3. 生成manifest.json(扁平化结构)
|
||||
const manifest: ManifestFile = {
|
||||
...metadata,
|
||||
...sanitizedConfig,
|
||||
requiredInputs: options.includeSensitive ? undefined : requiredInputs,
|
||||
};
|
||||
|
||||
// 4. 确定输出路径
|
||||
const presetsDir = path.join(HOME_DIR, 'presets');
|
||||
|
||||
// 确保预设目录存在
|
||||
await fs.mkdir(presetsDir, { recursive: true });
|
||||
|
||||
const outputPath = options.output || path.join(presetsDir, `${presetName}.ccrsets`);
|
||||
|
||||
// 5. 创建压缩包
|
||||
const output = fsSync.createWriteStream(outputPath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 9 } // 最高压缩级别
|
||||
});
|
||||
|
||||
return new Promise<ExportResult>((resolve, reject) => {
|
||||
output.on('close', () => {
|
||||
resolve({
|
||||
outputPath,
|
||||
sanitizedConfig,
|
||||
metadata,
|
||||
requiredInputs,
|
||||
sanitizedCount,
|
||||
});
|
||||
});
|
||||
|
||||
archive.on('error', (err: Error) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// 连接输出流
|
||||
archive.pipe(output);
|
||||
|
||||
// 添加manifest.json到压缩包
|
||||
archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' });
|
||||
|
||||
// 完成压缩
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
239
packages/shared/src/preset/install.ts
Normal file
239
packages/shared/src/preset/install.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 预设安装核心功能
|
||||
* 注意:这个模块不包含 CLI 交互逻辑,交互逻辑由调用者提供
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
* 获取预设目录的完整路径
|
||||
* @param presetName 预设名称
|
||||
*/
|
||||
export function getPresetDir(presetName: string): string {
|
||||
return path.join(HOME_DIR, 'presets', presetName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取临时目录路径
|
||||
*/
|
||||
export function getTempDir(): string {
|
||||
return path.join(HOME_DIR, 'temp');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压预设文件到目标目录
|
||||
* @param sourceZip 源ZIP文件路径
|
||||
* @param targetDir 目标目录
|
||||
*/
|
||||
export async function extractPreset(sourceZip: string, targetDir: string): Promise<void> {
|
||||
// 检查目标目录是否已存在
|
||||
try {
|
||||
await fs.access(targetDir);
|
||||
throw new Error(`Preset directory already exists: ${path.basename(targetDir)}`);
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
// ENOENT 表示目录不存在,可以继续
|
||||
}
|
||||
|
||||
// 创建目标目录
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
|
||||
// 解压文件
|
||||
const zip = new AdmZip(sourceZip);
|
||||
zip.extractAllTo(targetDir, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从解压目录读取manifest
|
||||
* @param presetDir 预设目录路径
|
||||
*/
|
||||
export async function readManifestFromDir(presetDir: string): Promise<ManifestFile> {
|
||||
const manifestPath = path.join(presetDir, 'manifest.json');
|
||||
const content = await fs.readFile(manifestPath, 'utf-8');
|
||||
return JSON5.parse(content) as ManifestFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将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;
|
||||
return {
|
||||
metadata,
|
||||
config: { Providers, Router, PORT, HOST, API_TIMEOUT_MS, PROXY_URL, LOG, LOG_LEVEL, StatusLine, NON_INTERACTIVE_MODE },
|
||||
requiredInputs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载预设文件到临时位置
|
||||
* @param url 下载URL
|
||||
* @returns 临时文件路径
|
||||
*/
|
||||
export async function downloadPresetToTemp(url: string): Promise<string> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download preset: ${response.statusText}`);
|
||||
}
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
// 创建临时文件
|
||||
const tempDir = getTempDir();
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
|
||||
const tempFile = path.join(tempDir, `preset-${Date.now()}.ccrsets`);
|
||||
await fs.writeFile(tempFile, Buffer.from(buffer));
|
||||
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地ZIP文件加载预设
|
||||
* @param zipFile ZIP文件路径
|
||||
* @returns PresetFile
|
||||
*/
|
||||
export async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {
|
||||
const zip = new AdmZip(zipFile);
|
||||
const entry = zip.getEntry('manifest.json');
|
||||
if (!entry) {
|
||||
throw new Error('Invalid preset file: manifest.json not found');
|
||||
}
|
||||
const manifest = JSON5.parse(entry.getData().toString('utf-8')) as ManifestFile;
|
||||
return manifestToPresetFile(manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载预设文件
|
||||
* @param source 预设来源(文件路径、URL 或预设名称)
|
||||
*/
|
||||
export async function loadPreset(source: string): Promise<PresetFile> {
|
||||
// 判断是否是 URL
|
||||
if (source.startsWith('http://') || source.startsWith('https://')) {
|
||||
const tempFile = await downloadPresetToTemp(source);
|
||||
const preset = await loadPresetFromZip(tempFile);
|
||||
// 删除临时文件
|
||||
await fs.unlink(tempFile).catch(() => {});
|
||||
return preset;
|
||||
}
|
||||
|
||||
// 判断是否是绝对路径或相对路径(包含 / 或 \)
|
||||
if (source.includes('/') || source.includes('\\')) {
|
||||
// 文件路径
|
||||
return await loadPresetFromZip(source);
|
||||
}
|
||||
|
||||
// 否则作为预设名称处理(从解压目录读取)
|
||||
const presetDir = getPresetDir(source);
|
||||
const manifest = await readManifestFromDir(presetDir);
|
||||
return manifestToPresetFile(manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证预设文件
|
||||
*/
|
||||
export async function validatePreset(preset: PresetFile): Promise<{
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}> {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// 验证元数据
|
||||
if (!preset.metadata) {
|
||||
warnings.push('Missing metadata section');
|
||||
} else {
|
||||
if (!preset.metadata.name) {
|
||||
errors.push('Missing preset name in metadata');
|
||||
}
|
||||
if (!preset.metadata.version) {
|
||||
warnings.push('Missing version in metadata');
|
||||
}
|
||||
}
|
||||
|
||||
// 验证配置部分
|
||||
if (!preset.config) {
|
||||
errors.push('Missing config section');
|
||||
}
|
||||
|
||||
// 验证 Providers
|
||||
if (preset.config.Providers) {
|
||||
for (const provider of preset.config.Providers) {
|
||||
if (!provider.name) {
|
||||
errors.push('Provider missing name field');
|
||||
}
|
||||
if (!provider.api_base_url) {
|
||||
errors.push(`Provider "${provider.name}" missing api_base_url`);
|
||||
}
|
||||
if (!provider.models || provider.models.length === 0) {
|
||||
warnings.push(`Provider "${provider.name}" has no models`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 manifest 到预设目录
|
||||
* @param presetName 预设名称
|
||||
* @param manifest manifest 对象
|
||||
*/
|
||||
export async function saveManifest(presetName: string, manifest: ManifestFile): Promise<void> {
|
||||
const presetDir = getPresetDir(presetName);
|
||||
const manifestPath = path.join(presetDir, 'manifest.json');
|
||||
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找预设文件
|
||||
* @param source 预设来源
|
||||
* @returns 文件路径或 null
|
||||
*/
|
||||
export async function findPresetFile(source: string): Promise<string | null> {
|
||||
// 当前目录文件
|
||||
const currentDirFile = path.join(process.cwd(), `${source}.ccrsets`);
|
||||
|
||||
// presets 目录文件
|
||||
const presetsDirFile = path.join(HOME_DIR, 'presets', `${source}.ccrsets`);
|
||||
|
||||
// 检查当前目录
|
||||
try {
|
||||
await fs.access(currentDirFile);
|
||||
return currentDirFile;
|
||||
} catch {
|
||||
// 检查presets目录
|
||||
try {
|
||||
await fs.access(presetsDirFile);
|
||||
return presetsDirFile;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查预设是否已安装
|
||||
* @param presetName 预设名称
|
||||
*/
|
||||
export async function isPresetInstalled(presetName: string): Promise<boolean> {
|
||||
const presetDir = getPresetDir(presetName);
|
||||
try {
|
||||
await fs.access(presetDir);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
280
packages/shared/src/preset/merge.ts
Normal file
280
packages/shared/src/preset/merge.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* 配置合并策略
|
||||
*/
|
||||
|
||||
import { MergeStrategy, ProviderConfig, RouterConfig, TransformerConfig, ProviderConflictAction } from './types';
|
||||
|
||||
/**
|
||||
* 合并 Provider 配置
|
||||
*/
|
||||
async function mergeProviders(
|
||||
existing: ProviderConfig[],
|
||||
incoming: ProviderConfig[],
|
||||
strategy: MergeStrategy,
|
||||
onProviderConflict?: (providerName: string) => Promise<ProviderConflictAction>
|
||||
): Promise<ProviderConfig[]> {
|
||||
const result = [...existing];
|
||||
const existingNames = new Set(existing.map(p => 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;
|
||||
}
|
||||
} else {
|
||||
// 新 Provider,直接添加
|
||||
result.push(provider);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 Router 配置
|
||||
*/
|
||||
async function mergeRouter(
|
||||
existing: RouterConfig,
|
||||
incoming: RouterConfig,
|
||||
strategy: MergeStrategy,
|
||||
onRouterConflict?: (key: string, existingValue: any, newValue: any) => Promise<boolean>
|
||||
): Promise<RouterConfig> {
|
||||
const result = { ...existing };
|
||||
|
||||
for (const [key, value] of Object.entries(incoming)) {
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingValue = result[key];
|
||||
|
||||
if (existingValue === undefined || existingValue === null) {
|
||||
// 现有配置中没有这个路由规则,直接添加
|
||||
result[key] = value;
|
||||
} else {
|
||||
// 存在冲突
|
||||
if (strategy === MergeStrategy.ASK && onRouterConflict) {
|
||||
const shouldOverwrite = await onRouterConflict(key, existingValue, value);
|
||||
if (shouldOverwrite) {
|
||||
result[key] = value;
|
||||
}
|
||||
} else if (strategy === MergeStrategy.OVERWRITE) {
|
||||
result[key] = value;
|
||||
} else if (strategy === MergeStrategy.MERGE) {
|
||||
// 对于 Router,merge 策略等同于 skip,保留现有值
|
||||
// 或者可以询问用户
|
||||
}
|
||||
// skip 策略:保留现有值,不做任何操作
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 Transformer 配置
|
||||
*/
|
||||
async function mergeTransformers(
|
||||
existing: TransformerConfig[],
|
||||
incoming: TransformerConfig[],
|
||||
strategy: MergeStrategy,
|
||||
onTransformerConflict?: (transformerPath: string) => Promise<'keep' | 'overwrite' | 'skip'>
|
||||
): Promise<TransformerConfig[]> {
|
||||
if (!existing || existing.length === 0) {
|
||||
return incoming;
|
||||
}
|
||||
|
||||
if (!incoming || incoming.length === 0) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Transformer 合并逻辑:按路径匹配
|
||||
const result = [...existing];
|
||||
const existingPaths = new Set(existing.map(t => t.path));
|
||||
|
||||
for (const transformer of incoming) {
|
||||
if (!transformer.path) {
|
||||
// 没有 path 的 transformer,直接添加
|
||||
result.push(transformer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingPaths.has(transformer.path)) {
|
||||
// 已存在相同 path 的 transformer
|
||||
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 都不做操作
|
||||
} else if (strategy === MergeStrategy.OVERWRITE) {
|
||||
const index = result.findIndex(t => t.path === transformer.path);
|
||||
result[index] = transformer;
|
||||
}
|
||||
// merge 和 skip 策略:保留现有
|
||||
} else {
|
||||
// 新 transformer,直接添加
|
||||
result.push(transformer);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并其他顶级配置
|
||||
*/
|
||||
async function mergeOtherConfig(
|
||||
existing: any,
|
||||
incoming: any,
|
||||
strategy: MergeStrategy,
|
||||
onConfigConflict?: (key: string) => Promise<boolean>,
|
||||
excludeKeys: string[] = ['Providers', 'Router', 'transformers']
|
||||
): Promise<any> {
|
||||
const result = { ...existing };
|
||||
|
||||
for (const [key, value] of Object.entries(incoming)) {
|
||||
if (excludeKeys.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingValue = result[key];
|
||||
|
||||
if (existingValue === undefined || existingValue === null) {
|
||||
// 现有配置中没有这个字段,直接添加
|
||||
result[key] = value;
|
||||
} else {
|
||||
// 存在冲突
|
||||
if (strategy === MergeStrategy.ASK && onConfigConflict) {
|
||||
const shouldOverwrite = await onConfigConflict(key);
|
||||
if (shouldOverwrite) {
|
||||
result[key] = value;
|
||||
}
|
||||
} else if (strategy === MergeStrategy.OVERWRITE) {
|
||||
result[key] = value;
|
||||
}
|
||||
// merge 和 skip 策略:保留现有值
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并交互回调接口
|
||||
*/
|
||||
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>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主配置合并函数
|
||||
* @param baseConfig 基础配置(现有配置)
|
||||
* @param presetConfig 预设配置
|
||||
* @param strategy 合并策略
|
||||
* @param callbacks 交互式回调函数
|
||||
* @returns 合并后的配置
|
||||
*/
|
||||
export async function mergeConfig(
|
||||
baseConfig: any,
|
||||
presetConfig: any,
|
||||
strategy: MergeStrategy = MergeStrategy.ASK,
|
||||
callbacks?: MergeCallbacks
|
||||
): Promise<any> {
|
||||
const result = { ...baseConfig };
|
||||
|
||||
// 合并 Providers
|
||||
if (presetConfig.Providers) {
|
||||
result.Providers = await mergeProviders(
|
||||
result.Providers || [],
|
||||
presetConfig.Providers,
|
||||
strategy,
|
||||
callbacks?.onProviderConflict
|
||||
);
|
||||
}
|
||||
|
||||
// 合并 Router
|
||||
if (presetConfig.Router) {
|
||||
result.Router = await mergeRouter(
|
||||
result.Router || {},
|
||||
presetConfig.Router,
|
||||
strategy,
|
||||
callbacks?.onRouterConflict
|
||||
);
|
||||
}
|
||||
|
||||
// 合并 transformers
|
||||
if (presetConfig.transformers) {
|
||||
result.transformers = await mergeTransformers(
|
||||
result.transformers || [],
|
||||
presetConfig.transformers,
|
||||
strategy,
|
||||
callbacks?.onTransformerConflict
|
||||
);
|
||||
}
|
||||
|
||||
// 合并其他配置
|
||||
const otherConfig = await mergeOtherConfig(
|
||||
result,
|
||||
presetConfig,
|
||||
strategy,
|
||||
callbacks?.onConfigConflict
|
||||
);
|
||||
|
||||
return otherConfig;
|
||||
}
|
||||
30
packages/shared/src/preset/readPreset.ts
Normal file
30
packages/shared/src/preset/readPreset.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 读取预设配置文件
|
||||
* 用于 CLI 快速读取预设配置
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import JSON5 from 'json5';
|
||||
import { HOME_DIR } from '../constants';
|
||||
|
||||
/**
|
||||
* 读取 preset 配置文件
|
||||
* @param name preset 名称
|
||||
* @returns preset 配置对象,如果文件不存在则返回 null
|
||||
*/
|
||||
export async function readPresetFile(name: string): Promise<any | null> {
|
||||
try {
|
||||
const presetDir = path.join(HOME_DIR, 'presets', name);
|
||||
const manifestPath = path.join(presetDir, 'manifest.json');
|
||||
const manifest = JSON5.parse(await fs.readFile(manifestPath, 'utf-8'));
|
||||
// manifest已经是扁平化结构,直接返回
|
||||
return manifest;
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
console.error(`Failed to read preset file: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
249
packages/shared/src/preset/sensitiveFields.ts
Normal file
249
packages/shared/src/preset/sensitiveFields.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 敏感字段识别和脱敏功能
|
||||
*/
|
||||
|
||||
import { RequiredInput, SanitizeResult } from './types';
|
||||
|
||||
// 敏感字段模式列表
|
||||
const SENSITIVE_PATTERNS = [
|
||||
'api_key', 'apikey', 'apiKey', 'APIKEY',
|
||||
'api_secret', 'apisecret', 'apiSecret',
|
||||
'secret', 'SECRET',
|
||||
'token', 'TOKEN', 'auth_token',
|
||||
'password', 'PASSWORD', 'passwd',
|
||||
'private_key', 'privateKey',
|
||||
'access_key', 'accessKey',
|
||||
];
|
||||
|
||||
// 环境变量占位符正则
|
||||
const ENV_VAR_REGEX = /^\$\{?[A-Z_][A-Z0-9_]*\}?$/;
|
||||
|
||||
/**
|
||||
* 检查字段名是否为敏感字段
|
||||
*/
|
||||
function isSensitiveField(fieldName: string): boolean {
|
||||
const lowerFieldName = fieldName.toLowerCase();
|
||||
return SENSITIVE_PATTERNS.some(pattern =>
|
||||
lowerFieldName.includes(pattern.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成环境变量名称
|
||||
* @param fieldType 字段类型 (provider, transformer, global)
|
||||
* @param entityName 实体名称 (如 provider name)
|
||||
* @param fieldName 字段名称
|
||||
*/
|
||||
export function generateEnvVarName(
|
||||
fieldType: 'provider' | 'transformer' | 'global',
|
||||
entityName: string,
|
||||
fieldName: string
|
||||
): string {
|
||||
// 生成大写的环境变量名
|
||||
// 例如: 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 === field) {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
return `${prefix}_${field}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否已经是环境变量占位符
|
||||
*/
|
||||
function isEnvPlaceholder(value: any): boolean {
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return ENV_VAR_REGEX.test(value.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从环境变量占位符中提取变量名
|
||||
* @param value 环境变量值(如 $VAR 或 ${VAR})
|
||||
*/
|
||||
function extractEnvVarName(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
|
||||
// 匹配 ${VAR_NAME} 格式
|
||||
const bracedMatch = trimmed.match(/^\$\{([A-Z_][A-Z0-9_]*)\}$/);
|
||||
if (bracedMatch) {
|
||||
return bracedMatch[1];
|
||||
}
|
||||
|
||||
// 匹配 $VAR_NAME 格式
|
||||
const unbracedMatch = trimmed.match(/^\$([A-Z_][A-Z0-9_]*)$/);
|
||||
if (unbracedMatch) {
|
||||
return unbracedMatch[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历对象,识别和脱敏敏感字段
|
||||
* @param config 配置对象
|
||||
* @param path 当前字段路径
|
||||
* @param requiredInputs 必需输入数组(累积)
|
||||
* @param sanitizedCount 脱敏字段计数
|
||||
*/
|
||||
function sanitizeObject(
|
||||
config: any,
|
||||
path: string = '',
|
||||
requiredInputs: RequiredInput[] = [],
|
||||
sanitizedCount: number = 0
|
||||
): { sanitized: any; requiredInputs: RequiredInput[]; count: number } {
|
||||
if (!config || typeof config !== 'object') {
|
||||
return { sanitized: config, requiredInputs, count: sanitizedCount };
|
||||
}
|
||||
|
||||
if (Array.isArray(config)) {
|
||||
const sanitizedArray: any[] = [];
|
||||
for (let i = 0; i < config.length; i++) {
|
||||
const result = sanitizeObject(
|
||||
config[i],
|
||||
path ? `${path}[${i}]` : `[${i}]`,
|
||||
requiredInputs,
|
||||
sanitizedCount
|
||||
);
|
||||
sanitizedArray.push(result.sanitized);
|
||||
requiredInputs = result.requiredInputs;
|
||||
sanitizedCount = result.count;
|
||||
}
|
||||
return { sanitized: sanitizedArray, requiredInputs, count: sanitizedCount };
|
||||
}
|
||||
|
||||
const sanitizedObj: any = {};
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
const currentPath = path ? `${path}.${key}` : key;
|
||||
|
||||
// 检查是否是敏感字段
|
||||
if (isSensitiveField(key) && typeof value === 'string') {
|
||||
// 如果值已经是环境变量,保持不变
|
||||
if (isEnvPlaceholder(value)) {
|
||||
sanitizedObj[key] = value;
|
||||
// 仍然需要记录为必需输入,但使用已有环境变量
|
||||
const envVarName = extractEnvVarName(value);
|
||||
if (envVarName && !requiredInputs.some(input => input.field === currentPath)) {
|
||||
requiredInputs.push({
|
||||
field: currentPath,
|
||||
prompt: `Enter ${key}`,
|
||||
placeholder: envVarName,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 脱敏:替换为环境变量占位符
|
||||
// 尝试从路径中推断实体名称
|
||||
let entityName = 'CONFIG';
|
||||
const pathParts = currentPath.split('.');
|
||||
|
||||
// 如果路径包含 Providers 或 transformers,尝试提取实体名称
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
if (pathParts[i] === 'Providers' || pathParts[i] === 'transformers') {
|
||||
// 查找 name 字段
|
||||
if (i + 1 < pathParts.length && pathParts[i + 1].match(/^\d+$/)) {
|
||||
// 这是数组索引,查找同级的 name 字段
|
||||
const parentPath = pathParts.slice(0, i + 2).join('.');
|
||||
// 在当前上下文中查找 name
|
||||
const context = config;
|
||||
if (context.name) {
|
||||
entityName = context.name;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const envVarName = generateEnvVarName('global', entityName, key);
|
||||
sanitizedObj[key] = `\${${envVarName}}`;
|
||||
|
||||
// 记录为必需输入
|
||||
requiredInputs.push({
|
||||
field: currentPath,
|
||||
prompt: `Enter ${key}`,
|
||||
placeholder: envVarName,
|
||||
});
|
||||
|
||||
sanitizedCount++;
|
||||
}
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// 递归处理嵌套对象
|
||||
const result = sanitizeObject(value, currentPath, requiredInputs, sanitizedCount);
|
||||
sanitizedObj[key] = result.sanitized;
|
||||
requiredInputs = result.requiredInputs;
|
||||
sanitizedCount = result.count;
|
||||
} else {
|
||||
// 保留原始值
|
||||
sanitizedObj[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return { sanitized: sanitizedObj, requiredInputs, count: sanitizedCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏配置对象
|
||||
* @param config 原始配置
|
||||
* @returns 脱敏结果
|
||||
*/
|
||||
export async function sanitizeConfig(config: any): Promise<SanitizeResult> {
|
||||
// 深拷贝配置,避免修改原始对象
|
||||
const configCopy = JSON.parse(JSON.stringify(config));
|
||||
|
||||
const result = sanitizeObject(configCopy);
|
||||
|
||||
return {
|
||||
sanitizedConfig: result.sanitized,
|
||||
requiredInputs: result.requiredInputs,
|
||||
sanitizedCount: result.count,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充敏感信息到配置中
|
||||
* @param config 预设配置(包含环境变量占位符)
|
||||
* @param inputs 用户输入的敏感信息
|
||||
* @returns 填充后的配置
|
||||
*/
|
||||
export function fillSensitiveInputs(config: any, inputs: Record<string, string>): any {
|
||||
const configCopy = JSON.parse(JSON.stringify(config));
|
||||
|
||||
function fillObject(obj: any, path: string = ''): any {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item, index) =>
|
||||
fillObject(item, path ? `${path}[${index}]` : `[${index}]`)
|
||||
);
|
||||
}
|
||||
|
||||
const result: any = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const currentPath = path ? `${path}.${key}` : key;
|
||||
|
||||
if (typeof value === 'string' && isEnvPlaceholder(value)) {
|
||||
// 查找是否有用户输入
|
||||
const input = inputs[currentPath];
|
||||
if (input) {
|
||||
result[key] = input;
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
result[key] = fillObject(value, currentPath);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return fillObject(configCopy);
|
||||
}
|
||||
138
packages/shared/src/preset/types.ts
Normal file
138
packages/shared/src/preset/types.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 预设功能的类型定义
|
||||
*/
|
||||
|
||||
// 敏感字段输入要求
|
||||
export interface RequiredInput {
|
||||
field: string; // 字段路径 (如 "Providers[0].api_key")
|
||||
prompt?: string; // 提示信息
|
||||
placeholder?: string; // 占位符环境变量名
|
||||
defaultValue?: string; // 默认值
|
||||
validator?: RegExp | string; // 验证规则
|
||||
}
|
||||
|
||||
// Provider 配置
|
||||
export interface ProviderConfig {
|
||||
name: string;
|
||||
api_base_url: string;
|
||||
api_key: string;
|
||||
models: string[];
|
||||
transformer?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Router 配置
|
||||
export interface RouterConfig {
|
||||
default?: string;
|
||||
background?: string;
|
||||
think?: string;
|
||||
longContext?: string;
|
||||
longContextThreshold?: number;
|
||||
webSearch?: string;
|
||||
image?: string;
|
||||
[key: string]: string | number | undefined;
|
||||
}
|
||||
|
||||
// Transformer 配置
|
||||
export interface TransformerConfig {
|
||||
path?: string;
|
||||
use: Array<string | [string, any]>;
|
||||
options?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 预设元数据(扁平化结构,用于manifest.json)
|
||||
export interface PresetMetadata {
|
||||
name: string; // 预设名称
|
||||
version: string; // 版本号 (semver)
|
||||
description?: string; // 描述
|
||||
author?: string; // 作者
|
||||
homepage?: string; // 主页
|
||||
repository?: string; // 源码仓库
|
||||
license?: string; // 许可证
|
||||
keywords?: string[]; // 关键词(原tags)
|
||||
ccrVersion?: string; // 兼容的 CCR 版本
|
||||
source?: string; // 预设来源 URL
|
||||
sourceType?: 'local' | 'gist' | 'registry';
|
||||
checksum?: string; // 预设内容校验和
|
||||
}
|
||||
|
||||
// 预设配置部分
|
||||
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;
|
||||
}
|
||||
|
||||
// 完整的预设文件格式
|
||||
export interface PresetFile {
|
||||
metadata?: PresetMetadata;
|
||||
config: PresetConfigSection;
|
||||
secrets?: {
|
||||
// 敏感信息存储,格式:字段路径 -> 值
|
||||
// 例如:{ "Providers[0].api_key": "sk-xxx", "APIKEY": "my-secret" }
|
||||
[fieldPath: string]: string;
|
||||
};
|
||||
requiredInputs?: RequiredInput[];
|
||||
}
|
||||
|
||||
// manifest.json 格式(压缩包内的文件)
|
||||
export interface ManifestFile extends PresetMetadata, PresetConfigSection {
|
||||
requiredInputs?: RequiredInput[];
|
||||
}
|
||||
|
||||
// 在线预设索引条目
|
||||
export interface PresetIndexEntry {
|
||||
id: string; // 唯一标识
|
||||
name: string; // 显示名称
|
||||
description?: string; // 简短描述
|
||||
version: string; // 最新版本
|
||||
author?: string; // 作者
|
||||
downloads?: number; // 下载次数
|
||||
stars?: number; // 点赞数
|
||||
tags?: string[]; // 标签
|
||||
url: string; // 下载地址
|
||||
checksum?: string; // SHA256 校验和
|
||||
ccrVersion?: string; // 兼容版本
|
||||
}
|
||||
|
||||
// 在线预设仓库索引
|
||||
export interface PresetRegistry {
|
||||
version: string; // 索引格式版本
|
||||
lastUpdated: string; // 最后更新时间
|
||||
presets: PresetIndexEntry[];
|
||||
}
|
||||
|
||||
// 配置验证结果
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
// 合并策略枚举
|
||||
export enum MergeStrategy {
|
||||
ASK = 'ask', // 交互式询问
|
||||
OVERWRITE = 'overwrite', // 覆盖现有
|
||||
MERGE = 'merge', // 智能合并
|
||||
SKIP = 'skip', // 跳过冲突项
|
||||
}
|
||||
|
||||
// 脱敏结果
|
||||
export interface SanitizeResult {
|
||||
sanitizedConfig: any;
|
||||
requiredInputs: RequiredInput[];
|
||||
sanitizedCount: number;
|
||||
}
|
||||
|
||||
// Provider 冲突处理动作
|
||||
export type ProviderConflictAction = 'keep' | 'overwrite' | 'merge' | 'skip';
|
||||
@@ -32,6 +32,7 @@
|
||||
"react-dom": "^19.1.0",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-router-dom": "^7.7.0",
|
||||
"remixicon": "^4.7.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
|
||||
@@ -10,13 +10,14 @@ import { LogViewer } from "@/components/LogViewer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useConfig } from "@/components/ConfigProvider";
|
||||
import { api } from "@/lib/api";
|
||||
import { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp, FileText } from "lucide-react";
|
||||
import { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp, FileText, FileCog } from "lucide-react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Toast } from "@/components/ui/toast";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -270,19 +271,51 @@ function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-gray-50 font-sans">
|
||||
<TooltipProvider>
|
||||
<div className="h-screen bg-gray-50 font-sans">
|
||||
<header className="flex h-16 items-center justify-between border-b bg-white px-6">
|
||||
<h1 className="text-xl font-semibold text-gray-800">{t('app.title')}</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => setIsSettingsOpen(true)} className="transition-all-ease hover:scale-110">
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
|
||||
<FileJson className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => setIsLogViewerOpen(true)} className="transition-all-ease hover:scale-110">
|
||||
<FileText className="h-5 w-5" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" onClick={() => setIsSettingsOpen(true)} className="transition-all-ease hover:scale-110">
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('app.settings')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" onClick={() => setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
|
||||
<FileJson className="h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('app.json_editor')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" onClick={() => setIsLogViewerOpen(true)} className="transition-all-ease hover:scale-110">
|
||||
<FileText className="h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('app.log_viewer')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate('/presets')} className="transition-all-ease hover:scale-110">
|
||||
<FileCog className="h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('app.presets')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
|
||||
@@ -310,25 +343,32 @@ function App() {
|
||||
</Popover>
|
||||
{/* 更新版本按钮 - 仅当更新功能可用时显示 */}
|
||||
{isUpdateFeatureAvailable && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => checkForUpdates(true)}
|
||||
disabled={isCheckingUpdate}
|
||||
className="transition-all-ease hover:scale-110 relative"
|
||||
>
|
||||
<div className="relative">
|
||||
<CircleArrowUp className="h-5 w-5" />
|
||||
{isNewVersionAvailable && !isCheckingUpdate && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-white"></div>
|
||||
)}
|
||||
</div>
|
||||
{isCheckingUpdate && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => checkForUpdates(true)}
|
||||
disabled={isCheckingUpdate}
|
||||
className="transition-all-ease hover:scale-110 relative"
|
||||
>
|
||||
<div className="relative">
|
||||
<CircleArrowUp className="h-5 w-5" />
|
||||
{isNewVersionAvailable && !isCheckingUpdate && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-white"></div>
|
||||
)}
|
||||
</div>
|
||||
{isCheckingUpdate && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('app.check_updates')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button onClick={saveConfig} variant="outline" className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
@@ -412,6 +452,7 @@ function App() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
689
packages/ui/src/components/Presets.tsx
Normal file
689
packages/ui/src/components/Presets.tsx
Normal file
@@ -0,0 +1,689 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/lib/api";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Upload, Link, Trash2, Info, Download, CheckCircle2, AlertCircle, Loader2, ArrowLeft, Store, Search, Package } from "lucide-react";
|
||||
import { Toast } from "@/components/ui/toast";
|
||||
|
||||
interface PresetMetadata {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
homepage?: string;
|
||||
repository?: string;
|
||||
license?: string;
|
||||
keywords?: string[];
|
||||
ccrVersion?: string;
|
||||
source?: string;
|
||||
sourceType?: 'local' | 'gist' | 'registry';
|
||||
checksum?: string;
|
||||
installed: boolean;
|
||||
}
|
||||
|
||||
interface PresetDetail extends PresetMetadata {
|
||||
config?: any;
|
||||
requiredInputs?: Array<{ field: string; prompt?: string; placeholder?: string }>;
|
||||
}
|
||||
|
||||
interface MarketPreset {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
homepage?: string;
|
||||
repository?: string;
|
||||
license?: string;
|
||||
keywords?: string[];
|
||||
downloadUrl: string;
|
||||
downloads?: number;
|
||||
rating?: number;
|
||||
}
|
||||
|
||||
export function Presets() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [presets, setPresets] = useState<PresetMetadata[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [installDialogOpen, setInstallDialogOpen] = useState(false);
|
||||
const [detailDialogOpen, setDetailDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [marketDialogOpen, setMarketDialogOpen] = useState(false);
|
||||
const [selectedPreset, setSelectedPreset] = useState<PresetDetail | null>(null);
|
||||
const [presetToDelete, setPresetToDelete] = useState<string | null>(null);
|
||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
|
||||
const [installMethod, setInstallMethod] = useState<'file' | 'url'>('file');
|
||||
const [installUrl, setInstallUrl] = useState('');
|
||||
const [installFile, setInstallFile] = useState<File | null>(null);
|
||||
const [installName, setInstallName] = useState('');
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [secrets, setSecrets] = useState<Record<string, string>>({});
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
const [marketSearch, setMarketSearch] = useState('');
|
||||
const [marketPresets, setMarketPresets] = useState<MarketPreset[]>([]);
|
||||
const [marketLoading, setMarketLoading] = useState(false);
|
||||
const [installingFromMarket, setInstallingFromMarket] = useState<string | null>(null);
|
||||
|
||||
// 返回上一页
|
||||
const handleGoBack = () => {
|
||||
navigate('/dashboard');
|
||||
};
|
||||
|
||||
// 加载市场预设
|
||||
const loadMarketPresets = async () => {
|
||||
setMarketLoading(true);
|
||||
try {
|
||||
// TODO: 替换为实际的市场 API
|
||||
// const response = await api.getMarketPresets();
|
||||
// setMarketPresets(response.presets || []);
|
||||
|
||||
// 模拟数据
|
||||
const mockMarketPresets: MarketPreset[] = [
|
||||
{
|
||||
id: 'openai-compatible',
|
||||
name: 'OpenAI Compatible',
|
||||
version: '1.0.0',
|
||||
description: 'Full-featured OpenAI API compatible preset with support for GPT-4, GPT-3.5, and more.',
|
||||
author: 'CCR Community',
|
||||
homepage: 'https://github.com/example/openai-preset',
|
||||
repository: 'https://github.com/example/openai-preset',
|
||||
license: 'MIT',
|
||||
keywords: ['openai', 'gpt', 'chat'],
|
||||
downloadUrl: 'https://example.com/openai.ccrsets',
|
||||
downloads: 1234,
|
||||
rating: 4.8
|
||||
},
|
||||
{
|
||||
id: 'anthropic-optimized',
|
||||
name: 'Anthropic Optimized',
|
||||
version: '1.2.0',
|
||||
description: 'Optimized configuration for Claude and other Anthropic models with enhanced token management.',
|
||||
author: 'CCR Team',
|
||||
homepage: 'https://github.com/example/anthropic-preset',
|
||||
repository: 'https://github.com/example/anthropic-preset',
|
||||
license: 'Apache-2.0',
|
||||
keywords: ['anthropic', 'claude', 'ai'],
|
||||
downloadUrl: 'https://example.com/anthropic.ccrsets',
|
||||
downloads: 892,
|
||||
rating: 4.9
|
||||
},
|
||||
{
|
||||
id: 'multi-provider',
|
||||
name: 'Multi-Provider Router',
|
||||
version: '2.0.0',
|
||||
description: 'Intelligent routing across multiple providers based on cost, speed, and capability.',
|
||||
author: 'CCR Community',
|
||||
homepage: 'https://github.com/example/multi-provider-preset',
|
||||
repository: 'https://github.com/example/multi-provider-preset',
|
||||
license: 'MIT',
|
||||
keywords: ['router', 'multi-provider', 'optimization'],
|
||||
downloadUrl: 'https://example.com/multi-provider.ccrsets',
|
||||
downloads: 567,
|
||||
rating: 4.6
|
||||
},
|
||||
{
|
||||
id: 'development-tools',
|
||||
name: 'Development Tools',
|
||||
version: '1.1.0',
|
||||
description: 'Optimized for coding and development tasks with special focus on code generation and debugging.',
|
||||
author: 'DevTeam',
|
||||
homepage: 'https://github.com/example/dev-tools-preset',
|
||||
repository: 'https://github.com/example/dev-tools-preset',
|
||||
license: 'MIT',
|
||||
keywords: ['development', 'coding', 'programming'],
|
||||
downloadUrl: 'https://example.com/dev-tools.ccrsets',
|
||||
downloads: 445,
|
||||
rating: 4.7
|
||||
}
|
||||
];
|
||||
setMarketPresets(mockMarketPresets);
|
||||
} catch (error) {
|
||||
console.error('Failed to load market presets:', error);
|
||||
setToast({ message: t('presets.load_market_failed'), type: 'error' });
|
||||
} finally {
|
||||
setMarketLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 从市场安装预设
|
||||
const handleInstallFromMarket = async (preset: MarketPreset) => {
|
||||
try {
|
||||
setInstallingFromMarket(preset.id);
|
||||
await api.installPresetFromUrl(preset.downloadUrl);
|
||||
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' });
|
||||
} finally {
|
||||
setInstallingFromMarket(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 打开市场对话框时加载预设
|
||||
useEffect(() => {
|
||||
if (marketDialogOpen && marketPresets.length === 0) {
|
||||
loadMarketPresets();
|
||||
}
|
||||
}, [marketDialogOpen]);
|
||||
|
||||
// 过滤市场预设
|
||||
const filteredMarketPresets = marketPresets.filter(preset =>
|
||||
preset.name.toLowerCase().includes(marketSearch.toLowerCase()) ||
|
||||
preset.description?.toLowerCase().includes(marketSearch.toLowerCase()) ||
|
||||
preset.author?.toLowerCase().includes(marketSearch.toLowerCase()) ||
|
||||
preset.keywords?.some(keyword => keyword.toLowerCase().includes(marketSearch.toLowerCase()))
|
||||
);
|
||||
|
||||
// 加载预设列表
|
||||
const loadPresets = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.getPresets();
|
||||
setPresets(response.presets || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load presets:', error);
|
||||
setToast({ message: t('presets.load_presets_failed'), type: 'error' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadPresets();
|
||||
}, []);
|
||||
|
||||
// 查看预设详情
|
||||
const handleViewDetail = async (preset: PresetMetadata) => {
|
||||
try {
|
||||
const detail = await api.getPreset(preset.id);
|
||||
setSelectedPreset({ ...preset, ...detail });
|
||||
setDetailDialogOpen(true);
|
||||
|
||||
// 初始化 secrets
|
||||
if (detail.requiredInputs) {
|
||||
const initialSecrets: Record<string, string> = {};
|
||||
for (const input of detail.requiredInputs) {
|
||||
initialSecrets[input.field] = '';
|
||||
}
|
||||
setSecrets(initialSecrets);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load preset details:', error);
|
||||
setToast({ message: t('presets.load_preset_details_failed'), type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
// 安装预设
|
||||
const handleInstall = async () => {
|
||||
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' });
|
||||
return;
|
||||
}
|
||||
|
||||
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' });
|
||||
} finally {
|
||||
setIsInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 应用预设(配置敏感信息)
|
||||
const handleApplyPreset = async () => {
|
||||
try {
|
||||
setIsApplying(true);
|
||||
|
||||
// 验证所有必填项都已填写
|
||||
if (selectedPreset?.requiredInputs) {
|
||||
for (const input of selectedPreset.requiredInputs) {
|
||||
if (!secrets[input.field] || secrets[input.field].trim() === '') {
|
||||
setToast({ message: t('presets.please_fill_field', { field: input.field }), type: 'warning' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await api.applyPreset(selectedPreset!.name, secrets);
|
||||
setToast({ message: t('presets.preset_applied'), type: 'success' });
|
||||
setDetailDialogOpen(false);
|
||||
setSecrets({});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to apply preset:', error);
|
||||
setToast({ message: t('presets.preset_apply_failed', { error: error.message }), type: 'error' });
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除预设
|
||||
const handleDelete = async () => {
|
||||
if (!presetToDelete) return;
|
||||
|
||||
try {
|
||||
await api.deletePreset(presetToDelete);
|
||||
setToast({ message: t('presets.preset_deleted'), type: 'success' });
|
||||
setDeleteDialogOpen(false);
|
||||
setPresetToDelete(null);
|
||||
await loadPresets();
|
||||
} catch (error: any) {
|
||||
console.error('Failed to delete preset:', error);
|
||||
setToast({ message: t('presets.preset_delete_failed', { error: error.message }), type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between border-b p-4">
|
||||
<Button variant="ghost" size="icon" onClick={handleGoBack}>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<CardTitle className="text-lg">{t('presets.title')} <span className="text-sm font-normal text-gray-500">({presets.length})</span></CardTitle>
|
||||
<Button variant="ghost" size="icon" onClick={() => setMarketDialogOpen(true)}>
|
||||
<Store className="h-5 w-5" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow overflow-y-auto p-4">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-gray-500" />
|
||||
</div>
|
||||
) : presets.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||||
<Download className="h-12 w-12 mb-4 opacity-50" />
|
||||
<p>{t('presets.no_presets')}</p>
|
||||
<p className="text-sm">{t('presets.no_presets_hint')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{presets.map((preset) => (
|
||||
<div
|
||||
key={preset.name}
|
||||
className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium">{preset.name}</h3>
|
||||
<span className="text-xs text-gray-500">v{preset.version}</span>
|
||||
</div>
|
||||
{preset.description && (
|
||||
<p className="text-sm text-gray-600 mt-1">{preset.description}</p>
|
||||
)}
|
||||
{preset.author && (
|
||||
<p className="text-xs text-gray-500 mt-1">by {preset.author}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleViewDetail(preset)}
|
||||
>
|
||||
<Info className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setPresetToDelete(preset.id);
|
||||
setDeleteDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
{/* Install Dialog */}
|
||||
<Dialog open={installDialogOpen} onOpenChange={setInstallDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('presets.install_dialog_title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('presets.install_dialog_description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={installMethod === 'file' ? 'default' : 'outline'}
|
||||
onClick={() => setInstallMethod('file')}
|
||||
className="flex-1"
|
||||
>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{t('presets.upload_file')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={installMethod === 'url' ? 'default' : 'outline'}
|
||||
onClick={() => setInstallMethod('url')}
|
||||
className="flex-1"
|
||||
>
|
||||
<Link className="mr-2 h-4 w-4" />
|
||||
{t('presets.from_url')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{installMethod === 'file' ? (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="preset-file">{t('presets.preset_file')}</Label>
|
||||
<Input
|
||||
id="preset-file"
|
||||
type="file"
|
||||
accept=".ccrsets"
|
||||
onChange={(e) => setInstallFile(e.target.files?.[0] || null)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="preset-url">{t('presets.preset_url')}</Label>
|
||||
<Input
|
||||
id="preset-url"
|
||||
type="url"
|
||||
placeholder={t('presets.preset_url_placeholder')}
|
||||
value={installUrl}
|
||||
onChange={(e) => setInstallUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="preset-name">{t('presets.preset_name')}</Label>
|
||||
<Input
|
||||
id="preset-name"
|
||||
placeholder={t('presets.preset_name_placeholder')}
|
||||
value={installName}
|
||||
onChange={(e) => setInstallName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setInstallDialogOpen(false)}>
|
||||
{t('presets.close')}
|
||||
</Button>
|
||||
<Button onClick={handleInstall} disabled={isInstalling}>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('presets.installing')}
|
||||
</>
|
||||
) : (
|
||||
t('presets.install')
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Detail Dialog */}
|
||||
<Dialog open={detailDialogOpen} onOpenChange={setDetailDialogOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{selectedPreset?.name}
|
||||
{selectedPreset?.version && (
|
||||
<span className="text-sm font-normal text-gray-500">v{selectedPreset.version}</span>
|
||||
)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto py-4">
|
||||
{selectedPreset?.description && (
|
||||
<p className="text-gray-700 mb-4">{selectedPreset.description}</p>
|
||||
)}
|
||||
|
||||
{selectedPreset?.author && (
|
||||
<p className="text-sm text-gray-600 mb-1">
|
||||
<strong>Author:</strong> {selectedPreset.author}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{selectedPreset?.homepage && (
|
||||
<p className="text-sm text-gray-600 mb-1">
|
||||
<strong>Homepage:</strong> <a href={selectedPreset.homepage} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">{selectedPreset.homepage}</a>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{selectedPreset?.repository && (
|
||||
<p className="text-sm text-gray-600 mb-1">
|
||||
<strong>Repository:</strong> <a href={selectedPreset.repository} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">{selectedPreset.repository}</a>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{selectedPreset?.keywords && selectedPreset.keywords.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<strong>Keywords:</strong>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{selectedPreset.keywords.map((keyword) => (
|
||||
<span key={keyword} className="px-2 py-1 bg-gray-100 rounded text-sm">
|
||||
{keyword}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPreset?.requiredInputs && selectedPreset.requiredInputs.length > 0 && (
|
||||
<div className="mt-6 space-y-4">
|
||||
<h4 className="font-medium text-sm">{t('presets.required_information')}</h4>
|
||||
{selectedPreset.requiredInputs.map((input) => (
|
||||
<div key={input.field} className="space-y-2">
|
||||
<Label htmlFor={`secret-${input.field}`}>
|
||||
{input.prompt || input.field}
|
||||
</Label>
|
||||
<Input
|
||||
id={`secret-${input.field}`}
|
||||
type="password"
|
||||
placeholder={input.placeholder || t('presets.please_fill_field', { field: input.field })}
|
||||
value={secrets[input.field] || ''}
|
||||
onChange={(e) => setSecrets({ ...secrets, [input.field]: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDetailDialogOpen(false)}>
|
||||
{t('presets.close')}
|
||||
</Button>
|
||||
{selectedPreset?.requiredInputs && selectedPreset.requiredInputs.length > 0 && (
|
||||
<Button onClick={handleApplyPreset} disabled={isApplying}>
|
||||
{isApplying ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('presets.applying')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||
{t('presets.apply')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Market Presets Dialog */}
|
||||
<Dialog open={marketDialogOpen} onOpenChange={setMarketDialogOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Store className="h-5 w-5" />
|
||||
{t('presets.market_title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('presets.market_description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex items-center gap-2 py-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder={t('presets.search_placeholder')}
|
||||
value={marketSearch}
|
||||
onChange={(e) => setMarketSearch(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{marketLoading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-gray-500" />
|
||||
</div>
|
||||
) : filteredMarketPresets.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
|
||||
<Package className="h-12 w-12 mb-4 opacity-50" />
|
||||
<p>{t('presets.no_presets_found')}</p>
|
||||
<p className="text-sm">{t('presets.no_presets_found_hint')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{filteredMarketPresets.map((preset) => (
|
||||
<div
|
||||
key={preset.id}
|
||||
className="p-4 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="font-semibold text-lg">{preset.name}</h3>
|
||||
<span className="text-xs text-gray-500">v{preset.version}</span>
|
||||
{preset.rating && (
|
||||
<div className="flex items-center gap-1 text-xs text-yellow-600">
|
||||
<span>★</span>
|
||||
<span>{preset.rating}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{preset.description && (
|
||||
<p className="text-sm text-gray-600 mb-2">{preset.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500 mb-2">
|
||||
{preset.author && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium">{t('presets.by', { author: preset.author })}</span>
|
||||
{preset.repository && (
|
||||
<a
|
||||
href={preset.repository}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||
title={t('presets.github_repository')}
|
||||
>
|
||||
<i className="ri-github-fill text-base"></i>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{preset.downloads && (
|
||||
<span>{t('presets.downloads', { count: preset.downloads })}</span>
|
||||
)}
|
||||
{preset.license && (
|
||||
<span>{preset.license}</span>
|
||||
)}
|
||||
</div>
|
||||
{preset.keywords && preset.keywords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{preset.keywords.map((keyword) => (
|
||||
<span
|
||||
key={keyword}
|
||||
className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-xs"
|
||||
>
|
||||
{keyword}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleInstallFromMarket(preset)}
|
||||
disabled={installingFromMarket === preset.id}
|
||||
className="shrink-0"
|
||||
>
|
||||
{installingFromMarket === preset.id ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('presets.installing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{t('presets.install')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('presets.delete_dialog_title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('presets.delete_dialog_description', { name: presetToDelete })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
|
||||
{t('presets.close')}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleDelete}>
|
||||
{t('presets.delete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
31
packages/ui/src/components/ui/tooltip.tsx
Normal file
31
packages/ui/src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-gray-900 px-3 py-1.5 text-xs text-white animate-in fade-in-0 zoom-in-95",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
|
||||
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
@@ -235,6 +235,73 @@ class ApiClient {
|
||||
async clearLogs(filePath: string): Promise<void> {
|
||||
return this.delete<void>(`/logs?file=${encodeURIComponent(filePath)}`);
|
||||
}
|
||||
|
||||
// ========== Preset API methods ==========
|
||||
|
||||
// Get presets list
|
||||
async getPresets(): Promise<{ presets: Array<any> }> {
|
||||
return this.get<{ presets: Array<any> }>('/presets');
|
||||
}
|
||||
|
||||
// Get preset details
|
||||
async getPreset(name: string): Promise<any> {
|
||||
return this.get<any>(`/presets/${encodeURIComponent(name)}`);
|
||||
}
|
||||
|
||||
// Install preset from URL
|
||||
async installPresetFromUrl(url: string, name?: string): Promise<any> {
|
||||
return this.post<any>('/presets/install', { url, name });
|
||||
}
|
||||
|
||||
// Upload preset file
|
||||
async uploadPresetFile(file: File, name?: string): Promise<any> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (name) {
|
||||
formData.append('name', name);
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}/presets/upload`;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('apiKey');
|
||||
window.dispatchEvent(new CustomEvent('unauthorized'));
|
||||
return new Promise(() => {}) as any;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload preset: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Apply preset (configure sensitive fields)
|
||||
async applyPreset(name: string, secrets: Record<string, string>): Promise<any> {
|
||||
return this.post<any>(`/presets/${encodeURIComponent(name)}/apply`, { secrets });
|
||||
}
|
||||
|
||||
// Delete preset
|
||||
async deletePreset(name: string): Promise<any> {
|
||||
return this.delete<any>(`/presets/${encodeURIComponent(name)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a default instance of the API client
|
||||
|
||||
@@ -25,7 +25,12 @@
|
||||
"no_updates_available": "No updates available",
|
||||
"update_check_failed": "Failed to check for updates",
|
||||
"update_successful": "Update successful",
|
||||
"update_failed": "Update failed"
|
||||
"update_failed": "Update failed",
|
||||
"json_editor": "JSON Editor",
|
||||
"log_viewer": "Log Viewer",
|
||||
"presets": "Presets",
|
||||
"language": "Language",
|
||||
"check_updates": "Check for Updates"
|
||||
},
|
||||
"login": {
|
||||
"title": "Sign in to your account",
|
||||
@@ -233,5 +238,50 @@
|
||||
"worker_init_failed": "Failed to initialize worker",
|
||||
"grouping_not_supported": "Log grouping not supported by server",
|
||||
"back": "Back"
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"market_title": "Preset Market",
|
||||
"market_description": "Browse and install presets from the community marketplace",
|
||||
"no_presets": "No presets installed",
|
||||
"no_presets_hint": "Install a preset to get started",
|
||||
"search_placeholder": "Search presets by name, description, author, or keywords...",
|
||||
"no_presets_found": "No presets found",
|
||||
"no_presets_found_hint": "Try adjusting your search terms",
|
||||
"loading": "Loading...",
|
||||
"by": "by {{author}}",
|
||||
"downloads": "{{count}} downloads",
|
||||
"github_repository": "GitHub Repository",
|
||||
"view_details": "View Details",
|
||||
"install": "Install",
|
||||
"installing": "Installing...",
|
||||
"apply": "Apply Preset",
|
||||
"applying": "Applying...",
|
||||
"close": "Close",
|
||||
"delete": "Delete",
|
||||
"install_dialog_title": "Install Preset",
|
||||
"install_dialog_description": "Install a preset from a file or URL",
|
||||
"upload_file": "Upload File",
|
||||
"from_url": "From URL",
|
||||
"preset_file": "Preset File (.ccrsets)",
|
||||
"preset_url": "Preset URL",
|
||||
"preset_url_placeholder": "https://example.com/preset.ccrsets",
|
||||
"preset_name": "Preset Name (Optional)",
|
||||
"preset_name_placeholder": "Auto-generated from file",
|
||||
"please_provide_file_or_url": "Please provide a file or URL",
|
||||
"detail_dialog_title": "Preset Details",
|
||||
"required_information": "Required Information",
|
||||
"delete_dialog_title": "Delete Preset",
|
||||
"delete_dialog_description": "Are you sure you want to delete preset \"{{name}}\"? This action cannot be undone.",
|
||||
"preset_installed": "Preset installed successfully",
|
||||
"preset_install_failed": "Failed to install preset: {{error}}",
|
||||
"preset_applied": "Preset applied successfully",
|
||||
"preset_apply_failed": "Failed to apply preset: {{error}}",
|
||||
"preset_deleted": "Preset deleted successfully",
|
||||
"preset_delete_failed": "Failed to delete preset: {{error}}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,12 @@
|
||||
"no_updates_available": "当前已是最新版本",
|
||||
"update_check_failed": "检查更新失败",
|
||||
"update_successful": "更新成功",
|
||||
"update_failed": "更新失败"
|
||||
"update_failed": "更新失败",
|
||||
"json_editor": "JSON 编辑器",
|
||||
"log_viewer": "日志查看器",
|
||||
"presets": "预设",
|
||||
"language": "语言",
|
||||
"check_updates": "检查更新"
|
||||
},
|
||||
"login": {
|
||||
"title": "登录到您的账户",
|
||||
@@ -233,5 +238,50 @@
|
||||
"worker_init_failed": "Worker初始化失败",
|
||||
"grouping_not_supported": "服务器不支持日志分组",
|
||||
"back": "返回"
|
||||
},
|
||||
"presets": {
|
||||
"title": "预设",
|
||||
"market_title": "预设市场",
|
||||
"market_description": "浏览并从社区市场安装预设",
|
||||
"no_presets": "暂无已安装的预设",
|
||||
"no_presets_hint": "安装一个预设以开始使用",
|
||||
"search_placeholder": "按名称、描述、作者或关键词搜索预设...",
|
||||
"no_presets_found": "未找到预设",
|
||||
"no_presets_found_hint": "请尝试调整搜索条件",
|
||||
"loading": "加载中...",
|
||||
"by": "{{author}} 创作",
|
||||
"downloads": "{{count}} 次下载",
|
||||
"github_repository": "GitHub 仓库",
|
||||
"view_details": "查看详情",
|
||||
"install": "安装",
|
||||
"installing": "安装中...",
|
||||
"apply": "应用预设",
|
||||
"applying": "应用中...",
|
||||
"close": "关闭",
|
||||
"delete": "删除",
|
||||
"install_dialog_title": "安装预设",
|
||||
"install_dialog_description": "从文件或URL安装预设",
|
||||
"upload_file": "上传文件",
|
||||
"from_url": "从 URL",
|
||||
"preset_file": "预设文件 (.ccrsets)",
|
||||
"preset_url": "预设 URL",
|
||||
"preset_url_placeholder": "https://example.com/preset.ccrsets",
|
||||
"preset_name": "预设名称 (可选)",
|
||||
"preset_name_placeholder": "从文件自动生成",
|
||||
"please_provide_file_or_url": "请提供文件或 URL",
|
||||
"detail_dialog_title": "预设详情",
|
||||
"required_information": "必需信息",
|
||||
"delete_dialog_title": "删除预设",
|
||||
"delete_dialog_description": "您确定要删除预设 \"{{name}}\" 吗?此操作无法撤销。",
|
||||
"preset_installed": "预设安装成功",
|
||||
"preset_install_failed": "预设安装失败:{{error}}",
|
||||
"preset_applied": "预设应用成功",
|
||||
"preset_apply_failed": "预设应用失败:{{error}}",
|
||||
"preset_deleted": "预设删除成功",
|
||||
"preset_delete_failed": "预设删除失败:{{error}}",
|
||||
"load_presets_failed": "加载预设失败",
|
||||
"load_preset_details_failed": "加载预设详情失败",
|
||||
"please_fill_field": "请填写 {{field}}",
|
||||
"load_market_failed": "加载市场预设失败"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import './i18n';
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import 'remixicon/fonts/remixicon.css'
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './routes';
|
||||
import { ConfigProvider } from '@/components/ConfigProvider';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createMemoryRouter, Navigate } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import { Login } from '@/components/Login';
|
||||
import { DebugPage } from '@/components/DebugPage';
|
||||
import { Presets } from '@/components/Presets';
|
||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||
import PublicRoute from '@/components/PublicRoute';
|
||||
|
||||
@@ -18,6 +19,10 @@ export const router = createMemoryRouter([
|
||||
path: '/dashboard',
|
||||
element: <ProtectedRoute><App /></ProtectedRoute>,
|
||||
},
|
||||
{
|
||||
path: '/presets',
|
||||
element: <ProtectedRoute><Presets /></ProtectedRoute>,
|
||||
},
|
||||
{
|
||||
path: '/debug',
|
||||
element: <ProtectedRoute><DebugPage /></ProtectedRoute>,
|
||||
|
||||
348
pnpm-lock.yaml
generated
348
pnpm-lock.yaml
generated
@@ -75,6 +75,12 @@ importers:
|
||||
'@inquirer/prompts':
|
||||
specifier: ^5.0.0
|
||||
version: 5.5.0
|
||||
adm-zip:
|
||||
specifier: ^0.5.16
|
||||
version: 0.5.16
|
||||
archiver:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
find-process:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
@@ -85,6 +91,9 @@ importers:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
devDependencies:
|
||||
'@types/archiver':
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
'@types/node':
|
||||
specifier: ^24.0.15
|
||||
version: 24.7.0
|
||||
@@ -100,12 +109,18 @@ importers:
|
||||
|
||||
packages/server:
|
||||
dependencies:
|
||||
'@fastify/multipart':
|
||||
specifier: ^9.0.0
|
||||
version: 9.3.0
|
||||
'@fastify/static':
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0
|
||||
'@musistudio/llms':
|
||||
specifier: ^1.0.51
|
||||
version: 1.0.51(ws@8.18.3)
|
||||
adm-zip:
|
||||
specifier: ^0.5.16
|
||||
version: 0.5.16
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.6.1
|
||||
@@ -131,6 +146,9 @@ importers:
|
||||
'@CCR/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
'@types/adm-zip':
|
||||
specifier: ^0.5.7
|
||||
version: 0.5.7
|
||||
'@types/node':
|
||||
specifier: ^24.0.15
|
||||
version: 24.7.0
|
||||
@@ -148,7 +166,23 @@ importers:
|
||||
version: 5.8.3
|
||||
|
||||
packages/shared:
|
||||
dependencies:
|
||||
adm-zip:
|
||||
specifier: ^0.5.16
|
||||
version: 0.5.16
|
||||
archiver:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
json5:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
devDependencies:
|
||||
'@types/adm-zip':
|
||||
specifier: ^0.5.7
|
||||
version: 0.5.7
|
||||
'@types/archiver':
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
'@types/node':
|
||||
specifier: ^24.0.15
|
||||
version: 24.7.0
|
||||
@@ -227,6 +261,9 @@ importers:
|
||||
react-router-dom:
|
||||
specifier: ^7.7.0
|
||||
version: 7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
remixicon:
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0
|
||||
tailwind-merge:
|
||||
specifier: ^3.3.1
|
||||
version: 3.4.0
|
||||
@@ -1837,9 +1874,15 @@ packages:
|
||||
'@fastify/ajv-compiler@4.0.2':
|
||||
resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==}
|
||||
|
||||
'@fastify/busboy@3.2.0':
|
||||
resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==}
|
||||
|
||||
'@fastify/cors@11.1.0':
|
||||
resolution: {integrity: sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA==}
|
||||
|
||||
'@fastify/deepmerge@3.1.0':
|
||||
resolution: {integrity: sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==}
|
||||
|
||||
'@fastify/error@4.2.0':
|
||||
resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==}
|
||||
|
||||
@@ -1852,6 +1895,9 @@ packages:
|
||||
'@fastify/merge-json-schemas@0.2.1':
|
||||
resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==}
|
||||
|
||||
'@fastify/multipart@9.3.0':
|
||||
resolution: {integrity: sha512-NpeKipTOjjL1dA7SSlRMrOWWtrE8/0yKOmeudkdQoEaz4sVDJw5MVdZIahsWhvpc3YTN7f04f9ep/Y65RKoOWA==}
|
||||
|
||||
'@fastify/proxy-addr@5.1.0':
|
||||
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
|
||||
|
||||
@@ -2090,6 +2136,10 @@ packages:
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@pnpm/config.env-replace@1.1.0':
|
||||
resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
@@ -2812,6 +2862,12 @@ packages:
|
||||
'@tsconfig/node16@1.0.4':
|
||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||
|
||||
'@types/adm-zip@0.5.7':
|
||||
resolution: {integrity: sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==}
|
||||
|
||||
'@types/archiver@7.0.0':
|
||||
resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||
|
||||
@@ -2951,6 +3007,9 @@ packages:
|
||||
'@types/react@18.3.27':
|
||||
resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==}
|
||||
|
||||
'@types/readdir-glob@1.1.5':
|
||||
resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==}
|
||||
|
||||
'@types/retry@0.12.2':
|
||||
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
|
||||
|
||||
@@ -3116,6 +3175,10 @@ packages:
|
||||
'@xtuc/long@4.2.2':
|
||||
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
|
||||
abstract-logging@2.0.1:
|
||||
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
||||
|
||||
@@ -3147,6 +3210,10 @@ packages:
|
||||
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
adm-zip@0.5.16:
|
||||
resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==}
|
||||
engines: {node: '>=12.0'}
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -3237,6 +3304,14 @@ packages:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
archiver-utils@5.0.2:
|
||||
resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
archiver@7.0.1:
|
||||
resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
arg@4.1.3:
|
||||
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||
|
||||
@@ -3264,6 +3339,9 @@ packages:
|
||||
resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==}
|
||||
hasBin: true
|
||||
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
|
||||
atomic-sleep@1.0.0:
|
||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -3278,6 +3356,14 @@ packages:
|
||||
avvio@9.1.0:
|
||||
resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==}
|
||||
|
||||
b4a@1.7.3:
|
||||
resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==}
|
||||
peerDependencies:
|
||||
react-native-b4a: '*'
|
||||
peerDependenciesMeta:
|
||||
react-native-b4a:
|
||||
optional: true
|
||||
|
||||
babel-loader@9.2.1:
|
||||
resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==}
|
||||
engines: {node: '>= 14.15.0'}
|
||||
@@ -3309,6 +3395,14 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
bare-events@2.8.2:
|
||||
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
||||
peerDependencies:
|
||||
bare-abort-controller: '*'
|
||||
peerDependenciesMeta:
|
||||
bare-abort-controller:
|
||||
optional: true
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
@@ -3362,12 +3456,19 @@ packages:
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
buffer-crc32@1.0.0:
|
||||
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
buffer@6.0.3:
|
||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||
|
||||
bundle-name@4.1.0:
|
||||
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3564,6 +3665,10 @@ packages:
|
||||
common-path-prefix@3.0.0:
|
||||
resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==}
|
||||
|
||||
compress-commons@6.0.2:
|
||||
resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
compressible@2.0.18:
|
||||
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3643,6 +3748,15 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
crc-32@1.2.2:
|
||||
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
|
||||
engines: {node: '>=0.8'}
|
||||
hasBin: true
|
||||
|
||||
crc32-stream@6.0.0:
|
||||
resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
create-require@1.1.1:
|
||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||
|
||||
@@ -4157,9 +4271,16 @@ packages:
|
||||
resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
|
||||
events-universal@1.0.1:
|
||||
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
|
||||
|
||||
events@3.3.0:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
@@ -4197,6 +4318,9 @@ packages:
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-fifo@1.3.2:
|
||||
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
@@ -4413,6 +4537,10 @@ packages:
|
||||
glob-to-regexp@0.4.1:
|
||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||
|
||||
glob@10.5.0:
|
||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||
hasBin: true
|
||||
|
||||
glob@11.0.3:
|
||||
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -4655,6 +4783,9 @@ packages:
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -4857,6 +4988,9 @@ packages:
|
||||
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -4960,6 +5094,10 @@ packages:
|
||||
launch-editor@2.12.0:
|
||||
resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==}
|
||||
|
||||
lazystream@1.0.1:
|
||||
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
||||
engines: {node: '>= 0.6.3'}
|
||||
|
||||
leven@3.1.0:
|
||||
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -5097,6 +5235,9 @@ packages:
|
||||
resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.2.2:
|
||||
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -5409,6 +5550,10 @@ packages:
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
minimatch@5.1.6:
|
||||
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -5702,6 +5847,10 @@ packages:
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-scurry@2.0.0:
|
||||
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -6208,6 +6357,10 @@ packages:
|
||||
process-warning@5.0.0:
|
||||
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
||||
|
||||
process@0.11.10:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
prompts@2.4.2:
|
||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -6423,6 +6576,13 @@ packages:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
readable-stream@4.7.0:
|
||||
resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
readdir-glob@1.1.3:
|
||||
resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==}
|
||||
|
||||
readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
@@ -6513,6 +6673,9 @@ packages:
|
||||
remark-stringify@11.0.0:
|
||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||
|
||||
remixicon@4.7.0:
|
||||
resolution: {integrity: sha512-g2pHOofQWARWpxdbrQu5+K3C8ZbqguQFzE54HIMWFCpFa63pumaAltIgZmFMRQpKKBScRWQASQfWxS9asNCcHQ==}
|
||||
|
||||
renderkid@3.0.0:
|
||||
resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==}
|
||||
|
||||
@@ -6840,6 +7003,9 @@ packages:
|
||||
std-env@3.10.0:
|
||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||
|
||||
streamx@2.23.0:
|
||||
resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6951,6 +7117,9 @@ packages:
|
||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tar-stream@3.1.7:
|
||||
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
|
||||
|
||||
terser-webpack-plugin@5.3.16:
|
||||
resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
@@ -6972,6 +7141,9 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
text-decoder@1.2.3:
|
||||
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||
engines: {node: '>=0.8'}
|
||||
@@ -7499,6 +7671,10 @@ packages:
|
||||
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
zip-stream@6.0.1:
|
||||
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
zod@4.2.1:
|
||||
resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==}
|
||||
|
||||
@@ -9702,11 +9878,15 @@ snapshots:
|
||||
ajv-formats: 3.0.1(ajv@8.17.1)
|
||||
fast-uri: 3.1.0
|
||||
|
||||
'@fastify/busboy@3.2.0': {}
|
||||
|
||||
'@fastify/cors@11.1.0':
|
||||
dependencies:
|
||||
fastify-plugin: 5.1.0
|
||||
toad-cache: 3.7.0
|
||||
|
||||
'@fastify/deepmerge@3.1.0': {}
|
||||
|
||||
'@fastify/error@4.2.0': {}
|
||||
|
||||
'@fastify/fast-json-stringify-compiler@5.0.3':
|
||||
@@ -9719,6 +9899,14 @@ snapshots:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
'@fastify/multipart@9.3.0':
|
||||
dependencies:
|
||||
'@fastify/busboy': 3.2.0
|
||||
'@fastify/deepmerge': 3.1.0
|
||||
'@fastify/error': 4.2.0
|
||||
fastify-plugin: 5.1.0
|
||||
secure-json-parse: 4.1.0
|
||||
|
||||
'@fastify/proxy-addr@5.1.0':
|
||||
dependencies:
|
||||
'@fastify/forwarded': 3.0.1
|
||||
@@ -10065,6 +10253,9 @@ snapshots:
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/config.env-replace@1.1.0': {}
|
||||
|
||||
'@pnpm/network.ca-file@1.0.2':
|
||||
@@ -10703,6 +10894,14 @@ snapshots:
|
||||
|
||||
'@tsconfig/node16@1.0.4': {}
|
||||
|
||||
'@types/adm-zip@0.5.7':
|
||||
dependencies:
|
||||
'@types/node': 24.7.0
|
||||
|
||||
'@types/archiver@7.0.0':
|
||||
dependencies:
|
||||
'@types/readdir-glob': 1.1.5
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.5
|
||||
@@ -10872,6 +11071,10 @@ snapshots:
|
||||
'@types/prop-types': 15.7.15
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/readdir-glob@1.1.5':
|
||||
dependencies:
|
||||
'@types/node': 24.7.0
|
||||
|
||||
'@types/retry@0.12.2': {}
|
||||
|
||||
'@types/sax@1.2.7':
|
||||
@@ -11107,6 +11310,10 @@ snapshots:
|
||||
|
||||
'@xtuc/long@4.2.2': {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
|
||||
abstract-logging@2.0.1: {}
|
||||
|
||||
accepts@1.3.8:
|
||||
@@ -11130,6 +11337,8 @@ snapshots:
|
||||
|
||||
address@1.2.2: {}
|
||||
|
||||
adm-zip@0.5.16: {}
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
aggregate-error@3.1.0:
|
||||
@@ -11225,6 +11434,29 @@ snapshots:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
|
||||
archiver-utils@5.0.2:
|
||||
dependencies:
|
||||
glob: 10.5.0
|
||||
graceful-fs: 4.2.11
|
||||
is-stream: 2.0.1
|
||||
lazystream: 1.0.1
|
||||
lodash: 4.17.21
|
||||
normalize-path: 3.0.0
|
||||
readable-stream: 4.7.0
|
||||
|
||||
archiver@7.0.1:
|
||||
dependencies:
|
||||
archiver-utils: 5.0.2
|
||||
async: 3.2.6
|
||||
buffer-crc32: 1.0.0
|
||||
readable-stream: 4.7.0
|
||||
readdir-glob: 1.1.3
|
||||
tar-stream: 3.1.7
|
||||
zip-stream: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
arg@4.1.3: {}
|
||||
|
||||
arg@5.0.2: {}
|
||||
@@ -11245,6 +11477,8 @@ snapshots:
|
||||
|
||||
astring@1.9.0: {}
|
||||
|
||||
async@3.2.6: {}
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
|
||||
autoprefixer@10.4.23(postcss@8.5.6):
|
||||
@@ -11261,6 +11495,8 @@ snapshots:
|
||||
'@fastify/error': 4.2.0
|
||||
fastq: 1.19.1
|
||||
|
||||
b4a@1.7.3: {}
|
||||
|
||||
babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.104.1(esbuild@0.25.10)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
@@ -11300,6 +11536,8 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
bare-events@2.8.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
baseline-browser-mapping@2.9.11: {}
|
||||
@@ -11379,10 +11617,17 @@ snapshots:
|
||||
node-releases: 2.0.27
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
|
||||
buffer-crc32@1.0.0: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@6.0.3:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
bundle-name@4.1.0:
|
||||
dependencies:
|
||||
run-applescript: 7.1.0
|
||||
@@ -11570,6 +11815,14 @@ snapshots:
|
||||
|
||||
common-path-prefix@3.0.0: {}
|
||||
|
||||
compress-commons@6.0.2:
|
||||
dependencies:
|
||||
crc-32: 1.2.2
|
||||
crc32-stream: 6.0.0
|
||||
is-stream: 2.0.1
|
||||
normalize-path: 3.0.0
|
||||
readable-stream: 4.7.0
|
||||
|
||||
compressible@2.0.18:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
@@ -11650,6 +11903,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
crc-32@1.2.2: {}
|
||||
|
||||
crc32-stream@6.0.0:
|
||||
dependencies:
|
||||
crc-32: 1.2.2
|
||||
readable-stream: 4.7.0
|
||||
|
||||
create-require@1.1.1: {}
|
||||
|
||||
cross-spawn@6.0.6:
|
||||
@@ -12232,8 +12492,16 @@ snapshots:
|
||||
'@types/node': 24.7.0
|
||||
require-like: 0.1.2
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
events-universal@1.0.1:
|
||||
dependencies:
|
||||
bare-events: 2.8.2
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
eventsource-parser@3.0.6: {}
|
||||
@@ -12312,6 +12580,8 @@ snapshots:
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-fifo@1.3.2: {}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -12564,6 +12834,15 @@ snapshots:
|
||||
|
||||
glob-to-regexp@0.4.1: {}
|
||||
|
||||
glob@10.5.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
glob@11.0.3:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
@@ -12941,6 +13220,8 @@ snapshots:
|
||||
dependencies:
|
||||
postcss: 8.5.6
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
ignore@7.0.5: {}
|
||||
@@ -13074,6 +13355,12 @@ snapshots:
|
||||
|
||||
isobject@3.0.1: {}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
@@ -13183,6 +13470,10 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
shell-quote: 1.8.3
|
||||
|
||||
lazystream@1.0.1:
|
||||
dependencies:
|
||||
readable-stream: 2.3.8
|
||||
|
||||
leven@3.1.0: {}
|
||||
|
||||
levn@0.4.1:
|
||||
@@ -13289,6 +13580,8 @@ snapshots:
|
||||
|
||||
lowercase-keys@3.0.0: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
@@ -13874,6 +14167,10 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
minimatch@5.1.6:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
@@ -14141,6 +14438,11 @@ snapshots:
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-scurry@2.0.0:
|
||||
dependencies:
|
||||
lru-cache: 11.2.2
|
||||
@@ -14676,6 +14978,8 @@ snapshots:
|
||||
|
||||
process-warning@5.0.0: {}
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
prompts@2.4.2:
|
||||
dependencies:
|
||||
kleur: 3.0.3
|
||||
@@ -14894,6 +15198,18 @@ snapshots:
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readable-stream@4.7.0:
|
||||
dependencies:
|
||||
abort-controller: 3.0.0
|
||||
buffer: 6.0.3
|
||||
events: 3.3.0
|
||||
process: 0.11.10
|
||||
string_decoder: 1.3.0
|
||||
|
||||
readdir-glob@1.1.3:
|
||||
dependencies:
|
||||
minimatch: 5.1.6
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
@@ -15049,6 +15365,8 @@ snapshots:
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
unified: 11.0.5
|
||||
|
||||
remixicon@4.7.0: {}
|
||||
|
||||
renderkid@3.0.0:
|
||||
dependencies:
|
||||
css-select: 4.3.0
|
||||
@@ -15417,6 +15735,15 @@ snapshots:
|
||||
|
||||
std-env@3.10.0: {}
|
||||
|
||||
streamx@2.23.0:
|
||||
dependencies:
|
||||
events-universal: 1.0.1
|
||||
fast-fifo: 1.3.2
|
||||
text-decoder: 1.2.3
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
@@ -15556,6 +15883,15 @@ snapshots:
|
||||
|
||||
tapable@2.3.0: {}
|
||||
|
||||
tar-stream@3.1.7:
|
||||
dependencies:
|
||||
b4a: 1.7.3
|
||||
fast-fifo: 1.3.2
|
||||
streamx: 2.23.0
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
terser-webpack-plugin@5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.25.10)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
@@ -15574,6 +15910,12 @@ snapshots:
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
text-decoder@1.2.3:
|
||||
dependencies:
|
||||
b4a: 1.7.3
|
||||
transitivePeerDependencies:
|
||||
- react-native-b4a
|
||||
|
||||
thenify-all@1.6.0:
|
||||
dependencies:
|
||||
thenify: 3.3.1
|
||||
@@ -16090,6 +16432,12 @@ snapshots:
|
||||
|
||||
yoctocolors-cjs@2.1.3: {}
|
||||
|
||||
zip-stream@6.0.1:
|
||||
dependencies:
|
||||
archiver-utils: 5.0.2
|
||||
compress-commons: 6.0.2
|
||||
readable-stream: 4.7.0
|
||||
|
||||
zod@4.2.1: {}
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
Reference in New Issue
Block a user