feat(web): AI 文档详情引用解析与查询卡片增强
- 新增 aiDocumentDetailReference,统一解析 #ai-open-document-detail / #ai-open-application-detail 引用,兼容 A/R/D 短格式与 AP-/RE-/AD- 旧格式单号,提供 isBusinessDocumentReference 判定 - aiDocumentQueryModel 文档卡片接入详情引用,按申请单/报销单生成对应 href,HTML 渲染器识别单据记录表格并生成卡片链接 - PersonalWorkbenchAiMode 处理文档详情点击跳转,卡片样式重构为结构化布局并更新背景资源 - expenseApplicationPreview 补充事由字段,同步新增/更新 ai-document-detail-reference、document-query-model、html-renderer、workbench-ai-mode 等测试 - 更新公司通信费报销规则表
This commit is contained in:
@@ -47,18 +47,30 @@ test('AI conversation renderer supports tables and escapes unsafe HTML', () => {
|
||||
|
||||
test('AI conversation renderer renders application detail action links as buttons', () => {
|
||||
const rendered = renderAiConversationHtml([
|
||||
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 操作 |',
|
||||
'| --- | --- | --- | --- | --- |',
|
||||
'| 出差申请 | AP-OVERLAP | 草稿 | 待提交 | [查看](#ai-open-application-detail:AP-OVERLAP) |'
|
||||
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 日期 | 地点 | 事由 | 操作 |',
|
||||
'| --- | --- | --- | --- | --- | --- | --- | --- |',
|
||||
'| 出差申请 | AP-OVERLAP | submitted | 直属领导审批 | 2026-02-20 至 2026-02-23 | 上海 | 辅助国网仿生产服务器部署 | [查看](#ai-open-application-detail:AP-OVERLAP) |'
|
||||
].join('\n'))
|
||||
|
||||
assert.match(rendered, /<div class="ai-html-record-list" role="list">/)
|
||||
assert.match(rendered, /<article class="ai-html-record-item" role="listitem">/)
|
||||
assert.match(rendered, /<strong class="ai-html-record-id">AP-OVERLAP<\/strong>/)
|
||||
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-application"/)
|
||||
assert.match(rendered, /<section class="ai-document-card-list" role="list" aria-label="单据结果">/)
|
||||
assert.match(rendered, /<article class="ai-document-card is-pending" role="listitem" aria-label="单据详情">/)
|
||||
assert.match(rendered, /<strong class="ai-document-card__reason">出差申请<\/strong>/)
|
||||
assert.match(rendered, /<span class="ai-document-card__status">审批中<\/span>/)
|
||||
assert.match(rendered, /<div class="ai-document-card__summary">/)
|
||||
assert.match(rendered, /<span class="ai-document-card__label">日期<\/span>/)
|
||||
assert.match(rendered, /2026-02-20 至 2026-02-23/)
|
||||
assert.match(rendered, /<span class="ai-document-card__label">当前节点<\/span>/)
|
||||
assert.match(rendered, /直属领导审批/)
|
||||
assert.match(rendered, /<span class="ai-document-card__label">地点<\/span>/)
|
||||
assert.match(rendered, /上海/)
|
||||
assert.match(rendered, /<span class="ai-document-card__label">事由<\/span>/)
|
||||
assert.match(rendered, /辅助国网仿生产服务器部署/)
|
||||
assert.match(rendered, /<strong class="ai-document-card__value ai-document-card__number">AP-OVERLAP<\/strong>/)
|
||||
assert.match(rendered, /class="ai-html-action-link ai-document-card__action ai-html-action-link-application"/)
|
||||
assert.match(rendered, /data-ai-action="open-application-detail"/)
|
||||
assert.match(rendered, /href="#ai-open-application-detail:AP-OVERLAP"/)
|
||||
assert.doesNotMatch(rendered, /<table>/)
|
||||
assert.doesNotMatch(rendered, /ai-html-record-item/)
|
||||
assert.doesNotMatch(rendered, /target="_blank"[\s\S]{0,120}#ai-open-application-detail/)
|
||||
})
|
||||
|
||||
@@ -69,7 +81,7 @@ test('AI conversation renderer renders deleted application detail actions as dis
|
||||
'| 出差申请 | AP-20260620-DRAFT | 已删除 | 已删除 | [草稿已删除](#ai-deleted-application-detail:claim-draft-1) |'
|
||||
].join('\n'))
|
||||
|
||||
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-application is-disabled"/)
|
||||
assert.match(rendered, /class="ai-html-action-link ai-document-card__action ai-html-action-link-application is-disabled"/)
|
||||
assert.match(rendered, /aria-disabled="true"/)
|
||||
assert.match(rendered, /data-ai-action="deleted-application-detail"/)
|
||||
assert.doesNotMatch(rendered, /href="#ai-deleted-application-detail/)
|
||||
@@ -82,12 +94,16 @@ test('AI conversation renderer turns application conflict tables into record lis
|
||||
'| AP-20260620063557-4JU2MWEF | 2026-02-20 至 2026-02-23 | 审批中 | 辅助国网仿生产服务器部署 | [查看](#ai-open-application-detail:AP-20260620063557-4JU2MWEF) |'
|
||||
].join('\n'))
|
||||
|
||||
assert.match(rendered, /<div class="ai-html-record-list" role="list">/)
|
||||
assert.match(rendered, /申请时间/)
|
||||
assert.match(rendered, /<section class="ai-document-card-list" role="list" aria-label="单据结果">/)
|
||||
assert.match(rendered, /<article class="ai-document-card is-pending" role="listitem" aria-label="单据详情">/)
|
||||
assert.match(rendered, /<div class="ai-document-card__summary">/)
|
||||
assert.match(rendered, /<span class="ai-document-card__label">日期<\/span>/)
|
||||
assert.match(rendered, /2026-02-20 至 2026-02-23/)
|
||||
assert.match(rendered, /<span class="ai-document-card__label">当前节点<\/span>/)
|
||||
assert.match(rendered, /辅助国网仿生产服务器部署/)
|
||||
assert.match(rendered, /<div class="ai-html-record-action">/)
|
||||
assert.match(rendered, /ai-document-card__field--action/)
|
||||
assert.doesNotMatch(rendered, /<table>/)
|
||||
assert.doesNotMatch(rendered, /ai-html-record-item/)
|
||||
})
|
||||
|
||||
test('AI conversation renderer renders document detail action links as buttons', () => {
|
||||
|
||||
51
web/tests/ai-document-detail-reference.test.mjs
Normal file
51
web/tests/ai-document-detail-reference.test.mjs
Normal file
@@ -0,0 +1,51 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
buildAiDocumentDetailRequest,
|
||||
parseAiApplicationDetailHref,
|
||||
parseAiDocumentDetailHref
|
||||
} from '../src/utils/aiDocumentDetailReference.js'
|
||||
|
||||
test('AI detail request keeps business application number out of claimId for legacy links', () => {
|
||||
const detailReference = parseAiApplicationDetailHref('#ai-open-application-detail:AP-202606200001-ABCDEFGH')
|
||||
const request = buildAiDocumentDetailRequest(detailReference)
|
||||
|
||||
assert.deepEqual(detailReference, {
|
||||
reference: 'AP-202606200001-ABCDEFGH',
|
||||
documentType: 'application'
|
||||
})
|
||||
assert.equal(request.id, 'AP-202606200001-ABCDEFGH')
|
||||
assert.equal(request.claimId, '')
|
||||
assert.equal(request.claimNo, 'AP-202606200001-ABCDEFGH')
|
||||
assert.equal(request.documentNo, 'AP-202606200001-ABCDEFGH')
|
||||
assert.equal(request.documentTypeCode, 'application')
|
||||
assert.equal(request.detailLookupOnly, true)
|
||||
})
|
||||
|
||||
test('AI detail request uses explicit claim_id as lookup identity', () => {
|
||||
const detailReference = parseAiDocumentDetailHref(
|
||||
'#ai-open-document-detail:claim_id%3Dapproval-1%26claim_no%3DAP-APPROVAL-001'
|
||||
)
|
||||
const request = buildAiDocumentDetailRequest(detailReference)
|
||||
|
||||
assert.deepEqual(detailReference, {
|
||||
reference: 'AP-APPROVAL-001',
|
||||
claimId: 'approval-1',
|
||||
claimNo: 'AP-APPROVAL-001'
|
||||
})
|
||||
assert.equal(request.id, 'approval-1')
|
||||
assert.equal(request.claimId, 'approval-1')
|
||||
assert.equal(request.claimNo, 'AP-APPROVAL-001')
|
||||
assert.equal(request.documentNo, 'AP-APPROVAL-001')
|
||||
assert.equal(request.documentTypeCode, 'application')
|
||||
})
|
||||
|
||||
test('AI detail request treats non-number references as internal claim ids', () => {
|
||||
const request = buildAiDocumentDetailRequest({ reference: 'approval-internal-id' })
|
||||
|
||||
assert.equal(request.id, 'approval-internal-id')
|
||||
assert.equal(request.claimId, 'approval-internal-id')
|
||||
assert.equal(request.claimNo, '')
|
||||
assert.equal(request.documentNo, 'approval-internal-id')
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
buildAiDocumentQueryConditionSummary,
|
||||
buildAiDocumentQueryMessage,
|
||||
filterAiDocumentQueryRecords,
|
||||
mergeAiDocumentQueryPayloads,
|
||||
resolveAiDocumentQueryIntent
|
||||
} from '../src/utils/aiDocumentQueryModel.js'
|
||||
import { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js'
|
||||
@@ -55,9 +56,9 @@ const claims = [
|
||||
test('AI document query intent detects my document list questions', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('我现在有哪些单据?', { today })
|
||||
|
||||
assert.equal(intent?.source, 'mine')
|
||||
assert.equal(intent?.source, 'accessible')
|
||||
assert.equal(intent?.documentType, 'all')
|
||||
assert.equal(intent?.sourceLabel, '我的单据')
|
||||
assert.equal(intent?.sourceLabel, '我可见的单据')
|
||||
})
|
||||
|
||||
test('AI document query intent detects approval document questions', () => {
|
||||
@@ -67,6 +68,38 @@ test('AI document query intent detects approval document questions', () => {
|
||||
assert.equal(intent?.sourceLabel, '待我审核的单据')
|
||||
})
|
||||
|
||||
test('AI document query keeps explicit own-document scope separate from accessible documents', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('我名下有哪些单据?', { today })
|
||||
|
||||
assert.equal(intent?.source, 'mine')
|
||||
assert.equal(intent?.sourceLabel, '我的单据')
|
||||
})
|
||||
|
||||
test('AI document query merges own and approval records for generic document list questions', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('我现在有哪些单据?', { today })
|
||||
const approvalClaims = [{
|
||||
id: 'approval-1',
|
||||
claim_no: 'CL-APPROVAL-001',
|
||||
document_type_code: 'reimbursement',
|
||||
expense_type: 'meeting',
|
||||
status: 'pending',
|
||||
approval_stage: '直属领导审批',
|
||||
reason: '待我审核的会议费',
|
||||
employee_name: '李文静',
|
||||
department_name: '市场部',
|
||||
occurred_at: '2026-02-22T09:00:00Z',
|
||||
amount: 880
|
||||
}]
|
||||
const merged = mergeAiDocumentQueryPayloads(claims, { items: approvalClaims, querySource: 'approval' }, [claims[0]])
|
||||
const records = filterAiDocumentQueryRecords(merged, intent)
|
||||
|
||||
assert.equal(intent?.source, 'accessible')
|
||||
assert.deepEqual(
|
||||
records.map((record) => record.documentNo),
|
||||
['CL-20260305001', 'CL-APPROVAL-001', 'CL-20260221001', 'AP-20260220001']
|
||||
)
|
||||
})
|
||||
|
||||
test('AI document query filters by month and document type', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('我2月有哪些申请单?', { today })
|
||||
const records = filterAiDocumentQueryRecords(claims, intent)
|
||||
@@ -136,26 +169,64 @@ test('AI document query message renders html document cards with detail actions'
|
||||
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, /<strong class="ai-document-card__reason">差旅费用申请<\/strong>/)
|
||||
assert.match(message, /<div class="ai-document-card__summary">/)
|
||||
assert.match(message, /<div class="ai-document-card__details">/)
|
||||
assert.match(message, /<span class="ai-document-card__label">日期<\/span>/)
|
||||
assert.match(message, /2026-02-20/)
|
||||
assert.match(message, /<span class="ai-document-card__label">预计金额<\/span>/)
|
||||
assert.match(message, /<strong class="ai-document-card__value ai-document-card__amount">¥3,000\.00<\/strong>/)
|
||||
assert.doesNotMatch(message, /<span class="ai-document-card__label">当前节点<\/span>/)
|
||||
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, /上海/)
|
||||
assert.match(message, /<span class="ai-document-card__label">事由<\/span>/)
|
||||
assert.match(message, /辅助国网仿生产服务器部署/)
|
||||
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__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"/)
|
||||
assert.match(message, /href="#ai-open-document-detail:claim_id%3Dapp-1%26claim_no%3DAP-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.match(message, /<span class="ai-document-card__status">审批中<\/span>/)
|
||||
assert.match(message, /href="#ai-open-document-detail:claim_id%3Dclaim-1%26claim_no%3DCL-20260221001"/)
|
||||
assert.doesNotMatch(message, />submitted</)
|
||||
assert.doesNotMatch(message, /\| 单据编号 \|/)
|
||||
assert.doesNotMatch(message, /^> /m)
|
||||
assert.doesNotMatch(message, /\*\*查询范围\*\*/)
|
||||
})
|
||||
|
||||
test('AI document query highlights approval-task cards and shows pending review status', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('我有哪些审核单', { today })
|
||||
const message = buildAiDocumentQueryMessage(intent, [{
|
||||
id: 'approval-1',
|
||||
claim_no: 'AP-APPROVAL-001',
|
||||
document_type_code: 'application',
|
||||
expense_type: 'travel_application',
|
||||
status: 'submitted',
|
||||
approval_stage: '直属领导审批',
|
||||
reason: '参加相关残联会议',
|
||||
employee_name: '曹笑竹',
|
||||
department_name: '技术部',
|
||||
location: '上海',
|
||||
occurred_at: '2026-02-20T09:00:00Z',
|
||||
amount: 2120
|
||||
}])
|
||||
|
||||
assert.match(message, /ai-document-card--application ai-document-card--approval-task is-pending/)
|
||||
assert.match(message, /<span class="ai-document-card__status">待审批<\/span>/)
|
||||
assert.match(message, /<span class="ai-document-card__label">预计金额<\/span>/)
|
||||
assert.match(message, /<strong class="ai-document-card__value ai-document-card__amount">¥2,120\.00<\/strong>/)
|
||||
assert.doesNotMatch(message, /<span class="ai-document-card__label">当前节点<\/span>/)
|
||||
assert.match(message, /<span class="ai-document-card__label">单据类型<\/span>/)
|
||||
assert.match(message, /<strong class="ai-document-card__value">申请单 · 差旅费用申请<\/strong>/)
|
||||
assert.match(message, /href="#ai-open-document-detail:claim_id%3Dapproval-1%26claim_no%3DAP-APPROVAL-001"/)
|
||||
})
|
||||
|
||||
test('AI document query html cards render as trusted card markup', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('我2月有哪些单据?', { today })
|
||||
const rendered = renderAiConversationHtml(buildAiDocumentQueryMessage(intent, claims))
|
||||
@@ -169,7 +240,7 @@ test('AI document query html cards render as trusted card markup', () => {
|
||||
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.match(rendered, /href="#ai-open-document-detail:claim_id%3Dclaim-1%26claim_no%3DCL-20260221001"/)
|
||||
assert.doesNotMatch(rendered, /ai-document-card__meta/)
|
||||
assert.doesNotMatch(rendered, /<section class="ai-document-card-list/)
|
||||
assert.doesNotMatch(rendered, /<blockquote>/)
|
||||
|
||||
26
web/tests/expense-application-preview-reason.test.mjs
Normal file
26
web/tests/expense-application-preview-reason.test.mjs
Normal file
@@ -0,0 +1,26 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
buildLocalApplicationPreview
|
||||
} from '../src/utils/expenseApplicationPreview.js'
|
||||
|
||||
test('application preview keeps compact travel meeting reason without date or location prefix', () => {
|
||||
const preview = buildLocalApplicationPreview(
|
||||
'2月20-23日去上海出差参加相关残联会议',
|
||||
{
|
||||
name: '曹笑竹',
|
||||
departmentName: '技术部',
|
||||
position: '财务智能化产品经理',
|
||||
managerName: '向万红',
|
||||
grade: 'P5'
|
||||
},
|
||||
{ today: '2026-06-09' }
|
||||
)
|
||||
|
||||
assert.equal(preview.fields.time, '2026-02-20 至 2026-02-23')
|
||||
assert.equal(preview.fields.days, '4天')
|
||||
assert.equal(preview.fields.location, '上海')
|
||||
assert.equal(preview.fields.reason, '参加相关残联会议')
|
||||
assert.doesNotMatch(preview.fields.reason, /2月20|23日|上海|出差/)
|
||||
})
|
||||
@@ -84,7 +84,10 @@ test('AI mode handles document query prompts locally before steward planning', (
|
||||
assert.match(aiMode, /async function handleAiDocumentQueryIntent/)
|
||||
assert.match(aiMode, /buildAiDocumentQueryConditionSummary/)
|
||||
assert.match(aiMode, /filterAiDocumentQueryRecords\(payload, intent\)/)
|
||||
assert.match(aiMode, /mergeAiDocumentQueryPayloads/)
|
||||
assert.match(aiMode, /fetchApprovalExpenseClaims/)
|
||||
assert.match(aiMode, /Promise\.all\(\[/)
|
||||
assert.match(aiMode, /fetchAiDocumentQueryPayload\(intent\)/)
|
||||
assert.match(aiMode, /buildAiDocumentQueryMessage/)
|
||||
assert.match(aiMode, /AI_DOCUMENT_QUERY_STEP_DELAY_MS/)
|
||||
assert.match(aiMode, /async function updateAiDocumentQueryThinking/)
|
||||
@@ -149,6 +152,16 @@ test('AI mode handles application preview save and submit through buttons or tex
|
||||
assert.match(aiMode, /#ai-open-application-detail:/)
|
||||
})
|
||||
|
||||
test('AI mode keeps missing application fields editable in the preview table without quick template action', () => {
|
||||
assert.match(aiMode, /function buildInlineApplicationPreviewSuggestedActions\(applicationPreview = \{\}, draftPayload = null\)/)
|
||||
assert.match(aiMode, /label:\s*'保存草稿'/)
|
||||
assert.match(aiMode, /function handleInlineApplicationPreviewTextAction\(prompt\)/)
|
||||
assert.doesNotMatch(aiMode, /label:\s*'快速模板'/)
|
||||
assert.doesNotMatch(aiMode, /action_type:\s*'prefill_composer'/)
|
||||
assert.doesNotMatch(aiMode, /buildInlineApplicationPreviewTemplatePrefill/)
|
||||
assert.doesNotMatch(aiMode, /applyInlineApplicationPreviewTemplateText/)
|
||||
})
|
||||
|
||||
test('AI mode waits for submit confirmation before adding submit action to the conversation', () => {
|
||||
const executeStart = aiMode.indexOf('async function executeInlineApplicationPreviewAction')
|
||||
const executeEnd = aiMode.indexOf('\nfunction handleInlineApplicationPreviewTextAction', executeStart)
|
||||
@@ -179,8 +192,14 @@ test('AI mode waits for submit confirmation before adding submit action to the c
|
||||
|
||||
test('AI mode formats saved application draft as a detail table without continuing submit flow', () => {
|
||||
assert.match(aiMode, /function buildInlineApplicationResultTable\(draftPayload = \{\}, options = \{\}\)/)
|
||||
assert.match(aiMode, /\| 单据类型 \| 单据编号 \| 单据状态 \| 当前节点 \| 操作 \|/)
|
||||
assert.match(aiMode, /function normalizeInlineApplicationStatusLabel\(value, fallback = ''\)/)
|
||||
assert.match(aiMode, /submitted:\s*'审批中'/)
|
||||
assert.match(aiMode, /const statusLabel = normalizeInlineApplicationStatusLabel\(info\.statusLabel, options\.statusLabel\)/)
|
||||
assert.match(aiMode, /\| 单据类型 \| 单据编号 \| 单据状态 \| 当前节点 \| 日期 \| 地点 \| 事由 \| 金额 \| 操作 \|/)
|
||||
assert.match(aiMode, /\[查看\]\(\$\{href\}\)/)
|
||||
assert.match(aiMode, /dateLabel:\s*rangeText \|\| dateText \|\| resolveBodyField\(\['时间', '日期', '申请时间'\]\) \|\| '待补充'/)
|
||||
assert.match(aiMode, /locationLabel:[\s\S]*resolveBodyField\(\['地点', '目的地'\]\) \|\| '待补充'/)
|
||||
assert.match(aiMode, /reasonLabel:[\s\S]*resolveBodyField\(\['事由', '事件', '申请事由'\]\) \|\| '待补充'/)
|
||||
assert.match(aiMode, /buildInlineApplicationActionDetailHref\(info\)/)
|
||||
assert.match(aiMode, /params\.set\('claim_id', claimId\)/)
|
||||
assert.match(aiMode, /params\.set\('claim_no', claimNo\)/)
|
||||
|
||||
@@ -266,32 +266,25 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
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\) \{[\s\S]*url\("\.\.\/\.\.\/ai-document-card-bg\.png"\);/)
|
||||
assert.doesNotMatch(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)::before/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\) \{[\s\S]*background: var\(--ai-document-card-head-bg\);/)
|
||||
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__summary\)/)
|
||||
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__field--wide\)/)
|
||||
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__details\) \{[\s\S]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\);/
|
||||
)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__field--action \.ai-document-card__action\)/)
|
||||
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\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-item\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-meta\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-action \.ai-html-action-link\)/)
|
||||
assert.match(
|
||||
aiModeStyles,
|
||||
/\.workbench-ai-answer-markdown :deep\(\.ai-html-record-item\)\s*\{[\s\S]*grid-template-columns:\s*minmax\(220px,\s*1\.15fr\)\s*minmax\(260px,\s*0\.85fr\)\s*auto;/
|
||||
)
|
||||
assert.match(
|
||||
aiModeStyles,
|
||||
/\.workbench-ai-answer-markdown :deep\(\.ai-html-record-action \.ai-html-action-link\)[\s\S]*background:\s*#2563eb;/
|
||||
)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-image-frame\)/)
|
||||
assert.match(aiMode, /import \{ fetchSettings \} from '\.\.\/\.\.\/services\/settings\.js'/)
|
||||
assert.match(aiMode, /import \{ fetchStewardPlan, fetchStewardPlanStream \} from '\.\.\/\.\.\/services\/steward\.js'/)
|
||||
@@ -320,7 +313,7 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.match(aiMode, /const hasServerStreamedContent = Boolean\(String\(pendingMessage\.content \|\| ''\)\.trim\(\)\)/)
|
||||
assert.match(aiMode, /if \(!hasServerStreamedContent\) \{[\s\S]*await streamInlineAssistantContent\(pendingMessage\.id, finalMessageText\)[\s\S]*\}/)
|
||||
assert.match(aiMode, /if \(actionType === AI_APPLICATION_ACTION_SUBMIT\) \{[\s\S]*buildInlineApplicationResultTable\(draftPayload/)
|
||||
assert.match(aiMode, /需要查看完整详情时,请点击列表最后一列的“查看”进入单据详情。/)
|
||||
assert.match(aiMode, /需要查看完整详情时,请点击卡片“操作”行的“查看”进入单据详情。/)
|
||||
assert.doesNotMatch(aiMode, /\*\*申请单号:\*\*/)
|
||||
assert.doesNotMatch(aiMode, /createInlineMessage\('assistant', buildStewardPlanMessageText\(plan\)/)
|
||||
assert.doesNotMatch(aiMode, /runOrchestrator\(/)
|
||||
|
||||
@@ -19,6 +19,10 @@ const aiMode = readFileSync(
|
||||
fileURLToPath(new URL('../src/components/business/PersonalWorkbenchAiMode.vue', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const aiDetailReference = readFileSync(
|
||||
fileURLToPath(new URL('../src/utils/aiDocumentDetailReference.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
test('workbench document detail keeps workbench as the return target', () => {
|
||||
assert.match(workbench, /source:\s*'workbench'/)
|
||||
@@ -35,12 +39,16 @@ test('workbench document detail keeps workbench as the return target', () => {
|
||||
assert.match(appShellComposable, /router\.push\(\{ name: 'app-documents', query: buildDocumentReturnQuery\(\) \}\)/)
|
||||
})
|
||||
|
||||
test('AI detail links wait for full document detail instead of rendering a half snapshot', () => {
|
||||
assert.match(aiMode, /detailLookupOnly:\s*true/)
|
||||
assert.match(aiMode, /params\.get\('claim_id'\)/)
|
||||
assert.match(aiMode, /params\.get\('claim_no'\)/)
|
||||
assert.match(aiMode, /claimId:\s*claimId \|\| reference/)
|
||||
assert.match(aiMode, /claimNo:\s*claimNo \|\| reference/)
|
||||
test('AI detail links resolve real claim identity before opening document detail', () => {
|
||||
assert.match(aiMode, /buildAiDocumentDetailRequest/)
|
||||
assert.match(aiMode, /parseAiApplicationDetailHref/)
|
||||
assert.match(aiMode, /parseAiDocumentDetailHref/)
|
||||
assert.match(aiDetailReference, /detailLookupOnly:\s*true/)
|
||||
assert.match(aiDetailReference, /params\.get\('claim_id'\)/)
|
||||
assert.match(aiDetailReference, /params\.get\('claim_no'\)/)
|
||||
assert.match(aiDetailReference, /isBusinessDocumentReference/)
|
||||
assert.match(aiDetailReference, /const claimId = explicitClaimId \|\| \(!referenceIsBusinessNo \? reference : ''\)/)
|
||||
assert.match(aiDetailReference, /const claimNo = explicitClaimNo \|\| \(referenceIsBusinessNo \? reference : ''\)/)
|
||||
assert.match(
|
||||
appShell,
|
||||
/v-else-if="activeView === 'documents' && detailMode && !selectedRequest"[\s\S]*正在加载完整单据详情/
|
||||
@@ -49,10 +57,16 @@ test('AI detail links wait for full document detail instead of rendering a half
|
||||
appShell,
|
||||
/const detailPayload = request \|\| \{[\s\S]*detailLookupOnly:\s*true[\s\S]*\}/
|
||||
)
|
||||
assert.match(appShell, /isBusinessDocumentReference/)
|
||||
assert.match(appShell, /const requestCandidates = Array\.isArray\(workbenchRequests\.value\)/)
|
||||
assert.match(appShell, /claimId:\s*fallbackClaimId/)
|
||||
assert.match(appShell, /claimNo:\s*fallbackClaimNo/)
|
||||
assert.match(
|
||||
appShellComposable,
|
||||
/const isDetailLookupOnlyRequest = isDetailLookupOnlyPayload\(request\)[\s\S]*selectedRequestSnapshot\.value = isDetailLookupOnlyRequest \? null : request \|\| null/
|
||||
)
|
||||
assert.match(appShellComposable, /const workbenchRequests = computed/)
|
||||
assert.match(appShellComposable, /workbenchRequests\.value\.find\(\(item\) => isSameRequestIdentity\(item, requestId\)\)/)
|
||||
assert.match(
|
||||
appShellComposable,
|
||||
/void refreshSelectedRequestDetail\(isDetailLookupOnlyRequest \? requestId : request\)/
|
||||
|
||||
Reference in New Issue
Block a user