239 lines
7.3 KiB
Go
239 lines
7.3 KiB
Go
package tools
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/voocel/agentcore/schema"
|
||
"github.com/voocel/ainovel-cli/domain"
|
||
"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 rules, err := t.store.LoadWorldRules(); err == nil && len(rules) > 0 {
|
||
result["world_rules"] = rules
|
||
}
|
||
|
||
if a.Chapter > 0 {
|
||
// 根据总章节数计算上下文策略
|
||
profile := domain.NewContextProfile(0)
|
||
if progress, err := t.store.LoadProgress(); err == nil && progress != nil && progress.TotalChapters > 0 {
|
||
profile = domain.NewContextProfile(progress.TotalChapters)
|
||
}
|
||
|
||
// 角色按 Tier 过滤:core/important 始终返回,secondary/decorative 按出场匹配
|
||
t.loadFilteredCharacters(result, a.Chapter)
|
||
|
||
// Writer/Editor 模式:加载章节相关上下文
|
||
if entry, err := t.store.GetChapterOutline(a.Chapter); err == nil {
|
||
result["current_chapter_outline"] = entry
|
||
}
|
||
if profile.FullContext {
|
||
if summaries, err := t.store.LoadAllSummaries(a.Chapter); err == nil && len(summaries) > 0 {
|
||
result["recent_summaries"] = summaries
|
||
}
|
||
} else {
|
||
if summaries, err := t.store.LoadRecentSummaries(a.Chapter, profile.SummaryWindow); err == nil && len(summaries) > 0 {
|
||
result["recent_summaries"] = summaries
|
||
}
|
||
}
|
||
|
||
// 状态数据按策略加载
|
||
if profile.FullContext {
|
||
if timeline, err := t.store.LoadTimeline(); err == nil && len(timeline) > 0 {
|
||
result["timeline"] = timeline
|
||
}
|
||
} else {
|
||
if timeline, err := t.store.LoadRecentTimeline(a.Chapter, profile.TimelineWindow); err == nil && len(timeline) > 0 {
|
||
result["timeline"] = timeline
|
||
}
|
||
}
|
||
// foreshadow:短篇全量,否则只取未回收条目
|
||
if profile.FullContext {
|
||
if foreshadow, err := t.store.LoadForeshadowLedger(); err == nil && len(foreshadow) > 0 {
|
||
result["foreshadow_ledger"] = foreshadow
|
||
}
|
||
} else {
|
||
if foreshadow, err := t.store.LoadActiveForeshadow(); err == nil && len(foreshadow) > 0 {
|
||
result["foreshadow_ledger"] = foreshadow
|
||
}
|
||
}
|
||
// relationships:保持全量(pair-key 去重,数据量天然可控)
|
||
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,
|
||
}
|
||
if len(progress.StrandHistory) > 0 {
|
||
checkpoint["strand_history"] = progress.StrandHistory
|
||
}
|
||
if len(progress.HookHistory) > 0 {
|
||
checkpoint["hook_history"] = progress.HookHistory
|
||
}
|
||
result["checkpoint"] = checkpoint
|
||
}
|
||
// V2: 加载已有的章节规划(支持场景恢复跳过已完成场景)
|
||
if plan, err := t.store.LoadChapterPlan(a.Chapter); err == nil && plan != nil {
|
||
result["chapter_plan"] = plan
|
||
}
|
||
|
||
// V3: 写作参考资料分阶段加载
|
||
result["references"] = t.writerReferences(a.Chapter)
|
||
} else {
|
||
// Architect 模式:全量角色 + 模板
|
||
if chars, err := t.store.LoadCharacters(); err == nil && chars != nil {
|
||
result["characters"] = chars
|
||
}
|
||
result["references"] = t.architectReferences()
|
||
}
|
||
|
||
return json.Marshal(result)
|
||
}
|
||
|
||
// loadFilteredCharacters 按 Tier 和场景出场过滤角色。
|
||
// core/important 始终返回;secondary/decorative 只在当前章节大纲提及时返回。
|
||
func (t *ContextTool) loadFilteredCharacters(result map[string]any, chapter int) {
|
||
chars, err := t.store.LoadCharacters()
|
||
if err != nil || len(chars) == 0 {
|
||
return
|
||
}
|
||
|
||
// 获取当前章节大纲的场景描述,用于匹配次要角色
|
||
entry, err := t.store.GetChapterOutline(chapter)
|
||
if err != nil {
|
||
result["characters"] = chars
|
||
return
|
||
}
|
||
sceneText := strings.Join(entry.Scenes, " ") + " " + entry.CoreEvent + " " + entry.Title
|
||
|
||
var filtered []domain.Character
|
||
for _, c := range chars {
|
||
switch c.Tier {
|
||
case "secondary", "decorative":
|
||
if strings.Contains(sceneText, c.Name) {
|
||
filtered = append(filtered, c)
|
||
}
|
||
default: // core, important, 或未设置
|
||
filtered = append(filtered, c)
|
||
}
|
||
}
|
||
result["characters"] = filtered
|
||
}
|
||
|
||
// writerReferences 返回写作参考资料。章节 1 返回全量,后续章节裁剪掉不再需要的模板。
|
||
func (t *ContextTool) writerReferences(chapter int) 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("consistency", t.refs.Consistency)
|
||
add("dialogue_writing", t.refs.DialogueWriting)
|
||
add("style_reference", t.refs.StyleReference)
|
||
|
||
// 仅首章加载的补充参考(后续章节不再需要)
|
||
if chapter <= 1 {
|
||
add("chapter_template", t.refs.ChapterTemplate)
|
||
add("content_expansion", t.refs.ContentExpansion)
|
||
}
|
||
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, ", ")
|
||
}
|