feat(web): 票据夹资产缓存接入与 AI 工作台附件流程完善
- ReceiptFolderView 删除票据后提示已关联附件副本保留,接入 useToast;fetchReceiptFolderAsset 加 no-store 避免预览缓存 - PersonalWorkbenchAiMode 附件区/对话气泡适配资产缓存,personal-workbench-ai-mode.css 调整布局 - usePersonalWorkbenchAiMode/useWorkbenchAiApplicationPreviewFlow/useWorkbenchAiAttachmentAssociationFlow/useWorkbenchAiStewardFlow 完善附件草稿选择与关联流程 - travelRequestDetailSmartEntryRecognition 智能识别增强,AppShellRouteView/PersonalWorkbenchView/useApplicationPreviewEditor/useTravelReimbursementSubmitComposer 等配套适配 - 新增 expense-attachment-draft-selection、receipt-folder-asset-cache、travel-request-detail-smart-entry-recognition 测试,更新 attachment-association-confirmation、expense-application-fast-preview、workbench-ai-mode-switch 测试
This commit is contained in:
@@ -78,6 +78,8 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
const {
|
||||
applicationPreviewEditor,
|
||||
resolveApplicationPreviewEditorControl,
|
||||
resolveApplicationPreviewEditorDateMax,
|
||||
resolveApplicationPreviewEditorDateMin,
|
||||
resolveApplicationPreviewEditorOptions,
|
||||
refreshApplicationPreviewEstimate,
|
||||
isApplicationPreviewEditing,
|
||||
@@ -112,7 +114,6 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
} = useWorkbenchComposerDate({ draft: assistantDraft, focusInput: focusAiModeInput })
|
||||
|
||||
const aiModeActionItems = AI_MODE_ACTION_ITEMS
|
||||
const selectedFileCards = computed(() => buildSelectedFileCards(selectedFiles.value))
|
||||
const displayUserName = computed(() => {
|
||||
const user = currentUser.value || {}
|
||||
return String(user.name || user.username || '同事').trim() || '同事'
|
||||
@@ -161,9 +162,19 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
scrollInlineConversationToBottom,
|
||||
sending,
|
||||
streamOrSetInlineAssistantContent,
|
||||
notifyRequestUpdated: (payload) => emit('request-updated', payload),
|
||||
toast
|
||||
})
|
||||
|
||||
watch(selectedFiles, (files) => {
|
||||
attachmentFlow.primeAiModeReceiptContext(files)
|
||||
})
|
||||
|
||||
const selectedFileCards = computed(() => buildSelectedFileCards(selectedFiles.value).map((card, index) => ({
|
||||
...card,
|
||||
ocrState: attachmentFlow.resolveAiModeReceiptRecognitionState(selectedFiles.value[index])
|
||||
})))
|
||||
|
||||
const applicationFlow = useWorkbenchAiApplicationPreviewFlow({
|
||||
activateInlineConversation,
|
||||
applicationPreviewEditor,
|
||||
@@ -189,6 +200,8 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
refreshApplicationPreviewEstimate,
|
||||
removeWorkbenchDateTag,
|
||||
replaceInlineMessage,
|
||||
resolveApplicationPreviewEditorDateMax,
|
||||
resolveApplicationPreviewEditorDateMin,
|
||||
resolveApplicationPreviewEditorControl,
|
||||
resolveApplicationPreviewEditorOptions,
|
||||
resolveInlineThinkingEvents,
|
||||
@@ -776,6 +789,8 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
requestDeleteCurrentConversation: () => sessionCommands.requestDeleteCurrentConversation(deleteDialogOpen),
|
||||
resolveApplicationPreviewEditorOptions: applicationFlow.resolveApplicationPreviewEditorOptions,
|
||||
resolveInlineApplicationPreviewEditorControl: applicationFlow.resolveInlineApplicationPreviewEditorControl,
|
||||
resolveInlineApplicationPreviewEditorDateMax: applicationFlow.resolveInlineApplicationPreviewEditorDateMax,
|
||||
resolveInlineApplicationPreviewEditorDateMin: applicationFlow.resolveInlineApplicationPreviewEditorDateMin,
|
||||
resolveInlineApplicationPreviewMissingFields: applicationFlow.resolveInlineApplicationPreviewMissingFields,
|
||||
resolveInlineApplicationPreviewRows: applicationFlow.resolveInlineApplicationPreviewRows,
|
||||
resolveInlineAttachmentOcrDocuments,
|
||||
|
||||
@@ -68,6 +68,8 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
refreshApplicationPreviewEstimate,
|
||||
removeWorkbenchDateTag,
|
||||
replaceInlineMessage,
|
||||
resolveApplicationPreviewEditorDateMax,
|
||||
resolveApplicationPreviewEditorDateMin,
|
||||
resolveApplicationPreviewEditorControl,
|
||||
resolveApplicationPreviewEditorOptions,
|
||||
resolveInlineThinkingEvents,
|
||||
@@ -105,8 +107,15 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
}
|
||||
|
||||
function resolveInlineApplicationPreviewEditorControl(fieldKey) {
|
||||
const control = resolveApplicationPreviewEditorControl(fieldKey)
|
||||
return control === 'date' ? 'text' : control
|
||||
return resolveApplicationPreviewEditorControl(fieldKey)
|
||||
}
|
||||
|
||||
function resolveInlineApplicationPreviewEditorDateMin(message, fieldKey) {
|
||||
return resolveApplicationPreviewEditorDateMin?.(message, fieldKey) || ''
|
||||
}
|
||||
|
||||
function resolveInlineApplicationPreviewEditorDateMax(message, fieldKey) {
|
||||
return resolveApplicationPreviewEditorDateMax?.(message, fieldKey) || ''
|
||||
}
|
||||
|
||||
function buildInlineApplicationPreviewSuggestedActions(applicationPreview = {}, draftPayload = null) {
|
||||
@@ -180,6 +189,14 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
return '申请核对表已补齐,费用测算已同步。你仍可点击表格继续修改;确认无误后,可以点击下方按钮保存草稿或直接提交,也可以直接回复“保存草稿”或“提交”。'
|
||||
}
|
||||
|
||||
function buildInlineApplicationActionFailureText(error, isSubmit) {
|
||||
return [
|
||||
isSubmit ? '### 申请提交失败' : '### 申请草稿保存失败',
|
||||
error?.message || (isSubmit ? '申请提交失败,请稍后重试。' : '申请草稿保存失败,请稍后重试。'),
|
||||
'我已保留当前申请核对表,您可以修改后重试,也可以稍后再次保存。'
|
||||
].join('\n\n')
|
||||
}
|
||||
|
||||
function resolveLatestApplicationPreviewMessage() {
|
||||
return [...conversationMessages.value]
|
||||
.reverse()
|
||||
@@ -385,8 +402,14 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
} catch (error) {
|
||||
replaceInlineMessage(
|
||||
pendingMessage.id,
|
||||
createInlineMessage('assistant', error?.message || (isSubmit ? '申请提交失败,请稍后重试。' : '申请草稿保存失败,请稍后重试。'), {
|
||||
createInlineMessage('assistant', buildInlineApplicationActionFailureText(error, isSubmit), {
|
||||
id: pendingMessage.id,
|
||||
applicationPreview: targetMessage.applicationPreview,
|
||||
draftPayload: targetMessage.draftPayload || options.draftPayload || null,
|
||||
suggestedActions: buildInlineApplicationPreviewSuggestedActions(
|
||||
targetMessage.applicationPreview,
|
||||
targetMessage.draftPayload || options.draftPayload || null
|
||||
),
|
||||
stewardPlan: {
|
||||
streamStatus: 'failed',
|
||||
thinkingEvents: resolveInlineThinkingEvents(pendingMessage).map((item) => ({
|
||||
@@ -504,6 +527,8 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
openApplicationPreviewEditor,
|
||||
resolveApplicationPreviewEditorOptions,
|
||||
resolveInlineApplicationPreviewEditorControl,
|
||||
resolveInlineApplicationPreviewEditorDateMax,
|
||||
resolveInlineApplicationPreviewEditorDateMin,
|
||||
resolveInlineApplicationPreviewMissingFields,
|
||||
resolveInlineApplicationPreviewRows,
|
||||
startAiApplicationPreview
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
import * as aiAttachmentAssociationModel from '../../utils/aiAttachmentAssociationModel.js'
|
||||
import { syncExpenseClaimFilesToDraft } from '../../utils/expenseClaimAttachmentSync.js'
|
||||
import { collectReceiptFiles } from '../../views/scripts/travelReimbursementAttachmentModel.js'
|
||||
import {
|
||||
buildFileIdentity,
|
||||
collectReceiptFiles
|
||||
} from '../../views/scripts/travelReimbursementAttachmentModel.js'
|
||||
import {
|
||||
createExpenseClaimItem,
|
||||
extractExpenseClaimItems,
|
||||
@@ -76,42 +81,256 @@ export function useWorkbenchAiAttachmentAssociationFlow({
|
||||
scrollInlineConversationToBottom,
|
||||
sending,
|
||||
streamOrSetInlineAssistantContent,
|
||||
notifyRequestUpdated,
|
||||
toast
|
||||
}) {
|
||||
async function collectAiModeReceiptContext(files = []) {
|
||||
const safeFiles = Array.isArray(files) ? files : []
|
||||
const aiModeReceiptContextCache = new Map()
|
||||
const aiModeReceiptRecognitionState = reactive({})
|
||||
|
||||
function resolveAiModeReceiptRecognitionStateKey(file) {
|
||||
return buildFileIdentity(file)
|
||||
}
|
||||
|
||||
function pruneAiModeReceiptRecognitionState(files = []) {
|
||||
const activeKeys = new Set(
|
||||
(Array.isArray(files) ? files : [])
|
||||
.filter((file) => isLikelyAiModeOcrFile(file))
|
||||
.map((file) => resolveAiModeReceiptRecognitionStateKey(file))
|
||||
.filter(Boolean)
|
||||
)
|
||||
Object.keys(aiModeReceiptRecognitionState).forEach((key) => {
|
||||
if (!activeKeys.has(key)) {
|
||||
delete aiModeReceiptRecognitionState[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setAiModeReceiptRecognitionState(files = [], patch = {}) {
|
||||
const recognitionFiles = (Array.isArray(files) ? files : [])
|
||||
.filter((file) => isLikelyAiModeOcrFile(file))
|
||||
recognitionFiles.forEach((file) => {
|
||||
const key = resolveAiModeReceiptRecognitionStateKey(file)
|
||||
if (!key) {
|
||||
return
|
||||
}
|
||||
aiModeReceiptRecognitionState[key] = {
|
||||
...(aiModeReceiptRecognitionState[key] || {}),
|
||||
fileName: String(file?.name || '').trim(),
|
||||
...patch
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function findAiModeReceiptDocumentForFile(file = {}, documents = [], index = 0) {
|
||||
const fileName = String(file?.name || '').trim()
|
||||
if (fileName) {
|
||||
const exactDocument = documents.find((document) => (
|
||||
String(document?.filename || document?.name || '').trim() === fileName
|
||||
))
|
||||
if (exactDocument) {
|
||||
return exactDocument
|
||||
}
|
||||
}
|
||||
return documents[index] || null
|
||||
}
|
||||
|
||||
function buildAiModeReceiptRecognitionPendingState() {
|
||||
return {
|
||||
status: 'recognizing',
|
||||
label: '智能录入识别中',
|
||||
title: '正在调用智能录入 OCR 识别票据内容'
|
||||
}
|
||||
}
|
||||
|
||||
function buildAiModeReceiptRecognitionDoneState(document = null) {
|
||||
const detail = String(
|
||||
document?.document_type_label ||
|
||||
document?.scene_label ||
|
||||
document?.document_type ||
|
||||
''
|
||||
).trim()
|
||||
return {
|
||||
status: 'recognized',
|
||||
label: detail ? `已识别票据 · ${detail}` : '已识别票据',
|
||||
title: detail ? `智能录入已完成,识别为${detail}` : '智能录入已完成'
|
||||
}
|
||||
}
|
||||
|
||||
function applyAiModeReceiptRecognitionResult(files = [], context = {}) {
|
||||
const documents = Array.isArray(context?.ocrDocuments) ? context.ocrDocuments : []
|
||||
const recognitionFiles = (Array.isArray(files) ? files : [])
|
||||
.filter((file) => isLikelyAiModeOcrFile(file))
|
||||
recognitionFiles.forEach((file, index) => {
|
||||
const document = findAiModeReceiptDocumentForFile(file, documents, index)
|
||||
setAiModeReceiptRecognitionState([file], buildAiModeReceiptRecognitionDoneState(document))
|
||||
})
|
||||
}
|
||||
|
||||
function resolveAiModeReceiptRecognitionState(file) {
|
||||
const key = resolveAiModeReceiptRecognitionStateKey(file)
|
||||
return key ? aiModeReceiptRecognitionState[key] || null : null
|
||||
}
|
||||
|
||||
function buildAiModeReceiptBaseContext(safeFiles = [], ocrFiles = []) {
|
||||
const attachmentNames = safeFiles
|
||||
.map((file) => String(file?.name || '').trim())
|
||||
.filter(Boolean)
|
||||
const ocrFiles = safeFiles.filter((file) => isLikelyAiModeOcrFile(file))
|
||||
const ocrSourceFileNames = ocrFiles
|
||||
.map((file) => String(file?.name || '').trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const baseContext = {
|
||||
return {
|
||||
attachmentNames,
|
||||
attachmentCount: attachmentNames.length,
|
||||
ocrSourceFileNames,
|
||||
ocrSummary: '',
|
||||
ocrDocuments: []
|
||||
}
|
||||
}
|
||||
|
||||
function buildAiModeReceiptContextCacheKey(ocrFiles = []) {
|
||||
return (Array.isArray(ocrFiles) ? ocrFiles : [])
|
||||
.map((file) => buildFileIdentity(file))
|
||||
.filter(Boolean)
|
||||
.join('|')
|
||||
}
|
||||
|
||||
function buildAiModeReceiptContextFromCollected(baseContext = {}, collected = {}) {
|
||||
return {
|
||||
...baseContext,
|
||||
ocrPayload: collected.ocrPayload || { documents: collected.ocrDocuments || [] },
|
||||
ocrSummary: String(collected.ocrSummary || '').trim(),
|
||||
ocrDocuments: Array.isArray(collected.ocrDocuments) ? collected.ocrDocuments : [],
|
||||
ocrFilePreviews: Array.isArray(collected.ocrFilePreviews) ? collected.ocrFilePreviews : []
|
||||
}
|
||||
}
|
||||
|
||||
function rememberAiModeReceiptContext(cacheKey, context) {
|
||||
if (!cacheKey) {
|
||||
return
|
||||
}
|
||||
aiModeReceiptContextCache.set(cacheKey, {
|
||||
status: 'resolved',
|
||||
context: {
|
||||
ocrPayload: context.ocrPayload,
|
||||
ocrSummary: context.ocrSummary,
|
||||
ocrDocuments: context.ocrDocuments,
|
||||
ocrFilePreviews: context.ocrFilePreviews
|
||||
}
|
||||
})
|
||||
if (aiModeReceiptContextCache.size > 20) {
|
||||
aiModeReceiptContextCache.delete(aiModeReceiptContextCache.keys().next().value)
|
||||
}
|
||||
}
|
||||
|
||||
function startAiModeReceiptRecognition(files = []) {
|
||||
const safeFiles = Array.isArray(files) ? files : []
|
||||
const ocrFiles = safeFiles.filter((file) => isLikelyAiModeOcrFile(file))
|
||||
const cacheKey = buildAiModeReceiptContextCacheKey(ocrFiles)
|
||||
if (!ocrFiles.length || !cacheKey) {
|
||||
return null
|
||||
}
|
||||
|
||||
const cached = aiModeReceiptContextCache.get(cacheKey)
|
||||
if (cached?.status === 'resolved') {
|
||||
applyAiModeReceiptRecognitionResult(ocrFiles, cached.context)
|
||||
return null
|
||||
}
|
||||
if (cached?.status === 'pending' && cached.promise) {
|
||||
setAiModeReceiptRecognitionState(ocrFiles, buildAiModeReceiptRecognitionPendingState())
|
||||
return cached.promise
|
||||
}
|
||||
|
||||
setAiModeReceiptRecognitionState(ocrFiles, buildAiModeReceiptRecognitionPendingState())
|
||||
|
||||
const promise = collectReceiptFiles({
|
||||
files: ocrFiles,
|
||||
recognizeOcrFiles
|
||||
}).then((collected) => {
|
||||
const context = buildAiModeReceiptContextFromCollected(
|
||||
buildAiModeReceiptBaseContext(safeFiles, ocrFiles),
|
||||
collected
|
||||
)
|
||||
rememberAiModeReceiptContext(cacheKey, context)
|
||||
applyAiModeReceiptRecognitionResult(ocrFiles, context)
|
||||
return context
|
||||
}).catch((error) => {
|
||||
aiModeReceiptContextCache.delete(cacheKey)
|
||||
setAiModeReceiptRecognitionState(ocrFiles, {
|
||||
status: 'failed',
|
||||
label: '识别失败',
|
||||
title: error?.message || '智能录入 OCR 识别失败'
|
||||
})
|
||||
throw error
|
||||
})
|
||||
|
||||
aiModeReceiptContextCache.set(cacheKey, {
|
||||
status: 'pending',
|
||||
promise
|
||||
})
|
||||
return promise
|
||||
}
|
||||
|
||||
function primeAiModeReceiptContext(files = []) {
|
||||
pruneAiModeReceiptRecognitionState(files)
|
||||
const promise = startAiModeReceiptRecognition(files)
|
||||
if (promise && typeof promise.catch === 'function') {
|
||||
promise.catch((error) => {
|
||||
console.warn('AI mode OCR preload failed:', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function collectAiModeReceiptContext(files = []) {
|
||||
const safeFiles = Array.isArray(files) ? files : []
|
||||
const ocrFiles = safeFiles.filter((file) => isLikelyAiModeOcrFile(file))
|
||||
const baseContext = buildAiModeReceiptBaseContext(safeFiles, ocrFiles)
|
||||
|
||||
if (!ocrFiles.length) {
|
||||
return baseContext
|
||||
}
|
||||
|
||||
const cacheKey = buildAiModeReceiptContextCacheKey(ocrFiles)
|
||||
const cached = cacheKey ? aiModeReceiptContextCache.get(cacheKey) : null
|
||||
if (cached?.status === 'resolved') {
|
||||
applyAiModeReceiptRecognitionResult(ocrFiles, cached.context)
|
||||
return {
|
||||
...baseContext,
|
||||
...cached.context
|
||||
}
|
||||
}
|
||||
if (cached?.status === 'pending' && cached.promise) {
|
||||
try {
|
||||
const cachedContext = await cached.promise
|
||||
applyAiModeReceiptRecognitionResult(ocrFiles, cachedContext)
|
||||
return {
|
||||
...baseContext,
|
||||
ocrPayload: cachedContext.ocrPayload,
|
||||
ocrSummary: cachedContext.ocrSummary,
|
||||
ocrDocuments: cachedContext.ocrDocuments,
|
||||
ocrFilePreviews: cachedContext.ocrFilePreviews
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('AI mode OCR preload result unavailable:', error)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
setAiModeReceiptRecognitionState(ocrFiles, buildAiModeReceiptRecognitionPendingState())
|
||||
const collected = await collectReceiptFiles({
|
||||
files: ocrFiles,
|
||||
recognizeOcrFiles
|
||||
})
|
||||
return {
|
||||
...baseContext,
|
||||
ocrSummary: String(collected.ocrSummary || '').trim(),
|
||||
ocrDocuments: Array.isArray(collected.ocrDocuments) ? collected.ocrDocuments : []
|
||||
}
|
||||
const context = buildAiModeReceiptContextFromCollected(baseContext, collected)
|
||||
rememberAiModeReceiptContext(cacheKey, context)
|
||||
applyAiModeReceiptRecognitionResult(ocrFiles, context)
|
||||
return context
|
||||
} catch (error) {
|
||||
console.warn('AI mode OCR request failed:', error)
|
||||
setAiModeReceiptRecognitionState(ocrFiles, {
|
||||
status: 'failed',
|
||||
label: '识别失败',
|
||||
title: error?.message || '智能录入 OCR 识别失败'
|
||||
})
|
||||
return {
|
||||
...baseContext,
|
||||
ocrError: error?.message || 'OCR识别失败,已继续使用附件名称。'
|
||||
@@ -220,6 +439,13 @@ export function useWorkbenchAiAttachmentAssociationFlow({
|
||||
createExpenseClaimItem,
|
||||
uploadExpenseClaimItemAttachment
|
||||
})
|
||||
notifyRequestUpdated?.({
|
||||
claimId: runtime.claimId,
|
||||
claimNo: runtime.claimNo,
|
||||
source: 'ai-workbench-attachment-association-sync',
|
||||
uploadedCount: Number(syncResult?.uploadedCount || 0),
|
||||
skippedCount: Number(syncResult?.skippedCount || 0)
|
||||
})
|
||||
const finalMessageText = aiAttachmentAssociationModel.buildAiAttachmentAssociationResultMessage({
|
||||
claimNo: runtime.claimNo,
|
||||
fileNames: runtime.fileNames,
|
||||
@@ -281,10 +507,7 @@ export function useWorkbenchAiAttachmentAssociationFlow({
|
||||
scrollInlineConversationToBottom()
|
||||
|
||||
try {
|
||||
const collected = await collectReceiptFiles({
|
||||
files,
|
||||
recognizeOcrFiles
|
||||
})
|
||||
const collected = await collectAiModeReceiptContext(files)
|
||||
const attachmentOcrDetails = buildInlineAttachmentOcrDetails(collected, files)
|
||||
const claimsPayload = await fetchExpenseClaims({ page: 1, pageSize: 100 })
|
||||
const claims = extractExpenseClaimItems(claimsPayload)
|
||||
@@ -351,7 +574,9 @@ export function useWorkbenchAiAttachmentAssociationFlow({
|
||||
return {
|
||||
collectAiModeReceiptContext,
|
||||
confirmAiAttachmentAssociation,
|
||||
primeAiModeReceiptContext,
|
||||
requestAiAttachmentAssociationReply,
|
||||
resolveAiModeReceiptRecognitionState,
|
||||
resolveAiAttachmentAssociationClaimNo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ import {
|
||||
buildRequiredApplicationSelectionText,
|
||||
filterRequiredApplicationCandidates
|
||||
} from '../../views/scripts/travelReimbursementApplicationLinkModel.js'
|
||||
import {
|
||||
buildInlineAttachmentOcrDetails
|
||||
} from './workbenchAiMessageModel.js'
|
||||
|
||||
function shouldCheckAiRequiredApplicationGate(prompt) {
|
||||
const compact = String(prompt || '').replace(/\s+/g, '')
|
||||
@@ -269,6 +272,7 @@ export function useWorkbenchAiStewardFlow({
|
||||
}
|
||||
|
||||
const receiptContext = await collectAiModeReceiptContext(files)
|
||||
const attachmentOcrDetails = buildInlineAttachmentOcrDetails(receiptContext, files)
|
||||
const planRequest = buildStewardPlanRequest({
|
||||
rawText: prompt,
|
||||
files,
|
||||
@@ -330,7 +334,8 @@ export function useWorkbenchAiStewardFlow({
|
||||
},
|
||||
suggestedActions: requiredApplicationContinuationFlow
|
||||
? buildAiRequiredApplicationGateSuggestedActions(requiredApplicationContinuationFlow, prompt)
|
||||
: buildStewardSuggestedActions(plan)
|
||||
: buildStewardSuggestedActions(plan),
|
||||
attachmentOcrDetails
|
||||
})
|
||||
)
|
||||
persistCurrentConversation()
|
||||
|
||||
Reference in New Issue
Block a user