feat(web): 设置中心缓存管理与文件预览资产工具
- 新增 documentPreviewAssets 工具,统一从 URL/Blob/File 推断预览类型(image/pdf/file/unsupported) - SettingsView/SettingsView.js/settingsModelHelper 新增系统缓存管理区块,调用 /settings/cache/clear 并展示清理结果;useSettings/services 适配 - WorkbenchAiFilePreviewDialog/useWorkbenchAiFilePreview 接入预览资产工具,workbenchAiComposerModel 调整文件处理 - ReceiptFolder/LogDetailView/DigitalEmployeeWorkRecords/travelReimbursementAttachmentModel 配套适配 - 新增 settings-cache-management-section 测试,更新 settings-llm/rendering/receipt-folder-view/composer-components/attachment-association 测试
This commit is contained in:
@@ -3,7 +3,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useSystemState } from './useSystemState.js'
|
||||
import { useThemeSkin } from './useThemeSkin.js'
|
||||
import { fetchSettings, saveSettings } from '../services/settings.js'
|
||||
import { clearSystemCaches, fetchSettings, saveSettings } from '../services/settings.js'
|
||||
import { useToast } from './useToast.js'
|
||||
import {
|
||||
isHermesEmployeeSettingsReady
|
||||
@@ -56,6 +56,10 @@ export function useSettings() {
|
||||
const sessionRetentionPickerOpen = ref(false)
|
||||
const sessionRetentionPickerRef = ref(null)
|
||||
const logoInputRef = ref(null)
|
||||
const cacheClearing = ref(false)
|
||||
const cacheClearItems = ref([])
|
||||
const cacheClearMessage = ref('')
|
||||
const cacheClearFailed = ref(false)
|
||||
|
||||
const sections = SECTION_DEFINITIONS
|
||||
const logLevels = LOG_LEVELS
|
||||
@@ -433,6 +437,46 @@ export function useSettings() {
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeCacheClearErrorMessage(error) {
|
||||
const message = String(error?.message || '').trim()
|
||||
|
||||
if (!message || /^not found$/i.test(message)) {
|
||||
return '缓存清理接口暂不可用,请确认后端服务已加载最新路由后重试。'
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
async function clearAllCaches() {
|
||||
if (cacheClearing.value) {
|
||||
return
|
||||
}
|
||||
|
||||
cacheClearing.value = true
|
||||
cacheClearMessage.value = ''
|
||||
cacheClearItems.value = []
|
||||
cacheClearFailed.value = false
|
||||
|
||||
try {
|
||||
const payload = await clearSystemCaches()
|
||||
const items = Array.isArray(payload?.items) ? payload.items : []
|
||||
const totalCleared = Number(payload?.totalCleared || 0)
|
||||
cacheClearItems.value = items
|
||||
cacheClearMessage.value = totalCleared > 0
|
||||
? `已清理 ${totalCleared} 条缓存。`
|
||||
: '当前没有可清理的缓存。'
|
||||
cacheClearFailed.value = false
|
||||
toast(cacheClearMessage.value)
|
||||
} catch (error) {
|
||||
const message = normalizeCacheClearErrorMessage(error)
|
||||
cacheClearFailed.value = true
|
||||
cacheClearMessage.value = message
|
||||
toast(message)
|
||||
} finally {
|
||||
cacheClearing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMailSection() {
|
||||
const mailForm = pageState.value.mailForm
|
||||
|
||||
@@ -494,6 +538,10 @@ export function useSettings() {
|
||||
return
|
||||
}
|
||||
|
||||
if (activeSection.value === 'cacheManagement') {
|
||||
return
|
||||
}
|
||||
|
||||
if (activeSection.value === 'rendering') {
|
||||
await saveRenderingSection()
|
||||
return
|
||||
@@ -557,6 +605,11 @@ export function useSettings() {
|
||||
activeThemeSkinId,
|
||||
archiveCycleOptions,
|
||||
activateSection,
|
||||
cacheClearFailed,
|
||||
cacheClearItems,
|
||||
cacheClearMessage,
|
||||
cacheClearing,
|
||||
clearAllCaches,
|
||||
clearRenderSecretMask,
|
||||
completedSectionCount,
|
||||
logLevels,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { buildSelectedFileCards } from './workbenchAiComposerModel.js'
|
||||
import { fetchReceiptFolderAsset } from '../../services/receiptFolder.js'
|
||||
import {
|
||||
inferPreviewKindFromBlob,
|
||||
inferPreviewKindFromFile,
|
||||
isInlinePreviewUrl,
|
||||
isTemporaryPreviewUrl,
|
||||
resolveDocumentPreviewAsset
|
||||
} from '../../utils/documentPreviewAssets.js'
|
||||
|
||||
function normalizePreviewText(value) {
|
||||
return String(value ?? '').replace(/\s+/g, ' ').trim()
|
||||
@@ -40,10 +48,6 @@ function normalizePreviewField(field = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDocumentPreviewUrl(document = null) {
|
||||
return normalizePreviewText(document?.preview_data_url || document?.previewDataUrl)
|
||||
}
|
||||
|
||||
function resolveSourceKind(sourceUrl, rawFile = {}) {
|
||||
const type = normalizePreviewText(rawFile?.type).toLowerCase()
|
||||
const name = normalizePreviewText(rawFile?.name).toLowerCase()
|
||||
@@ -72,8 +76,18 @@ export function useWorkbenchAiFilePreview({
|
||||
scrollInlineConversationToBottom,
|
||||
selectedFiles
|
||||
}) {
|
||||
const filePreviewState = ref({ open: false, key: '', objectUrl: '' })
|
||||
const selectedFileCards = computed(() => buildSelectedFileCards(selectedFiles.value).map((card, index) => ({
|
||||
const filePreviewState = ref({
|
||||
open: false,
|
||||
key: '',
|
||||
objectUrl: '',
|
||||
objectKind: '',
|
||||
objectSource: '',
|
||||
loading: false
|
||||
})
|
||||
const selectedFileCards = computed(() => buildSelectedFileCards(
|
||||
selectedFiles.value,
|
||||
(file) => attachmentFlow.resolveAiModeReceiptRecognitionState(file)
|
||||
).map((card, index) => ({
|
||||
...card,
|
||||
ocrState: attachmentFlow.resolveAiModeReceiptRecognitionState(selectedFiles.value[index])
|
||||
})))
|
||||
@@ -97,22 +111,94 @@ export function useWorkbenchAiFilePreview({
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTargetDocument(target) {
|
||||
if (!target?.rawFile) {
|
||||
return null
|
||||
}
|
||||
const recognitionState = attachmentFlow.resolveAiModeReceiptRecognitionState(target.rawFile) || target.card.ocrState || null
|
||||
return recognitionState?.document || null
|
||||
}
|
||||
|
||||
function resolveRemotePreviewAsset(target) {
|
||||
const asset = resolveDocumentPreviewAsset(resolveTargetDocument(target))
|
||||
if (!asset?.url || isInlinePreviewUrl(asset.url) || isTemporaryPreviewUrl(asset.url)) {
|
||||
return null
|
||||
}
|
||||
return asset
|
||||
}
|
||||
|
||||
async function loadRemotePreviewAsset(fileKey) {
|
||||
const target = findSelectedFile(fileKey)
|
||||
const asset = resolveRemotePreviewAsset(target)
|
||||
if (!target?.rawFile || !asset?.url) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await fetchReceiptFolderAsset(asset.url)
|
||||
const objectUrl = createObjectUrl(blob)
|
||||
const objectKind = inferPreviewKindFromBlob(blob) || asset.kind
|
||||
if (!objectUrl || !['image', 'pdf'].includes(objectKind)) {
|
||||
return
|
||||
}
|
||||
if (!filePreviewState.value.open || filePreviewState.value.key !== fileKey) {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
return
|
||||
}
|
||||
clearFilePreviewObjectUrl()
|
||||
filePreviewState.value = {
|
||||
...filePreviewState.value,
|
||||
objectUrl,
|
||||
objectKind,
|
||||
objectSource: asset.source,
|
||||
loading: false
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('AI mode remote attachment preview unavailable:', error)
|
||||
if (!filePreviewState.value.open || filePreviewState.value.key !== fileKey) {
|
||||
return
|
||||
}
|
||||
clearFilePreviewObjectUrl()
|
||||
filePreviewState.value = {
|
||||
...filePreviewState.value,
|
||||
objectUrl: createObjectUrl(target.rawFile),
|
||||
objectKind: inferPreviewKindFromFile(target.rawFile),
|
||||
objectSource: 'file',
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openAiModeFilePreview(fileKey) {
|
||||
const target = findSelectedFile(fileKey)
|
||||
if (!target?.rawFile) {
|
||||
return
|
||||
}
|
||||
clearFilePreviewObjectUrl()
|
||||
const remoteAsset = resolveRemotePreviewAsset(target)
|
||||
filePreviewState.value = {
|
||||
open: true,
|
||||
key: fileKey,
|
||||
objectUrl: createObjectUrl(target.rawFile)
|
||||
objectUrl: remoteAsset ? '' : createObjectUrl(target.rawFile),
|
||||
objectKind: remoteAsset ? '' : inferPreviewKindFromFile(target.rawFile),
|
||||
objectSource: remoteAsset ? remoteAsset.source : 'file',
|
||||
loading: Boolean(remoteAsset)
|
||||
}
|
||||
if (remoteAsset) {
|
||||
void loadRemotePreviewAsset(fileKey)
|
||||
}
|
||||
}
|
||||
|
||||
function closeAiModeFilePreview() {
|
||||
clearFilePreviewObjectUrl()
|
||||
filePreviewState.value = { open: false, key: '', objectUrl: '' }
|
||||
filePreviewState.value = {
|
||||
open: false,
|
||||
key: '',
|
||||
objectUrl: '',
|
||||
objectKind: '',
|
||||
objectSource: '',
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
|
||||
const activeAiModeFilePreview = computed(() => {
|
||||
@@ -128,9 +214,16 @@ export function useWorkbenchAiFilePreview({
|
||||
const document = recognitionState?.document || null
|
||||
const documentFields = Array.isArray(document?.document_fields) ? document.document_fields : document?.fields || []
|
||||
const ocrFields = documentFields.map((field) => normalizePreviewField(field)).filter(Boolean)
|
||||
const documentPreviewUrl = resolveDocumentPreviewUrl(document)
|
||||
const sourceUrl = documentPreviewUrl || filePreviewState.value.objectUrl
|
||||
const sourceKind = documentPreviewUrl ? 'image' : resolveSourceKind(sourceUrl, rawFile)
|
||||
const documentPreviewAsset = resolveDocumentPreviewAsset(document)
|
||||
const inlinePreviewAvailable = documentPreviewAsset?.url && isInlinePreviewUrl(documentPreviewAsset.url)
|
||||
const sourceUrl = inlinePreviewAvailable
|
||||
? documentPreviewAsset.url
|
||||
: filePreviewState.value.objectUrl
|
||||
const sourceKind = inlinePreviewAvailable
|
||||
? documentPreviewAsset.kind
|
||||
: filePreviewState.value.loading
|
||||
? 'loading'
|
||||
: filePreviewState.value.objectKind || resolveSourceKind(sourceUrl, rawFile)
|
||||
const documentTypeLabel = normalizePreviewText(
|
||||
document?.document_type_label ||
|
||||
document?.scene_label ||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { buildFileIdentity } from '../../views/scripts/travelReimbursementAttachmentModel.js'
|
||||
import { resolveDocumentPreviewAsset } from '../../utils/documentPreviewAssets.js'
|
||||
|
||||
export const AI_COMPOSER_FILE_TYPE_META = {
|
||||
pdf: { label: 'PDF', icon: 'mdi mdi-file-pdf-box', tone: 'pdf' },
|
||||
@@ -44,7 +45,14 @@ export function resolveAiComposerFileName(file) {
|
||||
return String(file?.name || '未命名附件').trim() || '未命名附件'
|
||||
}
|
||||
|
||||
export function resolveAiComposerFileType(file) {
|
||||
export function resolveAiComposerFileType(file, previewAsset = null) {
|
||||
if (previewAsset?.kind === 'image') {
|
||||
return AI_COMPOSER_FILE_TYPE_META.image
|
||||
}
|
||||
if (previewAsset?.kind === 'pdf') {
|
||||
return AI_COMPOSER_FILE_TYPE_META.pdf
|
||||
}
|
||||
|
||||
const fileName = resolveAiComposerFileName(file).toLowerCase()
|
||||
const mimeType = String(file?.type || '').toLowerCase()
|
||||
const extension = fileName.includes('.') ? fileName.split('.').pop() : ''
|
||||
@@ -66,12 +74,19 @@ export function resolveAiComposerFileType(file) {
|
||||
return AI_COMPOSER_FILE_TYPE_META.file
|
||||
}
|
||||
|
||||
export function buildSelectedFileCards(files = []) {
|
||||
return files.map((file) => ({
|
||||
key: buildFileIdentity(file),
|
||||
name: resolveAiComposerFileName(file),
|
||||
...resolveAiComposerFileType(file)
|
||||
}))
|
||||
export function buildSelectedFileCards(files = [], resolveRecognitionState = null) {
|
||||
return files.map((file, index) => {
|
||||
const recognitionState = typeof resolveRecognitionState === 'function'
|
||||
? resolveRecognitionState(file, index)
|
||||
: null
|
||||
const previewAsset = resolveDocumentPreviewAsset(recognitionState?.document || null)
|
||||
return {
|
||||
key: buildFileIdentity(file),
|
||||
name: resolveAiComposerFileName(file),
|
||||
...resolveAiComposerFileType(file, previewAsset),
|
||||
previewAsset: previewAsset || null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function isLikelyAiModeOcrFile(file = {}) {
|
||||
|
||||
Reference in New Issue
Block a user