support openclaw

This commit is contained in:
yincong
2026-02-27 09:38:20 +08:00
parent b30aa9b580
commit 6ba22acc8d
11 changed files with 773 additions and 310 deletions
+41 -2
View File
@@ -86,7 +86,7 @@ function setReceiver() {
} else {
// 处理用户的名称
const splitIndex = userContent.indexOf(':')
const splitIndex = userContent?.indexOf(':')
senderNickname = userContent.substring(0, splitIndex).trim();
messages.push({type: "text", data: {text: content}});
}
@@ -231,4 +231,43 @@ function generateAESKey() {
key += chars.charAt(Math.floor(Math.random() * chars.length));
}
return key;
}
}
// -----------------------测试函数-------------------------
function testGetProtobufRawBytes() {
const rawMemoryData = [
0x08, 0x00, 0x12, 0xb6, 0x02, 0x08, 0x01, 0x12, 0xb1, 0x02, 0x08, 0x05, 0x12, 0xac, 0x02, 0x08,
0xa6, 0x02, 0x12, 0xa6, 0x02, 0x08, 0xec, 0xfc, 0xb9, 0x96, 0x03, 0x12, 0x15, 0x0a, 0x13, 0x77,
0x78, 0x69, 0x64, 0x5f, 0x35, 0x79, 0x72, 0x74, 0x6c, 0x79, 0xf6, 0x33, 0x32, 0x68, 0x39, 0x75,
0x31, 0x32, 0x1a, 0x08, 0x0a, 0x06, 0x6b, 0x74, 0x73, 0x6b, 0x74, 0x73, 0x20, 0x01, 0x2a, 0x0d,
0x0a, 0x0b, 0x6a, 0x64, 0x6a, 0x78, 0x68, 0x64, 0x62, 0x62, 0x64, 0x64, 0x20, 0x30, 0x03, 0x38,
0x01, 0x42, 0x02, 0x08, 0x00, 0x48, 0xaa, 0xdb, 0xff, 0xcc, 0x06, 0x52, 0xc7, 0x01, 0x3c, 0x6d,
0x73, 0x67, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x3e, 0x0a, 0x09, 0x3c, 0x62, 0x69, 0x7a, 0x66,
0x6c, 0x61, 0x67, 0x3e, 0x30, 0x3c, 0x2f, 0x62, 0x69, 0x7a, 0x66, 0x6c, 0x61, 0x67, 0x3e, 0x0a,
0x09, 0x3c, 0x70, 0x75, 0x61, 0x3e, 0x31, 0x3c, 0x2f, 0x70, 0x75, 0x61, 0x3e, 0x0a, 0x09, 0x3c,
0x65, 0x67, 0x67, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x3e, 0x31, 0x3c, 0x2f, 0x65,
0x67, 0x67, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x3e, 0x0a, 0x09, 0x3c, 0x73, 0x69,
0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x3e, 0x4e, 0x30, 0x5f, 0x56, 0x31, 0x5f, 0x78, 0x6e,
0x68, 0x6b, 0x4e, 0x65, 0x68, 0x4b, 0x7c, 0x76, 0x31, 0x5f, 0x42, 0x4b, 0x71, 0x2b, 0x62, 0x45,
0x36, 0x31, 0x3c, 0x2f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x3e, 0x0a, 0x09,
0x3c, 0x74, 0x6d, 0x70, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x70, 0x75,
0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x2d, 0x69, 0x64, 0x3e, 0x3c, 0x2f, 0x70, 0x75, 0x62,
0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x2d, 0x69, 0x64, 0x3e, 0x3c, 0x2f, 0x70, 0x75, 0x62, 0x6c,
];
const pBuffer = {
// 模拟指针读取内存返回 ArrayBuffer
readByteArray: function(size) {
// 返回模拟数据的 ArrayBuffer 副本
const slice = rawMemoryData.slice(0, size);
const ab = new ArrayBuffer(slice.length);
const view = new Uint8Array(ab);
for (let i = 0; i < slice.length; i++) view[i] = slice[i];
return ab;
}
};
const results = getProtobufRawBytes(pBuffer, rawMemoryData.length);
console.log(results);
}
+4 -1
View File
@@ -2,6 +2,9 @@ module github.com/yincongcyincong/weixin-macos/onebot
go 1.25.0
require github.com/frida/frida-go v1.0.0
require (
github.com/frida/frida-go v1.0.0
github.com/gorilla/websocket v1.5.3
)
require github.com/google/uuid v1.6.0 // indirect
+2
View File
@@ -2,3 +2,5 @@ github.com/frida/frida-go v1.0.0 h1:Xlq1CB8QSAC6zbOFdjCX0oK8RjQGtdl2yATbUQKROwo=
github.com/frida/frida-go v1.0.0/go.mod h1:O8Dg1YBGfQsBEL1a8x3GURw/JllJrcuvg78ga2OgdM4=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+135
View File
@@ -0,0 +1,135 @@
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
)
func sendHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "仅支持 POST", http.StatusMethodNotAllowed)
return
}
req := new(SendRequest)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
http.Error(w, "无效的 JSON", http.StatusBadRequest)
return
}
// 参数校验
if len(req.Message) == 0 || (req.UserID == "" && req.GroupID == "") {
http.Error(w, "参数缺失", http.StatusBadRequest)
return
}
sendContent := ""
atUserID := ""
for _, v := range req.Message {
if v.Type == "text" {
sendContent += v.Data.Text
} else if v.Type == "at" {
if req.GroupID != "" {
if nicknameInter, ok := userID2NicknameMap.Load(req.GroupID + "_" + v.Data.QQ); ok {
sendContent += fmt.Sprintf("@%s\u2005", nicknameInter.(string))
atUserID += v.Data.QQ + ","
}
}
} else if v.Type == "image" {
msgChan <- &SendMsg{
UserId: req.UserID,
GroupID: req.GroupID,
Content: v.Data.File,
Type: v.Type,
}
}
}
if sendContent != "" {
msgChan <- &SendMsg{
UserId: req.UserID,
GroupID: req.GroupID,
Content: sendContent,
Type: "text",
AtUser: strings.TrimRight(atUserID, ","),
}
}
json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
})
}
func SendHttpReq(msg map[string]interface{}) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic: %v\n", r)
}
}()
time.Sleep(1 * time.Second)
// 这里处理你的 X1 数据
jsonData, err := json.Marshal(msg["payload"])
if err != nil {
log.Printf("JSON 序列化失败: %v\n", err)
return
}
fmt.Printf("发送数据: %s\n", string(jsonData))
if myWechatId == "" {
m := new(WechatMessage)
err = json.Unmarshal(jsonData, m)
if err != nil {
log.Printf("解析消息失败: %v\n", err)
return
}
myWechatId = m.SelfID
if m.GroupId != "" {
userID2NicknameMap.Store(m.GroupId+"_"+m.UserID, m.Sender.Nickname)
}
}
// 4. 创建 POST 请求
req, err := http.NewRequest("POST", config.SendURL, bytes.NewBuffer(jsonData))
if err != nil {
log.Printf("创建请求失败: %v\n", err)
return
}
// 5. 设置 Header (OneBot 接口通常要求 application/json)
h := hmac.New(sha1.New, []byte(config.OnebotToken))
h.Write(jsonData)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Signature", "sha1="+hex.EncodeToString(h.Sum(nil)))
client := &http.Client{
Timeout: 10 * time.Second,
}
// 6. 执行请求
resp, err := client.Do(req)
if err != nil {
log.Printf("请求执行失败: %v\n", err)
return
}
defer resp.Body.Close()
// 7. 读取返回结果
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("读取响应失败: %v\n", err)
return
}
fmt.Printf("状态码: %d 返回内容: %s\n", resp.StatusCode, string(body))
}
+38 -294
View File
@@ -2,28 +2,17 @@ package main
import (
"bytes"
"context"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"syscall"
"text/template"
"time"
"github.com/frida/frida-go/frida"
)
@@ -90,10 +79,42 @@ type Config struct {
WechatPid int `json:"wechat_pid"`
OnebotToken string `json:"onebot_token"`
ImagePath string `json:"image_path"`
ConnType string `json:"conn_type"`
WechatConf string `json:"wechat_conf"`
}
func main() {
initFlag()
if config.FridaType == "gadget" {
initFridaGadget()
} else {
initFrida()
}
go SendWorker()
http.HandleFunc("/send_private_msg", sendHandler)
http.HandleFunc("/send_group_msg", sendHandler)
http.HandleFunc("/ws", handleWebSocket)
http.HandleFunc("/test_ws", testWebSocket)
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
go func() {
<-stop
log.Fatalf("\n正在释放 Frida 资源并退出...")
}()
// 3. 启动服务
fmt.Printf("HTTP 服务启动在 %s", config.ReceiveHost)
if err := http.ListenAndServe(config.ReceiveHost, nil); err != nil {
log.Printf("服务启动失败: %v\n", err)
}
}
func initFlag() {
flag.StringVar(&config.FridaType, "type", "local", "frida 类型: local | gadget")
flag.StringVar(&config.SendURL, "send_url", "http://127.0.0.1:36060/onebot", "发送消息的 URL: http://127.0.0.1:36060/onebot")
@@ -103,6 +124,7 @@ func initFlag() {
flag.StringVar(&config.OnebotToken, "token", "MuseBot", "OneBot Token: MuseBot")
flag.StringVar(&config.ImagePath, "image_path", "", "图片路径: /Users/xxx/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/xxx/temp/xxx/2026-01/Img/")
flag.StringVar(&config.WechatConf, "wechat_conf", "../wechat_version/4_1_7_55_mac.json", "微信配置文件路径: ../wechat_version/4_1_6_12_mac.json")
flag.StringVar(&config.ConnType, "conn_type", "http", "连接类型: http | websocket")
flag.Parse()
@@ -201,7 +223,11 @@ func loadJs() {
if t, ok := pMap["type"]; ok {
switch t.(string) {
case "send":
go SendHttpReq(msg)
if config.ConnType == "http" {
go SendHttpReq(msg)
} else {
go SendWebSocketMsg(msg)
}
case "finish":
finishChan <- struct{}{}
case "upload":
@@ -243,285 +269,3 @@ func loadJs() {
fridaScript = script
fmt.Printf("✅ Frida 已就绪,微信控制通道已打通")
}
func sendHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "仅支持 POST", http.StatusMethodNotAllowed)
return
}
req := new(SendRequest)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
http.Error(w, "无效的 JSON", http.StatusBadRequest)
return
}
// 参数校验
if len(req.Message) == 0 || (req.UserID == "" && req.GroupID == "") {
http.Error(w, "参数缺失", http.StatusBadRequest)
return
}
sendContent := ""
atUserID := ""
for _, v := range req.Message {
if v.Type == "text" {
sendContent += v.Data.Text
} else if v.Type == "at" {
if req.GroupID != "" {
if nicknameInter, ok := userID2NicknameMap.Load(req.GroupID + "_" + v.Data.QQ); ok {
sendContent += fmt.Sprintf("@%s\u2005", nicknameInter.(string))
atUserID += v.Data.QQ + ","
}
}
} else if v.Type == "image" {
msgChan <- &SendMsg{
UserId: req.UserID,
GroupID: req.GroupID,
Content: v.Data.File,
Type: v.Type,
}
}
}
if sendContent != "" {
msgChan <- &SendMsg{
UserId: req.UserID,
GroupID: req.GroupID,
Content: sendContent,
Type: "text",
AtUser: strings.TrimRight(atUserID, ","),
}
}
json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
})
}
func SendWorker() {
defer func() {
if err := recover(); err != nil {
log.Printf("💥 SendWorker 异常: %v\n", err)
go SendWorker()
}
}()
for {
select {
case <-finishChan:
fmt.Printf("收到完成信号 \n")
case m, ok := <-msgChan:
if !ok {
return
}
SendWechatMsg(m)
}
}
}
func SendWechatMsg(m *SendMsg) {
time.Sleep(1 * time.Second)
currTaskId := atomic.AddInt64(&taskId, 1)
log.Printf("📩 收到任务: %d\n", currTaskId)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
targetId := m.UserId
if m.GroupID != "" && targetId == "" {
targetId = m.GroupID
}
switch m.Type {
case "text":
result := fridaScript.ExportsCall("triggerSendTextMessage", currTaskId, targetId, m.Content, m.AtUser)
log.Printf("📩 发送文本任务执行结果:%s, 参数:currTaskId: %d, targetId: %s, content: %s, atUser: %s\n",
result, currTaskId, targetId, m.Content, m.AtUser)
case "image":
targetPath, md5Str, err := SaveBase64Image(m.Content)
if err != nil {
log.Printf("保存图片失败: %v\n", err)
return
}
result := fridaScript.ExportsCall("triggerUploadImg", targetId, md5Str, targetPath)
log.Printf("📩 上传图片任务执行结果%s, 参数:targetId: %s, md5Str: %s, targetPath: %s\n", result, targetId, md5Str, targetPath)
case "send_image":
result := fridaScript.ExportsCall("triggerSendImgMessage", currTaskId, myWechatId, targetId)
log.Printf("📩 发送图片任务执行结果%s, 参数:currTaskId: %d, myWechatId: %s, targetId: %s\n", result, currTaskId, myWechatId, targetId)
}
select {
case <-ctx.Done():
log.Printf("任务 %d 执行超时!\n", currTaskId)
case <-finishChan:
log.Printf("收到完成信号,任务 %d 完成\n", currTaskId)
}
}
func main() {
initFlag()
if config.FridaType == "gadget" {
initFridaGadget()
} else {
initFrida()
}
go SendWorker()
http.HandleFunc("/send_private_msg", sendHandler)
http.HandleFunc("/send_group_msg", sendHandler)
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
go func() {
<-stop
log.Fatalf("\n正在释放 Frida 资源并退出...")
}()
// 3. 启动服务
fmt.Printf("HTTP 服务启动在 %s", config.ReceiveHost)
if err := http.ListenAndServe(config.ReceiveHost, nil); err != nil {
log.Printf("服务启动失败: %v\n", err)
}
}
func SendHttpReq(msg map[string]interface{}) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic: %v\n", r)
}
}()
time.Sleep(1 * time.Second)
// 这里处理你的 X1 数据
jsonData, err := json.Marshal(msg["payload"])
if err != nil {
log.Printf("JSON 序列化失败: %v\n", err)
return
}
fmt.Printf("发送数据: %s\n", string(jsonData))
if myWechatId == "" {
m := new(WechatMessage)
err = json.Unmarshal(jsonData, m)
if err != nil {
log.Printf("解析消息失败: %v\n", err)
return
}
myWechatId = m.SelfID
if m.GroupId != "" {
userID2NicknameMap.Store(m.GroupId+"_"+m.UserID, m.Sender.Nickname)
}
}
// 4. 创建 POST 请求
req, err := http.NewRequest("POST", config.SendURL, bytes.NewBuffer(jsonData))
if err != nil {
log.Printf("创建请求失败: %v\n", err)
return
}
// 5. 设置 Header (OneBot 接口通常要求 application/json)
h := hmac.New(sha1.New, []byte(config.OnebotToken))
h.Write(jsonData)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Signature", "sha1="+hex.EncodeToString(h.Sum(nil)))
client := &http.Client{
Timeout: 10 * time.Second,
}
// 6. 执行请求
resp, err := client.Do(req)
if err != nil {
log.Printf("请求执行失败: %v\n", err)
return
}
defer resp.Body.Close()
// 7. 读取返回结果
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("读取响应失败: %v\n", err)
return
}
fmt.Printf("状态码: %d 返回内容: %s\n", resp.StatusCode, string(body))
}
func SaveBase64Image(base64Data string) (string, string, error) {
rawContents := base64Data
if strings.HasPrefix(base64Data, "base64://") {
rawContents = strings.TrimPrefix(base64Data, "base64://")
} else if idx := strings.Index(base64Data, ","); idx != -1 {
rawContents = base64Data[idx+1:]
}
data, err := base64.StdEncoding.DecodeString(rawContents)
if err != nil {
return "", "", fmt.Errorf("base64 decode failed: %v", err)
}
salt := []byte(fmt.Sprintf("\n#md5_salt_%d_%d#", time.Now().UnixNano(), rand.Intn(10000)))
data = append(data, salt...)
r := rand.New(rand.NewSource(time.Now().UnixNano()))
randomNumber := r.Intn(1000) // 生成 0-999 的随机数
timestamp := time.Now().Unix()
fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, DetectImageFormat(data))
targetPath := config.ImagePath + fileName
dir := filepath.Dir(targetPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return "", "", fmt.Errorf("create directory failed: %v", err)
}
err = os.WriteFile(targetPath, data, 0644)
if err != nil {
return "", "", fmt.Errorf("write file failed: %v", err)
}
md5Str, err := GetFileMD5(targetPath)
if err != nil {
return "", "", fmt.Errorf("get file md5 failed: %v", err)
}
return targetPath, md5Str, nil
}
func GetFileMD5(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func DetectImageFormat(data []byte) string {
if len(data) < 12 {
return "unknown"
}
switch {
case bytes.HasPrefix(data, []byte{0xFF, 0xD8, 0xFF}):
return "jpg"
case bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}):
return "png"
case bytes.HasPrefix(data, []byte("GIF87a")) || bytes.HasPrefix(data, []byte("GIF89a")):
return "gif"
case bytes.HasPrefix(data, []byte{0x42, 0x4D}):
return "bmp"
case bytes.HasPrefix(data, []byte("RIFF")) && bytes.HasPrefix(data[8:], []byte("WEBP")):
return "webp"
default:
return "unknown"
}
}
+163
View File
@@ -0,0 +1,163 @@
{
"meta": {
"lastTouchedVersion": "2026.2.24",
"lastTouchedAt": "2026-02-26T06:26:29.404Z"
},
"wizard": {
"lastRunAt": "2026-02-26T02:54:36.789Z",
"lastRunVersion": "2026.2.24",
"lastRunCommand": "onboard",
"lastRunMode": "local"
},
"models": {
"mode": "merge",
"providers": {
"custom-api-deepseek-com": {
"baseUrl": "https://api.deepseek.com/v1",
"apiKey": "sk-xxx",
"api": "openai-completions",
"models": [
{
"id": "deepseek-chat",
"name": "deepseek-chat (Custom Provider)",
"reasoning": false,
"input": [
"text"
],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 16000,
"maxTokens": 16000
}
]
}
}
},
"agents": {
"defaults": {
"model": {
"primary": "custom-api-deepseek-com/deepseek-chat"
},
"models": {
"custom-api-deepseek-com/deepseek-chat": {
"alias": "deepseek-chat"
}
},
"workspace": "/Users/yincong/.openclaw/workspace",
"compaction": {
"mode": "safeguard"
},
"maxConcurrent": 4,
"subagents": {
"maxConcurrent": 8
}
}
},
"messages": {
"ackReactionScope": "group-mentions"
},
"commands": {
"native": "auto",
"nativeSkills": "auto",
"restart": true,
"ownerDisplay": "raw"
},
"session": {
"dmScope": "per-channel-peer"
},
"hooks": {
"internal": {
"enabled": true,
"entries": {
"command-logger": {
"enabled": true
}
}
}
},
"channels": {
"telegram": {
"enabled": true,
"dmPolicy": "pairing",
"botToken": "8430228911:xxx",
"groupPolicy": "allowlist",
"streaming": "off"
},
"qq": {
"wsUrl": "ws://127.0.0.1:58080/ws",
"accessToken": "MuseBot",
"admins": "12345678,87654321",
"adminOnlyChat": false,
"notifyNonAdminBlocked": false,
"nonAdminBlockedMessage": "当前仅管理员可触发机器人。\n如需使用请联系管理员。",
"blockedNotifyCooldownMs": 10000,
"showProcessingStatus": true,
"processingStatusDelayMs": 500,
"processingStatusText": "输入中",
"allowedGroups": "10001,10002",
"blockedUsers": "999999",
"systemPrompt": "你是一个名为“人工智障”的QQ机器人,说话风格要风趣幽默。",
"historyLimit": 3,
"keywordTriggers": "小助手, 帮助",
"autoApproveRequests": true,
"enableGuilds": true,
"enableTTS": false,
"sharedMediaHostDir": "/Users/yourname/openclaw_qq/deploy/napcat/shared_media",
"sharedMediaContainerDir": "/openclaw_media",
"rateLimitMs": 1000,
"formatMarkdown": true,
"antiRiskMode": false,
"maxMessageLength": 4000
}
},
"gateway": {
"port": 18789,
"mode": "local",
"bind": "loopback",
"auth": {
"mode": "token",
"token": "xxx"
},
"tailscale": {
"mode": "off",
"resetOnExit": false
},
"nodes": {
"denyCommands": [
"camera.snap",
"camera.clip",
"screen.record",
"calendar.add",
"contacts.add",
"reminders.add"
]
}
},
"plugins": {
"allow": [
"qq",
"telegram"
],
"entries": {
"telegram": {
"enabled": true
},
"qq": {
"enabled": true
}
},
"installs": {
"qq": {
"source": "path",
"sourcePath": "/Users/xxx/go/src/github.com/yincongcyincong/openclaw/extensions/qq",
"installPath": "/Users/xxx/.openclaw/extensions/qq",
"version": "1.0.0",
"installedAt": "2026-02-26T03:12:47.749Z"
}
}
}
}
+21 -1
View File
@@ -36,4 +36,24 @@ curl -i -X POST \
返回结果:
{"status":"ok"}
```
```
## 接入openclaw
```
# 进入插件目录
cd openclaw/extensions
# 克隆仓库
git clone https://github.com/constansino/openclaw_qq.git qq
# 安装依赖并构建
cd ../..
pnpm install && pnpm build
# 安装openclaw命令
npm install -g openclaw@latest
# 根据 https://github.com/constansino/openclaw_qq 把extension放到extensions目录下
openclaw plugins install ./extensions/qq
# 启动openclaw
openclaw gateway run
# 启动onebot
./onebot -wechat_pid=18835 -image_path='/Users/xxx/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/wxid_xxx/temp/xxx/2026-01/Img/ -conn_type=websocket
```
+25 -12
View File
@@ -1077,8 +1077,14 @@ function setReceiver() {
// 3. 开始拦截
Interceptor.attach(buf2RespAddr, {
onEnter: function (args) {
const currentPtr = this.context.x1;
// console.log(" [+] currentPtr: ", hexdump(currentPtr, {
// offset: 0,
// length: 512,
// header: true,
// ansi: true
// }));
let start = 0x1e;
let senderLen = currentPtr.add(start).readU8();
if (senderLen !== 0x14 && senderLen !== 0x13) {
@@ -1114,14 +1120,12 @@ function setReceiver() {
var messages = [];
var senderNickname = ""
let splitIndex = content.indexOf(':')
let pureContent = content.substring(splitIndex + 1).trim();
if (sender.includes("@chatroom")) {
msgType = "group"
groupId = sender
senderUser = content.substring(0, splitIndex).trim();
let splitIndex = content?.indexOf(':')
let pureContent = content?.substring(splitIndex + 1).trim();
const parts = pureContent.split('\u2005');
for (let part of parts) {
part = part.trim();
@@ -1145,23 +1149,31 @@ function setReceiver() {
}
// 处理用户的名称
splitIndex = userContent.indexOf(':')
splitIndex = userContent?.indexOf(':')
if (splitIndex === -1) {
splitIndex = userContent.indexOf('在群聊中@了你')
senderNickname = userContent.substring(0, splitIndex).trim();
splitIndex = userContent?.indexOf('在群聊中@了你')
senderNickname = userContent?.substring(0, splitIndex).trim();
} else {
senderNickname = userContent.substring(0, splitIndex).trim();
senderNickname = userContent?.substring(0, splitIndex).trim();
}
if (!senderNickname) {
senderNickname = sender
}
} else {
// 处理用户的名称
const splitIndex = userContent.indexOf(':')
senderNickname = userContent.substring(0, splitIndex).trim();
messages.push({type: "text", data: {text: pureContent}});
const splitIndex = userContent?.indexOf(':')
senderNickname = userContent?.substring(0, splitIndex).trim();
if (!senderNickname) {
senderNickname = sender
}
messages.push({type: "text", data: {text: content}});
}
const msgId = generateAESKey()
send({
time: Date.now(),
post_type: "message",
message_type: msgType,
user_id: senderUser, // 发送人的 ID
self_id: selfId, // 接收人的 ID
@@ -1171,6 +1183,7 @@ function setReceiver() {
raw: {peerUid: msgId},
message: messages,
sender: {user_id: senderUser, nickname: senderNickname},
raw_message: ""
})
},
});
+88
View File
@@ -0,0 +1,88 @@
package main
import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"strings"
"time"
)
func SaveBase64Image(base64Data string) (string, string, error) {
rawContents := base64Data
if strings.HasPrefix(base64Data, "base64://") {
rawContents = strings.TrimPrefix(base64Data, "base64://")
} else if idx := strings.Index(base64Data, ","); idx != -1 {
rawContents = base64Data[idx+1:]
}
data, err := base64.StdEncoding.DecodeString(rawContents)
if err != nil {
return "", "", fmt.Errorf("base64 decode failed: %v", err)
}
salt := []byte(fmt.Sprintf("\n#md5_salt_%d_%d#", time.Now().UnixNano(), rand.Intn(10000)))
data = append(data, salt...)
r := rand.New(rand.NewSource(time.Now().UnixNano()))
randomNumber := r.Intn(1000) // 生成 0-999 的随机数
timestamp := time.Now().Unix()
fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, DetectImageFormat(data))
targetPath := config.ImagePath + fileName
dir := filepath.Dir(targetPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return "", "", fmt.Errorf("create directory failed: %v", err)
}
err = os.WriteFile(targetPath, data, 0644)
if err != nil {
return "", "", fmt.Errorf("write file failed: %v", err)
}
md5Str, err := GetFileMD5(targetPath)
if err != nil {
return "", "", fmt.Errorf("get file md5 failed: %v", err)
}
return targetPath, md5Str, nil
}
func GetFileMD5(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func DetectImageFormat(data []byte) string {
if len(data) < 12 {
return "unknown"
}
switch {
case bytes.HasPrefix(data, []byte{0xFF, 0xD8, 0xFF}):
return "jpg"
case bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}):
return "png"
case bytes.HasPrefix(data, []byte("GIF87a")) || bytes.HasPrefix(data, []byte("GIF89a")):
return "gif"
case bytes.HasPrefix(data, []byte{0x42, 0x4D}):
return "bmp"
case bytes.HasPrefix(data, []byte("RIFF")) && bytes.HasPrefix(data[8:], []byte("WEBP")):
return "webp"
default:
return "unknown"
}
}
+186
View File
@@ -0,0 +1,186 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{}
var conn *websocket.Conn
type OneBotWSMsg struct {
Action string `json:"action"`
Echo string `json:"echo"`
Params *WSParams `json:"params"`
}
type WSParams struct {
Message interface{} `json:"message"`
UserID string `json:"user_id"`
GroupID string `json:"group_id"`
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
var err error
conn, err = upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("升级失败:", err)
return
}
defer conn.Close()
fmt.Println("机器人已连接!")
for {
m := new(OneBotWSMsg)
_, msgByte, err := conn.ReadMessage()
if err != nil {
log.Println("读取失败:", err)
break
}
fmt.Println("收到消息: " + string(msgByte))
err = json.Unmarshal(msgByte, m)
if err != nil {
log.Println("解析失败:", err)
continue
}
switch m.Action {
case "get_login_info":
err = conn.WriteJSON(map[string]any{
"echo": m.Echo,
"status": "ok",
"data": map[string]any{
"user_id": myWechatId,
"nickname": myWechatId,
},
})
if err != nil {
log.Println("写入失败:", err)
break
}
case "send_private_msg", "send_group_msg":
err = SendWS(m.Params)
if err != nil {
log.Println("发送失败:", err)
break
}
}
}
}
func SendWebSocketMsg(msg map[string]interface{}) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic: %v\n", r)
}
}()
time.Sleep(1 * time.Second)
// 这里处理你的 X1 数据
jsonData, err := json.Marshal(msg["payload"])
if err != nil {
log.Printf("JSON 序列化失败: %v\n", err)
return
}
fmt.Printf("发送数据: %s\n", string(jsonData))
if myWechatId == "" {
m := new(WechatMessage)
err = json.Unmarshal(jsonData, m)
if err != nil {
log.Printf("解析消息失败: %v\n", err)
return
}
myWechatId = m.SelfID
if m.GroupId != "" {
userID2NicknameMap.Store(m.GroupId+"_"+m.UserID, m.Sender.Nickname)
}
}
err = conn.WriteMessage(websocket.TextMessage, jsonData)
if err != nil {
log.Printf("发送消息失败: %v\n", err)
return
}
}
func SendWS(req *WSParams) error {
sendContent := ""
atUserID := ""
if msg, ok := req.Message.(string); ok {
sendContent = msg
} else {
bytes, err := json.Marshal(req.Message)
if err != nil {
log.Printf("JSON 序列化失败: %v\n", err)
return err
}
msgs := make([]*Message, 0)
err = json.Unmarshal(bytes, &msgs)
if err != nil {
log.Printf("JSON 反序列化失败: %v\n", err)
return err
}
for _, v := range msgs {
if v.Type == "text" {
sendContent += v.Data.Text
} else if v.Type == "at" {
if req.GroupID != "" {
if nicknameInter, ok := userID2NicknameMap.Load(req.GroupID + "_" + v.Data.QQ); ok {
sendContent += fmt.Sprintf("@%s\u2005", nicknameInter.(string))
atUserID += v.Data.QQ + ","
}
}
} else if v.Type == "image" {
msgChan <- &SendMsg{
UserId: req.UserID,
GroupID: req.GroupID,
Content: v.Data.File,
Type: v.Type,
}
}
}
}
if sendContent != "" {
msgChan <- &SendMsg{
UserId: req.UserID,
GroupID: req.GroupID,
Content: sendContent,
Type: "text",
AtUser: strings.TrimRight(atUserID, ","),
}
}
return nil
}
func testWebSocket(w http.ResponseWriter, r *http.Request) {
jsonData, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("读取消息失败: %v\n", err)
return
}
err = conn.WriteMessage(websocket.TextMessage, jsonData)
if err != nil {
log.Printf("发送消息失败: %v\n", err)
return
}
}
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"context"
"fmt"
"log"
"sync/atomic"
"time"
)
func SendWorker() {
defer func() {
if err := recover(); err != nil {
log.Printf("💥 SendWorker 异常: %v\n", err)
go SendWorker()
}
}()
for {
select {
case <-finishChan:
fmt.Printf("收到完成信号 \n")
case m, ok := <-msgChan:
if !ok {
return
}
SendWechatMsg(m)
}
}
}
func SendWechatMsg(m *SendMsg) {
time.Sleep(1 * time.Second)
currTaskId := atomic.AddInt64(&taskId, 1)
log.Printf("📩 收到任务: %d\n", currTaskId)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
targetId := m.UserId
if m.GroupID != "" && targetId == "" {
targetId = m.GroupID
}
switch m.Type {
case "text":
result := fridaScript.ExportsCall("triggerSendTextMessage", currTaskId, targetId, m.Content, m.AtUser)
log.Printf("📩 发送文本任务执行结果:%s, 参数:currTaskId: %d, targetId: %s, content: %s, atUser: %s\n",
result, currTaskId, targetId, m.Content, m.AtUser)
case "image":
targetPath, md5Str, err := SaveBase64Image(m.Content)
if err != nil {
log.Printf("保存图片失败: %v\n", err)
return
}
result := fridaScript.ExportsCall("triggerUploadImg", targetId, md5Str, targetPath)
log.Printf("📩 上传图片任务执行结果%s, 参数:targetId: %s, md5Str: %s, targetPath: %s\n", result, targetId, md5Str, targetPath)
case "send_image":
result := fridaScript.ExportsCall("triggerSendImgMessage", currTaskId, myWechatId, targetId)
log.Printf("📩 发送图片任务执行结果%s, 参数:currTaskId: %d, myWechatId: %s, targetId: %s\n", result, currTaskId, myWechatId, targetId)
}
select {
case <-ctx.Done():
log.Printf("任务 %d 执行超时!\n", currTaskId)
case <-finishChan:
log.Printf("收到完成信号,任务 %d 完成\n", currTaskId)
}
}