| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- <template>
- <div class="knowledge-workbench fill-height">
- <header class="workbench-header">
- <div>
- <div class="d-flex align-center mb-2">
- <v-icon color="primary" class="mr-2">mdi-book-search-outline</v-icon>
- <span class="text-overline primary--text">DATAOPS KNOWLEDGE</span>
- </div>
- <h1 class="text-h4 font-weight-bold mb-2">数据治理知识库</h1>
- <p class="text-body-1 grey--text text--darken-1 mb-0">
- 基于当前有效治理事实进行检索与问答,每条结论都能核验来源版本和知识点。
- </p>
- </div>
- <div class="status-cluster">
- <v-chip small outlined color="success">
- <v-icon left small>mdi-database-check-outline</v-icon>
- 标准检索可用
- </v-chip>
- <v-chip v-if="capabilities.lightrag_shadow_only" small outlined color="primary">
- LightRAG 影子模式
- </v-chip>
- </div>
- </header>
- <main class="workbench-grid">
- <section class="query-panel">
- <div class="mode-switch mb-5">
- <v-btn-toggle v-model="action" mandatory dense color="primary">
- <v-btn value="ask">问答</v-btn>
- <v-btn value="search">仅检索</v-btn>
- </v-btn-toggle>
- <v-select
- v-model="mode"
- :items="modes"
- item-text="label"
- item-value="value"
- label="检索模式"
- dense
- outlined
- hide-details
- class="mode-select"
- />
- </div>
- <label class="text-subtitle-2 d-block mb-2" for="knowledge-query">治理问题</label>
- <v-textarea
- id="knowledge-query"
- v-model="query"
- outlined
- auto-grow
- rows="3"
- :placeholder="action === 'ask' ? '例如:客户同步流程由谁负责?它依赖哪些上游数据?' : '输入对象名、字段名、编码或治理概念'"
- :error-messages="queryError"
- @keydown.ctrl.enter="submit"
- />
- <div class="d-flex align-center justify-space-between">
- <span class="text-caption grey--text">Ctrl + Enter 提交;业务域范围由服务端授权决定</span>
- <v-btn color="primary" depressed :loading="loading" :disabled="!query.trim()" @click="submit">
- <v-icon left>mdi-arrow-right</v-icon>
- {{ action === 'ask' ? '生成回答' : '开始检索' }}
- </v-btn>
- </div>
- <v-alert v-if="error" type="error" outlined dense class="mt-5 mb-0">
- {{ error }}
- </v-alert>
- <v-alert v-if="degradedComponents.length" type="warning" outlined dense class="mt-5 mb-0">
- 部分能力暂时降级:{{ degradedComponents.join('、') }}。当前结果仅使用可用检索器生成。
- </v-alert>
- <div v-if="loading" class="result-skeleton mt-8" aria-label="正在检索">
- <v-skeleton-loader type="heading, paragraph, list-item-three-line, list-item-three-line" />
- </div>
- <div v-else-if="hasResult" class="result-area mt-8">
- <div v-if="action === 'ask'" class="answer-block">
- <div class="d-flex align-center justify-space-between mb-3">
- <h2 class="text-h6 mb-0">回答</h2>
- <v-chip small :color="freshnessColor" outlined>{{ freshnessLabel }}</v-chip>
- </div>
- <p v-if="answer" class="answer-copy">{{ answer }}</p>
- <v-alert v-else type="info" outlined dense class="mb-0">
- 当前有效证据不足,知识库没有生成推测性答案。你仍可查看下方检索来源。
- </v-alert>
- </div>
- <div class="evidence-header">
- <h2 class="text-h6 mb-0">{{ action === 'ask' ? '引用来源' : '检索结果' }}</h2>
- <span class="text-caption grey--text">{{ evidence.length }} 条</span>
- </div>
- <div v-if="evidence.length" class="evidence-list">
- <button
- v-for="(item, index) in evidence"
- :key="`${item.chunk_id}-${index}`"
- type="button"
- class="evidence-row"
- @click="openSource(item)"
- >
- <span class="evidence-index">{{ String(index + 1).padStart(2, '0') }}</span>
- <span class="evidence-main">
- <span class="d-flex align-center flex-wrap mb-2">
- <strong class="mr-2">{{ item.object_type }}</strong>
- <v-chip x-small outlined class="mr-2">v{{ item.object_version }}</v-chip>
- <v-chip x-small outlined color="primary">point r{{ firstRevision(item) }}</v-chip>
- </span>
- <span class="evidence-content">{{ item.content }}</span>
- <span class="evidence-meta mt-2">
- {{ firstPoint(item) }} · generation {{ item.index_generation }}
- </span>
- </span>
- <v-icon color="primary">mdi-open-in-new</v-icon>
- </button>
- </div>
- <div v-else class="empty-result">
- <v-icon size="40" color="grey lighten-1">mdi-file-search-outline</v-icon>
- <div class="text-subtitle-1 mt-3">没有找到可访问的有效来源</div>
- <div class="text-body-2 grey--text mt-1">尝试使用对象名、字段名或更具体的治理术语。</div>
- </div>
- </div>
- </section>
- <aside class="context-panel">
- <div class="context-section">
- <h2 class="text-subtitle-1 font-weight-bold mb-3">可信检索边界</h2>
- <div class="boundary-row">
- <v-icon small color="success">mdi-shield-check-outline</v-icon>
- <span>服务端业务域授权</span>
- </div>
- <div class="boundary-row">
- <v-icon small color="success">mdi-history</v-icon>
- <span>仅使用 active 版本</span>
- </div>
- <div class="boundary-row">
- <v-icon small color="success">mdi-link-variant</v-icon>
- <span>知识点级引用</span>
- </div>
- </div>
- <div v-if="isAdmin" class="context-section admin-section">
- <div class="d-flex align-center justify-space-between mb-3">
- <h2 class="text-subtitle-1 font-weight-bold mb-0">索引运行状态</h2>
- <v-btn icon small :loading="adminLoading" @click="loadAdmin">
- <v-icon small>mdi-refresh</v-icon>
- </v-btn>
- </div>
- <div class="metric-row">
- <span>有效文档</span><strong>{{ adminStatus.active_documents || 0 }}</strong>
- </div>
- <div class="metric-row">
- <span>待处理变更</span><strong>{{ statusCount('pending') }}</strong>
- </div>
- <div class="metric-row">
- <span>失败变更</span><strong :class="{ 'error--text': statusCount('failed') }">{{ statusCount('failed') }}</strong>
- </div>
- <v-divider class="my-4" />
- <div v-if="changeSets.length" class="change-list">
- <div v-for="item in changeSets.slice(0, 5)" :key="item.id" class="change-row">
- <div>
- <div class="text-body-2 font-weight-medium">{{ item.source_type }} · v{{ item.source_revision }}</div>
- <div class="text-caption grey--text">+{{ item.added_count }} / ~{{ item.modified_count }} / -{{ item.deleted_count }}</div>
- </div>
- <v-chip x-small outlined :color="changeColor(item.status)">{{ item.status }}</v-chip>
- </div>
- </div>
- <div v-else class="text-caption grey--text">暂无变更记录</div>
- </div>
- </aside>
- </main>
- <v-dialog v-model="sourceDialog" max-width="760">
- <v-card>
- <v-card-title class="d-flex justify-space-between">
- <span>治理来源核验</span>
- <v-btn icon @click="sourceDialog = false"><v-icon>mdi-close</v-icon></v-btn>
- </v-card-title>
- <v-divider />
- <v-card-text class="pt-5">
- <v-skeleton-loader v-if="sourceLoading" type="heading, paragraph" />
- <template v-else-if="sourceDetail">
- <div class="source-facts mb-4">
- <div><span>对象</span><strong>{{ sourceDetail.object_type }}</strong></div>
- <div><span>名称</span><strong>{{ sourceDetail.object_name }}</strong></div>
- <div><span>版本</span><strong>v{{ sourceDetail.object_version }}</strong></div>
- <div><span>Generation</span><strong>{{ sourceDetail.active_generation }}</strong></div>
- </div>
- <pre class="source-content">{{ sourceDetail.content }}</pre>
- </template>
- </v-card-text>
- </v-card>
- </v-dialog>
- </div>
- </template>
- <script>
- import {
- askKnowledge,
- getKnowledgeCapabilities,
- getKnowledgeChangeSets,
- getKnowledgeSource,
- getKnowledgeSync,
- searchKnowledge
- } from '@/api/governanceKnowledge'
- export default {
- name: 'governance-knowledge-base',
- data () {
- return {
- action: 'ask',
- mode: 'auto',
- query: '',
- queryError: '',
- loading: false,
- error: '',
- answer: null,
- answerStatus: '',
- evidence: [],
- freshness: 'fresh',
- degradedComponents: [],
- capabilities: {},
- adminStatus: {},
- changeSets: [],
- adminLoading: false,
- sourceDialog: false,
- sourceLoading: false,
- sourceDetail: null,
- modes: [
- { label: '自动判断', value: 'auto' },
- { label: '精确', value: 'exact' },
- { label: '语义', value: 'semantic' },
- { label: '关系', value: 'relationship' },
- { label: '全局', value: 'global' }
- ]
- }
- },
- computed: {
- isAdmin () {
- const permissions = this.$store.getters.userInfo?.permissions || []
- return permissions.includes('knowledge:manage')
- },
- hasResult () {
- return Boolean(this.answerStatus || this.evidence.length)
- },
- freshnessLabel () {
- return { fresh: '知识已同步', updating: '更新处理中', degraded: '状态降级' }[this.freshness] || this.freshness
- },
- freshnessColor () {
- return { fresh: 'success', updating: 'warning', degraded: 'error' }[this.freshness] || 'grey'
- }
- },
- created () {
- getKnowledgeCapabilities().then(res => { this.capabilities = res.data || {} }).catch(() => {})
- if (this.isAdmin) this.loadAdmin()
- },
- methods: {
- async submit () {
- if (!this.query.trim()) {
- this.queryError = '请输入治理问题'
- return
- }
- this.queryError = ''
- this.error = ''
- this.loading = true
- this.answer = null
- this.answerStatus = ''
- this.evidence = []
- this.degradedComponents = []
- try {
- const request = { query: this.query.trim(), mode: this.mode }
- const response = this.action === 'ask' ? await askKnowledge(request) : await searchKnowledge(request)
- const data = response.data || {}
- this.degradedComponents = data.degraded_components || []
- if (this.action === 'ask') {
- this.answer = data.answer
- this.answerStatus = data.answer_status
- this.evidence = data.citations || []
- this.freshness = data.freshness_status || 'degraded'
- } else {
- this.answerStatus = 'retrieved'
- this.evidence = data.evidence || []
- this.freshness = this.evidence.some(item => item.freshness_status === 'updating') ? 'updating' : 'fresh'
- }
- } catch (error) {
- this.error = typeof error === 'string' ? error : '知识检索暂时不可用,请稍后重试。'
- } finally {
- this.loading = false
- }
- },
- async loadAdmin () {
- this.adminLoading = true
- try {
- const [status, changes] = await Promise.all([getKnowledgeSync(), getKnowledgeChangeSets(20)])
- this.adminStatus = status.data || {}
- this.changeSets = changes.data || []
- } catch (error) {
- this.error = '管理员状态加载失败。'
- } finally {
- this.adminLoading = false
- }
- },
- async openSource (item) {
- this.sourceDialog = true
- this.sourceLoading = true
- this.sourceDetail = null
- try {
- const response = await getKnowledgeSource(item.object_uid)
- this.sourceDetail = response.data
- } catch (error) {
- this.sourceDialog = false
- this.error = '来源已更新、删除或当前无权访问。'
- } finally {
- this.sourceLoading = false
- }
- },
- firstPoint (item) {
- return item.point_key || (item.point_keys && item.point_keys[0]) || '未标注知识点'
- },
- firstRevision (item) {
- return item.point_revision || (item.point_revisions && item.point_revisions[0]) || '-'
- },
- statusCount (status) {
- return (this.adminStatus.change_sets && this.adminStatus.change_sets[status]) || 0
- },
- changeColor (status) {
- return { canonical_active: 'success', complete: 'success', failed: 'error', degraded: 'warning', pending: 'primary' }[status] || 'grey'
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .knowledge-workbench {
- min-height: 100%;
- background: #f5f7fa;
- color: #1f2937;
- }
- .workbench-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 32px;
- padding: 36px 40px 28px;
- background: #fff;
- border-bottom: 1px solid #e5e9f0;
- }
- .status-cluster { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
- .workbench-grid {
- display: grid;
- grid-template-columns: minmax(0, 1fr) 320px;
- gap: 24px;
- max-width: 1440px;
- margin: 0 auto;
- padding: 28px 40px 48px;
- }
- .query-panel, .context-section {
- background: #fff;
- border: 1px solid #e3e8ef;
- border-radius: 12px;
- }
- .query-panel { padding: 28px; min-height: 620px; }
- .context-panel { display: flex; flex-direction: column; gap: 16px; }
- .context-section { padding: 20px; }
- .mode-switch { display: flex; justify-content: space-between; gap: 16px; }
- .mode-select { max-width: 180px; }
- .answer-block { padding: 20px; background: #f3f7fd; border-left: 3px solid #1976d2; border-radius: 8px; }
- .answer-copy { white-space: pre-wrap; line-height: 1.8; margin: 0; }
- .evidence-header { display: flex; justify-content: space-between; align-items: center; margin: 26px 0 10px; }
- .evidence-list { border-top: 1px solid #e5e9f0; }
- .evidence-row {
- width: 100%; display: grid; grid-template-columns: 38px minmax(0, 1fr) 24px; gap: 14px;
- align-items: start; padding: 18px 4px; border: 0; border-bottom: 1px solid #e5e9f0;
- background: transparent; color: inherit; text-align: left; cursor: pointer;
- }
- .evidence-row:hover { background: #f7f9fc; }
- .evidence-row:focus-visible { outline: 2px solid #1976d2; outline-offset: 2px; }
- .evidence-index { color: #78909c; font-size: 12px; font-weight: 700; padding-top: 3px; }
- .evidence-main { min-width: 0; display: flex; flex-direction: column; }
- .evidence-content { line-height: 1.65; overflow-wrap: anywhere; }
- .evidence-meta { color: #637083; font-size: 12px; overflow-wrap: anywhere; }
- .empty-result { text-align: center; padding: 56px 20px; border: 1px dashed #cfd8e3; border-radius: 10px; }
- .boundary-row, .metric-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 7px 0; }
- .boundary-row { justify-content: flex-start; color: #475569; }
- .change-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 0; border-top: 1px solid #edf0f4; }
- .source-facts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
- .source-facts > div { display: flex; flex-direction: column; gap: 3px; }
- .source-facts span { color: #637083; font-size: 12px; }
- .source-content { white-space: pre-wrap; overflow-wrap: anywhere; padding: 16px; background: #f5f7fa; border-radius: 8px; font: 13px/1.65 monospace; }
- @media (max-width: 960px) {
- .workbench-header { padding: 28px 24px; }
- .workbench-grid { grid-template-columns: 1fr; padding: 24px; }
- .context-panel { grid-row: 1; }
- }
- @media (max-width: 600px) {
- .workbench-header { flex-direction: column; gap: 16px; }
- .status-cluster { justify-content: flex-start; }
- .workbench-grid { padding: 16px; }
- .query-panel { padding: 20px 16px; }
- .mode-switch { flex-direction: column; }
- .mode-select { max-width: none; }
- .source-facts { grid-template-columns: 1fr; }
- }
- </style>
|