feat: 重构知识库系统,移除Hermes集成,增强RAG和同步功能

主要变更:
- 移除Hermes智能体及相关回调服务
- 新增知识库RAG、同步、调度、规范化和索引任务服务
- 重构orchestrator服务,增强运行时聊天功能
- 更新前端聊天、政策制度、设置等页面样式和逻辑
- 更新expense_claims和document_intelligence服务
- 删除llm_wiki相关服务和测试文件
- 更新docker-compose配置和启动脚本
This commit is contained in:
caoxiaozhu
2026-05-17 08:38:41 +00:00
parent 212c935308
commit 68f663f2f4
308 changed files with 83729 additions and 13588 deletions

View File

@@ -1,15 +1,15 @@
<template>
<section class="knowledge-page">
<div class="knowledge-grid" :class="{ 'has-preview': previewLayoutState.usesSplitLayout }">
<section class="knowledge-main">
<article class="library-panel panel">
<header class="panel-title">
<div>
<h2>文档库 / 文件夹</h2>
<p>默认展示文件列表点击具体文件后以弹窗方式展开预览</p>
</div>
<label class="file-search">
<i class="mdi mdi-magnify"></i>
<section class="knowledge-page">
<div class="knowledge-grid" :class="{ 'has-preview': previewLayoutState.usesSplitLayout }">
<section class="knowledge-main">
<article class="library-panel panel">
<header class="panel-title">
<div>
<h2>文档库 / 文件夹</h2>
<p>默认展示文件列表点击具体文件后以弹窗方式展开预览</p>
</div>
<label class="file-search">
<i class="mdi mdi-magnify"></i>
<input v-model="documentSearch" type="search" placeholder="搜索当前文件夹内文件" />
</label>
</header>
@@ -30,35 +30,43 @@
</button>
</nav>
<button class="new-folder-btn fixed" type="button" disabled>
<i class="mdi mdi-lock-outline"></i>
<span>固定文件夹</span>
</button>
<div class="folder-sync-block">
<button
class="new-folder-btn fixed knowledge-sync-btn"
type="button"
:disabled="!canTriggerKnowledgeSync"
@click="handleKnowledgeSync"
>
<i :class="syncingFolder ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-book-sync-outline'"></i>
<span>{{ knowledgeSyncButtonLabel }}</span>
</button>
<p class="folder-sync-meta">{{ knowledgeSyncHint }}</p>
</div>
</aside>
<section class="document-area" :class="{ 'read-only': !isAdmin }">
<div
v-if="isAdmin"
class="upload-zone"
:class="{ busy: uploading }"
@click="triggerUpload"
@dragover.prevent
@drop.prevent="handleDrop"
>
<input
ref="uploadInput"
class="upload-input"
<section class="document-area" :class="{ 'read-only': !isAdmin }">
<div
v-if="isAdmin"
class="upload-zone"
:class="{ busy: uploading }"
@click="triggerUpload"
@dragover.prevent
@drop.prevent="handleDrop"
>
<input
ref="uploadInput"
class="upload-input"
type="file"
multiple
@change="handleFileInput"
/>
<i class="mdi mdi-cloud-upload"></i>
<strong>{{ uploading ? '正在上传文件...' : '拖拽文档到此处,或点击上传' }}</strong>
<span>{{ uploadHint }}</span>
</div>
<div class="doc-table-wrap">
<table class="knowledge-document-table">
/>
<i class="mdi mdi-cloud-upload"></i>
<strong>{{ uploading ? '正在上传文件...' : '拖拽文档到此处,或点击上传' }}</strong>
<span>{{ uploadHint }}</span>
</div>
<div class="doc-table-wrap">
<table class="knowledge-document-table">
<thead>
<tr>
<th>文件名称</th>
@@ -89,53 +97,35 @@
</td>
<td>{{ doc.time }}</td>
<td>{{ doc.version }}</td>
<td><span class="state-tag" :class="doc.stateTone">{{ doc.state }}</span></td>
<td>
<div class="state-cell">
<span class="state-tag" :class="doc.stateTone">{{ doc.state }}</span>
<span v-if="doc.ingestTime" class="state-time">归纳时间{{ doc.ingestTime }}</span>
</div>
</td>
<td>{{ doc.owner }}</td>
<td>
<div class="row-actions" @click.stop>
<button
v-if="isAdmin"
class="more-btn ingest"
type="button"
:disabled="Boolean(ingestingId) || deletingId === doc.id || Number(doc.stateCode || 0) === 2"
:aria-label="resolveIngestActionTitle(doc)"
:title="resolveIngestActionTitle(doc)"
@click="handleManualIngest(doc)"
>
<i class="mdi mdi-book-sync-outline"></i>
</button>
<button
v-if="isAdmin"
class="more-btn llm-wiki-view"
:class="{ 'is-disabled': !canViewLlmWiki(doc) }"
type="button"
:aria-label="resolveViewLlmWikiTitle(doc)"
:aria-disabled="!canViewLlmWiki(doc)"
:title="resolveViewLlmWikiTitle(doc)"
@click="openLlmWikiSummary(doc)"
>
<i class="mdi mdi-eye-outline"></i>
</button>
<button
class="more-btn"
type="button"
aria-label="下载文件"
title="下载当前文件"
@click="handleDownload(doc)"
>
<i class="mdi mdi-download"></i>
</button>
<button
v-if="isAdmin"
class="more-btn danger"
type="button"
:disabled="deletingId === doc.id || ingestingId === doc.id"
aria-label="删除文件"
title="删除当前文件"
@click="handleDelete(doc)"
>
<i class="mdi mdi-delete-outline"></i>
</button>
<td>
<div class="row-actions" @click.stop>
<button
class="more-btn"
type="button"
aria-label="下载文件"
title="归纳时间:?"
@click="handleDownload(doc)"
>
<i class="mdi mdi-download"></i>
</button>
<button
v-if="isAdmin"
class="more-btn danger"
type="button"
:disabled="deletingId === doc.id || Number(doc.stateCode || 0) === 2"
aria-label="删除文件"
title="归纳时间:?"
@click="handleDelete(doc)"
>
<i class="mdi mdi-delete-outline"></i>
</button>
</div>
</td>
</tr>
@@ -190,29 +180,29 @@
</footer>
</section>
</div>
</article>
</section>
<Teleport to="body">
<Transition name="preview-modal">
<div
v-if="previewLayoutState.isPreviewModalOpen"
class="preview-modal-overlay"
role="presentation"
@click.self="closePreview"
>
<aside class="preview-modal-shell" role="dialog" aria-modal="true" aria-labelledby="knowledge-preview-title">
<article
ref="previewDialogPanel"
class="preview-panel preview-modal-panel panel"
tabindex="-1"
@click.stop
>
<header class="preview-head">
<div class="preview-copy">
<h2 id="knowledge-preview-title">{{ selectedDocument.name }}</h2>
<p class="preview-summary-line">
<span v-for="part in previewMetaLine" :key="part">{{ part }}</span>
</p>
</article>
</section>
<Teleport to="body">
<Transition name="preview-modal">
<div
v-if="previewLayoutState.isPreviewModalOpen"
class="preview-modal-overlay"
role="presentation"
@click.self="closePreview"
>
<aside class="preview-modal-shell" role="dialog" aria-modal="true" aria-labelledby="knowledge-preview-title">
<article
ref="previewDialogPanel"
class="preview-panel preview-modal-panel panel"
tabindex="-1"
@click.stop
>
<header class="preview-head">
<div class="preview-copy">
<h2 id="knowledge-preview-title">{{ selectedDocument.name }}</h2>
<p class="preview-summary-line">
<span v-for="part in previewMetaLine" :key="part">{{ part }}</span>
</p>
<div v-if="previewSecondaryMetaLine.length" class="preview-secondary-line">
<span v-for="part in previewSecondaryMetaLine" :key="part">{{ part }}</span>
</div>
@@ -227,34 +217,34 @@
<i class="mdi mdi-close"></i>
</button>
</div>
</header>
<div class="preview-viewer">
<div v-if="shouldRenderOnlyOffice" class="onlyoffice-preview-wrap">
<div
v-if="shouldRenderOnlyOfficeHostNode"
:id="onlyOfficeHostId"
class="onlyoffice-preview-host"
></div>
<div v-if="onlyOfficeLoading" class="preview-status preview-status-overlay">
正在加载 ONLYOFFICE 预览...
</div>
<div v-else-if="onlyOfficeError" class="preview-status error preview-status-overlay">
{{ onlyOfficeError }}
</div>
</div>
<div v-else-if="previewLoading" class="preview-status">正在加载预览...</div>
<div v-else-if="previewError" class="preview-status error">{{ previewError }}</div>
<div v-else-if="previewMode === 'pdf' && previewBlobUrl" class="preview-embed-wrap">
<iframe :src="previewBlobUrl" class="preview-embed" title="PDF 预览"></iframe>
</div>
<div v-else-if="previewMode === 'image' && previewBlobUrl" class="preview-image-wrap">
<img :src="previewBlobUrl" :alt="selectedDocument.name" class="preview-image" />
</div>
<div v-else-if="previewMode === 'table'" class="excel-preview-wrap">
<div v-if="selectedDocument.previewPages.length > 1" class="excel-sheet-tabs" role="tablist" aria-label="Excel 工作表页签">
<button
v-for="(page, index) in selectedDocument.previewPages"
</header>
<div class="preview-viewer">
<div v-if="shouldRenderOnlyOffice" class="onlyoffice-preview-wrap">
<div
v-if="shouldRenderOnlyOfficeHostNode"
:id="onlyOfficeHostId"
class="onlyoffice-preview-host"
></div>
<div v-if="onlyOfficeLoading" class="preview-status preview-status-overlay">
正在加载 ONLYOFFICE 预览...
</div>
<div v-else-if="onlyOfficeError" class="preview-status error preview-status-overlay">
{{ onlyOfficeError }}
</div>
</div>
<div v-else-if="previewLoading" class="preview-status">正在加载预览...</div>
<div v-else-if="previewError" class="preview-status error">{{ previewError }}</div>
<div v-else-if="previewMode === 'pdf' && previewBlobUrl" class="preview-embed-wrap">
<iframe :src="previewBlobUrl" class="preview-embed" title="PDF 预览"></iframe>
</div>
<div v-else-if="previewMode === 'image' && previewBlobUrl" class="preview-image-wrap">
<img :src="previewBlobUrl" :alt="selectedDocument.name" class="preview-image" />
</div>
<div v-else-if="previewMode === 'table'" class="excel-preview-wrap">
<div v-if="selectedDocument.previewPages.length > 1" class="excel-sheet-tabs" role="tablist" aria-label="Excel 工作表页签">
<button
v-for="(page, index) in selectedDocument.previewPages"
:key="`${selectedDocument.id}-sheet-${index}`"
type="button"
class="excel-sheet-tab"
@@ -299,163 +289,33 @@
</article>
<div v-if="!selectedDocument.previewPages.length" class="preview-status">
当前文件暂未生成结构化预览请下载后查看
</div>
</div>
</div>
</article>
</aside>
</div>
</Transition>
</Teleport>
<Teleport to="body">
<Transition name="preview-modal">
<div
v-if="llmWikiDialogOpen"
class="preview-modal-overlay llm-wiki-overlay"
role="presentation"
@click.self="closeLlmWikiSummary"
>
<aside class="preview-modal-shell llm-wiki-shell" role="dialog" aria-modal="true" aria-labelledby="llm-wiki-title">
<article
ref="llmWikiDialogPanel"
class="preview-panel preview-modal-panel llm-wiki-panel panel"
tabindex="-1"
@click.stop
>
<header class="preview-head llm-wiki-head">
<div class="preview-copy">
<h2 id="llm-wiki-title">LLM Wiki 归纳内容</h2>
<p>{{ llmWikiDocument ? llmWikiDocument.document_name : '正在读取当前文档的 LLM Wiki 知识总结。' }}</p>
<div v-if="llmWikiDocument" class="llm-wiki-meta">
<span>版本 {{ llmWikiDocument.document_version }}</span>
<span>{{ llmWikiDocument.chunk_count }} 个分块</span>
<span>{{ llmWikiDocument.knowledge_candidate_count }} 条知识</span>
<span>{{ llmWikiDocument.rule_candidate_count }} 条规则草稿</span>
</div>
</div>
<div class="preview-actions">
<button
v-if="isAdmin"
type="button"
class="mini-action"
:disabled="llmWikiLoading || llmWikiSaving || !llmWikiDocument"
@click="saveLlmWikiSummary"
>
<i class="mdi mdi-content-save-outline"></i>
<span>{{ llmWikiSaving ? '保存中...' : '保存总结' }}</span>
</button>
<button type="button" class="icon-action" aria-label="关闭归纳内容" @click="closeLlmWikiSummary">
<i class="mdi mdi-close"></i>
</button>
</div>
</header>
<div class="llm-wiki-body">
<div v-if="llmWikiLoading" class="preview-status">正在加载 LLM Wiki 归纳内容...</div>
<div v-else-if="llmWikiError" class="preview-status error">{{ llmWikiError }}</div>
<div v-else-if="llmWikiDocument" class="llm-wiki-grid">
<section class="llm-wiki-section llm-wiki-summary-section">
<div
v-if="llmWikiDocument.quality_status !== 'formal'"
class="llm-wiki-alert"
:class="resolveLlmWikiQualityTone(llmWikiDocument)"
>
<strong>{{ resolveLlmWikiQualityLabel(llmWikiDocument) }}</strong>
<p>{{ llmWikiDocument.quality_note || '当前展示内容不是正式 Hermes 归纳,请人工复核后再使用。' }}</p>
</div>
<div
v-else-if="llmWikiDocument.quality_note"
class="llm-wiki-alert info"
>
<strong>{{ resolveLlmWikiQualityLabel(llmWikiDocument) }}</strong>
<p>{{ llmWikiDocument.quality_note }}</p>
</div>
<div class="llm-wiki-section-head">
<div>
<h3>知识总结</h3>
<p>管理员可在审核后修订这份归纳总结作为知识预览内容保留</p>
</div>
<span class="llm-wiki-count">{{ llmWikiDocument.knowledge_candidate_count }} 条知识</span>
</div>
<div class="llm-wiki-stat-grid">
<span>正文分块 {{ llmWikiDocument.candidate_chunk_count }}</span>
<span>过滤分块 {{ llmWikiDocument.filtered_chunk_count }}</span>
<span>成功分组 {{ llmWikiDocument.successful_group_count }}/{{ llmWikiDocument.group_count }}</span>
<span>正式知识 {{ llmWikiDocument.formal_knowledge_candidate_count }}</span>
</div>
<textarea
v-model="llmWikiSummaryDraft"
class="llm-wiki-editor"
spellcheck="false"
placeholder="Hermes 归纳后的知识总结会显示在这里。"
></textarea>
</section>
<section class="llm-wiki-section llm-wiki-candidates-section">
<div class="llm-wiki-section-head">
<div>
<h3>知识条目预览</h3>
<p>展示 Hermes 已提炼出的知识点方便与总结内容逐项比对</p>
</div>
</div>
<div v-if="llmWikiDocument.knowledge_candidates.length" class="llm-wiki-candidate-list">
<article
v-for="candidate in llmWikiDocument.knowledge_candidates"
:key="candidate.candidate_id"
class="llm-wiki-candidate-card"
>
<header>
<strong>{{ candidate.title }}</strong>
<span>{{ candidate.scenario }}</span>
</header>
<p>{{ candidate.content }}</p>
<div v-if="candidate.tags.length" class="llm-wiki-chip-list">
<span
v-for="tag in candidate.tags"
:key="`${candidate.candidate_id}-${tag}`"
>
{{ tag }}
</span>
</div>
<ul v-if="candidate.evidence.length" class="llm-wiki-evidence">
<li
v-for="(evidence, index) in candidate.evidence.slice(0, 3)"
:key="`${candidate.candidate_id}-evidence-${index}`"
>
{{ evidence }}
</li>
</ul>
</article>
</div>
<div v-else class="preview-status">当前文档暂无可展示的知识条目</div>
</section>
</div>
</div>
</article>
</aside>
</div>
</Transition>
</Teleport>
</div>
<ConfirmDialog
:open="deleteDialogOpen"
badge="删除文件"
badge-tone="danger"
:title="`确认删除文件“${deleteTargetDocument?.name || ''}”吗?`"
description="删除后该知识库文件及其当前版本记录将不可恢复,请确认本次操作。"
cancel-text="取消"
confirm-text="确认删除"
busy-text="删除中..."
confirm-tone="danger"
confirm-icon="mdi mdi-delete-outline"
:busy="Boolean(deletingId)"
@close="closeDeleteDialog"
@confirm="confirmDeleteDocument"
/>
</section>
</template>
</div>
</div>
</div>
</article>
</aside>
</div>
</Transition>
</Teleport>
</div>
<ConfirmDialog
:open="deleteDialogOpen"
badge="删除文件"
badge-tone="danger"
:title="`确认删除文件“${deleteTargetDocument?.name || ''}”吗?`"
description="删除后该知识库文件及其当前版本记录将不可恢复,请确认本次操作。"
cancel-text="取消"
confirm-text="确认删除"
busy-text="删除中..."
confirm-tone="danger"
confirm-icon="mdi mdi-delete-outline"
:busy="Boolean(deletingId)"
@close="closeDeleteDialog"
@confirm="confirmDeleteDocument"
/>
</section>
</template>
<script src="./scripts/PoliciesView.js"></script>