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

refactor(日志): 替换console.log为tracing日志系统
style(代码): 移除未使用的代码和依赖项

feat(测试): 添加端到端测试文档和CI工作流
docs(变更日志): 更新CHANGELOG.md记录0.1.0版本变更

perf(构建): 更新依赖版本并优化CI流程
This commit is contained in:
iven
2026-03-26 19:49:03 +08:00
parent b8d565a9eb
commit 978dc5cdd8
79 changed files with 3953 additions and 5724 deletions

View File

@@ -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)} />