Refine travel reimbursement steward flow

Align planner, runtime rules, and policy assets so travel guidance
matches the updated reimbursement workflow.
This commit is contained in:
caoxiaozhu
2026-06-15 22:55:18 +08:00
parent 792741709a
commit 9f7b8b46a3
85 changed files with 9496 additions and 2555 deletions

View File

@@ -0,0 +1,50 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
buildStewardSuggestedActions
} from '../src/views/scripts/stewardPlanModel.js'
test('steward pending flow confirmation builds candidate actions', () => {
const actions = buildStewardSuggestedActions({
plan_id: 'steward-plan-pending-flow',
plan_status: 'needs_flow_confirmation',
next_action: 'confirm_flow',
pending_flow_confirmation: {
status: 'pending',
reason: '缺少申请或报销动作词。',
candidate_flows: [
{
flow_id: 'travel_application',
label: '补办出差申请',
confidence: 0.52,
ontology_fields: {
time_range: '2026-02-20',
location: '上海',
expense_type: 'travel',
reason: '辅助国网仿生产环境部署'
},
missing_fields: ['transport_mode']
},
{
flow_id: 'travel_reimbursement',
label: '发起费用报销',
confidence: 0.48,
ontology_fields: {
time_range: '2026-02-20',
location: '上海',
expense_type: 'travel',
reason: '辅助国网仿生产环境部署'
},
missing_fields: []
}
]
}
})
assert.equal(actions.length, 2)
assert.deepEqual(actions.map((item) => item.label), ['补办出差申请', '发起费用报销'])
assert.equal(actions[0].payload.steward_confirm_flow, true)
assert.equal(actions[0].payload.flow_id, 'travel_application')
assert.equal(actions[1].payload.flow_id, 'travel_reimbursement')
})

View File

