feat: 新增归档中心页面并完善知识库与报销查询能力

新增前端归档中心视图及相关工具函数,扩充知识库文档分类和
提取器支持多种格式,增强编排器报销查询的多维度检索,优
化本体规则和用户代理审核消息,前端完善报销创建和审批详
情交互细节,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-22 16:00:19 +08:00
parent 1f15699013
commit 88ff04bef8
120 changed files with 6236 additions and 643 deletions

View File

@@ -3,6 +3,8 @@ import { readFileSync } from 'node:fs'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import { buildReviewPlainFollowupCopy } from '../src/views/scripts/travelReimbursementReviewModel.js'
const createViewTemplate = readFileSync(
fileURLToPath(new URL('../src/views/TravelReimbursementCreateView.vue', import.meta.url)),
'utf8'
@@ -19,6 +21,10 @@ const reviewActionsScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementReviewActions.js', import.meta.url)),
'utf8'
)
const reviewDrawerScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementReviewDrawer.js', import.meta.url)),
'utf8'
)
const submitComposerScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSubmitComposer.js', import.meta.url)),
'utf8'
@@ -29,7 +35,7 @@ const attachmentsScript = readFileSync(
)
test('review drawer tools expose the default review tab before conditional document and risk tabs', () => {
assert.match(createViewTemplate, /title="报销识别核对"[\s\S]*@click="switchToReviewOverviewDrawer"/)
assert.match(createViewTemplate, /v-if="activeReviewPayload && reviewOverviewDrawerAvailable"[\s\S]*title="报销识别核对"[\s\S]*@click="switchToReviewOverviewDrawer"/)
assert.match(createViewTemplate, /v-if="activeReviewPayload && reviewDocumentDrawerAvailable"[\s\S]*title="单据识别"/)
assert.match(createViewTemplate, /v-if="activeReviewPayload && reviewRiskDrawerAvailable"[\s\S]*title="显示风险"/)
assert.match(createViewTemplate, /title="调用流程"/)
@@ -91,13 +97,22 @@ test('review risk drawer lists risk briefs without score and posts details into
createViewScript,
/function appendReviewRiskBriefToConversation\(item\) \{[\s\S]*messages\.value\.push\(createMessage\('assistant'/
)
assert.match(createViewScript, /function buildReviewRiskConversationText\(item, detailTarget = \{\}\)/)
assert.match(createViewScript, /function resolveReviewRiskDetailTarget\(\) \{[\s\S]*router\.resolve\(\{[\s\S]*name: 'app-request-detail'/)
assert.match(createViewScript, /进入 \$\{claimNo\} 详情重新填写/)
assert.match(createViewTemplate, /class="expense-query-risk-row"[\s\S]*appendExpenseQueryRiskToConversation\(record, risk\)/)
assert.match(createViewScript, /function appendExpenseQueryRiskToConversation\(record, risk\) \{[\s\S]*进入 \$\{claimNo\} 详情重新填写/)
})
test('review payload with risks opens risk drawer and travel overview uses travel-specific fields', () => {
assert.match(
createViewScript,
/reviewDrawerMode\.value = resolveReviewRiskBriefs\(payload\)\.length[\s\S]*\? REVIEW_DRAWER_MODE_RISK[\s\S]*: REVIEW_DRAWER_MODE_REVIEW/
)
test('review drawer default mode is scoped by the current action and travel overview uses travel-specific fields', () => {
assert.match(reviewDrawerScript, /activeReviewPanelScope/)
assert.match(reviewDrawerScript, /const reviewOverviewDrawerAvailable = computed\(\(\) => normalizedReviewPanelScope\.value === 'overview'\)/)
assert.match(reviewDrawerScript, /scope === 'documents' && hasDocuments[\s\S]*REVIEW_DRAWER_MODE_DOCUMENTS/)
assert.match(reviewDrawerScript, /scope === 'risk' && hasRisks[\s\S]*REVIEW_DRAWER_MODE_RISK/)
assert.match(reviewDrawerScript, /scope === 'overview'[\s\S]*REVIEW_DRAWER_MODE_REVIEW/)
assert.match(createViewScript, /function normalizeReviewPanelScope\(scope\)/)
assert.match(createViewScript, /canExposeReviewPanelScope\(item\.reviewPanelScope\)/)
assert.match(createViewScript, /currentInsight\.value\.intent === 'agent' && agent[\s\S]*return null/)
assert.match(createViewScript, /function isTravelReviewPayload\(reviewPayload/)
assert.match(createViewScript, /function resolveReviewTravelTransportType\(reviewPayload/)
assert.match(createViewScript, /label: '交通类型'[\s\S]*modelKey: 'transport_type'/)
@@ -107,6 +122,51 @@ test('review payload with risks opens risk drawer and travel overview uses trave
assert.match(createViewTemplate, /wide: item\.wide/)
})
test('submit composer scopes the side panel to intent overview, document upload, or triggered risk only', () => {
assert.match(submitComposerScript, /function resolveReviewPanelScope\(\{[\s\S]*reviewPayload = null/)
assert.match(submitComposerScript, /fileCount > 0 && documentCount > 0[\s\S]*return 'documents'/)
assert.match(submitComposerScript, /riskCount > 0 && \(asksRisk \|\| \['next_step', 'submit', 'submit_claim'\]\.includes\(normalizedAction\)\)[\s\S]*return 'risk'/)
assert.match(submitComposerScript, /!normalizedAction && fileCount === 0[\s\S]*return 'overview'/)
assert.match(submitComposerScript, /reviewPanelScope: resolveReviewPanelScope\(\{/)
assert.match(submitComposerScript, /nextInsight\.agent\.reviewPanelScope = assistantMessage\.reviewPanelScope/)
})
test('expense query answers keep one clear result structure with reimbursement center jump link', () => {
assert.match(createViewTemplate, /!message\.reviewPayload && !message\.queryPayload && message\.meta\?\.length/)
assert.match(createViewTemplate, /!message\.reviewPayload && !message\.queryPayload && message\.suggestedActions\?\.length/)
assert.match(createViewTemplate, /!message\.reviewPayload && !message\.queryPayload && message\.citations\?\.length/)
assert.match(createViewTemplate, /message\.queryPayload\.title \|\| \(message\.queryPayload\.selectionMode === 'draft_association' \? '选择关联草稿' : '最近 5 条筛选结果'\)/)
assert.match(createViewTemplate, /v-html="renderMarkdown\(buildExpenseQueryHint\(message\.queryPayload\)\)"/)
assert.match(createViewScript, /href\.startsWith\('\/app\/'\)[\s\S]*router\.push\(href\)/)
})
test('backend query response suppresses generic query actions and supports archived filter title', () => {
const responseScript = readFileSync(
fileURLToPath(new URL('../../server/src/app/services/user_agent_response.py', import.meta.url)),
'utf8'
)
const queryScript = readFileSync(
fileURLToPath(new URL('../../server/src/app/services/orchestrator_expense_query.py', import.meta.url)),
'utf8'
)
assert.match(responseScript, /if payload\.ontology\.intent in \{"query", "compare"\}:[\s\S]*return \[\]/)
assert.match(responseScript, /下面先列出最近 \{query_payload\.preview_count\} 条记录/)
assert.match(queryScript, /EXPENSE_QUERY_PREVIEW_LIMIT = 5/)
assert.match(queryScript, /"归档"[\s\S]*"archived"/)
assert.match(queryScript, /ExpenseClaim\.approval_stage\.ilike\("%归档%"\)/)
assert.match(queryScript, /"title": f"最近 \{len\(preview_claims\)\} 条\{scope_label\}"/)
})
test('closing the assistant while OCR is running defers unmount until the current flow finishes', () => {
assert.match(createViewScript, /const closeAfterBusy = ref\(false\)/)
assert.match(createViewScript, /function isWorkbenchBusy\(\) \{[\s\S]*submitting\.value \|\| reviewActionBusy\.value \|\| sessionSwitchBusy\.value/)
assert.match(createViewScript, /function maybeFinalizeDeferredClose\(\) \{[\s\S]*!closeAfterBusy\.value \|\| workbenchVisible\.value \|\| isWorkbenchBusy\(\)/)
assert.match(createViewScript, /function requestCloseWorkbench\(\) \{[\s\S]*closeAfterBusy\.value = isWorkbenchBusy\(\)[\s\S]*workbenchVisible\.value = false/)
assert.match(createViewScript, /function emitCloseAfterLeave\(\) \{[\s\S]*closeAfterBusy\.value && isWorkbenchBusy\(\)[\s\S]*return/)
assert.match(createViewScript, /\[submitting\.value, reviewActionBusy\.value, sessionSwitchBusy\.value, workbenchVisible\.value\][\s\S]*maybeFinalizeDeferredClose\(\)/)
})
test('composer exposes travel calculator and posts spreadsheet-backed result into conversation', () => {
assert.match(createViewTemplate, /class="tool-btn composer-side-btn travel-calculator-trigger"[\s\S]*差旅计算器/)
assert.match(createViewTemplate, /class="travel-calculator-popover"[\s\S]*v-model="travelCalculatorForm\.days"[\s\S]*v-model="travelCalculatorForm\.location"/)
@@ -188,3 +248,26 @@ test('review summary renders markdown and save draft relies on backend response
/messages\.value\.push\(\s*createMessage\('assistant', actionConfig\.successMessage/
)
})
test('saved draft review messages stop showing the save-draft prompt', () => {
const reviewPayload = {
slot_cards: [
{ key: 'amount', label: '金额', title: '金额', status: 'missing', required: true },
{ key: 'attachments', label: '票据状态', title: '票据状态', status: 'missing', required: true }
],
missing_slots: ['金额', '票据附件'],
risk_briefs: [],
confirmation_actions: [
{ label: '保存为草稿', action_type: 'save_draft' }
]
}
const followup = buildReviewPlainFollowupCopy(reviewPayload, { savedDraft: true })
assert.equal(followup.lead, '补充信息:')
assert.match(followup.summary, /草稿/)
assert.match(followup.summary, /关联|补充|提交/)
assert.doesNotMatch(followup.summary, /点击|点“草稿”|保存为草稿|临时保存|暂存/)
assert.match(createViewTemplate, /buildReviewPlainFollowupForMessage\(message\)/)
assert.match(createViewScript, /function isDraftSavedReviewMessage\(message\)/)
assert.match(createViewScript, /function canUseInlineSaveDraft\(message\)[\s\S]*isDraftSavedReviewMessage\(message\)/)
})