This commit is contained in:
voocel
2026-03-07 21:25:55 +08:00
commit 27bd85ef90
60 changed files with 5658 additions and 0 deletions

125
tools/check_consistency.go Normal file
View File

@@ -0,0 +1,125 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"github.com/voocel/agentcore/schema"
"github.com/voocel/ainovel-cli/domain"
"github.com/voocel/ainovel-cli/state"
)
// CheckConsistencyTool 对照状态文件检查章节一致性。
// 返回上下文数据和已知约束供 LLM 判断,不做 AI 推理。
type CheckConsistencyTool struct {
store *state.Store
}
func NewCheckConsistencyTool(store *state.Store) *CheckConsistencyTool {
return &CheckConsistencyTool{store: store}
}
func (t *CheckConsistencyTool) Name() string { return "check_consistency" }
func (t *CheckConsistencyTool) Description() string {
return "检查章节一致性。返回章节内容、全部状态数据和具体检查清单,你需要逐项对照并以 JSON 格式返回冲突项"
}
func (t *CheckConsistencyTool) Label() string { return "一致性检查" }
func (t *CheckConsistencyTool) Schema() map[string]any {
return schema.Object(
schema.Property("chapter", schema.Int("要检查的章节号")).Required(),
)
}
func (t *CheckConsistencyTool) Execute(_ context.Context, args json.RawMessage) (json.RawMessage, error) {
var a struct {
Chapter int `json:"chapter"`
}
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")
}
result := map[string]any{"chapter": a.Chapter}
// 加载章节内容polished 优先)
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)
}
result["content"] = content
result["word_count"] = wordCount
// 加载全部状态数据供 LLM 对照
if timeline, _ := t.store.LoadTimeline(); len(timeline) > 0 {
result["timeline"] = timeline
}
if foreshadow, _ := t.store.LoadForeshadowLedger(); len(foreshadow) > 0 {
result["foreshadow_ledger"] = foreshadow
if active := filterActive(foreshadow); len(active) > 0 {
result["unresolved_foreshadow"] = active
}
}
if relationships, _ := t.store.LoadRelationships(); len(relationships) > 0 {
result["relationships"] = relationships
}
if chars, _ := t.store.LoadCharacters(); len(chars) > 0 {
result["characters"] = chars
}
if rules, _ := t.store.LoadWorldRules(); len(rules) > 0 {
result["world_rules"] = rules
// 提取边界清单,方便 LLM 逐条对照
var boundaries []string
for _, r := range rules {
if r.Boundary != "" {
boundaries = append(boundaries, fmt.Sprintf("[%s] %s", r.Category, r.Boundary))
}
}
if len(boundaries) > 0 {
result["world_rules_boundaries"] = boundaries
}
}
// 加载前两章摘要
if summaries, _ := t.store.LoadRecentSummaries(a.Chapter, 2); len(summaries) > 0 {
result["recent_summaries"] = summaries
}
result["instruction"] = `请逐项对照以上状态数据检查本章内容,返回 JSON 数组格式的冲突项:
[
{
"type": "timeline|foreshadow|relationship|character|world_rules",
"severity": "error|warning",
"description": "具体冲突描述",
"suggestion": "建议修正范围和方式"
}
]
检查清单:
1. 时间线:本章事件时间是否与已有 timeline 矛盾
2. 伏笔unresolved_foreshadow 中是否有本章应推进但遗漏的
3. 人物关系:角色互动是否与 relationships 当前状态矛盾
4. 角色一致性:行为是否符合 characters 中的性格和弧线
5. 世界规则:逐条检查 world_rules_boundaries 中的边界约束,本章内容是否违反任何一条
如果没有发现冲突,返回空数组 []。不要返回其他格式。`
return json.Marshal(result)
}
func filterActive(entries []domain.ForeshadowEntry) []domain.ForeshadowEntry {
var active []domain.ForeshadowEntry
for _, e := range entries {
if e.Status != "resolved" {
active = append(active, e)
}
}
return active
}

168
tools/commit_chapter.go Normal file
View File

