feat: add external knowledge retrieval and cross-project activity

Connect Dify, FastGPT and RAGFlow through read-only bindings, encrypted local credentials, shared retrieval and persistent citations. Configuration and removal affect GoodBuddy only.

Show active conversations across projects, retain older live work, and improve project dialog scrolling. Queue concurrent questions and route managed SSH OpenCode questions through the Agent. Preserve recoverable read/search errors and partial search results.

Document the Electron UI automation standard, real model and external knowledge validation, and remaining acceptance limits.

Validation: earlier full regression passed 4081 tests with 66 skipped; subsequent focused remote-question checks and real Linux evidence are recorded in feature progress. Precommit typecheck and lint passed; latest project dialog and shared UI suites passed 58 tests. The full suite was not rerun for the final dialog changes.

Release note: Use external knowledge in conversations, find active work across projects, and answer concurrent local or managed SSH OpenCode questions. Managed SSH questions require the updated Agent adapter.
This commit is contained in:
mesalogo
2026-09-13 13:10:01 +08:00
parent b84250f62a
commit 3b560c9a5e
102 changed files with 6716 additions and 1484 deletions
+4 -1
View File
@@ -1,4 +1,5 @@
import { z } from 'zod'
import { externalKnowledgeLocatorSchema } from './external-knowledge-contracts'
import { agentRuntimeSelectionSchema } from './runtime-selection-contracts'
import { sshHostIdSchema } from './ssh-host-contracts'
@@ -368,7 +369,9 @@ export const conversationMessageSchema = z
.object({
libraryId: assistantIdSchema,
libraryName: z.string().max(200),
documentId: assistantIdSchema,
documentId: assistantIdSchema.optional(),
external: externalKnowledgeLocatorSchema.optional(),
warnings: z.array(z.string().max(500)).max(20).optional(),
chunkId: assistantIdSchema.optional(),
documentName: z.string().max(500),
sourceName: z.string().max(500),
+21 -1
View File
@@ -1308,6 +1308,7 @@ export type AgentEvent =
requestId: string
type: 'question'
questionId: string
childTaskId?: string
questions: Array<{
header: string
question: string
@@ -1319,6 +1320,11 @@ export type AgentEvent =
custom: boolean
}>
}
| {
requestId: string
type: 'question-resolved'
questionId: string
}
| {
requestId: string
type: 'artifact'
@@ -1633,6 +1639,8 @@ export const knowledgeRelationInputSchema = z
.strict()
export type KnowledgeLibrary = z.infer<typeof knowledgeCreateSchema> & {
kind?: 'local' | 'external'
external?: import('./external-knowledge-contracts').ExternalKnowledgeBinding
id: string
sourceCount: number
documentCount: number
@@ -1717,9 +1725,11 @@ export type KnowledgeSnapshot = {
}
export type KnowledgeSearchReference = {
warnings?: string[]
external?: import('./external-knowledge-contracts').ExternalKnowledgeLocator
libraryId: string
libraryName: string
documentId: string
documentId?: string
chunkId?: string
documentName: string
sourceName: string
@@ -2274,6 +2284,16 @@ export type DesktopApi = {
onTodoStatusChanged: (listener: () => void) => () => void
}
knowledge: {
externalInstancesList: () => Promise<import('./external-knowledge-contracts').ExternalKnowledgeInstanceSummary[]>
externalInstancesSave: (input: import('./external-knowledge-contracts').ExternalKnowledgeInstanceSaveInput) => Promise<import('./external-knowledge-contracts').ExternalKnowledgeInstanceSummary>
externalInstancesTest: (input: {instanceId:string}) => Promise<import('./external-knowledge-contracts').ExternalKnowledgeInstanceSummary>
externalInstancesSetEnabled: (input: {instanceId:string;enabled:boolean}) => Promise<import('./external-knowledge-contracts').ExternalKnowledgeInstanceSummary>
externalInstancesDelete: (input: {instanceId:string}) => Promise<void>
externalCatalogList: (input: {instanceId:string;page?:number;pageSize?:number;parentId?:string|null;search?:string}) => Promise<import('./external-knowledge-contracts').ExternalKnowledgeCatalogPage>
externalCatalogGet: (input: {instanceId:string;remoteKnowledgeBaseId:string}) => Promise<import('./external-knowledge-contracts').ExternalKnowledgeCatalogItem>
externalBindingsCreate: (input: import('./external-knowledge-contracts').ExternalKnowledgeBindingSaveInput) => Promise<KnowledgeSnapshot>
externalBindingsUpdate: (input: import('./external-knowledge-contracts').ExternalKnowledgeBindingSaveInput & {knowledgeBaseId:string}) => Promise<KnowledgeSnapshot>
externalRetrievalTest: (input: import('./external-knowledge-contracts').ExternalKnowledgeBindingTestInput) => Promise<import('./external-knowledge-contracts').ExternalKnowledgeTestResult>
getSnapshot: (libraryId?: string) => Promise<KnowledgeSnapshot>
createLibrary: (
input: z.infer<typeof knowledgeCreateSchema>
@@ -0,0 +1,52 @@
import { z } from 'zod'
const id = z.string().trim().min(1).max(512)
export const externalKnowledgeProviderSchema = z.enum(['dify', 'fastgpt', 'ragflow'])
export type ExternalKnowledgeProvider = z.infer<typeof externalKnowledgeProviderSchema>
export const externalKnowledgeCommonConfigSchema = z.object({
resultLimit: z.number().int().min(1).max(20).default(6),
requestTimeoutMs: z.number().int().min(1000).max(60000).default(15000),
maxSnippetCharacters: z.number().int().min(100).max(8000).default(4000)
}).strict()
export type ExternalKnowledgeCommonConfig = z.infer<typeof externalKnowledgeCommonConfigSchema>
const retrievalModel = z.object({
search_method: z.enum(['keyword_search', 'semantic_search', 'full_text_search', 'hybrid_search']),
reranking_enable: z.boolean(), top_k: z.number().int().min(1).max(20),
score_threshold_enabled: z.boolean(), score_threshold: z.number().min(0).max(1).optional(),
reranking_mode: z.enum(['reranking_model', 'weighted_score']).optional(),
reranking_model: z.object({reranking_provider_name: id, reranking_model_name: id}).strict().optional(),
weights: z.object({weight_type: z.literal('customized'), vector_setting: z.object({vector_weight: z.number().min(0).max(1), embedding_provider_name: id, embedding_model_name: id}).strict(), keyword_setting: z.object({keyword_weight: z.number().min(0).max(1)}).strict()}).strict().optional()
}).strict().superRefine((value, ctx) => {
if (value.score_threshold_enabled && value.score_threshold === undefined) ctx.addIssue({code:'custom',message:'Score threshold is required'})
if (value.reranking_enable && (!value.reranking_mode || (value.reranking_mode === 'reranking_model' ? !value.reranking_model : !value.weights))) ctx.addIssue({code:'custom',message:'Complete reranking configuration is required'})
})
export const externalKnowledgeProviderConfigSchema = z.union([
z.object({provider:z.literal('dify'),useDatasetDefaults:z.literal(true)}).strict(),
z.object({provider:z.literal('dify'),useDatasetDefaults:z.literal(false),retrievalModel}).strict(),
z.object({provider:z.literal('fastgpt'),searchMode:z.enum(['embedding','fullTextRecall','mixedRecall']),tokenLimit:z.number().int().min(1).max(30000),similarity:z.number().min(0).max(1),usingRerank:z.boolean()}).strict(),
z.object({provider:z.literal('ragflow'),similarityThreshold:z.number().min(0).max(1),vectorSimilarityWeight:z.number().min(0).max(1),knnTopK:z.number().int().min(1).max(2048),useKg:z.boolean(),includeKnowledgeCompilation:z.boolean()}).strict()
])
export type ExternalKnowledgeProviderConfig = z.infer<typeof externalKnowledgeProviderConfigSchema>
export const externalKnowledgeInstanceSaveInputSchema = z.object({
id:id.optional(),name:z.string().trim().min(1).max(512),provider:externalKnowledgeProviderSchema,
baseUrl:z.string().trim().url().max(8192).refine((value)=>{const url=new URL(value);return ['http:','https:'].includes(url.protocol)&&!url.username&&!url.password&&!url.search&&!url.hash},'Invalid service URL'),
enabled:z.boolean(),credential:z.discriminatedUnion('action',[z.object({action:z.literal('keep')}).strict(),z.object({action:z.literal('clear')}).strict(),z.object({action:z.literal('replace'),value:z.string().trim().min(1).max(16384)}).strict()])
}).strict()
export type ExternalKnowledgeInstanceSaveInput = z.infer<typeof externalKnowledgeInstanceSaveInputSchema>
export const externalKnowledgeInstanceInputSchema = z.object({instanceId:id}).strict()
export const externalKnowledgeInstanceEnabledInputSchema = externalKnowledgeInstanceInputSchema.extend({enabled:z.boolean()})
export const externalKnowledgeCatalogListInputSchema = externalKnowledgeInstanceInputSchema.extend({page:z.number().int().min(1).max(500).optional(),pageSize:z.number().int().min(1).max(100).optional(),parentId:id.nullable().optional(),search:z.string().trim().max(512).optional()})
export const externalKnowledgeCatalogGetInputSchema = externalKnowledgeInstanceInputSchema.extend({remoteKnowledgeBaseId:id})
export const externalKnowledgeBindingTestInputSchema = externalKnowledgeCatalogGetInputSchema.extend({commonConfig:externalKnowledgeCommonConfigSchema,providerConfig:externalKnowledgeProviderConfigSchema,testQuery:z.string().trim().min(1).max(4000)})
export const externalKnowledgeBindingSaveInputSchema = externalKnowledgeBindingTestInputSchema.extend({name:z.string().trim().min(1).max(512),description:z.string().max(4000).optional(),remoteName:z.string().trim().min(1).max(512)})
export const externalKnowledgeBindingUpdateInputSchema = externalKnowledgeBindingSaveInputSchema.extend({knowledgeBaseId:id})
export type ExternalKnowledgeBindingTestInput = z.infer<typeof externalKnowledgeBindingTestInputSchema>
export type ExternalKnowledgeBindingSaveInput = z.infer<typeof externalKnowledgeBindingSaveInputSchema>
export type ExternalKnowledgeInstanceSummary = {id:string;name:string;provider:ExternalKnowledgeProvider;baseUrl:string;enabled:boolean;credentialStatus:'configured'|'missing'|'unavailable';probeStatus:'untested'|'catalog-ready'|'healthy'|'list-restricted'|'auth-failed'|'unreachable'|'failed';bindingCount:number;lastTestedAt?:string;lastErrorCode?:string}
export type ExternalKnowledgeCatalogItem = {id:string;name:string;description?:string;kind?:'dataset'|'folder';graphEnabled?:boolean;knowledgeCompilationEnabled?:boolean}
export type ExternalKnowledgeCatalogPage = {items:ExternalKnowledgeCatalogItem[];total?:number;hasMore:boolean}
export type ExternalKnowledgeRemoteResult = {documentTitle:string;sourceDisplayName:string;snippet:string;providerScore?:number;providerScores?:{type:string;value:number;index?:number}[];remoteDocumentId?:string;remoteChunkId?:string;location?:string;sourceUrl?:string}
export type ExternalKnowledgeTestResult = {results:ExternalKnowledgeRemoteResult[];durationMs:number}
export type ExternalKnowledgeBinding = {knowledgeBaseId:string;instanceId:string;provider:ExternalKnowledgeProvider;remoteKnowledgeBaseId:string;remoteName:string;commonConfig:ExternalKnowledgeCommonConfig;providerConfig:ExternalKnowledgeProviderConfig;lastVerifiedAt:string}
export const externalKnowledgeLocatorSchema = z.object({kind:z.literal('external'),provider:externalKnowledgeProviderSchema,instanceId:id,remoteKnowledgeBaseId:id,remoteDocumentId:id.optional(),remoteChunkId:id.optional(),providerScore:z.number().finite().optional(),providerScores:z.array(z.object({type:z.string().max(128),value:z.number().finite(),index:z.number().int().optional()}).strict()).max(20).optional(),location:z.string().max(8192).optional(),sourceUrl:z.string().max(8192).optional()}).strict()
export type ExternalKnowledgeLocator = z.infer<typeof externalKnowledgeLocatorSchema>
+10
View File
@@ -1,4 +1,14 @@
export const ipcChannels = {
externalInstancesList: 'knowledge:external-instances:list',
externalInstancesSave: 'knowledge:external-instances:save',
externalInstancesTest: 'knowledge:external-instances:test',
externalInstancesSetEnabled: 'knowledge:external-instances:set-enabled',
externalInstancesDelete: 'knowledge:external-instances:delete',
externalCatalogList: 'knowledge:external-catalog:list',
externalCatalogGet: 'knowledge:external-catalog:get',
externalBindingsCreate: 'knowledge:external-bindings:create',
externalBindingsUpdate: 'knowledge:external-bindings:update',
externalRetrievalTest: 'knowledge:external-retrieval:test',
appInfo: 'app:get-info',
appShow: 'app:show',
appHide: 'app:hide',
+10 -6
View File
@@ -1,4 +1,5 @@
import { z } from 'zod'
import { externalKnowledgeLocatorSchema } from './external-knowledge-contracts'
import {
defaultKnowledgeOntologySettings,
knowledgeOntologySettingsSchema
@@ -148,20 +149,21 @@ const channelScoresSchema = z
export const knowledgeRetrievalResultSchema = z
.object({
knowledgeBaseId: idSchema,
documentId: idSchema,
sourceId: idSchema,
chunkId: idSchema,
documentId: idSchema.optional(),
sourceId: idSchema.optional(),
chunkId: idSchema.optional(),
external: externalKnowledgeLocatorSchema.optional(),
parentChunkId: idSchema.optional(),
documentTitle: z.string().max(512),
sourceDisplayName: z.string().max(512),
sourceType: z.enum(['file', 'directory', 'url']),
sourceType: z.enum(['file', 'directory', 'url']).optional(),
heading: z.string().max(512).optional(),
location: z.string().max(8_192).optional(),
snippet: z.string().max(8_000),
relevance: z.number().finite().min(0).max(1),
relevance: z.number().finite().min(0).max(1).optional(),
rank: z.number().int().positive().max(20),
preRerankRank: optionalRankSchema,
channels: z.array(knowledgeRetrievalChannelSchema).min(1).max(4),
channels: z.array(knowledgeRetrievalChannelSchema).max(4),
scores: channelScoresSchema
})
.strict()
@@ -200,6 +202,8 @@ const channelTimingSchema = z
export const knowledgeRetrievalDiagnosticsSchema = z
.object({
external: z.object({provider:z.enum(['dify','fastgpt','ragflow']),instanceId:z.string(),remoteKnowledgeBaseId:z.string()}).strict().optional(),
failure: z.string().max(500).optional(),
requestedChannels: z.array(knowledgeRetrievalChannelSchema).max(4),
usedChannels: z.array(knowledgeRetrievalChannelSchema).max(4),
degradedChannels: z
+30
View File
@@ -0,0 +1,30 @@
import type { KnowledgeSearchReference } from './contracts'
import { stripKnowledgeHighlightTags } from './knowledge-text'
import type { KnowledgeRetrievalResponse } from './knowledge-contracts'
export function knowledgeReferenceKey(reference: KnowledgeSearchReference): string {
const external = reference.external
return JSON.stringify([
reference.libraryId, reference.documentId, reference.chunkId,
external?.provider, external?.instanceId, external?.remoteKnowledgeBaseId,
external?.remoteDocumentId, external?.remoteChunkId,
external?.location ?? reference.locator, external?.sourceUrl, reference.snippet
])
}
export function toKnowledgeReference(
result: KnowledgeRetrievalResponse['results'][number], libraryName: string
): KnowledgeSearchReference {
return {
libraryId: result.knowledgeBaseId, libraryName: libraryName.slice(0, 200),
documentId: result.documentId, chunkId: result.chunkId,
documentName: result.documentTitle.slice(0, 500), sourceName: result.sourceDisplayName.slice(0, 500),
locator: (result.external?.location ?? result.location)?.slice(0, 1000),
snippet: stripKnowledgeHighlightTags(result.snippet).slice(0, 16000),
rank: result.rank, external: result.external,
score: result.external ? undefined : result.scores.fusedScore,
lexicalRank: result.scores.ftsRank, vectorRank: result.scores.vectorRank,
graphRank: result.scores.graphRank, similarity: result.scores.vectorSimilarity,
retrievalChannels: result.channels
}
}
+1
View File
@@ -317,6 +317,7 @@ export const remoteSemanticTranscriptPageResultSchema = z
acknowledgedSequence: agentSequenceSchema,
state: remoteOwnedPromptStateSchema,
sessionId: agentIdentifierSchema.optional(),
pendingQuestions: z.array(jsonValueSchema).optional(),
hasMore: z.boolean()
})
.strict()
+22
View File
@@ -0,0 +1,22 @@
import { z } from 'zod'
import { agentQuestionResponseSchema } from './contracts'
export const remoteQuestionSchema = z.object({
id: z.string().min(1),
sessionID: z.string().min(1),
questions: z.array(z.object({
header: z.string(),
question: z.string(),
options: z.array(z.object({ label: z.string(), description: z.string() })),
multiple: z.boolean().default(false),
custom: z.boolean().default(true)
})).min(1),
tool: z.object({ messageID: z.string(), callID: z.string() }).optional()
})
export const remoteQuestionResponseSchema = agentQuestionResponseSchema.extend({
bindingId: z.string().min(1),
operationId: z.string().min(1)
}).strict()
export type RemoteQuestionResponse = z.infer<typeof remoteQuestionResponseSchema>