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

165 lines
5.9 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.
// projectindex 包实现 DevDocs/design/03-记忆.md §3(「项目记忆」作用域)中的
// 项目索引层级:一个项目被索引为一条
// 条目加描述,其内容被切成可逐行检索的章节。
// 三层深度为 项目索引 → 项目描述 → 章节内容。
//
// 两张表刻画该深度:project 行是指引条目(第 1 层),
// 携带其描述(第 2 层),project_chapter 保存章节
// 内容(第 3 层)。项目记忆每个角色都可读写
// (design §3「人人可读可写」「项目索引各角色可查」),因此存储没有
// persona/role 列,也不在此按身份过滤。
//
// 本包刻意与向量索引
// (internal/runtime/memory/vector)分离:它对项目条目、描述与章节
// 做基于文本的分层索引。语义/向量检索属于
// 向量包。
//
// 存储自己拥有 schema:New 幂等地创建 project 与 project_chapter 表
// 及章节索引。所有 SQL 参数化(?),列清单
// 显式列出(无 SELECT *),读取带 context.Cancel,且每个错误都
// 用 %w 包裹,调用方可用 errors.Is 对包哨兵错误判断。
package projectindex
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"log/slog"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
)
// 区分 projectindex 各类失败的哨兵错误。每个都在
// 调用点用 %w 包裹,调用方可用 errors.Is 判断。
var (
// ErrProjectNotFound 在项目 id 为空或不存在时返回。
ErrProjectNotFound = errors.New("projectindex: project not found")
// ErrChapterNotFound 在必需的章节引用无效时返回。
ErrChapterNotFound = errors.New("projectindex: chapter not found")
// ErrNilDB 在 New 收到 nil *sql.DB 时返回。
ErrNilDB = errors.New("projectindex: nil db")
// ErrMissingProject 在必需的 project id 为空时返回。
ErrMissingProject = errors.New("projectindex: project_id is required")
// ErrMissingTitle 在项目或章节标题为空时返回。
ErrMissingTitle = errors.New("projectindex: title is required")
// ErrMissingContent 在章节没有内容时返回。
ErrMissingContent = errors.New("projectindex: content is required")
// ErrMissingKeyword 在 Search 收到空关键词时返回。
ErrMissingKeyword = errors.New("projectindex: keyword is required")
)
// Store 是基于 *sql.DB 的项目索引存储。
//
// 它自己拥有 schema:New 幂等地创建 project / project_chapter 表与
// 章节索引。并发交由 *sql.DB,按约定它可安全
// 并发使用,因此 Store 不再持锁。每个查询都
// 参数化,读取带 context.Cancel。
type Store struct {
db *sql.DB
log *slog.Logger
}
// rowScanner 同时被 *sql.Row 与 *sql.Rows 满足,因此单个 scanProject
// 或 scanChapter 辅助函数即可服务 Get 与 List 两条路径。
type rowScanner interface {
Scan(dest ...any) error
}
// New 在 db 上构建 Store,确保 schema 存在。
//
// db 必须是存活的连接(迁移框架由
// internal/db 拥有)。nil db 返回 ErrNilDB。log 可为 nil,此时
// 使用丢弃型 logger;实际使用的 logger 带 source
// "memory/projectindex" 标签,因此每条记录都可追溯。ctx 控制 schema
// 创建,调用方必须遵守。旧的变宽时间戳
// 就地规范化(见 normalizeTimestamps)。
func New(ctx context.Context, db *sql.DB, log *slog.Logger) (*Store, error) {
if db == nil {
return nil, ErrNilDB
}
log = logger.WithSource(discardLogger(log), "memory/projectindex")
for _, ddl := range []string{
createProjectTable,
createProjectChapterTable,
createProjectChapterIndex,
} {
if _, err := db.ExecContext(ctx, ddl); err != nil {
return nil, fmt.Errorf("projectindex: apply schema: %w", err)
}
}
if err := normalizeTimestamps(ctx, db); err != nil {
return nil, err
}
return &Store{db: db, log: log}, nil
}
// discardLogger 在 log 非 nil 时返回 log,否则返回一个不写任何地方的 logger。
func discardLogger(log *slog.Logger) *slog.Logger {
if log != nil {
return log
}
return slog.New(slog.NewTextHandler(io.Discard, nil))
}
// ListProjects 按插入顺序(created_at ASC、rowid ASC)返回每个
// 项目索引条目 —— rowid 打破 created_at 并列,使同一时钟刻内写入的项目
// 保持其插入顺序。没有条目时返回空的
// (非 nil)切片,调用方可以无条件 range。读
// 错误用 %w 包裹,调用方可用 errors.Is 对包
// 哨兵错误判断。
func (s *Store) ListProjects(ctx context.Context) ([]Project, error) {
rows, err := s.db.QueryContext(ctx, `SELECT id, title, desc, created_at, updated_at
FROM project ORDER BY created_at ASC, rowid ASC`)
if err != nil {
return nil, fmt.Errorf("projectindex: list projects: %w", err)
}
defer func() { _ = rows.Close() }()
projects := make([]Project, 0)
for rows.Next() {
p, err := scanProject(rows)
if err != nil {
return nil, fmt.Errorf("projectindex: scan project: %w", err)
}
projects = append(projects, *p)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("projectindex: iterate projects: %w", err)
}
return projects, nil
}
// schema DDL,由 New 幂等施加。两张表刻画三层
// 层级:project 行是指引条目(第 1 层)加其描述
// (第 2 层),project_chapter 保存章节内容(第 3 层)。
const createProjectTable = `
CREATE TABLE IF NOT EXISTS project (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
desc TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)`
const createProjectChapterTable = `
CREATE TABLE IF NOT EXISTS project_chapter (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)`
const createProjectChapterIndex = `
CREATE INDEX IF NOT EXISTS idx_project_chapter_project_id
ON project_chapter (project_id)`