init
This commit is contained in:
84
app/agents.go
Normal file
84
app/agents.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/voocel/agentcore"
|
||||
"github.com/voocel/ainovel-cli/state"
|
||||
"github.com/voocel/ainovel-cli/tools"
|
||||
)
|
||||
|
||||
// BuildCoordinator 组装 Coordinator Agent 及其 SubAgent。
|
||||
func BuildCoordinator(
|
||||
cfg Config,
|
||||
store *state.Store,
|
||||
model agentcore.ChatModel,
|
||||
refs tools.References,
|
||||
prompts Prompts,
|
||||
styles map[string]string,
|
||||
) *agentcore.Agent {
|
||||
// 共享工具
|
||||
contextTool := tools.NewContextTool(store, refs, cfg.Style)
|
||||
|
||||
// Architect SubAgent 工具
|
||||
architectTools := []agentcore.Tool{
|
||||
contextTool,
|
||||
tools.NewSaveFoundationTool(store),
|
||||
}
|
||||
|
||||
// Writer SubAgent 工具(V1: +polish_chapter +check_consistency)
|
||||
writerTools := []agentcore.Tool{
|
||||
contextTool,
|
||||
tools.NewPlanChapterTool(store),
|
||||
tools.NewWriteSceneTool(store),
|
||||
tools.NewPolishChapterTool(store),
|
||||
tools.NewCheckConsistencyTool(store),
|
||||
tools.NewCommitChapterTool(store),
|
||||
}
|
||||
|
||||
// Editor SubAgent 工具(V1)
|
||||
editorTools := []agentcore.Tool{
|
||||
contextTool,
|
||||
tools.NewSaveReviewTool(store),
|
||||
}
|
||||
|
||||
architect := agentcore.SubAgentConfig{
|
||||
Name: "architect",
|
||||
Description: "世界构建师:生成小说前提、大纲和角色档案",
|
||||
Model: model,
|
||||
SystemPrompt: prompts.Architect,
|
||||
Tools: architectTools,
|
||||
MaxTurns: 10,
|
||||
}
|
||||
|
||||
// 动态拼接风格指令到 Writer prompt
|
||||
writerPrompt := prompts.Writer
|
||||
if style, ok := styles[cfg.Style]; ok {
|
||||
writerPrompt += "\n\n" + style
|
||||
}
|
||||
|
||||
writer := agentcore.SubAgentConfig{
|
||||
Name: "writer",
|
||||
Description: "场景写作者:逐场景完成一章的创作,包含打磨和一致性检查",
|
||||
Model: model,
|
||||
SystemPrompt: writerPrompt,
|
||||
Tools: writerTools,
|
||||
MaxTurns: 25,
|
||||
}
|
||||
|
||||
editor := agentcore.SubAgentConfig{
|
||||
Name: "editor",
|
||||
Description: "全局审阅者:发现跨章结构问题,输出审阅结果",
|
||||
Model: model,
|
||||
SystemPrompt: prompts.Editor,
|
||||
Tools: editorTools,
|
||||
MaxTurns: 10,
|
||||
}
|
||||
|
||||
subagentTool := agentcore.NewSubAgentTool(architect, writer, editor)
|
||||
|
||||
return agentcore.NewAgent(
|
||||
agentcore.WithModel(model),
|
||||
agentcore.WithSystemPrompt(prompts.Coordinator),
|
||||
agentcore.WithTools(subagentTool, contextTool),
|
||||
agentcore.WithMaxTurns(60),
|
||||
)
|
||||
}
|
||||
56
app/config.go
Normal file
56
app/config.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Config 小说应用配置。
|
||||
type Config struct {
|
||||
Prompt string // 用户的小说需求
|
||||
NovelName string // 小说名(用作输出目录名)
|
||||
OutputDir string // 输出根目录,默认 output/{NovelName}
|
||||
ModelName string // LLM 模型名
|
||||
APIKey string // API Key
|
||||
BaseURL string // API Base URL(可选)
|
||||
MaxChapters int // 最大章节数
|
||||
Style string // 写作风格(default/suspense/fantasy/romance)
|
||||
}
|
||||
|
||||
// Prompts 嵌入的提示词。
|
||||
type Prompts struct {
|
||||
Coordinator string
|
||||
Architect string
|
||||
Writer string
|
||||
Editor string
|
||||
}
|
||||
|
||||
// Validate 校验配置。
|
||||
func (c *Config) Validate() error {
|
||||
if c.Prompt == "" {
|
||||
return fmt.Errorf("prompt is required")
|
||||
}
|
||||
if c.APIKey == "" {
|
||||
return fmt.Errorf("api key is required (set OPENAI_API_KEY)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FillDefaults 填充默认值。
|
||||
func (c *Config) FillDefaults() {
|
||||
if c.NovelName == "" {
|
||||
c.NovelName = "novel"
|
||||
}
|
||||
if c.OutputDir == "" {
|
||||
c.OutputDir = filepath.Join("output", c.NovelName)
|
||||
}
|
||||
if c.ModelName == "" {
|
||||
c.ModelName = "gpt-4o"
|
||||
}
|
||||
if c.Style == "" {
|
||||
c.Style = "default"
|
||||
}
|
||||
if c.MaxChapters <= 0 {
|
||||
c.MaxChapters = 3
|
||||
}
|
||||
}
|
||||
351
app/run.go
Normal file
351
app/run.go
Normal file
@@ -0,0 +1,351 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/voocel/agentcore"
|
||||
"github.com/voocel/agentcore/llm"
|
||||
"github.com/voocel/ainovel-cli/domain"
|
||||
"github.com/voocel/ainovel-cli/state"
|
||||
"github.com/voocel/ainovel-cli/tools"
|
||||
)
|
||||
|
||||
// Run 启动小说创作流程。
|
||||
func Run(cfg Config, refs tools.References, prompts Prompts, styles map[string]string) error {
|
||||
cfg.FillDefaults()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. 初始化状态
|
||||
store := state.NewStore(cfg.OutputDir)
|
||||
if err := store.Init(); err != nil {
|
||||
return fmt.Errorf("init store: %w", err)
|
||||
}
|
||||
|
||||
// 2. 创建模型
|
||||
var baseURL []string
|
||||
if cfg.BaseURL != "" {
|
||||
baseURL = append(baseURL, cfg.BaseURL)
|
||||
}
|
||||
model, err := llm.NewOpenAIModel(cfg.ModelName, cfg.APIKey, baseURL...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create model: %w", err)
|
||||
}
|
||||
|
||||
// 3. 组装 Coordinator
|
||||
coordinator := BuildCoordinator(cfg, store, model, refs, prompts, styles)
|
||||
|
||||
// 4. 确定性控制面:事件监听 + FollowUp 注入
|
||||
coordinator.Subscribe(func(ev agentcore.Event) {
|
||||
switch ev.Type {
|
||||
case agentcore.EventToolExecStart:
|
||||
log.Printf("[tool:start] %s", ev.Tool)
|
||||
|
||||
case agentcore.EventToolExecEnd:
|
||||
if ev.IsError {
|
||||
log.Printf("[tool:error] %s", ev.Tool)
|
||||
return
|
||||
}
|
||||
log.Printf("[tool:done] %s → %s", ev.Tool, truncateLog(string(ev.Result), 200))
|
||||
|
||||
// 宿主确定性控制:SubAgent 完成后读取信号文件
|
||||
if ev.Tool == "subagent" {
|
||||
handleSubAgentDone(coordinator, store, cfg.MaxChapters)
|
||||
handleEditorDone(coordinator, store)
|
||||
}
|
||||
|
||||
case agentcore.EventMessageEnd:
|
||||
if ev.Message != nil && ev.Message.GetRole() == agentcore.RoleAssistant {
|
||||
log.Printf("[assistant] %s", truncateLog(ev.Message.TextContent(), 300))
|
||||
}
|
||||
|
||||
case agentcore.EventError:
|
||||
log.Printf("[error] %v", ev.Err)
|
||||
}
|
||||
})
|
||||
|
||||
// 5. 初始化运行元信息(保留已有 SteerHistory)
|
||||
if err := store.InitRunMeta(cfg.Style, cfg.ModelName); err != nil {
|
||||
log.Printf("[warn] 初始化运行元信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 6. Steer 协程:stdin 读取用户干预
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
log.Printf("[steer] 用户干预: %s", text)
|
||||
if err := store.AppendSteerEntry(domain.SteerEntry{
|
||||
Input: text,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}); err != nil {
|
||||
log.Printf("[warn] 追加干预记录失败: %v", err)
|
||||
}
|
||||
if err := store.SetPendingSteer(text); err != nil {
|
||||
log.Printf("[warn] 设置待处理干预失败: %v", err)
|
||||
}
|
||||
if err := store.SetFlow(domain.FlowSteering); err != nil {
|
||||
log.Printf("[warn] 设置流程状态失败: %v", err)
|
||||
}
|
||||
coordinator.Steer(agentcore.UserMsg(fmt.Sprintf(
|
||||
"[用户干预] %s\n请评估影响范围,决定是否需要修改设定或重写已有章节。", text)))
|
||||
}
|
||||
}()
|
||||
|
||||
// 7. 恢复或启动(按优先级链)
|
||||
progress, _ := store.LoadProgress()
|
||||
runMeta, _ := store.LoadRunMeta()
|
||||
if progress != nil && progress.InProgressChapter > 0 {
|
||||
// 场景级恢复:章节写到一半
|
||||
ch := progress.InProgressChapter
|
||||
scenes := len(progress.CompletedScenes)
|
||||
log.Printf("场景级恢复:第 %d 章已完成 %d 个场景", ch, scenes)
|
||||
if err := coordinator.Prompt(fmt.Sprintf(
|
||||
"第 %d 章正在进行中,已完成 %d 个场景。请调用 writer 从场景 %d 继续写作。总共需要写 %d 章。",
|
||||
ch, scenes, scenes+1, progress.TotalChapters,
|
||||
)); err != nil {
|
||||
return fmt.Errorf("prompt: %w", err)
|
||||
}
|
||||
} else if progress != nil && len(progress.PendingRewrites) > 0 {
|
||||
// 重写恢复:有待重写章节
|
||||
log.Printf("重写恢复:%d 章待处理 %v", len(progress.PendingRewrites), progress.PendingRewrites)
|
||||
verb := "重写"
|
||||
if progress.Flow == domain.FlowPolishing {
|
||||
verb = "打磨"
|
||||
}
|
||||
if err := coordinator.Prompt(fmt.Sprintf(
|
||||
"有 %d 章待%s(受影响章节:%v)。原因:%s。请逐章调用 writer %s后继续正常写作。总共需要写 %d 章。",
|
||||
len(progress.PendingRewrites), verb, progress.PendingRewrites, progress.RewriteReason, verb, progress.TotalChapters,
|
||||
)); err != nil {
|
||||
return fmt.Errorf("prompt: %w", err)
|
||||
}
|
||||
} else if progress != nil && progress.Flow == domain.FlowReviewing {
|
||||
// 审阅恢复:审阅中断
|
||||
log.Printf("审阅恢复:上次审阅中断")
|
||||
if err := coordinator.Prompt(fmt.Sprintf(
|
||||
"上次审阅中断,请重新调用 editor 对已完成章节进行全局审阅。已完成 %d 章,共 %d 字。总共需要写 %d 章。",
|
||||
len(progress.CompletedChapters), progress.TotalWordCount, progress.TotalChapters,
|
||||
)); err != nil {
|
||||
return fmt.Errorf("prompt: %w", err)
|
||||
}
|
||||
} else if progress != nil && progress.IsResumable() && runMeta != nil && runMeta.PendingSteer != "" {
|
||||
next := progress.NextChapter()
|
||||
log.Printf("Steer 恢复:上次干预未完成,重新注入")
|
||||
if err := coordinator.Prompt(fmt.Sprintf(
|
||||
"从第 %d 章继续写作。之前已完成 %d 章,共 %d 字。总共需要写 %d 章。\n\n[用户干预-恢复] %s\n请评估影响范围,决定是否需要修改设定或重写已有章节。",
|
||||
next, len(progress.CompletedChapters), progress.TotalWordCount, progress.TotalChapters, runMeta.PendingSteer,
|
||||
)); err != nil {
|
||||
return fmt.Errorf("prompt: %w", err)
|
||||
}
|
||||
} else if progress != nil && progress.IsResumable() {
|
||||
next := progress.NextChapter()
|
||||
log.Printf("恢复模式:从第 %d 章继续(已完成 %d 章,共 %d 字)",
|
||||
next, len(progress.CompletedChapters), progress.TotalWordCount)
|
||||
if err := coordinator.Prompt(fmt.Sprintf(
|
||||
"从第 %d 章继续写作。之前已完成 %d 章,共 %d 字。总共需要写 %d 章。",
|
||||
next, len(progress.CompletedChapters), progress.TotalWordCount, progress.TotalChapters,
|
||||
)); err != nil {
|
||||
return fmt.Errorf("prompt: %w", err)
|
||||
}
|
||||
} else {
|
||||
// 新建:初始化进度
|
||||
if err := store.InitProgress(cfg.NovelName, cfg.MaxChapters); err != nil {
|
||||
return fmt.Errorf("init progress: %w", err)
|
||||
}
|
||||
log.Printf("新建模式:%s(%d 章)", cfg.NovelName, cfg.MaxChapters)
|
||||
if err := coordinator.Prompt(fmt.Sprintf(
|
||||
"请创作一部 %d 章的小说。要求如下:\n\n%s",
|
||||
cfg.MaxChapters, cfg.Prompt,
|
||||
)); err != nil {
|
||||
return fmt.Errorf("prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 等待完成
|
||||
coordinator.WaitForIdle()
|
||||
finalizeSteerIfIdle(store)
|
||||
|
||||
// 7. 输出结果
|
||||
finalProgress, _ := store.LoadProgress()
|
||||
if finalProgress != nil {
|
||||
log.Printf("创作完成:%d 章,共 %d 字,输出目录:%s",
|
||||
len(finalProgress.CompletedChapters), finalProgress.TotalWordCount, store.Dir())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleSubAgentDone 在每次 SubAgent 调用完成后读取文件系统信号,注入确定性任务。
|
||||
// SubAgent 内部工具事件不冒泡,所以通过 meta/last_commit.json 传递信号。
|
||||
func handleSubAgentDone(coordinator *agentcore.Agent, store *state.Store, maxChapters int) {
|
||||
result, err := store.LoadLastCommit()
|
||||
if err != nil || result == nil {
|
||||
return // 不是 Writer 的 commit,可能是 Architect 的 SubAgent 调用
|
||||
}
|
||||
// 消费即清除,防止重复注入 FollowUp
|
||||
if err := store.ClearLastCommit(); err != nil {
|
||||
log.Printf("[host] 清除 commit 信号失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[host] 章节提交信号:第 %d 章,%d 字,%d 个场景",
|
||||
result.Chapter, result.WordCount, result.SceneCount)
|
||||
|
||||
// 确定性判断 0:正在重写/打磨流程中
|
||||
progress, _ := store.LoadProgress()
|
||||
if progress != nil && (progress.Flow == domain.FlowRewriting || progress.Flow == domain.FlowPolishing) {
|
||||
if !slices.Contains(progress.PendingRewrites, result.Chapter) {
|
||||
log.Printf("[host] 警告:重写期间提交了非队列章节 %d,拒绝并提醒", result.Chapter)
|
||||
coordinator.FollowUp(agentcore.UserMsg(fmt.Sprintf(
|
||||
"[系统] 当前处于重写流程,但提交了非队列章节(第 %d 章)。请先完成待重写章节 %v 后再继续新章节。",
|
||||
result.Chapter, progress.PendingRewrites)))
|
||||
return
|
||||
}
|
||||
if err := store.CompleteRewrite(result.Chapter); err != nil {
|
||||
log.Printf("[host] 完成重写标记失败: %v", err)
|
||||
}
|
||||
clearHandledSteer(store)
|
||||
updated, _ := store.LoadProgress()
|
||||
if updated != nil && len(updated.PendingRewrites) == 0 {
|
||||
log.Printf("[host] 所有重写/打磨已完成,恢复正常写作")
|
||||
if err := store.SaveCheckpoint(fmt.Sprintf("ch%02d-commit", result.Chapter)); err != nil {
|
||||
log.Printf("[host] 保存检查点失败: %v", err)
|
||||
}
|
||||
if err := store.SaveCheckpoint("rewrite-done"); err != nil {
|
||||
log.Printf("[host] 保存检查点失败: %v", err)
|
||||
}
|
||||
} else if updated != nil {
|
||||
log.Printf("[host] 还有 %d 章待处理:%v", len(updated.PendingRewrites), updated.PendingRewrites)
|
||||
if err := store.SaveCheckpoint(fmt.Sprintf("ch%02d-commit", result.Chapter)); err != nil {
|
||||
log.Printf("[host] 保存检查点失败: %v", err)
|
||||
}
|
||||
}
|
||||
return // 重写期间不触发全书完成/审阅判断
|
||||
}
|
||||
|
||||
// 确定性判断 1:全书完成
|
||||
if result.NextChapter > maxChapters {
|
||||
log.Printf("[host] 所有 %d 章已完成,注入完成指令", maxChapters)
|
||||
if err := store.MarkComplete(); err != nil {
|
||||
log.Printf("[host] 标记完成失败: %v", err)
|
||||
}
|
||||
clearHandledSteer(store)
|
||||
if err := store.SaveCheckpoint(fmt.Sprintf("ch%02d-commit", result.Chapter)); err != nil {
|
||||
log.Printf("[host] 保存检查点失败: %v", err)
|
||||
}
|
||||
coordinator.FollowUp(agentcore.UserMsg(fmt.Sprintf(
|
||||
"[系统] 全部 %d 章已写完。请总结全书并结束。不要再调用 writer。",
|
||||
maxChapters)))
|
||||
return
|
||||
}
|
||||
|
||||
// 确定性判断 2:需要全局审阅
|
||||
if result.ReviewRequired {
|
||||
log.Printf("[host] review_required=true(%s),注入审阅指令", result.ReviewReason)
|
||||
if err := store.SetFlow(domain.FlowReviewing); err != nil {
|
||||
log.Printf("[host] 设置审阅流程失败: %v", err)
|
||||
}
|
||||
coordinator.FollowUp(agentcore.UserMsg(fmt.Sprintf(
|
||||
"[系统] review_required=true,%s。请调用 editor 对已完成章节进行全局审阅,然后根据审阅结果决定继续写第 %d 章还是修正已有章节。",
|
||||
result.ReviewReason, result.NextChapter)))
|
||||
}
|
||||
clearHandledSteer(store)
|
||||
if err := store.SaveCheckpoint(fmt.Sprintf("ch%02d-commit", result.Chapter)); err != nil {
|
||||
log.Printf("[host] 保存检查点失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleEditorDone 在 Editor SubAgent 完成后读取审阅信号。
|
||||
func handleEditorDone(coordinator *agentcore.Agent, store *state.Store) {
|
||||
review, err := store.LoadLastReviewSignal()
|
||||
if err != nil {
|
||||
log.Printf("[host] 加载审阅信号失败: %v", err)
|
||||
return
|
||||
}
|
||||
if review == nil {
|
||||
return
|
||||
}
|
||||
// 消费即清除,防止重复注入 FollowUp
|
||||
if err := store.ClearLastReview(); err != nil {
|
||||
log.Printf("[host] 清除审阅信号失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[host] 审阅信号:verdict=%s,%d 个问题", review.Verdict, len(review.Issues))
|
||||
|
||||
chaptersInfo := ""
|
||||
if len(review.AffectedChapters) > 0 {
|
||||
chaptersInfo = fmt.Sprintf("受影响章节:%v。", review.AffectedChapters)
|
||||
}
|
||||
|
||||
switch review.Verdict {
|
||||
case "rewrite":
|
||||
if err := store.SetPendingRewrites(review.AffectedChapters, review.Summary); err != nil {
|
||||
log.Printf("[host] 设置重写队列失败: %v", err)
|
||||
}
|
||||
if err := store.SetFlow(domain.FlowRewriting); err != nil {
|
||||
log.Printf("[host] 设置流程状态失败: %v", err)
|
||||
}
|
||||
coordinator.FollowUp(agentcore.UserMsg(fmt.Sprintf(
|
||||
"[系统] Editor 审阅结论:rewrite。%s%s请逐章调用 writer 重写受影响章节,全部完成后继续正常写作。",
|
||||
review.Summary, chaptersInfo)))
|
||||
case "polish":
|
||||
if err := store.SetPendingRewrites(review.AffectedChapters, review.Summary); err != nil {
|
||||
log.Printf("[host] 设置打磨队列失败: %v", err)
|
||||
}
|
||||
if err := store.SetFlow(domain.FlowPolishing); err != nil {
|
||||
log.Printf("[host] 设置流程状态失败: %v", err)
|
||||
}
|
||||
coordinator.FollowUp(agentcore.UserMsg(fmt.Sprintf(
|
||||
"[系统] Editor 审阅结论:polish。%s%s请逐章调用 writer 打磨受影响章节,全部完成后继续正常写作。",
|
||||
review.Summary, chaptersInfo)))
|
||||
default:
|
||||
// accept — 审阅通过,清除审阅状态
|
||||
if err := store.SetFlow(domain.FlowWriting); err != nil {
|
||||
log.Printf("[host] 清除审阅状态失败: %v", err)
|
||||
}
|
||||
}
|
||||
clearHandledSteer(store)
|
||||
if err := store.SaveCheckpoint(fmt.Sprintf("review-ch%02d-%s", review.Chapter, review.Verdict)); err != nil {
|
||||
log.Printf("[host] 保存检查点失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func truncateLog(s string, maxRunes int) string {
|
||||
runes := []rune(s)
|
||||
if len(runes) <= maxRunes {
|
||||
return s
|
||||
}
|
||||
return string(runes[:maxRunes]) + "..."
|
||||
}
|
||||
|
||||
func clearHandledSteer(store *state.Store) {
|
||||
if err := store.ClearPendingSteer(); err != nil {
|
||||
log.Printf("[host] 清除待处理干预失败: %v", err)
|
||||
}
|
||||
progress, _ := store.LoadProgress()
|
||||
if progress != nil && progress.Flow == domain.FlowSteering {
|
||||
if err := store.SetFlow(domain.FlowWriting); err != nil {
|
||||
log.Printf("[host] 重置流程状态失败: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func finalizeSteerIfIdle(store *state.Store) {
|
||||
runMeta, _ := store.LoadRunMeta()
|
||||
progress, _ := store.LoadProgress()
|
||||
if runMeta == nil || runMeta.PendingSteer == "" || progress == nil {
|
||||
return
|
||||
}
|
||||
if progress.Flow != domain.FlowSteering {
|
||||
return
|
||||
}
|
||||
clearHandledSteer(store)
|
||||
}
|
||||
72
app/run_test.go
Normal file
72
app/run_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/voocel/ainovel-cli/domain"
|
||||
"github.com/voocel/ainovel-cli/state"
|
||||
)
|
||||
|
||||
func TestFinalizeSteerIfIdleClearsPendingState(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store := state.NewStore(dir)
|
||||
if err := store.InitProgress("test", 3); err != nil {
|
||||
t.Fatalf("InitProgress: %v", err)
|
||||
}
|
||||
if err := store.SetFlow(domain.FlowSteering); err != nil {
|
||||
t.Fatalf("SetFlow: %v", err)
|
||||
}
|
||||
if err := store.SetPendingSteer("主角改成女性"); err != nil {
|
||||
t.Fatalf("SetPendingSteer: %v", err)
|
||||
}
|
||||
|
||||
finalizeSteerIfIdle(store)
|
||||
|
||||
progress, err := store.LoadProgress()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadProgress: %v", err)
|
||||
}
|
||||
if progress.Flow != domain.FlowWriting {
|
||||
t.Fatalf("expected flow writing, got %s", progress.Flow)
|
||||
}
|
||||
|
||||
runMeta, err := store.LoadRunMeta()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadRunMeta: %v", err)
|
||||
}
|
||||
if runMeta.PendingSteer != "" {
|
||||
t.Fatalf("expected pending steer cleared, got %q", runMeta.PendingSteer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalizeSteerIfIdleKeepsActiveFlow(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store := state.NewStore(dir)
|
||||
if err := store.InitProgress("test", 3); err != nil {
|
||||
t.Fatalf("InitProgress: %v", err)
|
||||
}
|
||||
if err := store.SetFlow(domain.FlowRewriting); err != nil {
|
||||
t.Fatalf("SetFlow: %v", err)
|
||||
}
|
||||
if err := store.SetPendingSteer("加入反转"); err != nil {
|
||||
t.Fatalf("SetPendingSteer: %v", err)
|
||||
}
|
||||
|
||||
finalizeSteerIfIdle(store)
|
||||
|
||||
progress, err := store.LoadProgress()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadProgress: %v", err)
|
||||
}
|
||||
if progress.Flow != domain.FlowRewriting {
|
||||
t.Fatalf("expected flow rewriting, got %s", progress.Flow)
|
||||
}
|
||||
|
||||
runMeta, err := store.LoadRunMeta()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadRunMeta: %v", err)
|
||||
}
|
||||
if runMeta.PendingSteer != "加入反转" {
|
||||
t.Fatalf("expected pending steer preserved, got %q", runMeta.PendingSteer)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user