feat(web): 文档查询意图补充风险过滤与 X 天前范围
- aiDocumentQueryIntent 新增风险等级过滤(无/高/中/低/有风险)与 N 天前日期范围解析 - aiDocumentQueryModel/aiConversationHtmlRenderer 渲染适配风险过滤标签 - useWorkbenchAiDocumentQueryFlow/aiWorkbenchConversationStore 会话流转适配命令意图 - 更新 ai-document-query-model/ai-conversation-html-renderer/assistant-session-draft-delete 测试
This commit is contained in:
@@ -115,6 +115,15 @@ test('AI conversation renderer renders document detail action links as buttons',
|
||||
assert.doesNotMatch(rendered, /target="_blank"[\s\S]{0,120}#ai-open-document-detail/)
|
||||
})
|
||||
|
||||
test('AI conversation renderer renders deleted document detail actions as disabled buttons', () => {
|
||||
const rendered = renderAiConversationHtml('[单据已删除](#ai-deleted-document-detail:claim-deleted-1)')
|
||||
|
||||
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-document is-disabled"/)
|
||||
assert.match(rendered, /aria-disabled="true"/)
|
||||
assert.match(rendered, /data-ai-action="deleted-document-detail"/)
|
||||
assert.doesNotMatch(rendered, /href="#ai-deleted-document-detail/)
|
||||
})
|
||||
|
||||
test('AI conversation renderer renders images as html and rejects unsafe image sources', () => {
|
||||
const rendered = renderAiConversationHtml([
|
||||
'### 图片材料',
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
resolveAiDocumentQueryIntent
|
||||
} from '../src/utils/aiDocumentQueryModel.js'
|
||||
import { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js'
|
||||
import { resolveWorkbenchIntentActionRoute } from '../src/composables/workbenchAiMode/workbenchIntentActionPolicy.js'
|
||||
import { resolveWorkbenchIntentFrame } from '../src/composables/workbenchAiMode/workbenchIntentFrameModel.js'
|
||||
|
||||
const today = '2026-06-20'
|
||||
|
||||
@@ -68,6 +70,21 @@ test('AI document query intent detects approval document questions', () => {
|
||||
assert.equal(intent?.sourceLabel, '待我审核的单据')
|
||||
})
|
||||
|
||||
test('AI document query intent detects short approval workbench commands', () => {
|
||||
const reviewIntent = resolveAiDocumentQueryIntent('我要审核', { today })
|
||||
const todoIntent = resolveAiDocumentQueryIntent('待办审批', { today })
|
||||
|
||||
assert.equal(reviewIntent?.source, 'approval')
|
||||
assert.equal(reviewIntent?.documentType, 'all')
|
||||
assert.equal(reviewIntent?.sourceLabel, '待我审核的单据')
|
||||
assert.equal(todoIntent?.source, 'approval')
|
||||
assert.equal(todoIntent?.sourceLabel, '待我审核的单据')
|
||||
})
|
||||
|
||||
test('AI document query intent keeps approval policy questions out of document query', () => {
|
||||
assert.equal(resolveAiDocumentQueryIntent('审批规则怎么走', { today }), null)
|
||||
})
|
||||
|
||||
test('AI document query keeps explicit own-document scope separate from accessible documents', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('我名下有哪些单据?', { today })
|
||||
|
||||
@@ -136,6 +153,66 @@ test('AI document query combines natural-language filters', () => {
|
||||
assert.match(buildAiDocumentQueryConditionSummary(intent), /金额:不少于1000元/)
|
||||
})
|
||||
|
||||
test('AI document query filters draft candidates by relative day', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('我的 3天前 草稿单据', { today: '2026-06-24' })
|
||||
const records = filterAiDocumentQueryRecords([
|
||||
{
|
||||
id: 'draft-3-days-ago',
|
||||
claim_no: 'AP-20260621001',
|
||||
document_type_code: 'application',
|
||||
status: 'draft',
|
||||
reason: '三天前保存的申请草稿',
|
||||
created_at: '2026-06-21T09:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 'draft-yesterday',
|
||||
claim_no: 'AP-20260623001',
|
||||
document_type_code: 'application',
|
||||
status: 'draft',
|
||||
reason: '昨天保存的申请草稿',
|
||||
created_at: '2026-06-23T09:00:00Z'
|
||||
}
|
||||
], intent)
|
||||
|
||||
assert.equal(intent?.source, 'mine')
|
||||
assert.equal(intent?.statusFilter?.label, '草稿')
|
||||
assert.equal(intent?.timeRange?.start, '2026-06-21')
|
||||
assert.equal(intent?.timeRange?.end, '2026-06-21')
|
||||
assert.deepEqual(records.map((record) => record.documentNo), ['AP-20260621001'])
|
||||
})
|
||||
|
||||
test('AI document query filters no-risk approval application candidates', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('待我审核 无风险 申请单', { today })
|
||||
const payload = [{
|
||||
id: 'approval-no-risk',
|
||||
claim_no: 'AP-NORISK-001',
|
||||
document_type_code: 'application',
|
||||
expense_type: 'travel_application',
|
||||
status: 'submitted',
|
||||
reason: '合规差旅申请',
|
||||
created_at: '2026-06-20T09:00:00Z',
|
||||
risk_flags_json: [],
|
||||
risk_summary: '无'
|
||||
}, {
|
||||
id: 'approval-high-risk',
|
||||
claim_no: 'AP-RISK-001',
|
||||
document_type_code: 'application',
|
||||
expense_type: 'travel_application',
|
||||
status: 'submitted',
|
||||
reason: '住宿超标申请',
|
||||
created_at: '2026-06-20T10:00:00Z',
|
||||
risk_flags_json: [{ severity: 'high', summary: '住宿超标' }],
|
||||
risk_summary: '住宿超标'
|
||||
}]
|
||||
const records = filterAiDocumentQueryRecords({ items: payload, querySource: 'approval' }, intent)
|
||||
|
||||
assert.equal(intent?.source, 'approval')
|
||||
assert.equal(intent?.documentType, 'application')
|
||||
assert.equal(intent?.riskFilter?.level, 'none')
|
||||
assert.match(buildAiDocumentQueryConditionSummary(intent), /风险:无风险/)
|
||||
assert.deepEqual(records.map((record) => record.documentNo), ['AP-NORISK-001'])
|
||||
})
|
||||
|
||||
test('AI document query excludes undated rows when a time condition is present', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('我2月有哪些单据?', { today })
|
||||
const records = filterAiDocumentQueryRecords([
|
||||
@@ -246,6 +323,28 @@ test('AI document query html cards render as trusted card markup', () => {
|
||||
assert.doesNotMatch(rendered, /<blockquote>/)
|
||||
})
|
||||
|
||||
test('AI document query keeps high-risk command guidance in trusted rendered markup', () => {
|
||||
const frame = resolveWorkbenchIntentFrame('删除申请单草稿', { today: '2026-06-24' })
|
||||
const route = resolveWorkbenchIntentActionRoute(frame)
|
||||
const intent = resolveAiDocumentQueryIntent(route.queryPrompt, { today: '2026-06-24' })
|
||||
const rendered = renderAiConversationHtml(buildAiDocumentQueryMessage(intent, [{
|
||||
id: 'draft-application-1',
|
||||
claim_no: 'AP-DRAFT-001',
|
||||
document_type_code: 'application',
|
||||
expense_type: 'travel_application',
|
||||
status: 'draft',
|
||||
reason: '测试申请草稿',
|
||||
created_at: '2026-06-24T09:00:00Z'
|
||||
}], { commandFrame: frame }))
|
||||
|
||||
assert.match(rendered, /<h3 class="ai-html-title">已查询到相关单据<\/h3>/)
|
||||
assert.match(rendered, /<section class="ai-document-command-guidance" aria-label="高风险操作提示">/)
|
||||
assert.match(rendered, /系统不会直接删除相关单据/)
|
||||
assert.match(rendered, /进入单据详情核对后再操作/)
|
||||
assert.match(rendered, /<section class="ai-document-card-list" aria-label="单据查询结果">/)
|
||||
assert.match(rendered, /进入详情确认删除/)
|
||||
})
|
||||
|
||||
test('AI document query trusted html rejects unsafe card markup', () => {
|
||||
const rendered = renderAiConversationHtml([
|
||||
'### 查询结果',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'
|
||||
|
||||
import {
|
||||
markAiWorkbenchConversationDraftDeleted,
|
||||
markAiWorkbenchConversationDocumentDeleted,
|
||||
loadAiWorkbenchConversationHistory,
|
||||
saveAiWorkbenchConversation
|
||||
} from '../src/utils/aiWorkbenchConversationStore.js'
|
||||
@@ -120,6 +121,54 @@ test('deleting an application draft marks AI workbench detail links as unavailab
|
||||
assert.equal(loadAiWorkbenchConversationHistory(user)[0].messages.length, 2)
|
||||
})
|
||||
|
||||
test('deleted document detail links are disabled for reopened AI conversations', () => {
|
||||
installWindowStub()
|
||||
const user = { username: 'zhangsan@example.com' }
|
||||
|
||||
saveAiWorkbenchConversation(user, {
|
||||
id: 'conversation-document-delete-candidate',
|
||||
title: '删除申请单草稿',
|
||||
messages: [
|
||||
{
|
||||
id: 'assistant-document-candidates',
|
||||
role: 'assistant',
|
||||
content: [
|
||||
'### 已查询到相关单据',
|
||||
'',
|
||||
'[进入详情确认删除](#ai-open-document-detail:claim_id%3Dclaim-draft-2%26claim_no%3DAP-DRAFT-002)'
|
||||
].join('\n')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const nextHistory = markAiWorkbenchConversationDocumentDeleted(user, {
|
||||
claimId: 'claim-draft-2',
|
||||
claimNo: 'AP-DRAFT-002'
|
||||
})
|
||||
const conversation = nextHistory.find((item) => item.id === 'conversation-document-delete-candidate')
|
||||
|
||||
assert.ok(conversation)
|
||||
assert.match(conversation.messages[0].content, /#ai-deleted-document-detail:/)
|
||||
assert.doesNotMatch(conversation.messages[0].content, /#ai-open-document-detail:/)
|
||||
assert.match(conversation.messages[0].content, /\[单据已删除\]/)
|
||||
assert.match(conversation.messages.at(-1).content, /单据 AP-DRAFT-002 已经删除或不可访问/)
|
||||
assert.match(conversation.messages.at(-1).content, /该历史入口已失效,请返回单据列表重新查询/)
|
||||
assert.equal(loadAiWorkbenchConversationHistory(user)[0].messages.length, 2)
|
||||
})
|
||||
|
||||
test('AI workbench validates document detail links before opening stale history entries', () => {
|
||||
const aiMode = readFileSync(
|
||||
fileURLToPath(new URL('../src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
assert.match(aiMode, /fetchExpenseClaimDetail/)
|
||||
assert.match(aiMode, /markAiWorkbenchConversationDocumentDeleted/)
|
||||
assert.match(aiMode, /async function handleAiAnswerMarkdownClick/)
|
||||
assert.match(aiMode, /await ensureAiDocumentDetailStillAvailable/)
|
||||
assert.match(aiMode, /已将这条历史入口标记为不可查看/)
|
||||
})
|
||||
|
||||
test('saving a draft keeps the financial assistant open for continued work', () => {
|
||||
const appShellScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/composables/useAppShell.js', import.meta.url)),
|
||||
|
||||
Reference in New Issue
Block a user