feat(web): AI 工作台命令意图解析与动作策略
- 新增 workbenchIntentFrameModel,解析删除/审批/查询/咨询政策等意图帧,含风险等级、目标模式(当前上下文/筛选候选)与日期归一化 - 新增 workbenchIntentActionPolicy,按意图帧路由下一步(直通/拦截批量/上下文确认/查询候选) - 新增 workbenchAiCommandIntentModel 与 useWorkbenchAiCommandIntents,识别草稿删除意图并解析最近草稿载荷生成引导 - usePersonalWorkbenchAiMode 接入命令意图处理,personal-workbench-ai-mode.css 适配 - 新增 command-intent-model/intent-frame-model 测试
This commit is contained in:
104
web/tests/workbench-ai-command-intent-model.test.mjs
Normal file
104
web/tests/workbench-ai-command-intent-model.test.mjs
Normal file
@@ -0,0 +1,104 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import {
|
||||
buildWorkbenchDraftDeletionGuidance,
|
||||
isWorkbenchDraftDeletionIntent,
|
||||
resolveLatestWorkbenchDraftPayload
|
||||
} from '../src/composables/workbenchAiMode/workbenchAiCommandIntentModel.js'
|
||||
|
||||
const personalWorkbenchAiModeScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const commandIntentsScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/composables/workbenchAiMode/useWorkbenchAiCommandIntents.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
test('workbench command intent detects draft deletion phrases without broad delete matching', () => {
|
||||
assert.equal(isWorkbenchDraftDeletionIntent('删除草稿'), true)
|
||||
assert.equal(isWorkbenchDraftDeletionIntent('把刚才保存的草稿删掉'), true)
|
||||
assert.equal(isWorkbenchDraftDeletionIntent('删除这个申请单'), true)
|
||||
assert.equal(isWorkbenchDraftDeletionIntent('删除附件'), false)
|
||||
assert.equal(isWorkbenchDraftDeletionIntent('草稿还要补什么'), false)
|
||||
})
|
||||
|
||||
test('workbench command intent resolves latest draft payload from conversation context', () => {
|
||||
const payload = resolveLatestWorkbenchDraftPayload([
|
||||
{
|
||||
role: 'assistant',
|
||||
draftPayload: {
|
||||
claim_id: 'old-draft',
|
||||
claim_no: 'AOLD001',
|
||||
status: 'draft'
|
||||
}
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
suggestedActions: [{
|
||||
action_type: 'open_application_detail',
|
||||
payload: {
|
||||
claim_id: 'latest-draft',
|
||||
claim_no: 'ALATEST1',
|
||||
status: 'draft'
|
||||
}
|
||||
}]
|
||||
}
|
||||
])
|
||||
|
||||
assert.deepEqual(payload, {
|
||||
claimId: 'latest-draft',
|
||||
claimNo: 'ALATEST1',
|
||||
status: 'draft',
|
||||
documentType: 'application'
|
||||
})
|
||||
})
|
||||
|
||||
test('workbench command intent skips submitted documents when deleting draft', () => {
|
||||
const payload = resolveLatestWorkbenchDraftPayload([
|
||||
{
|
||||
role: 'assistant',
|
||||
draftPayload: {
|
||||
claim_id: 'submitted-application',
|
||||
claim_no: 'ASUBMIT1',
|
||||
status: 'submitted'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
assert.equal(payload, null)
|
||||
})
|
||||
|
||||
test('workbench draft deletion guidance opens detail instead of deleting directly', () => {
|
||||
const guidance = buildWorkbenchDraftDeletionGuidance({
|
||||
claimId: 'latest-draft',
|
||||
claimNo: 'ALATEST1',
|
||||
documentType: 'application'
|
||||
})
|
||||
|
||||
assert.match(guidance.content, /已识别到您想删除草稿/)
|
||||
assert.match(guidance.content, /不会直接替您删除/)
|
||||
assert.equal(guidance.suggestedActions.length, 1)
|
||||
assert.equal(guidance.suggestedActions[0].action_type, 'open_application_detail')
|
||||
assert.equal(guidance.suggestedActions[0].payload.claim_id, 'latest-draft')
|
||||
assert.equal(guidance.suggestedActions[0].payload.claim_no, 'ALATEST1')
|
||||
})
|
||||
|
||||
test('workbench draft deletion intent is wired before draft slot continuation', () => {
|
||||
assert.match(commandIntentsScript, /isWorkbenchDraftDeletionIntent/)
|
||||
assert.match(commandIntentsScript, /function handleInlineDraftDeletionIntent\(cleanPrompt, entry = \{\}\)/)
|
||||
assert.match(commandIntentsScript, /resolveLatestWorkbenchDraftPayload\(conversationMessages\.value\)/)
|
||||
assert.match(commandIntentsScript, /buildWorkbenchDraftDeletionGuidance\(draftPayload\)/)
|
||||
assert.match(personalWorkbenchAiModeScript, /useWorkbenchAiCommandIntents/)
|
||||
|
||||
const startIndex = personalWorkbenchAiModeScript.indexOf('function startInlineConversation')
|
||||
const startBlock = personalWorkbenchAiModeScript.slice(startIndex)
|
||||
const deleteIntentIndex = startBlock.indexOf('if (commandIntents.handleInlineDraftDeletionIntent(cleanPrompt, entry))')
|
||||
const draftContinuationIndex = startBlock.indexOf('if (aiExpenseDraft.value && !isAiExpenseDraftComplete(aiExpenseDraft.value))')
|
||||
|
||||
assert.ok(deleteIntentIndex >= 0)
|
||||
assert.ok(draftContinuationIndex > deleteIntentIndex)
|
||||
})
|
||||
116
web/tests/workbench-intent-frame-model.test.mjs
Normal file
116
web/tests/workbench-intent-frame-model.test.mjs
Normal file
@@ -0,0 +1,116 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
resolveWorkbenchIntentFrame
|
||||
} from '../src/composables/workbenchAiMode/workbenchIntentFrameModel.js'
|
||||
import {
|
||||
resolveWorkbenchIntentActionRoute
|
||||
} from '../src/composables/workbenchAiMode/workbenchIntentActionPolicy.js'
|
||||
import {
|
||||
buildAiDocumentQueryMessage,
|
||||
buildAiDocumentQueryThinkingEvents,
|
||||
resolveAiDocumentQueryIntent
|
||||
} from '../src/utils/aiDocumentQueryModel.js'
|
||||
|
||||
const today = '2026-06-24'
|
||||
|
||||
test('workbench intent frame resolves contextual draft deletion as confirm-only current target', () => {
|
||||
const frame = resolveWorkbenchIntentFrame('请删除刚才那个草稿', { today })
|
||||
|
||||
assert.equal(frame?.action, 'delete')
|
||||
assert.equal(frame?.objectType, 'draft')
|
||||
assert.equal(frame?.targetMode, 'current_context')
|
||||
assert.equal(frame?.safetyLevel, 'confirm_required')
|
||||
assert.equal(frame?.filters.status?.label, '草稿')
|
||||
assert.equal(frame?.normalizedQuery, '我的草稿单据')
|
||||
})
|
||||
|
||||
test('workbench intent frame sends filtered draft deletion to candidate search', () => {
|
||||
const frame = resolveWorkbenchIntentFrame('删除3天前的草稿', { today })
|
||||
const route = resolveWorkbenchIntentActionRoute(frame)
|
||||
|
||||
assert.equal(frame?.action, 'delete')
|
||||
assert.equal(frame?.objectType, 'draft')
|
||||
assert.equal(frame?.targetMode, 'filtered_candidates')
|
||||
assert.equal(frame?.safetyLevel, 'confirm_required')
|
||||
assert.equal(frame?.filters.timeRange?.start, '2026-06-21')
|
||||
assert.equal(frame?.filters.timeRange?.end, '2026-06-21')
|
||||
assert.equal(frame?.normalizedQuery, '我的 3天前 草稿单据')
|
||||
assert.equal(route.nextStep, 'query_candidates')
|
||||
assert.equal(route.queryPrompt, '我的 3天前 草稿单据')
|
||||
})
|
||||
|
||||
test('workbench intent frame preserves application draft deletion filters', () => {
|
||||
const frame = resolveWorkbenchIntentFrame('删除申请单草稿', { today })
|
||||
const route = resolveWorkbenchIntentActionRoute(frame)
|
||||
const queryIntent = resolveAiDocumentQueryIntent(route.queryPrompt, { today })
|
||||
|
||||
assert.equal(frame?.action, 'delete')
|
||||
assert.equal(frame?.objectType, 'draft')
|
||||
assert.equal(frame?.filters.documentType, 'application')
|
||||
assert.equal(frame?.filters.status?.label, '草稿')
|
||||
assert.equal(frame?.targetMode, 'filtered_candidates')
|
||||
assert.equal(frame?.safetyLevel, 'confirm_required')
|
||||
assert.equal(route.queryPrompt, '我的 草稿 申请单')
|
||||
assert.equal(queryIntent?.source, 'mine')
|
||||
assert.equal(queryIntent?.documentType, 'application')
|
||||
assert.equal(queryIntent?.statusFilter?.label, '草稿')
|
||||
})
|
||||
|
||||
test('workbench document query thinking exposes destructive action policy before filtering', () => {
|
||||
const frame = resolveWorkbenchIntentFrame('删除申请单草稿', { today })
|
||||
const intent = resolveAiDocumentQueryIntent(resolveWorkbenchIntentActionRoute(frame).queryPrompt, { today })
|
||||
const events = buildAiDocumentQueryThinkingEvents(intent, { commandFrame: frame })
|
||||
|
||||
assert.equal(events[0].eventId, 'document-command-policy')
|
||||
assert.match(events[0].title, /识别高风险操作意图/)
|
||||
assert.match(events[0].content, /删除/)
|
||||
assert.match(events[0].content, /不会直接执行/)
|
||||
assert.match(events[0].content, /先筛选候选/)
|
||||
})
|
||||
|
||||
test('workbench high-risk command result explains confirmation boundary and labels detail shortcut', () => {
|
||||
const frame = resolveWorkbenchIntentFrame('删除申请单草稿', { today })
|
||||
const intent = resolveAiDocumentQueryIntent(resolveWorkbenchIntentActionRoute(frame).queryPrompt, { today })
|
||||
const message = 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',
|
||||
risk_flags_json: [],
|
||||
risk_summary: '无'
|
||||
}], { commandFrame: frame })
|
||||
|
||||
assert.match(message, /系统不会直接删除相关单据/)
|
||||
assert.match(message, /请点击下方候选单据里的快捷按钮/)
|
||||
assert.match(message, /进入单据详情核对后再操作/)
|
||||
assert.match(message, />进入详情确认删除</)
|
||||
})
|
||||
|
||||
test('workbench intent frame resolves compliant no-risk approval request as filtered approval candidates', () => {
|
||||
const frame = resolveWorkbenchIntentFrame('审核合规没有风险的申请', { today })
|
||||
const route = resolveWorkbenchIntentActionRoute(frame)
|
||||
|
||||
assert.equal(frame?.action, 'approve')
|
||||
assert.equal(frame?.objectType, 'application')
|
||||
assert.equal(frame?.targetMode, 'filtered_candidates')
|
||||
assert.equal(frame?.safetyLevel, 'confirm_required')
|
||||
assert.equal(frame?.filters.risk?.level, 'none')
|
||||
assert.equal(frame?.filters.documentType, 'application')
|
||||
assert.equal(frame?.normalizedQuery, '待我审核 无风险 申请单')
|
||||
assert.equal(route.nextStep, 'query_candidates')
|
||||
assert.equal(route.queryPrompt, '待我审核 无风险 申请单')
|
||||
})
|
||||
|
||||
test('workbench intent frame keeps approval policy questions out of document actions', () => {
|
||||
const frame = resolveWorkbenchIntentFrame('审批规则怎么走', { today })
|
||||
const route = resolveWorkbenchIntentActionRoute(frame)
|
||||
|
||||
assert.equal(frame?.action, 'ask_policy')
|
||||
assert.equal(frame?.safetyLevel, 'read_only')
|
||||
assert.equal(route.nextStep, 'pass_through')
|
||||
})
|
||||
Reference in New Issue
Block a user