feat: 新增归档中心页面并完善知识库与报销查询能力
新增前端归档中心视图及相关工具函数,扩充知识库文档分类和 提取器支持多种格式,增强编排器报销查询的多维度检索,优 化本体规则和用户代理审核消息,前端完善报销创建和审批详 情交互细节,补充单元测试覆盖。
This commit is contained in:
@@ -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\)/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user