diff --git a/web/src/views/AppShellRouteView.vue b/web/src/views/AppShellRouteView.vue index fd88d4a..2024f80 100644 --- a/web/src/views/AppShellRouteView.vue +++ b/web/src/views/AppShellRouteView.vue @@ -16,25 +16,28 @@ 'overview-main': activeView === 'overview', 'workbench-main': activeView === 'workbench', 'requests-main': activeView === 'requests', - 'approval-main': activeView === 'approval', + 'approval-main': activeView === 'approval', 'policies-main': activeView === 'policies', 'audit-main': activeView === 'audit', 'audit-detail-main': activeView === 'audit' && auditDetailOpen, + 'logs-main': activeView === 'logs', 'employees-main': activeView === 'employees', 'settings-main': activeView === 'settings' }" > - - + + - - + + + + @@ -133,10 +139,12 @@ import TravelReimbursementCreateView from './TravelReimbursementCreateView.vue' import TravelRequestDetailView from './TravelRequestDetailView.vue' import RequestsView from './RequestsView.vue' import ApprovalCenterView from './ApprovalCenterView.vue' -import PoliciesView from './PoliciesView.vue' -import AuditView from './AuditView.vue' -import EmployeeManagementView from './EmployeeManagementView.vue' -import SettingsView from './SettingsView.vue' +import PoliciesView from './PoliciesView.vue' +import AuditView from './AuditView.vue' +import LogsView from './LogsView.vue' +import LogDetailView from './LogDetailView.vue' +import EmployeeManagementView from './EmployeeManagementView.vue' +import SettingsView from './SettingsView.vue' import { useAppShell } from '../composables/useAppShell.js' import { useSystemState } from '../composables/useSystemState.js' @@ -144,6 +152,7 @@ import { filterNavItemsByAccess } from '../utils/accessControl.js' const employeeSummary = ref(null) const knowledgeSummary = ref(null) +const logsSummary = ref(null) const auditDetailOpen = ref(false) const { @@ -154,6 +163,7 @@ const { customRange, detailAlerts, detailMode, + logDetailMode, filteredRequests, filters, handleApprove, diff --git a/web/src/views/PoliciesView.vue b/web/src/views/PoliciesView.vue index 6596d51..58cfd04 100644 --- a/web/src/views/PoliciesView.vue +++ b/web/src/views/PoliciesView.vue @@ -97,7 +97,7 @@ v-if="isAdmin" class="more-btn ingest" type="button" - :disabled="Boolean(ingestingId) || deletingId === doc.id" + :disabled="Boolean(ingestingId) || deletingId === doc.id || Number(doc.stateCode || 0) === 2" :aria-label="resolveIngestActionTitle(doc)" :title="resolveIngestActionTitle(doc)" @click="handleManualIngest(doc)" @@ -356,6 +356,21 @@ {{ llmWikiError }} + + {{ resolveLlmWikiQualityLabel(llmWikiDocument) }} + {{ llmWikiDocument.quality_note || '当前展示内容不是正式 Hermes 归纳,请人工复核后再使用。' }} + + + {{ resolveLlmWikiQualityLabel(llmWikiDocument) }} + {{ llmWikiDocument.quality_note }} + 知识总结 @@ -363,6 +378,12 @@ {{ llmWikiDocument.knowledge_candidate_count }} 条知识 + + 正文分块 {{ llmWikiDocument.candidate_chunk_count }} + 过滤分块 {{ llmWikiDocument.filtered_chunk_count }} + 成功分组 {{ llmWikiDocument.successful_group_count }}/{{ llmWikiDocument.group_count }} + 正式知识 {{ llmWikiDocument.formal_knowledge_candidate_count }} + doc.id === selectedDocument.value.id) + const nextDocument = documents.value.find((doc) => doc.id === selectedDocument.value.id) + const exists = Boolean(nextDocument) if (!exists) { closePreview() + } else { + selectedDocument.value = { + ...selectedDocument.value, + ...nextDocument + } } } if (options.preserveSelection && llmWikiDocument.value?.document_id) { @@ -292,11 +301,12 @@ export default { closeLlmWikiSummary() } } + syncLibraryPolling() } catch (error) { - emit('summary-change', { totalDocuments: 0 }) - toast(error.message || '知识库加载失败。') - } finally { - loading.value = false + emit('summary-change', { totalDocuments: 0 }) + toast(error.message || '知识库加载失败。') + } finally { + loading.value = false } } @@ -351,10 +361,37 @@ export default { ...patch } } + syncLibraryPolling() + } + + function hasSyncingDocuments() { + return documents.value.some((doc) => Number(doc?.stateCode || 0) === 2) + } + + function stopLibraryPolling() { + if (libraryPollTimer) { + window.clearInterval(libraryPollTimer) + libraryPollTimer = 0 + } + } + + function startLibraryPolling() { + stopLibraryPolling() + libraryPollTimer = window.setInterval(() => { + loadLibrary({ preserveSelection: true }) + }, KNOWLEDGE_POLL_INTERVAL_MS) + } + + function syncLibraryPolling() { + if (hasSyncingDocuments()) { + startLibraryPolling() + return + } + stopLibraryPolling() } function resolveIngestActionLabel(document) { - if (ingestingId.value === document.id) { + if (ingestingId.value === document.id || Number(document?.stateCode || 0) === 2) { return '归纳中' } return Number(document?.stateCode || 0) === 3 ? '重新归纳' : '归纳' @@ -372,27 +409,64 @@ export default { } function canViewLlmWiki(document) { - return isAdmin.value && Number(document?.stateCode || 0) === 3 + return isAdmin.value && Boolean(document?.llmWikiAvailable) } function resolveViewLlmWikiTitle(document) { if (!isAdmin.value) { return '仅管理员可查看 LLM Wiki 归纳内容' } + if (document?.llmWikiAvailable && Number(document?.stateCode || 0) === 4) { + return '查看本次降级归纳结果,仅供人工排查,不能视为正式知识' + } + if (document?.llmWikiAvailable && document?.llmWikiQualityStatus === 'partial_degraded') { + return '查看当前归纳结果,存在部分降级分组,请人工复核' + } + if (document?.llmWikiAvailable) { + return '查看并编辑当前文档的 LLM Wiki 归纳内容' + } if (Number(document?.stateCode || 0) === 2) { return 'Hermes 正在归纳当前文档,完成后可查看 LLM Wiki 知识总结' } if (Number(document?.stateCode || 0) === 4) { - return '当前文档上次归纳失败,请重新归纳后再查看' + return '当前文档上次归纳失败,且没有可查看的 LLM Wiki 产物' } if (Number(document?.stateCode || 0) !== 3) { return '文档尚未完成归纳,暂无可查看的 LLM Wiki 知识总结' } - return '查看并编辑当前文档的 LLM Wiki 归纳内容' + return '查看当前文档的 LLM Wiki 归纳内容' + } + + function resolveLlmWikiQualityLabel(document) { + const qualityStatus = String(document?.quality_status || '').trim() + if (qualityStatus === 'partial_degraded') { + return '部分降级' + } + if (qualityStatus === 'fallback_only') { + return '降级兜底' + } + if (qualityStatus === 'runtime_only') { + return '非 Hermes 结果' + } + if (qualityStatus === 'failed') { + return '归纳失败' + } + return '正式归纳' + } + + function resolveLlmWikiQualityTone(document) { + const qualityStatus = String(document?.quality_status || '').trim() + if (qualityStatus === 'formal') { + return 'success' + } + if (qualityStatus === 'partial_degraded') { + return 'warning' + } + return 'danger' } async function handleManualIngest(document) { - if (!isAdmin.value || ingestingId.value || !document?.id) { + if (!isAdmin.value || ingestingId.value || !document?.id || Number(document?.stateCode || 0) === 2) { return } @@ -406,19 +480,13 @@ export default { try { const payload = await syncKnowledgeDocumentToLlmWiki({ folder: document.folder, - documentId: document.id + documentId: document.id, + force: true }) await loadLibrary({ preserveSelection: true }) - if (selectedDocument.value?.id === document.id) { - await selectDocument(document.id) - } - toast(payload.summary || 'Hermes 已完成文档归纳。') + toast(payload.summary || 'Hermes 已进入后台归纳。') } catch (error) { - patchDocumentState(document.id, { - stateCode: 4, - state: '归纳失败', - stateTone: 'danger' - }) + await loadLibrary({ preserveSelection: true }) toast(error.message || 'Hermes 归纳文档失败。') } finally { ingestingId.value = '' @@ -654,6 +722,7 @@ export default { revokePreviewBlob() destroyOnlyOfficeEditor() setBodyScrollLocked(false) + stopLibraryPolling() window.removeEventListener('keydown', handleWindowKeydown) }) @@ -711,6 +780,8 @@ export default { selectedDocument, resolveIngestActionLabel, resolveIngestActionTitle, + resolveLlmWikiQualityLabel, + resolveLlmWikiQualityTone, resolveViewLlmWikiTitle, saveLlmWikiSummary, totalCount,
{{ llmWikiDocument.quality_note || '当前展示内容不是正式 Hermes 归纳,请人工复核后再使用。' }}
{{ llmWikiDocument.quality_note }}