- 只翻注释/文档字符串:820 增 / 820 删 严格 1:1;剥掉整行注释与行尾注释尾巴后, 逐文件与 HEAD 比对「代码部分差异 = 0」;裁决/design 编号计数逐项与 HEAD 一致 - 保留可执行 SQL 样例、SQLite 报错原文、pgvector/HNSW/ANN/BLOB 等技术词原文 - 复核:go build 绿 + memory/projectindex/vector 三包测试绿;gofmt 仅剩 2 个既有未格式化文件(改动前即如此,未顺手修)
113 lines
3.2 KiB
Go
113 lines
3.2 KiB
Go
package projectindex
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Chapter 是一个项目章节(层级第 3 层):项目被切分成的
|
|
// 深层内容。ProjectID 把它链到所属项目。零值
|
|
// 不是合法章节:AddChapter 要求 title 与
|
|
// content 非空,并生成 id。
|
|
type Chapter struct {
|
|
ID string
|
|
ProjectID string
|
|
Title string
|
|
Content string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// AddChapter 以 projectID 持久化 ch,分配 id 与缺失的
|
|
// 时间戳。它要求 projectID、title 与 content 非空;id 为空时
|
|
// 自动生成。项目行本身不必存在,因此章节可以在其父项目
|
|
// 之前或之后添加,但 DeleteProject 会把它们一并级联
|
|
// 删除。
|
|
func (s *Store) AddChapter(ctx context.Context, projectID string, ch Chapter) error {
|
|
if strings.TrimSpace(projectID) == "" {
|
|
return ErrMissingProject
|
|
}
|
|
if strings.TrimSpace(ch.Title) == "" {
|
|
return ErrMissingTitle
|
|
}
|
|
if strings.TrimSpace(ch.Content) == "" {
|
|
return ErrMissingContent
|
|
}
|
|
now := time.Now().UTC()
|
|
if ch.ID == "" {
|
|
id, err := newID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ch.ID = id
|
|
}
|
|
if ch.CreatedAt.IsZero() {
|
|
ch.CreatedAt = now
|
|
}
|
|
if ch.UpdatedAt.IsZero() {
|
|
ch.UpdatedAt = now
|
|
}
|
|
_, err := s.db.ExecContext(ctx, `INSERT INTO project_chapter
|
|
(id, project_id, title, content, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
ch.ID, projectID, ch.Title, ch.Content,
|
|
formatTime(ch.CreatedAt), formatTime(ch.UpdatedAt))
|
|
if err != nil {
|
|
return fmt.Errorf("projectindex: insert chapter: %w", err)
|
|
}
|
|
s.log.Debug("chapter added", "project_id", projectID, "chapter_id", ch.ID)
|
|
return nil
|
|
}
|
|
|
|
// ListChapters 按插入顺序(created_at ASC、rowid ASC)返回 projectID 的每个
|
|
// 章节 —— rowid 打破 created_at 并列,使同一时钟刻内写入的章节
|
|
// 保持其插入顺序。没有条目时返回空的
|
|
// (非 nil)切片;projectID 为空返回
|
|
// ErrMissingProject。
|
|
func (s *Store) ListChapters(ctx context.Context, projectID string) ([]Chapter, error) {
|
|
if strings.TrimSpace(projectID) == "" {
|
|
return nil, ErrMissingProject
|
|
}
|
|
rows, err := s.db.QueryContext(ctx, `SELECT id, project_id, title, content,
|
|
created_at, updated_at FROM project_chapter
|
|
WHERE project_id = ? ORDER BY created_at ASC, rowid ASC`, projectID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("projectindex: list chapters: %w", err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
chapters := make([]Chapter, 0)
|
|
for rows.Next() {
|
|
ch, err := scanChapter(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("projectindex: scan chapter: %w", err)
|
|
}
|
|
chapters = append(chapters, ch)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("projectindex: iterate chapters: %w", err)
|
|
}
|
|
return chapters, nil
|
|
}
|
|
|
|
// scanChapter 读取一行 project_chapter。
|
|
func scanChapter(sc rowScanner) (Chapter, error) {
|
|
var (
|
|
ch Chapter
|
|
createdAt, updatedAt string
|
|
)
|
|
if err := sc.Scan(&ch.ID, &ch.ProjectID, &ch.Title, &ch.Content, &createdAt, &updatedAt); err != nil {
|
|
return Chapter{}, err
|
|
}
|
|
var err error
|
|
if ch.CreatedAt, err = parseTime(createdAt); err != nil {
|
|
return Chapter{}, err
|
|
}
|
|
if ch.UpdatedAt, err = parseTime(updatedAt); err != nil {
|
|
return Chapter{}, err
|
|
}
|
|
return ch, nil
|
|
}
|