@@ -0,0 +1,168 @@
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)),
)
}
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"`
}
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); 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,
}
// 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)
}

View File

@@ -0,0 +1,110 @@
package tools
import (
"context"
"encoding/json"
"os"
"testing"
"github.com/voocel/ainovel-cli/domain"
"github.com/voocel/ainovel-cli/state"
)
func TestCommitChapterRejectsNonPendingRewrite(t *testing.T) {
dir := t.TempDir()
store := state.NewStore(dir)
if err := store.Init(); err != nil {
t.Fatalf("Init: %v", err)
}
if err := store.InitProgress("test", 10); err != nil {
t.Fatalf("InitProgress: %v", err)
}
if err := store.SetPendingRewrites([]int{2}, "测试重写"); err != nil {
t.Fatalf("SetPendingRewrites: %v", err)
}
if err := store.SetFlow(domain.FlowRewriting); err != nil {
t.Fatalf("SetFlow: %v", err)
}
if err := store.SavePolished(3, "这是错误章节的正文。"); err != nil {
t.Fatalf("SavePolished: %v", err)
}
tool := NewCommitChapterTool(store)
args, err := json.Marshal(map[string]any{
"chapter": 3,
"summary": "错误提交",
"characters": []string{"主角"},
"key_events": []string{"误提交"},
"timeline_events": []any{},
})
if err != nil {
t.Fatalf("Marshal: %v", err)
}
if _, err := tool.Execute(context.Background(), args); err == nil {
t.Fatal("expected commit to be rejected during rewrite flow")
}
if _, err := os.Stat(dir + "/chapters/03.md"); !os.IsNotExist(err) {
t.Fatalf("chapter should not be persisted, stat err=%v", err)
}
progress, err := store.LoadProgress()
if err != nil {
t.Fatalf("LoadProgress: %v", err)
}
if len(progress.CompletedChapters) != 0 {
t.Fatalf("completed chapters should stay empty, got %v", progress.CompletedChapters)
}
if progress.CurrentChapter != 0 {
t.Fatalf("current chapter should not advance, got %d", progress.CurrentChapter)
}
}
func TestCommitChapterAllowsPendingRewrite(t *testing.T) {
dir := t.TempDir()
store := state.NewStore(dir)
if err := store.Init(); err != nil {
t.Fatalf("Init: %v", err)
}
if err := store.InitProgress("test", 10); err != nil {
t.Fatalf("InitProgress: %v", err)
}
if err := store.SetPendingRewrites([]int{2}, "测试重写"); err != nil {
t.Fatalf("SetPendingRewrites: %v", err)
}
if err := store.SetFlow(domain.FlowRewriting); err != nil {
t.Fatalf("SetFlow: %v", err)
}
if err := store.SavePolished(2, "这是正确待重写章节的正文。"); err != nil {
t.Fatalf("SavePolished: %v", err)
}
tool := NewCommitChapterTool(store)
args, err := json.Marshal(map[string]any{
"chapter": 2,
"summary": "正确提交",
"characters": []string{"主角"},
"key_events": []string{"完成重写"},
"timeline_events": []any{},
})
if err != nil {
t.Fatalf("Marshal: %v", err)
}
if _, err := tool.Execute(context.Background(), args); err != nil {
t.Fatalf("Execute: %v", err)
}
if _, err := os.Stat(dir + "/chapters/02.md"); err != nil {
t.Fatalf("chapter should be persisted: %v", err)
}
progress, err := store.LoadProgress()
if err != nil {
t.Fatalf("LoadProgress: %v", err)
}
if len(progress.CompletedChapters) != 1 || progress.CompletedChapters[0] != 2 {
t.Fatalf("unexpected completed chapters: %v", progress.CompletedChapters)
}
}

164
tools/context.go Normal file
View File

