Files
ainovel-clients/tools/commit_chapter.go
2026-03-12 22:25:34 +08:00

220 lines
8.2 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 提交章节:加载正文 → 保存终稿 → 生成摘要 → 更新状态 → 更新进度。
// 这是唯一允许写入 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(),
)
stateChangeSchema := schema.Object(
schema.Property("entity", schema.String("角色名或实体名")).Required(),
schema.Property("field", schema.String("变化属性realm/location/status/power/relation 等")).Required(),
schema.Property("old_value", schema.String("变化前的值(首次出现可空)")),
schema.Property("new_value", schema.String("变化后的值")).Required(),
schema.Property("reason", schema.String("变化原因")),
)
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")),
)
}
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"`
}
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)
}
}
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
// 每次提交时更新卷弧位置,确保 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
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,
ArcEnd: arcEnd,
VolumeEnd: volumeEnd,
Volume: vol,
Arc: arc,
}
// 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)
}