mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-02-20 15:40:50 +08:00
finished presets
This commit is contained in:
@@ -18,7 +18,7 @@ import { Upload, Link, Trash2, Info, Download, CheckCircle2, AlertCircle, Loader
|
||||
import { Toast } from "@/components/ui/toast";
|
||||
import { DynamicConfigForm } from "./preset/DynamicConfigForm";
|
||||
|
||||
// Schema 类型
|
||||
// Schema types
|
||||
interface InputOption {
|
||||
label: string;
|
||||
value: string | number | boolean;
|
||||
@@ -87,6 +87,7 @@ interface PresetDetail extends PresetMetadata {
|
||||
schema?: RequiredInput[];
|
||||
template?: any;
|
||||
configMappings?: any[];
|
||||
userValues?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface MarketPreset {
|
||||
@@ -126,7 +127,7 @@ export function Presets() {
|
||||
navigate('/dashboard');
|
||||
};
|
||||
|
||||
// 加载市场预设
|
||||
// Load market presets
|
||||
const loadMarketPresets = async () => {
|
||||
setMarketLoading(true);
|
||||
try {
|
||||
@@ -140,44 +141,51 @@ export function Presets() {
|
||||
}
|
||||
};
|
||||
|
||||
// 从市场安装预设
|
||||
// Install preset from market
|
||||
const handleInstallFromMarket = async (preset: MarketPreset) => {
|
||||
try {
|
||||
setInstallingFromMarket(preset.id);
|
||||
|
||||
// 第一步:安装预设(解压到目录)
|
||||
await api.installPresetFromGitHub(preset.repo, preset.name);
|
||||
// Step 1: Install preset (extract to directory)
|
||||
const installResult = await api.installPresetFromGitHub(preset.repo);
|
||||
|
||||
// 第二步:获取预设详情(检查是否需要配置)
|
||||
// Step 2: Get preset details (check if configuration is required)
|
||||
try {
|
||||
const detail = await api.getPreset(preset.name);
|
||||
const presetDetail: PresetDetail = { ...preset, ...detail };
|
||||
const installedPresetName = installResult.presetName || preset.name;
|
||||
const detail = await api.getPreset(installedPresetName);
|
||||
const presetDetail: PresetDetail = { ...preset, ...detail, id: installedPresetName };
|
||||
|
||||
// 检查是否需要配置
|
||||
// Check if configuration is required
|
||||
if (detail.schema && detail.schema.length > 0) {
|
||||
// 需要配置,打开配置对话框
|
||||
// Configuration required, open configuration dialog
|
||||
setSelectedPreset(presetDetail);
|
||||
|
||||
// 初始化默认值
|
||||
// Initialize form values: prefer saved userValues, otherwise use defaultValue
|
||||
const initialValues: Record<string, any> = {};
|
||||
for (const input of detail.schema) {
|
||||
initialValues[input.id] = input.defaultValue ?? '';
|
||||
// Prefer saved values
|
||||
if (detail.userValues && detail.userValues[input.id] !== undefined) {
|
||||
initialValues[input.id] = detail.userValues[input.id];
|
||||
} else {
|
||||
// Otherwise use default value
|
||||
initialValues[input.id] = input.defaultValue ?? '';
|
||||
}
|
||||
}
|
||||
setSecrets(initialValues);
|
||||
|
||||
// 关闭市场对话框,打开详情对话框
|
||||
// Close market dialog, open details dialog
|
||||
setMarketDialogOpen(false);
|
||||
setDetailDialogOpen(true);
|
||||
|
||||
setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' });
|
||||
} else {
|
||||
// 不需要配置,直接完成
|
||||
// No configuration required, complete directly
|
||||
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
||||
setMarketDialogOpen(false);
|
||||
await loadPresets();
|
||||
}
|
||||
} catch (error) {
|
||||
// 获取详情失败,但安装成功了,刷新列表
|
||||
// Failed to get details, but installation succeeded, refresh list
|
||||
console.error('Failed to get preset details after installation:', error);
|
||||
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
||||
setMarketDialogOpen(false);
|
||||
@@ -191,21 +199,21 @@ export function Presets() {
|
||||
}
|
||||
};
|
||||
|
||||
// 打开市场对话框时加载预设
|
||||
// Load presets when opening market dialog
|
||||
useEffect(() => {
|
||||
if (marketDialogOpen && marketPresets.length === 0) {
|
||||
loadMarketPresets();
|
||||
}
|
||||
}, [marketDialogOpen]);
|
||||
|
||||
// 过滤市场预设
|
||||
// Filter market presets
|
||||
const filteredMarketPresets = marketPresets.filter(preset =>
|
||||
preset.name.toLowerCase().includes(marketSearch.toLowerCase()) ||
|
||||
preset.description?.toLowerCase().includes(marketSearch.toLowerCase()) ||
|
||||
preset.author?.toLowerCase().includes(marketSearch.toLowerCase())
|
||||
);
|
||||
|
||||
// 加载预设列表
|
||||
// Load presets list
|
||||
const loadPresets = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -223,18 +231,24 @@ export function Presets() {
|
||||
loadPresets();
|
||||
}, []);
|
||||
|
||||
// 查看预设详情
|
||||
// View preset details
|
||||
const handleViewDetail = async (preset: PresetMetadata) => {
|
||||
try {
|
||||
const detail = await api.getPreset(preset.id);
|
||||
setSelectedPreset({ ...preset, ...detail });
|
||||
setDetailDialogOpen(true);
|
||||
|
||||
// 初始化默认值
|
||||
// 初始化表单值:优先使用已保存的 userValues,否则使用 defaultValue
|
||||
if (detail.schema && detail.schema.length > 0) {
|
||||
const initialValues: Record<string, any> = {};
|
||||
for (const input of detail.schema) {
|
||||
initialValues[input.id] = input.defaultValue ?? '';
|
||||
// 优先使用已保存的值
|
||||
if (detail.userValues && detail.userValues[input.id] !== undefined) {
|
||||
initialValues[input.id] = detail.userValues[input.id];
|
||||
} else {
|
||||
// Otherwise use default value
|
||||
initialValues[input.id] = input.defaultValue ?? '';
|
||||
}
|
||||
}
|
||||
setSecrets(initialValues);
|
||||
}
|
||||
@@ -266,38 +280,47 @@ export function Presets() {
|
||||
: installUrl!.split('/').pop()!.replace('.ccrsets', '')
|
||||
);
|
||||
|
||||
// 第一步:安装预设(解压到目录)
|
||||
// Step 1: Install preset (extract to directory)
|
||||
let installResult;
|
||||
if (installMethod === 'url' && installUrl) {
|
||||
await api.installPresetFromUrl(installUrl, presetName);
|
||||
installResult = await api.installPresetFromUrl(installUrl, presetName);
|
||||
} else if (installMethod === 'file' && installFile) {
|
||||
await api.uploadPresetFile(installFile, presetName);
|
||||
installResult = await api.uploadPresetFile(installFile, presetName);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// 第二步:获取预设详情(检查是否需要配置)
|
||||
// Step 2: Get preset details (check if configuration is required)
|
||||
try {
|
||||
const detail = await api.getPreset(presetName);
|
||||
// 使用服务器返回的实际预设名称
|
||||
const actualPresetName = installResult?.presetName || presetName;
|
||||
const detail = await api.getPreset(actualPresetName);
|
||||
|
||||
// 检查是否需要配置
|
||||
// Check if configuration is required
|
||||
if (detail.schema && detail.schema.length > 0) {
|
||||
// 需要配置,打开配置对话框
|
||||
// Configuration required, open configuration dialog
|
||||
setSelectedPreset({
|
||||
id: presetName,
|
||||
name: presetName,
|
||||
id: actualPresetName,
|
||||
name: detail.name || actualPresetName,
|
||||
version: detail.version || '1.0.0',
|
||||
installed: true,
|
||||
...detail
|
||||
});
|
||||
|
||||
// 初始化默认值
|
||||
// Initialize form values: prefer saved userValues, otherwise use defaultValue
|
||||
const initialValues: Record<string, any> = {};
|
||||
for (const input of detail.schema) {
|
||||
initialValues[input.id] = input.defaultValue ?? '';
|
||||
// Prefer saved values
|
||||
if (detail.userValues && detail.userValues[input.id] !== undefined) {
|
||||
initialValues[input.id] = detail.userValues[input.id];
|
||||
} else {
|
||||
// Otherwise use default value
|
||||
initialValues[input.id] = input.defaultValue ?? '';
|
||||
}
|
||||
}
|
||||
setSecrets(initialValues);
|
||||
|
||||
// 关闭安装对话框,打开详情对话框
|
||||
// Close installation dialog, open details dialog
|
||||
setInstallDialogOpen(false);
|
||||
setInstallUrl('');
|
||||
setInstallFile(null);
|
||||
@@ -306,7 +329,7 @@ export function Presets() {
|
||||
|
||||
setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' });
|
||||
} else {
|
||||
// 不需要配置,直接完成
|
||||
// No configuration required, complete directly
|
||||
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
||||
setInstallDialogOpen(false);
|
||||
setInstallUrl('');
|
||||
@@ -315,7 +338,7 @@ export function Presets() {
|
||||
await loadPresets();
|
||||
}
|
||||
} catch (error) {
|
||||
// 获取详情失败,但安装成功了,刷新列表
|
||||
// Failed to get details, but installation succeeded, refresh list
|
||||
console.error('Failed to get preset details after installation:', error);
|
||||
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
||||
setInstallDialogOpen(false);
|
||||
@@ -332,20 +355,24 @@ export function Presets() {
|
||||
}
|
||||
};
|
||||
|
||||
// 应用预设(配置敏感信息)
|
||||
// Apply preset (configure sensitive information)
|
||||
const handleApplyPreset = async (values?: Record<string, any>) => {
|
||||
try {
|
||||
setIsApplying(true);
|
||||
|
||||
// 使用传入的values或现有的secrets
|
||||
// Use passed values or existing secrets
|
||||
const inputValues = values || secrets;
|
||||
|
||||
// 验证所有必填项都已填写
|
||||
// Verify all required fields are filled
|
||||
if (selectedPreset?.schema && selectedPreset.schema.length > 0) {
|
||||
// 验证在 DynamicConfigForm 中已完成
|
||||
// 这里只做简单检查
|
||||
// Validation completed in DynamicConfigForm
|
||||
// 这里只做简单检查(对于 confirm 类型,false 是有效值)
|
||||
for (const input of selectedPreset.schema) {
|
||||
if (input.required !== false && !inputValues[input.id]) {
|
||||
const value = inputValues[input.id];
|
||||
const isEmpty = value === undefined || value === null || value === '' ||
|
||||
(Array.isArray(value) && value.length === 0);
|
||||
|
||||
if (input.required !== false && isEmpty) {
|
||||
setToast({ message: t('presets.please_fill_field', { field: input.label || input.id }), type: 'warning' });
|
||||
setIsApplying(false);
|
||||
return;
|
||||
@@ -353,11 +380,11 @@ export function Presets() {
|
||||
}
|
||||
}
|
||||
|
||||
await api.applyPreset(selectedPreset!.name, inputValues);
|
||||
await api.applyPreset(selectedPreset!.id, inputValues);
|
||||
setToast({ message: t('presets.preset_applied'), type: 'success' });
|
||||
setDetailDialogOpen(false);
|
||||
setSecrets({});
|
||||
// 刷新预设列表
|
||||
// Refresh presets list
|
||||
await loadPresets();
|
||||
} catch (error: any) {
|
||||
console.error('Failed to apply preset:', error);
|
||||
@@ -367,7 +394,7 @@ export function Presets() {
|
||||
}
|
||||
};
|
||||
|
||||
// 删除预设
|
||||
// Delete preset
|
||||
const handleDelete = async () => {
|
||||
if (!presetToDelete) return;
|
||||
|
||||
@@ -576,7 +603,7 @@ export function Presets() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 配置表单 */}
|
||||
{/* Configuration form */}
|
||||
{selectedPreset?.schema && selectedPreset.schema.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h4 className="font-medium text-sm mb-4">{t('presets.required_information')}</h4>
|
||||
@@ -591,11 +618,6 @@ export function Presets() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDetailDialogOpen(false)}>
|
||||
{t('presets.close')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -11,9 +12,9 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { CheckCircle2, Loader2 } from 'lucide-react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
// 类型定义
|
||||
// Type definitions
|
||||
interface InputOption {
|
||||
label: string;
|
||||
value: string | number | boolean;
|
||||
@@ -77,11 +78,12 @@ export function DynamicConfigForm({
|
||||
isSubmitting = false,
|
||||
initialValues = {},
|
||||
}: DynamicConfigFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const [values, setValues] = useState<Record<string, any>>(initialValues);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [visibleFields, setVisibleFields] = useState<Set<string>>(new Set());
|
||||
|
||||
// 计算可见字段
|
||||
// Calculate visible fields
|
||||
useEffect(() => {
|
||||
const updateVisibility = () => {
|
||||
const visible = new Set<string>();
|
||||
@@ -98,7 +100,7 @@ export function DynamicConfigForm({
|
||||
updateVisibility();
|
||||
}, [values, schema]);
|
||||
|
||||
// 评估条件
|
||||
// Evaluate condition
|
||||
const evaluateCondition = (condition: Condition): boolean => {
|
||||
const actualValue = values[condition.field];
|
||||
|
||||
@@ -132,7 +134,7 @@ export function DynamicConfigForm({
|
||||
}
|
||||
};
|
||||
|
||||
// 判断字段是否应该显示
|
||||
// Determine if field should be displayed
|
||||
const shouldShowField = (field: RequiredInput): boolean => {
|
||||
if (!field.when) {
|
||||
return true;
|
||||
@@ -142,7 +144,7 @@ export function DynamicConfigForm({
|
||||
return conditions.every(condition => evaluateCondition(condition));
|
||||
};
|
||||
|
||||
// 获取选项列表
|
||||
// Get options list
|
||||
const getOptions = (field: RequiredInput): InputOption[] => {
|
||||
if (!field.options) {
|
||||
return [];
|
||||
@@ -197,13 +199,13 @@ export function DynamicConfigForm({
|
||||
return [];
|
||||
};
|
||||
|
||||
// 更新字段值
|
||||
// Update field value
|
||||
const updateValue = (fieldId: string, value: any) => {
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
[fieldId]: value,
|
||||
}));
|
||||
// 清除该字段的错误
|
||||
// Clear errors for this field
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[fieldId];
|
||||
@@ -211,44 +213,44 @@ export function DynamicConfigForm({
|
||||
});
|
||||
};
|
||||
|
||||
// 验证单个字段
|
||||
// Validate single field
|
||||
const validateField = (field: RequiredInput): string | null => {
|
||||
const value = values[field.id];
|
||||
const fieldName = field.label || field.id;
|
||||
|
||||
// 检查必填
|
||||
if (field.required !== false && (value === undefined || value === null || value === '')) {
|
||||
return `${field.label || field.id} is required`;
|
||||
// Check required (for confirm type, false is a valid value)
|
||||
const isEmpty = value === undefined || value === null || value === '' ||
|
||||
(Array.isArray(value) && value.length === 0);
|
||||
|
||||
if (field.required !== false && isEmpty) {
|
||||
return t('presets.form.field_required', { field: fieldName });
|
||||
}
|
||||
|
||||
if (!value && field.required === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 类型检查
|
||||
if (field.type === 'number' && isNaN(Number(value))) {
|
||||
return `${field.label || field.id} must be a number`;
|
||||
// Type check
|
||||
if (field.type === 'number' && value !== '' && isNaN(Number(value))) {
|
||||
return t('presets.form.must_be_number', { field: fieldName });
|
||||
}
|
||||
|
||||
if (field.type === 'number') {
|
||||
const numValue = Number(value);
|
||||
if (field.min !== undefined && numValue < field.min) {
|
||||
return `${field.label || field.id} must be at least ${field.min}`;
|
||||
return t('presets.form.must_be_at_least', { field: fieldName, min: field.min });
|
||||
}
|
||||
if (field.max !== undefined && numValue > field.max) {
|
||||
return `${field.label || field.id} must be at most ${field.max}`;
|
||||
return t('presets.form.must_be_at_most', { field: fieldName, max: field.max });
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义验证器
|
||||
if (field.validator) {
|
||||
// Custom validator
|
||||
if (field.validator && value !== '') {
|
||||
if (field.validator instanceof RegExp) {
|
||||
if (!field.validator.test(String(value))) {
|
||||
return `${field.label || field.id} format is invalid`;
|
||||
return t('presets.form.format_invalid', { field: fieldName });
|
||||
}
|
||||
} else if (typeof field.validator === 'string') {
|
||||
const regex = new RegExp(field.validator);
|
||||
if (!regex.test(String(value))) {
|
||||
return `${field.label || field.id} format is invalid`;
|
||||
return t('presets.form.format_invalid', { field: fieldName });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,11 +258,11 @@ export function DynamicConfigForm({
|
||||
return null;
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
// Submit form
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 验证所有可见字段
|
||||
// Validate all visible fields
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
for (const field of schema) {
|
||||
@@ -338,7 +340,7 @@ export function DynamicConfigForm({
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<SelectTrigger id={`field-${field.id}`}>
|
||||
<SelectValue placeholder={field.placeholder || `Select ${label}`} />
|
||||
<SelectValue placeholder={field.placeholder || t('presets.form.select', { label })} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getOptions(field).map((option) => (
|
||||
@@ -432,19 +434,16 @@ export function DynamicConfigForm({
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
{t('app.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Applying...
|
||||
{t('presets.form.saving')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||
Apply
|
||||
</>
|
||||
t('app.save')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -43,12 +43,12 @@ export function Toast({ message, type, onClose }: ToastProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`fixed top-4 right-4 z-50 flex items-center justify-between p-4 rounded-lg border shadow-lg ${getBackgroundColor()} transition-all duration-300 ease-in-out`}>
|
||||
<div className={`fixed top-4 right-4 z-[100] flex items-center justify-between p-4 rounded-lg border shadow-lg ${getBackgroundColor()} transition-all duration-300 ease-in-out`}>
|
||||
<div className="flex items-center space-x-2">
|
||||
{getIcon()}
|
||||
<span className="text-sm font-medium">{message}</span>
|
||||
</div>
|
||||
<button
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="ml-4 text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||
>
|
||||
|
||||
@@ -285,6 +285,17 @@
|
||||
"load_market_failed": "Failed to load market presets",
|
||||
"preset_installed_config_required": "Preset installed, please complete configuration",
|
||||
"please_provide_file": "Please provide a preset file",
|
||||
"please_provide_url": "Please provide a preset URL"
|
||||
"please_provide_url": "Please provide a preset URL",
|
||||
"form": {
|
||||
"field_required": "{{field}} is required",
|
||||
"must_be_number": "{{field}} must be a number",
|
||||
"must_be_at_least": "{{field}} must be at least {{min}}",
|
||||
"must_be_at_most": "{{field}} must be at most {{max}}",
|
||||
"format_invalid": "{{field}} format is invalid",
|
||||
"select": "Select {{label}}",
|
||||
"applying": "Applying...",
|
||||
"apply": "Apply",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +285,17 @@
|
||||
"load_market_failed": "加载市场预设失败",
|
||||
"preset_installed_config_required": "预设已安装,请完成配置",
|
||||
"please_provide_file": "请提供预设文件",
|
||||
"please_provide_url": "请提供预设 URL"
|
||||
"please_provide_url": "请提供预设 URL",
|
||||
"form": {
|
||||
"field_required": "{{field}} 为必填项",
|
||||
"must_be_number": "{{field}} 必须是数字",
|
||||
"must_be_at_least": "{{field}} 至少为 {{min}}",
|
||||
"must_be_at_most": "{{field}} 最多为 {{max}}",
|
||||
"format_invalid": "{{field}} 格式无效",
|
||||
"select": "选择 {{label}}",
|
||||
"applying": "应用中...",
|
||||
"apply": "应用",
|
||||
"cancel": "取消"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user