feat: 增强知识库索引与设置页面模块化拆分
扩展知识库索引任务和 RAG 检索支持增量入库和文档去重,优 化本体检测和规则匹配精度,前端设置页面拆分为 LLM、邮件 和 Hermes 员工同步子面板并重构样式,新增日志详情组件和 知识入库日志模型,补充单元测试覆盖。
This commit is contained in:
@@ -3,7 +3,14 @@ import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { buildReviewPlainFollowupCopy } from '../src/views/scripts/travelReimbursementReviewModel.js'
|
||||
import {
|
||||
buildLocallySyncedReviewPayload,
|
||||
buildReviewNextStepRichCopy,
|
||||
buildReviewPlainFollowupCopy,
|
||||
isTravelReviewPayload,
|
||||
resolveReviewFooterActions
|
||||
} from '../src/views/scripts/travelReimbursementReviewModel.js'
|
||||
import { renderMarkdown } from '../src/utils/markdown.js'
|
||||
|
||||
const createViewTemplate = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/TravelReimbursementCreateView.vue', import.meta.url)),
|
||||
@@ -33,6 +40,26 @@ const attachmentsScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementAttachments.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const sessionStateScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSessionState.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const createViewBaseStyles = readFileSync(
|
||||
fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view.css', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const createViewPart2Styles = readFileSync(
|
||||
fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view-part2.css', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const createViewPart3Styles = readFileSync(
|
||||
fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view-part3.css', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const createViewPart4Styles = readFileSync(
|
||||
fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view-part4.css', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
test('review drawer tools expose the default review tab before conditional document and risk tabs', () => {
|
||||
assert.match(createViewTemplate, /v-if="activeReviewPayload && reviewOverviewDrawerAvailable"[\s\S]*title="报销识别核对"[\s\S]*@click="switchToReviewOverviewDrawer"/)
|
||||
@@ -58,6 +85,157 @@ test('review drawer tool buttons switch modes instead of toggling the active mod
|
||||
assert.doesNotMatch(createViewScript, /REVIEW_DRAWER_MODE_FLOW\s*\?\s*REVIEW_DRAWER_MODE_REVIEW/)
|
||||
})
|
||||
|
||||
test('document review drawer fills sidebar height and preview dialog is centered', () => {
|
||||
assert.match(createViewTemplate, /class="insight-body"[\s\S]*:class="\{ 'document-review-body': isReviewDocumentDrawer \}"/)
|
||||
assert.match(createViewBaseStyles, /\.insight-panel-shell\s*\{[\s\S]*display:\s*flex;[\s\S]*min-height:\s*0;/)
|
||||
assert.match(createViewPart2Styles, /\.insight-body\.document-review-body\s*\{[\s\S]*display:\s*flex;[\s\S]*overflow:\s*hidden;/)
|
||||
assert.match(createViewPart2Styles, /\.review-ticket-drawer\s*\{[\s\S]*grid-template-rows:\s*auto minmax\(0,\s*1fr\);[\s\S]*height:\s*100%;[\s\S]*overflow:\s*hidden;/)
|
||||
assert.match(createViewPart2Styles, /\.review-document-stage\s*\{[\s\S]*grid-template-rows:\s*auto auto minmax\(0,\s*1fr\);/)
|
||||
assert.match(createViewPart2Styles, /\.review-document-scroll\s*\{[\s\S]*max-height:\s*none;[\s\S]*min-height:\s*0;/)
|
||||
assert.match(createViewPart2Styles, /\.review-document-preview-card\.image\s*\{[\s\S]*place-items:\s*center;[\s\S]*min-height:\s*220px;/)
|
||||
assert.match(createViewPart2Styles, /\.review-document-preview-card\.image img\s*\{[\s\S]*height:\s*auto;[\s\S]*object-fit:\s*contain;/)
|
||||
assert.doesNotMatch(createViewPart2Styles, /\.review-document-preview-card\.image img\s*\{[\s\S]*object-fit:\s*cover;/)
|
||||
assert.match(createViewPart4Styles, /\.review-overlay\s*\{[\s\S]*align-items:\s*center;[\s\S]*justify-content:\s*center;/)
|
||||
assert.match(createViewPart4Styles, /\.review-preview-modal\s*\{[\s\S]*margin:\s*auto;[\s\S]*flex:\s*none;/)
|
||||
})
|
||||
|
||||
test('document preview avoids restored stale object urls', () => {
|
||||
assert.match(createViewTemplate, /v-if="documentPreviewDialog\.kind === 'image'"[\s\S]*:key="documentPreviewDialog\.renderKey"/)
|
||||
assert.match(createViewTemplate, /v-else-if="documentPreviewDialog\.kind === 'pdf'"[\s\S]*:key="documentPreviewDialog\.renderKey"/)
|
||||
assert.match(reviewDrawerScript, /renderKey:\s*''/)
|
||||
assert.match(reviewDrawerScript, /renderKey:\s*\[[\s\S]*Date\.now\(\)[\s\S]*\]\.join\('__'\)/)
|
||||
assert.match(attachmentsScript, /isTemporaryPreviewUrl/)
|
||||
assert.match(attachmentsScript, /existingPreview\?\.url && !isTemporaryPreviewUrl\(existingPreview\.url\)/)
|
||||
assert.match(sessionStateScript, /filterPersistableFilePreviews\(state\.reviewFilePreviews\)/)
|
||||
assert.doesNotMatch(sessionStateScript, /filterPersistableFilePreviews\(nextState\.reviewFilePreviews\)/)
|
||||
})
|
||||
|
||||
test('local transport review no longer uses the travel hotel template', () => {
|
||||
const reviewPayload = {
|
||||
slot_cards: [
|
||||
{
|
||||
key: 'expense_type',
|
||||
label: '报销类型',
|
||||
value: '交通费',
|
||||
normalized_value: 'transport',
|
||||
status: 'identified'
|
||||
}
|
||||
],
|
||||
document_cards: [
|
||||
{
|
||||
document_type: 'taxi_receipt',
|
||||
suggested_expense_type: 'transport',
|
||||
scene_label: '交通费'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
assert.equal(isTravelReviewPayload(reviewPayload, { expense_type: '交通费' }), false)
|
||||
assert.match(createViewScript, /shouldShowReviewFactCard\(reviewPayload, 'customer_name'/)
|
||||
assert.doesNotMatch(
|
||||
createViewScript,
|
||||
/key:\s*'customer_name'[\s\S]{0,220}placeholder:\s*'请输入客户名称'[\s\S]{0,80}\},\s*\{[\s\S]{0,80}key:\s*'attachments'/
|
||||
)
|
||||
assert.match(createViewTemplate, /placeholder="例如:出租车\/网约车票据 \/ 火车\/高铁票"/)
|
||||
assert.doesNotMatch(createViewTemplate, /票据场景[\s\S]{0,260}例如:业务招待费 \/ 差旅费/)
|
||||
})
|
||||
|
||||
test('local save of changed reimbursement category updates edit fields too', () => {
|
||||
const nextPayload = buildLocallySyncedReviewPayload(
|
||||
{
|
||||
can_proceed: false,
|
||||
edit_fields: [
|
||||
{ key: 'expense_type', label: '报销分类', value: '交通费' },
|
||||
{ key: 'reason', label: '事由', value: '打车去客户现场' }
|
||||
],
|
||||
slot_cards: [
|
||||
{
|
||||
key: 'expense_type',
|
||||
label: '报销类型',
|
||||
value: '交通费',
|
||||
normalized_value: 'transport',
|
||||
required: true,
|
||||
status: 'identified'
|
||||
}
|
||||
],
|
||||
confirmation_actions: []
|
||||
},
|
||||
{
|
||||
expense_type: '办公用品费',
|
||||
reason_value: '右侧核对后改为办公用品费'
|
||||
}
|
||||
)
|
||||
|
||||
const expenseTypeField = nextPayload.edit_fields.find((item) => item.key === 'expense_type')
|
||||
assert.equal(expenseTypeField.value, '办公用品费')
|
||||
assert.equal(nextPayload.slot_cards[0].value, '办公用品费')
|
||||
})
|
||||
|
||||
test('next step action uses rich text guidance and confirm dialog instead of footer button', () => {
|
||||
const reviewPayload = {
|
||||
can_proceed: true,
|
||||
risk_briefs: [
|
||||
{ level: 'low', title: '票据提示', content: '普通提示' }
|
||||
],
|
||||
confirmation_actions: [
|
||||
{ action_type: 'save_draft', label: '保存为草稿' },
|
||||
{ action_type: 'next_step', label: '继续下一步', emphasis: 'primary' }
|
||||
]
|
||||
}
|
||||
const copy = buildReviewNextStepRichCopy(reviewPayload, { detailHref: '/app/requests/claim-1' })
|
||||
const rendered = renderMarkdown(copy)
|
||||
|
||||
assert.match(copy, /系统识别您的单据已经填写完所有已知信息/)
|
||||
assert.match(copy, /现存在 1 条低风险,0 条中风险,0 条高风险/)
|
||||
assert.doesNotMatch(copy, /#review-risk-low/)
|
||||
assert.doesNotMatch(copy, /#review-risk-medium/)
|
||||
assert.doesNotMatch(copy, /#review-risk-high/)
|
||||
assert.match(copy, /\[右侧\]\(#review-risk-panel\) 风险信息提示窗/)
|
||||
assert.match(copy, /\[继续下一步\]\(#review-next-step\)/)
|
||||
assert.match(copy, /\[快速修改单据信息\]\(\/app\/requests\/claim-1\)/)
|
||||
assert.doesNotMatch(rendered, /markdown-risk-link-/)
|
||||
assert.match(rendered, /<span class="markdown-risk-text-low">低风险<\/span>/)
|
||||
assert.match(rendered, /<span class="markdown-risk-text-medium">中风险<\/span>/)
|
||||
assert.match(rendered, /<span class="markdown-risk-text-high">高风险<\/span>/)
|
||||
assert.doesNotMatch(rendered, /href="#review-risk-low"/)
|
||||
assert.doesNotMatch(rendered, /href="#review-risk-medium"/)
|
||||
assert.doesNotMatch(rendered, /href="#review-risk-high"/)
|
||||
assert.match(rendered, /markdown-action-link-risk/)
|
||||
assert.match(rendered, /markdown-action-link-next/)
|
||||
assert.deepEqual(resolveReviewFooterActions(reviewPayload), [])
|
||||
|
||||
const highRiskCopy = buildReviewNextStepRichCopy(
|
||||
{
|
||||
...reviewPayload,
|
||||
risk_briefs: [{ level: 'high', title: '金额超标' }]
|
||||
},
|
||||
{ detailHref: '/app/requests/claim-1' }
|
||||
)
|
||||
assert.doesNotMatch(highRiskCopy, /\[继续下一步\]\(#review-next-step\)/)
|
||||
|
||||
assert.match(createViewTemplate, /class="review-next-step-rich-copy message-answer-markdown"[\s\S]*renderMarkdown\(buildReviewNextStepRichCopyForMessage\(message\)\)/)
|
||||
assert.match(createViewTemplate, /class="message-bubble" :class="buildMessageBubbleClass\(message\)"/)
|
||||
assert.match(createViewTemplate, /:open="nextStepConfirmDialog\.open"[\s\S]*title="确认提交当前单据?"[\s\S]*confirm-text="确认提交"/)
|
||||
assert.match(createViewScript, /const REVIEW_NEXT_STEP_HREF = '#review-next-step'/)
|
||||
assert.match(createViewScript, /buildReviewRiskLevelCounts/)
|
||||
assert.match(createViewScript, /function buildMessageBubbleClass\(message\)/)
|
||||
assert.match(createViewScript, /message-bubble-review-risk-high/)
|
||||
assert.match(createViewScript, /message-bubble-review-risk-medium/)
|
||||
assert.match(createViewScript, /message-bubble-review-risk-low/)
|
||||
assert.match(createViewScript, /function openReviewNextStepConfirm\(message\)/)
|
||||
assert.match(createViewScript, /async function confirmReviewNextStepSubmit\(\)/)
|
||||
assert.match(createViewScript, /href === REVIEW_NEXT_STEP_HREF[\s\S]*openReviewNextStepConfirm\(message\)/)
|
||||
assert.match(createViewScript, /href\.startsWith\(REVIEW_RISK_PANEL_HREF_PREFIX\)[\s\S]*switchReviewDrawerMode\(REVIEW_DRAWER_MODE_RISK\)/)
|
||||
assert.match(createViewBaseStyles, /\.message-bubble-review-risk-low\s*\{[\s\S]*border-color:\s*rgba\(37,\s*99,\s*235,/)
|
||||
assert.match(createViewBaseStyles, /\.message-bubble-review-risk-medium\s*\{[\s\S]*border-color:\s*rgba\(217,\s*119,\s*6,/)
|
||||
assert.match(createViewBaseStyles, /\.message-bubble-review-risk-high\s*\{[\s\S]*border-color:\s*rgba\(220,\s*38,\s*38,/)
|
||||
assert.doesNotMatch(createViewBaseStyles, /markdown-risk-link-low/)
|
||||
assert.match(createViewBaseStyles, /\.markdown-risk-text-low[\s\S]*color:\s*#2563eb/)
|
||||
assert.match(createViewBaseStyles, /\.markdown-risk-text-medium[\s\S]*color:\s*#d97706/)
|
||||
assert.match(createViewBaseStyles, /\.markdown-risk-text-high[\s\S]*color:\s*#dc2626/)
|
||||
assert.match(createViewPart3Styles, /\.review-next-step-rich-copy\s*\{[\s\S]*margin-top:\s*30px;/)
|
||||
})
|
||||
|
||||
test('review risk drawer lists risk briefs without score and posts details into the conversation', () => {
|
||||
const riskItemsBlock = createViewScript.match(/function buildReviewRiskItems\(reviewPayload\) \{[\s\S]*?\n\}\n\nfunction buildReviewRiskConversationText/)
|
||||
assert.ok(riskItemsBlock, 'risk item builder should be present')
|
||||
@@ -80,8 +258,12 @@ test('review risk drawer lists risk briefs without score and posts details into
|
||||
)
|
||||
assert.doesNotMatch(createViewTemplate, /\{\{\s*item\.levelLabel\s*\}\}/)
|
||||
assert.match(createViewTemplate, /class="review-side-risk-icon" :title="item\.levelLabel"/)
|
||||
assert.match(createViewScript, /info:\s*\{[\s\S]*label:\s*'提示'/)
|
||||
assert.match(createViewScript, /medium:\s*\{[\s\S]*label:\s*'中风险'/)
|
||||
assert.match(createViewScript, /low:\s*\{[\s\S]*label:\s*'低风险'/)
|
||||
assert.match(createViewScript, /const isInfo = String\(item\?\.level \|\| ''\)\.trim\(\) === 'info'/)
|
||||
assert.match(createViewScript, /\$\{isInfo \? '提示内容' : '风险点'\}:\$\{summary\}/)
|
||||
assert.match(createViewScript, /\$\{isInfo \? '处理建议' : '修改建议'\}:\$\{suggestion\}/)
|
||||
assert.match(createViewScript, /function normalizeReviewRiskTitle/)
|
||||
assert.match(createViewScript, /\.replace\(\/AI\\s\*预审/)
|
||||
assert.match(createViewScript, /\.replace\(\/\(高风险\|中风险\|低风险\)\/g,\s*''\)/)
|
||||
@@ -98,7 +280,8 @@ test('review risk drawer lists risk briefs without score and posts details into
|
||||
/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, /function resolveReviewDetailTarget\(message = null\) \{[\s\S]*router\.resolve\(\{[\s\S]*name: 'app-request-detail'/)
|
||||
assert.match(createViewScript, /function resolveReviewRiskDetailTarget\(\) \{[\s\S]*return resolveReviewDetailTarget\(\)/)
|
||||
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\} 详情重新填写/)
|
||||
|
||||
Reference in New Issue
Block a user