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:
@@ -436,6 +436,8 @@ pub fn run() {
|
||||
intelligence::pain_aggregator::butler_generate_solution,
|
||||
intelligence::pain_aggregator::butler_list_proposals,
|
||||
intelligence::pain_aggregator::butler_update_proposal_status,
|
||||
// Industry config loader
|
||||
viking_commands::viking_load_industry_keywords,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -74,6 +74,16 @@ pub struct EmbeddingConfigResult {
|
||||
pub configured: bool,
|
||||
}
|
||||
|
||||
/// Industry keyword config received from the frontend (JSON string).
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IndustryConfigPayload {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub keywords: Vec<String>,
|
||||
pub system_prompt: String,
|
||||
}
|
||||
|
||||
// === Global Storage Instance ===
|
||||
|
||||
/// Global storage instance
|
||||
@@ -676,6 +686,72 @@ pub async fn viking_store_with_summaries(
|
||||
|
||||
// === Tests ===
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Industry Keywords Loader
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Load industry keywords into the ButlerRouter middleware.
|
||||
///
|
||||
/// Called from the frontend after fetching industry configs from SaaS.
|
||||
/// Updates the ButlerRouter's dynamic keyword source for routing.
|
||||
#[tauri::command]
|
||||
pub async fn viking_load_industry_keywords(
|
||||
configs: String,
|
||||
) -> Result<(), String> {
|
||||
let raw: Vec<IndustryConfigPayload> = serde_json::from_str(&configs)
|
||||
.map_err(|e| format!("Failed to parse industry configs: {}", e))?;
|
||||
|
||||
let industry_configs: Vec<zclaw_runtime::IndustryKeywordConfig> = raw
|
||||
.into_iter()
|
||||
.map(|c| zclaw_runtime::IndustryKeywordConfig {
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
keywords: c.keywords,
|
||||
system_prompt: c.system_prompt,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// The ButlerRouter is in the kernel's middleware chain.
|
||||
// For now, log and store for future retrieval by the kernel.
|
||||
tracing::info!(
|
||||
"[viking_commands] Loading {} industry keyword configs",
|
||||
industry_configs.len()
|
||||
);
|
||||
|
||||
// Store in a global for kernel middleware access
|
||||
{
|
||||
let mutex = INDUSTRY_CONFIGS
|
||||
.get_or_init(|| async { std::sync::Mutex::new(Vec::new()) })
|
||||
.await;
|
||||
let mut guard = mutex.lock().map_err(|e| format!("Lock poisoned: {}", e))?;
|
||||
*guard = industry_configs;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Global industry configs storage (accessed by kernel middleware)
|
||||
static INDUSTRY_CONFIGS: tokio::sync::OnceCell<std::sync::Mutex<Vec<zclaw_runtime::IndustryKeywordConfig>>> =
|
||||
tokio::sync::OnceCell::const_new();
|
||||
|
||||
/// Get the stored industry configs
|
||||
pub async fn get_industry_configs() -> Vec<zclaw_runtime::IndustryKeywordConfig> {
|
||||
let mutex = INDUSTRY_CONFIGS
|
||||
.get_or_init(|| async { std::sync::Mutex::new(Vec::new()) })
|
||||
.await;
|
||||
match mutex.lock() {
|
||||
Ok(guard) => guard.clone(),
|
||||
Err(e) => {
|
||||
tracing::warn!("[viking_commands] Industry configs lock poisoned: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
61
desktop/src/lib/saas-industry.ts
Normal file
61
desktop/src/lib/saas-industry.ts
Normal 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`);
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
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