fix(industry): 审计收尾 — MEDIUM + LOW 全部清零
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

M-1: Industries 创建弹窗添加 cold_start_template + pain_seed_categories
M-3: industryStore console.warn → createLogger 结构化日志
B2: classify_with_industries 平局打破 + 归一化因子 3.0 文档化
S3: set_account_industries 验证移入事务内消除 TOCTOU
T1: 4 个 SaaS 请求类型添加 deny_unknown_fields
I3: store_trigger_experience Debug 格式 → signal_name 描述名
L-1: 删除 Accounts.tsx 死代码 editingIndustries
L-3: Industries.tsx filters 类型补全 source 字段
This commit is contained in:
iven
2026-04-12 20:37:48 +08:00
parent 3cff31ec03
commit f8c5a76ce6
7 changed files with 28 additions and 13 deletions

View File

@@ -42,7 +42,6 @@ export default function Accounts() {
const [modalOpen, setModalOpen] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const [searchParams, setSearchParams] = useState<Record<string, string>>({})
const [editingIndustries, setEditingIndustries] = useState<string[]>([])
const { data, isLoading } = useQuery({
queryKey: ['accounts', searchParams],
@@ -67,7 +66,6 @@ export default function Accounts() {
useEffect(() => {
if (accountIndustries && editingId) {
const ids = accountIndustries.map((item) => item.industry_id)
setEditingIndustries(ids)
form.setFieldValue('industry_ids', ids)
}
}, [accountIndustries, editingId, form])
@@ -212,7 +210,6 @@ export default function Accounts() {
const handleClose = () => {
setModalOpen(false)
setEditingId(null)
setEditingIndustries([])
form.resetFields()
}

View File

@@ -31,7 +31,7 @@ function IndustryListPanel() {
const queryClient = useQueryClient()
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [filters, setFilters] = useState<{ status?: string }>({})
const [filters, setFilters] = useState<{ status?: string; source?: string }>({})
const [editId, setEditId] = useState<string | null>(null)
const [createOpen, setCreateOpen] = useState(false)
@@ -337,6 +337,12 @@ function IndustryCreateModal({ open, onClose }: {
<Form.Item name="system_prompt" label="系统提示词">
<TextArea rows={4} placeholder="行业专属系统提示词" />
</Form.Item>
<Form.Item name="cold_start_template" label="冷启动模板" extra="新用户首次对话时使用的引导模板">
<TextArea rows={3} placeholder="如:您好!我是您的{行业}管家,可以帮您处理..." />
</Form.Item>
<Form.Item name="pain_seed_categories" label="痛点种子类别" extra="预置的痛点分类,用逗号或回车分隔">
<Select mode="tags" placeholder="如:库存管理、客户服务、合规" />
</Form.Item>
</Form>
</Modal>
)

View File

@@ -130,11 +130,15 @@ impl KeywordClassifier {
if hits == 0 {
return 0.0;
}
// Normalize: more hits = higher score, capped at 1.0
// Normalize: 3 keyword hits → score 1.0 (saturated). Threshold 0.2 ≈ 0.6 hits.
(hits as f32 / 3.0).min(1.0)
}
/// Classify against dynamic industry keyword configs.
///
/// Tie-breaking: when two industries score equally, the *first* entry wins
/// (keeps existing best on `<=`). Industries should be ordered by priority
/// in the config array if specific tie-breaking is desired.
fn classify_with_industries(query: &str, industries: &[IndustryKeywordConfig]) -> Option<RoutingHint> {
let lower = query.to_lowercase();

View File

@@ -204,24 +204,25 @@ pub async fn set_account_industries(
req: &SetAccountIndustriesRequest,
) -> SaasResult<Vec<AccountIndustryItem>> {
let now = chrono::Utc::now();
// 批量验证:一次查询所有行业是否存在且启用
let ids: Vec<&str> = req.industries.iter().map(|e| e.industry_id.as_str()).collect();
// 事务:验证 + DELETE + INSERT 原子执行,消除 TOCTOU
let mut tx = pool.begin().await.map_err(SaasError::Database)?;
// 验证:所有行业必须存在且启用
let valid_count: (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM industries WHERE id = ANY($1) AND status = 'active'"
)
.bind(&ids)
.fetch_one(pool)
.fetch_one(&mut *tx)
.await
.map_err(SaasError::Database)?;
if valid_count.0 != ids.len() as i64 {
tx.rollback().await.ok();
return Err(SaasError::InvalidInput("部分行业不存在或已禁用".to_string()));
}
// 事务性 DELETE + INSERT
let mut tx = pool.begin().await.map_err(SaasError::Database)?;
sqlx::query("DELETE FROM account_industries WHERE account_id = $1")
.bind(account_id)
.execute(&mut *tx)

View File

@@ -36,6 +36,7 @@ pub struct IndustryListItem {
/// 创建行业请求
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CreateIndustryRequest {
pub id: String,
pub name: String,
@@ -57,6 +58,7 @@ pub struct CreateIndustryRequest {
/// 更新行业请求
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UpdateIndustryRequest {
pub name: Option<String>,
pub icon: Option<String>,
@@ -99,12 +101,14 @@ pub struct AccountIndustryItem {
/// 设置用户行业请求
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SetAccountIndustriesRequest {
pub industries: Vec<AccountIndustryEntry>,
}
/// 用户行业条目
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AccountIndustryEntry {
pub industry_id: String,
#[serde(default)]

View File

@@ -426,7 +426,7 @@ async fn store_trigger_experience(
let entry = zclaw_growth::MemoryEntry::new(
agent_id,
zclaw_growth::MemoryType::Experience,
&format!("trigger/{:?}", signal),
&format!("trigger/{}", signal_name),
content,
);

View File

@@ -9,6 +9,9 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { IndustryFullConfig, AccountIndustryItem } from '../lib/saas-client';
import { saasClient } from '../lib/saas-client';
import { createLogger } from '../lib/logger';
const log = createLogger('industryStore');
// ============ Types ============
@@ -61,7 +64,7 @@ export const useIndustryStore = create<IndustryStore>()(
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);
log.warn(`Failed to fetch config for ${item.industry_id}:`, err);
}
}