219 lines
7.9 KiB
Go
219 lines
7.9 KiB
Go
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)
|
||
}
|