Files
HomeAgent/internal/sdk/knowledge.go
JianFeeeee 5dd98d7a05 feat(knowledge): 目录批量导入 + 派生数据批量收口
## 能力缺口

导入只能一条条 Add(knowledge_create)。agent 拿到一份 200 页的
文档目录要调 200 次工具,且每次都得自己决定分类与名字。

新增工具 `knowledge_import_dir(dir, category?, include_media?, dry_run?, max_items?)`。
语义是**复制**不是引用:源文件删改不影响已导入的副本。
- 文本经 Write 整份写入 <知识根>/<分类>/<名>/content.md
- 媒体按 sha256 进媒体库(内容寻址天然去重),条目只存 digest 引用

## 目录约定(自动适配,不要求改造资料)

1. 含 content.md 的目录 ⇒ 整体作为一个条目(与 scanDir 既有语义一致,
   所以知识库自身目录能被原样再导入而不会被拆散)
2. 否则 .md/.txt 等文件各成一条,**目录路径即分类**

## 三个语义决策

- category 是**前缀叠加**(tech + 源结构),不替换:替换会丢掉源目录
  自身最有价值的层级信息
- 同名冲突**跳过并计数**,绝不覆盖:Write 对同名本就是覆盖语义
  (knowledge_create 靠它做更新),若直接调它,一次重导就会把手工
  补充的内容悄悄抹掉,而日志只写"导入完成"
- dry_run **默认 true**:批量写,agent 第一次试某目录应先看清会写什么

## 安全边界(批量操作,缺一道就可能读到不该读的)

- 必须绝对路径:agent 的 cwd 不受控,相对路径会静默导到别处
- 符号链接不跟随:否则一个软链就把知识根之外的文件导进来
- 拒绝把知识库自身当源(自导会无限自我复制)
- category 复用 normalizeName(与 Write 同一道闸,两处分叉就成了绕过)
- MaxItems 默认 500:防 agent 误传 "/" 把盘灌满

## ★ 批量导入暴露的既有 O(N²)

Write 每条末尾都调 flushDenseLocked,而 saveDenseCacheLocked 是
**全量序列化整个 items map 再重写整个文件**。按 512 维 float64 估,
单条约 10KB,导入 500 条累计要写约 1.4GB。

仓库里索引侧早已有 indexDirty 的「标脏+延迟收口」(实测 writeIndexLocked
6.7ms/次、占单条 Add 绝大部分),**稠密缓存却还是逐条全量重写** ——
同一类开销只修了一半。

照 indexDirty 的模式补 batchDepth:批量期只标脏,endBatch 收口一次。
用 defer 保证提前 return 也会收口 —— 否则这批向量会留成"标脏未写",
下次启动被当作缺失而全量重算。

## 判据:17 条 + 变异

安全边界做了 4 组变异验证(去符号链接拦截/去绝对路径要求/去自导检查/
content.md 目录不下钻)。

★ 判据第一版有两处自己骗自己,被变异抓出来:
1. 符号链接判据造的是**目录软链**,而 WalkDir 对目录软链本来就不下钻
   ⇒ 有无防护结果都一样,是假绿。改成**文件软链**后才真正判红。
2. 同名冲突判据里已有条目写成 "a/b"、源映射出的是 "b"(不同名),
   判据自己就错了 —— 修判据而不是改实现。

媒体路径用假 MediaPutter:验的是「调了 Put 且 digest 挂到条目上」,
媒体库自身的落盘去重是 media 包的判据,不该在这里重测。

全量 41 包绿。
2026-09-26 16:19:10 +08:00

