feat(desktop): integrate SaaS llm_routing, template API, and onboarding template selection
- Add AgentTemplateAvailable/AgentTemplateFull types and fetchAvailableTemplates/fetchTemplateFull API methods to saas-client - Add llm_routing field to SaaSAccountInfo for admin-configured routing priority - Add availableTemplates state and fetchAvailableTemplates action to saasStore with background fetch on login - Add admin llm_routing priority check in connectionStore connect() to force relay or local mode - Add createFromTemplate action to agentStore with SOUL.md persistence - Add Step 0 template selection to AgentOnboardingWizard with grid layout for template browsing
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import type { GatewayClient } from '../lib/gateway-client';
|
||||
import type { AgentTemplateFull } from '../lib/saas-client';
|
||||
import { useChatStore } from './chatStore';
|
||||
|
||||
// === Types ===
|
||||
@@ -33,6 +34,7 @@ export interface Clone {
|
||||
communicationStyle?: string; // 沟通风格描述
|
||||
notes?: string; // 用户备注
|
||||
onboardingCompleted?: boolean; // 是否完成首次引导
|
||||
source_template_id?: string; // 模板来源 ID
|
||||
}
|
||||
|
||||
export interface UsageStats {
|
||||
@@ -83,6 +85,7 @@ export interface AgentStateSlice {
|
||||
export interface AgentActionsSlice {
|
||||
loadClones: () => Promise<void>;
|
||||
createClone: (opts: CloneCreateOptions) => Promise<Clone | undefined>;
|
||||
createFromTemplate: (template: AgentTemplateFull) => Promise<Clone | undefined>;
|
||||
updateClone: (id: string, updates: Partial<Clone>) => Promise<Clone | undefined>;
|
||||
deleteClone: (id: string) => Promise<void>;
|
||||
loadUsageStats: () => Promise<void>;
|
||||
@@ -173,6 +176,43 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
createFromTemplate: async (template: AgentTemplateFull) => {
|
||||
const client = getClient();
|
||||
if (!client) return undefined;
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const result = await client.createClone({
|
||||
name: template.name,
|
||||
emoji: template.emoji,
|
||||
personality: template.personality,
|
||||
scenarios: template.scenarios,
|
||||
communicationStyle: template.communication_style,
|
||||
model: template.model,
|
||||
});
|
||||
|
||||
const cloneId = result?.clone?.id;
|
||||
|
||||
// Persist SOUL.md via identity system
|
||||
if (cloneId && template.soul_content) {
|
||||
try {
|
||||
const { intelligenceClient } = await import('../lib/intelligence-client');
|
||||
await intelligenceClient.identity.updateFile(cloneId, 'soul', template.soul_content);
|
||||
} catch (e) {
|
||||
console.warn('Failed to persist soul_content:', e);
|
||||
}
|
||||
}
|
||||
|
||||
await get().loadClones();
|
||||
return result?.clone;
|
||||
} catch (error) {
|
||||
set({ error: String(error) });
|
||||
return undefined;
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
updateClone: async (id: string, updates: Partial<Clone>) => {
|
||||
const client = getClient();
|
||||
if (!client) {
|
||||
|
||||
@@ -350,6 +350,70 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
try {
|
||||
set({ error: null });
|
||||
|
||||
// === Admin Routing Priority ===
|
||||
// Admin-configured llm_routing takes priority over localStorage connectionMode.
|
||||
// This allows admins to force all clients to use relay or local mode.
|
||||
let adminForceLocal = false;
|
||||
try {
|
||||
const storedAccount = JSON.parse(localStorage.getItem('zclaw-saas-account') || '{}');
|
||||
const adminRouting = storedAccount?.account?.llm_routing;
|
||||
|
||||
if (adminRouting === 'relay') {
|
||||
// Force SaaS Relay mode — admin override
|
||||
// Set connection mode to 'saas' so the SaaS relay section below activates
|
||||
localStorage.setItem('zclaw-connection-mode', 'saas');
|
||||
log.debug('Admin llm_routing=relay: forcing SaaS relay mode');
|
||||
} else if (adminRouting === 'local' && isTauriRuntime()) {
|
||||
// Force local Kernel mode — skip SaaS relay entirely
|
||||
adminForceLocal = true;
|
||||
localStorage.setItem('zclaw-connection-mode', 'tauri');
|
||||
log.debug('Admin llm_routing=local: forcing local Kernel mode');
|
||||
}
|
||||
} catch { /* ignore parse errors, fall through to default logic */ }
|
||||
|
||||
// === Internal Kernel Mode: Admin forced local ===
|
||||
// If admin forced local mode, skip directly to Tauri Kernel section
|
||||
if (adminForceLocal) {
|
||||
const kernelClient = getKernelClient();
|
||||
const modelConfig = await getDefaultModelConfigAsync();
|
||||
|
||||
if (!modelConfig) {
|
||||
throw new Error('请先在"模型与 API"设置页面添加自定义模型配置');
|
||||
}
|
||||
|
||||
if (!modelConfig.apiKey) {
|
||||
throw new Error(`模型 ${modelConfig.model} 未配置 API Key,请在"模型与 API"设置页面配置`);
|
||||
}
|
||||
|
||||
kernelClient.setConfig({
|
||||
provider: modelConfig.provider,
|
||||
model: modelConfig.model,
|
||||
apiKey: modelConfig.apiKey,
|
||||
baseUrl: modelConfig.baseUrl,
|
||||
apiProtocol: modelConfig.apiProtocol,
|
||||
});
|
||||
|
||||
kernelClient.onStateChange = (state: ConnectionState) => {
|
||||
set({ connectionState: state });
|
||||
};
|
||||
|
||||
kernelClient.onLog = (level, message) => {
|
||||
set((s) => ({
|
||||
logs: [...s.logs.slice(-99), { timestamp: Date.now(), level, message }],
|
||||
}));
|
||||
};
|
||||
|
||||
set({ client: kernelClient });
|
||||
|
||||
const { initializeStores } = await import('./index');
|
||||
initializeStores();
|
||||
|
||||
await kernelClient.connect();
|
||||
set({ gatewayVersion: '0.1.0-internal' });
|
||||
log.debug('Connected to internal ZCLAW Kernel (admin forced local)');
|
||||
return;
|
||||
}
|
||||
|
||||
// === SaaS Relay Mode ===
|
||||
// Check connection mode from localStorage (set by saasStore).
|
||||
// When SaaS is unreachable, gracefully degrade to local kernel mode
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
type SaaSLoginResponse,
|
||||
type TotpSetupResponse,
|
||||
type SyncConfigRequest,
|
||||
type AgentTemplateAvailable,
|
||||
} from '../lib/saas-client';
|
||||
import { createLogger } from '../lib/logger';
|
||||
import {
|
||||
@@ -70,6 +71,8 @@ export interface SaaSStateSlice {
|
||||
totpSetupData: TotpSetupResponse | null;
|
||||
/** Whether SaaS backend is currently reachable */
|
||||
saasReachable: boolean;
|
||||
/** Agent templates available for onboarding */
|
||||
availableTemplates: AgentTemplateAvailable[];
|
||||
/** Consecutive heartbeat/health-check failures */
|
||||
_consecutiveFailures: number;
|
||||
_heartbeatTimer?: ReturnType<typeof setInterval>;
|
||||
@@ -86,6 +89,7 @@ export interface SaaSActionsSlice {
|
||||
syncConfigFromSaaS: () => Promise<void>;
|
||||
pushConfigToSaaS: () => Promise<void>;
|
||||
registerCurrentDevice: () => Promise<void>;
|
||||
fetchAvailableTemplates: () => Promise<void>;
|
||||
clearError: () => void;
|
||||
restoreSession: () => void;
|
||||
setupTotp: () => Promise<TotpSetupResponse>;
|
||||
@@ -140,6 +144,7 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
|
||||
totpRequired: false,
|
||||
totpSetupData: null,
|
||||
saasReachable: true,
|
||||
availableTemplates: [],
|
||||
_consecutiveFailures: 0,
|
||||
|
||||
// === Actions ===
|
||||
@@ -189,6 +194,11 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
|
||||
log.warn('Failed to register device:', err);
|
||||
});
|
||||
|
||||
// Fetch available templates in background (non-blocking)
|
||||
get().fetchAvailableTemplates().catch((err: unknown) => {
|
||||
log.warn('Failed to fetch templates after login:', err);
|
||||
});
|
||||
|
||||
// Fetch available models in background (non-blocking)
|
||||
get().fetchAvailableModels().catch((err: unknown) => {
|
||||
log.warn('Failed to fetch models after login:', err);
|
||||
@@ -587,6 +597,16 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
|
||||
}
|
||||
},
|
||||
|
||||
fetchAvailableTemplates: async () => {
|
||||
try {
|
||||
const templates = await saasClient.fetchAvailableTemplates();
|
||||
set({ availableTemplates: templates });
|
||||
} catch {
|
||||
// Graceful degradation - don't block login
|
||||
set({ availableTemplates: [] });
|
||||
}
|
||||
},
|
||||
|
||||
clearError: () => {
|
||||
set({ error: null });
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user