@@ -0,0 +1,164 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/voocel/agentcore/schema"
"github.com/voocel/ainovel-cli/state"
)
// References 嵌入的参考资料。
type References struct {
// V0
ChapterGuide string
HookTechniques string
QualityChecklist string
OutlineTemplate string
CharacterTemplate string
ChapterTemplate string
// V1
Consistency string
ContentExpansion string
DialogueWriting string
// V2
StyleReference string // 风格补充参考(可为空)
}
// ContextTool 组装当前章节所需上下文。
type ContextTool struct {
store *state.Store
refs References
style string
}
func NewContextTool(store *state.Store, refs References, style string) *ContextTool {
return &ContextTool{store: store, refs: refs, style: style}
}
func (t *ContextTool) Name() string { return "novel_context" }
func (t *ContextTool) Description() string {
return "获取小说创作上下文,包括基础设定、状态数据、前情摘要和写作参考资料"
}
func (t *ContextTool) Label() string { return "加载上下文" }
func (t *ContextTool) Schema() map[string]any {
return schema.Object(
schema.Property("chapter", schema.Int("章节号。不传则返回基础设定和模板(供 Architect 使用)")),
)
}
func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.RawMessage, error) {
var a struct {
Chapter int `json:"chapter"`
}
if err := json.Unmarshal(args, &a); err != nil {
return nil, fmt.Errorf("invalid args: %w", err)
}
result := make(map[string]any)
// 加载基础设定
if premise, err := t.store.LoadPremise(); err == nil && premise != "" {
result["premise"] = premise
}
if outline, err := t.store.LoadOutline(); err == nil && outline != nil {
result["outline"] = outline
}
if chars, err := t.store.LoadCharacters(); err == nil && chars != nil {
result["characters"] = chars
}
if rules, err := t.store.LoadWorldRules(); err == nil && len(rules) > 0 {
result["world_rules"] = rules
}
if a.Chapter > 0 {
// Writer/Editor 模式:加载章节相关上下文
if entry, err := t.store.GetChapterOutline(a.Chapter); err == nil {
result["current_chapter_outline"] = entry
}
if summaries, err := t.store.LoadRecentSummaries(a.Chapter, 2); err == nil && len(summaries) > 0 {
result["recent_summaries"] = summaries
}
// V1: 加载状态数据
if timeline, err := t.store.LoadTimeline(); err == nil && len(timeline) > 0 {
result["timeline"] = timeline
}
if foreshadow, err := t.store.LoadForeshadowLedger(); err == nil && len(foreshadow) > 0 {
result["foreshadow_ledger"] = foreshadow
}
if relationships, err := t.store.LoadRelationships(); err == nil && len(relationships) > 0 {
result["relationship_state"] = relationships
}
// V2: 加载场景级恢复状态
if progress, err := t.store.LoadProgress(); err == nil && progress != nil {
checkpoint := map[string]any{
"in_progress_chapter": progress.InProgressChapter,
"completed_scenes": progress.CompletedScenes,
}
result["checkpoint"] = checkpoint
}
// V2: 加载已有的章节规划(支持场景恢复跳过已完成场景)
if plan, err := t.store.LoadChapterPlan(a.Chapter); err == nil && plan != nil {
result["chapter_plan"] = plan
}
// 写作参考资料
result["references"] = t.writerReferences()
} else {
// Architect 模式:加载模板
result["references"] = t.architectReferences()
}
return json.Marshal(result)
}
func (t *ContextTool) writerReferences() map[string]string {
refs := map[string]string{}
add := func(k, v string) {
if v != "" {
refs[k] = v
}
}
add("chapter_guide", t.refs.ChapterGuide)
add("hook_techniques", t.refs.HookTechniques)
add("quality_checklist", t.refs.QualityChecklist)
add("chapter_template", t.refs.ChapterTemplate)
add("consistency", t.refs.Consistency)
add("content_expansion", t.refs.ContentExpansion)
add("dialogue_writing", t.refs.DialogueWriting)
add("style_reference", t.refs.StyleReference)
return refs
}
func (t *ContextTool) architectReferences() map[string]string {
refs := map[string]string{}
add := func(k, v string) {
if v != "" {
refs[k] = v
}
}
add("outline_template", t.refs.OutlineTemplate)
add("character_template", t.refs.CharacterTemplate)
return refs
}
// ContextSummary 返回当前状态的简要摘要(供日志使用)。
func (t *ContextTool) ContextSummary() string {
var parts []string
if p, _ := t.store.LoadPremise(); p != "" {
parts = append(parts, "premise:ok")
}
if o, _ := t.store.LoadOutline(); o != nil {
parts = append(parts, fmt.Sprintf("outline:%d chapters", len(o)))
}
if c, _ := t.store.LoadCharacters(); c != nil {
parts = append(parts, fmt.Sprintf("characters:%d", len(c)))
}
if len(parts) == 0 {
return "empty"
}
return strings.Join(parts, ", ")
}

