Files
X-Financial/web/tests/workbench-ai-intent-planner-model.test.mjs
caoxiaozhu bc560145a4 feat(web): AI 工作台意图规划与规划思考模型
- 新增 workbenchAiIntentPlannerModel,基于 LLM function_call 解析建单/草稿/提交意图,区分 model 与 rule_fallback 来源
- 新增 workbenchAiPlanningThinkingModel 合并规划思考事件流,按 eventId 去重合并
- application gate/preview 模型接入意图规划,usePersonalWorkbenchAiMode/useWorkbenchAiStewardFlow/useWorkbenchAiActionRouter 链路适配,支持上下文提交
- steward 服务与 stewardPlanModel 适配新动作结构,receipt-folder-view 微调样式
- 新增 intent-planner-model/application-context-submit/steward-actions-service 测试,更新 gate-model/action-router/plan-message-copy/fast-preview 测试
2026-06-24 21:58:46 +08:00

332 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import {
WORKBENCH_AI_INTENT_SOURCE_MODEL,
WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK,
WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW,
WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT,
WORKBENCH_AI_STEP_RUN_DUPLICATE_PRECHECK,
WORKBENCH_AI_STEP_SUBMIT_APPLICATION,
WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS,
buildRuleFallbackWorkbenchAiIntentPlan,
normalizeWorkbenchAiIntentPlan,
resolveExecutableTravelApplicationPlan,
shouldRequestWorkbenchAiIntentPlan
} from '../src/composables/workbenchAiMode/workbenchAiIntentPlannerModel.js'
import { buildInlineApplicationPreview } from '../src/composables/workbenchAiMode/workbenchAiApplicationPreviewModel.js'
const personalWorkbenchAiModeScript = readFileSync(
fileURLToPath(new URL('../src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js', import.meta.url)),
'utf8'
)
const stewardFlowScript = readFileSync(
fileURLToPath(new URL('../src/composables/workbenchAiMode/useWorkbenchAiStewardFlow.js', import.meta.url)),
'utf8'
)
const applicationPreviewFlowScript = readFileSync(
fileURLToPath(new URL('../src/composables/workbenchAiMode/useWorkbenchAiApplicationPreviewFlow.js', import.meta.url)),
'utf8'
)
const planningThinkingModelScript = readFileSync(
fileURLToPath(new URL('../src/composables/workbenchAiMode/workbenchAiPlanningThinkingModel.js', import.meta.url)),
'utf8'
)
test('workbench AI intent planner normalizes model travel application submit plan into executable steps', () => {
const plan = normalizeWorkbenchAiIntentPlan({
planning_source: 'llm_function_call',
tasks: [{
task_type: 'expense_application',
assigned_agent: 'application_assistant',
requested_action: 'submit',
confidence: 0.91,
ontology_fields: {
time_range: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '辅助国网仿生产服务器部署',
transport_mode: '火车'
}
}]
}, {
prompt: '去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交'
})
assert.equal(plan.source, WORKBENCH_AI_INTENT_SOURCE_MODEL)
assert.equal(plan.intent, 'create_travel_application')
assert.equal(plan.requestedAction, 'submit')
assert.deepEqual(plan.steps, [
WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW,
WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS,
WORKBENCH_AI_STEP_RUN_DUPLICATE_PRECHECK,
WORKBENCH_AI_STEP_SUBMIT_APPLICATION
])
assert.deepEqual(plan.ontologyFields, {
time_range: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '辅助国网仿生产服务器部署',
transport_mode: '火车'
})
assert.deepEqual(plan.slots, {
timeRange: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '辅助国网仿生产服务器部署',
transportMode: '火车'
})
})
test('workbench AI intent planner prefers server action steps when present', () => {
const plan = normalizeWorkbenchAiIntentPlan({
planning_source: 'llm_function_call',
tasks: [{
task_type: 'expense_application',
assigned_agent: 'application_assistant',
requested_action: 'submit',
confidence: 0.91,
ontology_fields: {
time_range: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '辅助国网仿生产服务器部署',
transport_mode: '火车'
},
action_steps: [
{ action_type: 'fill_application_fields' },
{ action_type: 'build_application_preview' },
{ action_type: 'validate_required_fields' },
{ action_type: 'save_application_draft' }
]
}]
}, {
prompt: '2026-02-20 至 2026-02-23去上海出差辅助国网仿生产服务器部署交通火车直接提交'
})
assert.deepEqual(plan.steps, [
WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW,
WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS,
WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT
])
})
test('workbench AI intent planner falls back to rule plan for compact travel direct submit', () => {
const plan = buildRuleFallbackWorkbenchAiIntentPlan('去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交')
assert.equal(plan.source, WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK)
assert.equal(plan.intent, 'create_travel_application')
assert.equal(plan.requestedAction, 'submit')
assert.deepEqual(plan.steps, [
WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW,
WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS,
WORKBENCH_AI_STEP_RUN_DUPLICATE_PRECHECK,
WORKBENCH_AI_STEP_SUBMIT_APPLICATION
])
})
test('workbench AI intent planner detects compact travel save-draft variant before rules are enough', () => {
const prompt = '2026-02-20 至 2026-02-23上海出差国网仿生产服务器部署火车保存草稿。'
const plan = buildRuleFallbackWorkbenchAiIntentPlan(prompt)
assert.equal(shouldRequestWorkbenchAiIntentPlan(prompt), true)
assert.equal(shouldRequestWorkbenchAiIntentPlan('帮我查询上海差旅标准'), true)
assert.equal(shouldRequestWorkbenchAiIntentPlan('1'), false)
assert.equal(plan.source, WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK)
assert.equal(plan.intent, 'create_travel_application')
assert.equal(plan.requestedAction, 'save_draft')
assert.deepEqual(plan.steps, [
WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW,
WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS,
WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT
])
assert.deepEqual(resolveExecutableTravelApplicationPlan(plan), {
expenseType: 'travel',
expenseTypeLabel: '差旅费',
sourceText: prompt,
ontologyFields: {},
autoSubmit: false,
autoSaveDraft: true
})
})
test('workbench AI intent planner turns model fields and action into executable application preview payload', () => {
const prompt = '2026-02-20 至 2026-02-23上海出差国网仿生产服务器部署火车保存草稿。'
const plan = normalizeWorkbenchAiIntentPlan({
planning_source: 'llm_function_call',
tasks: [{
task_type: 'expense_application',
assigned_agent: 'application_assistant',
requested_action: 'save_draft',
confidence: 0.95,
ontology_fields: {
time_range: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '国网仿生产服务器部署',
transport_mode: '火车'
},
missing_fields: []
}]
}, { prompt })
assert.deepEqual(resolveExecutableTravelApplicationPlan(plan), {
expenseType: 'travel',
expenseTypeLabel: '差旅费',
sourceText: prompt,
ontologyFields: {
time_range: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '国网仿生产服务器部署',
transport_mode: '火车'
},
autoSubmit: false,
autoSaveDraft: true
})
})
test('workbench AI intent planner turns single application candidate flow into executable preview payload', () => {
const prompt = '2026-02-20 至 2026-02-23去上海出差辅助国网仿生产服务器部署交通火车'
const plan = normalizeWorkbenchAiIntentPlan({
planning_source: 'rule_fallback',
plan_status: 'needs_flow_confirmation',
pending_flow_confirmation: {
status: 'pending',
candidate_flows: [{
flow_id: 'travel_application',
label: '先发起出差申请',
confidence: 0.86,
ontology_fields: {
expense_type: 'travel',
time_range: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '辅助国网仿生产服务器部署',
transport_mode: '火车'
},
missing_fields: []
}]
}
}, { prompt })
assert.equal(plan.source, WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK)
assert.equal(plan.intent, 'create_travel_application')
assert.equal(plan.requestedAction, 'preview')
assert.deepEqual(resolveExecutableTravelApplicationPlan(plan), {
expenseType: 'travel',
expenseTypeLabel: '差旅费',
sourceText: prompt,
ontologyFields: {
expense_type: 'travel',
time_range: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '辅助国网仿生产服务器部署',
transport_mode: '火车'
},
autoSubmit: false,
autoSaveDraft: false
})
})
test('workbench AI application preview prefers model ontology fields over local text guesses', () => {
const preview = buildInlineApplicationPreview(
'差旅费',
'2026-02-20 至 2026-02-23上海出差国网仿生产服务器部署火车保存草稿。',
{ name: '李文静', grade: 'P5', location: '武汉' },
{
ontologyFields: {
time_range: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '国网仿生产服务器部署',
transport_mode: '火车'
}
}
)
assert.equal(preview.fields.time, '2026-02-20 至 2026-02-23')
assert.equal(preview.fields.location, '上海')
assert.equal(preview.fields.reason, '国网仿生产服务器部署')
assert.equal(preview.fields.transportMode, '火车')
})
test('workbench AI intent planner rejects policy question and resolves executable application request', () => {
assert.equal(buildRuleFallbackWorkbenchAiIntentPlan('帮我查询上海差旅标准'), null)
const request = resolveExecutableTravelApplicationPlan(
buildRuleFallbackWorkbenchAiIntentPlan('去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交')
)
assert.deepEqual(request, {
expenseType: 'travel',
expenseTypeLabel: '差旅费',
sourceText: '去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交',
ontologyFields: {},
autoSubmit: true,
autoSaveDraft: false
})
})
test('workbench AI mode asks steward model plan before fallback execution', () => {
assert.match(stewardFlowScript, /async function resolveInlineExecutionPlan\(prompt, entry = \{\}, files = \[\], options = \{\}\)/)
assert.match(stewardFlowScript, /fetchStewardPlan\(planRequest/)
assert.match(stewardFlowScript, /timeoutMs:\s*35000/)
assert.match(personalWorkbenchAiModeScript, /async function executeModelPlannedWorkbenchIntent\(cleanPrompt, entry = \{\}, files = \[\]\)/)
assert.match(personalWorkbenchAiModeScript, /await stewardFlow\.resolveInlineExecutionPlan\(cleanPrompt, entry, files,\s*\{/)
assert.match(personalWorkbenchAiModeScript, /normalizeWorkbenchAiIntentPlan\(modelPlan,\s*\{\s*prompt:\s*cleanPrompt/)
assert.match(personalWorkbenchAiModeScript, /buildRuleFallbackWorkbenchAiIntentPlan\(cleanPrompt\)/)
assert.match(personalWorkbenchAiModeScript, /shouldRequestWorkbenchAiIntentPlan\(cleanPrompt\)/)
assert.match(personalWorkbenchAiModeScript, /resolveExecutableTravelApplicationPlan\(intentPlan\)/)
assert.doesNotMatch(personalWorkbenchAiModeScript, /fallbackIntentPlan/)
assert.match(personalWorkbenchAiModeScript, /autoSaveDraft:\s*travelApplicationRequest\.autoSaveDraft/)
assert.match(personalWorkbenchAiModeScript, /ontologyFields:\s*travelApplicationRequest\.ontologyFields/)
assert.match(applicationPreviewFlowScript, /options\.autoSaveDraft/)
assert.match(applicationPreviewFlowScript, /ontologyFields:\s*options\.ontologyFields/)
assert.match(applicationPreviewFlowScript, /executeInlineApplicationPreviewAction\(AI_APPLICATION_ACTION_SAVE_DRAFT/)
assert.match(applicationPreviewFlowScript, /buildInlineApplicationPreview\([\s\S]*ontologyFields:\s*options\.ontologyFields/)
assert.doesNotMatch(personalWorkbenchAiModeScript, /const travelApplicationRequest = resolveInlineTravelApplicationRequest\(cleanPrompt\)/)
})
test('workbench AI mode shows a visible planning response before waiting for steward model plan', () => {
assert.match(personalWorkbenchAiModeScript, /function startModelPlanningConversation\(cleanPrompt, entry = \{\}\)/)
assert.match(personalWorkbenchAiModeScript, /conversationMessages\.value\.push\(createInlineMessage\('user', cleanPrompt\)\)/)
assert.match(personalWorkbenchAiModeScript, /正在识别意图,准备拆解申请、报销和附件任务/)
assert.match(
personalWorkbenchAiModeScript,
/const plannerPendingMessage = startModelPlanningConversation\(cleanPrompt, entry\)[\s\S]*await stewardFlow\.resolveInlineExecutionPlan\(cleanPrompt, entry, files,\s*\{/
)
assert.match(
personalWorkbenchAiModeScript,
/pendingMessageId:\s*plannerPendingMessage\?\.id/
)
assert.match(applicationPreviewFlowScript, /options\.pendingMessageId/)
})
test('workbench AI mode streams planning thinking into the pending message', () => {
assert.match(planningThinkingModelScript, /buildModelPlanningProgressSchedule/)
assert.match(planningThinkingModelScript, /判断办理意图/)
assert.match(planningThinkingModelScript, /抽取关键信息/)
assert.match(planningThinkingModelScript, /规划执行步骤/)
assert.match(planningThinkingModelScript, /准备兜底策略/)
assert.match(personalWorkbenchAiModeScript, /function startModelPlanningProgressUpdates\(messageId\)/)
assert.match(personalWorkbenchAiModeScript, /globalThis\.setTimeout\(\(\) => \{\s*updateModelPlanningThinkingEvent\(messageId, event\)/)
assert.match(personalWorkbenchAiModeScript, /const stopPlanningProgressUpdates = startModelPlanningProgressUpdates\(plannerPendingMessage\.id\)/)
assert.match(personalWorkbenchAiModeScript, /stopPlanningProgressUpdates\(\)/)
assert.match(
personalWorkbenchAiModeScript,
/await stewardFlow\.resolveInlineExecutionPlan\(cleanPrompt, entry, files,\s*\{\s*pendingMessageId:\s*plannerPendingMessage\.id\s*\}\)/
)
assert.match(stewardFlowScript, /async function resolveInlineExecutionPlan\(prompt, entry = \{\}, files = \[\], options = \{\}\)/)
assert.match(stewardFlowScript, /fetchInlineStewardPlan\(planningMessageId, planRequest,\s*\{[\s\S]*includeAnswerDelta:\s*false/)
assert.match(applicationPreviewFlowScript, /mergeWorkbenchAiThinkingEvents\(previousThinkingEvents,\s*\[/)
})
test('workbench AI mode reuses planning pending message for regular steward replies', () => {
assert.match(
personalWorkbenchAiModeScript,
/stewardFlow\.requestInlineAssistantReply\(cleanPrompt, entry, files,\s*\{\s*pendingMessageId:\s*plannerPendingMessage\.id\s*\}\)/
)
assert.doesNotMatch(
personalWorkbenchAiModeScript,
/replaceInlineMessage\(plannerPendingMessage\.id,\s*createInlineMessage\('assistant', '已完成意图识别,继续为您整理回复。'/
)
assert.match(stewardFlowScript, /async function requestInlineAssistantReply\(prompt, entry = \{\}, files = \[\], options = \{\}\)/)
assert.match(stewardFlowScript, /const reusablePendingMessageId = String\(options\.pendingMessageId \|\| ''\)\.trim\(\)/)
assert.match(stewardFlowScript, /reusablePendingMessageId \? replaceInlineMessage\(reusablePendingMessageId, pendingMessage\) : conversationMessages\.value\.push\(pendingMessage\)/)
})