Files
goodbuddy/src/main/knowledge/knowledge-ipc.ts
T
mesalogo f81f342cde feat: open local runtime clients from the composer
Add composer shortcuts for local Continue and OpenCode terminals and the official DeepSeek Harness Web client, using the current project and model bridge. Manage browser service reuse, stop actions and terminal cleanup. Upgrade DeepSeek Harness to 0.1.7-rc.2 and Cordis to 4.0.4.

Include the coordinated IPC handler extraction and related tests and documentation. Remote native-client coordinator wiring, managed Node consolidation, Ask MCP mappings and full desktop/package acceptance remain unfinished; this commit records the implemented local workflow and supporting SSH groundwork.

Validation: typecheck and lint passed; 267 focused tests passed and 5 skipped. No new real-provider calls were made for this commit. Prior real-model evidence and remaining acceptance work are documented in the feature progress files.

Release note: 本地项目选择 Continue 或 OpenCode 后,可从输入框下方打开原生终端;选择 DS 后,可打开使用当前项目和模型的官方 Web 客户端。客户端使用独立会话,DS 网页关闭后服务继续运行,可在 GoodBuddy 中停止;远程入口尚未完成。
2026-09-27 08:38:00 +08:00

842 lines
28 KiB
TypeScript

import { dialog, shell, type BrowserWindow, type ipcMain } from 'electron'
import { lstat, realpath, stat } from 'node:fs/promises'
import { isAbsolute } from 'node:path'
import { z } from 'zod'
import {
externalKnowledgeInstanceInputSchema,
externalKnowledgeInstanceEnabledInputSchema,
externalKnowledgeInstanceSaveInputSchema,
externalKnowledgeCatalogListInputSchema,
externalKnowledgeCatalogGetInputSchema,
externalKnowledgeBindingSaveInputSchema,
externalKnowledgeBindingUpdateInputSchema,
externalKnowledgeBindingTestInputSchema
} from '../../shared/external-knowledge-contracts'
import { knowledgeReferenceKey, toKnowledgeReference } from '../../shared/knowledge-reference'
import {
knowledgeCreateSchema,
knowledgeEntityUpdateSchema,
knowledgeIdSchema,
knowledgeImportPathsSchema,
knowledgeRelationInputSchema,
knowledgeUpdateLibrarySchema,
knowledgeUrlImportSchema,
type KnowledgeSnapshot
} from '../../shared/contracts'
import {
knowledgeChunkDeleteInputSchema,
knowledgeChunkPageSchema,
knowledgeChunksListInputSchema,
knowledgeChunkUpdateInputSchema,
knowledgeDocumentOpenInputSchema,
knowledgeDocumentRebuildInputSchema,
knowledgeLibraryRebuildInputSchema,
knowledgeReferenceContextInputSchema,
knowledgeReferenceContextSchema,
knowledgeReferenceOpenInputSchema,
knowledgeRetrieveInputSchema,
knowledgeRetrievalResponseSchema,
knowledgeSettingsUpdateInputSchema
} from '../../shared/knowledge-contracts'
import {
knowledgeTaskActionInputSchema,
knowledgeTaskItemSchema
} from '../../shared/knowledge-task-contracts'
import {
knowledgeEmbeddingIndexCancelRequestSchema,
knowledgeEmbeddingIndexRequestSchema,
knowledgeEmbeddingIndexSnapshotSchema
} from '../../shared/embedding-contracts'
import { ipcChannels } from '../../shared/ipc-channels'
import { assertTrustedSender } from '../trusted-ipc-sender'
import type { RuntimeSettingsStore } from '../runtime-settings-store'
import type { KnowledgeService } from './knowledge-service'
import { supportedDocumentExtensions } from './document-parser'
const knowledgeSearchSchema = z
.object({
libraryIds: z.array(knowledgeIdSchema).max(20),
query: z.string().trim().min(1).max(512)
})
.strict()
const knowledgeSelectionSchema = z
.object({
libraryId: knowledgeIdSchema,
graphStrategy: z.enum(['rules', 'model', 'hybrid']).optional()
})
.strict()
const knowledgeEntityPayloadSchema = z
.object({
entityId: knowledgeIdSchema,
update: knowledgeEntityUpdateSchema
})
.strict()
const knowledgeCreateEntitySchema = z
.object({
libraryId: knowledgeIdSchema,
input: knowledgeEntityUpdateSchema
})
.strict()
const knowledgeMoveEntitySchema = z
.object({
entityId: knowledgeIdSchema,
position: z
.object({
x: z.number().finite().min(-100_000).max(100_000),
y: z.number().finite().min(-100_000).max(100_000)
})
.strict()
})
.strict()
const knowledgeMergeSchema = z
.object({
sourceEntityId: knowledgeIdSchema,
targetEntityId: knowledgeIdSchema
})
.strict()
const knowledgeCreateRelationSchema = z
.object({
libraryId: knowledgeIdSchema,
input: knowledgeRelationInputSchema
})
.strict()
const knowledgeUpdateRelationSchema = z
.object({
relationId: knowledgeIdSchema,
input: knowledgeRelationInputSchema
})
.strict()
function getKnowledgeSnapshot(
service: KnowledgeService,
selectedLibraryId?: string
): KnowledgeSnapshot {
const snapshot = service.snapshot(selectedLibraryId)
const activeLibraryId =
selectedLibraryId ?? snapshot.libraries[0]?.id
const documentsById = new Map(
snapshot.documents.map((document) => [document.id, document])
)
const evidenceByEntity = new Map<string, string[]>()
const evidenceByRelation = new Map<string, string[]>()
for (const item of snapshot.evidence) {
if (item.entityId) {
evidenceByEntity.set(item.entityId, [
...(evidenceByEntity.get(item.entityId) ?? []),
item.id
])
}
if (item.relationId) {
evidenceByRelation.set(item.relationId, [
...(evidenceByRelation.get(item.relationId) ?? []),
item.id
])
}
}
const bindingsByLibraryId = new Map(
service.database.externalStore
.listBindings()
.map((binding) => [binding.knowledgeBaseId, binding])
)
return {
libraries: snapshot.libraries.map((library) => ({
kind: bindingsByLibraryId.has(library.id) ? 'external' : 'local',
external: bindingsByLibraryId.get(library.id),
id: library.id,
name: library.name,
description: library.description ?? '',
storageMode: library.storageMode,
graphEnabled: library.graphEnabled,
graphStrategy: library.graphStrategy,
sourceCount: library.sourceCount,
documentCount: library.documentCount,
indexedDocumentCount: library.indexedDocumentCount,
processingDocumentCount: library.processingDocumentCount,
failedDocumentCount: library.failedDocumentCount,
retrievalSettings: library.retrievalSettings,
chunkingSettings: library.chunkingSettings,
chunkingRebuildRequired: library.chunkingRebuildRequired,
ontologySettings: library.ontologySettings,
ontologyRebuildRequired: library.ontologyRebuildRequired,
updatedAt: library.updatedAt
})),
selectedLibraryId: activeLibraryId,
sources: snapshot.sources.map((source) => ({
id: source.id,
libraryId: source.knowledgeBaseId,
name: source.displayName,
kind: source.type,
location: source.location,
status:
source.status === 'pending'
? 'queued'
: source.status === 'indexing'
? 'syncing'
: source.status === 'error'
? 'failed'
: source.status,
progress: source.progress,
documentCount: source.documentCount,
lastSyncedAt: source.lastSyncedAt,
error: source.lastError
})),
documents: snapshot.documents.map((document) => ({
id: document.id,
libraryId: document.knowledgeBaseId,
sourceId: document.sourceId,
name: document.title,
resultId: typeof document.metadata.parsedResultId === 'string' ? document.metadata.parsedResultId : undefined,
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,
updatedAt: document.updatedAt,
error: document.error
})),
graphNodes: snapshot.entities.map((entity, index) => ({
id: entity.id,
label: entity.name,
type: entity.type,
description: entity.description,
aliases: entity.aliases,
x:
typeof entity.properties.x === 'number'
? entity.properties.x
: 120 + (index % 5) * 150,
y:
typeof entity.properties.y === 'number'
? entity.properties.y
: 100 + Math.floor(index / 5) * 120,
evidenceIds: evidenceByEntity.get(entity.id)
})),
graphRelations: snapshot.relations.map((relation) => ({
id: relation.id,
sourceId: relation.sourceEntityId,
targetId: relation.targetEntityId,
type: relation.type,
description: relation.label,
evidenceIds: evidenceByRelation.get(relation.id)
})),
evidence: snapshot.evidence.map((item) => ({
id: item.id,
documentId: item.documentId,
documentName:
documentsById.get(item.documentId)?.title ?? '未知文档',
excerpt: item.quote ?? '',
location: item.location
})),
tasks: snapshot.tasks.map((task) => knowledgeTaskItemSchema.parse(task))
}
}
export function registerKnowledgeIpcHandlers(
registerHandler: typeof ipcMain.handle,
window: BrowserWindow,
knowledgeService: KnowledgeService,
settingsStore: RuntimeSettingsStore
): void {
registerHandler(ipcChannels.externalInstancesList, (event) => {assertTrustedSender(event,window);return knowledgeService.external.listInstances()})
registerHandler(ipcChannels.externalInstancesSave, (event,input:unknown) => {assertTrustedSender(event,window);return knowledgeService.external.saveInstance(externalKnowledgeInstanceSaveInputSchema.parse(input))})
registerHandler(ipcChannels.externalInstancesTest, (event,input:unknown) => {assertTrustedSender(event,window);return knowledgeService.external.testInstance(externalKnowledgeInstanceInputSchema.parse(input).instanceId)})
registerHandler(ipcChannels.externalInstancesSetEnabled, (event,input:unknown) => {assertTrustedSender(event,window);const value=externalKnowledgeInstanceEnabledInputSchema.parse(input);return knowledgeService.external.setEnabled(value.instanceId,value.enabled)})
registerHandler(ipcChannels.externalInstancesDelete, (event,input:unknown) => {assertTrustedSender(event,window);return knowledgeService.external.deleteInstance(externalKnowledgeInstanceInputSchema.parse(input).instanceId)})
registerHandler(ipcChannels.externalCatalogList, (event,input:unknown) => {assertTrustedSender(event,window);return knowledgeService.external.listCatalog(externalKnowledgeCatalogListInputSchema.parse(input))})
registerHandler(ipcChannels.externalCatalogGet, (event,input:unknown) => {assertTrustedSender(event,window);return knowledgeService.external.getCatalog(externalKnowledgeCatalogGetInputSchema.parse(input))})
registerHandler(ipcChannels.externalRetrievalTest, (event,input:unknown) => {assertTrustedSender(event,window);return knowledgeService.external.testRetrieval(externalKnowledgeBindingTestInputSchema.parse(input))})
registerHandler(ipcChannels.externalBindingsCreate, async (event,input:unknown) => {assertTrustedSender(event,window);await knowledgeService.external.saveBinding(externalKnowledgeBindingSaveInputSchema.parse(input));return getKnowledgeSnapshot(knowledgeService)})
registerHandler(ipcChannels.externalBindingsUpdate, async (event,input:unknown) => {assertTrustedSender(event,window);await knowledgeService.external.saveBinding(externalKnowledgeBindingUpdateInputSchema.parse(input));return getKnowledgeSnapshot(knowledgeService)})
registerHandler(ipcChannels.knowledgeSnapshot, (event, input: unknown) => {
assertTrustedSender(event, window)
const libraryId =
input === undefined ? undefined : knowledgeIdSchema.parse(input)
return getKnowledgeSnapshot(knowledgeService, libraryId)
})
registerHandler(
ipcChannels.knowledgeCreateLibrary,
(event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeCreateSchema.parse(input)
const library = knowledgeService.createLibrary(value)
const created = getKnowledgeSnapshot(
knowledgeService,
library.id
).libraries.find((item) => item.id === library.id)
if (!created) {
throw new Error('知识库创建失败')
}
return created
}
)
registerHandler(
ipcChannels.knowledgeDeleteLibrary,
async (event, input: unknown) => {
assertTrustedSender(event, window)
await knowledgeService.deleteLibrary(knowledgeIdSchema.parse(input))
}
)
registerHandler(
ipcChannels.knowledgeUpdateLibrary,
(event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeUpdateLibrarySchema.parse(input)
knowledgeService.database.updateKnowledgeBase(value.libraryId, {
name: value.name,
description: value.description,
graphEnabled: value.graphEnabled,
graphStrategy: value.graphStrategy
})
}
)
registerHandler(
ipcChannels.knowledgeReextractGraph,
async (event, input: unknown) => {
assertTrustedSender(event, window)
return knowledgeService.reextractGraph(knowledgeIdSchema.parse(input))
}
)
registerHandler(
ipcChannels.knowledgeSelectFiles,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const selection = knowledgeSelectionSchema.parse(input)
const result = await dialog.showOpenDialog(window, {
properties: ['openFile', 'multiSelections'],
filters: [
{
name: '支持的知识文档',
extensions: supportedDocumentExtensions.map((extension) =>
extension.slice(1)
)
}
]
})
if (!result.canceled) {
await knowledgeService.importPaths(
selection.libraryId,
result.filePaths,
selection.graphStrategy
)
}
}
)
registerHandler(
ipcChannels.knowledgeSelectDirectory,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const selection = knowledgeSelectionSchema.parse(input)
const result = await dialog.showOpenDialog(window, {
properties: ['openDirectory']
})
if (!result.canceled && result.filePaths[0]) {
await knowledgeService.importPaths(
selection.libraryId,
[result.filePaths[0]],
selection.graphStrategy
)
}
}
)
registerHandler(
ipcChannels.knowledgeImportPaths,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeImportPathsSchema.parse(input)
await knowledgeService.importPaths(
value.libraryId,
value.paths,
value.graphStrategy
)
}
)
registerHandler(
ipcChannels.knowledgeImportUrl,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeUrlImportSchema.parse(input)
await knowledgeService.importUrl(
value.libraryId,
value.url,
new AbortController().signal,
undefined,
value.graphStrategy
)
}
)
for (const [channel, action] of [
[
ipcChannels.knowledgeSyncSource,
(id: string) => knowledgeService.syncSource(id)
],
[
ipcChannels.knowledgePauseSource,
(id: string) => knowledgeService.pauseSource(id)
],
[
ipcChannels.knowledgeRetrySource,
(id: string) => knowledgeService.retrySource(id)
],
[
ipcChannels.knowledgeRemoveSource,
(id: string) => knowledgeService.removeSource(id)
]
] as const) {
registerHandler(channel, async (event, input: unknown) => {
assertTrustedSender(event, window)
await action(knowledgeIdSchema.parse(input))
})
}
registerHandler(ipcChannels.knowledgeSearch, async (event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeSearchSchema.parse(input)
if (value.libraryIds.length === 0) {
return []
}
const availableLibraries =
knowledgeService.database.listKnowledgeBases(100)
const libraries = [...new Set(value.libraryIds)]
const names = new Map(
availableLibraries.map((library) => [library.id, library.name])
)
const results = (
await knowledgeService.retrieveMany(
libraries,
value.query
)
)
const failures = results.flatMap(item => item.response.diagnostics.failure ? [item.response.diagnostics.failure] : [])
if (failures.length === results.length) throw new Error(failures.join('; '))
const references = results.flatMap(({ knowledgeBaseId, response }) => response.results.map(result => ({
...toKnowledgeReference(result, names.get(knowledgeBaseId) ?? '知识库'),
...(failures.length ? { warnings: failures } : {})
})))
if (!references.length && failures.length) throw new Error(failures.join('; '))
return [...new Map(references.map(reference => [knowledgeReferenceKey(reference), reference])).values()]
.sort((left, right) => left.rank - right.rank)
.slice(0, 8)
})
registerHandler(
ipcChannels.knowledgeRetrieve,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const response = await knowledgeService.retrieve(
knowledgeRetrieveInputSchema.parse(input)
)
return knowledgeRetrievalResponseSchema.parse(response)
}
)
registerHandler(
ipcChannels.knowledgeUpdateSettings,
(event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeSettingsUpdateInputSchema.parse(input)
knowledgeService.updateSettings(value)
const library = getKnowledgeSnapshot(
knowledgeService,
value.knowledgeBaseId
).libraries.find((item) => item.id === value.knowledgeBaseId)
if (!library) {
throw new Error('知识库不存在')
}
return library
}
)
registerHandler(
ipcChannels.knowledgeListChunks,
(event, input: unknown) => {
assertTrustedSender(event, window)
const page = knowledgeService.listChunks(
knowledgeChunksListInputSchema.parse(input)
)
return knowledgeChunkPageSchema.parse({
items: page.items.map((chunk) => ({
id: chunk.id,
ordinal: chunk.ordinal,
role: chunk.role,
parentChunkId: chunk.parentChunkId,
heading: chunk.heading,
locator: chunk.location,
characterCount: chunk.content.length,
enabled: chunk.enabled,
content: chunk.content,
manuallyEdited: chunk.manuallyEdited,
updatedAt: chunk.updatedAt
})),
page: page.page,
pageSize: page.pageSize,
totalItems: page.total
})
}
)
registerHandler(
ipcChannels.knowledgeUpdateChunk,
async (event, input: unknown) => {
assertTrustedSender(event, window)
await knowledgeService.updateChunk(
knowledgeChunkUpdateInputSchema.parse(input)
)
}
)
registerHandler(
ipcChannels.knowledgeDeleteChunk,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const deleted = await knowledgeService.deleteChunk(
knowledgeChunkDeleteInputSchema.parse(input)
)
if (!deleted) {
throw new Error('知识分块不存在')
}
}
)
registerHandler(
ipcChannels.knowledgeRebuildDocument,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeDocumentRebuildInputSchema.parse(input)
await knowledgeService.rebuildDocument(value)
return getKnowledgeSnapshot(
knowledgeService,
value.knowledgeBaseId
)
}
)
registerHandler(
ipcChannels.knowledgeRebuildLibrary,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeLibraryRebuildInputSchema.parse(input)
return knowledgeService.rebuildLibrary(value)
}
)
registerHandler(
ipcChannels.knowledgeCancelRebuild,
(event, input: unknown) => {
assertTrustedSender(event, window)
const knowledgeBaseId = knowledgeIdSchema.parse(input)
return knowledgeService.cancelLibraryRebuild(knowledgeBaseId)
}
)
registerHandler(
ipcChannels.knowledgeTaskCancel,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const { taskId } = knowledgeTaskActionInputSchema.parse(input)
return knowledgeService.cancelTask(taskId)
}
)
registerHandler(
ipcChannels.knowledgeTaskRetry,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const { taskId } = knowledgeTaskActionInputSchema.parse(input)
await knowledgeService.retryTask(taskId)
}
)
const embeddingConfiguration = async () => {
const settings = await settingsStore.getPublicSettings()
const connection = settings.embeddingConnections?.find(
(candidate) =>
candidate.id === settings.activeEmbeddingConnectionId
)
return connection?.kind === 'builtin'
? {
provider: 'builtin',
model: 'granite-embedding-97m-multilingual-r2',
credentialConfigured: false
}
: {
provider: 'openai-compatible',
model:
connection?.modelName ??
settings.knowledgeEmbeddingModel,
endpoint:
connection?.baseUrl ??
settings.knowledgeEmbeddingBaseUrl,
credentialConfigured:
connection?.apiKeyConfigured ??
settings.knowledgeEmbeddingApiKeyConfigured
}
}
registerHandler(
ipcChannels.knowledgeEmbeddingIndexGet,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const { knowledgeBaseId } =
knowledgeEmbeddingIndexRequestSchema.parse(input)
const settings = await settingsStore.getResolvedSettings()
return knowledgeEmbeddingIndexSnapshotSchema.parse(
await knowledgeService.getEmbeddingIndexSnapshot(
knowledgeBaseId,
settings.knowledgeEmbeddingEnabled
? await embeddingConfiguration()
: undefined
)
)
}
)
registerHandler(
ipcChannels.knowledgeEmbeddingIndexRebuild,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const { knowledgeBaseId } =
knowledgeEmbeddingIndexRequestSchema.parse(input)
const snapshot = await knowledgeService.rebuildEmbeddingIndex(
knowledgeBaseId,
await embeddingConfiguration()
)
return knowledgeEmbeddingIndexSnapshotSchema.parse(snapshot)
}
)
registerHandler(
ipcChannels.knowledgeEmbeddingIndexCancel,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const { knowledgeBaseId, jobId } =
knowledgeEmbeddingIndexCancelRequestSchema.parse(input)
return knowledgeService.cancelEmbeddingIndex(
knowledgeBaseId,
jobId
)
}
)
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) => {
assertTrustedSender(event, window)
const value = knowledgeReferenceContextInputSchema.parse(input)
const reference = knowledgeService.getReferenceContext(value)
if (!reference) {
throw new Error('引用上下文不存在或已停用')
}
const fullContext = reference.contextChunks
.map((chunk) => chunk.content)
.join('\n\n')
return knowledgeReferenceContextSchema.parse({
knowledgeBaseId: value.knowledgeBaseId,
documentId: value.documentId,
chunkId: value.chunkId,
documentTitle: reference.document.title,
sourceDisplayName: reference.source.displayName,
locator: reference.chunk.location,
matchedContent: reference.chunk.content.slice(0, 48_000),
contextContent: fullContext.slice(0, 48_000),
contextChunkIds: reference.contextChunks.map((chunk) => chunk.id),
truncated:
reference.chunk.content.length > 48_000 ||
fullContext.length > 48_000
})
}
)
registerHandler(
ipcChannels.knowledgeOpenReferenceSource,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeReferenceOpenInputSchema.parse(input)
const reference = knowledgeService.getReferenceContext(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)
const targetStat = await stat(targetPath)
if (!targetStat.isFile() && !targetStat.isDirectory()) {
throw new Error('引用来源不是可打开的文件或目录')
}
const openError = await shell.openPath(targetPath)
if (openError) {
throw new Error('无法打开引用来源')
}
}
)
registerHandler(
ipcChannels.knowledgeCreateEntity,
(event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeCreateEntitySchema.parse(input)
knowledgeService.database.createEntity({
knowledgeBaseId: value.libraryId,
name: value.input.label,
type: value.input.type,
description: value.input.description || undefined,
aliases: value.input.aliases,
locked: true
})
}
)
registerHandler(
ipcChannels.knowledgeUpdateEntity,
(event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeEntityPayloadSchema.parse(input)
knowledgeService.database.updateEntity(value.entityId, {
name: value.update.label,
type: value.update.type,
description: value.update.description || null,
aliases: value.update.aliases,
locked: true
})
}
)
registerHandler(
ipcChannels.knowledgeMoveEntity,
(event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeMoveEntitySchema.parse(input)
const entity = knowledgeService.database.getEntity(value.entityId)
if (!entity) {
throw new Error('图谱实体不存在')
}
knowledgeService.database.updateEntity(entity.id, {
properties: {
...entity.properties,
x: value.position.x,
y: value.position.y
}
})
}
)
registerHandler(
ipcChannels.knowledgeDeleteEntity,
(event, input: unknown) => {
assertTrustedSender(event, window)
knowledgeService.database.deleteEntity(knowledgeIdSchema.parse(input))
}
)
registerHandler(
ipcChannels.knowledgeMergeEntities,
(event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeMergeSchema.parse(input)
knowledgeService.database.mergeEntities(
value.targetEntityId,
value.sourceEntityId
)
}
)
registerHandler(
ipcChannels.knowledgeCreateRelation,
(event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeCreateRelationSchema.parse(input)
knowledgeService.database.createRelation({
knowledgeBaseId: value.libraryId,
sourceEntityId: value.input.sourceId,
targetEntityId: value.input.targetId,
type: value.input.type,
label: value.input.description || undefined,
locked: true
})
}
)
registerHandler(
ipcChannels.knowledgeUpdateRelation,
(event, input: unknown) => {
assertTrustedSender(event, window)
const value = knowledgeUpdateRelationSchema.parse(input)
knowledgeService.database.updateRelation(value.relationId, {
sourceEntityId: value.input.sourceId,
targetEntityId: value.input.targetId,
type: value.input.type,
label: value.input.description || null,
locked: true
})
}
)
registerHandler(
ipcChannels.knowledgeDeleteRelation,
(event, input: unknown) => {
assertTrustedSender(event, window)
knowledgeService.database.deleteRelation(knowledgeIdSchema.parse(input))
}
)
}