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 [modalOpen, setModalOpen] = useState(false)
|
||||||
const [editingId, setEditingId] = useState<string | null>(null)
|
const [editingId, setEditingId] = useState<string | null>(null)
|
||||||
const [searchParams, setSearchParams] = useState<Record<string, string>>({})
|
const [searchParams, setSearchParams] = useState<Record<string, string>>({})
|
||||||
const [editingIndustries, setEditingIndustries] = useState<string[]>([])
|
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['accounts', searchParams],
|
queryKey: ['accounts', searchParams],
|
||||||
@@ -67,7 +66,6 @@ export default function Accounts() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accountIndustries && editingId) {
|
if (accountIndustries && editingId) {
|
||||||
const ids = accountIndustries.map((item) => item.industry_id)
|
const ids = accountIndustries.map((item) => item.industry_id)
|
||||||
setEditingIndustries(ids)
|
|
||||||
form.setFieldValue('industry_ids', ids)
|
form.setFieldValue('industry_ids', ids)
|
||||||
}
|
}
|
||||||
}, [accountIndustries, editingId, form])
|
}, [accountIndustries, editingId, form])
|
||||||
@@ -212,7 +210,6 @@ export default function Accounts() {
|
|||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setModalOpen(false)
|
setModalOpen(false)
|
||||||
setEditingId(null)
|
setEditingId(null)
|
||||||
setEditingIndustries([])
|
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function IndustryListPanel() {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(20)
|
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 [editId, setEditId] = useState<string | null>(null)
|
||||||
const [createOpen, setCreateOpen] = useState(false)
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
|
|
||||||
@@ -337,6 +337,12 @@ function IndustryCreateModal({ open, onClose }: {
|
|||||||
<Form.Item name="system_prompt" label="系统提示词">
|
<Form.Item name="system_prompt" label="系统提示词">
|
||||||
<TextArea rows={4} placeholder="行业专属系统提示词" />
|
<TextArea rows={4} placeholder="行业专属系统提示词" />
|
||||||
</Form.Item>
|
</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>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -130,11 +130,15 @@ impl KeywordClassifier {
|
|||||||
if hits == 0 {
|
if hits == 0 {
|
||||||
return 0.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)
|
(hits as f32 / 3.0).min(1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Classify against dynamic industry keyword configs.
|
/// 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> {
|
fn classify_with_industries(query: &str, industries: &[IndustryKeywordConfig]) -> Option<RoutingHint> {
|
||||||
let lower = query.to_lowercase();
|
let lower = query.to_lowercase();
|
||||||
|
|
||||||
|
|||||||
@@ -204,24 +204,25 @@ pub async fn set_account_industries(
|
|||||||
req: &SetAccountIndustriesRequest,
|
req: &SetAccountIndustriesRequest,
|
||||||
) -> SaasResult<Vec<AccountIndustryItem>> {
|
) -> SaasResult<Vec<AccountIndustryItem>> {
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
|
|
||||||
// 批量验证:一次查询所有行业是否存在且启用
|
|
||||||
let ids: Vec<&str> = req.industries.iter().map(|e| e.industry_id.as_str()).collect();
|
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(
|
let valid_count: (i64,) = sqlx::query_as(
|
||||||
"SELECT COUNT(*) FROM industries WHERE id = ANY($1) AND status = 'active'"
|
"SELECT COUNT(*) FROM industries WHERE id = ANY($1) AND status = 'active'"
|
||||||
)
|
)
|
||||||
.bind(&ids)
|
.bind(&ids)
|
||||||
.fetch_one(pool)
|
.fetch_one(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(SaasError::Database)?;
|
.map_err(SaasError::Database)?;
|
||||||
|
|
||||||
if valid_count.0 != ids.len() as i64 {
|
if valid_count.0 != ids.len() as i64 {
|
||||||
|
tx.rollback().await.ok();
|
||||||
return Err(SaasError::InvalidInput("部分行业不存在或已禁用".to_string()));
|
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")
|
sqlx::query("DELETE FROM account_industries WHERE account_id = $1")
|
||||||
.bind(account_id)
|
.bind(account_id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub struct IndustryListItem {
|
|||||||
|
|
||||||
/// 创建行业请求
|
/// 创建行业请求
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct CreateIndustryRequest {
|
pub struct CreateIndustryRequest {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -57,6 +58,7 @@ pub struct CreateIndustryRequest {
|
|||||||
|
|
||||||
/// 更新行业请求
|
/// 更新行业请求
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct UpdateIndustryRequest {
|
pub struct UpdateIndustryRequest {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
@@ -99,12 +101,14 @@ pub struct AccountIndustryItem {
|
|||||||
|
|
||||||
/// 设置用户行业请求
|
/// 设置用户行业请求
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct SetAccountIndustriesRequest {
|
pub struct SetAccountIndustriesRequest {
|
||||||
pub industries: Vec<AccountIndustryEntry>,
|
pub industries: Vec<AccountIndustryEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 用户行业条目
|
/// 用户行业条目
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct AccountIndustryEntry {
|
pub struct AccountIndustryEntry {
|
||||||
pub industry_id: String,
|
pub industry_id: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -426,7 +426,7 @@ async fn store_trigger_experience(
|
|||||||
let entry = zclaw_growth::MemoryEntry::new(
|
let entry = zclaw_growth::MemoryEntry::new(
|
||||||
agent_id,
|
agent_id,
|
||||||
zclaw_growth::MemoryType::Experience,
|
zclaw_growth::MemoryType::Experience,
|
||||||
&format!("trigger/{:?}", signal),
|
&format!("trigger/{}", signal_name),
|
||||||
content,
|
content,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import { create } from 'zustand';
|
|||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import type { IndustryFullConfig, AccountIndustryItem } from '../lib/saas-client';
|
import type { IndustryFullConfig, AccountIndustryItem } from '../lib/saas-client';
|
||||||
import { saasClient } from '../lib/saas-client';
|
import { saasClient } from '../lib/saas-client';
|
||||||
|
import { createLogger } from '../lib/logger';
|
||||||
|
|
||||||
|
const log = createLogger('industryStore');
|
||||||
|
|
||||||
// ============ Types ============
|
// ============ Types ============
|
||||||
|
|
||||||
@@ -61,7 +64,7 @@ export const useIndustryStore = create<IndustryStore>()(
|
|||||||
configs[item.industry_id] = fullConfig;
|
configs[item.industry_id] = fullConfig;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Non-fatal: one industry failing shouldn't block others
|
// 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