Files
ainovel-clients/tui/app.go
shiyue c73f024c83 fix: tmux detach 后 ask_user 不再永久阻塞
问题根因:askUserBridge.requests 是无缓冲 channel,TUI 退出后
listenAskUser goroutine 消失,LLM 调用 ask_user 工具时 handler()
阻塞在 `b.requests <- req`,只有 ctx 取消才能解除(会杀掉整个任务)。

修复:
- askUserBridge 新增 detachCh(chan struct{})和 atomic 的 detached 标志
- Detach() 用 CAS 保证只关闭一次 channel,防止 double-close panic
- handler() 两处 select 均增加 `<-b.detachCh` case,立即返回
  "用户不在线,请自行决策" 错误,LLM 收到后自主继续
- tui/app.go fallback 分支(p.Run() 退出且任务仍在运行时)
  立即调用 bridge.Detach(),解除所有 ask_user 阻塞
2026-03-18 22:31:15 +08:00

87 lines
2.3 KiB
Go
Raw Permalink 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 tui
import (
"fmt"
"io"
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
tea "github.com/charmbracelet/bubbletea"
"github.com/voocel/ainovel-cli/app"
"github.com/voocel/ainovel-cli/tools"
)
// Run 启动 TUI 模式。
func Run(cfg app.Config, refs tools.References, prompts app.Prompts, styles map[string]string) error {
rt, err := app.NewRuntime(cfg, refs, prompts, styles)
if err != nil {
return err
}
bridge := newAskUserBridge()
rt.AskUser().SetHandler(bridge.handler)
restoreLog := redirectLogger(rt.Dir())
defer restoreLog()
m := NewModel(rt, bridge)
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
_, err = p.Run()
// bubbletea 退出后,检查任务是否仍在运行
snap := rt.Snapshot()
if snap.IsRunning {
// 任务仍在运行(可能是 tmux detach 导致 TUI 退出),
// 不中断任务,回退到无 UI 阻塞等待模式。
// 脱离 TUI 后解除所有阻塞在 ask_user 的 handler goroutine
// 让 LLM 收到"用户不在线,请自行决策"提示后继续运行,
// 而非无限等待一个永远不会到来的用户输入。
bridge.Detach()
fmt.Println("\n[TUI 已退出,任务仍在后台运行中...]")
fmt.Printf("[小说: %s | 阶段: %s | 进度: %d/%d 章]\n",
snap.NovelName, snap.Phase, snap.CompletedCount, snap.TotalChapters)
fmt.Println("[等待任务完成,按 Ctrl+C 强制中断]")
// 监听中断信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
select {
case <-rt.Done():
fmt.Println("\n[任务已完成!]")
case <-sigCh:
fmt.Println("\n[收到中断信号,正在停止...]")
rt.Close()
}
return nil
}
// 任务未在运行,正常关闭
rt.Close()
return err
}
// redirectLogger 将标准日志重定向到文件,避免破坏 TUI 画面。
func redirectLogger(outputDir string) func() {
prev := log.Writer()
logPath := filepath.Join(outputDir, "meta", "tui.log")
if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil {
log.SetOutput(io.Discard)
return func() { log.SetOutput(prev) }
}
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
log.SetOutput(io.Discard)
return func() { log.SetOutput(prev) }
}
log.SetOutput(f)
return func() {
log.SetOutput(prev)
_ = f.Close()
}
}