Files
HomeAgent/cmd/waiter/state.go
JianFeeeee 4e70b8d5d8 refactor(module): 核心仓 Go module 路径改为 github.com/JianFeeeee/HologramAgent
批 1,不含 SDK 仓与目录改名。

- go.mod 第 1 行 module 路径
- 366 个 .go 文件的完整前缀 github.com/JianFeeeee/HomeAgent 机械替换
  (857 处,仅完整前缀;展示名/注释里的 HomeAgent 字样留给批 5)
- 未改:third_party 目录名、SDK 自身 go.mod module、go.mod replace 目标、
  homed/waiter 可执行名、.hmap 后缀
2026-10-21 12:17:19 +08:00

193 lines
4.2 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"
safeclose "github.com/JianFeeeee/HologramAgent/internal/safeclose"
"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"`
// Phase 是 stage 帧的阶段名(on_input / pre_action / post_action /
// toolcall / before_output / after_output …)。
//
// 为何要加(2026-10-13 协议审计):CLI 此前是唯一看不到阶段进展的客户端,
// 服务端补发 stage 帧后,客户端必须有字段承载它。
Phase string `json:"phase,omitempty"`
// Event / Plugin 是 system 帧的字段(目前只有 plugin_crashed)。
//
// 为何要加(2026-10-13 协议审计):system 是内核级状态事件,
// 而 CLI 此前既不订阅它、也没有字段承载 —— 插件崩溃对终端用户不可见。
Event string `json:"event,omitempty"`
Plugin string `json:"plugin,omitempty"`
Reset bool `json:"reset,omitempty"` // delta 帧:服务端轮次作废,清空累积
}
// 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 doneOnce sync.Once
var line string
var readErr error
go func() {
line, readErr = c.ReadLine()
safeclose.Safe(&doneOnce, done)
}()
select {
case <-done:
if readErr != nil {
return
}
cb(line)
case <-ctx.Done():
return
}
}
}