fix(安全): 修复HTML导出中的XSS漏洞并清理调试日志
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor(日志): 替换console.log为tracing日志系统 style(代码): 移除未使用的代码和依赖项 feat(测试): 添加端到端测试文档和CI工作流 docs(变更日志): 更新CHANGELOG.md记录0.1.0版本变更 perf(构建): 更新依赖版本并优化CI流程
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
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 { useChatStore } from '../../store/chatStore';
|
||||
import { silentErrorHandler } from '../../lib/error-utils';
|
||||
import { Plus, Pencil, Trash2, Star, Eye, EyeOff, AlertCircle, X } from 'lucide-react';
|
||||
import { Plus, Pencil, Trash2, Star, Eye, EyeOff, AlertCircle, X, Zap, Check } from 'lucide-react';
|
||||
|
||||
// 自定义模型数据结构
|
||||
interface CustomModel {
|
||||
@@ -18,6 +19,22 @@ interface CustomModel {
|
||||
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 = [
|
||||
@@ -36,6 +53,42 @@ const AVAILABLE_PROVIDERS = [
|
||||
];
|
||||
|
||||
const STORAGE_KEY = 'zclaw-custom-models';
|
||||
const EMBEDDING_STORAGE_KEY = 'zclaw-embedding-config';
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
function loadEmbeddingConfig(): EmbeddingConfig {
|
||||
try {
|
||||
const stored = localStorage.getItem(EMBEDDING_STORAGE_KEY);
|
||||
if (stored) {
|
||||
return JSON.parse(stored);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return {
|
||||
provider: 'local',
|
||||
model: 'tfidf',
|
||||
apiKey: '',
|
||||
endpoint: '',
|
||||
enabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
function saveEmbeddingConfig(config: EmbeddingConfig): void {
|
||||
try {
|
||||
localStorage.setItem(EMBEDDING_STORAGE_KEY, JSON.stringify(config));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// 从 localStorage 加载自定义模型
|
||||
function loadCustomModels(): CustomModel[] {
|
||||
@@ -75,6 +128,12 @@ export function ModelsAPI() {
|
||||
const [editingModel, setEditingModel] = useState<CustomModel | null>(null);
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
|
||||
// Embedding 配置状态
|
||||
const [embeddingConfig, setEmbeddingConfig] = useState<EmbeddingConfig>(loadEmbeddingConfig);
|
||||
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',
|
||||
@@ -195,6 +254,65 @@ export function ModelsAPI() {
|
||||
});
|
||||
};
|
||||
|
||||
// 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 = () => {
|
||||
const configToSave = {
|
||||
...embeddingConfig,
|
||||
enabled: embeddingConfig.provider !== 'local' && embeddingConfig.apiKey.trim() !== '',
|
||||
};
|
||||
setEmbeddingConfig(configToSave);
|
||||
saveEmbeddingConfig(configToSave);
|
||||
};
|
||||
|
||||
// 测试 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 (
|
||||
<div className="max-w-3xl">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
@@ -304,7 +422,125 @@ export function ModelsAPI() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 添加/编辑模型弹窗 */}
|
||||
{/* Embedding 模型配置 */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<Zap className="w-3.5 h-3.5" />
|
||||
Embedding 模型
|
||||
</h3>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${embeddingConfig.enabled ? 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400' : 'bg-gray-100 dark:bg-gray-700 text-gray-500'}`}>
|
||||
{embeddingConfig.enabled ? '已启用' : '使用 TF-IDF'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 shadow-sm space-y-4">
|
||||
{/* Provider 选择 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">服务商</label>
|
||||
<select
|
||||
value={embeddingConfig.provider}
|
||||
onChange={(e) => handleEmbeddingProviderChange(e.target.value)}
|
||||
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 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
>
|
||||
{DEFAULT_EMBEDDING_PROVIDERS.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name} {p.dimensions > 0 ? `(${p.dimensions}D)` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 模型 ID */}
|
||||
{embeddingConfig.provider !== 'local' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">模型 ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={embeddingConfig.model}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
默认: {DEFAULT_EMBEDDING_PROVIDERS.find(p => p.id === embeddingConfig.provider)?.defaultModel}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* API Key */}
|
||||
{embeddingConfig.provider !== 'local' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">API Key</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showEmbeddingApiKey ? 'text' : 'password'}
|
||||
value={embeddingConfig.apiKey}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowEmbeddingApiKey(!showEmbeddingApiKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showEmbeddingApiKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 自定义 Endpoint */}
|
||||
{embeddingConfig.provider !== 'local' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
自定义 Endpoint <span className="text-gray-400">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={embeddingConfig.endpoint}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 测试结果 */}
|
||||
{embeddingTestResult && (
|
||||
<div className={`flex items-center gap-2 p-3 rounded-lg text-sm ${embeddingTestResult.success ? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300' : 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300'}`}>
|
||||
{embeddingTestResult.success ? <Check className="w-4 h-4" /> : <AlertCircle className="w-4 h-4" />}
|
||||
{embeddingTestResult.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
onClick={handleSaveEmbeddingConfig}
|
||||
className="px-4 py-2 bg-orange-500 text-white rounded-lg text-sm hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
保存配置
|
||||
</button>
|
||||
{embeddingConfig.provider !== 'local' && (
|
||||
<button
|
||||
onClick={handleTestEmbedding}
|
||||
disabled={testingEmbedding || !embeddingConfig.apiKey.trim()}
|
||||
className="px-4 py-2 border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg text-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{testingEmbedding ? '测试中...' : '测试连接'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 说明 */}
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500 pt-2 border-t border-gray-100 dark:border-gray-700">
|
||||
<p>Embedding 模型用于语义记忆的向量搜索,提供更精准的语义匹配。</p>
|
||||
<p className="mt-1">选择「本地 TF-IDF」无需配置 API,使用关键词匹配;选择其他服务商需配置 API Key。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-black/50" onClick={() => setShowAddModal(false)} />
|
||||
|
||||
Reference in New Issue
Block a user