67
tools/plan_chapter.go Normal file
View File

@@ -0,0 +1,67 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"github.com/voocel/agentcore/schema"
"github.com/voocel/ainovel-cli/domain"
"github.com/voocel/ainovel-cli/state"
)
// PlanChapterTool 生成章节规划。
type PlanChapterTool struct {
store *state.Store
}
func NewPlanChapterTool(store *state.Store) *PlanChapterTool {
return &PlanChapterTool{store: store}
}
func (t *PlanChapterTool) Name() string { return "plan_chapter" }
func (t *PlanChapterTool) Description() string {
return "创建章节写作规划,包括目标、冲突、场景拆分和钩子设计。必须在 write_scene 之前调用"
}
func (t *PlanChapterTool) Label() string { return "规划章节" }
func (t *PlanChapterTool) Schema() map[string]any {
sceneSchema := schema.Object(
schema.Property("index", schema.Int("场景编号,从 1 开始")).Required(),
schema.Property("summary", schema.String("场景概要")).Required(),
schema.Property("pov", schema.String("视角人物")),
schema.Property("location", schema.String("场景地点")),
)
return schema.Object(
schema.Property("chapter", schema.Int("章节号")).Required(),
schema.Property("title", schema.String("章节标题")).Required(),
schema.Property("goal", schema.String("本章目标")).Required(),
schema.Property("conflict", schema.String("核心冲突")).Required(),
schema.Property("scenes", schema.Array("场景列表", sceneSchema)).Required(),
schema.Property("hook", schema.String("章末钩子")).Required(),
schema.Property("emotion_arc", schema.String("情绪曲线")),
)
}
func (t *PlanChapterTool) Execute(_ context.Context, args json.RawMessage) (json.RawMessage, error) {
var plan domain.ChapterPlan
if err := json.Unmarshal(args, &plan); err != nil {
return nil, fmt.Errorf("invalid args: %w", err)
}
if plan.Chapter <= 0 {
return nil, fmt.Errorf("chapter must be > 0")
}
if len(plan.Scenes) == 0 {
return nil, fmt.Errorf("scenes must not be empty")
}
if err := t.store.SaveChapterPlan(plan); err != nil {
return nil, fmt.Errorf("save chapter plan: %w", err)
}
return json.Marshal(map[string]any{
"planned": true,
"chapter": plan.Chapter,
"scene_count": len(plan.Scenes),
})
}

59
tools/polish_chapter.go Normal file
View File

@@ -0,0 +1,59 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"unicode/utf8"
"github.com/voocel/agentcore/schema"
"github.com/voocel/ainovel-cli/state"
)
// PolishChapterTool 保存打磨后的章节正文,替换原始场景拼接。
type PolishChapterTool struct {
store *state.Store
}
func NewPolishChapterTool(store *state.Store) *PolishChapterTool {
return &PolishChapterTool{store: store}
}
func (t *PolishChapterTool) Name() string { return "polish_chapter" }
func (t *PolishChapterTool) Description() string {
return "保存打磨后的章节正文。在 write_scene 全部完成后、commit_chapter 之前调用。提交时会优先使用打磨版本"
}
func (t *PolishChapterTool) Label() string { return "打磨章节" }
func (t *PolishChapterTool) Schema() map[string]any {
return schema.Object(
schema.Property("chapter", schema.Int("章节号")).Required(),
schema.Property("content", schema.String("打磨后的完整章节正文")).Required(),
)
}
func (t *PolishChapterTool) Execute(_ context.Context, args json.RawMessage) (json.RawMessage, error) {
var a struct {
Chapter int `json:"chapter"`
Content string `json:"content"`
}
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 a.Content == "" {
return nil, fmt.Errorf("content must not be empty")
}
if err := t.store.SavePolished(a.Chapter, a.Content); err != nil {
return nil, fmt.Errorf("save polished: %w", err)
}
return json.Marshal(map[string]any{
"polished": true,
"chapter": a.Chapter,
"word_count": utf8.RuneCountInString(a.Content),
})
}

