feat: rhythm tracking and structured review

This commit is contained in:
voocel
2026-03-10 17:24:48 +08:00
parent ef55c89e9d
commit 0a48b66ed1
14 changed files with 246 additions and 80 deletions

View File

@@ -41,6 +41,7 @@ func (s *Store) UpdateForeshadow(chapter int, updates []domain.ForeshadowUpdate)
for _, u := range updates {
switch u.Action {
case "plant":
idx[u.ID] = len(entries)
entries = append(entries, domain.ForeshadowEntry{
ID: u.ID,
Description: u.Description,
@@ -61,6 +62,21 @@ func (s *Store) UpdateForeshadow(chapter int, updates []domain.ForeshadowUpdate)
return s.SaveForeshadowLedger(entries)
}
// LoadActiveForeshadow 返回未回收的伏笔条目status != "resolved")。
func (s *Store) LoadActiveForeshadow() ([]domain.ForeshadowEntry, error) {
all, err := s.LoadForeshadowLedger()
if err != nil {
return nil, err
}
var active []domain.ForeshadowEntry
for _, e := range all {
if e.Status != "resolved" {
active = append(active, e)
}
}
return active, nil
}
func renderForeshadow(entries []domain.ForeshadowEntry) string {
var b strings.Builder
b.WriteString("# 伏笔账本\n\n")

View File

@@ -62,7 +62,8 @@ func (s *Store) UpdatePhase(phase domain.Phase) error {
// MarkChapterComplete 标记章节完成,原子性更新进度。
// 支持重写场景:如果章节已完成,先减去旧字数再加新字数。
func (s *Store) MarkChapterComplete(chapter, wordCount int) error {
// hookType 和 dominantStrand 用于节奏追踪,可为空。
func (s *Store) MarkChapterComplete(chapter, wordCount int, hookType, dominantStrand string) error {
p, err := s.LoadProgress()
if err != nil {
return err
@@ -89,6 +90,29 @@ func (s *Store) MarkChapterComplete(chapter, wordCount int) error {
p.InProgressChapter = 0
p.CompletedScenes = nil
p.Phase = domain.PhaseWriting
// 节奏追踪:按章节顺序填充 history确保索引对齐
if dominantStrand != "" {
for len(p.StrandHistory) < chapter-1 {
p.StrandHistory = append(p.StrandHistory, "")
}
if len(p.StrandHistory) < chapter {
p.StrandHistory = append(p.StrandHistory, dominantStrand)
} else {
p.StrandHistory[chapter-1] = dominantStrand
}
}
if hookType != "" {
for len(p.HookHistory) < chapter-1 {
p.HookHistory = append(p.HookHistory, "")
}
if len(p.HookHistory) < chapter {
p.HookHistory = append(p.HookHistory, hookType)
} else {
p.HookHistory[chapter-1] = hookType
}
}
return s.SaveProgress(p)
}

View File

@@ -37,6 +37,22 @@ func (s *Store) AppendTimelineEvents(newEvents []domain.TimelineEvent) error {
return s.SaveTimeline(append(existing, newEvents...))
}
// LoadRecentTimeline 返回最近 window 章内的时间线事件chapter >= current-window
func (s *Store) LoadRecentTimeline(current, window int) ([]domain.TimelineEvent, error) {
all, err := s.LoadTimeline()
if err != nil {
return nil, err
}
minCh := max(current-window, 1)
var filtered []domain.TimelineEvent
for _, e := range all {
if e.Chapter >= minCh {
filtered = append(filtered, e)
}
}
return filtered, nil
}
func renderTimeline(events []domain.TimelineEvent) string {
var b strings.Builder
b.WriteString("# 时间线\n\n")