240 lines
6.2 KiB
Go
240 lines
6.2 KiB
Go
package state
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
|
||
"github.com/voocel/ainovel-cli/domain"
|
||
)
|
||
|
||
// SavePremise 保存故事前提到 premise.md。
|
||
func (s *Store) SavePremise(content string) error {
|
||
return s.writeMarkdown("premise.md", content)
|
||
}
|
||
|
||
// LoadPremise 读取 premise.md。不存在时返回空字符串。
|
||
func (s *Store) LoadPremise() (string, error) {
|
||
data, err := s.readFile("premise.md")
|
||
if os.IsNotExist(err) {
|
||
return "", nil
|
||
}
|
||
return string(data), err
|
||
}
|
||
|
||
// SaveOutline 同时保存 outline.json(机器读)和 outline.md(人读)。
|
||
func (s *Store) SaveOutline(entries []domain.OutlineEntry) error {
|
||
if err := s.writeJSON("outline.json", entries); err != nil {
|
||
return err
|
||
}
|
||
return s.writeMarkdown("outline.md", renderOutline(entries))
|
||
}
|
||
|
||
// LoadOutline 从 outline.json 读取结构化大纲。
|
||
func (s *Store) LoadOutline() ([]domain.OutlineEntry, error) {
|
||
var entries []domain.OutlineEntry
|
||
if err := s.readJSON("outline.json", &entries); err != nil {
|
||
if os.IsNotExist(err) {
|
||
return nil, nil
|
||
}
|
||
return nil, err
|
||
}
|
||
return entries, nil
|
||
}
|
||
|
||
// GetChapterOutline 获取指定章节的大纲条目。
|
||
func (s *Store) GetChapterOutline(chapter int) (*domain.OutlineEntry, error) {
|
||
entries, err := s.LoadOutline()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for i := range entries {
|
||
if entries[i].Chapter == chapter {
|
||
return &entries[i], nil
|
||
}
|
||
}
|
||
return nil, fmt.Errorf("chapter %d not found in outline", chapter)
|
||
}
|
||
|
||
// SaveLayeredOutline 保存分层大纲(长篇模式)。
|
||
// 同时保存 layered_outline.json(机器读)和 layered_outline.md(人读)。
|
||
func (s *Store) SaveLayeredOutline(volumes []domain.VolumeOutline) error {
|
||
if err := s.writeJSON("layered_outline.json", volumes); err != nil {
|
||
return err
|
||
}
|
||
return s.writeMarkdown("layered_outline.md", renderLayeredOutline(volumes))
|
||
}
|
||
|
||
// LoadLayeredOutline 读取分层大纲。
|
||
func (s *Store) LoadLayeredOutline() ([]domain.VolumeOutline, error) {
|
||
var volumes []domain.VolumeOutline
|
||
if err := s.readJSON("layered_outline.json", &volumes); err != nil {
|
||
if os.IsNotExist(err) {
|
||
return nil, nil
|
||
}
|
||
return nil, err
|
||
}
|
||
return volumes, nil
|
||
}
|
||
|
||
// ClearLayeredOutline 清理分层大纲文件,供从长篇降级为普通大纲时使用。
|
||
func (s *Store) ClearLayeredOutline() error {
|
||
return s.withWriteLock(func() error {
|
||
if err := s.removeFileUnlocked("layered_outline.json"); err != nil {
|
||
return err
|
||
}
|
||
return s.removeFileUnlocked("layered_outline.md")
|
||
})
|
||
}
|
||
|
||
// GetChapterFromLayered 从分层大纲中按全局章节号查找。
|
||
func (s *Store) GetChapterFromLayered(chapter int) (*domain.OutlineEntry, error) {
|
||
volumes, err := s.LoadLayeredOutline()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
ch := 1
|
||
for _, v := range volumes {
|
||
for _, a := range v.Arcs {
|
||
for i := range a.Chapters {
|
||
if ch == chapter {
|
||
e := a.Chapters[i]
|
||
e.Chapter = ch
|
||
return &e, nil
|
||
}
|
||
ch++
|
||
}
|
||
}
|
||
}
|
||
return nil, fmt.Errorf("chapter %d not found in layered outline", chapter)
|
||
}
|
||
|
||
// LocateChapter 根据全局章节号定位所在的卷和弧。
|
||
func (s *Store) LocateChapter(chapter int) (volume, arc int, err error) {
|
||
volumes, err := s.LoadLayeredOutline()
|
||
if err != nil {
|
||
return 0, 0, err
|
||
}
|
||
ch := 1
|
||
for _, v := range volumes {
|
||
for _, a := range v.Arcs {
|
||
for range a.Chapters {
|
||
if ch == chapter {
|
||
return v.Index, a.Index, nil
|
||
}
|
||
ch++
|
||
}
|
||
}
|
||
}
|
||
return 0, 0, fmt.Errorf("chapter %d not found in layered outline", chapter)
|
||
}
|
||
|
||
// ArcBoundary 弧边界信息。
|
||
type ArcBoundary struct {
|
||
IsArcEnd bool // 是否为弧内最后一章
|
||
IsVolumeEnd bool // 是否同时为卷内最后一章
|
||
Volume int // 当前章所在卷
|
||
Arc int // 当前章所在弧
|
||
NextVolume int // 下一章所在卷(0 = 全书结束)
|
||
NextArc int // 下一章所在弧(0 = 全书结束)
|
||
}
|
||
|
||
// CheckArcBoundary 检查某章是否为弧/卷的最后一章。
|
||
// 非分层大纲或未找到章节时返回 nil。
|
||
func (s *Store) CheckArcBoundary(chapter int) (*ArcBoundary, error) {
|
||
volumes, err := s.LoadLayeredOutline()
|
||
if err != nil || len(volumes) == 0 {
|
||
return nil, err
|
||
}
|
||
|
||
type chapterPos struct {
|
||
volume, arc, indexInArc, arcLen int
|
||
isLastArc bool
|
||
}
|
||
|
||
// 构建全局章节号 → 位置映射
|
||
ch := 1
|
||
var cur *chapterPos
|
||
var nextVol, nextArc int
|
||
for _, v := range volumes {
|
||
for ai, a := range v.Arcs {
|
||
for ci := range a.Chapters {
|
||
if ch == chapter {
|
||
cur = &chapterPos{
|
||
volume: v.Index,
|
||
arc: a.Index,
|
||
indexInArc: ci,
|
||
arcLen: len(a.Chapters),
|
||
isLastArc: ai == len(v.Arcs)-1,
|
||
}
|
||
} else if cur != nil && nextVol == 0 {
|
||
// 紧跟 cur 的下一章
|
||
nextVol = v.Index
|
||
nextArc = a.Index
|
||
}
|
||
ch++
|
||
}
|
||
}
|
||
}
|
||
if cur == nil {
|
||
return nil, nil
|
||
}
|
||
|
||
b := &ArcBoundary{
|
||
Volume: cur.volume,
|
||
Arc: cur.arc,
|
||
NextVolume: nextVol,
|
||
NextArc: nextArc,
|
||
}
|
||
if cur.indexInArc == cur.arcLen-1 {
|
||
b.IsArcEnd = true
|
||
if cur.isLastArc {
|
||
b.IsVolumeEnd = true
|
||
}
|
||
}
|
||
return b, nil
|
||
}
|
||
|
||
func renderLayeredOutline(volumes []domain.VolumeOutline) string {
|
||
var b strings.Builder
|
||
b.WriteString("# 分层大纲\n\n")
|
||
ch := 1
|
||
for _, v := range volumes {
|
||
fmt.Fprintf(&b, "## 第 %d 卷:%s\n\n", v.Index, v.Title)
|
||
fmt.Fprintf(&b, "**主题**:%s\n\n", v.Theme)
|
||
for _, a := range v.Arcs {
|
||
fmt.Fprintf(&b, "### 第 %d 弧:%s\n\n", a.Index, a.Title)
|
||
fmt.Fprintf(&b, "**目标**:%s\n\n", a.Goal)
|
||
for _, e := range a.Chapters {
|
||
fmt.Fprintf(&b, "#### 第 %d 章:%s\n\n", ch, e.Title)
|
||
fmt.Fprintf(&b, "**核心事件**:%s\n\n", e.CoreEvent)
|
||
if e.Hook != "" {
|
||
fmt.Fprintf(&b, "**钩子**:%s\n\n", e.Hook)
|
||
}
|
||
ch++
|
||
}
|
||
}
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
func renderOutline(entries []domain.OutlineEntry) string {
|
||
var b strings.Builder
|
||
b.WriteString("# 大纲\n\n")
|
||
for _, e := range entries {
|
||
fmt.Fprintf(&b, "## 第 %d 章:%s\n\n", e.Chapter, e.Title)
|
||
fmt.Fprintf(&b, "**核心事件**:%s\n\n", e.CoreEvent)
|
||
if e.Hook != "" {
|
||
fmt.Fprintf(&b, "**钩子**:%s\n\n", e.Hook)
|
||
}
|
||
if len(e.Scenes) > 0 {
|
||
b.WriteString("**场景**:\n")
|
||
for i, sc := range e.Scenes {
|
||
fmt.Fprintf(&b, "%d. %s\n", i+1, sc)
|
||
}
|
||
b.WriteString("\n")
|
||
}
|
||
}
|
||
return b.String()
|
||
}
|