import { useState, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { getStoredGatewayToken, getStoredGatewayUrl } from '../../lib/gateway-client'; import { useConnectionStore } from '../../store/connectionStore'; import { useConfigStore } from '../../store/configStore'; import { useConversationStore } from '../../store/chat/conversationStore'; import { silentErrorHandler } from '../../lib/error-utils'; import { secureStorage } from '../../lib/secure-storage'; import { Plus, Pencil, Trash2, Star, Eye, EyeOff, AlertCircle, X, Zap, Check } from 'lucide-react'; // 自定义模型数据结构 interface CustomModel { id: string; name: string; provider: string; apiKey?: string; apiProtocol: 'openai' | 'anthropic' | 'custom'; baseUrl?: string; isDefault?: boolean; createdAt: string; } // Embedding 配置数据结构 interface EmbeddingConfig { provider: string; model: string; apiKey: string; endpoint: string; enabled: boolean; } interface EmbeddingProvider { id: string; name: string; defaultModel: string; dimensions: number; } // 可用的 Provider 列表 // 注意: Coding Plan 是专为编程助手设计的优惠套餐,使用专用端点 const AVAILABLE_PROVIDERS = [ // === Coding Plan 专用端点 (推荐用于编程场景) === { id: 'kimi-coding', name: 'Kimi Coding Plan', baseUrl: 'https://api.kimi.com/coding/v1' }, { id: 'qwen-coding', name: '百炼 Coding Plan', baseUrl: 'https://coding.dashscope.aliyuncs.com/v1' }, { id: 'zhipu-coding', name: '智谱 GLM Coding Plan', baseUrl: 'https://open.bigmodel.cn/api/coding/paas/v4' }, // === 标准 API 端点 === { id: 'kimi', name: 'Kimi (标准 API)', baseUrl: 'https://api.moonshot.cn/v1' }, { id: 'zhipu', name: '智谱 (标准 API)', baseUrl: 'https://open.bigmodel.cn/api/paas/v4' }, { id: 'qwen', name: '百炼/通义千问 (标准)', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1' }, { id: 'deepseek', name: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1' }, { id: 'openai', name: 'OpenAI', baseUrl: 'https://api.openai.com/v1' }, { id: 'anthropic', name: 'Anthropic', baseUrl: 'https://api.anthropic.com' }, { id: 'custom', name: '自定义', baseUrl: '' }, ]; const STORAGE_KEY = 'zclaw-custom-models'; const MODEL_KEY_SECURE_PREFIX = 'zclaw-secure-model-key:'; const EMBEDDING_STORAGE_KEY = 'zclaw-embedding-config'; const EMBEDDING_KEY_SECURE = 'zclaw-secure-embedding-apikey'; const DEFAULT_EMBEDDING_PROVIDERS: EmbeddingProvider[] = [ { id: 'local', name: '本地 TF-IDF (无需 API)', defaultModel: 'tfidf', dimensions: 0 }, { id: 'openai', name: 'OpenAI', defaultModel: 'text-embedding-3-small', dimensions: 1536 }, { id: 'zhipu', name: '智谱 AI', defaultModel: 'embedding-3', dimensions: 1024 }, { id: 'doubao', name: '火山引擎 (Doubao)', defaultModel: 'doubao-embedding', dimensions: 1024 }, { id: 'qwen', name: '百炼/通义千问', defaultModel: 'text-embedding-v3', dimensions: 1024 }, { id: 'deepseek', name: 'DeepSeek', defaultModel: 'deepseek-embedding', dimensions: 1536 }, ]; /** * Load embedding config from localStorage. apiKey will be empty here; * call loadEmbeddingApiKey() to retrieve it from secure storage. */ function loadEmbeddingConfigBase(): Omit & { apiKey: string } { try { const stored = localStorage.getItem(EMBEDDING_STORAGE_KEY); if (stored) { const parsed = JSON.parse(stored); return { ...parsed, apiKey: '' }; } } catch { // ignore } return { provider: 'local', model: 'tfidf', apiKey: '', endpoint: '', enabled: false, }; } /** * Save embedding config to localStorage. API key is NOT saved here; * use saveEmbeddingApiKey() separately. */ function saveEmbeddingConfigBase(config: Omit): void { try { localStorage.setItem(EMBEDDING_STORAGE_KEY, JSON.stringify(config)); } catch { // ignore } } /** * Save embedding API key to secure storage. */ async function saveEmbeddingApiKey(apiKey: string): Promise { if (!apiKey.trim()) { await secureStorage.delete(EMBEDDING_KEY_SECURE); return; } await secureStorage.set(EMBEDDING_KEY_SECURE, apiKey.trim()); } /** * Load embedding API key from secure storage. */ async function loadEmbeddingApiKey(): Promise { return secureStorage.get(EMBEDDING_KEY_SECURE); } // 从 localStorage 加载自定义模型 (apiKeys are stripped from localStorage) function loadCustomModelsBase(): CustomModel[] { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { return JSON.parse(stored); } } catch { // ignore } return []; } // 保存自定义模型到 localStorage (apiKeys are stripped before saving) function saveCustomModelsBase(models: CustomModel[]): void { try { const sanitized = models.map(m => { const { apiKey: _, ...rest } = m; return rest; }); localStorage.setItem(STORAGE_KEY, JSON.stringify(sanitized)); } catch { // ignore } } /** * Async load: fetches models from localStorage and merges apiKeys from secure storage. */ async function loadCustomModelsWithKeys(): Promise { const models = loadCustomModelsBase(); const modelsWithKeys = await Promise.all( models.map(async (model) => { const apiKey = await secureStorage.get(MODEL_KEY_SECURE_PREFIX + model.id); return { ...model, apiKey: apiKey || undefined }; }) ); return modelsWithKeys; } export function ModelsAPI() { const connectionState = useConnectionStore((s) => s.connectionState); const connect = useConnectionStore((s) => s.connect); const disconnect = useConnectionStore((s) => s.disconnect); const quickConfig = useConfigStore((s) => s.quickConfig); const loadModels = useConfigStore((s) => s.loadModels); const currentModel = useConversationStore((s) => s.currentModel); const setCurrentModel = useConversationStore((s) => s.setCurrentModel); const [gatewayUrl, setGatewayUrl] = useState(getStoredGatewayUrl()); const [gatewayToken, setGatewayToken] = useState(quickConfig.gatewayToken || getStoredGatewayToken()); // 自定义模型状态 const [customModels, setCustomModels] = useState([]); const [showAddModal, setShowAddModal] = useState(false); const [editingModel, setEditingModel] = useState(null); const [showApiKey, setShowApiKey] = useState(false); // Embedding 配置状态 const [embeddingConfig, setEmbeddingConfig] = useState(loadEmbeddingConfigBase); const [showEmbeddingApiKey, setShowEmbeddingApiKey] = useState(false); const [testingEmbedding, setTestingEmbedding] = useState(false); const [embeddingTestResult, setEmbeddingTestResult] = useState<{ success: boolean; message: string } | null>(null); // 表单状态 const [formData, setFormData] = useState({ provider: 'zhipu', modelId: 'glm-4-flash', displayName: '', apiKey: '', apiProtocol: 'openai' as 'openai' | 'anthropic' | 'custom', baseUrl: '', }); const connected = connectionState === 'connected'; const connecting = connectionState === 'connecting' || connectionState === 'reconnecting'; // 加载自定义模型和 embedding API key (async for secure storage) useEffect(() => { const loadAll = async () => { // Load custom models with their secure apiKeys const modelsWithKeys = await loadCustomModelsWithKeys(); setCustomModels(modelsWithKeys); // Load embedding apiKey from secure storage const embApiKey = await loadEmbeddingApiKey(); if (embApiKey) { setEmbeddingConfig(prev => ({ ...prev, apiKey: embApiKey })); } }; loadAll().catch(silentErrorHandler('ModelsAPI')); }, []); useEffect(() => { setGatewayUrl(quickConfig.gatewayUrl || getStoredGatewayUrl()); setGatewayToken(quickConfig.gatewayToken || getStoredGatewayToken()); }, [quickConfig.gatewayToken, quickConfig.gatewayUrl]); const handleReconnect = () => { disconnect(); setTimeout(() => connect( gatewayUrl || quickConfig.gatewayUrl || 'ws://127.0.0.1:50051/ws', gatewayToken || quickConfig.gatewayToken || getStoredGatewayToken() ).catch(silentErrorHandler('ModelsAPI')), 500); }; // 打开添加模型弹窗 const handleOpenAddModal = () => { setFormData({ provider: 'zhipu', modelId: '', displayName: '', apiKey: '', apiProtocol: 'openai', baseUrl: AVAILABLE_PROVIDERS[0].baseUrl, }); setEditingModel(null); setShowAddModal(true); }; // 打开编辑模型弹窗 const handleOpenEditModal = (model: CustomModel) => { setFormData({ provider: model.provider, modelId: model.id, displayName: model.name, apiKey: model.apiKey || '', apiProtocol: model.apiProtocol, baseUrl: model.baseUrl || '', }); setEditingModel(model); setShowAddModal(true); }; // 保存模型 const handleSaveModel = async () => { if (!formData.modelId.trim()) return; const newModel: CustomModel = { id: formData.modelId.trim(), name: formData.displayName.trim() || formData.modelId.trim(), provider: formData.provider, apiKey: formData.apiKey.trim() || undefined, apiProtocol: formData.apiProtocol, baseUrl: formData.baseUrl.trim() || AVAILABLE_PROVIDERS.find(p => p.id === formData.provider)?.baseUrl, createdAt: editingModel?.createdAt || new Date().toISOString(), }; let updatedModels: CustomModel[]; if (editingModel) { // 编辑模式 updatedModels = customModels.map(m => m.id === editingModel.id ? newModel : m); } else { // 添加模式 updatedModels = [...customModels, newModel]; } // Save apiKey to secure storage if (newModel.apiKey) { await secureStorage.set(MODEL_KEY_SECURE_PREFIX + newModel.id, newModel.apiKey); } else { await secureStorage.delete(MODEL_KEY_SECURE_PREFIX + newModel.id); } setCustomModels(updatedModels); saveCustomModelsBase(updatedModels); setShowAddModal(false); setEditingModel(null); // 刷新模型列表 loadModels(); }; // 删除模型 const handleDeleteModel = async (modelId: string) => { const updatedModels = customModels.filter(m => m.id !== modelId); setCustomModels(updatedModels); saveCustomModelsBase(updatedModels); // Also remove apiKey from secure storage await secureStorage.delete(MODEL_KEY_SECURE_PREFIX + modelId); }; // 设为默认模型 const handleSetDefault = (modelId: string) => { setCurrentModel(modelId); // 更新自定义模型的默认状态 const updatedModels = customModels.map(m => ({ ...m, isDefault: m.id === modelId, })); setCustomModels(updatedModels); saveCustomModelsBase(updatedModels); }; // Provider 变更时更新 baseUrl const handleProviderChange = (providerId: string) => { const provider = AVAILABLE_PROVIDERS.find(p => p.id === providerId); setFormData({ ...formData, provider: providerId, baseUrl: provider?.baseUrl || '', }); }; // Embedding Provider 变更 const handleEmbeddingProviderChange = (providerId: string) => { const provider = DEFAULT_EMBEDDING_PROVIDERS.find(p => p.id === providerId); setEmbeddingConfig(prev => ({ ...prev, provider: providerId, model: provider?.defaultModel || 'tfidf', })); setEmbeddingTestResult(null); }; // 保存 Embedding 配置 const handleSaveEmbeddingConfig = async () => { const configToSave = { ...embeddingConfig, enabled: embeddingConfig.provider !== 'local' && embeddingConfig.apiKey.trim() !== '', }; setEmbeddingConfig(configToSave); // Save apiKey to secure storage, rest to localStorage await saveEmbeddingApiKey(configToSave.apiKey); saveEmbeddingConfigBase(configToSave); // Push config to Rust backend for semantic memory search if (configToSave.enabled) { try { await invoke('viking_configure_embedding', { provider: configToSave.provider, apiKey: configToSave.apiKey, model: configToSave.model || undefined, endpoint: configToSave.endpoint || undefined, }); setEmbeddingTestResult({ success: true, message: 'Embedding 配置已应用到语义记忆搜索' }); } catch (error) { setEmbeddingTestResult({ success: false, message: `配置保存成功但应用失败: ${error}` }); } } else { setEmbeddingTestResult(null); } }; // 测试 Embedding API const handleTestEmbedding = async () => { if (embeddingConfig.provider === 'local') { setEmbeddingTestResult({ success: true, message: '本地 TF-IDF 模式无需测试' }); return; } if (!embeddingConfig.apiKey.trim()) { setEmbeddingTestResult({ success: false, message: '请先填写 API Key' }); return; } setTestingEmbedding(true); setEmbeddingTestResult(null); try { const result = await invoke<{ embedding: number[]; model: string }>('embedding_create', { provider: embeddingConfig.provider, apiKey: embeddingConfig.apiKey, text: '测试文本', model: embeddingConfig.model || undefined, endpoint: embeddingConfig.endpoint || undefined, }); setEmbeddingTestResult({ success: true, message: `成功!向量维度: ${result.embedding.length}`, }); } catch (error) { setEmbeddingTestResult({ success: false, message: String(error), }); } finally { setTestingEmbedding(false); } }; return (

模型与 API

{/* Gateway 连接状态 */}

Gateway 连接

连接状态 {connected ? '已连接' : connecting ? '连接中...' : '未连接'}
当前模型 {currentModel || '未选择'}
{/* 内置模型 */}

内置模型

ZCLAW 默认模型 由 Gateway 配置决定
{/* 自定义模型 */}

自定义模型

{customModels.length === 0 ? (

暂无自定义模型

点击上方按钮添加你的第一个自定义模型

) : (
{customModels.map((model) => (
{model.name} {currentModel === model.id && ( 当前 )}
{AVAILABLE_PROVIDERS.find(p => p.id === model.provider)?.name || model.provider} {model.apiKey ? ' · 已配置 API Key' : ' · 未配置 API Key'}
{currentModel !== model.id && ( )}
))}
)}
{/* Embedding 模型配置 */}

Embedding 模型

{embeddingConfig.enabled ? '已启用' : '使用 TF-IDF'}
{/* Provider 选择 */}
{/* 模型 ID */} {embeddingConfig.provider !== 'local' && (
setEmbeddingConfig(prev => ({ ...prev, model: e.target.value }))} placeholder="text-embedding-3-small" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500" />

默认: {DEFAULT_EMBEDDING_PROVIDERS.find(p => p.id === embeddingConfig.provider)?.defaultModel}

)} {/* API Key */} {embeddingConfig.provider !== 'local' && (
setEmbeddingConfig(prev => ({ ...prev, apiKey: e.target.value }))} placeholder="请填写 API Key" className="w-full px-3 py-2 pr-10 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500" />
)} {/* 自定义 Endpoint */} {embeddingConfig.provider !== 'local' && (
setEmbeddingConfig(prev => ({ ...prev, endpoint: e.target.value }))} placeholder="留空使用默认端点" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500" />
)} {/* 测试结果 */} {embeddingTestResult && (
{embeddingTestResult.success ? : } {embeddingTestResult.message}
)} {/* 操作按钮 */}
{embeddingConfig.provider !== 'local' && ( )}
{/* 说明 */}

