- 新增 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 测试
332 lines
15 KiB
JavaScript
332 lines
15 KiB
JavaScript
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\)/)
|
||
})
|