mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-28 13:23:03 +00:00
背景:旧版 Add 整串 sanitize 名字、逐段 sanitize 建目录,留下 tech/_go_/note、
Tech/Upper、a/b with space 这类「知识名与盘上目录不一致」的目录。修复后
normalizeName 要求二者逐字一致,故需一次性改名。
- 迁移逻辑放在 internal/knowledge/migrate_names.go(PlanMigration/
ApplyMigration),CLI 与 homed 启动**共用同一份实现**,避免口径漂移。
- 三条硬约束:
1. 默认只报告(-apply 才真改名)—— 批量 os.Rename 不可逆。
2. 检出目标名冲突(两条迁到同一目标 / 目标已存在)则**整批拒绝**,
不做部分迁移:半迁移状态比不迁移更难收拾。
3. 逐条失败不中断,最后统一报告;执行前重查冲突(计划生成与执行之间
可能有人改过盘上状态),并校验目标不越出知识根。
- homed 启动在 initKnowledgeStore **之前**调 initKnowledgeMigration:改名后
扫盘一次到位,避免先以旧名建索引再改名造成内存键与盘上目录短暂不一致。
defaultApply=false ⇒ 启动只扫描+报告+打印可执行命令行,不替人决定。
单次改名上限 200 条,防失控目录规模拖住启动。
实测:报告模式零改动;冲突场景整批拒绝且盘上原封不动;迁移后 Store
正确载入 4 条并可按规范名逐条删除。
101 lines
3.2 KiB
Go
101 lines
3.2 KiB
Go
// Command homed-kb-migrate 迁移存量知识库的目录名到规范名。
|
||
//
|
||
// 背景:旧版 Add 对名字**整串** sanitize、对路径**逐段** sanitize,
|
||
// 于是知识名(内存键 / LLM 可见的名字)与盘上目录从第一次落盘起就对不上。
|
||
// 典型残留:
|
||
//
|
||
// tech/_go_/note 分类段内的空格未被 TrimSpace 掉
|
||
// Tech/Upper 未小写化
|
||
// a/b with space 空格未替换成下划线
|
||
//
|
||
// 迁移把它们重命名到规范名,使三者一致。
|
||
//
|
||
// 安全设计:
|
||
// 1. **默认只报告**(-apply 才真改名)。批量 os.Rename 不可逆。
|
||
// 2. 检出目标名冲突则**整批拒绝**,不做部分迁移——半迁移状态比不迁移更难收拾。
|
||
// 3. 单条失败不中断整体,最后统一报告;执行前再查一次目标越界。
|
||
//
|
||
// 与 homed 启动时的关系:`homed` 启动会调同一个 PlanMigration 并**只报告**
|
||
// (见 cmd/homed/bootstrap.go 的 defaultApply)。本命令是人工确认后真正执行
|
||
// 的那一步。两者共用 internal/knowledge 里的同一份实现,避免口径漂移。
|
||
//
|
||
// 用法:
|
||
//
|
||
// homed-kb-migrate -root /data/homeagent/knowledge # 报告
|
||
// homed-kb-migrate -root /data/homeagent/knowledge -apply # 执行
|
||
package main
|
||
|
||
import (
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||
)
|
||
|
||
func main() {
|
||
root := flag.String("root", "", "知识库根目录(必填)")
|
||
apply := flag.Bool("apply", false, "真正执行重命名(缺省只报告)")
|
||
dryRun := flag.Bool("dry-run", false, "只报告(显式写法,与默认相同)")
|
||
limit := flag.Int("limit", 0, "单次最多改名条数,0 = 不限")
|
||
flag.Parse()
|
||
|
||
if *root == "" {
|
||
fmt.Fprintln(os.Stderr, "错误:必须指定 -root <知识库根目录>")
|
||
flag.Usage()
|
||
os.Exit(2)
|
||
}
|
||
if *apply && *dryRun {
|
||
fmt.Fprintln(os.Stderr, "错误:-apply 与 -dry-run 互斥")
|
||
os.Exit(2)
|
||
}
|
||
|
||
abs, err := filepath.Abs(*root)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "错误:%v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
abs = filepath.Clean(abs)
|
||
|
||
items, err := knowledge.PlanMigration(abs)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "错误:无法读取 %s:%v\n", abs, err)
|
||
os.Exit(1)
|
||
}
|
||
if len(items) == 0 {
|
||
fmt.Printf("未发现任何知识条目(%s)\n", abs)
|
||
return
|
||
}
|
||
|
||
var need, illegal int
|
||
for _, it := range items {
|
||
switch {
|
||
case it.Illegal:
|
||
illegal++
|
||
fmt.Printf(" [非法] %-40s 含 .. / 点段 / 隐藏段;写入与删除均已拒绝,需人工处理\n", it.OldName)
|
||
case it.NewName != "":
|
||
need++
|
||
fmt.Printf(" [迁移] %-40s → %s\n", it.OldName, it.NewName)
|
||
}
|
||
}
|
||
fmt.Printf("\n共 %d 条:需迁移 %d,已规范 %d,非法 %d\n",
|
||
len(items), need, len(items)-need-illegal, illegal)
|
||
|
||
if !*apply {
|
||
if need == 0 {
|
||
fmt.Println("\n无需迁移。加 -apply 不会改变任何东西。")
|
||
return
|
||
}
|
||
fmt.Println("\n这是报告(未改动任何文件)。确认无误后加 -apply 执行。")
|
||
return
|
||
}
|
||
|
||
applied, failed := knowledge.ApplyMigration(abs, items, *limit)
|
||
fmt.Printf("\n迁移完成:成功 %d,失败 %d\n", applied, failed)
|
||
if failed > 0 {
|
||
fmt.Fprintln(os.Stderr, "存在失败项。若为名称冲突,请先人工处理冲突的目录再重跑。")
|
||
os.Exit(1)
|
||
}
|
||
}
|