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

@@ -6,6 +6,7 @@
*/
import { invoke } from '@tauri-apps/api/core';
import { secureStorage } from './secure-storage';
export interface EmbeddingConfig {
provider: string;
@@ -32,12 +33,18 @@ export interface EmbeddingProvider {
}
const EMBEDDING_STORAGE_KEY = 'zclaw-embedding-config';
const EMBEDDING_KEY_SECURE = 'zclaw-secure-embedding-apikey';
/**
* Load embedding config from localStorage. apiKey is NOT included;
* use loadEmbeddingApiKey() to retrieve it from secure storage.
*/
export function loadEmbeddingConfig(): EmbeddingConfig {
try {
const stored = localStorage.getItem(EMBEDDING_STORAGE_KEY);
if (stored) {
return JSON.parse(stored);
const parsed = JSON.parse(stored);
return { ...parsed, apiKey: '' };
}
} catch {
// ignore
@@ -51,14 +58,37 @@ export function loadEmbeddingConfig(): EmbeddingConfig {
};
}
/**
* Save embedding config to localStorage. API key is NOT saved here;
* use saveEmbeddingApiKey() separately.
*/
export function saveEmbeddingConfig(config: EmbeddingConfig): void {
try {
localStorage.setItem(EMBEDDING_STORAGE_KEY, JSON.stringify(config));
const { apiKey: _, ...rest } = config;
localStorage.setItem(EMBEDDING_STORAGE_KEY, JSON.stringify(rest));
} catch {
// ignore
}
}
/**
* Load embedding API key from secure storage.
*/
export async function loadEmbeddingApiKey(): Promise<string | null> {
return secureStorage.get(EMBEDDING_KEY_SECURE);
}
/**
* Save embedding API key to secure storage.
*/
export 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());
}
export async function getEmbeddingProviders(): Promise<EmbeddingProvider[]> {
const result = await invoke<[string, string, string, number][]>('embedding_providers');
return result.map(([id, name, defaultModel, dimensions]) => ({
@@ -75,7 +105,9 @@ export async function createEmbedding(
): Promise<EmbeddingResponse> {
const savedConfig = loadEmbeddingConfig();
const provider = config?.provider ?? savedConfig.provider;
const apiKey = config?.apiKey ?? savedConfig.apiKey;
// Resolve apiKey: use explicit config value, then secure storage, then empty
const explicitKey = config?.apiKey?.trim();
const apiKey = explicitKey || await loadEmbeddingApiKey() || '';
const model = config?.model ?? savedConfig.model;
const endpoint = config?.endpoint ?? savedConfig.endpoint;
@@ -136,6 +168,8 @@ export class EmbeddingClient {
constructor(config?: EmbeddingConfig) {
this.config = config ?? loadEmbeddingConfig();
// If no explicit apiKey was provided and config was loaded from localStorage,
// the apiKey will be empty. It will be resolved from secure storage lazily.
}
get isApiMode(): boolean {
@@ -143,7 +177,11 @@ export class EmbeddingClient {
}
async embed(text: string): Promise<number[]> {
const response = await createEmbedding(text, this.config);
// Resolve apiKey from secure storage if not in config
const effectiveConfig = this.config.apiKey
? this.config
: { ...this.config, apiKey: await loadEmbeddingApiKey() ?? '' };
const response = await createEmbedding(text, effectiveConfig);
return response.embedding;
}
@@ -161,7 +199,12 @@ export class EmbeddingClient {
if (config.provider !== undefined || config.apiKey !== undefined) {
this.config.enabled = this.config.provider !== 'local' && !!this.config.apiKey;
}
// Save non-key fields to localStorage
saveEmbeddingConfig(this.config);
// Save apiKey to secure storage (fire-and-forget)
if (config.apiKey !== undefined) {
saveEmbeddingApiKey(config.apiKey).catch(() => {});
}
}
getConfig(): EmbeddingConfig {