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
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:
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user