feat: complete HomeAgent architecture v2

- IO abstraction layer with OutputChannel routing and capability validation
- Three-layer memory (Context-Document-Graph) with TF-IDF relevance pruning
- OneBot V11 QQ protocol plugin with Reverse WebSocket client
- Plugin system with hot-reload (SKILL.md + native factories)
- Knowledge system with TF-IDF vector indexing
- Personality system (personal.md)
- Text memory (JSONL with rotation)
- Change tracker (overlayfs) with rollback
- Lua adapter VM
- Design document (DESIGN.md)

Module: gitcode.com/JianFeeeee/HomeAgent
This commit is contained in:
root
2026-07-02 12:04:36 +08:00
parent 1a7a846d58
commit bc26850b50
43 changed files with 10296 additions and 0 deletions

244
internal/onebot/client.go Normal file
View File

@ -0,0 +1,244 @@
package onebot
import (
"encoding/json"
"fmt"
"log"
"sync"
"sync/atomic"
"time"
"github.com/gorilla/websocket"
)
// EventHandler 处理 OneBot 推送的事件
type EventHandler func(event *Event)
// Client 是 OneBot 反向 WebSocket 客户端
// 连接到 OneBot 兼容前端(如 go-cqhttp、Lagrange)的 WS 地址
type Client struct {
url string
accessToken string
conn *websocket.Conn
mu sync.Mutex
done chan struct{}
eventHandler EventHandler
// 等待响应的 Action 调用
pending sync.Map
echoCount int64
connected atomic.Bool
}
// NewClient 创建 OneBot 反向 WS 客户端
// wsURL: ws://host:port/onebot/v11/ws
func NewClient(wsURL, accessToken string) *Client {
return &Client{
url: wsURL,
accessToken: accessToken,
done: make(chan struct{}),
}
}
// SetEventHandler 注册事件处理函数
func (c *Client) SetEventHandler(h EventHandler) {
c.eventHandler = h
}
// Connected 返回是否已连接
func (c *Client) Connected() bool {
return c.connected.Load()
}
// Connect 连接到 OneBot 前端(阻塞直到连接建立或失败)
func (c *Client) Connect() error {
header := make(map[string][]string)
if c.accessToken != "" {
header["Authorization"] = []string{"Bearer " + c.accessToken}
}
conn, _, err := websocket.DefaultDialer.Dial(c.url, header)
if err != nil {
return err
}
c.mu.Lock()
if c.conn != nil {
c.conn.Close()
}
c.conn = conn
c.mu.Unlock()
c.connected.Store(true)
log.Printf("[onebot] connected to %s", c.url)
go c.readLoop()
return nil
}
// reconnect 自动重连
func (c *Client) reconnect() {
c.connected.Store(false)
backoff := time.Second
for {
select {
case <-c.done:
return
case <-time.After(backoff):
log.Printf("[onebot] reconnecting in %v...", backoff)
if err := c.Connect(); err != nil {
log.Printf("[onebot] reconnect failed: %v, retry", err)
backoff *= 2
if backoff > 30*time.Second {
backoff = 30 * time.Second
}
continue
}
log.Printf("[onebot] reconnected")
return
}
}
}
func (c *Client) readLoop() {
defer c.connected.Store(false)
defer c.mu.Lock()
defer c.mu.Unlock()
defer func() {
if c.conn != nil {
c.conn.Close()
}
}()
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
log.Printf("[onebot] read error: %v", err)
go c.reconnect()
return
}
// 尝试解析为 ActionResponse(有 echo 字段)
var resp ActionResponse
if err := json.Unmarshal(message, &resp); err == nil && resp.Echo != "" {
if ch, ok := c.pending.Load(resp.Echo); ok {
ch.(chan *ActionResponse) <- &resp
}
continue
}
// 解析为 Event
var evt Event
if err := json.Unmarshal(message, &evt); err != nil {
log.Printf("[onebot] parse error: %v", err)
continue
}
if c.eventHandler != nil {
c.eventHandler(&evt)
}
}
}
// SendAction 发送一个 OneBot API 请求并等待响应
func (c *Client) SendAction(action string, params map[string]interface{}, timeout time.Duration) (*ActionResponse, error) {
echo := atomic.AddInt64(&c.echoCount, 1)
echoStr := formatInt64(echo)
msg := Action{
Action: action,
Params: params,
Echo: echoStr,
}
data, err := json.Marshal(msg)
if err != nil {
return nil, err
}
ch := make(chan *ActionResponse, 1)
c.pending.Store(echoStr, ch)
defer c.pending.Delete(echoStr)
c.mu.Lock()
if c.conn == nil {
c.mu.Unlock()
return nil, fmt.Errorf("not connected")
}
err = c.conn.WriteMessage(websocket.TextMessage, data)
c.mu.Unlock()
if err != nil {
return nil, err
}
if timeout <= 0 {
timeout = 10 * time.Second
}
select {
case resp := <-ch:
if resp.Status == "failed" {
return resp, fmt.Errorf("onebot action %s failed: retcode=%d", action, resp.RetCode)
}
return resp, nil
case <-time.After(timeout):
return nil, fmt.Errorf("onebot action %s timeout", action)
}
}
// SendPrivateMessage 发送私聊消息(便捷方法)
func (c *Client) SendPrivateMessage(userID int64, message interface{}, autoEscape bool) (*ActionResponse, error) {
params := map[string]interface{}{
"user_id": userID,
"message": message,
"auto_escape": autoEscape,
}
return c.SendAction("send_private_msg", params, 0)
}
// SendGroupMessage 发送群消息(便捷方法)
func (c *Client) SendGroupMessage(groupID int64, message interface{}, autoEscape bool) (*ActionResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"message": message,
"auto_escape": autoEscape,
}
return c.SendAction("send_group_msg", params, 0)
}
// GetLoginInfo 获取登录号信息
func (c *Client) GetLoginInfo() (*ActionResponse, error) {
return c.SendAction("get_login_info", nil, 0)
}
// GetGroupMemberInfo 获取群成员信息
func (c *Client) GetGroupMemberInfo(groupID, userID int64) (*ActionResponse, error) {
return c.SendAction("get_group_member_info", map[string]interface{}{
"group_id": groupID,
"user_id": userID,
}, 0)
}
// GetGroupList 获取群列表
func (c *Client) GetGroupList() (*ActionResponse, error) {
return c.SendAction("get_group_list", nil, 0)
}
// Close 关闭连接
func (c *Client) Close() error {
close(c.done)
c.mu.Lock()
defer c.mu.Unlock()
if c.conn != nil {
return c.conn.Close()
}
return nil
}
func formatInt64(n int64) string {
return fmt.Sprintf("%d", n)
}

