From 0b512a3d85a446a0e8d0feab0e9b4baada38eab4 Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 12 Apr 2026 21:04:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(industry):=20=E4=B8=89=E8=BD=AE=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E4=BF=AE=E5=A4=8D=20=E2=80=94=203=20HIGH=20+=204=20ME?= =?UTF-8?q?DIUM=20=E6=B8=85=E9=9B=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit H1: status 值不匹配 disabled→inactive + source 补 admin 映射 + valueEnum H2: experience.rs format_for_injection 添加 xml_escape H3: TriggerContext industry_keywords 接通全局缓存 M2: ID 自动生成移除中文字符保留 + 无 ASCII 时提示手动输入 M3: TS CreateIndustryRequest 添加 id? 字段 M4: ListIndustriesQuery 添加 deny_unknown_fields --- admin-v2/src/pages/Industries.tsx | 23 +++++++++++++------ admin-v2/src/services/industries.ts | 1 + crates/zclaw-saas/src/industry/types.rs | 1 + .../src-tauri/src/intelligence/experience.rs | 15 ++++++++---- desktop/src-tauri/src/intelligence_hooks.rs | 4 ++-- desktop/src-tauri/src/viking_commands.rs | 19 +++++++++++++++ 6 files changed, 50 insertions(+), 13 deletions(-) diff --git a/admin-v2/src/pages/Industries.tsx b/admin-v2/src/pages/Industries.tsx index 77eaa1f..01dd651 100644 --- a/admin-v2/src/pages/Industries.tsx +++ b/admin-v2/src/pages/Industries.tsx @@ -21,9 +21,9 @@ import { PageHeader } from '@/components/PageHeader' const { TextArea } = Input const { Text } = Typography -const statusLabels: Record = { active: '启用', disabled: '禁用' } -const statusColors: Record = { active: 'green', disabled: 'default' } -const sourceLabels: Record = { builtin: '内置', custom: '自定义' } +const statusLabels: Record = { active: '启用', inactive: '禁用' } +const statusColors: Record = { active: 'green', inactive: 'default' } +const sourceLabels: Record = { builtin: '内置', admin: '自定义', custom: '自定义' } // === 行业列表 === @@ -77,6 +77,7 @@ function IndustryListPanel() { valueType: 'select', valueEnum: { builtin: { text: '内置' }, + admin: { text: '自定义' }, custom: { text: '自定义' }, }, render: (_, r) => {sourceLabels[r.source] || r.source}, @@ -95,7 +96,7 @@ function IndustryListPanel() { valueType: 'select', valueEnum: { active: { text: '启用', status: 'Success' }, - disabled: { text: '禁用', status: 'Default' }, + inactive: { text: '禁用', status: 'Default' }, }, render: (_, r) => {statusLabels[r.status] || r.status}, }, @@ -121,7 +122,7 @@ function IndustryListPanel() { 编辑 {r.status === 'active' ? ( - updateStatusMutation.mutate({ id: r.id, status: 'disabled' })}> + updateStatusMutation.mutate({ id: r.id, status: 'inactive' })}> ) : ( @@ -309,9 +310,17 @@ function IndustryCreateModal({ open, onClose }: { onFinish={(values) => { // Auto-generate id from name if not provided if (!values.id && values.name) { - values.id = values.name.toLowerCase() - .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-') + // Strip non-ASCII, keep only lowercase alphanumeric + hyphens + const generated = values.name.toLowerCase() + .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') + if (generated) { + values.id = generated + } else { + // Name has no ASCII chars — require manual ID entry + message.warning('中文行业名称无法自动生成标识,请手动填写行业标识') + return + } } createMutation.mutate(values) }} diff --git a/admin-v2/src/services/industries.ts b/admin-v2/src/services/industries.ts index 8b373b0..0906144 100644 --- a/admin-v2/src/services/industries.ts +++ b/admin-v2/src/services/industries.ts @@ -38,6 +38,7 @@ export interface IndustryFullConfig { /** 创建行业请求 */ export interface CreateIndustryRequest { + id?: string name: string icon: string description: string diff --git a/crates/zclaw-saas/src/industry/types.rs b/crates/zclaw-saas/src/industry/types.rs index fd254af..56f49ed 100644 --- a/crates/zclaw-saas/src/industry/types.rs +++ b/crates/zclaw-saas/src/industry/types.rs @@ -135,6 +135,7 @@ pub struct IndustryFullConfig { /// 列表查询参数 #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ListIndustriesQuery { pub page: Option, pub page_size: Option, diff --git a/desktop/src-tauri/src/intelligence/experience.rs b/desktop/src-tauri/src/intelligence/experience.rs index b0b3db7..7c1ad48 100644 --- a/desktop/src-tauri/src/intelligence/experience.rs +++ b/desktop/src-tauri/src/intelligence/experience.rs @@ -229,10 +229,10 @@ impl ExperienceExtractor { .unwrap_or_default(); let line = format!( "- 类似「{}」做过:{},结果是{} ({})", - truncate(&exp.pain_pattern, 30), - step_summary, - exp.outcome, - industry_tag.trim_start() + xml_escape(&truncate(&exp.pain_pattern, 30)), + xml_escape(&step_summary), + xml_escape(&exp.outcome), + xml_escape(industry_tag.trim_start()) ); total_chars += line.chars().count(); parts.push(line); @@ -257,6 +257,13 @@ fn truncate(s: &str, max_chars: usize) -> String { } } +/// Escape XML special characters for safe injection into ``. +fn xml_escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/desktop/src-tauri/src/intelligence_hooks.rs b/desktop/src-tauri/src/intelligence_hooks.rs index a60500b..3b0c09d 100644 --- a/desktop/src-tauri/src/intelligence_hooks.rs +++ b/desktop/src-tauri/src/intelligence_hooks.rs @@ -124,10 +124,10 @@ pub async fn post_conversation_hook( if !_user_message.is_empty() { let trigger_ctx = crate::intelligence::triggers::TriggerContext { user_message: _user_message.to_string(), - tool_call_count: 0, // Will be populated from trajectory recorder in future + tool_call_count: 0, conversation_messages: vec![_user_message.to_string()], pain_confidence, - industry_keywords: vec![], // Will be populated from industry config in Phase 3 + industry_keywords: crate::viking_commands::get_industry_keywords_flat(), }; let signals = crate::intelligence::triggers::evaluate_triggers(&trigger_ctx); if !signals.is_empty() { diff --git a/desktop/src-tauri/src/viking_commands.rs b/desktop/src-tauri/src/viking_commands.rs index c55c630..4d3e3a3 100644 --- a/desktop/src-tauri/src/viking_commands.rs +++ b/desktop/src-tauri/src/viking_commands.rs @@ -89,6 +89,15 @@ pub struct IndustryConfigPayload { /// Global storage instance static STORAGE: OnceCell> = OnceCell::const_new(); +/// Flattened industry keywords cache for trigger evaluation. +/// Updated when `viking_load_industry_keywords` is called. +static INDUSTRY_KEYWORDS_CACHE: std::sync::RwLock> = std::sync::RwLock::new(Vec::new()); + +/// Get the flattened list of all industry keywords (for trigger evaluation). +pub fn get_industry_keywords_flat() -> Vec { + INDUSTRY_KEYWORDS_CACHE.read().map(|g| g.clone()).unwrap_or_default() +} + /// Get the storage directory path pub fn get_storage_dir() -> PathBuf { // Use platform-specific data directory @@ -727,7 +736,17 @@ pub async fn viking_load_industry_keywords( }; if let Some(shared) = shared { let mut guard = shared.write().await; + // Flatten all keywords for trigger evaluation cache + let all_keywords: Vec = guard.iter() + .flat_map(|c| c.keywords.iter().cloned()) + .collect(); *guard = industry_configs; + drop(guard); + + // Update the flattened keyword cache (for trigger system) + if let Ok(mut cache) = INDUSTRY_KEYWORDS_CACHE.write() { + *cache = all_keywords; + } tracing::info!("[viking_commands] Industry keywords synced to ButlerRouter middleware"); } else { tracing::warn!("[viking_commands] Kernel not initialized, industry keywords not loaded");