feat: 支持长篇小说分层架构(卷/弧/章三级结构)
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/voocel/agentcore/schema"
|
||||
"github.com/voocel/ainovel-cli/domain"
|
||||
@@ -139,7 +140,30 @@ func (t *CommitChapterTool) Execute(_ context.Context, args json.RawMessage) (js
|
||||
if progress != nil {
|
||||
completedCount = len(progress.CompletedChapters)
|
||||
}
|
||||
reviewRequired, reviewReason := domain.ShouldReview(completedCount)
|
||||
// 6b. 长篇模式:弧级边界检测(替代固定间隔评审)+ 更新卷弧位置
|
||||
var arcEnd, volumeEnd bool
|
||||
var vol, arc int
|
||||
if progress != nil && progress.Layered {
|
||||
boundary, bErr := t.store.CheckArcBoundary(a.Chapter)
|
||||
if bErr != nil {
|
||||
log.Printf("[commit] 弧边界检测失败(chapter=%d): %v", a.Chapter, bErr)
|
||||
} else if boundary != nil {
|
||||
arcEnd = boundary.IsArcEnd
|
||||
volumeEnd = boundary.IsVolumeEnd
|
||||
vol = boundary.Volume
|
||||
arc = boundary.Arc
|
||||
// 每次提交时更新卷弧位置,确保 novel_context 的 position 始终正确
|
||||
_ = t.store.UpdateVolumeArc(vol, arc)
|
||||
}
|
||||
}
|
||||
|
||||
var reviewRequired bool
|
||||
var reviewReason string
|
||||
if progress != nil && progress.Layered {
|
||||
reviewRequired, reviewReason = domain.ShouldArcReview(arcEnd, volumeEnd, vol, arc)
|
||||
} else {
|
||||
reviewRequired, reviewReason = domain.ShouldReview(completedCount)
|
||||
}
|
||||
|
||||
// 7. 计算场景数
|
||||
sceneCount := 0
|
||||
@@ -158,6 +182,10 @@ func (t *CommitChapterTool) Execute(_ context.Context, args json.RawMessage) (js
|
||||
ReviewReason: reviewReason,
|
||||
HookType: a.HookType,
|
||||
DominantStrand: a.DominantStrand,
|
||||
ArcEnd: arcEnd,
|
||||
VolumeEnd: volumeEnd,
|
||||
Volume: vol,
|
||||
Arc: arc,
|
||||
}
|
||||
|
||||
// 9. 写入信号文件供宿主程序读取(优先于清理操作,确保信号不丢失)
|
||||
|
||||
@@ -75,18 +75,31 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
if a.Chapter > 0 {
|
||||
// 根据总章节数计算上下文策略
|
||||
profile := domain.NewContextProfile(0)
|
||||
if progress, err := t.store.LoadProgress(); err == nil && progress != nil && progress.TotalChapters > 0 {
|
||||
progress, _ := t.store.LoadProgress()
|
||||
if progress != nil && progress.TotalChapters > 0 {
|
||||
profile = domain.NewContextProfile(progress.TotalChapters)
|
||||
}
|
||||
// Layered 以 Progress 的显式标志为准,而非章节数推断
|
||||
if progress == nil || !progress.Layered {
|
||||
profile.Layered = false
|
||||
}
|
||||
|
||||
// 角色按 Tier 过滤:core/important 始终返回,secondary/decorative 按出场匹配
|
||||
t.loadFilteredCharacters(result, a.Chapter)
|
||||
// 角色加载:Layered 模式优先用快照,回退到原始设定
|
||||
if profile.Layered {
|
||||
t.loadLayeredCharacters(result, a.Chapter)
|
||||
} else {
|
||||
t.loadFilteredCharacters(result, a.Chapter)
|
||||
}
|
||||
|
||||
// Writer/Editor 模式:加载章节相关上下文
|
||||
if entry, err := t.store.GetChapterOutline(a.Chapter); err == nil {
|
||||
result["current_chapter_outline"] = entry
|
||||
}
|
||||
if profile.FullContext {
|
||||
|
||||
// 摘要加载:分层 vs 扁平
|
||||
if profile.Layered {
|
||||
t.loadLayeredSummaries(result, a.Chapter, profile.SummaryWindow)
|
||||
} else if profile.FullContext {
|
||||
if summaries, err := t.store.LoadAllSummaries(a.Chapter); err == nil && len(summaries) > 0 {
|
||||
result["recent_summaries"] = summaries
|
||||
}
|
||||
@@ -96,7 +109,7 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
}
|
||||
}
|
||||
|
||||
// 状态数据按策略加载
|
||||
// 时间线:Layered 用窗口,其他按策略
|
||||
if profile.FullContext {
|
||||
if timeline, err := t.store.LoadTimeline(); err == nil && len(timeline) > 0 {
|
||||
result["timeline"] = timeline
|
||||
@@ -121,8 +134,33 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
result["relationship_state"] = relationships
|
||||
}
|
||||
|
||||
// V2: 加载场景级恢复状态 + 节奏追踪
|
||||
if progress, err := t.store.LoadProgress(); err == nil && progress != nil {
|
||||
// Layered 模式:注入当前卷弧位置 + 弧目标/卷主题
|
||||
if profile.Layered && progress != nil {
|
||||
pos := map[string]any{
|
||||
"volume": progress.CurrentVolume,
|
||||
"arc": progress.CurrentArc,
|
||||
}
|
||||
if volumes, err := t.store.LoadLayeredOutline(); err == nil {
|
||||
for _, v := range volumes {
|
||||
if v.Index == progress.CurrentVolume {
|
||||
pos["volume_title"] = v.Title
|
||||
pos["volume_theme"] = v.Theme
|
||||
for _, arc := range v.Arcs {
|
||||
if arc.Index == progress.CurrentArc {
|
||||
pos["arc_title"] = arc.Title
|
||||
pos["arc_goal"] = arc.Goal
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
result["position"] = pos
|
||||
}
|
||||
|
||||
// 加载场景级恢复状态 + 节奏追踪
|
||||
if progress != nil {
|
||||
checkpoint := map[string]any{
|
||||
"in_progress_chapter": progress.InProgressChapter,
|
||||
"completed_scenes": progress.CompletedScenes,
|
||||
@@ -135,18 +173,26 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
}
|
||||
result["checkpoint"] = checkpoint
|
||||
}
|
||||
// V2: 加载已有的章节规划(支持场景恢复跳过已完成场景)
|
||||
// 加载已有的章节规划(支持场景恢复跳过已完成场景)
|
||||
if plan, err := t.store.LoadChapterPlan(a.Chapter); err == nil && plan != nil {
|
||||
result["chapter_plan"] = plan
|
||||
}
|
||||
|
||||
// V3: 写作参考资料分阶段加载
|
||||
// 写作参考资料分阶段加载
|
||||
result["references"] = t.writerReferences(a.Chapter)
|
||||
} else {
|
||||
// Architect 模式:全量角色 + 模板
|
||||
if chars, err := t.store.LoadCharacters(); err == nil && chars != nil {
|
||||
result["characters"] = chars
|
||||
}
|
||||
// Architect 模式下也加载分层大纲(弧级规划需要看全貌)
|
||||
if layered, err := t.store.LoadLayeredOutline(); err == nil && len(layered) > 0 {
|
||||
result["layered_outline"] = layered
|
||||
}
|
||||
// 加载已有的弧摘要(弧级规划时需要参考前续弧的内容)
|
||||
if volSummaries, err := t.store.LoadAllVolumeSummaries(); err == nil && len(volSummaries) > 0 {
|
||||
result["volume_summaries"] = volSummaries
|
||||
}
|
||||
result["references"] = t.architectReferences()
|
||||
}
|
||||
|
||||
@@ -183,6 +229,54 @@ func (t *ContextTool) loadFilteredCharacters(result map[string]any, chapter int)
|
||||
result["characters"] = filtered
|
||||
}
|
||||
|
||||
// loadLayeredSummaries 分层摘要加载:卷摘要 + 当前卷弧摘要 + 弧内章摘要。
|
||||
func (t *ContextTool) loadLayeredSummaries(result map[string]any, chapter, summaryWindow int) {
|
||||
vol, arc, err := t.store.LocateChapter(chapter)
|
||||
if err != nil {
|
||||
// 回退到扁平模式
|
||||
if summaries, err := t.store.LoadRecentSummaries(chapter, summaryWindow); err == nil && len(summaries) > 0 {
|
||||
result["recent_summaries"] = summaries
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 已完成卷的卷摘要
|
||||
if volSummaries, err := t.store.LoadAllVolumeSummaries(); err == nil && len(volSummaries) > 0 {
|
||||
result["volume_summaries"] = volSummaries
|
||||
}
|
||||
|
||||
// 2. 当前卷内已完成弧的弧摘要(不含当前弧)
|
||||
if arcSummaries, err := t.store.LoadArcSummaries(vol); err == nil && len(arcSummaries) > 0 {
|
||||
var prior []domain.ArcSummary
|
||||
for _, s := range arcSummaries {
|
||||
if s.Arc < arc {
|
||||
prior = append(prior, s)
|
||||
}
|
||||
}
|
||||
if len(prior) > 0 {
|
||||
result["arc_summaries"] = prior
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 当前弧内最近 N 章的章摘要
|
||||
if summaries, err := t.store.LoadRecentSummaries(chapter, summaryWindow); err == nil && len(summaries) > 0 {
|
||||
result["recent_summaries"] = summaries
|
||||
}
|
||||
}
|
||||
|
||||
// loadLayeredCharacters Layered 模式下的角色加载:优先用最近快照,回退到原始设定 + Tier 过滤。
|
||||
func (t *ContextTool) loadLayeredCharacters(result map[string]any, chapter int) {
|
||||
snapshots, err := t.store.LoadLatestSnapshots()
|
||||
if err == nil && len(snapshots) > 0 {
|
||||
result["character_snapshots"] = snapshots
|
||||
// 同时保留原始设定中的 core/important 角色(快照可能不含新登场角色)
|
||||
t.loadFilteredCharacters(result, chapter)
|
||||
return
|
||||
}
|
||||
// 无快照时回退到原始设定
|
||||
t.loadFilteredCharacters(result, chapter)
|
||||
}
|
||||
|
||||
// writerReferences 返回写作参考资料。章节 1 返回全量,后续章节裁剪掉不再需要的模板。
|
||||
func (t *ContextTool) writerReferences(chapter int) map[string]string {
|
||||
refs := map[string]string{}
|
||||
|
||||
86
tools/save_arc_summary.go
Normal file
86
tools/save_arc_summary.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/voocel/agentcore/schema"
|
||||
"github.com/voocel/ainovel-cli/domain"
|
||||
"github.com/voocel/ainovel-cli/state"
|
||||
)
|
||||
|
||||
// SaveArcSummaryTool 保存弧级摘要和角色快照,Editor 在弧结束时调用。
|
||||
type SaveArcSummaryTool struct {
|
||||
store *state.Store
|
||||
}
|
||||
|
||||
func NewSaveArcSummaryTool(store *state.Store) *SaveArcSummaryTool {
|
||||
return &SaveArcSummaryTool{store: store}
|
||||
}
|
||||
|
||||
func (t *SaveArcSummaryTool) Name() string { return "save_arc_summary" }
|
||||
func (t *SaveArcSummaryTool) Description() string { return "保存弧级摘要和角色状态快照(长篇模式,弧结束时调用)" }
|
||||
func (t *SaveArcSummaryTool) Label() string { return "保存弧摘要" }
|
||||
|
||||
func (t *SaveArcSummaryTool) Schema() map[string]any {
|
||||
snapshotSchema := schema.Object(
|
||||
schema.Property("name", schema.String("角色名")).Required(),
|
||||
schema.Property("status", schema.String("当前状态(存活/受伤/失踪等)")).Required(),
|
||||
schema.Property("power", schema.String("能力变化")),
|
||||
schema.Property("motivation", schema.String("当前动机")).Required(),
|
||||
schema.Property("relations", schema.String("关键关系变化")),
|
||||
)
|
||||
return schema.Object(
|
||||
schema.Property("volume", schema.Int("卷号")).Required(),
|
||||
schema.Property("arc", schema.Int("弧号")).Required(),
|
||||
schema.Property("title", schema.String("弧标题")).Required(),
|
||||
schema.Property("summary", schema.String("弧摘要(500字以内)")).Required(),
|
||||
schema.Property("key_events", schema.Array("弧内关键事件", schema.String(""))).Required(),
|
||||
schema.Property("character_snapshots", schema.Array("角色状态快照", snapshotSchema)).Required(),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *SaveArcSummaryTool) Execute(_ context.Context, args json.RawMessage) (json.RawMessage, error) {
|
||||
var a struct {
|
||||
Volume int `json:"volume"`
|
||||
Arc int `json:"arc"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
KeyEvents []string `json:"key_events"`
|
||||
CharacterSnapshots []domain.CharacterSnapshot `json:"character_snapshots"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return nil, fmt.Errorf("invalid args: %w", err)
|
||||
}
|
||||
if a.Volume <= 0 || a.Arc <= 0 {
|
||||
return nil, fmt.Errorf("volume and arc must be > 0")
|
||||
}
|
||||
|
||||
arcSummary := domain.ArcSummary{
|
||||
Volume: a.Volume,
|
||||
Arc: a.Arc,
|
||||
Title: a.Title,
|
||||
Summary: a.Summary,
|
||||
KeyEvents: a.KeyEvents,
|
||||
}
|
||||
if err := t.store.SaveArcSummary(arcSummary); err != nil {
|
||||
return nil, fmt.Errorf("save arc summary: %w", err)
|
||||
}
|
||||
|
||||
if len(a.CharacterSnapshots) > 0 {
|
||||
for i := range a.CharacterSnapshots {
|
||||
a.CharacterSnapshots[i].Volume = a.Volume
|
||||
a.CharacterSnapshots[i].Arc = a.Arc
|
||||
}
|
||||
if err := t.store.SaveCharacterSnapshots(a.Volume, a.Arc, a.CharacterSnapshots); err != nil {
|
||||
return nil, fmt.Errorf("save character snapshots: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(map[string]any{
|
||||
"saved": true, "type": "arc_summary",
|
||||
"volume": a.Volume, "arc": a.Arc,
|
||||
"snapshots": len(a.CharacterSnapshots),
|
||||
})
|
||||
}
|
||||
@@ -27,8 +27,8 @@ func (t *SaveFoundationTool) Label() string { return "保存设定" }
|
||||
|
||||
func (t *SaveFoundationTool) Schema() map[string]any {
|
||||
return schema.Object(
|
||||
schema.Property("type", schema.Enum("设定类型", "premise", "outline", "characters", "world_rules")).Required(),
|
||||
schema.Property("content", schema.String("内容。premise 为 Markdown 文本,outline 和 characters 为 JSON 字符串")).Required(),
|
||||
schema.Property("type", schema.Enum("设定类型", "premise", "outline", "layered_outline", "characters", "world_rules")).Required(),
|
||||
schema.Property("content", schema.String("内容。premise 为 Markdown 文本,outline/layered_outline/characters/world_rules 为 JSON 字符串")).Required(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,31 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
_ = t.store.SetTotalChapters(len(entries))
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "outline", "chapters": len(entries)})
|
||||
|
||||
case "layered_outline":
|
||||
var volumes []domain.VolumeOutline
|
||||
if err := json.Unmarshal([]byte(a.Content), &volumes); err != nil {
|
||||
return nil, fmt.Errorf("parse layered_outline JSON: %w", err)
|
||||
}
|
||||
if err := t.store.SaveLayeredOutline(volumes); err != nil {
|
||||
return nil, fmt.Errorf("save layered_outline: %w", err)
|
||||
}
|
||||
// 展开为扁平大纲,兼容现有 GetChapterOutline
|
||||
flat := domain.FlattenOutline(volumes)
|
||||
if err := t.store.SaveOutline(flat); err != nil {
|
||||
return nil, fmt.Errorf("save flattened outline: %w", err)
|
||||
}
|
||||
total := domain.TotalChapters(volumes)
|
||||
_ = t.store.UpdatePhase(domain.PhaseOutline)
|
||||
_ = t.store.SetTotalChapters(total)
|
||||
_ = t.store.SetLayered(true)
|
||||
if len(volumes) > 0 && len(volumes[0].Arcs) > 0 {
|
||||
_ = t.store.UpdateVolumeArc(volumes[0].Index, volumes[0].Arcs[0].Index)
|
||||
}
|
||||
return json.Marshal(map[string]any{
|
||||
"saved": true, "type": "layered_outline",
|
||||
"volumes": len(volumes), "chapters": total,
|
||||
})
|
||||
|
||||
case "characters":
|
||||
var chars []domain.Character
|
||||
if err := json.Unmarshal([]byte(a.Content), &chars); err != nil {
|
||||
@@ -83,6 +108,6 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "world_rules", "count": len(rules)})
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown type %q, expected premise/outline/characters/world_rules", a.Type)
|
||||
return nil, fmt.Errorf("unknown type %q, expected premise/outline/layered_outline/characters/world_rules", a.Type)
|
||||
}
|
||||
}
|
||||
|
||||
62
tools/save_volume_summary.go
Normal file
62
tools/save_volume_summary.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/voocel/agentcore/schema"
|
||||
"github.com/voocel/ainovel-cli/domain"
|
||||
"github.com/voocel/ainovel-cli/state"
|
||||
)
|
||||
|
||||
// SaveVolumeSummaryTool 保存卷级摘要,Editor 在卷结束时调用。
|
||||
type SaveVolumeSummaryTool struct {
|
||||
store *state.Store
|
||||
}
|
||||
|
||||
func NewSaveVolumeSummaryTool(store *state.Store) *SaveVolumeSummaryTool {
|
||||
return &SaveVolumeSummaryTool{store: store}
|
||||
}
|
||||
|
||||
func (t *SaveVolumeSummaryTool) Name() string { return "save_volume_summary" }
|
||||
func (t *SaveVolumeSummaryTool) Description() string { return "保存卷级摘要(长篇模式,卷结束时调用)" }
|
||||
func (t *SaveVolumeSummaryTool) Label() string { return "保存卷摘要" }
|
||||
|
||||
func (t *SaveVolumeSummaryTool) Schema() map[string]any {
|
||||
return schema.Object(
|
||||
schema.Property("volume", schema.Int("卷号")).Required(),
|
||||
schema.Property("title", schema.String("卷标题")).Required(),
|
||||
schema.Property("summary", schema.String("卷摘要(500字以内)")).Required(),
|
||||
schema.Property("key_events", schema.Array("卷内关键事件", schema.String(""))).Required(),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *SaveVolumeSummaryTool) Execute(_ context.Context, args json.RawMessage) (json.RawMessage, error) {
|
||||
var a struct {
|
||||
Volume int `json:"volume"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
KeyEvents []string `json:"key_events"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return nil, fmt.Errorf("invalid args: %w", err)
|
||||
}
|
||||
if a.Volume <= 0 {
|
||||
return nil, fmt.Errorf("volume must be > 0")
|
||||
}
|
||||
|
||||
volSummary := domain.VolumeSummary{
|
||||
Volume: a.Volume,
|
||||
Title: a.Title,
|
||||
Summary: a.Summary,
|
||||
KeyEvents: a.KeyEvents,
|
||||
}
|
||||
if err := t.store.SaveVolumeSummary(volSummary); err != nil {
|
||||
return nil, fmt.Errorf("save volume summary: %w", err)
|
||||
}
|
||||
|
||||
return json.Marshal(map[string]any{
|
||||
"saved": true, "type": "volume_summary", "volume": a.Volume,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user