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 阻塞
This commit is contained in:
@@ -33,7 +33,12 @@ func Run(cfg app.Config, refs tools.References, prompts app.Prompts, styles map[
|
|||||||
snap := rt.Snapshot()
|
snap := rt.Snapshot()
|
||||||
if snap.IsRunning {
|
if snap.IsRunning {
|
||||||
// 任务仍在运行(可能是 tmux detach 导致 TUI 退出),
|
// 任务仍在运行(可能是 tmux detach 导致 TUI 退出),
|
||||||
// 不中断任务,回退到无 UI 阻塞等待模式
|
// 不中断任务,回退到无 UI 阻塞等待模式。
|
||||||
|
|
||||||
|
// 脱离 TUI 后解除所有阻塞在 ask_user 的 handler goroutine,
|
||||||
|
// 让 LLM 收到"用户不在线,请自行决策"提示后继续运行,
|
||||||
|
// 而非无限等待一个永远不会到来的用户输入。
|
||||||
|
bridge.Detach()
|
||||||
fmt.Println("\n[TUI 已退出,任务仍在后台运行中...]")
|
fmt.Println("\n[TUI 已退出,任务仍在后台运行中...]")
|
||||||
fmt.Printf("[小说: %s | 阶段: %s | 进度: %d/%d 章]\n",
|
fmt.Printf("[小说: %s | 阶段: %s | 进度: %d/%d 章]\n",
|
||||||
snap.NovelName, snap.Phase, snap.CompletedCount, snap.TotalChapters)
|
snap.NovelName, snap.Phase, snap.CompletedCount, snap.TotalChapters)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/voocel/ainovel-cli/tools"
|
"github.com/voocel/ainovel-cli/tools"
|
||||||
@@ -19,23 +20,44 @@ type askUserResult struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// askUserBridge 在 TUI goroutine 与 LLM handler goroutine 之间传递 ask_user 请求。
|
||||||
|
// detachCh 被关闭后,所有阻塞在 handler() 中的调用会立即返回错误,让 LLM 自行决策。
|
||||||
type askUserBridge struct {
|
type askUserBridge struct {
|
||||||
requests chan askUserRequest
|
requests chan askUserRequest
|
||||||
|
detachCh chan struct{}
|
||||||
|
detached uint32 // atomic: 0=active, 1=detached
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAskUserBridge() *askUserBridge {
|
func newAskUserBridge() *askUserBridge {
|
||||||
return &askUserBridge{
|
return &askUserBridge{
|
||||||
requests: make(chan askUserRequest),
|
requests: make(chan askUserRequest),
|
||||||
|
detachCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detach 标记桥接器为已脱离状态,关闭 detachCh,使所有阻塞在 handler() 中的
|
||||||
|
// goroutine 立即收到错误并返回,让 LLM 收到"用户不在线,请自行决策"后继续运行。
|
||||||
|
// 多次调用安全(只有第一次真正关闭 channel)。
|
||||||
|
func (b *askUserBridge) Detach() {
|
||||||
|
if atomic.CompareAndSwapUint32(&b.detached, 0, 1) {
|
||||||
|
close(b.detachCh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *askUserBridge) handler(ctx context.Context, questions []tools.Question) (*tools.AskUserResponse, error) {
|
func (b *askUserBridge) handler(ctx context.Context, questions []tools.Question) (*tools.AskUserResponse, error) {
|
||||||
|
// 若已脱离 TUI,直接返回错误,让 tools/ask_user.go 将错误提示返回给 LLM
|
||||||
|
if atomic.LoadUint32(&b.detached) == 1 {
|
||||||
|
return nil, fmt.Errorf("用户不在线,请自行决策")
|
||||||
|
}
|
||||||
|
|
||||||
req := askUserRequest{
|
req := askUserRequest{
|
||||||
questions: questions,
|
questions: questions,
|
||||||
resultCh: make(chan askUserResult, 1),
|
resultCh: make(chan askUserResult, 1),
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case b.requests <- req:
|
case b.requests <- req:
|
||||||
|
case <-b.detachCh:
|
||||||
|
return nil, fmt.Errorf("用户不在线,请自行决策")
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
@@ -43,6 +65,8 @@ func (b *askUserBridge) handler(ctx context.Context, questions []tools.Question)
|
|||||||
select {
|
select {
|
||||||
case result := <-req.resultCh:
|
case result := <-req.resultCh:
|
||||||
return result.resp, result.err
|
return result.resp, result.err
|
||||||
|
case <-b.detachCh:
|
||||||
|
return nil, fmt.Errorf("用户不在线,请自行决策")
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user