Files
wechat-access-unqclawed/auth/qclaw-api.ts
HenryXiaoYang ba754ccc31 feat: add WeChat QR code login and AGP WebSocket channel plugin
- Auth module: WeChat OAuth2 scan-to-login flow with terminal QR code
- Token persistence to ~/.openclaw/wechat-access-auth.json (chmod 600)
- Token resolution: config > saved state > interactive login
- Invite code verification (configurable bypass)
- Production/test environment support
- AGP WebSocket client with heartbeat, reconnect, wake detection
- Message handler: Agent dispatch with streaming text and tool calls
- Random device GUID generation (persisted, no real machine ID)
2026-03-10 02:29:06 +08:00

130 lines
3.8 KiB
TypeScript
Raw Permalink 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.
/**
* @file qclaw-api.ts
* @description QClaw JPRX 网关 API 客户端
*
* 对应 Python demo 的 QClawAPI 类,所有业务接口走 POST {jprxGateway}data/{cmdId}/forward。
*/
import type { QClawEnvironment, QClawApiResponse } from "./types.js";
import { TokenExpiredError } from "./types.js";
import { nested } from "./utils.js";
export class QClawAPI {
private env: QClawEnvironment;
private guid: string;
/** 鉴权 key登录后可由服务端返回新值 */
loginKey = "m83qdao0AmE5";
jwtToken: string;
userId = "";
constructor(env: QClawEnvironment, guid: string, jwtToken = "") {
this.env = env;
this.guid = guid;
this.jwtToken = jwtToken;
}
private headers(): Record<string, string> {
const h: Record<string, string> = {
"Content-Type": "application/json",
"X-Version": "1",
"X-Token": this.loginKey,
"X-Guid": this.guid,
"X-Account": this.userId || "1",
"X-Session": "",
};
if (this.jwtToken) {
h["X-OpenClaw-Token"] = this.jwtToken;
}
return h;
}
private async post(path: string, body: Record<string, unknown> = {}): Promise<QClawApiResponse> {
const url = `${this.env.jprxGateway}${path}`;
const payload = { ...body, web_version: "1.4.0", web_env: "release" };
const res = await fetch(url, {
method: "POST",
headers: this.headers(),
body: JSON.stringify(payload),
signal: AbortSignal.timeout(30_000),
});
// Token 续期
const newToken = res.headers.get("X-New-Token");
if (newToken) this.jwtToken = newToken;
const data = (await res.json()) as Record<string, unknown>;
const ret = data.ret;
const commonCode =
nested(data, "data", "resp", "common", "code") ??
nested(data, "data", "common", "code") ??
nested(data, "resp", "common", "code") ??
nested(data, "common", "code");
// Token 过期
if (commonCode === 21004) {
throw new TokenExpiredError();
}
if (ret === 0 || commonCode === 0) {
const respData =
nested(data, "data", "resp", "data") ??
nested(data, "data", "data") ??
data.data ??
data;
return { success: true, data: respData as Record<string, unknown> };
}
const message =
(nested(data, "data", "common", "message") as string) ??
(nested(data, "resp", "common", "message") as string) ??
(nested(data, "common", "message") as string) ??
"请求失败";
return { success: false, message, data: data as Record<string, unknown> };
}
// ---------- 业务 API ----------
/** 获取微信登录 stateOAuth2 安全校验) */
async getWxLoginState(): Promise<QClawApiResponse> {
return this.post("data/4050/forward", { guid: this.guid });
}
/** 用微信授权 code 换取 token */
async wxLogin(code: string, state: string): Promise<QClawApiResponse> {
return this.post("data/4026/forward", { guid: this.guid, code, state });
}
/** 创建模型 API Key */
async createApiKey(): Promise<QClawApiResponse> {
return this.post("data/4055/forward", {});
}
/** 获取用户信息 */
async getUserInfo(): Promise<QClawApiResponse> {
return this.post("data/4027/forward", {});
}
/** 检查邀请码验证状态 */
async checkInviteCode(userId: string): Promise<QClawApiResponse> {
return this.post("data/4056/forward", { user_id: userId });
}
/** 提交邀请码 */
async submitInviteCode(userId: string, code: string): Promise<QClawApiResponse> {
return this.post("data/4057/forward", { user_id: userId, code });
}
/** 刷新渠道 token */
async refreshChannelToken(): Promise<string | null> {
const result = await this.post("data/4058/forward", {});
if (result.success) {
return (result.data as Record<string, unknown>)?.openclaw_channel_token as string ?? null;
}
return null;
}
}