chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -5,6 +5,7 @@ import { useConnectionStore } from '../../store/connectionStore';
import { useConfigStore } from '../../store/configStore';
import { useChatStore } from '../../store/chatStore';
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';
// 自定义模型数据结构
@@ -53,7 +54,9 @@ const AVAILABLE_PROVIDERS = [
];
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 },
@@ -64,11 +67,16 @@ const DEFAULT_EMBEDDING_PROVIDERS: EmbeddingProvider[] = [
{ id: 'deepseek', name: 'DeepSeek', defaultModel: 'deepseek-embedding', dimensions: 1536 },
];
function loadEmbeddingConfig(): EmbeddingConfig {
/**
* Load embedding config from localStorage. apiKey will be empty here;
* call loadEmbeddingApiKey() to retrieve it from secure storage.
*/
function loadEmbeddingConfigBase(): Omit<EmbeddingConfig, 'apiKey'> & { apiKey: string } {
try {
const stored = localStorage.getItem(EMBEDDING_STORAGE_KEY);
if (stored) {
return JSON.parse(stored);
const parsed = JSON.parse(stored);
return { ...parsed, apiKey: '' };
}
} catch {
// ignore
@@ -82,7 +90,11 @@ function loadEmbeddingConfig(): EmbeddingConfig {
};
}
function saveEmbeddingConfig(config: EmbeddingConfig): void {
/**
* Save embedding config to localStorage. API key is NOT saved here;
* use saveEmbeddingApiKey() separately.
*/
function saveEmbeddingConfigBase(config: Omit<EmbeddingConfig, 'apiKey'>): void {
try {
localStorage.setItem(EMBEDDING_STORAGE_KEY, JSON.stringify(config));
} catch {
@@ -90,8 +102,26 @@ function saveEmbeddingConfig(config: EmbeddingConfig): void {
}
}
// 从 localStorage 加载自定义模型
function loadCustomModels(): CustomModel[] {
/**
* Save embedding API key to secure storage.
*/
async function saveEmbeddingApiKey(apiKey: string): Promise<void> {
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<string | null> {
return secureStorage.get(EMBEDDING_KEY_SECURE);
}
// 从 localStorage 加载自定义模型 (apiKeys are stripped from localStorage)
function loadCustomModelsBase(): CustomModel[] {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
@@ -103,15 +133,33 @@ function loadCustomModels(): CustomModel[] {
return [];
}
// 保存自定义模型到 localStorage
function saveCustomModels(models: CustomModel[]): void {
// 保存自定义模型到 localStorage (apiKeys are stripped before saving)
function saveCustomModelsBase(models: CustomModel[]): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(models));
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<CustomModel[]> {
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);
@@ -129,7 +177,7 @@ export function ModelsAPI() {
const [showApiKey, setShowApiKey] = useState(false);
// Embedding 配置状态
const [embeddingConfig, setEmbeddingConfig] = useState<EmbeddingConfig>(loadEmbeddingConfig);
const [embeddingConfig, setEmbeddingConfig] = useState<EmbeddingConfig>(loadEmbeddingConfigBase);
const [showEmbeddingApiKey, setShowEmbeddingApiKey] = useState(false);
const [testingEmbedding, setTestingEmbedding] = useState(false);
const [embeddingTestResult, setEmbeddingTestResult] = useState<{ success: boolean; message: string } | null>(null);
@@ -147,9 +195,20 @@ export function ModelsAPI() {
const connected = connectionState === 'connected';
const connecting = connectionState === 'connecting' || connectionState === 'reconnecting';
// 加载自定义模型
// 加载自定义模型和 embedding API key (async for secure storage)
useEffect(() => {
setCustomModels(loadCustomModels());
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(() => {
@@ -194,14 +253,14 @@ export function ModelsAPI() {
};
// 保存模型
const handleSaveModel = () => {
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(),
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(),
@@ -216,8 +275,15 @@ export function ModelsAPI() {
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);
saveCustomModels(updatedModels);
saveCustomModelsBase(updatedModels);
setShowAddModal(false);
setEditingModel(null);
@@ -226,10 +292,12 @@ export function ModelsAPI() {
};
// 删除模型
const handleDeleteModel = (modelId: string) => {
const handleDeleteModel = async (modelId: string) => {
const updatedModels = customModels.filter(m => m.id !== modelId);
setCustomModels(updatedModels);
saveCustomModels(updatedModels);
saveCustomModelsBase(updatedModels);
// Also remove apiKey from secure storage
await secureStorage.delete(MODEL_KEY_SECURE_PREFIX + modelId);
};
// 设为默认模型
@@ -241,7 +309,7 @@ export function ModelsAPI() {
isDefault: m.id === modelId,
}));
setCustomModels(updatedModels);
saveCustomModels(updatedModels);
saveCustomModelsBase(updatedModels);
};
// Provider 变更时更新 baseUrl
@@ -272,7 +340,10 @@ export function ModelsAPI() {
enabled: embeddingConfig.provider !== 'local' && embeddingConfig.apiKey.trim() !== '',
};
setEmbeddingConfig(configToSave);
saveEmbeddingConfig(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) {