86
tools/save_foundation.go Normal file
View 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"
)
// SaveFoundationTool 保存基础设定premise/outline/charactersArchitect 专用。
type SaveFoundationTool struct {
store *state.Store
}
func NewSaveFoundationTool(store *state.Store) *SaveFoundationTool {
return &SaveFoundationTool{store: store}
}
func (t *SaveFoundationTool) Name() string { return "save_foundation" }
func (t *SaveFoundationTool) Description() string {
return "保存小说基础设定。type=premise 时 content 为 Markdowntype=outline 时 content 为 JSON 数组type=characters 时 content 为 JSON 数组type=world_rules 时 content 为 JSON 数组"
}
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(),
)
}
func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (json.RawMessage, error) {
var a struct {
Type string `json:"type"`
Content string `json:"content"`
}
if err := json.Unmarshal(args, &a); err != nil {
return nil, fmt.Errorf("invalid args: %w", err)
}
switch a.Type {
case "premise":
if err := t.store.SavePremise(a.Content); err != nil {
return nil, fmt.Errorf("save premise: %w", err)
}
_ = t.store.UpdatePhase(domain.PhasePremise)
return json.Marshal(map[string]any{"saved": true, "type": "premise"})
case "outline":
var entries []domain.OutlineEntry
if err := json.Unmarshal([]byte(a.Content), &entries); err != nil {
return nil, fmt.Errorf("parse outline JSON: %w", err)
}
if err := t.store.SaveOutline(entries); err != nil {
return nil, fmt.Errorf("save outline: %w", err)
}
_ = t.store.UpdatePhase(domain.PhaseOutline)
return json.Marshal(map[string]any{"saved": true, "type": "outline", "chapters": len(entries)})
case "characters":
var chars []domain.Character
if err := json.Unmarshal([]byte(a.Content), &chars); err != nil {
return nil, fmt.Errorf("parse characters JSON: %w", err)
}
if err := t.store.SaveCharacters(chars); err != nil {
return nil, fmt.Errorf("save characters: %w", err)
}
return json.Marshal(map[string]any{"saved": true, "type": "characters", "count": len(chars)})
case "world_rules":
var rules []domain.WorldRule
if err := json.Unmarshal([]byte(a.Content), &rules); err != nil {
return nil, fmt.Errorf("parse world_rules JSON: %w", err)
}
if err := t.store.SaveWorldRules(rules); err != nil {
return nil, fmt.Errorf("save world_rules: %w", err)
}
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)
}
}

70
tools/save_review.go Normal file
View File