222
internal/onebot/device.go Normal file
View File

@ -0,0 +1,222 @@
package onebot
import (
"encoding/json"
"fmt"
"log"
"strings"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
)
// Device 将 OneBot 客户端包装为 IO 抽象层的 Device
// 作为 QQ 通道与 HomeAgent 之间的桥梁
type Device struct {
name string
desc string
client *Client
iom *agentIO.IOManager
}
// NewDevice 创建 OneBot IO 设备
// name: 设备名称(如 "qq")
// wsURL: OneBot 前端 WebSocket 地址(如 "ws://127.0.0.1:6700")
// accessToken: OneBot 鉴权令牌(可选)
func NewDevice(name, wsURL, accessToken string, iom *agentIO.IOManager) *Device {
return &Device{
name: name,
desc: fmt.Sprintf("OneBot 标准 QQ 通道 (%s)", wsURL),
client: NewClient(wsURL, accessToken),
iom: iom,
}
}
// Name 返回设备名称
func (d *Device) Name() string { return d.name }
// Type 返回设备类型(双向 IO)
func (d *Device) Type() agentIO.DeviceType { return agentIO.DeviceIO }
// Description 返回设备描述
func (d *Device) Description() string { return d.desc }
// OutputCapabilities 返回输出能力(文本+文件+图片)
func (d *Device) OutputCapabilities() agentIO.OutputCapability {
return agentIO.CapText | agentIO.CapFile | agentIO.CapImage
}
// Tools 返回 OneBot 标准 API 的工具定义
// AI 可以通过这些工具调用 OneBot 功能
func (d *Device) Tools() []agentIO.ToolDef {
return []agentIO.ToolDef{
{
Name: d.name + "_send_private_msg",
Description: "发送 QQ 私聊消息",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"user_id": map[string]interface{}{"type": "integer", "description": "目标 QQ 号"},
"message": map[string]interface{}{"type": "string", "description": "消息内容(支持 CQ 码,如 [CQ:image,file=xxx.jpg])"},
"auto_escape": map[string]interface{}{"type": "boolean", "description": "是否作为纯文本发送(不解析 CQ 码)"},
},
"required": []string{"user_id", "message"},
},
},
{
Name: d.name + "_send_group_msg",
Description: "发送 QQ 群消息",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"group_id": map[string]interface{}{"type": "integer", "description": "目标群号"},
"message": map[string]interface{}{"type": "string", "description": "消息内容(支持 CQ 码)"},
"auto_escape": map[string]interface{}{"type": "boolean", "description": "是否作为纯文本发送"},
},
"required": []string{"group_id", "message"},
},
},
{
Name: d.name + "_get_group_member_info",
Description: "获取 QQ 群成员信息",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"group_id": map[string]interface{}{"type": "integer", "description": "群号"},
"user_id": map[string]interface{}{"type": "integer", "description": "QQ 号"},
},
"required": []string{"group_id", "user_id"},
},
},
{
Name: d.name + "_get_group_list",
Description: "获取 QQ 群列表",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
},
}
}
// Execute 执行 OneBot 工具调用
func (d *Device) Execute(tool string, args map[string]interface{}) (interface{}, error) {
if !d.client.Connected() {
return nil, fmt.Errorf("onebot 未连接到前端")
}
// 去掉名称前缀以匹配方法名
method := strings.TrimPrefix(tool, d.name+"_")
switch method {
case "send_private_msg":
userID, _ := toInt64(args["user_id"])
message, _ := args["message"].(string)
autoEscape, _ := args["auto_escape"].(bool)
return d.client.SendPrivateMessage(userID, message, autoEscape)
case "send_group_msg":
groupID, _ := toInt64(args["group_id"])
message, _ := args["message"].(string)
autoEscape, _ := args["auto_escape"].(bool)
return d.client.SendGroupMessage(groupID, message, autoEscape)
case "get_group_member_info":
groupID, _ := toInt64(args["group_id"])
userID, _ := toInt64(args["user_id"])
return d.client.GetGroupMemberInfo(groupID, userID)
case "get_group_list":
return d.client.GetGroupList()
default:
return nil, fmt.Errorf("unknown onebot tool: %s", tool)
}
}
// Start 连接到 OneBot 前端
func (d *Device) Start() error {
// 非阻塞连接
go func() {
if err := d.client.Connect(); err != nil {
log.Printf("[onebot] %s initial connect failed, will retry: %v", d.name, err)
}
}()
// 注册事件处理:OneBot 事件 → IO InputEvent
d.client.SetEventHandler(func(evt *Event) {
d.handleEvent(evt)
})
return nil
}
// Stop 断开连接
func (d *Device) Stop() error {
return d.client.Close()
}
// handleEvent 将 OneBot 事件转换为 IO InputEvent
func (d *Device) handleEvent(evt *Event) {
if d.iom == nil {
return
}
switch evt.PostType {
case "message":
var text string
if evt.RawMessage != "" {
text = evt.RawMessage
} else if s, ok := evt.Message.(string); ok {
text = s
}
if text == "" {
return
}
// 构造输入源标识
source := d.name
payload := map[string]interface{}{
"content": text,
"source": source,
"user_id": evt.UserID,
"sender": evt.Sender,
}
if evt.MessageType == "group" {
payload["group_id"] = evt.GroupID
payload["label"] = fmt.Sprintf("group:%d:%d", evt.GroupID, evt.UserID)
} else {
payload["label"] = fmt.Sprintf("private:%d", evt.UserID)
}
d.iom.InjectText(source, text)
// 同时注册输出路由:QQ 消息默认回复到 QQ 通道
d.iom.RegisterOutputRoute(source, d.name)
case "notice":
log.Printf("[onebot] notice from %s: type=%s", d.name, evt.NoticeType)
case "request":
log.Printf("[onebot] request from %s: type=%s flag=%s", d.name, evt.RequestType, evt.Flag)
case "meta_event":
if evt.MetaEventType == "heartbeat" {
log.Printf("[onebot] %s heartbeat: online=%v", d.name, evt.Status != nil && evt.Status.Online)
}
}
}
func toInt64(v interface{}) (int64, bool) {
switch n := v.(type) {
case int64:
return n, true
case float64:
return int64(n), true
case int:
return int64(n), true
case json.Number:
i, err := n.Int64()
return i, err == nil
}
return 0, false
}

