feat: 新增风险规则生成引擎与知识图谱可视化

后端新增风险规则自动生成和模板执行服务,支持从规则资产
批量生成并持久化风险规则文件;知识库入库日志增强图谱
查询和本地 RAG 回退,前端审计页面增加风险规则模型和流
程图组件,知识入库面板拆分为图谱可视化子组件,报销创
建页面增加引导式流程模型,更新知识库索引数据。
This commit is contained in:
caoxiaozhu
2026-05-23 19:54:42 +08:00
parent 5b388d08c0
commit 575f093c74
63 changed files with 35497 additions and 1517 deletions

View File

@@ -0,0 +1,189 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import {
EXPENSE_WELCOME_QUICK_ACTIONS,
SESSION_TYPE_EXPENSE,
SESSION_TYPE_KNOWLEDGE,
buildWelcomeQuickActions
} from '../src/views/scripts/travelReimbursementConversationModel.js'
import {
GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW,
GUIDED_ACTION_CONTINUE_FILLING,
GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR,
GUIDED_ACTION_PROCESS_INTERRUPTION,
GUIDED_ACTION_SELECT_EXPENSE_TYPE,
GUIDED_ACTION_SELECT_QUERY_MODE,
GUIDED_ACTION_SELECT_QUERY_STATUS,
GUIDED_ACTION_START_REIMBURSEMENT,
GUIDED_ACTION_START_STATUS_QUERY,
GUIDED_FLOW_MODE_REIMBURSEMENT,
GUIDED_FLOW_MODE_STATUS_QUERY,
applyGuidedReimbursementAnswer,
buildGuidedExpenseTypeActions,
buildGuidedInterruptionActions,
buildGuidedQueryModeActions,
buildGuidedQueryStatusActions,
buildGuidedReimbursementSummaryText,
buildGuidedReviewConfirmationActions,
buildGuidedReviewSubmitOptions,
buildGuidedStatusQueryText,
buildGuidedStepPromptText,
createEmptyGuidedFlowState,
createGuidedReimbursementState,
createGuidedStatusQueryState,
isGuidedReimbursementReadyForReview,
normalizeGuidedFlowState,
selectGuidedExpenseType,
selectGuidedQueryMode,
shouldConfirmGuidedInterruption
} from '../src/views/scripts/travelReimbursementGuidedFlowModel.js'
const createViewScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/TravelReimbursementCreateView.js', import.meta.url)),
'utf8'
)
const guidedFlowScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementGuidedFlow.js', import.meta.url)),
'utf8'
)
const sessionStateScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSessionState.js', import.meta.url)),
'utf8'
)
test('welcome quick actions are reduced to three guided local actions', () => {
assert.deepEqual(
EXPENSE_WELCOME_QUICK_ACTIONS.map((item) => item.label),
['快速发起报销', '查询单据状态', '差旅计算器']
)
assert.deepEqual(
EXPENSE_WELCOME_QUICK_ACTIONS.map((item) => item.action),
[
GUIDED_ACTION_START_REIMBURSEMENT,
GUIDED_ACTION_START_STATUS_QUERY,
GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR
]
)
assert.ok(EXPENSE_WELCOME_QUICK_ACTIONS.every((item) => !item.prompt))
assert.equal(buildWelcomeQuickActions(SESSION_TYPE_EXPENSE).length, 3)
assert.notDeepEqual(
buildWelcomeQuickActions(SESSION_TYPE_KNOWLEDGE).map((item) => item.label),
EXPENSE_WELCOME_QUICK_ACTIONS.map((item) => item.label),
'knowledge hot questions should stay independent'
)
})
test('guided reimbursement asks type first and walks travel fields in order', () => {
const typeActions = buildGuidedExpenseTypeActions()
assert.deepEqual(
typeActions.map((action) => action.label),
['差旅费', '交通费', '住宿费', '业务招待费', '办公用品费', '其他费用']
)
assert.ok(typeActions.every((action) => action.action_type === GUIDED_ACTION_SELECT_EXPENSE_TYPE))
let state = createGuidedReimbursementState()
assert.equal(state.mode, GUIDED_FLOW_MODE_REIMBURSEMENT)
assert.equal(state.stepKey, 'expense_type')
state = selectGuidedExpenseType(state, 'travel')
assert.equal(state.stepKey, 'reason')
assert.match(buildGuidedStepPromptText(state), /第 1 步:事由/)
state = applyGuidedReimbursementAnswer(state, '去上海支持上海电力部署项目')
assert.equal(state.stepKey, 'location')
state = applyGuidedReimbursementAnswer(state, '上海')
assert.equal(state.stepKey, 'time_range')
state = applyGuidedReimbursementAnswer(state, '2026-05-20 至 2026-05-23出差 3 天')
assert.equal(state.stepKey, 'amount')
state = applyGuidedReimbursementAnswer(state, '待核算')
assert.equal(state.stepKey, 'attachments')
state = applyGuidedReimbursementAnswer(state, '稍后上传')
assert.ok(isGuidedReimbursementReadyForReview(state))
assert.match(buildGuidedReimbursementSummaryText(state), /已完成“差旅费”的引导填写/)
assert.deepEqual(
buildGuidedReviewConfirmationActions().map((action) => action.action_type),
[GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW]
)
const submitOptions = buildGuidedReviewSubmitOptions(state)
assert.equal(submitOptions.systemGenerated, true)
assert.equal(submitOptions.extraContext.expense_scene_selection.expense_type, 'travel')
assert.equal(submitOptions.extraContext.review_form_values.expense_type, '差旅费')
assert.match(submitOptions.rawText, /事由:去上海支持上海电力部署项目/)
assert.match(submitOptions.rawText, /出差时间\/天数2026-05-20 至 2026-05-23出差 3 天/)
})
test('guided reimbursement interrupts suspicious questions before expensive flow', () => {
const state = selectGuidedExpenseType(createGuidedReimbursementState(), 'transport')
assert.equal(shouldConfirmGuidedInterruption('送客户去机场', state), false)
assert.equal(shouldConfirmGuidedInterruption('帮我查询一下上周的报销状态?', state), true)
assert.deepEqual(
buildGuidedInterruptionActions().map((action) => action.action_type),
[GUIDED_ACTION_CONTINUE_FILLING, GUIDED_ACTION_PROCESS_INTERRUPTION]
)
})
test('status query guide collects a query mode before calling existing query flow', () => {
let state = createGuidedStatusQueryState()
assert.equal(state.mode, GUIDED_FLOW_MODE_STATUS_QUERY)
assert.equal(state.stepKey, 'query_mode')
assert.ok(buildGuidedQueryModeActions().every((action) => action.action_type === GUIDED_ACTION_SELECT_QUERY_MODE))
state = selectGuidedQueryMode(state, 'status')
assert.equal(state.stepKey, 'status_value')
assert.ok(buildGuidedQueryStatusActions().every((action) => action.action_type === GUIDED_ACTION_SELECT_QUERY_STATUS))
assert.equal(buildGuidedStatusQueryText(state, '已归档'), '帮我查询已归档的报销单据,筛选最近的 5 条记录')
const keywordState = selectGuidedQueryMode(createGuidedStatusQueryState(), 'keyword')
assert.equal(keywordState.stepKey, 'query_value')
assert.equal(
buildGuidedStatusQueryText(keywordState, '上海电力'),
'帮我查询地点或事由包含“上海电力”的报销单据状态,筛选最近的 5 条记录'
)
})
test('guided flow state is serializable and restored through session state', () => {
const empty = createEmptyGuidedFlowState()
assert.deepEqual(normalizeGuidedFlowState({ mode: 'bad' }), empty)
assert.deepEqual(
normalizeGuidedFlowState({
mode: GUIDED_FLOW_MODE_REIMBURSEMENT,
stepKey: 'amount',
expenseType: 'travel',
values: {
amount: 200,
attachment_names: ['a.pdf', '', 'a.pdf']
},
pendingInterruptionText: '查询状态?'
}),
{
mode: GUIDED_FLOW_MODE_REIMBURSEMENT,
stepKey: 'amount',
expenseType: 'travel',
values: {
amount: '200',
attachment_names: ['a.pdf']
},
pendingInterruptionText: '查询状态?'
}
)
assert.match(sessionStateScript, /guidedFlowState:\s*normalizeGuidedFlowState\(state\.guidedFlowState\)/)
assert.match(sessionStateScript, /runtimeRefs\.guidedFlowState\?\.value/)
assert.match(sessionStateScript, /guidedFlowState,\s*\n\s*insightPanelCollapsed/)
assert.match(sessionStateScript, /function refreshWelcomeQuickActions/)
assert.match(sessionStateScript, /buildWelcomeQuickActions\(/)
})
test('guided flow is local until final confirmation or collected query handoff', () => {
assert.doesNotMatch(guidedFlowScript, /runOrchestrator/)
assert.doesNotMatch(guidedFlowScript, /startExpenseClaimDraftFlowStep/)
assert.doesNotMatch(guidedFlowScript, /review_action:\s*['"]save_draft['"]/)
assert.match(createViewScript, /if \(await handleGuidedComposerSubmit\(options\)\) \{[\s\S]*return null[\s\S]*\}[\s\S]*return submitComposerInternal\(options\)/)
assert.match(guidedFlowScript, /submitExistingComposer\(submitOptions\)/)
assert.match(guidedFlowScript, /submitExistingComposer\(\{[\s\S]*pendingText:\s*'正在查询单据状态\.\.\.'/)
})