@@ -438,7 +438,7 @@ test('AI advice hides generic auto review summaries when a specific hotel over-s
})
assert.equal(riskCards.length, 1)
assert.equal(riskCards[0].title, '第 1 条AI提示住宿金额超出报销标准')
assert.equal(riskCards[0].title, '住宿金额超出报销标准')
assert.equal(riskCards[0].tone, 'high')
})
@@ -636,7 +636,7 @@ test('route-level risk cards keep related item ids for every affected expense ro
assert.equal(riskCards.length, 1)
assert.deepEqual(riskCards[0].itemIds, ['travel-item-2', 'travel-item-3'])
assert.equal(riskCards[0].title, '第 2、3 条:多城市行程待说明')
assert.equal(riskCards[0].title, '多城市行程待说明')
assert.match(detailViewScript, /cardItemIds\.includes\(itemId\)/)
})
@@ -810,10 +810,35 @@ test('expense detail table shows each item filled time from item creation time',
test('expense detail table has per-item risk explanation column', () => {
assert.match(detailViewTemplate, /<th class="col-risk-note">异常说明<\/th>/)
assert.match(detailViewTemplate, /<EnterpriseSelect[\s\S]*class="editor-select"/)
assert.match(detailViewTemplate, /<ElDatePicker[\s\S]*v-model="expenseEditor\.itemDate"/)
assert.match(detailViewTemplate, /<ElInput[\s\S]*v-model="expenseEditor\.itemReason"/)
assert.match(detailViewTemplate, /<ElInput[\s\S]*v-model="expenseEditor\.itemAmount"/)
assert.match(detailViewTemplate, /<ElInput[\s\S]*v-model="expenseEditor\.itemNote"[\s\S]*type="textarea"[\s\S]*:rows="1"/)
assert.doesNotMatch(detailViewTemplate, /<input[\s\S]*v-model="expenseEditor\./)
assert.doesNotMatch(detailViewTemplate, /<textarea[\s\S]*v-model="expenseEditor\./)
assert.match(detailViewScript, /import \{ ElDatePicker \} from 'element-plus\/es\/components\/date-picker\/index\.mjs'/)
assert.match(detailViewScript, /import \{ ElInput \} from 'element-plus\/es\/components\/input\/index\.mjs'/)
assert.match(detailViewScript, /ElDatePicker,[\s\S]*ElInput,/)
assert.match(detailViewTemplate, /v-model="expenseEditor\.itemNote"/)
assert.match(detailViewTemplate, /class="editor-textarea risk-note-editor-textarea"[\s\S]*rows="1"/)
assert.match(detailViewTemplate, /@input="resizeExpenseNoteInput"/)
assert.match(detailViewStyle, /\.risk-note-editor-textarea[\s\S]*max-height: 78px/)
assert.doesNotMatch(detailViewTemplate, /@input="resizeExpenseNoteInput"/)
assert.doesNotMatch(detailViewTemplate, /用于说明改签、绕行、超标、票据异常等情况/)
assert.match(detailViewStyle, /\.detail-expense-table \.col-type \{ width: 14%; \}/)
assert.match(detailViewStyle, /\.detail-expense-table \.col-attachment \{ width: 15%; \}/)
assert.match(detailViewStyle, /\.detail-expense-table \{[\s\S]*--expense-editor-control-height: 34px;[\s\S]*--expense-editor-control-line-height: 16px;/)
assert.match(detailViewStyle, /\.editor-control \{/)
assert.match(detailViewStyle, /\.editor-control:not\(\.risk-note-editor-input\),[\s\S]*\.editor-select \{[\s\S]*height: var\(--expense-editor-control-height\);/)
assert.match(detailViewStyle, /\.editor-control :deep\(\.el-input__wrapper\),[\s\S]*\.editor-date-picker :deep\(\.el-input__wrapper\) \{/)
assert.match(detailViewStyle, /\.editor-control :deep\(\.el-input__wrapper\),[\s\S]*height: var\(--expense-editor-control-height\);[\s\S]*line-height: var\(--expense-editor-control-line-height\);/)
assert.match(detailViewStyle, /\.editor-control :deep\(\.el-input__inner\),[\s\S]*height: var\(--expense-editor-control-line-height\)( !important)?;[\s\S]*line-height: var\(--expense-editor-control-line-height\)( !important)?;/)
assert.match(detailViewStyle, /\.editor-control :deep\(\.el-input__prefix\),[\s\S]*height: var\(--expense-editor-control-height\);/)
assert.doesNotMatch(detailViewStyle, /\.editor-input,\s*\.editor-select,\s*\.editor-textarea \{/)
assert.match(detailViewStyle, /\.editor-select \{[\s\S]*padding: 0;[\s\S]*border: 0;/)
assert.match(detailViewStyle, /\.editor-select :deep\(\.el-select__wrapper\) \{[\s\S]*min-height: var\(--expense-editor-control-height\);[\s\S]*height: var\(--expense-editor-control-height\);/)
assert.match(detailViewStyle, /\.risk-note-editor-input\.el-textarea \{[\s\S]*min-height: var\(--expense-editor-control-height\);[\s\S]*height: var\(--expense-editor-control-height\);/)
assert.match(detailViewStyle, /\.risk-note-editor-input :deep\(\.el-textarea__inner\) \{[\s\S]*min-height: var\(--expense-editor-control-height\) !important;[\s\S]*height: var\(--expense-editor-control-height\);[\s\S]*line-height: var\(--expense-editor-control-line-height\)( !important)?;[\s\S]*max-height: calc\(var\(--expense-editor-control-height\) \+ var\(--expense-editor-control-line-height\) \* 2\)( !important)?;[\s\S]*resize: none( !important)?;/)
assert.doesNotMatch(detailViewScript, /resizeExpenseNoteInput/)
assert.doesNotMatch(detailViewScript, /scrollHeight/)
assert.match(detailViewTemplate, /hasExpenseRiskOrAbnormal\(item\)[\s\S]*待补充异常说明/)
assert.match(detailViewScript, /itemNote: ''/)
assert.match(detailViewScript, /expenseEditor\.itemNote = item\.itemNote \|\| ''/)

View File

@@ -7,6 +7,7 @@ import {
ASSISTANT_SCOPE_SESSION_APPLICATION,
ASSISTANT_SCOPE_SESSION_EXPENSE,
ASSISTANT_SCOPE_SESSION_KNOWLEDGE,
ASSISTANT_SCOPE_SESSION_STEWARD,
inferAssistantScopeTarget
} from '../src/utils/assistantSessionScope.js'
import {
@@ -48,6 +49,10 @@ test('workbench prompt applies travel phrases to application assistant scope', (
inferAssistantScopeTarget('准备去国网现场做仿生产环境部署差旅3天'),
ASSISTANT_SCOPE_SESSION_APPLICATION
)
assert.equal(
inferAssistantScopeTarget('2月20-23日去上海出差辅助国网仿生产环境部署'),
ASSISTANT_SCOPE_SESSION_STEWARD
)
assert.equal(
inferAssistantScopeTarget('我要报销去北京的费用'),
ASSISTANT_SCOPE_SESSION_EXPENSE
@@ -103,6 +108,14 @@ test('workbench model routing maps ontology result before entering assistant', (
),
ASSISTANT_SCOPE_SESSION_APPLICATION
)
assert.equal(
resolveWorkbenchSessionTypeFromOntology(
travelOntology,
'2月20-23日去上海出差辅助国网仿生产环境部署',
ASSISTANT_SCOPE_SESSION_APPLICATION
),
ASSISTANT_SCOPE_SESSION_STEWARD
)
assert.equal(
resolveWorkbenchSessionTypeFromOntology(
reimbursementOntology,
@@ -128,3 +141,16 @@ test('workbench model routing maps ontology result before entering assistant', (
ASSISTANT_SCOPE_SESSION_APPLICATION
)
})
test('workbench ambiguous travel flow uses steward fast path before ontology parsing', () => {
const fastPathIndex = appShellComposable.indexOf(
'fallbackSessionType === ASSISTANT_SCOPE_SESSION_STEWARD'
)
const ontologyParseIndex = appShellComposable.indexOf('fetchOntologyParse(')
assert.ok(fastPathIndex >= 0, 'expected steward fallback fast path in smart entry routing')
assert.ok(
fastPathIndex < ontologyParseIndex,
'expected steward fallback to return before slow ontology parsing'
)
})