docs/examples: 扫码登录改为官方 POST+local_token_list+iLink-App-Id/ClientVersion 头 + 完整状态机 + 换码竞态警告
This commit is contained in:
1 parent
38b776705d
commit
8a36915377
6 files changed
+566
-96
No files matched your search
+90
-13
@@ -11,6 +11,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -18,12 +19,16 @@ import (
|
||||
|
||||
const baseURL = "https://ilinkai.weixin.qq.com"
|
||||
|
||||
// apiBaseURL 为扫码轮询的 base;收到 scaned_but_redirect 时切换,其余 API 仍走固定 baseURL。
|
||||
var apiBaseURL = baseURL
|
||||
|
||||
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"`
|
||||
BaseURL string `json:"baseurl,omitempty"`
|
||||
Cursor string `json:"cursor"`
|
||||
Contexts map[string]string `json:"contexts"`
|
||||
}
|
||||
@@ -44,7 +49,9 @@ func saveState() {
|
||||
os.WriteFile(stateFile, b, 0o600)
|
||||
}
|
||||
|
||||
// ---------- 通用请求头: 鉴权 + 防重放 X-WECHAT-UIN ----------
|
||||
// ---------- 通用请求头: 鉴权 + 防重放 X-WECHAT-UIN + iLink-App 标识 ----------
|
||||
// 注: get_bot_qrcode / get_qrcode_status(预绑定阶段)无 token 时不带 Authorization,
|
||||
// 但 iLink-App-Id / iLink-App-ClientVersion / AuthorizationType / X-WECHAT-UIN 恒带。
|
||||
func headers(req *http.Request) {
|
||||
var u [4]byte
|
||||
rand.Read(u[:])
|
||||
@@ -52,6 +59,9 @@ func headers(req *http.Request) {
|
||||
req.Header.Set("AuthorizationType", "ilink_bot_token")
|
||||
req.Header.Set("X-WECHAT-UIN", base64.StdEncoding.EncodeToString(
|
||||
[]byte(fmt.Sprint(binary.BigEndian.Uint32(u[:])))))
|
||||
req.Header.Set("iLink-App-Id", "bot")
|
||||
// 2.4.8 -> (2&0xff)<<16 | (4&0xff)<<8 | (8&0xff) = 132104(数字字符串,非 "1")
|
||||
req.Header.Set("iLink-App-ClientVersion", "132104")
|
||||
if state.BotToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+state.BotToken)
|
||||
}
|
||||
@@ -74,11 +84,13 @@ func apiPost(path string, body any, timeout time.Duration) (map[string]any, erro
|
||||
return out, json.NewDecoder(resp.Body).Decode(&out)
|
||||
}
|
||||
|
||||
// apiGet 仅用于扫码状态轮询,base 用 apiBaseURL(可被 scaned_but_redirect 切换)。
|
||||
func apiGet(path string, extraHeaders map[string]string) (map[string]any, error) {
|
||||
req, err := http.NewRequest("GET", baseURL+path, nil)
|
||||
req, err := http.NewRequest("GET", apiBaseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers(req)
|
||||
for k, v := range extraHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
@@ -91,34 +103,99 @@ func apiGet(path string, extraHeaders map[string]string) (map[string]any, error)
|
||||
return out, json.NewDecoder(resp.Body).Decode(&out)
|
||||
}
|
||||
|
||||
// ---------- 接口1+2: 扫码登录(get_bot_qrcode / get_qrcode_status) ----------
|
||||
// ---------- 接口1: POST /ilink/bot/get_bot_qrcode 获取登录二维码(官方姿势) ----------
|
||||
func fetchQRCode() (map[string]any, error) {
|
||||
// 官方为 POST + body {"local_token_list":[...]},非裸 GET。首次绑定传空数组;
|
||||
// 若已持久化其它 bot_token 可收集传入(最多 10 个),官方据此识别触发 binded_redirect。
|
||||
return apiPost("/ilink/bot/get_bot_qrcode?bot_type=3",
|
||||
map[string]any{"local_token_list": []string{}}, 15*time.Second)
|
||||
}
|
||||
|
||||
// ---------- 接口2: GET /ilink/bot/get_qrcode_status 轮询扫码状态(完整状态机) ----------
|
||||
func login() error {
|
||||
qr, err := apiGet("/ilink/bot/get_bot_qrcode?bot_type=3", nil)
|
||||
// 状态机: wait / scaned / scaned_but_redirect / need_verifycode /
|
||||
// verify_code_blocked / binded_redirect / confirmed / expired
|
||||
// 两大坑(务必读 docs/02 §4.5):
|
||||
// ① 码服务端 TTL≈60s,生成后须在窗口内扫(超窗服务端自返回 expired,此处换新码)。
|
||||
// ② 「换码竞态」:一旦收到 scaned(或 scaned_but_redirect / need_verifycode),
|
||||
// 绝不能再换码——换码会砍掉"扫码→确认"会话,微信端显示成功但后端永远收不到 confirmed。
|
||||
qr, err := fetchQRCode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("请用微信扫码:", qr["qrcode_img_content"])
|
||||
qrcode, _ := qr["qrcode"].(string)
|
||||
img, _ := qr["qrcode_img_content"].(string)
|
||||
fmt.Println("请用微信扫码:", img)
|
||||
fmt.Println("(二维码内容即上面的 liteapp URL,请用二维码库编码展示;勿把 qrcode 字段当二维码)")
|
||||
deadline := time.Now().Add(8 * time.Minute)
|
||||
var verifyCode string
|
||||
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
|
||||
path := "/ilink/bot/get_qrcode_status?qrcode=" + url.QueryEscape(qrcode)
|
||||
if verifyCode != "" {
|
||||
path += "&verify_code=" + url.QueryEscape(verifyCode)
|
||||
}
|
||||
s, err := apiGet(path, nil)
|
||||
if err != nil {
|
||||
time.Sleep(time.Second) // 网络抖动按等待处理继续轮询
|
||||
continue
|
||||
}
|
||||
switch st, _ := s["status"].(string); st {
|
||||
case "wait":
|
||||
fmt.Println("等待扫码...")
|
||||
case "scaned":
|
||||
verifyCode = ""
|
||||
fmt.Println("已扫码,请在手机上确认...(此后绝不换码,避免砍掉确认会话)")
|
||||
case "scaned_but_redirect":
|
||||
verifyCode = ""
|
||||
if host, _ := s["redirect_host"].(string); host != "" {
|
||||
apiBaseURL = "https://" + host
|
||||
fmt.Println("IDC 重定向,切换轮询至", apiBaseURL)
|
||||
}
|
||||
case "need_verifycode":
|
||||
fmt.Print("请在手机微信上输入显示的数字后回车: ")
|
||||
fmt.Scanln(&verifyCode)
|
||||
continue
|
||||
case "verify_code_blocked":
|
||||
verifyCode = ""
|
||||
fmt.Println("配对码多次输入错误,重新获取二维码")
|
||||
if qr, err = fetchQRCode(); err != nil {
|
||||
return err
|
||||
}
|
||||
qrcode, _ = qr["qrcode"].(string)
|
||||
img, _ = qr["qrcode_img_content"].(string)
|
||||
fmt.Println("新二维码:", img)
|
||||
continue
|
||||
case "binded_redirect":
|
||||
fmt.Println("该 bot 已绑定过本实例,视为连接成功")
|
||||
return nil
|
||||
case "confirmed":
|
||||
state.BotToken, _ = s["bot_token"].(string)
|
||||
state.BotID, _ = s["ilink_bot_id"].(string)
|
||||
if bu, _ := s["baseurl"].(string); bu != "" {
|
||||
state.BaseURL = bu
|
||||
}
|
||||
saveState()
|
||||
fmt.Println("登录成功, bot:", state.BotID)
|
||||
return nil
|
||||
case "expired":
|
||||
// 码过期(服务端作废)→ 换新码;仅在 expired 时换,scaned 后绝不换
|
||||
verifyCode = ""
|
||||
fmt.Println("二维码已过期,重新获取")
|
||||
if qr, err = fetchQRCode(); err != nil {
|
||||
return err
|
||||
}
|
||||
qrcode, _ = qr["qrcode"].(string)
|
||||
img, _ = qr["qrcode_img_content"].(string)
|
||||
fmt.Println("新二维码:", img)
|
||||
continue
|
||||
default:
|
||||
fmt.Println("未知状态:", st)
|
||||
}
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user