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\)/)
|
|||
|
|
})
|