Files
AskaEth 61cf80d5ee
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/memory(31 文件,纯替换)
- 只翻注释/文档字符串:820 增 / 820 删 严格 1:1;剥掉整行注释与行尾注释尾巴后,
  逐文件与 HEAD 比对「代码部分差异 = 0」;裁决/design 编号计数逐项与 HEAD 一致
- 保留可执行 SQL 样例、SQLite 报错原文、pgvector/HNSW/ANN/BLOB 等技术词原文
- 复核:go build 绿 + memory/projectindex/vector 三包测试绿;gofmt 仅剩 2 个既有未格式化文件(改动前即如此,未顺手修)
2026-10-04 13:32:54 +08:00

109 lines
3.3 KiB
Go
Raw Permalink 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 projectindex
import (
"context"
"strings"
)
// contextRadius 是 LineMatch.Context 中匹配行每侧的
// 显示行数。
const contextRadius = 2
// LineMatch 是章节内容中的一条匹配行及其
// 周边上下文。LineNumber 在章节的 Content 中以 1 为基。
type LineMatch struct {
LineNumber int
Text string
Context string
}
// ChapterHit 是内容命中关键词的一个章节,附带定位到的
// 匹配行(及其上下文)。
type ChapterHit struct {
Chapter *Chapter
Matches []LineMatch
}
// SearchResult 是一次 Search 的分层结果。Project 携带命中的
// 项目索引条目 + 描述(第 1/2 层),仅当关键词命中
// 项目标题或 desc 时设置;Chapters 携带命中的章节
// (第 3 层)及定位到的行。无任何命中时,Project 为 nil,
// Chapters 为空,且不报错。
type SearchResult struct {
Project *Project
Chapters []ChapterHit
}
// Search 在单个项目内查找关键词,沿层级
// 项目索引 → 项目描述 → 章节内容 遍历。它先加载项目(不存在时返回
// ErrProjectNotFound),当关键词命中
// 项目标题或描述时设置 SearchResult.Project,然后扫描每个章节找内容命中,
// 产出定位到的行及其上下文。关键词为空返回
// ErrMissingKeyword;未命中任何内容的关键词返回非 nil 的 SearchResult,
// 其中 Project 与 Chapters 不设置。
func (s *Store) Search(ctx context.Context, projectID, keyword string) (*SearchResult, error) {
kw := strings.ToLower(strings.TrimSpace(keyword))
if kw == "" {
return nil, ErrMissingKeyword
}
proj, err := s.GetProject(ctx, projectID)
if err != nil {
return nil, err
}
result := &SearchResult{}
if containsFold(proj.Title, kw) || containsFold(proj.Desc, kw) {
result.Project = proj
}
chapters, err := s.ListChapters(ctx, projectID)
if err != nil {
return nil, err
}
for i := range chapters {
matches := findLineMatches(chapters[i].Content, kw)
if len(matches) == 0 {
continue
}
ch := chapters[i]
result.Chapters = append(result.Chapters, ChapterHit{Chapter: &ch, Matches: matches})
}
return result, nil
}
// containsFold 报告 s 是否(不区分大小写地)包含 sub,
// sub 必须已由调用方转为小写。
func containsFold(s, sub string) bool {
return strings.Contains(strings.ToLower(s), sub)
}
// findLineMatches 为每行(不区分大小写地)包含 keyword 的内容返回一个
// LineMatch。Context 是每侧 contextRadius 行的周边上下文,
// 以换行连接,并包含匹配行本身。
func findLineMatches(content, keyword string) []LineMatch {
lines := strings.Split(content, "\n")
matches := make([]LineMatch, 0)
for i, line := range lines {
if strings.Contains(strings.ToLower(line), keyword) {
matches = append(matches, LineMatch{
LineNumber: i + 1,
Text: line,
Context: buildContext(lines, i),
})
}
}
return matches
}
// buildContext 返回 idx 周围(含 idx)的 lines[start..end],以 \n 连接,
// 并夹到切片边界内,因此上下文绝不会越界。
func buildContext(lines []string, idx int) string {
start := idx - contextRadius
if start < 0 {
start = 0
}
end := idx + contextRadius
if end >= len(lines) {
end = len(lines) - 1
}
return strings.Join(lines[start:end+1], "\n")
}