Files
ainovel-clients/tools/commit_chapter.go

219 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package tools
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/voocel/agentcore/schema"
"github.com/voocel/ainovel-cli/domain"
"github.com/voocel/ainovel-cli/state"
)
// CommitChapterTool 提交章节:加载正文 → 保存终稿 → 生成摘要 → 更新状态 → 更新进度。
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")).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(),
)
stateChangeSchema := schema.Object(
schema.Property("entity", schema.String("角色名或实体名")).Required(),
schema.Property("field", schema.String("变化属性")).Required(),
schema.Property("old_value", schema.String("变化前的值")),
schema.Property("new_value", schema.String("变化后的值")).Required(),
schema.Property("reason", schema.String("变化原因")),
)
feedbackSchema := schema.Object(
schema.Property("deviation", schema.String("偏离大纲的描述")).Required(),
schema.Property("suggestion", 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("state_changes", schema.Array("角色/实体状态变化", stateChangeSchema)),
schema.Property("hook_type", schema.Enum("章末钩子类型", "crisis", "mystery", "desire", "emotion", "choice")),
schema.Property("dominant_strand", schema.Enum("本章主导叙事线", "quest", "fire", "constellation")),
schema.Property("feedback", feedbackSchema),
)
}
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"`
StateChanges []domain.StateChange `json:"state_changes"`
HookType string `json:"hook_type"`
DominantStrand string `json:"dominant_strand"`
Feedback *domain.OutlineFeedback `json:"feedback"`
}
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. 加载章节正文
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)
}
}
if len(a.StateChanges) > 0 {
for i := range a.StateChanges {
a.StateChanges[i].Chapter = a.Chapter
}
if err := t.store.AppendStateChanges(a.StateChanges); err != nil {
return nil, fmt.Errorf("append state changes: %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)
}
// 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
_ = 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. 构造结构化信号
result := domain.CommitResult{
Chapter: a.Chapter,
Committed: true,
WordCount: wordCount,
NextChapter: a.Chapter + 1,
ReviewRequired: reviewRequired,
ReviewReason: reviewReason,
HookType: a.HookType,
DominantStrand: a.DominantStrand,
Feedback: a.Feedback,
ArcEnd: arcEnd,
VolumeEnd: volumeEnd,
Volume: vol,
Arc: arc,
}
// 8. 写入信号文件
if err := t.store.SaveLastCommit(result); err != nil {
return nil, fmt.Errorf("save commit signal: %w", err)
}
// 9. 清除进度中间状态
if err := t.store.ClearInProgress(); err != nil {
return nil, fmt.Errorf("clear in-progress: %w", err)
}
return json.Marshal(result)
}