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

- 新增 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:
iven
2026-04-12 17:18:53 +08:00
parent 29fbfbec59
commit b853978771
6 changed files with 310 additions and 0 deletions

View File

@@ -67,6 +67,9 @@ export type {
AgentTemplateAvailable,
AgentTemplateFull,
AgentConfigFromTemplate,
IndustryInfo,
IndustryFullConfig,
AccountIndustryItem,
} from './saas-types';
export { SaaSApiError } from './saas-errors';
@@ -110,6 +113,7 @@ import { installRelayMethods } from './saas-relay';
import { installPromptMethods } from './saas-prompt';
import { installTelemetryMethods } from './saas-telemetry';
import { installBillingMethods } from './saas-billing';
import { installIndustryMethods } from './saas-industry';
export type { UsageIncrementResult } from './saas-billing';
// Re-export billing types for convenience
@@ -443,6 +447,7 @@ installRelayMethods(SaaSClient);
installPromptMethods(SaaSClient);
installTelemetryMethods(SaaSClient);
installBillingMethods(SaaSClient);
installIndustryMethods(SaaSClient);
export { installBillingMethods };
// === API Method Type Declarations ===
@@ -500,6 +505,12 @@ export interface SaaSClient {
getSubscription(): Promise<import('./saas-types').SubscriptionInfo>;
createPayment(data: import('./saas-types').CreatePaymentRequest): Promise<import('./saas-types').PaymentResult>;
getPaymentStatus(paymentId: string): Promise<import('./saas-types').PaymentStatus>;
// --- Industry (saas-industry.ts) ---
listIndustries(opts?: { page?: number; page_size?: number; status?: string }): Promise<import('./saas-types').PaginatedResponse<import('./saas-types').IndustryInfo>>;
getIndustryFullConfig(industryId: string): Promise<import('./saas-types').IndustryFullConfig>;
getMyIndustries(): Promise<import('./saas-types').AccountIndustryItem[]>;
getAccountIndustries(accountId: string): Promise<import('./saas-types').AccountIndustryItem[]>;
}
// === Singleton ===

View File

@@ -0,0 +1,61 @@
/**
* SaaS Industry Methods — Mixin
*
* Installs industry-related methods onto SaaSClient.prototype.
* Covers listing industries, fetching full configs, and account-industry associations.
*/
import type {
IndustryInfo,
IndustryFullConfig,
AccountIndustryItem,
PaginatedResponse,
} from './saas-types';
export function installIndustryMethods(ClientClass: { prototype: any }): void {
const proto = ClientClass.prototype;
/**
* List available industries.
*/
proto.listIndustries = async function (
this: { request<T>(method: string, path: string, body?: unknown): Promise<T> },
opts?: { page?: number; page_size?: number; status?: string },
): Promise<PaginatedResponse<IndustryInfo>> {
const params = new URLSearchParams();
if (opts?.page) params.set('page', String(opts.page));
if (opts?.page_size) params.set('page_size', String(opts.page_size));
if (opts?.status) params.set('status', opts.status);
const qs = params.toString();
return this.request('GET', `/api/v1/industries${qs ? '?' + qs : ''}`);
};
/**
* Get full industry config (keywords, prompts, etc.).
*/
proto.getIndustryFullConfig = async function (
this: { request<T>(method: string, path: string, body?: unknown): Promise<T> },
industryId: string,
): Promise<IndustryFullConfig> {
return this.request('GET', `/api/v1/industries/${industryId}/full-config`);
};
/**
* Get the current user's authorized industries.
*/
proto.getMyIndustries = async function (
this: { request<T>(method: string, path: string, body?: unknown): Promise<T> },
): Promise<AccountIndustryItem[]> {
return this.request('GET', '/api/v1/accounts/me/industries');
};
/**
* Get a specific account's authorized industries (admin).
*/
proto.getAccountIndustries = async function (
this: { request<T>(method: string, path: string, body?: unknown): Promise<T> },
accountId: string,
): Promise<AccountIndustryItem[]> {
return this.request('GET', `/api/v1/accounts/${accountId}/industries`);
};
}

View File

@@ -237,6 +237,41 @@ export interface PromptUpdatePayload {
// === Admin Types: Providers ===
// === Industry Types ===
/** Industry list item */
export interface IndustryInfo {
id: string;
name: string;
icon: string;
description: string;
status: string;
source: string;
}
/** Industry full config (with keywords, prompts, etc.) */
export interface IndustryFullConfig {
id: string;
name: string;
icon: string;
description: string;
keywords: string[];
system_prompt: string;
cold_start_template: string;
pain_seed_categories: string[];
skill_priorities: Array<{ skill_id: string; priority: number }>;
status: string;
source: string;
}
/** Account industry association */
export interface AccountIndustryItem {
industry_id: string;
is_primary: boolean;
industry_name: string;
industry_icon: string;
}
/** Provider info from GET /api/v1/providers */
export interface ProviderInfo {
id: string;

View 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,
}),
},
),
);