For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 为 kb-cli 添加 --with-content 和 --with-links 功能,并调整 kb-search 技能优先使用 kb-cli
Architecture: 在现有 SearchResult 结构体上添加可选字段 Content 和 Links,通过 SearchOptions 控制是否填充这些字段。修改输出格式化器支持新字段的展示。
Tech Stack: Go 1.22, SQLite FTS5, Cobra CLI
omitempty 标签Files:
- Modify: internal/index/sqlite.go:120-140
- Test: internal/index/sqlite_test.go
Interfaces:
- Consumes: Store.db (SQLite 连接)
- Produces: GetNodeLinks(nodeID int64) ([]string, error) 方法
在 internal/index/sqlite_test.go 添加:
func TestGetNodeLinks(t *testing.T) {
store := setupTestStore(t)
defer store.Close()
// 插入测试节点
node1 := &graph.Node{
Path: "FAQ/001-测试.md",
Title: "测试文档",
Section: "FAQ",
Content: "# 测试\n\n内容",
}
node2 := &graph.Node{
Path: "FAQ/002-相关.md",
Title: "相关文档",
Section: "FAQ",
Content: "# 相关\n\n内容",
}
id1, err := store.InsertNode(node1)
require.NoError(t, err)
id2, err := store.InsertNode(node2)
require.NoError(t, err)
// 插入 wikilink 边
edge := &graph.Edge{
FromNode: id1,
ToNode: id2,
Relation: "wikilink",
Label: "相关文档",
}
err = store.InsertEdge(edge)
require.NoError(t, err)
// 测试获取链接
links, err := store.GetNodeLinks(id1)
require.NoError(t, err)
assert.Len(t, links, 1)
assert.Equal(t, "FAQ/002-相关.md", links[0])
// 测试无链接的节点
links, err = store.GetNodeLinks(id2)
require.NoError(t, err)
assert.Len(t, links, 0)
}
cd ~/workspace/kb-cli
go test ./internal/index -run TestGetNodeLinks -v
Expected: FAIL - "store.GetNodeLinks undefined"
在 internal/index/sqlite.go 的 GetNodeContent 方法后添加:
// GetNodeLinks 获取节点的关联链接(wikilink 目标)
func (s *Store) GetNodeLinks(nodeID int64) ([]string, error) {
query := `
SELECT n.path
FROM edges e
JOIN nodes n ON n.id = e.to_node
WHERE e.from_node = ? AND e.relation = 'wikilink'
`
rows, err := s.db.Query(query, nodeID)
if err != nil {
return nil, fmt.Errorf("查询链接失败: %w", err)
}
defer rows.Close()
var links []string
for rows.Next() {
var path string
if err := rows.Scan(&path); err != nil {
return nil, fmt.Errorf("扫描链接失败: %w", err)
}
links = append(links, path)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("遍历链接失败: %w", err)
}
return links, nil
}
cd ~/workspace/kb-cli
go test ./internal/index -run TestGetNodeLinks -v
Expected: PASS
cd ~/workspace/kb-cli
git add internal/index/sqlite.go internal/index/sqlite_test.go
git commit -m "feat: add GetNodeLinks method for wikilink retrieval"
Files:
- Modify: internal/search/engine.go:15-25
- Test: internal/search/engine_test.go
Interfaces:
- Consumes: 无
- Produces: 扩展的 SearchResult 结构体(Content, Links 字段)
在 internal/search/engine_test.go 添加:
func TestSearchResultJSON_OmitEmpty(t *testing.T) {
// 测试无内容和链接时,JSON 不包含这些字段
result := SearchResult{
ID: 1,
Path: "test.md",
Title: "测试",
Section: "FAQ",
Score: 10,
}
jsonBytes, err := json.Marshal(result)
require.NoError(t, err)
jsonStr := string(jsonBytes)
assert.NotContains(t, jsonStr, "content")
assert.NotContains(t, jsonStr, "links")
// 测试有内容时,JSON 包含 content 字段
result.Content = "# 测试内容"
jsonBytes, err = json.Marshal(result)
require.NoError(t, err)
jsonStr = string(jsonBytes)
assert.Contains(t, jsonStr, `"content":"# 测试内容"`)
// 测试有链接时,JSON 包含 links 字段
result.Links = []string{"related.md"}
jsonBytes, err = json.Marshal(result)
require.NoError(t, err)
jsonStr = string(jsonBytes)
assert.Contains(t, jsonStr, `"links":["related.md"]`)
}
cd ~/workspace/kb-cli
go test ./internal/search -run TestSearchResultJSON_OmitEmpty -v
Expected: FAIL - "Content undefined" 或测试失败(因为字段不存在)
修改 internal/search/engine.go 中的 SearchResult 结构体:
// SearchResult 搜索结果
type SearchResult struct {
ID int64 `json:"id"`
Path string `json:"path"`
Title string `json:"title"`
Section string `json:"section"`
Score int `json:"score"`
// 可选字段(omitempty)
Content string `json:"content,omitempty"` // --with-content 时填充
Links []string `json:"links,omitempty"` // --with-links 时填充
}
cd ~/workspace/kb-cli
go test ./internal/search -run TestSearchResultJSON_OmitEmpty -v
Expected: PASS
cd ~/workspace/kb-cli
git add internal/search/engine.go internal/search/engine_test.go
git commit -m "feat: extend SearchResult with Content and Links fields"
Files:
- Modify: internal/search/engine.go:30-40, 80-120
- Test: internal/search/engine_test.go
Interfaces:
- Consumes: Store.GetNodeContent, Store.GetNodeLinks (Task 1)
- Produces: 扩展的 SearchOptions 和增强的 Search 函数
在 internal/search/engine_test.go 添加:
func TestSearch_WithContent(t *testing.T) {
store := setupTestStore(t)
defer store.Close()
// 插入测试节点
node := &graph.Node{
Path: "FAQ/001-测试.md",
Title: "测试文档",
Section: "FAQ",
Content: "# 测试\n\n这是测试内容",
}
id, err := store.InsertNode(node)
require.NoError(t, err)
err = store.CreateFTS()
require.NoError(t, err)
err = store.PopulateFTS()
require.NoError(t, err)
// 测试带内容的搜索
opts := SearchOptions{
TopN: 5,
WithContent: true,
}
results, err := Search(store, []string{"测试"}, opts)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, id, results[0].ID)
assert.Equal(t, "# 测试\n\n这是测试内容", results[0].Content)
// 测试不带内容的搜索
opts.WithContent = false
results, err = Search(store, []string{"测试"}, opts)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Empty(t, results[0].Content)
}
func TestSearch_WithLinks(t *testing.T) {
store := setupTestStore(t)
defer store.Close()
// 插入两个节点
node1 := &graph.Node{
Path: "FAQ/001-测试.md",
Title: "测试文档",
Section: "FAQ",
Content: "# 测试",
}
node2 := &graph.Node{
Path: "FAQ/002-相关.md",
Title: "相关文档",
Section: "FAQ",
Content: "# 相关",
}
id1, err := store.InsertNode(node1)
require.NoError(t, err)
id2, err := store.InsertNode(node2)
require.NoError(t, err)
// 插入 wikilink 边
edge := &graph.Edge{
FromNode: id1,
ToNode: id2,
Relation: "wikilink",
Label: "相关文档",
}
err = store.InsertEdge(edge)
require.NoError(t, err)
err = store.CreateFTS()
require.NoError(t, err)
err = store.PopulateFTS()
require.NoError(t, err)
// 测试带链接的搜索
opts := SearchOptions{
TopN: 5,
WithLinks: true,
}
results, err := Search(store, []string{"测试"}, opts)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, id1, results[0].ID)
assert.Len(t, results[0].Links, 1)
assert.Equal(t, "FAQ/002-相关.md", results[0].Links[0])
// 测试不带链接的搜索
opts.WithLinks = false
results, err = Search(store, []string{"测试"}, opts)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Empty(t, results[0].Links)
}
cd ~/workspace/kb-cli
go test ./internal/search -run "TestSearch_WithContent|TestSearch_WithLinks" -v
Expected: FAIL - "WithContent undefined" 或测试失败
修改 internal/search/engine.go:
// SearchOptions 搜索选项
type SearchOptions struct {
Expanded []string
Symptom []string
TopN int
WithContent bool // 新增:是否返回内容
WithLinks bool // 新增:是否返回链接
}
// Search 执行搜索
func Search(store *index.Store, keywords []string, opts SearchOptions) ([]SearchResult, error) {
// 合并所有关键词
allKeywords := append(keywords, opts.Expanded...)
allKeywords = append(allKeywords, opts.Symptom...)
// FTS5 搜索
ftsResults, err := store.FTSSearch(allKeywords, 100)
if err != nil {
return nil, err
}
// 评分
scoreMap := make(map[int64]int)
for _, r := range ftsResults {
score := 0
for _, kw := range keywords {
score += scoreResult(r, kw, ScoreNormal)
}
for _, kw := range opts.Expanded {
score += scoreResult(r, kw, ScoreExpanded)
}
for _, kw := range opts.Symptom {
score += scoreResult(r, kw, ScoreSymptom)
}
scoreMap[r.ID] = score
}
// 转换为结果列表
var results []SearchResult
for _, r := range ftsResults {
results = append(results, SearchResult{
ID: r.ID,
Path: r.Path,
Title: r.Title,
Section: r.Section,
Score: scoreMap[r.ID],
})
}
// 按分数排序
sort.Slice(results, func(i, j int) bool {
return results[i].Score > results[j].Score
})
// 限制返回数量
if opts.TopN > 0 && len(results) > opts.TopN {
results = results[:opts.TopN]
}
// 增强结果(新增)
for i := range results {
if opts.WithContent {
content, _, _, err := store.GetNodeContent(results[i].ID)
if err == nil {
results[i].Content = content
}
}
if opts.WithLinks {
links, err := store.GetNodeLinks(results[i].ID)
if err == nil {
results[i].Links = links
}
}
}
return results, nil
}
cd ~/workspace/kb-cli
go test ./internal/search -run "TestSearch_WithContent|TestSearch_WithLinks" -v
Expected: PASS
cd ~/workspace/kb-cli
go test ./... -v
Expected: 所有测试通过
cd ~/workspace/kb-cli
git add internal/search/engine.go internal/search/engine_test.go
git commit -m "feat: add WithContent and WithLinks options to Search"
Files:
- Modify: cmd/search.go:20-50
Interfaces:
- Consumes: SearchOptions.WithContent, SearchOptions.WithLinks (Task 3)
- Produces: 新增 --with-content 和 --with-links flags
在 cmd/search.go 的变量声明区域添加:
var (
withContent bool
withLinks bool
)
在 init() 函数中添加:
func init() {
searchCmd.Flags().StringSliceVar(&expanded, "expanded", nil, "扩展词(提升相关实体权重)")
searchCmd.Flags().StringSliceVar(&symptom, "symptom", nil, "症状词(针对具体症状)")
searchCmd.Flags().IntVar(&topN, "top", 10, "返回前 N 条结果")
searchCmd.Flags().BoolVar(&jsonOut, "json", false, "JSON 格式输出")
// 新增 flags
searchCmd.Flags().BoolVar(&withContent, "with-content", false, "返回完整文件内容")
searchCmd.Flags().BoolVar(&withLinks, "with-links", false, "显示关联文档链接")
}
修改 runSearch 函数:
func runSearch(cmd *cobra.Command, args []string) error {
// ... 现有代码 ...
opts := search.SearchOptions{
Expanded: expanded,
Symptom: symptom,
TopN: topN,
WithContent: withContent,
WithLinks: withLinks,
}
results, err := search.Search(store, args, opts)
// ... 现有代码 ...
}
cd ~/workspace/kb-cli
go build -o bin/kb .
./bin/kb search --help
Expected: 显示 --with-content 和 --with-links flags
cd ~/workspace/kb-cli
git add cmd/search.go
git commit -m "feat: add --with-content and --with-links CLI flags"
Files:
- Modify: internal/output/formatter.go
- Test: internal/output/formatter_test.go
Interfaces:
- Consumes: SearchResult.Content, SearchResult.Links (Task 2)
- Produces: 增强的 FormatTable 和 FormatJSON 方法
在 internal/output/formatter_test.go 添加:
func TestFormatTable_WithContent(t *testing.T) {
results := []search.SearchResult{
{
ID: 1,
Path: "FAQ/001-测试.md",
Title: "测试文档",
Section: "FAQ",
Score: 10,
Content: "# 测试\n\n这是内容",
},
}
output := FormatTable(results)
assert.Contains(t, output, "FAQ/001-测试.md")
assert.Contains(t, output, "--- 内容 ---")
assert.Contains(t, output, "# 测试")
}
func TestFormatTable_WithLinks(t *testing.T) {
results := []search.SearchResult{
{
ID: 1,
Path: "FAQ/001-测试.md",
Title: "测试文档",
Section: "FAQ",
Score: 10,
Links: []string{"FAQ/002-相关.md"},
},
}
output := FormatTable(results)
assert.Contains(t, output, "--- 关联文档 ---")
assert.Contains(t, output, "FAQ/002-相关.md")
}
func TestFormatTable_WithBoth(t *testing.T) {
results := []search.SearchResult{
{
ID: 1,
Path: "FAQ/001-测试.md",
Title: "测试文档",
Section: "FAQ",
Score: 10,
Content: "# 测试",
Links: []string{"FAQ/002-相关.md"},
},
}
output := FormatTable(results)
assert.Contains(t, output, "--- 内容 ---")
assert.Contains(t, output, "--- 关联文档 ---")
}
func TestFormatJSON_WithContentAndLinks(t *testing.T) {
results := []search.SearchResult{
{
ID: 1,
Path: "FAQ/001-测试.md",
Title: "测试文档",
Section: "FAQ",
Score: 10,
Content: "# 测试",
Links: []string{"FAQ/002-相关.md"},
},
}
output, err := FormatJSON(results)
require.NoError(t, err)
assert.Contains(t, output, `"content":"# 测试"`)
assert.Contains(t, output, `"links":["FAQ/002-相关.md"]`)
}
cd ~/workspace/kb-cli
go test ./internal/output -run "TestFormatTable_With|TestFormatJSON_With" -v
Expected: FAIL - 测试失败(输出不包含新字段)
修改 internal/output/formatter.go 中的 FormatTable 函数:
// FormatTable 格式化表格输出
func FormatTable(results []search.SearchResult) string {
if len(results) == 0 {
return "未找到匹配结果\n"
}
var buf bytes.Buffer
// 表头
buf.WriteString(fmt.Sprintf("%-50s %-30s %-10s %s\n", "路径", "标题", "板块", "得分"))
buf.WriteString(strings.Repeat("-", 100) + "\n")
// 每行结果
for _, r := range results {
title := r.Title
if len(title) > 28 {
title = title[:28] + ".."
}
buf.WriteString(fmt.Sprintf("%-50s %-30s %-10s %d\n", r.Path, title, r.Section, r.Score))
// 输出内容(如果有)
if r.Content != "" {
buf.WriteString("\n--- 内容 ---\n")
buf.WriteString(r.Content)
buf.WriteString("\n")
}
// 输出关联文档(如果有)
if len(r.Links) > 0 {
buf.WriteString("\n--- 关联文档 ---\n")
for _, link := range r.Links {
buf.WriteString(fmt.Sprintf("- %s\n", link))
}
}
}
buf.WriteString(fmt.Sprintf("\n共 %d 条结果\n", len(results)))
return buf.String()
}
cd ~/workspace/kb-cli
go test ./internal/output -run "TestFormatTable_With|TestFormatJSON_With" -v
Expected: PASS
cd ~/workspace/kb-cli
go test ./... -v
Expected: 所有测试通过
cd ~/workspace/kb-cli
git add internal/output/formatter.go internal/output/formatter_test.go
git commit -m "feat: enhance output formatter with content and links support"
Files:
- 无新文件(使用现有测试数据)
Interfaces:
- Consumes: 所有之前的任务
- Produces: 验证完整功能链
cd ~/workspace/kb-cli
go build -o bin/kb .
./bin/kb search 充装 --top 3
Expected: 显示 3 条结果,不包含内容和链接
./bin/kb search 充装 --with-content --top 3
Expected: 显示 3 条结果,每条后跟 --- 内容 --- 和文件内容
./bin/kb search 充装 --with-links --top 3
Expected: 显示 3 条结果,有链接的显示 --- 关联文档 ---
./bin/kb search 充装 --with-content --with-links --top 3
Expected: 显示 3 条结果,同时包含内容和链接
./bin/kb search 充装 --with-content --with-links --json --top 3
Expected: JSON 格式输出,包含 content 和 links 字段
time ./bin/kb search 充装 --with-content --with-links --top 10
Expected: 总耗时 < 100ms
cd ~/workspace/kb-cli
git add -A
git commit -m "test: integration tests for enhanced search"
Files:
- Modify: ~/.hermes/skills/kb-knowledge/kb-search/SKILL.md
Interfaces:
- Consumes: kb-cli 新功能
- Produces: 更新的技能文档
cp ~/.hermes/skills/kb-knowledge/kb-search/SKILL.md ~/.hermes/skills/kb-knowledge/kb-search/SKILL.md.bak
将 ## 核心工具 部分从:
## 核心工具
**kb-search.py**:知识库搜索
改为:
## 核心工具
**kb-cli**:知识库搜索(优先使用)
**完整路径**:`~/go/bin/kb`
**基本用法**:
kb search "关键词" --top 5
kb search "关键词" --with-content --top 3
kb search "关键词" --with-links --top 3
kb search "充装" --expanded "重量 规格" --symptom "报错" --with-content --top 3
kb search "关键词" --json --top 5
```
kb-search.py:知识库搜索(降级方案,仅当 kb-cli 不可用时)
```
将 ## 执行流程 部分的命令示例从 python3 kb-search.py 改为 kb:
# 方式1: 基础搜索
kb search "用户问题关键词" --top 3
# 方式2: 带内容(推荐)
kb search "用户问题关键词" --with-content --top 3
# 方式3: 带内容和链接
kb search "用户问题关键词" --with-content --with-links --top 3
# 方式4: 带扩展词和症状词
kb search "关键词" --expanded "扩展词" --symptom "症状词" --with-content --top 3
在文档末尾添加:
## 降级方案
**仅当 kb-cli 不可用时**,使用 kb-search.py:
export KB_VAULT=/home/aisim-p/aisim/note/001/笔记001
python3 ~/.hermes/skills/kb-knowledge/kb-search/scripts/kb-search.py search "关键词" --with-content --top 3
```
注意:kb-search.py 当前有索引问题,可能返回空结果。优先使用 kb-cli。
```
cat ~/.hermes/skills/kb-knowledge/kb-search/SKILL.md | head -50
Expected: 显示更新后的文档,核心工具为 kb-cli
cd ~/aisim/note/001/笔记001
git add .
git commit -m "docs: update kb-search skill to prioritize kb-cli"
git push
Files:
- Modify: ~/workspace/kb-cli/README.md
Interfaces:
- Consumes: 所有功能
- Produces: 完整的文档和验证
在 ~/workspace/kb-cli/README.md 的 ## 使用方法 部分添加新功能示例:
### 高级搜索
kb search 充装规格 --with-content --top 3
kb search 充装规格 --with-links --top 3
kb search 充装规格 --with-content --with-links --top 3
kb search 充装 --expanded "重量 规格" --symptom "报错" --with-content --top 3
cd ~/workspace/kb-cli
go test ./... -v
Expected: 所有测试通过
cd ~/workspace/kb-cli
go build -o bin/kb .
cp bin/kb ~/go/bin/
# 测试所有功能
kb search 充装 --top 3
kb search 充装 --with-content --top 3
kb search 充装 --with-links --top 3
kb search 充装 --with-content --with-links --top 3
kb search 充装 --with-content --json --top 3
Expected: 所有命令正常工作
cd ~/workspace/kb-cli
git add README.md
git commit -m "docs: update README with new features"
git push
cat > RELEASE_NOTES.md << 'EOF'
# kb-cli v1.1.0 发布说明
## 新功能
- `--with-content`: 搜索时返回完整文件内容
- `--with-links`: 搜索时显示关联文档链接
## 使用示例
kb search 充装规格 --with-content --top 3
kb search 充装规格 --with-links --top 3
kb search 充装规格 --with-content --with-links --top 3
```
[ ] Step 7: 提交发布说明
cd ~/workspace/kb-cli
git add RELEASE_NOTES.md
git commit -m "docs: add release notes for v1.1.0"
git push
所有任务完成后,必须满足:
Plan complete and saved to docs/superpowers/plans/2026-07-26-kb-cli-enhancement.md. Two execution options:
1. Subagent-Driven (recommended) - I dispatch a fresh subagent per task, review between tasks, fast iteration
2. Inline Execution - Execute tasks in this session using executing-plans, batch execution with checkpoints
Which approach?