@@ -220,7 +239,8 @@ export function SourceCatalogBrowserView({
) : null}
{refreshJob.state === 'failed' ? (
- 刷新失败;旧快照仍然保留,目录浏览与标注不受影响。
+ 刷新失败;旧快照仍然保留,目录浏览与标注不受影响。原因:
+ {sanitizeRefreshReason(refreshJob.last_error) || '未提供'} · 作业 {refreshJob.id}
) : null}
>
@@ -427,6 +447,15 @@ export function SourceCatalogBrowserView({
{node.failed_count ? (
失败 {node.failed_count}
) : null}
+ {onViewProcessing && node.node_type === 'file' && node.imported_count > 0 ? (
+
+ ) : null}
{node.modified_at
@@ -497,7 +526,13 @@ export function SourceCatalogBrowserView({
)
}
-export function SourceCatalogBrowser() {
+export function SourceCatalogBrowser({
+ focus,
+ onViewProcessing,
+}: {
+ focus?: { connectionID: string; nodeID: string } | null
+ onViewProcessing?: (source: CatalogSource, node: CatalogNode) => void
+} = {}) {
const [sources, setSources] = useState([])
const [selected, setSelected] = useState(null)
const [parentID, setParentID] = useState('')
@@ -621,6 +656,17 @@ export function SourceCatalogBrowser() {
return () => controller.abort()
}, [selected, parentID, offset, revision])
+ // 从解析监控“在来源与目录定位”进入:选中该来源并直接打开文件所在目录。
+ useEffect(() => {
+ if (!focus || !sources.length) return
+ const target = sources.find((source) => source.connection_id === focus.connectionID)
+ if (!target) return
+ setSelected(target)
+ setParentID(focus.nodeID || target.root_node_id)
+ setOffset(0)
+ setPage(null)
+ closeHistory()
+ }, [focus, sources])
// 有来源正在首次扫描时自动刷新列表:百分比会自己往前走,扫完那一行自动变成正式来源。
useEffect(() => {
if (!sources.some((source) => source.snapshot_status === 'scanning')) return
@@ -777,6 +823,7 @@ export function SourceCatalogBrowser() {
refreshHidden={refreshHidden}
onToggleRefreshHidden={() => setRefreshHidden((value) => !value)}
onFiltersChanged={() => setRevision((value) => value + 1)}
+ onViewProcessing={onViewProcessing && selected ? (node) => onViewProcessing(selected, node) : undefined}
/>
)
}
diff --git a/web/src/WeKnoraMonitor.test.tsx b/web/src/WeKnoraMonitor.test.tsx
index bb5dd46..10398c1 100644
--- a/web/src/WeKnoraMonitor.test.tsx
+++ b/web/src/WeKnoraMonitor.test.tsx
@@ -9,6 +9,8 @@ const page: MonitorTaskPage = {
id: 'tp-1',
title: '维护手册.pdf',
path: '产品/维护',
+ node_id: 'node-1',
+ parent_node_id: 'folder-1',
source_id: 's1',
source_name: '技术资料',
target_scope: 'internal',
@@ -27,6 +29,8 @@ const page: MonitorTaskPage = {
id: 'tp-2',
title: '过期资料.pdf',
path: '历史',
+ node_id: 'node-2',
+ parent_node_id: 'folder-2',
source_id: 's1',
source_name: '技术资料',
target_scope: 'external',
@@ -47,6 +51,7 @@ const page: MonitorTaskPage = {
offset: 0,
sources: [{ id: 's1', name: '技术资料' }],
targets: [],
+ bindings: { internal: '', external: '' },
retryable: 1,
}
const props = {
@@ -103,3 +108,65 @@ describe('unified monitor', () => {
expect(html).not.toContain('aria-label="选择过期资料.pdf"')
})
})
+describe('target identity and probe attribution', () => {
+ it('labels same-named targets with short id and current/history state', () => {
+ const current = '0c84a098-d3ed-4b82-a165-e6de9691d6a8'
+ const historical = 'sc-ed1f812c4e5fabe817fc6351104938c4'
+ const withTargets = {
+ ...page,
+ items: [
+ { ...page.items[0], target_kb: current },
+ { ...page.items[1], target_kb: historical },
+ ],
+ targets: [
+ { id: current, name: current, scope: 'internal' },
+ { id: historical, name: historical, scope: 'internal' },
+ ],
+ bindings: { internal: current, external: '' },
+ }
+ const html = renderToStaticMarkup()
+ expect(html).toContain('0c84a098')
+ expect(html).toContain('当前绑定')
+ expect(html).toContain('sc-ed1f8')
+ expect(html).toContain('历史')
+ expect(html).toContain('产品/维护')
+ })
+ it('shows liveness and the sanitized probe target separately from readiness', () => {
+ const overview = {
+ mode: 'real',
+ simulated: false,
+ observed_at: '2026-11-05T02:00:00Z',
+ host: {
+ cpu_percent: 1,
+ memory_used_bytes: 1,
+ memory_total_bytes: 2,
+ disk_used_bytes: 1,
+ disk_total_bytes: 2,
+ gpu_state: 'none',
+ gpu_detail: '',
+ simulated: false,
+ },
+ services: [],
+ knowledge_bases: [],
+ health: [
+ {
+ key: 'weknora',
+ name: 'WeKnora API',
+ liveness: 'up',
+ readiness: 'blocked',
+ reason: 'WeKnora 探测目标知识库不存在(实例 weknora.test · 知识库 1b52c140)',
+ latency_ms: 3,
+ observed_at: '2026-11-05T02:00:00Z',
+ last_success_at: null,
+ consecutive_failures: 1,
+ probe_target: '实例 weknora.test · 知识库 1b52c140',
+ configured: true,
+ applicable: true,
+ },
+ ],
+ }
+ const html = renderToStaticMarkup()
+ expect(html).toContain('存活 up')
+ expect(html).toContain('探测对象 实例 weknora.test · 知识库 1b52c140')
+ })
+})
diff --git a/web/src/WeKnoraMonitor.tsx b/web/src/WeKnoraMonitor.tsx
index 1ad0479..e2ddfaf 100644
--- a/web/src/WeKnoraMonitor.tsx
+++ b/web/src/WeKnoraMonitor.tsx
@@ -8,6 +8,7 @@ import {
actionLabels,
stageLabels,
taskStatusLabels,
+ targetLabel,
} from './monitorTasks'
import type { MonitorFilters, MonitorTask, MonitorTaskPage } from './monitorTasks'
import { requeueJobFailure, retryPublicationsBatch } from './testPublications'
@@ -16,7 +17,16 @@ import { useOptionalAuth } from './auth'
import { WorkspaceContextTabsSetter } from './workspaceContext'
import './monitorWorkbench.css'
-const emptyPage: MonitorTaskPage = { items: [], total: 0, limit: 20, offset: 0, sources: [], targets: [], retryable: 0 }
+const emptyPage: MonitorTaskPage = {
+ items: [],
+ total: 0,
+ limit: 20,
+ offset: 0,
+ sources: [],
+ targets: [],
+ bindings: { internal: '', external: '' },
+ retryable: 0,
+}
const time = (value: string) => (value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—')
const percent = (value: number | null | undefined) => (value == null ? '—' : `${value}%`)
const seconds = (value: number | null | undefined) => (value == null ? '—' : `${Number(value.toFixed(1))} 秒`)
@@ -51,6 +61,7 @@ interface ViewProps {
onRetry: (rows: MonitorTask[]) => void
onDetail: (row: MonitorTask) => void
onRefresh: () => void
+ onLocateSource?: (sourceID: string, parentNodeID: string) => void
}
export function WeKnoraMonitorView(p: ViewProps) {
const { overview, metrics, page, filters, selected, busy, loading } = p
@@ -63,6 +74,8 @@ export function WeKnoraMonitorView(p: ViewProps) {
const selectedIDs = new Set(selected.map((r) => r.id))
const allPage = eligible.length > 0 && eligible.every((r) => selectedIDs.has(r.id))
const kbName = (id: string) => overview?.knowledge_bases.find((k) => k.id === id)?.name || id
+ const bindings = page.bindings
+ const scopeBinding = filters.scope === 'external' ? bindings.external : bindings.internal
return (
@@ -180,7 +193,8 @@ export function WeKnoraMonitorView(p: ViewProps) {
{s.reason || '未提供状态说明'}
- 检测:{time(s.observed_at)} · {s.latency_ms} ms
+ 检测:{time(s.observed_at)} · {s.latency_ms} ms · 存活 {s.liveness || '未确认'}
+ {s.probe_target ? ` · 探测对象 ${s.probe_target}` : ''}
))
@@ -234,10 +248,23 @@ export function WeKnoraMonitorView(p: ViewProps) {
{page.targets.map((t) => (
))}
+
|
{item.target_scope === 'internal' ? '对内' : '对外'}
- {kbName(item.target_kb)}
+
+ {targetLabel(kbName(item.target_kb), item.target_kb, item.target_scope, bindings)}
+
|
{actionLabels[item.action]} |
{stageLabels[item.stage]} |
@@ -466,7 +495,15 @@ export function WeKnoraMonitorView(p: ViewProps) {
)
}
-function TaskDetail({ item, onClose }: { item: MonitorTask; onClose: () => void }) {
+function TaskDetail({
+ item,
+ onClose,
+ onLocateSource,
+}: {
+ item: MonitorTask
+ onClose: () => void
+ onLocateSource?: (sourceID: string, parentNodeID: string) => void
+}) {
const ref = useRef
(null)
useEffect(() => {
ref.current?.showModal()
@@ -483,6 +520,7 @@ function TaskDetail({ item, onClose }: { item: MonitorTask; onClose: () => void
{[
['来源', item.source_name],
+ ['来源连接', item.source_id],
['目录', item.path],
['目标知识库', item.target_kb],
['处理动作', actionLabels[item.action]],
@@ -499,6 +537,17 @@ function TaskDetail({ item, onClose }: { item: MonitorTask; onClose: () => void
))}
+ {onLocateSource ? (
+