Embedding 模型用于语义记忆的向量搜索,提供更精准的语义匹配。

选择「本地 TF-IDF」无需配置 API,使用关键词匹配;选择其他服务商需配置 API Key。

{showAddModal && (
setShowAddModal(false)} />
{/* 弹窗头部 */}

{editingModel ? '编辑模型' : '添加模型'}

{/* 弹窗内容 */}
{/* 警告提示 */}
添加外部模型即表示你理解并同意自行承担使用风险。
{/* 服务商 */}
{/* 模型 ID */}
setFormData({ ...formData, modelId: e.target.value })} placeholder="如:glm-4-flash, glm-4-plus, glm-4.5" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500" />

智谱: glm-4-flash(免费), glm-4-plus, glm-4.5, glm-4.6

{/* 显示名称 */}
setFormData({ ...formData, displayName: e.target.value })} placeholder="如:GLM-4-Plus" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500" />
{/* API Key */}
setFormData({ ...formData, apiKey: e.target.value })} placeholder="请填写 API Key" className="w-full px-3 py-2 pr-10 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500" />
{/* API 协议 */}
{/* Base URL */}
setFormData({ ...formData, baseUrl: e.target.value })} placeholder="https://api.example.com/v1" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500" />
{/* 弹窗底部 */}
)}
); }