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 测试
This commit is contained in:
caoxiaozhu
2026-06-24 21:58:46 +08:00
parent 5311c99d69
commit bc560145a4
18 changed files with 1914 additions and 38 deletions

View File

@@ -51,6 +51,9 @@ import {
import {
shouldUseBudgetCompileReport
} from '../src/views/scripts/budgetAssistantReportModel.js'
import {
buildInlineApplicationPreview
} from '../src/composables/workbenchAiMode/workbenchAiApplicationPreviewModel.js'
import { resolveStewardTypewriterNextIndex } from '../src/views/scripts/stewardTypewriter.js'
import {
ASSISTANT_SCOPE_ACTION_SWITCH,
@@ -140,6 +143,14 @@ const flowScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementFlow.js', import.meta.url)),
'utf8'
)
const personalWorkbenchAiModeScript = readFileSync(
fileURLToPath(new URL('../src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js', import.meta.url)),
'utf8'
)
const applicationPreviewFlowScript = readFileSync(
fileURLToPath(new URL('../src/composables/workbenchAiMode/useWorkbenchAiApplicationPreviewFlow.js', import.meta.url)),
'utf8'
)
function createFlowHarness() {
return useTravelReimbursementFlow({
@@ -241,6 +252,32 @@ test('application intent uses local preview instead of immediate orchestrator ca
assert.match(buildLocalApplicationPreviewMessage(preview), /点击对应行即可直接编辑/)
})
test('AI workbench routes compact travel direct-submit planner into application preview auto submit', () => {
assert.match(personalWorkbenchAiModeScript, /buildRuleFallbackWorkbenchAiIntentPlan/)
assert.match(personalWorkbenchAiModeScript, /normalizeWorkbenchAiIntentPlan/)
assert.match(personalWorkbenchAiModeScript, /resolveExecutableTravelApplicationPlan/)
assert.match(
personalWorkbenchAiModeScript,
/async function executeModelPlannedWorkbenchIntent\(cleanPrompt, entry = \{\}, files = \[\]\)/
)
assert.match(
personalWorkbenchAiModeScript,
/modelPlan = await stewardFlow\.resolveInlineExecutionPlan\(cleanPrompt, entry, files\)/
)
assert.match(
personalWorkbenchAiModeScript,
/const rulePlan = buildRuleFallbackWorkbenchAiIntentPlan\(cleanPrompt\)/
)
assert.match(
personalWorkbenchAiModeScript,
/applicationFlow\.startAiApplicationPreview\([\s\S]*travelApplicationRequest\.expenseType[\s\S]*travelApplicationRequest\.expenseTypeLabel[\s\S]*travelApplicationRequest\.sourceText[\s\S]*ontologyFields:\s*travelApplicationRequest\.ontologyFields[\s\S]*autoSubmit:\s*travelApplicationRequest\.autoSubmit/
)
assert.doesNotMatch(personalWorkbenchAiModeScript, /fallbackIntentPlan/)
assert.match(applicationPreviewFlowScript, /if \(options\.autoSubmit && normalizeApplicationPreview\(preview\)\.readyToSubmit\)/)
assert.match(applicationPreviewFlowScript, /confirmed:\s*true/)
assert.match(applicationPreviewFlowScript, /skipUserMessage:\s*true/)
})
test('unsupported business guidance opens in assistant conversation form', () => {
const conversation = buildUnsupportedBusinessScopeConversation('你好')
@@ -366,6 +403,20 @@ test('application preview renders ordered editable rows and submit text uses edi
assert.match(buildApplicationPreviewSubmitText(editedPreview), /系统预估费用1900元/)
})
test('application preview keeps compact direct-submit command out of business reason', () => {
const preview = buildInlineApplicationPreview(
'差旅费',
'去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交',
{ grade: 'P5' }
)
assert.equal(preview.fields.location, '上海')
assert.equal(preview.fields.reason, '辅助国网仿生产服务器部署')
assert.equal(preview.fields.transportMode, '火车')
assert.equal(preview.readyToSubmit, false)
assert.deepEqual(preview.missingFields, ['出发时间', '天数'])
})
test('application estimate builds deterministic mock transport amount and total', () => {
const trainEstimate = buildMockApplicationTransportEstimate({ transportMode: '高铁', location: '上海' })
const datedTrainEstimate = buildMockApplicationTransportEstimate({