docs: weixin ClawBot bridge protocol and examples

This commit is contained in:
AskaEth committed 2026-08-21 18:15:20 +08:00
commit 4ea3957da9
13 files changed
+2158

No files matched your search

+223
View File
@@ -0,0 +1,223 @@
// main.go — 微信 ClawBot iLink 最小示例(Go,仅标准库)
// 流程: 扫码登录 → 长轮询收消息 → 回声回复
// 运行: go run main.go
package main
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
)
const baseURL = "https://ilinkai.weixin.qq.com"
var stateFile = filepath.Join(func() string { d, _ := os.Executable(); return filepath.Dir(d) }(), "bridge-state.json")
// ---------- 状态持久化: bot_token / 游标 / context_token ----------
type State struct {
BotToken string `json:"bot_token"`
BotID string `json:"bot_id,omitempty"`
Cursor string `json:"cursor"`
Contexts map[string]string `json:"contexts"`
}
var state = State{Contexts: map[string]string{}}
func loadState() {
if b, err := os.ReadFile(stateFile); err == nil {
json.Unmarshal(b, &state)
}
if state.Contexts == nil {
state.Contexts = map[string]string{}
}
}
func saveState() {
b, _ := json.MarshalIndent(state, "", " ")
os.WriteFile(stateFile, b, 0o600)
}
// ---------- 通用请求头: 鉴权 + 防重放 X-WECHAT-UIN ----------
func headers(req *http.Request) {
var u [4]byte
rand.Read(u[:])
req.Header.Set("Content-Type", "application/json")
req.Header.Set("AuthorizationType", "ilink_bot_token")
req.Header.Set("X-WECHAT-UIN", base64.StdEncoding.EncodeToString(
[]byte(fmt.Sprint(binary.BigEndian.Uint32(u[:])))))
if state.BotToken != "" {
req.Header.Set("Authorization", "Bearer "+state.BotToken)
}
}
func apiPost(path string, body any, timeout time.Duration) (map[string]any, error) {
buf, _ := json.Marshal(body)
req, err := http.NewRequest("POST", baseURL+path, bytes.NewReader(buf))
if err != nil {
return nil, err
}
headers(req)
client := &http.Client{Timeout: timeout}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var out map[string]any
return out, json.NewDecoder(resp.Body).Decode(&out)
}
func apiGet(path string, extraHeaders map[string]string) (map[string]any, error) {
req, err := http.NewRequest("GET", baseURL+path, nil)
if err != nil {
return nil, err
}
for k, v := range extraHeaders {
req.Header.Set(k, v)
}
resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var out map[string]any
return out, json.NewDecoder(resp.Body).Decode(&out)
}
// ---------- 接口1+2: 扫码登录(get_bot_qrcode / get_qrcode_status) ----------
func login() error {
qr, err := apiGet("/ilink/bot/get_bot_qrcode?bot_type=3", nil)
if err != nil {
return err
}
fmt.Println("请用微信扫码:", qr["qrcode_img_content"])
deadline := time.Now().Add(8 * time.Minute)
for time.Now().Before(deadline) {
s, err := apiGet("/ilink/bot/get_qrcode_status?qrcode="+url(qr["qrcode"]),
map[string]string{"iLink-App-ClientVersion": "1"})
if err == nil && s["status"] == "confirmed" { // wait → scaned → confirmed
state.BotToken, _ = s["bot_token"].(string)
state.BotID, _ = s["ilink_bot_id"].(string)
saveState()
fmt.Println("登录成功, bot:", state.BotID)
return nil
}
time.Sleep(time.Second)
}
return fmt.Errorf("扫码超时")
}
func url(v any) string { // 极简 query 转义(qrcode 内容一般安全,正式代码用 url.QueryEscape)
s, _ := v.(string)
return s
}
// ---------- 你的 AI 后端: 替换成自己项目的调用 ----------
func AskYourAI(peerID, text string) string {
return "你说的是:" + text
}
// ---------- 接口4: POST /ilink/bot/sendmessage ----------
func sendText(toUserID, text, contextToken string) {
if text == "" {
text = " "
}
for i := 0; i < len(text); i += 1500 { // 长文本分片(按字节切仅适合演示,正式按 rune 切)
end := i + 1500
if end > len(text) {
end = len(text)
}
apiPost("/ilink/bot/sendmessage", map[string]any{
"msg": map[string]any{
"to_user_id": toUserID,
"client_id": fmt.Sprintf("%d-%d", time.Now().UnixNano(), i),
"message_type": 2, // bot 消息
"message_state": 2, // FINISH
"context_token": contextToken,
"item_list": []any{map[string]any{
"type": 1, "text_item": map[string]string{"text": text[i:end]},
}},
},
}, 15*time.Second)
}
}
// ---------- 接口3: POST /ilink/bot/getupdates 长轮询 ----------
func pollLoop() {
failures := 0
for {
resp, err := apiPost("/ilink/bot/getupdates", map[string]any{
"get_updates_buf": state.Cursor,
"base_info": map[string]string{"channel_version": "1.0.2"},
}, 45*time.Second)
if err != nil {
if os.IsTimeout(err) { // 长轮询正常超时
continue
}
failures++
wait := 2 * time.Second
if failures > 3 {
wait = 30 * time.Second // 退避: 2s ×3 → 30s
}
fmt.Printf("轮询异常(%d次),%v 后重试: %v\n", failures, wait, err)
time.Sleep(wait)
continue
}
if code, _ := resp["errcode"].(float64); code == -14 { // 会话过期
fmt.Println("会话过期(errcode=-14),请删除 bridge-state.json 重新扫码")
os.Exit(2)
}
if buf, ok := resp["get_updates_buf"].(string); ok && buf != "" {
state.Cursor = buf // 游标立即落盘
saveState()
}
msgs, _ := resp["msgs"].([]any)
for _, m := range msgs {
msg, _ := m.(map[string]any)
if t, _ := msg["message_type"].(float64); t != 1 { // 只处理用户消息
continue
}
text := ""
for _, it := range msg["item_list"].([]any) {
item, _ := it.(map[string]any)
if t, _ := item["type"].(float64); t == 1 {
if ti, ok := item["text_item"].(map[string]any); ok {
s, _ := ti["text"].(string)
text += s + "\n"
}
}
}
if text == "" {
continue
}
peer, _ := msg["from_user_id"].(string)
token, _ := msg["context_token"].(string)
state.Contexts[peer] = token
saveState()
sendText(peer, AskYourAI(peer, text), token)
}
failures = 0
}
}
func main() {
loadState()
if state.BotToken == "" {
if err := login(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
pollLoop()
}