feat: 完善文档中心与报销申请交互及侧边栏重构

后端优化编排器报销查询和本体检测精度,增强报销单草稿保
存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导
航,完善文档中心状态筛选和详情提示,报销创建和审批详情
页优化会话管理和费用明细交互,新增助手应用服务和预设动
作工具函数,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-25 13:35:39 +08:00
parent 50b1c3f9a9
commit d0e946cf47
59 changed files with 5117 additions and 416 deletions

View File

@@ -1,6 +1,6 @@
import { nextTick, ref } from 'vue'
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
import { fetchLatestConversation } from '../../services/orchestrator.js'
import {
clearAssistantSessionSnapshot,
readAssistantSessionSnapshot,
@@ -11,8 +11,9 @@ import {
filterPersistableFilePreviews
} from './travelReimbursementAttachmentModel.js'
import {
ASSISTANT_SESSION_TYPES,
SESSION_TYPE_APPLICATION,
SESSION_TYPE_EXPENSE,
SESSION_TYPE_KNOWLEDGE,
buildInitialInsightFromConversation,
buildWelcomeInsight,
buildWelcomeQuickActions,
@@ -20,6 +21,7 @@ import {
hasMeaningfulSessionMessages,
normalizeInitialConversationMessages,
normalizeSnapshotMessages,
normalizeAssistantSessionType,
resolveInitialConversationId,
resolveInitialDraftClaimId,
resolveInitialSessionType,
@@ -41,6 +43,10 @@ export function useTravelReimbursementSessionState({
scrollToBottom,
getSessionRuntimeRefs = () => ({})
}) {
function resolveDefaultSessionTypeFromEntry() {
return props.entrySource === 'application' ? SESSION_TYPE_APPLICATION : SESSION_TYPE_EXPENSE
}
function refreshWelcomeQuickActions(messages, sessionType) {
if (!Array.isArray(messages) || !messages.length) {
return []
@@ -58,8 +64,8 @@ export function useTravelReimbursementSessionState({
))
}
function buildConversationSessionState(conversation, fallbackSessionType = SESSION_TYPE_EXPENSE) {
const sessionType = resolveInitialSessionType(conversation) || fallbackSessionType
function buildConversationSessionState(conversation, fallbackSessionType = resolveDefaultSessionTypeFromEntry()) {
const sessionType = resolveInitialSessionType(conversation, fallbackSessionType)
const restoredMessages = refreshWelcomeQuickActions(normalizeInitialConversationMessages(conversation), sessionType)
const initialInsight = buildInitialInsightFromConversation(conversation)
const restoredReviewFilePreviews = buildReviewFilePreviewsFromMessages(restoredMessages)
@@ -84,17 +90,18 @@ export function useTravelReimbursementSessionState({
}
function buildEmptySessionState(sessionType) {
const normalizedSessionType = normalizeAssistantSessionType(sessionType, resolveDefaultSessionTypeFromEntry())
return {
sessionType,
sessionType: normalizedSessionType,
messages: [
createWelcomeAssistantMessage(props.entrySource, linkedRequest.value, sessionType, currentUser.value)
createWelcomeAssistantMessage(props.entrySource, linkedRequest.value, normalizedSessionType, currentUser.value)
],
conversationId: '',
draftClaimId: '',
currentInsight: buildWelcomeInsight(
props.entrySource,
linkedRequest.value,
sessionType,
normalizedSessionType,
currentUser.value
),
reviewFilePreviews: [],
@@ -107,13 +114,16 @@ export function useTravelReimbursementSessionState({
}
}
function buildPersistedSessionState(snapshot, fallbackSessionType = SESSION_TYPE_EXPENSE) {
function buildPersistedSessionState(snapshot, fallbackSessionType = resolveDefaultSessionTypeFromEntry()) {
const state = snapshot?.state && typeof snapshot.state === 'object' ? snapshot.state : null
if (!state) {
return null
}
const sessionType = String(state.sessionType || snapshot.sessionType || fallbackSessionType || '').trim() || SESSION_TYPE_EXPENSE
const sessionType = normalizeAssistantSessionType(
state.sessionType || snapshot.sessionType || fallbackSessionType,
fallbackSessionType
)
const restoredMessages = refreshWelcomeQuickActions(normalizeSnapshotMessages(state.messages), sessionType)
if (
!hasMeaningfulSessionMessages(restoredMessages)
@@ -148,13 +158,16 @@ export function useTravelReimbursementSessionState({
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
}
const initialSessionType = resolveInitialSessionType(props.initialConversation)
const defaultInitialSessionType = resolveDefaultSessionTypeFromEntry()
const initialSessionType = props.initialConversation
? resolveInitialSessionType(props.initialConversation, defaultInitialSessionType)
: defaultInitialSessionType
const shouldPersistLocalSnapshot = props.entrySource !== 'detail'
const conversationInitialState = props.initialConversation
? buildConversationSessionState(props.initialConversation, initialSessionType)
: buildEmptySessionState(initialSessionType)
const canRestorePersistedInitialState =
props.entrySource === 'workbench'
shouldPersistLocalSnapshot
&& !String(props.initialPrompt || '').trim()
&& !props.initialFiles.length
const persistedInitialSnapshot = readAssistantSessionSnapshot(resolveCurrentUserId(), initialSessionType)
@@ -174,21 +187,22 @@ export function useTravelReimbursementSessionState({
const conversationId = ref(initialSessionState.conversationId)
const draftClaimId = ref(initialSessionState.draftClaimId)
const reviewFilePreviews = ref(initialSessionState.reviewFilePreviews)
const sessionSnapshots = ref({
[SESSION_TYPE_EXPENSE]: null,
[SESSION_TYPE_KNOWLEDGE]: null
})
const sessionSnapshots = ref(
ASSISTANT_SESSION_TYPES.reduce((result, sessionType) => {
result[sessionType] = null
return result
}, {})
)
const currentInsight = ref(initialSessionState.currentInsight)
const composerUploadIntent = ref(String(initialSessionState.composerUploadIntent || '').trim())
const guidedFlowState = ref(normalizeGuidedFlowState(initialSessionState.guidedFlowState))
const insightPanelCollapsed = ref(false)
const sessionSwitchBusy = ref(false)
let knowledgeSessionResetPromise = Promise.resolve()
function buildPersistableSessionState(sessionState) {
const state = sessionState || captureCurrentSessionState()
return {
sessionType: state.sessionType || SESSION_TYPE_EXPENSE,
sessionType: normalizeAssistantSessionType(state.sessionType, resolveDefaultSessionTypeFromEntry()),
messages: serializeSessionMessages(state.messages),
conversationId: String(state.conversationId || '').trim(),
draftClaimId: String(state.draftClaimId || '').trim(),
@@ -244,7 +258,7 @@ export function useTravelReimbursementSessionState({
function applySessionState(sessionState) {
const runtimeRefs = getSessionRuntimeRefs()
const nextState = sessionState || buildEmptySessionState(activeSessionType.value)
activeSessionType.value = nextState.sessionType || SESSION_TYPE_EXPENSE
activeSessionType.value = normalizeAssistantSessionType(nextState.sessionType, resolveDefaultSessionTypeFromEntry())
messages.value = Array.isArray(nextState.messages) && nextState.messages.length
? nextState.messages
: [
@@ -287,38 +301,18 @@ export function useTravelReimbursementSessionState({
}
async function loadLatestSessionState(targetSessionType) {
const payload = await fetchLatestConversation(resolveCurrentUserId(), targetSessionType, {
preferRecoverable: targetSessionType === SESSION_TYPE_EXPENSE
const normalizedTarget = normalizeAssistantSessionType(targetSessionType, resolveDefaultSessionTypeFromEntry())
const payload = await fetchLatestConversation(resolveCurrentUserId(), normalizedTarget, {
preferRecoverable: normalizedTarget === SESSION_TYPE_EXPENSE
})
if (payload?.found && payload.conversation) {
return buildConversationSessionState(payload.conversation, targetSessionType)
return buildConversationSessionState(payload.conversation, normalizedTarget)
}
return buildEmptySessionState(targetSessionType)
}
function resetKnowledgeSessionSnapshot() {
const emptyKnowledgeState = buildEmptySessionState(SESSION_TYPE_KNOWLEDGE)
sessionSnapshots.value[SESSION_TYPE_KNOWLEDGE] = emptyKnowledgeState
if (activeSessionType.value === SESSION_TYPE_KNOWLEDGE) {
applySessionState(emptyKnowledgeState)
}
}
function clearKnowledgeSessionOnEntry() {
resetKnowledgeSessionSnapshot()
knowledgeSessionResetPromise = clearUserConversations(resolveCurrentUserId(), SESSION_TYPE_KNOWLEDGE)
.catch((error) => {
console.warn('Failed to clear knowledge session on entry:', error)
})
.finally(() => {
resetKnowledgeSessionSnapshot()
})
return knowledgeSessionResetPromise
return buildEmptySessionState(normalizedTarget)
}
async function switchSessionType(targetSessionType) {
const normalizedTarget = String(targetSessionType || '').trim() || SESSION_TYPE_EXPENSE
const normalizedTarget = normalizeAssistantSessionType(targetSessionType, resolveDefaultSessionTypeFromEntry())
if (normalizedTarget === activeSessionType.value || sessionSwitchBusy.value) {
return
}
@@ -338,7 +332,7 @@ export function useTravelReimbursementSessionState({
const emptyState = buildEmptySessionState(normalizedTarget)
sessionSnapshots.value[normalizedTarget] = emptyState
applySessionState(emptyState)
toast(error?.message || '?????????????????')
toast(error?.message || '切换助手失败,请稍后重试。')
} finally {
sessionSwitchBusy.value = false
}
@@ -368,8 +362,6 @@ export function useTravelReimbursementSessionState({
captureCurrentSessionState,
applySessionState,
loadLatestSessionState,
resetKnowledgeSessionSnapshot,
clearKnowledgeSessionOnEntry,
switchSessionType
}
}