init
This commit is contained in:
125
tools/check_consistency.go
Normal file
125
tools/check_consistency.go
Normal 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
168
tools/commit_chapter.go
Normal 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)
|
||||
}
|
||||
110
tools/commit_chapter_test.go
Normal file
110
tools/commit_chapter_test.go
Normal 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
164
tools/context.go
Normal 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
67
tools/plan_chapter.go
Normal 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
59
tools/polish_chapter.go
Normal 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
86
tools/save_foundation.go
Normal 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/characters),Architect 专用。
|
||||
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 为 Markdown;type=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
70
tools/save_review.go
Normal 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
76
tools/write_scene.go
Normal 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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user