fix: make local knowledge bases ready for daily use
Knowledge selection was shared globally, document readiness overstated usable content, and important import and source-management paths exposed technical details or lacked safe recovery actions. Knowledge scope now persists per conversation, readiness reflects real processing channels, and the workspace guides users from creation and import into cited chat with controlled source actions. Release note: 知识库现按对话保存使用范围,准确区分可用、处理中和失败文档,并提供更简洁的创建、检索测试、来源打开、删除确认和失败重试流程。
This commit is contained in:
@@ -3954,6 +3954,9 @@ describe('AssistantDatabase', () => {
|
||||
provider: 'model',
|
||||
profileId: '00000000-0000-4000-8000-000000000299'
|
||||
},
|
||||
knowledgeLibraryIds: [
|
||||
'00000000-0000-4000-8000-000000000214'
|
||||
],
|
||||
knowledgeRetrievalMode: 'always',
|
||||
title: '发布讨论',
|
||||
updatedAt: 1_775_000_000_000,
|
||||
@@ -4055,6 +4058,9 @@ describe('AssistantDatabase', () => {
|
||||
provider: 'model',
|
||||
profileId: '00000000-0000-4000-8000-000000000299'
|
||||
},
|
||||
knowledgeLibraryIds: [
|
||||
'00000000-0000-4000-8000-000000000214'
|
||||
],
|
||||
knowledgeRetrievalMode: 'always',
|
||||
messages: [
|
||||
expect.objectContaining({
|
||||
@@ -4442,6 +4448,9 @@ describe('AssistantDatabase', () => {
|
||||
provider: 'model',
|
||||
profileId: channelDefaultProfileId
|
||||
},
|
||||
knowledgeLibraryIds: [
|
||||
'00000000-0000-4000-8000-000000000559'
|
||||
],
|
||||
knowledgeRetrievalMode: 'always',
|
||||
contextMetrics: {
|
||||
runtimeSelectionKey: `model:${channelDefaultProfileId}`,
|
||||
@@ -4550,6 +4559,9 @@ describe('AssistantDatabase', () => {
|
||||
provider: 'model',
|
||||
profileId: channelDefaultProfileId
|
||||
},
|
||||
knowledgeLibraryIds: [
|
||||
'00000000-0000-4000-8000-000000000559'
|
||||
],
|
||||
knowledgeRetrievalMode: 'always',
|
||||
branch: {
|
||||
sourceConversationId,
|
||||
@@ -4623,6 +4635,7 @@ describe('AssistantDatabase', () => {
|
||||
id: branch.id,
|
||||
projectId: branch.projectId,
|
||||
runtimeSelection: branch.runtimeSelection,
|
||||
knowledgeLibraryIds: branch.knowledgeLibraryIds,
|
||||
knowledgeRetrievalMode: branch.knowledgeRetrievalMode,
|
||||
branch: branch.branch,
|
||||
title: '发布方案讨论 · 分支(独立更新)',
|
||||
|
||||
@@ -1033,6 +1033,7 @@ function interruptActiveToolBlocks(
|
||||
}
|
||||
|
||||
const conversationContextStateSchema = conversationSnapshotSchema.pick({
|
||||
knowledgeLibraryIds: true,
|
||||
contextMetrics: true,
|
||||
contextCompressionState: true
|
||||
})
|
||||
@@ -1041,7 +1042,7 @@ function parseConversationContextState(
|
||||
value: string | null
|
||||
): Pick<
|
||||
ConversationSnapshot,
|
||||
'contextMetrics' | 'contextCompressionState'
|
||||
'knowledgeLibraryIds' | 'contextMetrics' | 'contextCompressionState'
|
||||
> {
|
||||
if (!value) {
|
||||
return {}
|
||||
@@ -1059,12 +1060,14 @@ function parseConversationContextState(
|
||||
function serializeConversationContextState(
|
||||
conversation: Pick<
|
||||
ConversationSnapshot,
|
||||
'contextMetrics' | 'contextCompressionState'
|
||||
'knowledgeLibraryIds' | 'contextMetrics' | 'contextCompressionState'
|
||||
>
|
||||
): string | null {
|
||||
return conversation.contextMetrics ||
|
||||
return conversation.knowledgeLibraryIds !== undefined ||
|
||||
conversation.contextMetrics ||
|
||||
conversation.contextCompressionState
|
||||
? JSON.stringify({
|
||||
knowledgeLibraryIds: conversation.knowledgeLibraryIds,
|
||||
contextMetrics: conversation.contextMetrics,
|
||||
contextCompressionState: conversation.contextCompressionState
|
||||
})
|
||||
@@ -1081,9 +1084,9 @@ function toConversationSnapshot(
|
||||
runtimeSelection: parseRuntimeSelection(
|
||||
conversation.runtime_selection_json
|
||||
),
|
||||
...parseConversationContextState(conversation.context_state_json),
|
||||
knowledgeRetrievalMode:
|
||||
conversation.knowledge_retrieval_mode ?? undefined,
|
||||
...parseConversationContextState(conversation.context_state_json),
|
||||
...(conversation.channel &&
|
||||
conversation.conversation_type &&
|
||||
conversation.account_display
|
||||
@@ -2992,7 +2995,7 @@ export class AssistantDatabase {
|
||||
const source = database
|
||||
.prepare(
|
||||
`SELECT id, project_id, runtime_selection_json,
|
||||
knowledge_retrieval_mode, title
|
||||
knowledge_retrieval_mode, context_state_json, title
|
||||
FROM conversations
|
||||
WHERE id = ? AND status = 'active' AND channel IS NULL`
|
||||
)
|
||||
@@ -3002,6 +3005,7 @@ export class AssistantDatabase {
|
||||
project_id: string | null
|
||||
runtime_selection_json: string | null
|
||||
knowledge_retrieval_mode: 'auto' | 'always' | null
|
||||
context_state_json: string | null
|
||||
title: string
|
||||
}
|
||||
| undefined
|
||||
@@ -3052,13 +3056,18 @@ export class AssistantDatabase {
|
||||
knowledge_retrieval_mode, context_state_json, work_mode,
|
||||
title, branch_source_conversation_id, branch_source_title,
|
||||
status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, NULL, 'ask', ?, ?, ?, 'active', ?, ?)`
|
||||
VALUES (?, ?, ?, ?, ?, 'ask', ?, ?, ?, 'active', ?, ?)`
|
||||
)
|
||||
.run(
|
||||
destinationConversationId,
|
||||
source.project_id,
|
||||
source.runtime_selection_json,
|
||||
source.knowledge_retrieval_mode,
|
||||
serializeConversationContextState({
|
||||
knowledgeLibraryIds:
|
||||
parseConversationContextState(source.context_state_json)
|
||||
.knowledgeLibraryIds
|
||||
}),
|
||||
input.title,
|
||||
source.id,
|
||||
source.title,
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
knowledgeChunkPageSchema,
|
||||
knowledgeChunksListInputSchema,
|
||||
knowledgeChunkUpdateInputSchema,
|
||||
knowledgeDocumentOpenInputSchema,
|
||||
knowledgeDocumentRebuildInputSchema,
|
||||
knowledgeLibraryRebuildInputSchema,
|
||||
knowledgeReferenceContextInputSchema,
|
||||
@@ -892,6 +893,8 @@ function getKnowledgeSnapshot(
|
||||
sourceCount: library.sourceCount,
|
||||
documentCount: library.documentCount,
|
||||
indexedDocumentCount: library.indexedDocumentCount,
|
||||
processingDocumentCount: library.processingDocumentCount,
|
||||
failedDocumentCount: library.failedDocumentCount,
|
||||
retrievalSettings: library.retrievalSettings,
|
||||
chunkingSettings: library.chunkingSettings,
|
||||
chunkingRebuildRequired: library.chunkingRebuildRequired,
|
||||
@@ -926,6 +929,9 @@ function getKnowledgeSnapshot(
|
||||
name: document.title,
|
||||
path: document.sourceLocation,
|
||||
status: document.status,
|
||||
textIndexStatus: document.textIndexStatus,
|
||||
vectorIndexStatus: document.vectorIndexStatus,
|
||||
graphIndexStatus: document.graphIndexStatus,
|
||||
indexProgress: document.status === 'ready' ? 100 : 0,
|
||||
chunkCount: document.chunkCount,
|
||||
size: document.size,
|
||||
@@ -7969,6 +7975,42 @@ export function registerIpcHandlers(
|
||||
}
|
||||
)
|
||||
|
||||
registerHandler(
|
||||
ipcChannels.knowledgeOpenDocumentSource,
|
||||
async (event, input: unknown) => {
|
||||
assertTrustedSender(event, window)
|
||||
const value = knowledgeDocumentOpenInputSchema.parse(input)
|
||||
const reference = knowledgeService.getDocumentSource(value)
|
||||
if (!reference) {
|
||||
throw new Error('文档来源不存在')
|
||||
}
|
||||
if (reference.source.type === 'url') {
|
||||
const target = new URL(reference.source.location)
|
||||
if (!['http:', 'https:'].includes(target.protocol)) {
|
||||
throw new Error('文档来源 URL 协议不受支持')
|
||||
}
|
||||
await shell.openExternal(target.href)
|
||||
return
|
||||
}
|
||||
const storedPath =
|
||||
reference.document.sourceLocation ?? reference.source.location
|
||||
if (!isAbsolute(storedPath)) {
|
||||
throw new Error('文档来源路径无效')
|
||||
}
|
||||
if ((await lstat(storedPath)).isSymbolicLink()) {
|
||||
throw new Error('文档来源不能是符号链接')
|
||||
}
|
||||
const targetPath = await realpath(storedPath)
|
||||
if (!(await stat(targetPath)).isFile()) {
|
||||
throw new Error('文档来源不是可打开的文件')
|
||||
}
|
||||
const openError = await shell.openPath(targetPath)
|
||||
if (openError) {
|
||||
throw new Error('无法打开文档来源')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
registerHandler(
|
||||
ipcChannels.knowledgeReferenceContext,
|
||||
(event, input: unknown) => {
|
||||
|
||||
@@ -207,6 +207,51 @@ describe('KnowledgeDatabase', () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('counts only ready documents as indexed and separates actionable states', async () => {
|
||||
const { database } = await createDatabase()
|
||||
const library = database.createKnowledgeBase({
|
||||
name: 'Status counts',
|
||||
storageMode: 'reference'
|
||||
})
|
||||
const source = database.upsertSource({
|
||||
knowledgeBaseId: library.id,
|
||||
type: 'file',
|
||||
location: 'C:\\notes\\status.md',
|
||||
displayName: 'status.md',
|
||||
status: 'ready'
|
||||
})
|
||||
for (const [index, status] of [
|
||||
'ready',
|
||||
'queued',
|
||||
'parsing',
|
||||
'indexing',
|
||||
'failed'
|
||||
].entries()) {
|
||||
database.upsertDocument(
|
||||
{
|
||||
knowledgeBaseId: library.id,
|
||||
sourceId: source.id,
|
||||
externalId: `status-${index}`,
|
||||
title: `Status ${index}`,
|
||||
metadata: { status }
|
||||
},
|
||||
[{
|
||||
id: `status-${index}-chunk`,
|
||||
ordinal: 0,
|
||||
content: `status ${status}`
|
||||
}]
|
||||
)
|
||||
}
|
||||
|
||||
expect(database.getKnowledgeBaseCounts().get(library.id)).toEqual({
|
||||
sourceCount: 1,
|
||||
documentCount: 5,
|
||||
indexedDocumentCount: 1,
|
||||
processingDocumentCount: 3,
|
||||
failedDocumentCount: 1
|
||||
})
|
||||
})
|
||||
|
||||
it('normalizes legacy negative vector thresholds without losing settings', async () => {
|
||||
const { database, path } = await createDatabase()
|
||||
const library = database.createKnowledgeBase({
|
||||
|
||||
@@ -846,6 +846,8 @@ export class KnowledgeDatabase {
|
||||
sourceCount: number
|
||||
documentCount: number
|
||||
indexedDocumentCount: number
|
||||
processingDocumentCount: number
|
||||
failedDocumentCount: number
|
||||
}
|
||||
> {
|
||||
const rows = this.requireDatabase()
|
||||
@@ -855,8 +857,15 @@ export class KnowledgeDatabase {
|
||||
COUNT(DISTINCT d.id) AS document_count,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN COALESCE(json_extract(d.metadata, '$.status'), 'ready')
|
||||
<> 'failed'
|
||||
= 'ready'
|
||||
THEN d.id END) AS indexed_document_count
|
||||
,COUNT(DISTINCT CASE
|
||||
WHEN json_extract(d.metadata, '$.status')
|
||||
IN ('queued', 'parsing', 'indexing')
|
||||
THEN d.id END) AS processing_document_count
|
||||
,COUNT(DISTINCT CASE
|
||||
WHEN json_extract(d.metadata, '$.status') = 'failed'
|
||||
THEN d.id END) AS failed_document_count
|
||||
FROM knowledge_bases kb
|
||||
LEFT JOIN knowledge_sources s ON s.knowledge_base_id = kb.id
|
||||
LEFT JOIN documents d ON d.knowledge_base_id = kb.id
|
||||
@@ -872,6 +881,14 @@ export class KnowledgeDatabase {
|
||||
indexedDocumentCount: asNumber(
|
||||
row,
|
||||
'indexed_document_count'
|
||||
),
|
||||
processingDocumentCount: asNumber(
|
||||
row,
|
||||
'processing_document_count'
|
||||
),
|
||||
failedDocumentCount: asNumber(
|
||||
row,
|
||||
'failed_document_count'
|
||||
)
|
||||
}
|
||||
])
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { strToU8, zipSync } from 'fflate'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ExtractStructured } from './graph-extractor'
|
||||
import { embeddingStorageProvider } from './embedding-provider-key'
|
||||
@@ -17,6 +18,83 @@ import { UrlImporter } from './url-importer'
|
||||
const temporaryDirectories: string[] = []
|
||||
const services: KnowledgeService[] = []
|
||||
|
||||
function createRealDocxFixture(): Buffer {
|
||||
return Buffer.from(
|
||||
zipSync({
|
||||
'[Content_Types].xml': strToU8(
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
||||
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
|
||||
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
||||
'<Default Extension="xml" ContentType="application/xml"/>' +
|
||||
'<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>' +
|
||||
'</Types>'
|
||||
),
|
||||
'_rels/.rels': strToU8(
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
||||
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
||||
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>' +
|
||||
'</Relationships>'
|
||||
),
|
||||
'word/document.xml': strToU8(
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
||||
'<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">' +
|
||||
'<w:body>' +
|
||||
'<w:p><w:r><w:t>蓝鲸发布流程</w:t></w:r></w:p>' +
|
||||
'<w:p><w:r><w:t>发布前必须完成审批和回归测试。</w:t></w:r></w:p>' +
|
||||
'<w:tbl><w:tr><w:tc><w:p><w:r><w:t>负责人</w:t></w:r></w:p></w:tc>' +
|
||||
'<w:tc><w:p><w:r><w:t>产品团队</w:t></w:r></w:p></w:tc></w:tr></w:tbl>' +
|
||||
'<w:sectPr/></w:body></w:document>'
|
||||
),
|
||||
'word/_rels/document.xml.rels': strToU8(
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
||||
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"/>'
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function createRealPdfFixture(...pageTexts: string[]): Buffer {
|
||||
const texts = pageTexts.length > 0 ? pageTexts : ['']
|
||||
const fontObjectId = texts.length + 3
|
||||
const firstContentObjectId = fontObjectId + 1
|
||||
const objects = [
|
||||
'<< /Type /Catalog /Pages 2 0 R >>',
|
||||
`<< /Type /Pages /Kids [${texts
|
||||
.map((_, index) => `${index + 3} 0 R`)
|
||||
.join(' ')}] /Count ${texts.length} >>`,
|
||||
...texts.map(
|
||||
(_, index) =>
|
||||
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] ' +
|
||||
`/Resources << /Font << /F1 ${fontObjectId} 0 R >> >> ` +
|
||||
`/Contents ${firstContentObjectId + index} 0 R >>`
|
||||
),
|
||||
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
|
||||
...texts.map((text) => {
|
||||
const stream = `BT /F1 14 Tf 72 720 Td (${text}) Tj ET`
|
||||
return (
|
||||
`<< /Length ${Buffer.byteLength(stream)} >>\n` +
|
||||
`stream\n${stream}\nendstream`
|
||||
)
|
||||
})
|
||||
]
|
||||
let content = '%PDF-1.4\n'
|
||||
const offsets = [0]
|
||||
for (const [index, object] of objects.entries()) {
|
||||
offsets.push(Buffer.byteLength(content))
|
||||
content += `${index + 1} 0 obj\n${object}\nendobj\n`
|
||||
}
|
||||
const xrefOffset = Buffer.byteLength(content)
|
||||
content += `xref\n0 ${objects.length + 1}\n`
|
||||
content += '0000000000 65535 f \n'
|
||||
content += offsets
|
||||
.slice(1)
|
||||
.map((offset) => `${String(offset).padStart(10, '0')} 00000 n \n`)
|
||||
.join('')
|
||||
content += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\n`
|
||||
content += `startxref\n${xrefOffset}\n%%EOF\n`
|
||||
return Buffer.from(content)
|
||||
}
|
||||
|
||||
async function createService(
|
||||
urlImporter?: UrlImporter,
|
||||
embeddingProvider?: EmbeddingProvider,
|
||||
@@ -48,6 +126,83 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('KnowledgeService', () => {
|
||||
it('imports real DOCX and PDF files from disk into searchable chunks', async () => {
|
||||
const { directory, service } = await createService()
|
||||
const docxPath = join(directory, '蓝鲸发布手册.docx')
|
||||
const pdfPath = join(directory, 'Blue-Harbor-operations.pdf')
|
||||
await Promise.all([
|
||||
writeFile(docxPath, createRealDocxFixture()),
|
||||
writeFile(
|
||||
pdfPath,
|
||||
createRealPdfFixture(
|
||||
'Blue Harbor deployment requires a signed approval.',
|
||||
'Rollback begins by restoring the verified package.'
|
||||
)
|
||||
)
|
||||
])
|
||||
const library = service.createLibrary({
|
||||
name: '真实办公文档',
|
||||
storageMode: 'reference',
|
||||
graphEnabled: false
|
||||
})
|
||||
|
||||
await service.importPaths(library.id, [docxPath, pdfPath])
|
||||
|
||||
const snapshot = service.snapshot(library.id)
|
||||
expect(snapshot.libraries[0]).toMatchObject({
|
||||
documentCount: 2,
|
||||
indexedDocumentCount: 2,
|
||||
processingDocumentCount: 0,
|
||||
failedDocumentCount: 0
|
||||
})
|
||||
expect(snapshot.documents).toHaveLength(2)
|
||||
expect(
|
||||
snapshot.documents.every(
|
||||
(document) =>
|
||||
document.status === 'ready' &&
|
||||
document.textIndexStatus === 'ready' &&
|
||||
document.vectorIndexStatus === 'disabled'
|
||||
)
|
||||
).toBe(true)
|
||||
|
||||
const docxDocument = snapshot.documents.find(
|
||||
(document) => document.sourceLocation === docxPath
|
||||
)
|
||||
const pdfDocument = snapshot.documents.find(
|
||||
(document) => document.sourceLocation === pdfPath
|
||||
)
|
||||
expect(docxDocument).toBeDefined()
|
||||
expect(pdfDocument).toBeDefined()
|
||||
expect(
|
||||
service.getDocumentSource({
|
||||
knowledgeBaseId: library.id,
|
||||
documentId: docxDocument!.id
|
||||
})
|
||||
).toMatchObject({
|
||||
document: { sourceLocation: docxPath },
|
||||
source: { knowledgeBaseId: library.id }
|
||||
})
|
||||
const docxChunks = service.database.listChunks(docxDocument!.id)
|
||||
const pdfChunks = service.database.listChunks(pdfDocument!.id)
|
||||
expect(docxChunks.map((chunk) => chunk.content).join('\n')).toContain(
|
||||
'发布前必须完成审批和回归测试'
|
||||
)
|
||||
expect(docxChunks.some((chunk) => chunk.location?.includes('表格'))).toBe(
|
||||
true
|
||||
)
|
||||
expect(pdfChunks.map((chunk) => chunk.location)).toEqual([
|
||||
'第 1 页',
|
||||
'第 2 页'
|
||||
])
|
||||
|
||||
expect(service.search(library.id, '发布审批')[0]?.document.id).toBe(
|
||||
docxDocument!.id
|
||||
)
|
||||
expect(
|
||||
service.search(library.id, 'verified package')[0]?.document.id
|
||||
).toBe(pdfDocument!.id)
|
||||
})
|
||||
|
||||
it('uses document and query embedding roles without exposing providers to runtimes', async () => {
|
||||
const embed = vi.fn(async (input: readonly string[]) =>
|
||||
input.map(() => [9, 9])
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
knowledgeDocumentRebuildInputSchema,
|
||||
knowledgeLibraryRebuildInputSchema,
|
||||
knowledgeReferenceContextInputSchema,
|
||||
knowledgeDocumentOpenInputSchema,
|
||||
type KnowledgeDocumentOpenInput,
|
||||
knowledgeRetrieveInputSchema,
|
||||
knowledgeSettingsUpdateInputSchema,
|
||||
type KnowledgeChunkDeleteInput,
|
||||
@@ -109,6 +111,8 @@ export type KnowledgeLibrarySnapshot = KnowledgeBase & {
|
||||
sourceCount: number
|
||||
documentCount: number
|
||||
indexedDocumentCount: number
|
||||
processingDocumentCount: number
|
||||
failedDocumentCount: number
|
||||
}
|
||||
|
||||
export type KnowledgeSourceSnapshot = KnowledgeSource & {
|
||||
@@ -120,6 +124,9 @@ export type KnowledgeSourceSnapshot = KnowledgeSource & {
|
||||
export type KnowledgeDocumentSnapshot = Document & {
|
||||
chunkCount: number
|
||||
status: 'queued' | 'parsing' | 'indexing' | 'ready' | 'failed'
|
||||
textIndexStatus: 'waiting' | 'ready' | 'failed'
|
||||
vectorIndexStatus: 'disabled' | 'waiting' | 'ready' | 'failed' | 'missing'
|
||||
graphIndexStatus: 'disabled' | 'on-demand' | 'waiting' | 'ready' | 'failed'
|
||||
size?: number
|
||||
error?: string
|
||||
}
|
||||
@@ -1067,7 +1074,9 @@ export class KnowledgeService {
|
||||
const counts = libraryCounts.get(library.id) ?? {
|
||||
sourceCount: 0,
|
||||
documentCount: 0,
|
||||
indexedDocumentCount: 0
|
||||
indexedDocumentCount: 0,
|
||||
processingDocumentCount: 0,
|
||||
failedDocumentCount: 0
|
||||
}
|
||||
return {
|
||||
...library,
|
||||
@@ -1120,6 +1129,21 @@ export class KnowledgeService {
|
||||
: undefined
|
||||
}))
|
||||
const chunkCounts = this.database.getDocumentChunkCounts(libraryId)
|
||||
const tasks = this.database.listKnowledgeTasks(libraryId)
|
||||
const library = libraries.find((item) => item.id === libraryId)
|
||||
const graphTaskByDocument = new Map<
|
||||
string,
|
||||
KnowledgeTaskItem
|
||||
>()
|
||||
for (const task of tasks) {
|
||||
if (
|
||||
task.documentId &&
|
||||
task.kind === 'graph' &&
|
||||
!graphTaskByDocument.has(task.documentId)
|
||||
) {
|
||||
graphTaskByDocument.set(task.documentId, task)
|
||||
}
|
||||
}
|
||||
const documents = libraryDocuments.map((document) => {
|
||||
const status =
|
||||
typeof document.metadata.status === 'string' &&
|
||||
@@ -1132,6 +1156,49 @@ export class KnowledgeService {
|
||||
...document,
|
||||
chunkCount: chunkCounts.get(document.id) ?? 0,
|
||||
status,
|
||||
textIndexStatus:
|
||||
status === 'ready'
|
||||
? ('ready' as const)
|
||||
: status === 'failed'
|
||||
? ('failed' as const)
|
||||
: ('waiting' as const),
|
||||
vectorIndexStatus: (() => {
|
||||
if (!this.embeddingProvider) {
|
||||
return 'disabled' as const
|
||||
}
|
||||
if (status !== 'ready') {
|
||||
return 'waiting' as const
|
||||
}
|
||||
const state = this.database.getEmbeddingIndexState(
|
||||
document.id,
|
||||
embeddingStorageProvider(this.embeddingProvider),
|
||||
this.embeddingProvider.model
|
||||
)
|
||||
return state?.status === 'ready'
|
||||
? ('ready' as const)
|
||||
: state?.status === 'error'
|
||||
? ('failed' as const)
|
||||
: ('missing' as const)
|
||||
})(),
|
||||
graphIndexStatus: (() => {
|
||||
if (!library?.graphEnabled) {
|
||||
return 'disabled' as const
|
||||
}
|
||||
if (library.graphStrategy === 'ask') {
|
||||
return 'on-demand' as const
|
||||
}
|
||||
if (status !== 'ready') {
|
||||
return 'waiting' as const
|
||||
}
|
||||
const task = graphTaskByDocument.get(document.id)
|
||||
if (task?.status === 'failed' || task?.status === 'interrupted') {
|
||||
return 'failed' as const
|
||||
}
|
||||
if (task?.status === 'succeeded') {
|
||||
return 'ready' as const
|
||||
}
|
||||
return 'waiting' as const
|
||||
})(),
|
||||
size:
|
||||
typeof document.metadata.size === 'number'
|
||||
? document.metadata.size
|
||||
@@ -1150,7 +1217,7 @@ export class KnowledgeService {
|
||||
entities: graph.entities,
|
||||
relations: graph.relations,
|
||||
evidence: graph.evidence,
|
||||
tasks: this.database.listKnowledgeTasks(libraryId)
|
||||
tasks
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1698,6 +1765,25 @@ export class KnowledgeService {
|
||||
}
|
||||
}
|
||||
|
||||
getDocumentSource(rawInput: KnowledgeDocumentOpenInput) {
|
||||
const input = knowledgeDocumentOpenInputSchema.parse(rawInput)
|
||||
const document = this.database.getDocument(input.documentId)
|
||||
if (
|
||||
!document ||
|
||||
document.knowledgeBaseId !== input.knowledgeBaseId
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
const source = this.database.getSource(document.sourceId)
|
||||
if (
|
||||
!source ||
|
||||
source.knowledgeBaseId !== input.knowledgeBaseId
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
return { document, source }
|
||||
}
|
||||
|
||||
async searchHybrid(
|
||||
knowledgeBaseId: string,
|
||||
query: string,
|
||||
|
||||
Reference in New Issue
Block a user