Files
ainovel-clients/app/run.go
voocel 27bd85ef90 init
2026-03-07 21:25:55 +08:00

352 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}