perf: 拆分规划策略
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/voocel/agentcore/schema"
|
||||
@@ -25,7 +26,9 @@ type References struct {
|
||||
ContentExpansion string
|
||||
DialogueWriting string
|
||||
// V2
|
||||
StyleReference string // 风格补充参考(可为空)
|
||||
StyleReference string // 风格补充参考(可为空)
|
||||
LongformPlanning string // 通用长篇规划参考
|
||||
Differentiation string // 通用差异化设计参考
|
||||
}
|
||||
|
||||
// ContextTool 组装当前章节所需上下文。
|
||||
@@ -60,22 +63,47 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
}
|
||||
|
||||
result := make(map[string]any)
|
||||
var warnings []string
|
||||
seenWarnings := make(map[string]struct{})
|
||||
warn := func(scope string, err error) {
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf("%s 读取失败: %v", scope, err)
|
||||
if _, ok := seenWarnings[msg]; ok {
|
||||
return
|
||||
}
|
||||
seenWarnings[msg] = struct{}{}
|
||||
warnings = append(warnings, msg)
|
||||
}
|
||||
|
||||
// 加载基础设定
|
||||
if premise, err := t.store.LoadPremise(); err == nil && premise != "" {
|
||||
result["premise"] = premise
|
||||
} else {
|
||||
warn("premise", err)
|
||||
}
|
||||
if outline, err := t.store.LoadOutline(); err == nil && outline != nil {
|
||||
result["outline"] = outline
|
||||
} else {
|
||||
warn("outline", err)
|
||||
}
|
||||
if rules, err := t.store.LoadWorldRules(); err == nil && len(rules) > 0 {
|
||||
result["world_rules"] = rules
|
||||
} else {
|
||||
warn("world_rules", err)
|
||||
}
|
||||
|
||||
if a.Chapter > 0 {
|
||||
// 根据总章节数计算上下文策略
|
||||
profile := domain.NewContextProfile(0)
|
||||
progress, _ := t.store.LoadProgress()
|
||||
progress, err := t.store.LoadProgress()
|
||||
warn("progress", err)
|
||||
runMeta, err := t.store.LoadRunMeta()
|
||||
warn("run_meta", err)
|
||||
if runMeta != nil && runMeta.PlanningTier != "" {
|
||||
result["planning_tier"] = runMeta.PlanningTier
|
||||
}
|
||||
if progress != nil && progress.TotalChapters > 0 {
|
||||
profile = domain.NewContextProfile(progress.TotalChapters)
|
||||
}
|
||||
@@ -86,26 +114,32 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
|
||||
// 角色加载:Layered 模式优先用快照,回退到原始设定
|
||||
if profile.Layered {
|
||||
t.loadLayeredCharacters(result, a.Chapter)
|
||||
t.loadLayeredCharacters(result, a.Chapter, warn)
|
||||
} else {
|
||||
t.loadFilteredCharacters(result, a.Chapter)
|
||||
t.loadFilteredCharacters(result, a.Chapter, warn)
|
||||
}
|
||||
|
||||
// Writer/Editor 模式:加载章节相关上下文
|
||||
if entry, err := t.store.GetChapterOutline(a.Chapter); err == nil {
|
||||
result["current_chapter_outline"] = entry
|
||||
} else {
|
||||
warn("current_chapter_outline", err)
|
||||
}
|
||||
|
||||
// 摘要加载:分层 vs 扁平
|
||||
if profile.Layered {
|
||||
t.loadLayeredSummaries(result, a.Chapter, profile.SummaryWindow)
|
||||
t.loadLayeredSummaries(result, a.Chapter, profile.SummaryWindow, warn)
|
||||
} else if profile.FullContext {
|
||||
if summaries, err := t.store.LoadAllSummaries(a.Chapter); err == nil && len(summaries) > 0 {
|
||||
result["recent_summaries"] = summaries
|
||||
} else {
|
||||
warn("recent_summaries", err)
|
||||
}
|
||||
} else {
|
||||
if summaries, err := t.store.LoadRecentSummaries(a.Chapter, profile.SummaryWindow); err == nil && len(summaries) > 0 {
|
||||
result["recent_summaries"] = summaries
|
||||
} else {
|
||||
warn("recent_summaries", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,29 +147,41 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
if profile.FullContext {
|
||||
if timeline, err := t.store.LoadTimeline(); err == nil && len(timeline) > 0 {
|
||||
result["timeline"] = timeline
|
||||
} else {
|
||||
warn("timeline", err)
|
||||
}
|
||||
} else {
|
||||
if timeline, err := t.store.LoadRecentTimeline(a.Chapter, profile.TimelineWindow); err == nil && len(timeline) > 0 {
|
||||
result["timeline"] = timeline
|
||||
} else {
|
||||
warn("timeline", err)
|
||||
}
|
||||
}
|
||||
// foreshadow:短篇全量,否则只取未回收条目
|
||||
if profile.FullContext {
|
||||
if foreshadow, err := t.store.LoadForeshadowLedger(); err == nil && len(foreshadow) > 0 {
|
||||
result["foreshadow_ledger"] = foreshadow
|
||||
} else {
|
||||
warn("foreshadow_ledger", err)
|
||||
}
|
||||
} else {
|
||||
if foreshadow, err := t.store.LoadActiveForeshadow(); err == nil && len(foreshadow) > 0 {
|
||||
result["foreshadow_ledger"] = foreshadow
|
||||
} else {
|
||||
warn("foreshadow_ledger", err)
|
||||
}
|
||||
}
|
||||
// relationships:保持全量(pair-key 去重,数据量天然可控)
|
||||
if relationships, err := t.store.LoadRelationships(); err == nil && len(relationships) > 0 {
|
||||
result["relationship_state"] = relationships
|
||||
} else {
|
||||
warn("relationship_state", err)
|
||||
}
|
||||
// 状态变化:最近 5 章的角色/实体状态变化
|
||||
if changes, err := t.store.LoadRecentStateChanges(a.Chapter, 5); err == nil && len(changes) > 0 {
|
||||
result["recent_state_changes"] = changes
|
||||
} else {
|
||||
warn("recent_state_changes", err)
|
||||
}
|
||||
|
||||
// Layered 模式:注入当前卷弧位置 + 弧目标/卷主题
|
||||
@@ -159,6 +205,8 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn("layered_outline", err)
|
||||
}
|
||||
result["position"] = pos
|
||||
}
|
||||
@@ -180,26 +228,42 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
// 加载已有的章节规划(支持场景恢复跳过已完成场景)
|
||||
if plan, err := t.store.LoadChapterPlan(a.Chapter); err == nil && plan != nil {
|
||||
result["chapter_plan"] = plan
|
||||
} else {
|
||||
warn("chapter_plan", err)
|
||||
}
|
||||
|
||||
// 写作参考资料分阶段加载
|
||||
result["references"] = t.writerReferences(a.Chapter)
|
||||
} else {
|
||||
runMeta, err := t.store.LoadRunMeta()
|
||||
warn("run_meta", err)
|
||||
if runMeta != nil && runMeta.PlanningTier != "" {
|
||||
result["planning_tier"] = runMeta.PlanningTier
|
||||
}
|
||||
// Architect 模式:全量角色 + 模板
|
||||
if chars, err := t.store.LoadCharacters(); err == nil && chars != nil {
|
||||
result["characters"] = chars
|
||||
} else {
|
||||
warn("characters", err)
|
||||
}
|
||||
// Architect 模式下也加载分层大纲(弧级规划需要看全貌)
|
||||
if layered, err := t.store.LoadLayeredOutline(); err == nil && len(layered) > 0 {
|
||||
result["layered_outline"] = layered
|
||||
} else {
|
||||
warn("layered_outline", err)
|
||||
}
|
||||
// 加载已有的弧摘要(弧级规划时需要参考前续弧的内容)
|
||||
if volSummaries, err := t.store.LoadAllVolumeSummaries(); err == nil && len(volSummaries) > 0 {
|
||||
result["volume_summaries"] = volSummaries
|
||||
} else {
|
||||
warn("volume_summaries", err)
|
||||
}
|
||||
result["references"] = t.architectReferences()
|
||||
}
|
||||
|
||||
if len(warnings) > 0 {
|
||||
result["_warnings"] = warnings
|
||||
}
|
||||
result["_loading_summary"] = buildLoadingSummary(result, a.Chapter)
|
||||
return json.Marshal(result)
|
||||
}
|
||||
@@ -213,6 +277,9 @@ func buildLoadingSummary(result map[string]any, chapter int) string {
|
||||
} else {
|
||||
parts = append(parts, "architect")
|
||||
}
|
||||
if tier, ok := result["planning_tier"].(domain.PlanningTier); ok && tier != "" {
|
||||
parts = append(parts, fmt.Sprintf("tier=%s", tier))
|
||||
}
|
||||
|
||||
// 卷弧位置
|
||||
if pos, ok := result["position"].(map[string]any); ok {
|
||||
@@ -272,6 +339,9 @@ func buildLoadingSummary(result map[string]any, chapter int) string {
|
||||
if refs, ok := result["references"].(map[string]string); ok && len(refs) > 0 {
|
||||
items = append(items, fmt.Sprintf("参考:%d项", len(refs)))
|
||||
}
|
||||
if warnings, ok := result["_warnings"].([]string); ok && len(warnings) > 0 {
|
||||
items = append(items, fmt.Sprintf("告警:%d", len(warnings)))
|
||||
}
|
||||
|
||||
if len(items) > 0 {
|
||||
parts = append(parts, strings.Join(items, " "))
|
||||
@@ -309,15 +379,20 @@ func sliceLen(v any) int {
|
||||
|
||||
// loadFilteredCharacters 按 Tier 和场景出场过滤角色。
|
||||
// core/important 始终返回;secondary/decorative 只在当前章节大纲提及时返回。
|
||||
func (t *ContextTool) loadFilteredCharacters(result map[string]any, chapter int) {
|
||||
func (t *ContextTool) loadFilteredCharacters(result map[string]any, chapter int, warn func(string, error)) {
|
||||
chars, err := t.store.LoadCharacters()
|
||||
if err != nil || len(chars) == 0 {
|
||||
if err != nil {
|
||||
warn("characters", err)
|
||||
return
|
||||
}
|
||||
if len(chars) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前章节大纲的场景描述,用于匹配次要角色
|
||||
entry, err := t.store.GetChapterOutline(chapter)
|
||||
if err != nil {
|
||||
warn("current_chapter_outline", err)
|
||||
result["characters"] = chars
|
||||
return
|
||||
}
|
||||
@@ -351,12 +426,15 @@ func matchCharacter(text string, c domain.Character) bool {
|
||||
}
|
||||
|
||||
// loadLayeredSummaries 分层摘要加载:卷摘要 + 当前卷弧摘要 + 弧内章摘要。
|
||||
func (t *ContextTool) loadLayeredSummaries(result map[string]any, chapter, summaryWindow int) {
|
||||
func (t *ContextTool) loadLayeredSummaries(result map[string]any, chapter, summaryWindow int, warn func(string, error)) {
|
||||
vol, arc, err := t.store.LocateChapter(chapter)
|
||||
if err != nil {
|
||||
warn("layered_outline_position", err)
|
||||
// 回退到扁平模式
|
||||
if summaries, err := t.store.LoadRecentSummaries(chapter, summaryWindow); err == nil && len(summaries) > 0 {
|
||||
result["recent_summaries"] = summaries
|
||||
} else {
|
||||
warn("recent_summaries", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -364,6 +442,8 @@ func (t *ContextTool) loadLayeredSummaries(result map[string]any, chapter, summa
|
||||
// 1. 已完成卷的卷摘要
|
||||
if volSummaries, err := t.store.LoadAllVolumeSummaries(); err == nil && len(volSummaries) > 0 {
|
||||
result["volume_summaries"] = volSummaries
|
||||
} else {
|
||||
warn("volume_summaries", err)
|
||||
}
|
||||
|
||||
// 2. 当前卷内已完成弧的弧摘要(不含当前弧)
|
||||
@@ -377,25 +457,30 @@ func (t *ContextTool) loadLayeredSummaries(result map[string]any, chapter, summa
|
||||
if len(prior) > 0 {
|
||||
result["arc_summaries"] = prior
|
||||
}
|
||||
} else {
|
||||
warn("arc_summaries", err)
|
||||
}
|
||||
|
||||
// 3. 当前弧内最近 N 章的章摘要
|
||||
if summaries, err := t.store.LoadRecentSummaries(chapter, summaryWindow); err == nil && len(summaries) > 0 {
|
||||
result["recent_summaries"] = summaries
|
||||
} else {
|
||||
warn("recent_summaries", err)
|
||||
}
|
||||
}
|
||||
|
||||
// loadLayeredCharacters Layered 模式下的角色加载:优先用最近快照,回退到原始设定 + Tier 过滤。
|
||||
func (t *ContextTool) loadLayeredCharacters(result map[string]any, chapter int) {
|
||||
func (t *ContextTool) loadLayeredCharacters(result map[string]any, chapter int, warn func(string, error)) {
|
||||
snapshots, err := t.store.LoadLatestSnapshots()
|
||||
if err == nil && len(snapshots) > 0 {
|
||||
result["character_snapshots"] = snapshots
|
||||
// 同时保留原始设定中的 core/important 角色(快照可能不含新登场角色)
|
||||
t.loadFilteredCharacters(result, chapter)
|
||||
t.loadFilteredCharacters(result, chapter, warn)
|
||||
return
|
||||
}
|
||||
warn("character_snapshots", err)
|
||||
// 无快照时回退到原始设定
|
||||
t.loadFilteredCharacters(result, chapter)
|
||||
t.loadFilteredCharacters(result, chapter, warn)
|
||||
}
|
||||
|
||||
// writerReferences 返回写作参考资料。章节 1 返回全量,后续章节裁剪掉不再需要的模板。
|
||||
@@ -431,6 +516,9 @@ func (t *ContextTool) architectReferences() map[string]string {
|
||||
}
|
||||
add("outline_template", t.refs.OutlineTemplate)
|
||||
add("character_template", t.refs.CharacterTemplate)
|
||||
add("longform_planning", t.refs.LongformPlanning)
|
||||
add("differentiation", t.refs.Differentiation)
|
||||
add("style_reference", t.refs.StyleReference)
|
||||
return refs
|
||||
}
|
||||
|
||||
|
||||
67
tools/novel_context_test.go
Normal file
67
tools/novel_context_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/voocel/ainovel-cli/state"
|
||||
)
|
||||
|
||||
func TestContextToolReportsWarningsForCorruptedState(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store := state.NewStore(dir)
|
||||
if err := store.Init(); err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(dir, "outline.json"), []byte("{invalid"), 0o644); err != nil {
|
||||
t.Fatalf("write outline.json: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "meta", "progress.json"), []byte("{invalid"), 0o644); err != nil {
|
||||
t.Fatalf("write progress.json: %v", err)
|
||||
}
|
||||
|
||||
tool := NewContextTool(store, References{}, "default")
|
||||
args, err := json.Marshal(map[string]any{"chapter": 2})
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal: %v", err)
|
||||
}
|
||||
|
||||
result, err := tool.Execute(context.Background(), args)
|
||||
if err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Warnings []string `json:"_warnings"`
|
||||
Summary string `json:"_loading_summary"`
|
||||
}
|
||||
if err := json.Unmarshal(result, &payload); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if len(payload.Warnings) == 0 {
|
||||
t.Fatal("expected context warnings for corrupted files")
|
||||
}
|
||||
if !containsWarning(payload.Warnings, "outline") {
|
||||
t.Fatalf("expected outline warning, got %v", payload.Warnings)
|
||||
}
|
||||
if !containsWarning(payload.Warnings, "progress") {
|
||||
t.Fatalf("expected progress warning, got %v", payload.Warnings)
|
||||
}
|
||||
if !strings.Contains(payload.Summary, "告警:") {
|
||||
t.Fatalf("expected loading summary to contain warning count, got %q", payload.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
func containsWarning(warnings []string, key string) bool {
|
||||
for _, warning := range warnings {
|
||||
if strings.Contains(warning, key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -21,7 +21,7 @@ func NewSaveFoundationTool(store *state.Store) *SaveFoundationTool {
|
||||
|
||||
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 数组"
|
||||
return "保存小说基础设定。type=premise 时 content 为 Markdown;type=outline 时 content 为 JSON 数组;type=characters 时 content 为 JSON 数组;type=world_rules 时 content 为 JSON 数组。scale 可选,用于记录 short/mid/long 规划级别"
|
||||
}
|
||||
func (t *SaveFoundationTool) Label() string { return "保存设定" }
|
||||
|
||||
@@ -29,6 +29,7 @@ func (t *SaveFoundationTool) Schema() map[string]any {
|
||||
return schema.Object(
|
||||
schema.Property("type", schema.Enum("设定类型", "premise", "outline", "layered_outline", "characters", "world_rules")).Required(),
|
||||
schema.Property("content", schema.String("内容。premise 为 Markdown 文本,outline/layered_outline/characters/world_rules 为 JSON 字符串")).Required(),
|
||||
schema.Property("scale", schema.Enum("规划级别", "short", "mid", "long")),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,10 +37,21 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
var a struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
Scale string `json:"scale"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return nil, fmt.Errorf("invalid args: %w", err)
|
||||
}
|
||||
if a.Scale != "" {
|
||||
switch domain.PlanningTier(a.Scale) {
|
||||
case domain.PlanningTierShort, domain.PlanningTierMid, domain.PlanningTierLong:
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid scale %q, expected short/mid/long", a.Scale)
|
||||
}
|
||||
if err := t.store.SetPlanningTier(domain.PlanningTier(a.Scale)); err != nil {
|
||||
return nil, fmt.Errorf("save planning tier: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
switch a.Type {
|
||||
case "premise":
|
||||
@@ -47,7 +59,7 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
return nil, fmt.Errorf("save premise: %w", err)
|
||||
}
|
||||
_ = t.store.UpdatePhase(domain.PhasePremise)
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "premise"})
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "premise", "scale": a.Scale})
|
||||
|
||||
case "outline":
|
||||
var entries []domain.OutlineEntry
|
||||
@@ -60,7 +72,12 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
_ = t.store.UpdatePhase(domain.PhaseOutline)
|
||||
// 根据大纲长度自动设定总章节数
|
||||
_ = t.store.SetTotalChapters(len(entries))
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "outline", "chapters": len(entries)})
|
||||
if domain.PlanningTier(a.Scale) != domain.PlanningTierLong {
|
||||
_ = t.store.SetLayered(false)
|
||||
_ = t.store.UpdateVolumeArc(0, 0)
|
||||
_ = t.store.ClearLayeredOutline()
|
||||
}
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "outline", "chapters": len(entries), "scale": a.Scale})
|
||||
|
||||
case "layered_outline":
|
||||
var volumes []domain.VolumeOutline
|
||||
@@ -85,6 +102,7 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
return json.Marshal(map[string]any{
|
||||
"saved": true, "type": "layered_outline",
|
||||
"volumes": len(volumes), "chapters": total,
|
||||
"scale": a.Scale,
|
||||
})
|
||||
|
||||
case "characters":
|
||||
@@ -95,7 +113,7 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
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)})
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "characters", "count": len(chars), "scale": a.Scale})
|
||||
|
||||
case "world_rules":
|
||||
var rules []domain.WorldRule
|
||||
@@ -105,7 +123,7 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
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)})
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "world_rules", "count": len(rules), "scale": a.Scale})
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown type %q, expected premise/outline/layered_outline/characters/world_rules", a.Type)
|
||||
|
||||
113
tools/save_foundation_test.go
Normal file
113
tools/save_foundation_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/voocel/ainovel-cli/domain"
|
||||
"github.com/voocel/ainovel-cli/state"
|
||||
)
|
||||
|
||||
func TestSaveFoundationPersistsPlanningTier(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store := state.NewStore(dir)
|
||||
if err := store.Init(); err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
tool := NewSaveFoundationTool(store)
|
||||
args, err := json.Marshal(map[string]any{
|
||||
"type": "premise",
|
||||
"content": "# Premise\n\n测试",
|
||||
"scale": "long",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal: %v", err)
|
||||
}
|
||||
|
||||
if _, err := tool.Execute(context.Background(), args); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
|
||||
meta, err := store.LoadRunMeta()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadRunMeta: %v", err)
|
||||
}
|
||||
if meta == nil {
|
||||
t.Fatal("expected run meta to exist")
|
||||
}
|
||||
if meta.PlanningTier != domain.PlanningTierLong {
|
||||
t.Fatalf("expected planning tier %q, got %q", domain.PlanningTierLong, meta.PlanningTier)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveFoundationOutlineClearsLayeredStateWhenDowngrading(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", 0); err != nil {
|
||||
t.Fatalf("InitProgress: %v", err)
|
||||
}
|
||||
|
||||
tool := NewSaveFoundationTool(store)
|
||||
|
||||
layeredArgs, err := json.Marshal(map[string]any{
|
||||
"type": "layered_outline",
|
||||
"content": `[{"index":1,"title":"第一卷","theme":"主题","arcs":[{"index":1,"title":"第一弧","goal":"目标","chapters":[{"chapter":1,"title":"第一章","core_event":"开局","hook":"继续"}]}]}]`,
|
||||
"scale": "long",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal layered args: %v", err)
|
||||
}
|
||||
if _, err := tool.Execute(context.Background(), layeredArgs); err != nil {
|
||||
t.Fatalf("Execute layered outline: %v", err)
|
||||
}
|
||||
|
||||
outlineArgs, err := json.Marshal(map[string]any{
|
||||
"type": "outline",
|
||||
"content": `[{"chapter":1,"title":"第一章","core_event":"改为中篇","hook":"继续"}]`,
|
||||
"scale": "mid",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal outline args: %v", err)
|
||||
}
|
||||
if _, err := tool.Execute(context.Background(), outlineArgs); err != nil {
|
||||
t.Fatalf("Execute outline: %v", err)
|
||||
}
|
||||
|
||||
progress, err := store.LoadProgress()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadProgress: %v", err)
|
||||
}
|
||||
if progress == nil {
|
||||
t.Fatal("expected progress to exist")
|
||||
}
|
||||
if progress.Layered {
|
||||
t.Fatal("expected layered mode to be disabled")
|
||||
}
|
||||
if progress.CurrentVolume != 0 || progress.CurrentArc != 0 {
|
||||
t.Fatalf("expected volume/arc reset, got volume=%d arc=%d", progress.CurrentVolume, progress.CurrentArc)
|
||||
}
|
||||
|
||||
volumes, err := store.LoadLayeredOutline()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadLayeredOutline: %v", err)
|
||||
}
|
||||
if len(volumes) != 0 {
|
||||
t.Fatalf("expected layered outline cleared, got %d volumes", len(volumes))
|
||||
}
|
||||
|
||||
meta, err := store.LoadRunMeta()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadRunMeta: %v", err)
|
||||
}
|
||||
if meta == nil {
|
||||
t.Fatal("expected run meta to exist")
|
||||
}
|
||||
if meta.PlanningTier != domain.PlanningTierMid {
|
||||
t.Fatalf("expected planning tier %q, got %q", domain.PlanningTierMid, meta.PlanningTier)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user