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
+139
-25
@@ -37,12 +37,17 @@ const saveState = () =>
|
||||
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 0o600 });
|
||||
|
||||
// ---------- iLink HTTP 封装 ----------
|
||||
// 注: get_bot_qrcode / get_qrcode_status(预绑定阶段)无 token 时不带 Authorization,
|
||||
// 但 iLink-App-Id / iLink-App-ClientVersion / AuthorizationType / X-WECHAT-UIN 恒带。
|
||||
function headers() {
|
||||
const uin = Buffer.from(String(randomBytes(4).readUInt32BE(0))).toString("base64");
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
AuthorizationType: "ilink_bot_token",
|
||||
"X-WECHAT-UIN": uin,
|
||||
"iLink-App-Id": "bot",
|
||||
// 2.4.8 -> (2&0xff)<<16 | (4&0xff)<<8 | (8&0xff) = 132104(数字字符串,非 "1")
|
||||
"iLink-App-ClientVersion": "132104",
|
||||
...(state.bot_token ? { Authorization: `Bearer ${state.bot_token}` } : {}),
|
||||
};
|
||||
}
|
||||
@@ -58,29 +63,86 @@ async function apiPost(path, body, timeoutMs = 45_000) {
|
||||
}
|
||||
|
||||
// ---------- 1. 扫码登录(一次性) ----------
|
||||
async function login() {
|
||||
const qr = await fetch(`${BASE_URL}/ilink/bot/get_bot_qrcode?bot_type=3`)
|
||||
.then((r) => r.json());
|
||||
// 接口1: POST /ilink/bot/get_bot_qrcode(官方姿势:body={"local_token_list":[]},非裸 GET)
|
||||
async function refreshQR() {
|
||||
const qr = await fetch(`${BASE_URL}/ilink/bot/get_bot_qrcode?bot_type=3`, {
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ local_token_list: state.bot_token ? [state.bot_token] : [] }),
|
||||
}).then((r) => r.json());
|
||||
console.log("请用微信扫码:", qr.qrcode_img_content);
|
||||
// 终端显示二维码: npm i qrcode-terminal,然后 qrcode.generate(qr.qrcode_img_content, {small:true})
|
||||
// qrcode_img_content 是 liteapp 跳转 URL,须作为二维码内容编码展示,勿把 qr.qrcode 当二维码
|
||||
// 终端显示: npm i qrcode-terminal,然后 qrcode.generate(qr.qrcode_img_content, {small:true})
|
||||
return qr.qrcode;
|
||||
}
|
||||
|
||||
async function login() {
|
||||
// 状态机: 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。
|
||||
let base = BASE_URL;
|
||||
let qrcode = await refreshQR();
|
||||
let verifyCode = null;
|
||||
const deadline = Date.now() + 8 * 60_000;
|
||||
while (Date.now() < deadline) {
|
||||
const s = await fetch(
|
||||
`${BASE_URL}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qr.qrcode)}`,
|
||||
{ headers: { "iLink-App-ClientVersion": "1" } }
|
||||
).then((r) => r.json());
|
||||
let ep = `${base}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
|
||||
if (verifyCode) ep += `&verify_code=${encodeURIComponent(verifyCode)}`;
|
||||
const s = await fetch(ep, { headers: headers() }).then((r) => r.json());
|
||||
|
||||
if (s.status === "confirmed") {
|
||||
state.bot_token = s.bot_token;
|
||||
state.bot_id = s.ilink_bot_id;
|
||||
state.owner_id = s.ilink_user_id;
|
||||
saveState();
|
||||
console.log("登录成功, bot:", s.ilink_bot_id);
|
||||
return;
|
||||
switch (s.status) {
|
||||
case "wait":
|
||||
process.stdout.write(`\r等待扫码... status=${s.status} `);
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
continue;
|
||||
case "scaned":
|
||||
verifyCode = null;
|
||||
console.log("\n已扫码,请在手机上确认...(此后绝不换码,避免砍掉确认会话)");
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
continue;
|
||||
case "scaned_but_redirect":
|
||||
verifyCode = null;
|
||||
if (s.redirect_host) {
|
||||
base = `https://${s.redirect_host}`;
|
||||
console.log("IDC 重定向,切换轮询至", base);
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
continue;
|
||||
case "need_verifycode": {
|
||||
const { createInterface } = await import("node:readline");
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
verifyCode = await new Promise((r) => rl.question("请在手机微信上输入显示的数字后回车: ", r));
|
||||
rl.close();
|
||||
continue;
|
||||
}
|
||||
case "verify_code_blocked":
|
||||
verifyCode = null;
|
||||
console.log("\n配对码多次输入错误,重新获取二维码");
|
||||
qrcode = await refreshQR();
|
||||
continue;
|
||||
case "binded_redirect":
|
||||
console.log("该 bot 已绑定过本实例,视为连接成功");
|
||||
return;
|
||||
case "confirmed":
|
||||
state.bot_token = s.bot_token;
|
||||
state.bot_id = s.ilink_bot_id;
|
||||
state.owner_id = s.ilink_user_id;
|
||||
state.baseurl = s.baseurl;
|
||||
saveState();
|
||||
console.log("登录成功, bot:", s.ilink_bot_id);
|
||||
return;
|
||||
case "expired":
|
||||
verifyCode = null;
|
||||
console.log("\n二维码已过期,重新获取");
|
||||
qrcode = await refreshQR();
|
||||
continue;
|
||||
default:
|
||||
console.log("\n未知状态:", s.status);
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
continue;
|
||||
}
|
||||
process.stdout.write(`\r等待扫码... status=${s.status} `);
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
throw new Error("扫码超时,请重试");
|
||||
}
|
||||
@@ -203,11 +265,16 @@ def save_state():
|
||||
os.chmod(STATE_FILE, 0o600)
|
||||
|
||||
# ---------- iLink HTTP 封装 ----------
|
||||
# 注: get_bot_qrcode / get_qrcode_status(预绑定阶段)无 token 时不带 Authorization,
|
||||
# 但 iLink-App-Id / iLink-App-ClientVersion / AuthorizationType / X-WECHAT-UIN 恒带。
|
||||
def headers():
|
||||
h = {
|
||||
"Content-Type": "application/json",
|
||||
"AuthorizationType": "ilink_bot_token",
|
||||
"X-WECHAT-UIN": base64.b64encode(str(random.getrandbits(32)).encode()).decode(),
|
||||
"iLink-App-Id": "bot",
|
||||
# 2.4.8 -> (2&0xff)<<16 | (4&0xff)<<8 | (8&0xff) = 132104(数字字符串,非 "1")
|
||||
"iLink-App-ClientVersion": "132104",
|
||||
}
|
||||
if state["bot_token"]:
|
||||
h["Authorization"] = f"Bearer {state['bot_token']}"
|
||||
@@ -218,22 +285,69 @@ def api_post(path, body, timeout=45):
|
||||
return r.json()
|
||||
|
||||
# ---------- 1. 扫码登录(一次性) ----------
|
||||
# 接口1: POST /ilink/bot/get_bot_qrcode(官方姿势:body={"local_token_list":[]},非裸 GET)
|
||||
def fetch_qrcode():
|
||||
return requests.post(
|
||||
f"{BASE_URL}/ilink/bot/get_bot_qrcode?bot_type=3",
|
||||
headers=headers(), json={"local_token_list": []}, timeout=15,
|
||||
).json()
|
||||
|
||||
def login():
|
||||
qr = requests.get(f"{BASE_URL}/ilink/bot/get_bot_qrcode?bot_type=3").json()
|
||||
# 状态机: 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。
|
||||
base_url = BASE_URL
|
||||
qr = fetch_qrcode()
|
||||
qrcode = qr["qrcode"]
|
||||
print("请用微信扫码:", qr["qrcode_img_content"])
|
||||
# qrcode_img_content 是 liteapp 跳转 URL,须作为二维码内容编码展示,勿把 qr["qrcode"] 当二维码
|
||||
deadline = time.time() + 8 * 60
|
||||
verify_code = None
|
||||
while time.time() < deadline:
|
||||
s = requests.get(
|
||||
f"{BASE_URL}/ilink/bot/get_qrcode_status",
|
||||
params={"qrcode": qr["qrcode"]},
|
||||
headers={"iLink-App-ClientVersion": "1"},
|
||||
).json()
|
||||
if s.get("status") == "confirmed":
|
||||
params = {"qrcode": qrcode}
|
||||
if verify_code:
|
||||
params["verify_code"] = verify_code
|
||||
s = requests.get(f"{base_url}/ilink/bot/get_qrcode_status",
|
||||
params=params, headers=headers(), timeout=35).json()
|
||||
st = s.get("status")
|
||||
if st == "wait":
|
||||
print("等待扫码...")
|
||||
elif st == "scaned":
|
||||
verify_code = None
|
||||
print("已扫码,请在手机上确认...(此后绝不换码,避免砍掉确认会话)")
|
||||
elif st == "scaned_but_redirect":
|
||||
verify_code = None
|
||||
if s.get("redirect_host"):
|
||||
base_url = f"https://{s['redirect_host']}"
|
||||
print("IDC 重定向,切换轮询至", base_url)
|
||||
elif st == "need_verifycode":
|
||||
verify_code = input("请在手机微信上输入显示的数字后回车: ").strip()
|
||||
continue
|
||||
elif st == "verify_code_blocked":
|
||||
verify_code = None
|
||||
print("配对码多次输入错误,重新获取二维码")
|
||||
qr = fetch_qrcode(); qrcode = qr["qrcode"]; print("新二维码:", qr["qrcode_img_content"])
|
||||
continue
|
||||
elif st == "binded_redirect":
|
||||
print("该 bot 已绑定过本实例,视为连接成功")
|
||||
return
|
||||
elif st == "confirmed":
|
||||
state.update(bot_token=s["bot_token"], bot_id=s["ilink_bot_id"],
|
||||
owner_id=s["ilink_user_id"])
|
||||
owner_id=s["ilink_user_id"], baseurl=s.get("baseurl"))
|
||||
save_state()
|
||||
print("登录成功, bot:", s["ilink_bot_id"])
|
||||
return
|
||||
elif st == "expired":
|
||||
# 码过期(服务端作废)→ 换新码;仅在 expired 时换,scaned 后绝不换
|
||||
verify_code = None
|
||||
print("二维码已过期,重新获取")
|
||||
qr = fetch_qrcode(); qrcode = qr["qrcode"]; print("新二维码:", qr["qrcode_img_content"])
|
||||
continue
|
||||
else:
|
||||
print("未知状态:", st)
|
||||
time.sleep(1)
|
||||
raise RuntimeError("扫码超时")
|
||||
|
||||
|
||||
Reference in new issue
Block a user