// 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() }