Files
goodbuddy/src/shared/knowledge-contracts.ts
T
mesalogo dd8289fdef 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: 知识库现按对话保存使用范围,准确区分可用、处理中和失败文档,并提供更简洁的创建、检索测试、来源打开、删除确认和失败重试流程。
2026-09-05 15:51:07 +08:00

400 lines
12 KiB
TypeScript

import { z } from 'zod'
import {
defaultKnowledgeOntologySettings,
knowledgeOntologySettingsSchema
} from './knowledge-ontology'
import {
rerankExecutionDiagnosticsSchema,
rerankModeSchema
} from './rerank-contracts'
const idSchema = z.string().trim().min(1).max(128)
const boundedTextSchema = (maximum: number) =>
z.string().min(1).max(maximum)
export const knowledgeRetrievalChannelSchema = z.enum([
'fts',
'cjk',
'vector',
'graph'
])
export type KnowledgeRetrievalChannel = z.infer<
typeof knowledgeRetrievalChannelSchema
>
export const knowledgeRetrievalSettingsSchema = z
.object({
version: z.literal(1).default(1),
topK: z.number().int().min(1).max(20).default(6),
minimumVectorSimilarity: z
.number()
.finite()
.min(-1)
.max(1)
.transform((value) => Math.max(0, value))
.default(0),
ftsWeight: z.number().finite().min(0).max(2).default(1),
vectorWeight: z.number().finite().min(0).max(2).default(1),
graphWeight: z.number().finite().min(0).max(2).default(0.8),
candidateMultiplier: z.number().int().min(2).max(10).default(4),
contextMaxCharacters: z.number().int().min(2_000).max(48_000).default(16_000),
adjacentChunkCount: z.number().int().min(0).max(2).default(0),
localRerankEnabled: z.boolean().default(false),
rerankMode: rerankModeSchema.default('none')
})
.strict()
.superRefine((value, context) => {
if (
value.ftsWeight === 0 &&
value.vectorWeight === 0 &&
value.graphWeight === 0
) {
context.addIssue({
code: 'custom',
message: 'at least one retrieval channel weight must be greater than zero'
})
}
})
.transform((value) => {
const rerankMode =
value.rerankMode === 'none' && value.localRerankEnabled
? 'local'
: value.rerankMode
return {
...value,
rerankMode,
localRerankEnabled: rerankMode !== 'none'
}
})
export type KnowledgeRetrievalSettings = z.infer<
typeof knowledgeRetrievalSettingsSchema
>
export const defaultKnowledgeRetrievalSettings =
knowledgeRetrievalSettingsSchema.parse({})
export const knowledgeChunkingModeSchema = z.enum([
'fixed',
'structure',
'parent-child'
])
export const knowledgeChunkingSettingsSchema = z
.object({
version: z.literal(1).default(1),
mode: knowledgeChunkingModeSchema.default('structure'),
targetCharacters: z.number().int().min(400).max(8_000).default(1_600),
overlapCharacters: z.number().int().min(0).max(3_200).default(160),
parentCharacters: z.number().int().min(1_600).max(16_000).default(4_800),
childCharacters: z.number().int().min(300).max(4_000).default(900),
contextualIndexingEnabled: z.boolean().default(false)
})
.strict()
.superRefine((value, context) => {
if (value.overlapCharacters > value.targetCharacters * 0.4) {
context.addIssue({
code: 'custom',
path: ['overlapCharacters'],
message: 'overlapCharacters must not exceed 40% of targetCharacters'
})
}
if (value.childCharacters > value.parentCharacters) {
context.addIssue({
code: 'custom',
path: ['childCharacters'],
message: 'childCharacters must not exceed parentCharacters'
})
}
})
export type KnowledgeChunkingSettings = z.infer<
typeof knowledgeChunkingSettingsSchema
>
export const defaultKnowledgeChunkingSettings =
knowledgeChunkingSettingsSchema.parse({})
export const knowledgeChunkRoleSchema = z.enum([
'standalone',
'parent',
'child'
])
export type KnowledgeChunkRole = z.infer<typeof knowledgeChunkRoleSchema>
export const knowledgeRetrieveInputSchema = z
.object({
knowledgeBaseId: idSchema,
query: boundedTextSchema(4_000),
settings: knowledgeRetrievalSettingsSchema.optional()
})
.strict()
export type KnowledgeRetrieveInput = z.infer<
typeof knowledgeRetrieveInputSchema
>
const optionalRankSchema = z.number().int().positive().max(1_000_000).optional()
const channelScoresSchema = z
.object({
ftsRank: optionalRankSchema,
cjkRank: optionalRankSchema,
vectorRank: optionalRankSchema,
graphRank: optionalRankSchema,
vectorSimilarity: z.number().finite().min(-1).max(1).optional(),
fusedScore: z.number().finite().nonnegative(),
phraseMatch: z.boolean().optional(),
tokenCoverage: z.number().finite().min(0).max(1).optional(),
duplicatePenalty: z.number().finite().min(0).max(1).optional(),
rerankScore: z.number().finite().min(0).max(1).optional()
})
.strict()
export const knowledgeRetrievalResultSchema = z
.object({
knowledgeBaseId: idSchema,
documentId: idSchema,
sourceId: idSchema,
chunkId: idSchema,
parentChunkId: idSchema.optional(),
documentTitle: z.string().max(512),
sourceDisplayName: z.string().max(512),
sourceType: z.enum(['file', 'directory', 'url']),
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),
rank: z.number().int().positive().max(20),
preRerankRank: optionalRankSchema,
channels: z.array(knowledgeRetrievalChannelSchema).min(1).max(4),
scores: channelScoresSchema
})
.strict()
export type KnowledgeRetrievalResult = z.infer<
typeof knowledgeRetrievalResultSchema
>
export const knowledgeContextGroupSchema = z
.object({
resultChunkId: idSchema,
chunkIds: z.array(idSchema).min(1).max(20),
documentId: idSchema,
content: z.string().max(48_000),
characterCount: z.number().int().nonnegative().max(48_000),
truncated: z.boolean()
})
.strict()
export type KnowledgeContextGroup = z.infer<typeof knowledgeContextGroupSchema>
const channelCountSchema = z
.object({
fts: z.number().int().nonnegative().optional(),
cjk: z.number().int().nonnegative().optional(),
vector: z.number().int().nonnegative().optional(),
graph: z.number().int().nonnegative().optional()
})
.strict()
const channelTimingSchema = z
.object({
fts: z.number().int().nonnegative().optional(),
cjk: z.number().int().nonnegative().optional(),
vector: z.number().int().nonnegative().optional(),
graph: z.number().int().nonnegative().optional()
})
.strict()
export const knowledgeRetrievalDiagnosticsSchema = z
.object({
requestedChannels: z.array(knowledgeRetrievalChannelSchema).max(4),
usedChannels: z.array(knowledgeRetrievalChannelSchema).max(4),
degradedChannels: z
.array(
z
.object({
channel: knowledgeRetrievalChannelSchema,
reason: z.string().trim().min(1).max(500)
})
.strict()
)
.max(8),
candidateCounts: channelCountSchema,
channelDurationMs: channelTimingSchema,
vectorScannedCount: z.number().int().nonnegative(),
filteredByThresholdCount: z.number().int().nonnegative(),
filteredByBudgetCount: z.number().int().nonnegative(),
rerank: rerankExecutionDiagnosticsSchema.default({
requested: 'none',
used: 'none',
status: 'skipped',
candidateCount: 0,
durationMs: 0
})
})
.strict()
export const knowledgeRetrievalResponseSchema = z
.object({
query: boundedTextSchema(4_000),
durationMs: z.number().int().nonnegative(),
settings: knowledgeRetrievalSettingsSchema,
diagnostics: knowledgeRetrievalDiagnosticsSchema,
results: z.array(knowledgeRetrievalResultSchema).max(20),
context: z
.object({
characterCount: z.number().int().nonnegative().max(48_000),
truncated: z.boolean(),
groups: z.array(knowledgeContextGroupSchema).max(20)
})
.strict()
})
.strict()
export type KnowledgeRetrievalResponse = z.infer<
typeof knowledgeRetrievalResponseSchema
>
export const knowledgeSettingsUpdateInputSchema = z
.object({
knowledgeBaseId: idSchema,
retrieval: knowledgeRetrievalSettingsSchema.optional(),
chunking: knowledgeChunkingSettingsSchema.optional(),
ontology: knowledgeOntologySettingsSchema.optional()
})
.strict()
.refine((value) =>
value.retrieval !== undefined ||
value.chunking !== undefined ||
value.ontology !== undefined, {
message: 'at least one settings group is required'
})
export type KnowledgeSettingsUpdateInput = z.infer<
typeof knowledgeSettingsUpdateInputSchema
>
export const knowledgeChunksListInputSchema = z
.object({
knowledgeBaseId: idSchema,
documentId: idSchema,
page: z.number().int().min(1).max(1_000_000).default(1),
pageSize: z.number().int().min(1).max(200).default(50),
search: z.string().trim().max(1_000).optional()
})
.strict()
export type KnowledgeChunksListInput = z.infer<
typeof knowledgeChunksListInputSchema
>
export const knowledgeChunkUpdateInputSchema = z
.object({
knowledgeBaseId: idSchema,
documentId: idSchema,
chunkId: idSchema,
content: boundedTextSchema(2_000_000).optional(),
enabled: z.boolean().optional()
})
.strict()
.refine((value) => value.content !== undefined || value.enabled !== undefined, {
message: 'content or enabled is required'
})
export type KnowledgeChunkUpdateInput = z.infer<
typeof knowledgeChunkUpdateInputSchema
>
export const knowledgeChunkDeleteInputSchema = z
.object({
knowledgeBaseId: idSchema,
documentId: idSchema,
chunkId: idSchema
})
.strict()
export type KnowledgeChunkDeleteInput = z.infer<
typeof knowledgeChunkDeleteInputSchema
>
export const knowledgeDocumentRebuildInputSchema = z
.object({
knowledgeBaseId: idSchema,
documentId: idSchema
})
.strict()
export type KnowledgeDocumentRebuildInput = z.infer<
typeof knowledgeDocumentRebuildInputSchema
>
export const knowledgeDocumentOpenInputSchema = z
.object({
knowledgeBaseId: idSchema,
documentId: idSchema
})
.strict()
export type KnowledgeDocumentOpenInput = z.infer<
typeof knowledgeDocumentOpenInputSchema
>
export const knowledgeLibraryRebuildInputSchema = z
.object({
knowledgeBaseId: idSchema
})
.strict()
export type KnowledgeLibraryRebuildInput = z.infer<
typeof knowledgeLibraryRebuildInputSchema
>
export const knowledgeReferenceContextInputSchema =
knowledgeChunkDeleteInputSchema
export type KnowledgeReferenceContextInput = z.infer<
typeof knowledgeReferenceContextInputSchema
>
export const knowledgeReferenceOpenInputSchema =
knowledgeChunkDeleteInputSchema
export type KnowledgeReferenceOpenInput = z.infer<
typeof knowledgeReferenceOpenInputSchema
>
export const knowledgeManagedChunkSchema = z
.object({
id: idSchema,
ordinal: z.number().int().nonnegative(),
role: knowledgeChunkRoleSchema,
parentChunkId: idSchema.optional(),
heading: z.string().max(512).optional(),
locator: z.string().max(8_192).optional(),
characterCount: z.number().int().nonnegative().max(2_000_000),
enabled: z.boolean(),
content: z.string().max(2_000_000),
manuallyEdited: z.boolean(),
updatedAt: z.string().datetime().optional()
})
.strict()
export type KnowledgeManagedChunk = z.infer<
typeof knowledgeManagedChunkSchema
>
export const knowledgeChunkPageSchema = z
.object({
items: z.array(knowledgeManagedChunkSchema).max(200),
page: z.number().int().min(1).max(1_000_000),
pageSize: z.number().int().min(1).max(200),
totalItems: z.number().int().nonnegative()
})
.strict()
export type KnowledgeChunkPage = z.infer<typeof knowledgeChunkPageSchema>
export const knowledgeReferenceContextSchema = z
.object({
knowledgeBaseId: idSchema,
documentId: idSchema,
chunkId: idSchema,
documentTitle: z.string().max(512),
sourceDisplayName: z.string().max(512),
locator: z.string().max(8_192).optional(),
matchedContent: z.string().max(48_000),
contextContent: z.string().max(48_000),
contextChunkIds: z.array(idSchema).max(5),
truncated: z.boolean()
})
.strict()
export type KnowledgeReferenceContext = z.infer<
typeof knowledgeReferenceContextSchema
>
export {
defaultKnowledgeOntologySettings,
knowledgeOntologySettingsSchema
}