mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 11:37:31 +08:00
feat(ui): hot cache panel — view, regenerate, reset from KB detail
This commit is contained in:
parent
2ae8758cd1
commit
af5670f2f7
@ -653,3 +653,24 @@ export const featureFlagApi = {
|
|||||||
update: (flagKey: string, data: FeatureFlagUpdate) =>
|
update: (flagKey: string, data: FeatureFlagUpdate) =>
|
||||||
http.put(`/feature-flags/${flagKey}`, data),
|
http.put(`/feature-flags/${flagKey}`, data),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WikiHotCache {
|
||||||
|
id: number
|
||||||
|
kbId: number
|
||||||
|
content: string | null
|
||||||
|
contentHash: string | null
|
||||||
|
lastUpdated: string | null
|
||||||
|
updateReason: string | null
|
||||||
|
rebuildCount: number
|
||||||
|
lastRebuildStartedAt: string | null
|
||||||
|
lastRebuildDurationMs: number | null
|
||||||
|
lastRebuildError: string | null
|
||||||
|
createTime: string
|
||||||
|
updateTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hotCacheApi = {
|
||||||
|
get: (kbId: number) => http.get<WikiHotCache | null>(`/wiki/hot-cache/${kbId}`),
|
||||||
|
regenerate: (kbId: number) => http.post(`/wiki/hot-cache/${kbId}/regenerate`),
|
||||||
|
reset: (kbId: number) => http.delete(`/wiki/hot-cache/${kbId}`),
|
||||||
|
}
|
||||||
|
|||||||
@ -1489,6 +1489,27 @@ export default {
|
|||||||
rawMaterials: 'Raw Materials',
|
rawMaterials: 'Raw Materials',
|
||||||
pages: 'Wiki Pages',
|
pages: 'Wiki Pages',
|
||||||
config: 'Config',
|
config: 'Config',
|
||||||
|
hotCache: {
|
||||||
|
tab: 'Hot Cache',
|
||||||
|
title: 'Recent Activity Snapshot (Hot Cache)',
|
||||||
|
desc: 'A rolling markdown digest of what has been happening in this knowledge base, injected into the agent system prompt so chat does not have to re-search the obvious. Rebuilt asynchronously by the listener with a 5-minute debounce.',
|
||||||
|
empty: 'No snapshot yet — one will be built after the bound agent finishes its next conversation.',
|
||||||
|
contentEmpty: '(snapshot body is empty)',
|
||||||
|
regenerate: 'Rebuild now',
|
||||||
|
regenerating: 'Rebuilding…',
|
||||||
|
regenerateQueued: 'Rebuild queued; refreshing in a few seconds',
|
||||||
|
regenerateFailed: 'Failed to schedule rebuild',
|
||||||
|
reset: 'Clear snapshot',
|
||||||
|
resetConfirm: 'Clear the current snapshot? The next agent conversation will regenerate it.',
|
||||||
|
resetDone: 'Cleared',
|
||||||
|
resetFailed: 'Clear failed',
|
||||||
|
lastUpdated: 'Last updated',
|
||||||
|
updateReason: 'Trigger reason',
|
||||||
|
rebuildCount: 'Rebuilds',
|
||||||
|
lastDuration: 'Last duration',
|
||||||
|
lastError: 'Last error',
|
||||||
|
footer: 'Toggling wiki.hot_cache.enabled off short-circuits the injection path. The "Rebuild now" button bypasses the debounce window for easier debugging.',
|
||||||
|
},
|
||||||
searchPages: 'Search pages...',
|
searchPages: 'Search pages...',
|
||||||
dropFiles: 'Drop files here or click to upload',
|
dropFiles: 'Drop files here or click to upload',
|
||||||
dropToUpload: 'Release to upload',
|
dropToUpload: 'Release to upload',
|
||||||
|
|||||||
@ -1501,6 +1501,27 @@ export default {
|
|||||||
rawMaterials: '原始材料',
|
rawMaterials: '原始材料',
|
||||||
pages: 'Wiki 页面',
|
pages: 'Wiki 页面',
|
||||||
config: '处理配置',
|
config: '处理配置',
|
||||||
|
hotCache: {
|
||||||
|
tab: '近况快照',
|
||||||
|
title: '近况快照(Hot Cache)',
|
||||||
|
desc: '该知识库最近发生了什么的滚动 markdown 摘要,会被自动注入到 Agent 的系统提示,让对话不必每次都重新检索。事件触发后台异步重建,5 分钟去抖。',
|
||||||
|
empty: '尚未生成快照——绑定的 Agent 完成一次对话后会自动构建。',
|
||||||
|
contentEmpty: '(快照内容为空)',
|
||||||
|
regenerate: '立即重建',
|
||||||
|
regenerating: '重建中…',
|
||||||
|
regenerateQueued: '已加入重建队列,几秒后刷新',
|
||||||
|
regenerateFailed: '重建调度失败',
|
||||||
|
reset: '清除快照',
|
||||||
|
resetConfirm: '确认清除当前快照?下次 Agent 对话会重新生成。',
|
||||||
|
resetDone: '已清除',
|
||||||
|
resetFailed: '清除失败',
|
||||||
|
lastUpdated: '上次更新',
|
||||||
|
updateReason: '触发原因',
|
||||||
|
rebuildCount: '累计重建次数',
|
||||||
|
lastDuration: '上次耗时',
|
||||||
|
lastError: '最近一次错误',
|
||||||
|
footer: '关闭 wiki.hot_cache.enabled 后注入路径短路;该面板的"立即重建"会绕过去抖,方便调试。',
|
||||||
|
},
|
||||||
searchPages: '搜索页面...',
|
searchPages: '搜索页面...',
|
||||||
dropFiles: '拖拽文件到此处或点击上传',
|
dropFiles: '拖拽文件到此处或点击上传',
|
||||||
dropToUpload: '松开即开始上传',
|
dropToUpload: '松开即开始上传',
|
||||||
|
|||||||
289
mateclaw-ui/src/views/Wiki/components/HotCachePanel.vue
Normal file
289
mateclaw-ui/src/views/Wiki/components/HotCachePanel.vue
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hot-cache-panel">
|
||||||
|
<header class="panel-header">
|
||||||
|
<div class="header-text">
|
||||||
|
<h3 class="panel-title">{{ t('wiki.hotCache.title') }}</h3>
|
||||||
|
<p class="panel-desc">{{ t('wiki.hotCache.desc') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button
|
||||||
|
class="btn-secondary"
|
||||||
|
:disabled="loading || !cache"
|
||||||
|
@click="onReset"
|
||||||
|
>
|
||||||
|
{{ t('wiki.hotCache.reset') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
:disabled="regenerating"
|
||||||
|
@click="onRegenerate"
|
||||||
|
>
|
||||||
|
<span v-if="regenerating">{{ t('wiki.hotCache.regenerating') }}</span>
|
||||||
|
<span v-else>{{ t('wiki.hotCache.regenerate') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="loading" class="state-row">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>{{ t('common.loading') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="state-row state-row--error">
|
||||||
|
<el-icon><WarningFilled /></el-icon>
|
||||||
|
<span>{{ error }}</span>
|
||||||
|
<button class="btn-secondary" @click="load">{{ t('common.retry', 'Retry') }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!cache" class="state-row state-row--empty">
|
||||||
|
<span>{{ t('wiki.hotCache.empty') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="cache-body">
|
||||||
|
<div class="meta-grid">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">{{ t('wiki.hotCache.lastUpdated') }}</span>
|
||||||
|
<span class="meta-value">{{ formatTimestamp(cache.lastUpdated) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">{{ t('wiki.hotCache.updateReason') }}</span>
|
||||||
|
<span class="meta-value">{{ cache.updateReason || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">{{ t('wiki.hotCache.rebuildCount') }}</span>
|
||||||
|
<span class="meta-value">{{ cache.rebuildCount ?? 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item" v-if="cache.lastRebuildDurationMs">
|
||||||
|
<span class="meta-label">{{ t('wiki.hotCache.lastDuration') }}</span>
|
||||||
|
<span class="meta-value">{{ formatDuration(cache.lastRebuildDurationMs) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="cache.lastRebuildError" class="error-banner">
|
||||||
|
<el-icon><WarningFilled /></el-icon>
|
||||||
|
<div>
|
||||||
|
<div class="error-title">{{ t('wiki.hotCache.lastError') }}</div>
|
||||||
|
<div class="error-detail">{{ cache.lastRebuildError }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre class="cache-content">{{ cache.content || t('wiki.hotCache.contentEmpty') }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="footer-note">{{ t('wiki.hotCache.footer') }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { ElIcon, ElMessage } from 'element-plus'
|
||||||
|
import { Loading, WarningFilled } from '@element-plus/icons-vue'
|
||||||
|
import { hotCacheApi, type WikiHotCache } from '@/api/index'
|
||||||
|
import { useWikiStore } from '@/stores/useWikiStore'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const store = useWikiStore()
|
||||||
|
|
||||||
|
const cache = ref<WikiHotCache | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const regenerating = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
if (!store.currentKB) return
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
const resp: any = await hotCacheApi.get(store.currentKB.id)
|
||||||
|
cache.value = resp?.data ?? null
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e?.message ?? String(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRegenerate() {
|
||||||
|
if (!store.currentKB) return
|
||||||
|
regenerating.value = true
|
||||||
|
try {
|
||||||
|
await hotCacheApi.regenerate(store.currentKB.id)
|
||||||
|
ElMessage.success(t('wiki.hotCache.regenerateQueued'))
|
||||||
|
// Background rebuild — poll once after a short delay so the operator sees
|
||||||
|
// the row update without manual refresh. The LLM call typically returns
|
||||||
|
// within 5-15s; if the user hits "regenerate" again, a fresh poll fires.
|
||||||
|
setTimeout(load, 4000)
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message ?? t('wiki.hotCache.regenerateFailed'))
|
||||||
|
} finally {
|
||||||
|
regenerating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onReset() {
|
||||||
|
if (!store.currentKB) return
|
||||||
|
if (!confirm(t('wiki.hotCache.resetConfirm'))) return
|
||||||
|
try {
|
||||||
|
await hotCacheApi.reset(store.currentKB.id)
|
||||||
|
ElMessage.success(t('wiki.hotCache.resetDone'))
|
||||||
|
await load()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message ?? t('wiki.hotCache.resetFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(iso: string | null): string {
|
||||||
|
if (!iso) return t('common.never', 'Never')
|
||||||
|
return new Date(iso).toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(ms: number): string {
|
||||||
|
if (ms < 1000) return `${ms}ms`
|
||||||
|
return `${(ms / 1000).toFixed(1)}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => store.currentKB?.id, () => {
|
||||||
|
cache.value = null
|
||||||
|
if (store.currentKB) load()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hot-cache-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.header-text { flex: 1; min-width: 240px; }
|
||||||
|
.header-actions { display: flex; gap: 8px; }
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
}
|
||||||
|
.panel-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mc-text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary, .btn-secondary {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--mc-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-primary:hover:not(:disabled) { background: var(--mc-primary-hover); }
|
||||||
|
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--mc-bg-elevated);
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
border-color: var(--mc-border);
|
||||||
|
}
|
||||||
|
.btn-secondary:hover:not(:disabled) { background: var(--mc-bg-sunken); }
|
||||||
|
.btn-secondary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.state-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--mc-bg-muted);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--mc-text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.state-row--error {
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
background: var(--el-color-danger-light-9);
|
||||||
|
}
|
||||||
|
.state-row--empty {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
background: var(--mc-bg-elevated);
|
||||||
|
border: 1px solid var(--mc-border-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
.meta-item { display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
.meta-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--mc-text-tertiary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.meta-value {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-banner {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: var(--el-color-danger-light-9);
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--el-color-danger-light-7);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.error-title { font-weight: 600; margin-bottom: 2px; }
|
||||||
|
.error-detail { font-family: var(--mc-font-mono, ui-monospace, Menlo, monospace); font-size: 12px; word-break: break-word; }
|
||||||
|
|
||||||
|
.cache-content {
|
||||||
|
background: var(--mc-bg-elevated);
|
||||||
|
border: 1px solid var(--mc-border-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
font-family: var(--mc-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-note {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--mc-text-tertiary);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -282,6 +282,10 @@
|
|||||||
<div v-if="activeTab === 'config'" class="tab-content tab-content--config">
|
<div v-if="activeTab === 'config'" class="tab-content tab-content--config">
|
||||||
<WikiConfig />
|
<WikiConfig />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeTab === 'hotCache'" class="tab-content tab-content--hot-cache">
|
||||||
|
<HotCachePanel />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -318,6 +322,7 @@ import RawMaterialPanel from './components/RawMaterialPanel.vue'
|
|||||||
import WikiPageViewer from './components/WikiPageViewer.vue'
|
import WikiPageViewer from './components/WikiPageViewer.vue'
|
||||||
import WikiConfig from './components/WikiConfig.vue'
|
import WikiConfig from './components/WikiConfig.vue'
|
||||||
import WikiGraphView from './components/WikiGraphView.vue'
|
import WikiGraphView from './components/WikiGraphView.vue'
|
||||||
|
import HotCachePanel from './components/HotCachePanel.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useWikiStore()
|
const store = useWikiStore()
|
||||||
@ -507,6 +512,7 @@ const tabs = computed(() => [
|
|||||||
{ key: 'pages', label: t('wiki.pages') },
|
{ key: 'pages', label: t('wiki.pages') },
|
||||||
{ key: 'graph', label: t('wiki.graph.tab') },
|
{ key: 'graph', label: t('wiki.graph.tab') },
|
||||||
{ key: 'config', label: t('wiki.config') },
|
{ key: 'config', label: t('wiki.config') },
|
||||||
|
{ key: 'hotCache', label: t('wiki.hotCache.tab') },
|
||||||
])
|
])
|
||||||
|
|
||||||
async function selectKB(id: number) {
|
async function selectKB(id: number) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user