Files
HomeAgent/cmd/waiter/state.go
JianFeeeee ece06b0375 feat(cli): streaming process output with npm-style spinner
CLI 对话现在像 npm 安装一样先显示 braille 加载动画,然后逐步吐出
推理内容和工具调用状态,最后输出最终响应。

协议扩展(JSON 行,向后兼容):
- {"type":"reasoning","content":...}   推理过程帧
- {"type":"tool_call","tool":...,"status":...,"result":...} 工具调用帧
- response / error 仍为终结帧,语义不变

服务端(internal/plugins/cli):
- handleChat: 通过 SDK 订阅 EventReasoning/EventToolCall(按 channel=="cli"
  过滤),InjectTextSync 阻塞期间实时转发事件到 socket;connWriter 互斥
  保护并发写。纯插件层实现,不触碰内核。
- 不订阅 EventAgentOutput:内核先写 ResponseCh 再 publish 该事件,
  订阅会导致响应重复。

客户端(cmd/waiter):
- startSpinner: npm 风格 braille 转圈(80ms),幂等 stop(),非 TTY 自动禁用
- SendChatStream: 循环读帧直至终结帧,onEvent 回调渲染过程帧
- printServerOutput: reasoning 灰色 · 前缀;tool_call ✔/✘ 状态行 + 结果预览
- 交互模式发送后自动起 spinner,首帧到达即停;oneshot 同理
- 向后兼容旧服务器(无类型行直接作为最终输出)

端到端验证:本地 homed 测试实例 + llmsproxy,oneshot 与交互模式均正确
渲染 推理→工具调用→最终响应 完整链路。

另外修正 dashboard.html renderReasoningCard 流式态使用 preview 结构
(与 GUI 渲染器一致,配合此前 renderChatStreamChunk 增量更新)。
2026-08-24 23:34:48 +08:00

178 lines
3.3 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 main
import (
"context"
"encoding/json"
"fmt"
"sync"
)
type State struct {
conn Conn
mu sync.Mutex
}
func (s *State) Connect(cfg *Config) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.conn != nil {
s.conn.Close()
}
c, err := dial(cfg)
if err != nil {
return err
}
s.conn = c
return nil
}
func (s *State) Disconnect() {
s.mu.Lock()
defer s.mu.Unlock()
if s.conn != nil {
s.conn.Close()
s.conn = nil
}
}
func (s *State) Connected() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.conn != nil
}
func (s *State) RemoteConn() *remoteConn {
s.mu.Lock()
defer s.mu.Unlock()
if rc, ok := s.conn.(*remoteConn); ok {
return rc
}
return nil
}
func (s *State) Send(line string) error {
s.mu.Lock()
c := s.conn
s.mu.Unlock()
if c == nil {
return fmt.Errorf("not connected")
}
return c.Send(line)
}
func (s *State) SendChat(msg string) (string, error) {
return s.SendChatStream(msg, nil)
}
// SendChatStream 发送一条对话消息并循环读取响应行直至终结帧。
// onEvent 回调在每收到一个过程帧reasoning/tool_call时被调用
// 可为 nil返回值为最终响应内容或错误。
func (s *State) SendChatStream(msg string, onEvent func(respLine)) (string, error) {
if err := s.Send(msg); err != nil {
return "", err
}
for {
line, err := s.readLine()
if err != nil {
return "", err
}
rl := parseRespLineStruct(line)
switch rl.Type {
case "response":
return rl.Content, nil
case "error":
if rl.Error == "" {
rl.Error = line
}
return "", fmt.Errorf("%s", rl.Error)
default:
// 过程帧reasoning / tool_call / 旧版服务器的普通文本
if rl.Type == "" && onEvent == nil && rl.Content == "" && rl.Error == "" {
// 非JSON旧行且无回调直接当最终输出向后兼容旧服务器
return line, nil
}
if onEvent != nil {
onEvent(rl)
}
}
}
}
func (s *State) SendBuiltin(cmd string) (string, error) {
if err := s.Send(cmd); err != nil {
return "", err
}
line, err := s.readLine()
if err != nil {
return "", err
}
return parseRespLine(line)
}
func (s *State) readLine() (string, error) {
s.mu.Lock()
c := s.conn
s.mu.Unlock()
if c == nil {
return "", fmt.Errorf("not connected")
}
return c.ReadLine()
}
type respLine struct {
Type string `json:"type"`
Content string `json:"content"`
Error string `json:"error"`
Tool string `json:"tool"`
Status string `json:"status"`
Result string `json:"result"`
}
// parseRespLineStruct 解析一行 JSON 响应帧,解析失败时将原文放入 Content。
func parseRespLineStruct(line string) respLine {
var rl respLine
if err := json.Unmarshal([]byte(line), &rl); err != nil {
return respLine{Content: line}
}
return rl
}
func parseRespLine(line string) (string, error) {
rl := parseRespLineStruct(line)
switch rl.Type {
case "response":
return rl.Content, nil
case "error":
return "", fmt.Errorf("%s", rl.Error)
default:
return line, nil
}
}
func (s *State) ReadLoop(ctx context.Context, cb func(string)) {
for {
s.mu.Lock()
c := s.conn
s.mu.Unlock()
if c == nil {
return
}
done := make(chan struct{})
var line string
var readErr error
go func() {
line, readErr = c.ReadLine()
close(done)
}()
select {
case <-done:
if readErr != nil {
return
}
cb(line)
case <-ctx.Done():
return
}
}
}