@@ -0,0 +1,70 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"github.com/voocel/agentcore/schema"
"github.com/voocel/ainovel-cli/domain"
"github.com/voocel/ainovel-cli/state"
)
// SaveReviewTool 保存 Editor 的审阅结果。
type SaveReviewTool struct {
store *state.Store
}
func NewSaveReviewTool(store *state.Store) *SaveReviewTool {
return &SaveReviewTool{store: store}
}
func (t *SaveReviewTool) Name() string { return "save_review" }
func (t *SaveReviewTool) Description() string {
return "保存审阅结果。verdict 必须是 accept/polish/rewrite 之一"
}
func (t *SaveReviewTool) Label() string { return "保存审阅" }
func (t *SaveReviewTool) Schema() map[string]any {
issueSchema := schema.Object(
schema.Property("type", schema.Enum("问题类型", "timeline", "foreshadow", "relationship", "character", "pacing", "logic")).Required(),
schema.Property("severity", schema.Enum("严重程度", "error", "warning")).Required(),
schema.Property("description", schema.String("问题描述")).Required(),
schema.Property("suggestion", schema.String("修改建议")),
)
return schema.Object(
schema.Property("chapter", schema.Int("审阅的章节号(全局审阅填最新章节号)")).Required(),
schema.Property("scope", schema.Enum("审阅范围", "chapter", "global")).Required(),
schema.Property("issues", schema.Array("发现的问题", issueSchema)).Required(),
schema.Property("verdict", schema.Enum("审阅结论", "accept", "polish", "rewrite")).Required(),
schema.Property("summary", schema.String("审阅总结")).Required(),
schema.Property("affected_chapters", schema.Array("需要重写或打磨的章节号列表verdict 为 polish/rewrite 时必填)", schema.Int(""))),
)
}
func (t *SaveReviewTool) Execute(_ context.Context, args json.RawMessage) (json.RawMessage, error) {
var r domain.ReviewEntry
if err := json.Unmarshal(args, &r); err != nil {
return nil, fmt.Errorf("invalid args: %w", err)
}
if r.Chapter <= 0 {
return nil, fmt.Errorf("chapter must be > 0")
}
if err := t.store.SaveReview(r); err != nil {
return nil, fmt.Errorf("save review: %w", err)
}
// 写入信号文件供宿主读取
if err := t.store.SaveLastReview(r); err != nil {
return nil, fmt.Errorf("save review signal: %w", err)
}
return json.Marshal(map[string]any{
"saved": true,
"chapter": r.Chapter,
"scope": r.Scope,
"verdict": r.Verdict,
"issues": len(r.Issues),
})
}

76
tools/write_scene.go Normal file
View File

@@ -0,0 +1,76 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"unicode/utf8"
"github.com/voocel/agentcore/schema"
"github.com/voocel/ainovel-cli/domain"
"github.com/voocel/ainovel-cli/state"
)
// WriteSceneTool 写入单个场景草稿。
type WriteSceneTool struct {
store *state.Store
}
func NewWriteSceneTool(store *state.Store) *WriteSceneTool {
return &WriteSceneTool{store: store}
}
func (t *WriteSceneTool) Name() string { return "write_scene" }
func (t *WriteSceneTool) Description() string {
return "写入单个场景草稿。严格按场景级写作,每次只写一个场景。必须先调用 plan_chapter"
}
func (t *WriteSceneTool) Label() string { return "写入场景" }
func (t *WriteSceneTool) Schema() map[string]any {
return schema.Object(
schema.Property("chapter", schema.Int("章节号")).Required(),
schema.Property("scene", schema.Int("场景编号,从 1 开始")).Required(),
schema.Property("content", schema.String("场景正文")).Required(),
)
}
func (t *WriteSceneTool) Execute(_ context.Context, args json.RawMessage) (json.RawMessage, error) {
var a struct {
Chapter int `json:"chapter"`
Scene int `json:"scene"`
Content string `json:"content"`
}
if err := json.Unmarshal(args, &a); err != nil {
return nil, fmt.Errorf("invalid args: %w", err)
}
if a.Chapter <= 0 || a.Scene <= 0 {
return nil, fmt.Errorf("chapter and scene must be > 0")
}
if a.Content == "" {
return nil, fmt.Errorf("content must not be empty")
}
wordCount := utf8.RuneCountInString(a.Content)
draft := domain.SceneDraft{
Chapter: a.Chapter,
Scene: a.Scene,
Content: a.Content,
WordCount: wordCount,
}
if err := t.store.SaveSceneDraft(draft); err != nil {
return nil, fmt.Errorf("save scene draft: %w", err)
}
// 场景级 checkpoint
if err := t.store.MarkSceneComplete(a.Chapter, a.Scene); err != nil {
return nil, fmt.Errorf("mark scene complete: %w", err)
}
return json.Marshal(map[string]any{
"written": true,
"chapter": a.Chapter,
"scene": a.Scene,
"word_count": wordCount,
})
}