diff --git a/frida/receiver.js b/frida/receiver.js index 90d6b71..30553d6 100644 --- a/frida/receiver.js +++ b/frida/receiver.js @@ -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; -} \ No newline at end of file +} + +// -----------------------测试函数------------------------- + +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); +} diff --git a/onebot/go.mod b/onebot/go.mod index fa47074..823cd3e 100644 --- a/onebot/go.mod +++ b/onebot/go.mod @@ -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 diff --git a/onebot/go.sum b/onebot/go.sum index caeaed6..4368817 100644 --- a/onebot/go.sum +++ b/onebot/go.sum @@ -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= diff --git a/onebot/http.go b/onebot/http.go new file mode 100644 index 0000000..1f60446 --- /dev/null +++ b/onebot/http.go @@ -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)) +} diff --git a/onebot/main.go b/onebot/main.go index b1b733d..e58db86 100644 --- a/onebot/main.go +++ b/onebot/main.go @@ -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" - } -} diff --git a/onebot/openclaw.json b/onebot/openclaw.json new file mode 100644 index 0000000..df8ef14 --- /dev/null +++ b/onebot/openclaw.json @@ -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" + } + } + } +} diff --git a/onebot/readme.md b/onebot/readme.md index 49aeb2d..9658295 100644 --- a/onebot/readme.md +++ b/onebot/readme.md @@ -36,4 +36,24 @@ curl -i -X POST \ 返回结果: {"status":"ok"} -``` \ No newline at end of file +``` + +## 接入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 +``` + diff --git a/onebot/script.js b/onebot/script.js index f0668c4..59f870b 100644 --- a/onebot/script.js +++ b/onebot/script.js @@ -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: "" }) }, }); diff --git a/onebot/utils.go b/onebot/utils.go new file mode 100644 index 0000000..ec88e7b --- /dev/null +++ b/onebot/utils.go @@ -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" + } +} diff --git a/onebot/websocket.go b/onebot/websocket.go new file mode 100644 index 0000000..6d5ca83 --- /dev/null +++ b/onebot/websocket.go @@ -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 + } +} diff --git a/onebot/worker.go b/onebot/worker.go new file mode 100644 index 0000000..7f1cc17 --- /dev/null +++ b/onebot/worker.go @@ -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) + } +}