Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/cognida-web/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ server {
listen 80;
server_name _;

# Keep the reverse proxy aligned with the Go and frontend 10 MiB upload contract.
client_max_body_size 10m;

root /usr/share/nginx/html;
index index.html;

Expand Down
23 changes: 23 additions & 0 deletions apps/cognida-web/src/api/knowledge/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { post } = vi.hoisted(() => ({ post: vi.fn() }))

vi.mock('@/utils/request', () => ({
http: { post }
}))

import { knowledgeApi } from './index'

describe('knowledgeApi.rebuildGraph', () => {
beforeEach(() => {
post.mockReset()
})

it('uses a five-minute timeout without changing the global request timeout', () => {
knowledgeApi.rebuildGraph('kb-1')

expect(post).toHaveBeenCalledWith('/knowledge-bases/kb-1/graph/rebuild', undefined, {
timeout: 5 * 60 * 1000
})
})
})
14 changes: 9 additions & 5 deletions apps/cognida-web/src/api/knowledge/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { http } from '@/utils/request'
import type {
import type {
KnowledgeBase,
CreateKnowledgeBaseRequest,
UpdateKnowledgeBaseRequest,
Expand All @@ -11,7 +11,9 @@ import type {
SearchRequest,
SearchResponse,
RebuildGraphResult
} from '@/types'
} from '@/types'

const graphRebuildRequestTimeout = 5 * 60 * 1000

/**
* 知识库相关API
Expand Down Expand Up @@ -62,9 +64,11 @@ export const knowledgeApi = {
/**
* 为知识库补建知识图谱(复用已存分块,为历史文档重建,幂等)
*/
rebuildGraph(id: string) {
return http.post<RebuildGraphResult>(`/knowledge-bases/${id}/graph/rebuild`)
},
rebuildGraph(id: string) {
return http.post<RebuildGraphResult>(`/knowledge-bases/${id}/graph/rebuild`, undefined, {
timeout: graphRebuildRequestTimeout
})
},

/**
* 上传知识库文件
Expand Down
4 changes: 4 additions & 0 deletions apps/cognida-web/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ export interface RebuildGraphResult {
failed_documents: number
total_nodes: number
total_relations: number
/** 图谱是否实际替换;存在失败文档或抽取结果为空时为 false,旧图保留 */
graph_replaced: boolean
}

// 知识条目相关
Expand All @@ -306,6 +308,7 @@ export interface Knowledge {
storage_size: number
file_path: string
parse_status: 'unprocessed' | 'pending' | 'processing' | 'completed' | 'failed'
error_message?: string
enable_status: 'enabled' | 'disabled'
chunk_count: number
created_at: string
Expand All @@ -323,6 +326,7 @@ export interface UploadKnowledgeFileRequest {
export interface KnowledgeStatus {
knowledge_id: string
parse_status: 'pending' | 'processing' | 'completed' | 'failed'
error_message?: string
enable_status: 'enabled' | 'disabled'
chunk_count: number
created_at: string
Expand Down
4 changes: 4 additions & 0 deletions apps/cognida-web/src/utils/__tests__/directoryTree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ describe('directoryTree/buildFileTree', () => {
})

describe('directoryTree/filterAcceptableFiles', () => {
it('默认文件大小上限为 10 MiB', () => {
expect(MAX_KNOWLEDGE_FILE_SIZE).toBe(10 * 1024 * 1024)
})

it('按扩展名过滤不支持的文件类型', () => {
const files = [makeFile('a.md'), makeFile('b.exe'), makeFile('c.pdf')]
const result = filterAcceptableFiles(files)
Expand Down
2 changes: 1 addition & 1 deletion apps/cognida-web/src/utils/directoryTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

/** 与后端知识库上传接口保持一致的文件类型/大小限制 */
export const ACCEPTED_KNOWLEDGE_EXTENSIONS = ['.txt', '.md', '.pdf', '.doc', '.docx']
export const MAX_KNOWLEDGE_FILE_SIZE = 50 * 1024 * 1024 // 50MB
export const MAX_KNOWLEDGE_FILE_SIZE = 10 * 1024 * 1024 // 10 MiB

export interface FileTreeNode {
/** 该层级的文件/目录名 */
Expand Down
10 changes: 6 additions & 4 deletions apps/cognida-web/src/views/knowledge/GraphView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
<!-- 图谱画布区 -->
<!-- 全屏目标为外层 wrap(含图例/缩放/背景点阵),而非内层裸 canvas〔FE-2〕 -->
<div ref="canvasWrap" class="gv-canvas-wrap">
<div ref="graphContainer" class="gv-canvas">
<div v-if="loading" class="gv-loader">
<UiLoader size="lg" />
</div>
<!-- vis-network 专用容器:它会清空/接管容器内部 DOM,Vue 不得在此放置任何节点
(包括 v-if 的 loader),否则节点被摘掉后 Vue patch 会以 insertBefore(null) 崩溃 -->
<div ref="graphContainer" class="gv-canvas"></div>
<!-- loading 遮罩放在容器外层(gv-canvas-wrap)作为兄弟节点,绝对定位覆盖画布 -->
<div v-if="loading" class="gv-loader">
<UiLoader size="lg" />
</div>

<!-- 图例浮层(左下角) -->
Expand Down
50 changes: 35 additions & 15 deletions apps/cognida-web/src/views/knowledge/KnowledgeBase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,20 @@
{{ row.chunk_count || 0 }}
</template>
<template #cell-parse_status="{ row }">
<UiTag :variant="getParseStatusType(row.parse_status)">
{{ getParseStatusText(row.parse_status) }}
</UiTag>
<div class="parse-status-cell">
<UiTag :variant="getParseStatusType(row.parse_status)">
{{ getParseStatusText(row.parse_status) }}
</UiTag>
<UiText
v-if="row.parse_status === 'failed' && row.error_message"
class="parse-error-message"
type="danger"
size="sm"
:title="row.error_message"
>
{{ row.error_message }}
</UiText>
</div>
</template>
<template #cell-created_at="{ row }">
{{ formatDateTime(row.created_at) }}
Expand Down Expand Up @@ -447,8 +458,10 @@ import type {
KnowledgeBaseStats,
Knowledge,
Chunk,
UpdateKnowledgeBaseRequest
UpdateKnowledgeBaseRequest,
RebuildGraphResult
} from '@/types'
import { rebuildSuccessMessage } from './rebuild'

const router = useRouter()
const route = useRoute()
Expand Down Expand Up @@ -555,7 +568,7 @@ let chunkLoadSeq = 0
const settingsLoading = ref(false)
const settingsSaving = ref(false)
// 补建图谱状态机:idle→pending→success|error,就地由 <UiAsyncStatus> 常驻展示
const rebuildTask = useAsyncTask()
const rebuildTask = useAsyncTask<RebuildGraphResult | undefined>()
// 后端 UpdateKnowledgeBase 接受 name/description/status,以及库级图谱开关 graph_enabled。
// 分块/BM25 等其余数据处理配置仅在创建时生效,故不在此维护。
const settingsForm = reactive<UpdateKnowledgeBaseRequest>({
Expand Down Expand Up @@ -732,11 +745,11 @@ function handleFileChange(e: Event) {
if (!files.length) return

// accept 属性只是文件选择器的过滤提示(用户可切到「所有文件」绕过),且完全不管大小,
// 故这里与文件夹流程走同一套过滤,避免超过 50MB 或不支持格式的文件直接打到后端
// 故这里与文件夹流程走同一套过滤,避免超过 10 MiB 或不支持格式的文件直接打到后端
const { accepted, rejectedByType, rejectedBySize } = filterAcceptableFiles(files)
const skipped = rejectedByType.length + rejectedBySize.length
if (skipped > 0) {
toast.warning(`已过滤 ${skipped} 个不支持格式或超过 50MB 大小限制的文件`)
toast.warning(`已过滤 ${skipped} 个不支持格式或超过 10 MiB 大小限制的文件`)
}
if (!accepted.length) return

Expand Down Expand Up @@ -993,14 +1006,7 @@ async function rebuildGraph() {
},
{
pendingMessage: '补建中…(文档较多时耗时较长,请勿关闭页面)',
successMessage: (r: any) => {
if (!r) return '补建完成'
// 有失败文档时如实标注,避免把「部分失败」伪装成完全成功
const failed = r.failed_documents ? `,失败 ${r.failed_documents} 篇` : ''
const skipped = r.skipped_documents ? `,跳过 ${r.skipped_documents} 篇` : ''
return `补建完成:处理 ${r.processed_documents}/${r.total_documents} 篇,` +
`新增 ${r.total_nodes} 节点、${r.total_relations} 关系${skipped}${failed}`
},
successMessage: rebuildSuccessMessage,
errorMessage: (e: any) => e?.message || '补建失败',
}
)
Expand Down Expand Up @@ -1078,6 +1084,20 @@ onUnmounted(() => {
color: var(--color-text-muted);
}

.parse-status-cell {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}

.parse-error-message {
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.search-input {
margin-bottom: 16px;
}
Expand Down
42 changes: 42 additions & 0 deletions apps/cognida-web/src/views/knowledge/__tests__/rebuild.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, it, expect } from 'vitest'
import { rebuildSuccessMessage } from '../rebuild'
import type { RebuildGraphResult } from '@/types'

function makeResult(overrides: Partial<RebuildGraphResult> = {}): RebuildGraphResult {
return {
total_documents: 5,
processed_documents: 5,
skipped_documents: 0,
failed_documents: 0,
total_nodes: 10,
total_relations: 8,
graph_replaced: true,
...overrides
}
}

describe('rebuildSuccessMessage', () => {
it('空结果返回默认文案', () => {
expect(rebuildSuccessMessage(null)).toBe('补建完成')
expect(rebuildSuccessMessage(undefined)).toBe('补建完成')
})

it('替换成功时展示处理进度与去重后写入计数', () => {
const msg = rebuildSuccessMessage(makeResult({ skipped_documents: 1, failed_documents: 1 }))
expect(msg).toBe('补建完成:处理 5/5 篇,写入 10 节点、8 关系,跳过 1 篇,失败 1 篇')
})

it('存在失败文档时说明未替换且不展示写入计数', () => {
const msg = rebuildSuccessMessage(
makeResult({ failed_documents: 2, graph_replaced: false, total_nodes: 0, total_relations: 0 })
)
expect(msg).toBe('有 2 篇文档处理失败,为保留现有图谱本次未做任何替换')
expect(msg).not.toContain('节点')
})

it('无失败但未替换(空图)时说明保留旧图', () => {
expect(rebuildSuccessMessage(makeResult({ graph_replaced: false }))).toBe(
'未提取到任何图谱数据,已保留现有图谱'
)
})
})
21 changes: 21 additions & 0 deletions apps/cognida-web/src/views/knowledge/rebuild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { RebuildGraphResult } from '@/types'

/**
* 补建成功文案:如实区分「已替换」与「保留旧图未替换」。
* 节点/关系计数仅在图谱实际写入后才展示,避免把未发生的写入伪装成成功。
*/
export function rebuildSuccessMessage(r: RebuildGraphResult | null | undefined): string {
if (!r) return '补建完成'
if (!r.graph_replaced) {
if (r.failed_documents > 0) {
return `有 ${r.failed_documents} 篇文档处理失败,为保留现有图谱本次未做任何替换`
}
return '未提取到任何图谱数据,已保留现有图谱'
}
const failed = r.failed_documents ? `,失败 ${r.failed_documents} 篇` : ''
const skipped = r.skipped_documents ? `,跳过 ${r.skipped_documents} 篇` : ''
return (
`补建完成:处理 ${r.processed_documents}/${r.total_documents} 篇,` +
`写入 ${r.total_nodes} 节点、${r.total_relations} 关系${skipped}${failed}`
)
}
Loading