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

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
}