feat(ai): add personal expense application memory
This commit is contained in:
@@ -66,6 +66,7 @@ import {
|
||||
} from '../src/utils/assistantSessionScope.js'
|
||||
import { useTravelReimbursementFlow } from '../src/views/scripts/useTravelReimbursementFlow.js'
|
||||
import { useApplicationPreviewEditor } from '../src/views/scripts/useApplicationPreviewEditor.js'
|
||||
import { resolveAssistantResultText } from '../src/views/scripts/travelReimbursementSubmitResponseModel.js'
|
||||
|
||||
const submitComposerScript = [
|
||||
'../src/views/scripts/travelReimbursementSubmitConstants.js',
|
||||
@@ -83,10 +84,12 @@ const stewardServiceScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/services/steward.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const createViewScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/TravelReimbursementCreateView.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const createViewScript = [
|
||||
'../src/views/scripts/TravelReimbursementCreateView.js',
|
||||
'../src/views/scripts/useTravelReimbursementCreateViewState.js',
|
||||
'../src/views/scripts/useTravelReimbursementCreateViewMessageHandlers.js',
|
||||
'../src/views/scripts/useTravelReimbursementApplicationPreviewDateEditor.js'
|
||||
].map((path) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')).join('\n')
|
||||
const messageActionsScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementMessageActions.js', import.meta.url)),
|
||||
'utf8'
|
||||
@@ -95,10 +98,11 @@ const suggestedActionsScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSuggestedActions.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const stewardRuntimeScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementStewardRuntime.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const stewardRuntimeScript = [
|
||||
'../src/views/scripts/useTravelReimbursementStewardRuntime.js',
|
||||
'../src/views/scripts/useTravelReimbursementStewardRuntimeDecision.js',
|
||||
'../src/views/scripts/useTravelReimbursementApplicationSubmitConfirm.js'
|
||||
].map((path) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')).join('\n')
|
||||
const stewardRuntimeTextModelScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/travelReimbursementStewardRuntimeTextModel.js', import.meta.url)),
|
||||
'utf8'
|
||||
@@ -131,18 +135,20 @@ const applicationMessageStyles = readFileSync(
|
||||
fileURLToPath(new URL('../src/assets/styles/components/travel-reimbursement-message-application.css', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const conversationModelScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/travelReimbursementConversationModel.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const conversationModelScript = [
|
||||
'../src/views/scripts/travelReimbursementConversationModel.js',
|
||||
'../src/views/scripts/travelReimbursementConversationMessageModel.js',
|
||||
'../src/views/scripts/travelReimbursementConversationStateModel.js'
|
||||
].map((path) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')).join('\n')
|
||||
const previewEditorScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useApplicationPreviewEditor.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const flowScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementFlow.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const flowScript = [
|
||||
'../src/views/scripts/useTravelReimbursementFlow.js',
|
||||
'../src/views/scripts/travelReimbursementFlowToolModel.js',
|
||||
'../src/views/scripts/travelReimbursementFlowTiming.js'
|
||||
].map((path) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')).join('\n')
|
||||
const personalWorkbenchAiModeScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js', import.meta.url)),
|
||||
'utf8'
|
||||
@@ -151,6 +157,10 @@ const applicationPreviewFlowScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/composables/workbenchAiMode/useWorkbenchAiApplicationPreviewFlow.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const applicationPreviewActionsScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementApplicationPreviewActions.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
function createFlowHarness() {
|
||||
return useTravelReimbursementFlow({
|
||||
@@ -288,7 +298,7 @@ test('unsupported business guidance opens in assistant conversation form', () =>
|
||||
assert.equal(conversation.messages.length, 1)
|
||||
assert.equal(conversation.messages[0].role, 'assistant')
|
||||
assert.match(conversation.messages[0].content, /小财管家暂时不处理「你好」/)
|
||||
assert.equal(conversation.messages[0].assistantName, '小财管家')
|
||||
assert.equal(conversation.messages[0].message_json.assistant_name, '小财管家')
|
||||
assert.match(conversation.messages[0].content, /### 当前可继续的场景/)
|
||||
assert.equal(
|
||||
conversation.messages[0].message_json.orchestrator_payload.result.suggested_actions.length,
|
||||
@@ -312,8 +322,8 @@ test('assistant scope guard blocks unsupported non-financial intent', () => {
|
||||
assert.equal(guard.suggestedActions.length, 4)
|
||||
assert.equal(guard.blocked, true)
|
||||
assert.equal(guard.targetSessionType, '')
|
||||
assert.match(guard.text, /此意图系统不支持/)
|
||||
assert.match(guard.text, /当前系统支持的业务范围/)
|
||||
assert.match(guard.text, /没有识别到当前系统支持的财务业务意图/)
|
||||
assert.match(guard.text, /当前可继续的场景/)
|
||||
})
|
||||
|
||||
|
||||
@@ -1057,7 +1067,7 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(createViewScript, /const isApplicationSession = computed/)
|
||||
assert.match(createViewScript, /insightPanelCollapsed,/)
|
||||
assert.doesNotMatch(createViewScript, /if \(isApplicationSession\.value\) \{\s*return false\s*\}/)
|
||||
assert.match(createViewScript, /activeFlowSteps\.value\.length > 0/)
|
||||
assert.match(createViewScript, /getActiveFlowSteps\(\)\.length > 0/)
|
||||
assert.match(createViewScript, /useApplicationPreviewEditor/)
|
||||
assert.match(messageActionsScript, /message-bubble-application-preview/)
|
||||
assert.match(messageActionsScript, /buildApplicationPreviewFooterMessage/)
|
||||
@@ -1078,7 +1088,7 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(messageItemTemplate, /v-html="ui\.renderMarkdown\(ui\.buildApplicationPreviewFooterText\(message\)\)"/)
|
||||
assert.doesNotMatch(messageItemTemplate, /class="application-date-editor-layer"/)
|
||||
assert.doesNotMatch(messageItemTemplate, /ui\.commitApplicationPreviewDateEditor\(message\)/)
|
||||
assert.doesNotMatch(messageItemTemplate, /application-preview-date-chip/)
|
||||
assert.match(messageItemTemplate, /'application-preview-date-chip': \['time', 'time_return'\]\.includes\(row\.key\) && !row\.missing/)
|
||||
assert.match(messageItemTemplate, /申请单据已生成/)
|
||||
assert.match(messageItemTemplate, /ui\.shouldShowDraftSavedCard\(message\)/)
|
||||
assert.match(messageItemTemplate, /报销草稿已生成/)
|
||||
@@ -1130,7 +1140,7 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(messageItemTemplate, /commitApplicationPreviewEditor/)
|
||||
assert.match(createViewScript, /resolveApplicationPreviewMissingFields/)
|
||||
assert.match(createViewScript, /function applyLinkedApplicationPreviewDateSelection/)
|
||||
assert.match(createViewScript, /onComposerDateSelection: applyLinkedApplicationPreviewDateSelection/)
|
||||
assert.match(createViewScript, /onComposerDateSelection: \(\.\.\.args\) => applyLinkedApplicationPreviewDateSelection\(\.\.\.args\)/)
|
||||
assert.match(createViewScript, /function openApplicationPreviewEditorFromUi/)
|
||||
assert.match(createViewScript, /syncComposerDateFromApplicationEditor/)
|
||||
assert.match(messageActionsScript, /function shouldShowAssistantMessageActions/)
|
||||
@@ -1149,8 +1159,9 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(previewEditorScript, /getTodayDateValue/)
|
||||
assert.match(previewEditorScript, /buildLocalApplicationPreviewMessage/)
|
||||
assert.match(previewEditorScript, /targetRow\.editable === false/)
|
||||
assert.match(previewEditorScript, /\[editor\.fieldKey\]: nextValue/)
|
||||
assert.match(previewEditorScript, /fieldKey === 'time'\) return 'date'/)
|
||||
assert.match(previewEditorScript, /function buildEditedApplicationPreviewFields/)
|
||||
assert.match(previewEditorScript, /\[isDateField \? 'time' : editor\.fieldKey\]: nextValue/)
|
||||
assert.match(previewEditorScript, /isApplicationPreviewDateField\(fieldKey\)\) return 'date'/)
|
||||
assert.match(previewEditorScript, /commitApplicationPreviewDateEditor/)
|
||||
|
||||
assert.match(messageItemStyles, /@import "\.\/travel-reimbursement-message-application\.css";/)
|
||||
@@ -1198,7 +1209,7 @@ test('steward application missing transport blocks preview table', () => {
|
||||
assert.match(submitComposerScript, /applicationPreview:\s*pauseForMissingFields \? null : applicationPreview/)
|
||||
assert.match(submitComposerScript, /我已经识别出这一步要先处理申请单,但现在还不能生成可提交的申请核对表/)
|
||||
assert.match(submitComposerScript, /applicationPreview:\s*normalized/)
|
||||
assert.doesNotMatch(submitComposerScript, /请先告诉我您打算怎么出行:\*\*火车、飞机或轮船\*\*/)
|
||||
assert.match(submitComposerScript, /请先告诉我您打算怎么出行:\*\*火车、飞机或轮船\*\*/)
|
||||
|
||||
assert.match(suggestedActionsScript, /payload\.applicationPreview/)
|
||||
assert.match(suggestedActionsScript, /function continueStewardApplicationFieldCompletion/)
|
||||
@@ -1208,7 +1219,14 @@ test('steward application missing transport blocks preview table', () => {
|
||||
assert.match(suggestedActionsScript, /openApplicationPreviewEditor\(targetMessage, fieldKey/)
|
||||
assert.match(suggestedActionsScript, /commitApplicationPreviewEditor\(targetMessage\)/)
|
||||
assert.match(stewardFieldCompletionScript, /transportMode:\s*'transport_mode'/)
|
||||
assert.match(stewardFieldCompletionScript, /基础规则交通费用预估表/)
|
||||
assert.match(stewardFieldCompletionScript, /模拟查询交通票据和费用口径/)
|
||||
})
|
||||
|
||||
test('steward lets server memory resolve transport but still blocks other missing fields', () => {
|
||||
assert.match(
|
||||
submitComposerScript,
|
||||
/missingFields\.some\(\(field\) => String\(field \|\| ''\)\.trim\(\) !== '出行方式'\)/
|
||||
)
|
||||
})
|
||||
|
||||
test('steward field completion reruns application preview instead of directly rendering table', () => {
|
||||
@@ -1253,7 +1271,7 @@ test('steward field completion reruns application preview instead of directly re
|
||||
assert.match(carryText, /用户已补充:出行方式:火车/)
|
||||
assert.match(carryText, /地点:北京/)
|
||||
assert.match(carryText, /天数:3天/)
|
||||
assert.match(carryText, /请先根据已补齐字段按基础规则交通费用预估表/)
|
||||
assert.match(carryText, /请先根据已补齐字段模拟查询交通票据和费用口径/)
|
||||
|
||||
const rebuiltPreview = buildLocalApplicationPreview(carryText, { name: '曹笑竹', grade: 'P5' })
|
||||
assert.equal(rebuiltPreview.fields.location, '北京')
|
||||
@@ -1309,8 +1327,7 @@ test('text confirmation submits pending application preview before replanning st
|
||||
assert.match(suggestedActionsScript, /skipApplicationModelReview:\s*targetSessionType === SESSION_TYPE_APPLICATION/)
|
||||
assert.match(suggestedActionsScript, /skipStewardSlotDecision:\s*targetSessionType === SESSION_TYPE_APPLICATION/)
|
||||
assert.match(submitComposerScript, /skipModelReview:\s*Boolean\(stewardDelegated && options\.skipApplicationModelReview\)/)
|
||||
assert.match(submitComposerScript, /const requireModelReview = shouldRequireApplicationModelReview\(rawText\)/)
|
||||
assert.match(submitComposerScript, /if \(options\.skipModelReview && !requireModelReview\) \{[\s\S]*结构化快路径/)
|
||||
assert.match(submitComposerScript, /if \(options\.skipModelReview\) \{[\s\S]*结构化快路径/)
|
||||
assert.match(submitComposerScript, /const localPauseForMissingFields = shouldPauseStewardApplicationPreview\(applicationPreview\)/)
|
||||
assert.match(submitComposerScript, /const shouldFetchSlotDecision = localPauseForMissingFields && !options\.skipStewardSlotDecision/)
|
||||
assert.match(submitComposerScript, /const slotDecision = shouldFetchSlotDecision[\s\S]*fetchStewardApplicationSlotDecision/)
|
||||
@@ -1327,29 +1344,33 @@ test('text confirmation submits pending application preview before replanning st
|
||||
assert.match(stewardRuntimeScript, /executeStewardRuntimeDecision\(decision, rawText, \{ userMessageAlreadyAdded \}\)/)
|
||||
assert.match(stewardRuntimeScript, /skipUserMessage: userMessageAlreadyAdded \|\| options\.skipUserMessage/)
|
||||
assert.match(stewardRuntimeScript, /fetchStewardRuntimeDecision\(\{[\s\S]*runtime_state: runtimeState/)
|
||||
assert.match(createViewScript, /if \(await handleStewardRuntimeDecision\(options\)\) \{[\s\S]*return null/)
|
||||
assert.match(createViewScript, /if \(await handleStewardRuntimeDecision\(options\)\) return null/)
|
||||
assert.match(stewardRuntimeTextModelScript, /function isApplicationSubmitConfirmationText/)
|
||||
assert.match(stewardRuntimeTextModelScript, /APPLICATION_SUBMIT_CONFIRM_TEXT_PATTERN[\s\S]*确认提交[\s\S]*提交审批/)
|
||||
assert.match(stewardRuntimeScript, /function findPendingApplicationSubmitMessage/)
|
||||
assert.match(stewardRuntimeScript, /normalizedPreview\.readyToSubmit/)
|
||||
assert.match(stewardRuntimeScript, /async function handleApplicationSubmitConfirmationText/)
|
||||
assert.match(stewardRuntimeScript, /await confirmApplicationSubmit\(\{ userText: rawText \}\)/)
|
||||
assert.match(createViewScript, /if \(await handleApplicationSubmitConfirmationText\(options\)\) \{[\s\S]*return null[\s\S]*\}[\s\S]*if \(isStewardSession\.value && !options\.skipStewardPlan/)
|
||||
assert.match(createViewScript, /if \(await handleApplicationSubmitConfirmationText\(options\)\) return null[\s\S]*if \(await handleGuidedStewardPlan\(options\)\) return null/)
|
||||
assert.match(stewardRuntimeScript, /message\.applicationSubmitConfirmed = true/)
|
||||
assert.match(stewardRuntimeScript, /message\.applicationSubmitConfirmed[\s\S]*continue/)
|
||||
})
|
||||
|
||||
test('application submit result does not render reimbursement review followup', () => {
|
||||
assert.match(submitComposerScript, /function shouldExposeReviewPayloadForMessage\(payload, options = \{\}\)/)
|
||||
assert.match(submitComposerScript, /options\.isApplicationSubmitOperation \|\| isApplicationDraftPayload\(result\.draft_payload\)/)
|
||||
assert.match(submitComposerScript, /function buildPresentationPayload\(payload, \{ exposeReviewPayload = true \} = \{\}\)/)
|
||||
assert.match(submitComposerScript, /review_payload:\s*null/)
|
||||
assert.match(submitComposerScript, /const exposeReviewPayload = shouldExposeReviewPayloadForMessage\(payload, \{ isApplicationSubmitOperation \}\)/)
|
||||
assert.match(submitComposerScript, /const presentationPayload = buildPresentationPayload\(payload, \{ exposeReviewPayload \}\)/)
|
||||
assert.match(submitComposerScript, /const resultReviewPayload = presentationResult\.review_payload \|\| null/)
|
||||
assert.match(submitComposerScript, /suggestedActions:\s*resultSuggestedActions/)
|
||||
assert.match(submitComposerScript, /reviewPayload:\s*resultReviewPayload/)
|
||||
assert.match(submitComposerScript, /buildAgentInsight\(\s*presentationPayload,/)
|
||||
test('application submit result uses dedicated action without reimbursement review followup', () => {
|
||||
const submittedPayload = {
|
||||
result: {
|
||||
answer: '申请提交成功',
|
||||
draft_payload: {
|
||||
draft_type: 'expense_application',
|
||||
status: 'submitted'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(resolveAssistantResultText(submittedPayload, 'fallback'), '')
|
||||
assert.match(stewardRuntimeScript, /applicationPreview[\s\S]*await runApplicationPreviewAction\([\s\S]*AI_APPLICATION_ACTION_SUBMIT/)
|
||||
assert.match(applicationPreviewActionsScript, /message\.draftPayload = nextDraftPayload/)
|
||||
assert.doesNotMatch(applicationPreviewActionsScript, /message\.reviewPayload\s*=/)
|
||||
})
|
||||
|
||||
test('steward streaming uses chunked typewriter to reduce perceived latency', () => {
|
||||
@@ -1371,18 +1392,19 @@ test('steward typewriter renders markdown table blocks at once', () => {
|
||||
assert.equal(resolveStewardTypewriterNextIndex(tableChars, normalIndex), 3)
|
||||
assert.equal(resolveStewardTypewriterNextIndex(tableChars, tableIndex), nextParagraphIndex)
|
||||
assert.equal(resolveStewardTypewriterNextIndex(tableChars, tableIndex - 1), nextParagraphIndex)
|
||||
assert.equal(resolveStewardTypewriterNextIndex(Array.from('### 核对结果'), 0), 2)
|
||||
assert.equal(resolveStewardTypewriterNextIndex(Array.from('### 核对结果'), 0), 3)
|
||||
})
|
||||
|
||||
test('application preview table appears as a whole card instead of row-by-row animation', () => {
|
||||
assert.doesNotMatch(
|
||||
test('application preview rows use bounded staggered reveal with reduced-motion fallback', () => {
|
||||
assert.match(
|
||||
messageItemStyles,
|
||||
/structured-card-reveal-enter-active\s+\.application-preview-row\s*\{[\s\S]*animation:/,
|
||||
)
|
||||
assert.doesNotMatch(
|
||||
assert.match(
|
||||
messageItemStyles,
|
||||
/application-preview-row:nth-child\([^)]*\)\s*\{[\s\S]*animation-delay:/,
|
||||
/application-preview-row:nth-child\(n \+ 6\)\s*\{[\s\S]*animation-delay: 165ms;/,
|
||||
)
|
||||
assert.match(messageItemStyles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.structured-card-reveal-enter-active \.application-preview-row,[\s\S]*animation: none;/)
|
||||
})
|
||||
|
||||
test('complex travel application sentences require model review', () => {
|
||||
@@ -1437,7 +1459,7 @@ test('steward application carry text does not leak transport examples into extra
|
||||
assert.match(carryText, /费用类型:差旅/)
|
||||
assert.doesNotMatch(carryText, /费用类型:travel/)
|
||||
assert.match(carryText, /还需要补充:出行方式/)
|
||||
assert.doesNotMatch(carryText, /请先追问上述缺失信息/)
|
||||
assert.match(carryText, /请先追问上述缺失信息/)
|
||||
assert.doesNotMatch(carryText, /请直接生成申请单核对结果/)
|
||||
assert.doesNotMatch(carryText, /入库或提交审批前/)
|
||||
assert.doesNotMatch(carryText, /高铁|火车|飞机|轮船|自驾|出租车/)
|
||||
@@ -1601,10 +1623,10 @@ test('assistant markdown tables render with component-scoped table styling', ()
|
||||
assert.match(rendered, /<th/)
|
||||
assert.match(rendered, /<td/)
|
||||
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(\.markdown-table-wrap\) \{[\s\S]*overflow-x: auto;[\s\S]*border: 1px solid #dbe4ee;/)
|
||||
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(table\) \{[\s\S]*min-width: 560px;[\s\S]*table-layout: fixed;/)
|
||||
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(th\),[\s\S]*\.message-answer-markdown :deep\(td\) \{[\s\S]*padding: 8px 10px;[\s\S]*overflow-wrap: break-word;/)
|
||||
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(th:first-child\),[\s\S]*\.message-answer-markdown :deep\(td:first-child\) \{[\s\S]*width: 88px;[\s\S]*white-space: nowrap;[\s\S]*word-break: keep-all;/)
|
||||
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(th:last-child\),[\s\S]*\.message-answer-markdown :deep\(td:last-child\) \{[\s\S]*width: 112px;[\s\S]*text-align: right;[\s\S]*white-space: nowrap;[\s\S]*word-break: keep-all;/)
|
||||
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(table\) \{[\s\S]*min-width: 460px;[\s\S]*border-collapse: separate;/)
|
||||
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(th\),[\s\S]*\.message-answer-markdown :deep\(td\) \{[\s\S]*padding: 8px 10px;[\s\S]*white-space: normal;/)
|
||||
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(tbody tr:nth-child\(even\) td\) \{[\s\S]*background: #fbfdff;/)
|
||||
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(tbody tr:last-child td\) \{[\s\S]*border-bottom: 0;/)
|
||||
})
|
||||
|
||||
test('assistant reimbursement recognition copy renders structured markdown sections', () => {
|
||||
|
||||
Reference in New Issue
Block a user