package tools import ( "context" "encoding/json" "fmt" "github.com/voocel/agentcore/schema" "github.com/voocel/ainovel-cli/domain" "github.com/voocel/ainovel-cli/state" ) // CommitChapterTool 提交章节:加载正文 → 保存终稿 → 生成摘要 → 更新状态 → 更新进度。 // 这是唯一允许写入 chapters/、summaries/、更新状态文件和进度的工具。 type CommitChapterTool struct { store *state.Store } func NewCommitChapterTool(store *state.Store) *CommitChapterTool { return &CommitChapterTool{store: store} } func (t *CommitChapterTool) Name() string { return "commit_chapter" } func (t *CommitChapterTool) Description() string { return "提交章节。优先使用打磨版正文,同时更新时间线、伏笔、关系状态。返回结构化信号" } func (t *CommitChapterTool) Label() string { return "提交章节" } func (t *CommitChapterTool) Schema() map[string]any { timelineSchema := schema.Object( schema.Property("time", schema.String("故事内时间")).Required(), schema.Property("event", schema.String("事件描述")).Required(), schema.Property("characters", schema.Array("涉及角色", schema.String(""))), ) foreshadowSchema := schema.Object( schema.Property("id", schema.String("伏笔 ID(新埋设时自定义,推进/回收时使用已有 ID)")).Required(), schema.Property("action", schema.Enum("操作", "plant", "advance", "resolve")).Required(), schema.Property("description", schema.String("伏笔描述(仅 plant 时必需)")), ) relationshipSchema := schema.Object( schema.Property("character_a", schema.String("角色 A")).Required(), schema.Property("character_b", schema.String("角色 B")).Required(), schema.Property("relation", schema.String("当前关系描述")).Required(), ) return schema.Object( schema.Property("chapter", schema.Int("章节号")).Required(), schema.Property("summary", schema.String("本章内容摘要(200字以内)")).Required(), schema.Property("characters", schema.Array("本章出场角色名", schema.String(""))).Required(), schema.Property("key_events", schema.Array("本章关键事件", schema.String(""))).Required(), schema.Property("timeline_events", schema.Array("本章时间线事件", timelineSchema)), schema.Property("foreshadow_updates", schema.Array("伏笔操作", foreshadowSchema)), schema.Property("relationship_changes", schema.Array("关系变化", relationshipSchema)), schema.Property("hook_type", schema.Enum("章末钩子类型", "crisis", "mystery", "desire", "emotion", "choice")), schema.Property("dominant_strand", schema.Enum("本章主导叙事线", "quest", "fire", "constellation")), ) } func (t *CommitChapterTool) Execute(_ context.Context, args json.RawMessage) (json.RawMessage, error) { var a struct { Chapter int `json:"chapter"` Summary string `json:"summary"` Characters []string `json:"characters"` KeyEvents []string `json:"key_events"` TimelineEvents []domain.TimelineEvent `json:"timeline_events"` ForeshadowUpdates []domain.ForeshadowUpdate `json:"foreshadow_updates"` RelationshipChanges []domain.RelationshipEntry `json:"relationship_changes"` HookType string `json:"hook_type"` DominantStrand string `json:"dominant_strand"` } if err := json.Unmarshal(args, &a); err != nil { return nil, fmt.Errorf("invalid args: %w", err) } if a.Chapter <= 0 { return nil, fmt.Errorf("chapter must be > 0") } if err := t.store.ValidateChapterCommit(a.Chapter); err != nil { return nil, err } // 1. 加载章节正文(polished 优先,否则 merge scenes) content, wordCount, err := t.store.LoadChapterContent(a.Chapter) if err != nil { return nil, fmt.Errorf("load chapter content: %w", err) } if content == "" { return nil, fmt.Errorf("no content found for chapter %d", a.Chapter) } // 2. 保存终稿 if err := t.store.SaveFinalChapter(a.Chapter, content); err != nil { return nil, fmt.Errorf("save final chapter: %w", err) } // 3. 保存摘要 summary := domain.ChapterSummary{ Chapter: a.Chapter, Summary: a.Summary, Characters: a.Characters, KeyEvents: a.KeyEvents, } if err := t.store.SaveSummary(summary); err != nil { return nil, fmt.Errorf("save summary: %w", err) } // 4. 更新状态增量 if len(a.TimelineEvents) > 0 { for i := range a.TimelineEvents { a.TimelineEvents[i].Chapter = a.Chapter } if err := t.store.AppendTimelineEvents(a.TimelineEvents); err != nil { return nil, fmt.Errorf("append timeline: %w", err) } } if len(a.ForeshadowUpdates) > 0 { if err := t.store.UpdateForeshadow(a.Chapter, a.ForeshadowUpdates); err != nil { return nil, fmt.Errorf("update foreshadow: %w", err) } } if len(a.RelationshipChanges) > 0 { for i := range a.RelationshipChanges { a.RelationshipChanges[i].Chapter = a.Chapter } if err := t.store.UpdateRelationships(a.RelationshipChanges); err != nil { return nil, fmt.Errorf("update relationships: %w", err) } } // 5. 更新进度 if err := t.store.MarkChapterComplete(a.Chapter, wordCount, a.HookType, a.DominantStrand); err != nil { return nil, fmt.Errorf("mark chapter complete: %w", err) } // 6. 判断是否需要审阅 progress, err := t.store.LoadProgress() if err != nil { return nil, fmt.Errorf("load progress: %w", err) } completedCount := 0 if progress != nil { completedCount = len(progress.CompletedChapters) } reviewRequired, reviewReason := domain.ShouldReview(completedCount) // 7. 计算场景数 sceneCount := 0 if scenes, err := t.store.LoadSceneDrafts(a.Chapter); err == nil { sceneCount = len(scenes) } // 8. 构造结构化信号 result := domain.CommitResult{ Chapter: a.Chapter, Committed: true, WordCount: wordCount, SceneCount: sceneCount, NextChapter: a.Chapter + 1, ReviewRequired: reviewRequired, ReviewReason: reviewReason, HookType: a.HookType, DominantStrand: a.DominantStrand, } // 9. 写入信号文件供宿主程序读取(优先于清理操作,确保信号不丢失) if err := t.store.SaveLastCommit(result); err != nil { return nil, fmt.Errorf("save commit signal: %w", err) } // 10. 清除场景级进度(章节已提交) if err := t.store.ClearInProgress(); err != nil { return nil, fmt.Errorf("clear in-progress: %w", err) } return json.Marshal(result) }