feat(industry): Phase 3 Tauri 行业配置加载 — SaaS API mixin + industryStore + Tauri 命令
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
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
- 新增 saas-industry.ts mixin: listIndustries/getIndustryFullConfig/getMyIndustries - 新增 saas-types 行业类型: IndustryInfo/IndustryFullConfig/AccountIndustryItem - 新增 industryStore.ts: Zustand store + localStorage persist + Rust 注入 - 新增 viking_load_industry_keywords Tauri 命令: 接收 JSON configs → 全局存储 - 前端 bootstrap 后自动拉取行业配置并推送到 ButlerRouter
This commit is contained in:
125
desktop/src/store/industryStore.ts
Normal file
125
desktop/src/store/industryStore.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Industry Store — Zustand store for industry configuration
|
||||
*
|
||||
* Manages industry configs fetched from SaaS, cached locally for
|
||||
* offline fallback. Injected into ButlerRouter for dynamic keyword routing.
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { IndustryFullConfig, AccountIndustryItem } from '../lib/saas-client';
|
||||
import { saasClient } from '../lib/saas-client';
|
||||
|
||||
// ============ Types ============
|
||||
|
||||
interface IndustryState {
|
||||
/** User's authorized industries */
|
||||
accountIndustries: AccountIndustryItem[];
|
||||
/** Full configs for authorized industries (keyed by industry id) */
|
||||
configs: Record<string, IndustryFullConfig>;
|
||||
/** Last sync timestamp */
|
||||
lastSynced: string | null;
|
||||
/** Loading state */
|
||||
isLoading: boolean;
|
||||
/** Error message */
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface IndustryActions {
|
||||
/** Fetch user industries + full configs from SaaS */
|
||||
fetchIndustries: () => Promise<void>;
|
||||
/** Get all loaded industry keywords (for trigger system) */
|
||||
getAllKeywords: () => string[];
|
||||
/** Clear all data */
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
type IndustryStore = IndustryState & IndustryActions;
|
||||
|
||||
// ============ Store ============
|
||||
|
||||
export const useIndustryStore = create<IndustryStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
accountIndustries: [],
|
||||
configs: {},
|
||||
lastSynced: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
fetchIndustries: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
// Step 1: Get user's authorized industries
|
||||
const accountIndustries = await saasClient.getMyIndustries();
|
||||
|
||||
// Step 2: Fetch full config for each authorized industry
|
||||
const configs: Record<string, IndustryFullConfig> = {};
|
||||
for (const item of accountIndustries) {
|
||||
try {
|
||||
const fullConfig = await saasClient.getIndustryFullConfig(item.industry_id);
|
||||
configs[item.industry_id] = fullConfig;
|
||||
} catch (err) {
|
||||
// Non-fatal: one industry failing shouldn't block others
|
||||
console.warn(`[industryStore] Failed to fetch config for ${item.industry_id}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
set({
|
||||
accountIndustries,
|
||||
configs,
|
||||
lastSynced: new Date().toISOString(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
// Step 3: Push to Rust ButlerRouter via Tauri invoke
|
||||
try {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
const industryConfigs = Object.values(configs).map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
keywords: c.keywords,
|
||||
system_prompt: c.system_prompt,
|
||||
}));
|
||||
await invoke('viking_load_industry_keywords', { configs: JSON.stringify(industryConfigs) });
|
||||
} catch (err) {
|
||||
// Tauri not available (browser mode) — ignore
|
||||
}
|
||||
} catch (err) {
|
||||
set({
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getAllKeywords: () => {
|
||||
const { configs } = get();
|
||||
const allKeywords: string[] = [];
|
||||
for (const config of Object.values(configs)) {
|
||||
allKeywords.push(...config.keywords);
|
||||
}
|
||||
return allKeywords;
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
set({
|
||||
accountIndustries: [],
|
||||
configs: {},
|
||||
lastSynced: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'zclaw-industry-store',
|
||||
// Only persist configs and industries (not loading/error state)
|
||||
partialize: (state) => ({
|
||||
accountIndustries: state.accountIndustries,
|
||||
configs: state.configs,
|
||||
lastSynced: state.lastSynced,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user