Files
weixin_ClawBot_Bridge/examples/go/main.go
T

224 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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()
}