94
internal/onebot/types.go Normal file
View File

@ -0,0 +1,94 @@
// Package onebot 实现 OneBot V11 标准协议(Reverse WebSocket 通信)
// 参考: https://github.com/botuniverse/onebot-11
package onebot
import "fmt"
// Action 是 OneBot 标准 API 请求
type Action struct {
Action string `json:"action"`
Params map[string]interface{} `json:"params,omitempty"`
Echo string `json:"echo,omitempty"`
}
// ActionResponse 是 OneBot 标准 API 响应
type ActionResponse struct {
Status string `json:"status"`
RetCode int `json:"retcode"`
Data interface{} `json:"data"`
Echo string `json:"echo,omitempty"`
}
// Event 是 OneBot 推送的事件
type Event struct {
Time int64 `json:"time"`
SelfID int64 `json:"self_id"`
PostType string `json:"post_type"` // message, notice, request, meta_event
DetailType string `json:"-"`
// 消息事件字段
MessageType string `json:"message_type,omitempty"` // private, group
SubType string `json:"sub_type,omitempty"`
MessageID int64 `json:"message_id,omitempty"`
UserID int64 `json:"user_id,omitempty"`
GroupID int64 `json:"group_id,omitempty"`
Message interface{} `json:"message,omitempty"` // string 或 []MessageSegment
RawMessage string `json:"raw_message,omitempty"`
Font int `json:"font,omitempty"`
Sender *Sender `json:"sender,omitempty"`
// 通知事件字段
NoticeType string `json:"notice_type,omitempty"`
// 请求事件字段
RequestType string `json:"request_type,omitempty"`
Flag string `json:"flag,omitempty"`
Comment string `json:"comment,omitempty"`
// 元事件字段
MetaEventType string `json:"meta_event_type,omitempty"`
Interval int64 `json:"interval,omitempty"`
Status *Status `json:"status,omitempty"`
}
type Sender struct {
UserID int64 `json:"user_id"`
Nickname string `json:"nickname"`
Sex string `json:"sex,omitempty"`
Age int `json:"age,omitempty"`
Card string `json:"card,omitempty"` // 群名片
Area string `json:"area,omitempty"`
Level string `json:"level,omitempty"`
Role string `json:"role,omitempty"` // owner, admin, member
Title string `json:"title,omitempty"`
}
type Status struct {
AppInitialized bool `json:"app_initialized"`
AppEnabled bool `json:"app_enabled"`
PluginsGood bool `json:"plugins_good"`
AppGood bool `json:"app_good"`
Online bool `json:"online"`
Good bool `json:"good"`
}
// MessageSegment 表示 OneBot 消息段(数组格式)
type MessageSegment struct {
Type string `json:"type"`
Data map[string]string `json:"data"`
}
// MessageText 快速构造纯文本消息段
func MessageText(text string) MessageSegment {
return MessageSegment{Type: "text", Data: map[string]string{"text": text}}
}
// MessageImage 构造图片消息段
func MessageImage(file string) MessageSegment {
return MessageSegment{Type: "image", Data: map[string]string{"file": file}}
}
// MessageAt 构造 @ 消息段
func MessageAt(userID int64) MessageSegment {
return MessageSegment{Type: "at", Data: map[string]string{"qq": fmt.Sprintf("%d", userID)}}
}