chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user