feat(web): AI 文档查询卡片重构与单号判定统一

- documentClassification 抽出 isApplicationDocumentNo,统一兼容 AP-/APP- 旧格式与 A+8 新格式,aiDocumentQueryModel 复用
- aiDocumentQueryModel 文档卡片改为结构化字段布局(单据类型/金额/申请人/编号/操作),新增查询范围摘要区,渲染走 HTML 信任块
- AppShellRouteView/useAppShell/useRequests/detailAlerts/riskVisibility 等差旅详情模型适配单号判定
- 同步更新 ai-document-query-model/workbench-ai-mode-switch 测试,新增 document-classification 测试
This commit is contained in:
caoxiaozhu
2026-06-20 22:04:37 +08:00
parent 8158716e23
commit 3b74a330a3
17 changed files with 348 additions and 209 deletions

View File

@@ -127,24 +127,33 @@ test('AI document query message renders html document cards with detail actions'
assert.match(message, /### 已查询到相关单据/)
assert.match(message, /<!-- ai-trusted-html:start -->/)
assert.match(message, /<section class="ai-document-query-summary" aria-label="单据查询范围">/)
assert.match(message, /<span class="ai-document-query-summary__label">查询范围<\/span>/)
assert.match(message, /<strong class="ai-document-query-summary__scope">/)
assert.match(message, /<span class="ai-document-query-summary__count">/)
assert.match(message, /<section class="ai-document-card-list" aria-label="单据查询结果">/)
// 申请单 app-1 状态为 approved → is-success 语义类
assert.match(message, /<article class="ai-document-card ai-document-card--application is-success" aria-label="单据详情">/)
assert.match(message, /<header class="ai-document-card__head">/)
assert.match(message, /<span class="ai-document-card__status">已审批<\/span>/)
assert.match(message, /<strong class="ai-document-card__reason">辅助国网仿生产服务器部署<\/strong>/)
assert.match(message, /<span class="ai-document-card__owner">曹小筑<\/span>/)
assert.match(message, /<span class="ai-document-card__dept">交付部<\/span>/)
assert.match(message, /<span class="ai-document-card__number">AP-20260220001<\/span>/)
assert.match(message, /<div class="ai-document-card__details">/)
assert.match(message, /<span class="ai-document-card__label">单据类型<\/span>/)
assert.match(message, /<strong class="ai-document-card__value">申请单 · 差旅费用申请<\/strong>/)
assert.match(message, /<span class="ai-document-card__label">申请人<\/span>/)
assert.match(message, /<strong class="ai-document-card__value">曹小筑 · 交付部<\/strong>/)
assert.match(message, /<strong class="ai-document-card__value ai-document-card__number">AP-20260220001<\/strong>/)
assert.match(message, /<strong class="ai-document-card__amount">¥3,000\.00<\/strong>/)
assert.match(message, /<div class="ai-document-card__meta">/)
assert.match(message, /<span class="ai-document-card__meta-item">上海<\/span>/)
assert.match(message, /<div class="ai-document-card__field ai-document-card__field--action">/)
assert.doesNotMatch(message, /ai-document-card__meta/)
assert.doesNotMatch(message, /ai-document-card__meta-item/)
assert.match(message, /href="#ai-open-document-detail:AP-20260220001"/)
// 报销单 claim-1 状态为 submitted → is-pending 语义类
assert.match(message, /<article class="ai-document-card ai-document-card--reimbursement is-pending" aria-label="单据详情">/)
assert.match(message, /href="#ai-open-document-detail:CL-20260221001"/)
assert.doesNotMatch(message, /\| 单据编号 \|/)
assert.doesNotMatch(message, /^> /m)
assert.doesNotMatch(message, /\*\*查询范围\*\*/)
})
test('AI document query html cards render as trusted card markup', () => {
@@ -152,13 +161,16 @@ test('AI document query html cards render as trusted card markup', () => {
const rendered = renderAiConversationHtml(buildAiDocumentQueryMessage(intent, claims))
assert.match(rendered, /<h3 class="ai-html-title">已查询到相关单据<\/h3>/)
assert.match(rendered, /<section class="ai-document-query-summary" aria-label="单据查询范围">/)
assert.match(rendered, /<section class="ai-document-card-list" aria-label="单据查询结果">/)
assert.match(rendered, /<article class="ai-document-card ai-document-card--application is-success" aria-label="单据详情">/)
assert.match(rendered, /class="ai-document-card__head"/)
assert.match(rendered, /class="ai-document-card__meta"/)
assert.match(rendered, /class="ai-document-card__meta-item"/)
assert.match(rendered, /class="ai-document-card__details"/)
assert.match(rendered, /class="ai-document-card__field"/)
assert.match(rendered, /class="ai-document-card__label"/)
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-document ai-document-card__action"/)
assert.match(rendered, /href="#ai-open-document-detail:CL-20260221001"/)
assert.doesNotMatch(rendered, /ai-document-card__meta/)
assert.doesNotMatch(rendered, /&lt;section class=&quot;ai-document-card-list/)
assert.doesNotMatch(rendered, /<blockquote>/)
})

View File

@@ -0,0 +1,20 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
isApplicationDocumentNo,
isApplicationRequestLike
} from '../src/utils/documentClassification.js'
test('application document number detection supports short and legacy formats', () => {
assert.equal(isApplicationDocumentNo('A7K3M9Q2P'), true)
assert.equal(isApplicationDocumentNo('AP-20260525103045-ABCDEFGH'), true)
assert.equal(isApplicationDocumentNo('APP-20260525-ABC123'), true)
assert.equal(isApplicationDocumentNo('R7K3M9Q2P'), false)
assert.equal(isApplicationDocumentNo('RE-20260525103045-HGFEDCBA'), false)
})
test('application request classification can rely on a short document number', () => {
assert.equal(isApplicationRequestLike({ claim_no: 'A7K3M9Q2P' }), true)
assert.equal(isApplicationRequestLike({ claim_no: 'R7K3M9Q2P' }), false)
})

View File

@@ -172,7 +172,10 @@ const orbIconPngAsset = fileURLToPath(
const orbIconBuffer = readFileSync(orbIconAsset)
test('app shell owns the workbench mode and wires it through topbar and content', () => {
assert.match(appShell, /const workbenchMode = ref\('traditional'\)/)
assert.match(appShell, /function resolveDefaultWorkbenchMode\(user\)\s*\{[\s\S]*isPlatformAdminUser\(user\)[\s\S]*'traditional'[\s\S]*'ai'/)
assert.match(appShell, /const workbenchMode = ref\(resolveDefaultWorkbenchMode\(currentUser\.value\)\)/)
assert.doesNotMatch(appShell, /const workbenchMode = ref\('traditional'\)/)
assert.match(appShell, /watch\(\s*\(\) => currentUser\.value,[\s\S]*resolveDefaultWorkbenchMode\(user\)/)
assert.match(appShell, /function toggleWorkbenchMode\(\)/)
assert.match(appShell, /const nextMode = workbenchMode\.value === 'ai' \? 'traditional' : 'ai'/)
assert.match(appShell, /sidebarCollapsedBeforeAiMode\.value = sidebarCollapsed\.value/)
@@ -260,11 +263,21 @@ test('AI mode screen follows the approved reference structure', () => {
assert.doesNotMatch(aiMode, /message\.pending \?/)
assert.match(aiMode, /继续和小财管家对话\.\.\./)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-query-summary\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-query-summary__scope\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card-list\) \{[\s\S]*gap:\s*16px;/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\) \{[\s\S]*background: rgba\(37, 99, 235, 0\.11\);/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\.is-success \.ai-document-card__head\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__body\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__foot\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__details\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__field\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__label\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__amount\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__meta\)/)
assert.match(
aiModeStyles,
/\.workbench-ai-answer-markdown :deep\(\.ai-document-card__details\) \{[\s\S]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\);/
)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-action-link\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-table-wrap\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-list\)/)