Files
HomeAgent/cmd/waiter/conn.go
JianFeeeee c44ec0f210 feat(waiter): daemon模式 + localuse本机外设插件
waiter 新增 --daemon 后台驻留模式 (daemon.go):
  - 维持 homed 连接,TUI实例经 Unix socket 接入
  - 单客户端串行模型:每个TUI独占homed响应,新客户端回放缓冲(256行)
  - 设备桥场景可无 homed 运行(纯设备桥驻留)
  - 设备桥看护循环:WS断开自动重连(3-5s间隔)
  - conn.go 新增 daemonConn 类型,dial() 优先检测 daemon

localuse 插件 (internal/plugins/localuse/):
  - local_screensee / local_camerasue / local_speakeruse
  - local_screensue / local_clipboardsee / local_clipboardsue / local_computeruse
  - 跨平台实现(Linux/macOS/Windows),能力与 waiter device.go 对齐
  - headless服务器上缺依赖工具自动返回安装提示

SDK sync: third_party SetToolBlocks 多模态类型同步
README 补充 daemon 模式使用文档
2026-08-27 09:27:15 +08:00

206 lines
4.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 (
"bufio"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"sync"
"time"
)
type Conn interface {
Send(line string) error
ReadLine() (string, error)
Close() error
}
func dial(cfg *Config) (Conn, error) {
// 优先连接运行中的 daemon后台驻留模式
if daemonIsRunning() {
if c, err := dialDaemon(); err == nil {
return c, nil
}
}
if cfg.Remote != "" {
return dialRemote(cfg.Remote, cfg.APIKey)
}
return dialLocal(cfg.Socket, cfg.APIKey)
}
func dialLocal(socket, apiKey string) (Conn, error) {
if socket == "" {
socket = discoverSocket("")
}
c, err := net.DialTimeout("unix", socket, 5*time.Second)
if err != nil {
return nil, fmt.Errorf("unix %s: %w", socket, err)
}
lc := &localConn{conn: c, r: bufio.NewReader(c)}
if apiKey != "" {
if err := lc.Send("/auth " + apiKey); err != nil {
c.Close()
return nil, fmt.Errorf("auth send: %w", err)
}
line, err := lc.ReadLine()
if err != nil {
c.Close()
return nil, fmt.Errorf("auth read: %w", err)
}
if strings.Contains(line, "unauthorized") {
c.Close()
return nil, fmt.Errorf("auth rejected: %s", line)
}
}
return lc, nil
}
// daemonConn 是连接到运行中 waiter daemon 的轻量封装。
// 协议与 localConn 完全一致(行式 \n 分隔),但不做 /authdaemon 已集中鉴权)。
type daemonConn struct {
conn net.Conn
r *bufio.Reader
}
func dialDaemon() (Conn, error) {
sock := daemonSocketPath()
c, err := net.DialTimeout("unix", sock, 3*time.Second)
if err != nil {
return nil, fmt.Errorf("daemon %s: %w", sock, err)
}
return &daemonConn{conn: c, r: bufio.NewReader(c)}, nil
}
func (c *daemonConn) Send(line string) error {
_, err := fmt.Fprintf(c.conn, "%s\n", line)
return err
}
func (c *daemonConn) ReadLine() (string, error) {
s, err := c.r.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSuffix(s, "\n"), nil
}
func (c *daemonConn) Close() error {
return c.conn.Close()
}
type localConn struct {
conn net.Conn
r *bufio.Reader
}
func (c *localConn) Send(line string) error {
_, err := fmt.Fprintf(c.conn, "%s\n", line)
return err
}
func (c *localConn) ReadLine() (string, error) {
s, err := c.r.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSuffix(s, "\n"), nil
}
func (c *localConn) Close() error {
return c.conn.Close()
}
func dialRemote(baseURL, apiKey string) (Conn, error) {
baseURL = strings.TrimRight(baseURL, "/")
return &remoteConn{baseURL: baseURL, apiKey: apiKey}, nil
}
type remoteConn struct {
baseURL string
apiKey string
mu sync.Mutex
buf []string
closed bool
}
func (c *remoteConn) Send(line string) error {
req, err := http.NewRequest("POST", c.baseURL+"/api/v1/chat", strings.NewReader(
fmt.Sprintf(`{"message":%q}`, line),
))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if c.apiKey != "" {
req.Header.Set("X-API-Key", c.apiKey)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
content, _ := result["response"].(string)
c.mu.Lock()
c.buf = append(c.buf, content)
c.mu.Unlock()
return nil
}
func (c *remoteConn) DoAPI(method, path string, body string) (map[string]interface{}, error) {
var bodyReader io.Reader
if body != "" {
bodyReader = strings.NewReader(body)
}
req, err := http.NewRequest(method, c.baseURL+path, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if c.apiKey != "" {
req.Header.Set("X-API-Key", c.apiKey)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result, nil
}
func (c *remoteConn) ReadLine() (string, error) {
c.mu.Lock()
defer c.mu.Unlock()
for len(c.buf) == 0 && !c.closed {
c.mu.Unlock()
time.Sleep(50 * time.Millisecond)
c.mu.Lock()
}
if c.closed && len(c.buf) == 0 {
return "", io.EOF
}
s := c.buf[0]
c.buf = c.buf[1:]
return s, nil
}
func (c *remoteConn) Close() error {
c.mu.Lock()
c.closed = true
c.mu.Unlock()
return nil
}