93 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sdk
import (
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
// KnowledgeAPI 是内置插件使用的全量知识库接口。
//
// 为什么不把多模态加进 pubsdk.KnowledgeAPI:那会改动
// third_party/homeagent-sdk/sdk/ 的公开契约,而发布纪律要求
// `git diff main -- third_party/homeagent-sdk/sdk/` 恒为 0
// (改动它等于一次 major bump + 完整的外部发版流程)。
// internal/sdk 明确不受此约束("内部实现自由"),且这里本来就是
// "内核侧接口 = 公共接口 + 内置插件额外能力" 的既有范式
// (见 PluginSDK 遮蔽访问器:Settings/Memory/TextMemory/DocMemory…)。
// 结果是:内置插件(含子进程插件的 core handler)拿到多模态能力,
// 而外部 SDK 契约保持逐字不变。
type KnowledgeAPI interface {
pubsdk.KnowledgeAPI
// Stats 返回知识库的运行统计。
Stats() map[string]interface{}
// Remove 按名称删除一条知识。
Remove(name string) error
// SearchIn 在某个分类子树内检索(category 为空 = 全库)。
SearchIn(query, category string, topK int) ([]*Knowledge, error)
// Tree 返回分类树视图(面向服务:不带向量,只带计数与摘要)。
Tree(opt KnowledgeTreeOptions) (*KnowledgeTreeView, error)
// Subtree 返回某棵子树;category 为空等价于 Tree。
Subtree(category string, opt KnowledgeTreeOptions) (*KnowledgeTreeView, error)
// Categories 列出全部分类路径(去重排序)。
Categories() ([]string, error)
// CategoryCounts 给出每个分类的条目数,按数量倒序。
CategoryCounts() ([]KnowledgeCategoryCount, error)
// AddWithMedia 写入带媒体的知识。媒体是一等节点:其向量会与正文向量
// 在多模态统一空间内融合,使该条目能按图本身被召回。
//
// 未接入多模态空间时与 Add 等价(媒体仍被记录,只是不参与召回)。
AddWithMedia(name, content string, media []KnowledgeMediaRef) error
// AttachMedia 给已有知识追加媒体,并当场重算其稠密向量。
AttachMedia(name string, media ...KnowledgeMediaRef) error
// ReindexDense 重建稠密向量(模型/维度变化后调用),返回新建与跳过条数。
ReindexDense() (built, skipped int)
// ImportDir 从目录批量导入知识(复制,不是引用),见 knowledge.ImportDir。
//
// 放接口里而不是只用内核:子进程插件与外部 agent 拿到知识库后,
// "把这份资料灌进来"是常见诉求,不该逼它们回去调内核工具。
ImportDir(opt knowledge.ImportOptions) (knowledge.ImportStats, error)
// DenseStats 报告稠密路的接线与覆盖情况。
DenseStats() map[string]interface{}
}
// KnowledgeImportOptions 是 ImportDir 的参数(别名,便于外部引用)。
type KnowledgeImportOptions = knowledge.ImportOptions
// KnowledgeImportStats 是 ImportDir 的结果(别名)。
type KnowledgeImportStats = knowledge.ImportStats
// KnowledgeMediaRef 是媒体在知识条目中的一等引用。
//
// 与内核 knowledge.KnowledgeMediaRef 是**类型别名**而非新类型:别名
// 才能穿过 C ABI / JSON 边界;若是两种结构,core handler 还得再做一次
// 手工转换,漏一处就是「媒体被静默丢弃」。
type KnowledgeMediaRef = knowledge.KnowledgeMediaRef
// Knowledge 沿用公共 SDK 的类型,保证内外两侧对同一批知识条目的
// 字段理解一致(跨 ABI 传递时按此结构序列化)。
type Knowledge = pubsdk.Knowledge
// KnowledgeTreeOptions 控制树视图的取舍。
type KnowledgeTreeOptions struct {
// MaxDepth 限制层数,0 = 不限。分类多时用它做懒加载。
MaxDepth int
// IncludeItems 是否填充条目详情(只看结构时可关掉)。
IncludeItems bool
// PreviewLimit 预览字数上限,0 用内核默认。
PreviewLimit int
}
// KnowledgeTreeView 是分类树节点。
type KnowledgeTreeView = knowledge.TreeView
// KnowledgeTreeItemView 是树上的知识条目。
type KnowledgeTreeItemView = knowledge.TreeItemView
// KnowledgeTreeMediaView 是条目挂载的媒体摘要。
type KnowledgeTreeMediaView = knowledge.TreeMediaView
// KnowledgeCategoryCount 是一个分类的条目数。
type KnowledgeCategoryCount = knowledge.CategoryCount