fix(industry): 三轮审计修复 — 3 HIGH + 4 MEDIUM 清零
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
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
This commit is contained in:
@@ -21,9 +21,9 @@ import { PageHeader } from '@/components/PageHeader'
|
|||||||
const { TextArea } = Input
|
const { TextArea } = Input
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
const statusLabels: Record<string, string> = { active: '启用', disabled: '禁用' }
|
const statusLabels: Record<string, string> = { active: '启用', inactive: '禁用' }
|
||||||
const statusColors: Record<string, string> = { active: 'green', disabled: 'default' }
|
const statusColors: Record<string, string> = { active: 'green', inactive: 'default' }
|
||||||
const sourceLabels: Record<string, string> = { builtin: '内置', custom: '自定义' }
|
const sourceLabels: Record<string, string> = { builtin: '内置', admin: '自定义', custom: '自定义' }
|
||||||
|
|
||||||
// === 行业列表 ===
|
// === 行业列表 ===
|
||||||
|
|
||||||
@@ -77,6 +77,7 @@ function IndustryListPanel() {
|
|||||||
valueType: 'select',
|
valueType: 'select',
|
||||||
valueEnum: {
|
valueEnum: {
|
||||||
builtin: { text: '内置' },
|
builtin: { text: '内置' },
|
||||||
|
admin: { text: '自定义' },
|
||||||
custom: { text: '自定义' },
|
custom: { text: '自定义' },
|
||||||
},
|
},
|
||||||
render: (_, r) => <Tag color={r.source === 'builtin' ? 'blue' : 'purple'}>{sourceLabels[r.source] || r.source}</Tag>,
|
render: (_, r) => <Tag color={r.source === 'builtin' ? 'blue' : 'purple'}>{sourceLabels[r.source] || r.source}</Tag>,
|
||||||
@@ -95,7 +96,7 @@ function IndustryListPanel() {
|
|||||||
valueType: 'select',
|
valueType: 'select',
|
||||||
valueEnum: {
|
valueEnum: {
|
||||||
active: { text: '启用', status: 'Success' },
|
active: { text: '启用', status: 'Success' },
|
||||||
disabled: { text: '禁用', status: 'Default' },
|
inactive: { text: '禁用', status: 'Default' },
|
||||||
},
|
},
|
||||||
render: (_, r) => <Tag color={statusColors[r.status]}>{statusLabels[r.status] || r.status}</Tag>,
|
render: (_, r) => <Tag color={statusColors[r.status]}>{statusLabels[r.status] || r.status}</Tag>,
|
||||||
},
|
},
|
||||||
@@ -121,7 +122,7 @@ function IndustryListPanel() {
|
|||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
{r.status === 'active' ? (
|
{r.status === 'active' ? (
|
||||||
<Popconfirm title="确定禁用此行业?" onConfirm={() => updateStatusMutation.mutate({ id: r.id, status: 'disabled' })}>
|
<Popconfirm title="确定禁用此行业?" onConfirm={() => updateStatusMutation.mutate({ id: r.id, status: 'inactive' })}>
|
||||||
<Button type="link" size="small" danger icon={<StopOutlined />}>禁用</Button>
|
<Button type="link" size="small" danger icon={<StopOutlined />}>禁用</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
) : (
|
) : (
|
||||||
@@ -309,9 +310,17 @@ function IndustryCreateModal({ open, onClose }: {
|
|||||||
onFinish={(values) => {
|
onFinish={(values) => {
|
||||||
// Auto-generate id from name if not provided
|
// Auto-generate id from name if not provided
|
||||||
if (!values.id && values.name) {
|
if (!values.id && values.name) {
|
||||||
values.id = values.name.toLowerCase()
|
// Strip non-ASCII, keep only lowercase alphanumeric + hyphens
|
||||||
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
|
const generated = values.name.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
.replace(/^-|-$/g, '')
|
.replace(/^-|-$/g, '')
|
||||||
|
if (generated) {
|
||||||
|
values.id = generated
|
||||||
|
} else {
|
||||||
|
// Name has no ASCII chars — require manual ID entry
|
||||||
|
message.warning('中文行业名称无法自动生成标识,请手动填写行业标识')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createMutation.mutate(values)
|
createMutation.mutate(values)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface IndustryFullConfig {
|
|||||||
|
|
||||||
/** 创建行业请求 */
|
/** 创建行业请求 */
|
||||||
export interface CreateIndustryRequest {
|
export interface CreateIndustryRequest {
|
||||||
|
id?: string
|
||||||
name: string
|
name: string
|
||||||
icon: string
|
icon: string
|
||||||
description: string
|
description: string
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ pub struct IndustryFullConfig {
|
|||||||
|
|
||||||
/// 列表查询参数
|
/// 列表查询参数
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct ListIndustriesQuery {
|
pub struct ListIndustriesQuery {
|
||||||
pub page: Option<u32>,
|
pub page: Option<u32>,
|
||||||
pub page_size: Option<u32>,
|
pub page_size: Option<u32>,
|
||||||
|
|||||||
@@ -229,10 +229,10 @@ impl ExperienceExtractor {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let line = format!(
|
let line = format!(
|
||||||
"- 类似「{}」做过:{},结果是{} ({})",
|
"- 类似「{}」做过:{},结果是{} ({})",
|
||||||
truncate(&exp.pain_pattern, 30),
|
xml_escape(&truncate(&exp.pain_pattern, 30)),
|
||||||
step_summary,
|
xml_escape(&step_summary),
|
||||||
exp.outcome,
|
xml_escape(&exp.outcome),
|
||||||
industry_tag.trim_start()
|
xml_escape(industry_tag.trim_start())
|
||||||
);
|
);
|
||||||
total_chars += line.chars().count();
|
total_chars += line.chars().count();
|
||||||
parts.push(line);
|
parts.push(line);
|
||||||
@@ -257,6 +257,13 @@ fn truncate(s: &str, max_chars: usize) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Escape XML special characters for safe injection into `<butler-context>`.
|
||||||
|
fn xml_escape(s: &str) -> String {
|
||||||
|
s.replace('&', "&")
|
||||||
|
.replace('<', "<")
|
||||||
|
.replace('>', ">")
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -124,10 +124,10 @@ pub async fn post_conversation_hook(
|
|||||||
if !_user_message.is_empty() {
|
if !_user_message.is_empty() {
|
||||||
let trigger_ctx = crate::intelligence::triggers::TriggerContext {
|
let trigger_ctx = crate::intelligence::triggers::TriggerContext {
|
||||||
user_message: _user_message.to_string(),
|
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()],
|
conversation_messages: vec![_user_message.to_string()],
|
||||||
pain_confidence,
|
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);
|
let signals = crate::intelligence::triggers::evaluate_triggers(&trigger_ctx);
|
||||||
if !signals.is_empty() {
|
if !signals.is_empty() {
|
||||||
|
|||||||
@@ -89,6 +89,15 @@ pub struct IndustryConfigPayload {
|
|||||||
/// Global storage instance
|
/// Global storage instance
|
||||||
static STORAGE: OnceCell<Arc<SqliteStorage>> = OnceCell::const_new();
|
static STORAGE: OnceCell<Arc<SqliteStorage>> = 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<Vec<String>> = std::sync::RwLock::new(Vec::new());
|
||||||
|
|
||||||
|
/// Get the flattened list of all industry keywords (for trigger evaluation).
|
||||||
|
pub fn get_industry_keywords_flat() -> Vec<String> {
|
||||||
|
INDUSTRY_KEYWORDS_CACHE.read().map(|g| g.clone()).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the storage directory path
|
/// Get the storage directory path
|
||||||
pub fn get_storage_dir() -> PathBuf {
|
pub fn get_storage_dir() -> PathBuf {
|
||||||
// Use platform-specific data directory
|
// Use platform-specific data directory
|
||||||
@@ -727,7 +736,17 @@ pub async fn viking_load_industry_keywords(
|
|||||||
};
|
};
|
||||||
if let Some(shared) = shared {
|
if let Some(shared) = shared {
|
||||||
let mut guard = shared.write().await;
|
let mut guard = shared.write().await;
|
||||||
|
// Flatten all keywords for trigger evaluation cache
|
||||||
|
let all_keywords: Vec<String> = guard.iter()
|
||||||
|
.flat_map(|c| c.keywords.iter().cloned())
|
||||||
|
.collect();
|
||||||
*guard = industry_configs;
|
*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");
|
tracing::info!("[viking_commands] Industry keywords synced to ButlerRouter middleware");
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("[viking_commands] Kernel not initialized, industry keywords not loaded");
|
tracing::warn!("[viking_commands] Kernel not initialized, industry keywords not loaded");
|
||||||
|
|||||||
Reference in New Issue
Block a user