@henryxiaoyang/wechat-access-unqclawed 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.ts +138 -0
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -81,6 +81,144 @@ const tencentAccessPlugin = {
81
81
  },
82
82
  },
83
83
 
84
+ // 认证适配器:openclaw channels login --channel wechat-access-unqclawed
85
+ auth: {
86
+ login: async ({ cfg, accountId, runtime }: { cfg: any; accountId?: string; runtime: any; verbose?: boolean; channelInput?: string }) => {
87
+ const channelCfg = cfg?.channels?.["wechat-access-unqclawed"];
88
+ const envName = channelCfg?.environment ? String(channelCfg.environment) : "production";
89
+ const authStatePath = channelCfg?.authStatePath ? String(channelCfg.authStatePath) : undefined;
90
+
91
+ const env = getEnvironment(envName);
92
+ const guid = getDeviceGuid();
93
+
94
+ // 1. 获取 OAuth state
95
+ runtime.log("[wechat-access] 获取登录 state...");
96
+ const api = new QClawAPI(env, guid);
97
+ const stateResult = await api.getWxLoginState();
98
+ let state = String(Math.floor(Math.random() * 10000));
99
+ if (stateResult.success) {
100
+ const s = nested(stateResult.data, "state") as string | undefined;
101
+ if (s) state = s;
102
+ }
103
+
104
+ // 2. 构造 auth URL
105
+ runtime.log("[wechat-access] 生成微信登录二维码...");
106
+ const authUrl = buildAuthUrl(state, env);
107
+
108
+ // 3. 终端显示 QR 码
109
+ try {
110
+ const qrterm = await import("qrcode-terminal");
111
+ const generate = qrterm.default?.generate ?? qrterm.generate;
112
+ generate(authUrl, { small: true }, (qrcode: string) => {
113
+ runtime.log("\n" + qrcode);
114
+ });
115
+ } catch {
116
+ runtime.log("(qrcode-terminal 不可用)");
117
+ }
118
+ runtime.log(`\n或在浏览器打开: ${authUrl}\n`);
119
+
120
+ // 4. 用临时文件接收 code:用户扫码授权后浏览器跳转,把地址栏 URL 或 code 写到临时文件
121
+ const { join } = await import("node:path");
122
+ const { homedir } = await import("node:os");
123
+ const { readFileSync, unlinkSync, existsSync } = await import("node:fs");
124
+ const codeTmpFile = join(homedir(), ".openclaw", "wechat-auth-code.tmp");
125
+
126
+ // 清理上次残留
127
+ try { unlinkSync(codeTmpFile); } catch { /* ignore */ }
128
+
129
+ runtime.log("=".repeat(60));
130
+ runtime.log(" 扫码并在手机上确认后,浏览器会跳转到新页面。");
131
+ runtime.log(" 请复制地址栏的完整 URL 或其中的 code 参数值,");
132
+ runtime.log(" 然后在另一个终端窗口执行:");
133
+ runtime.log("");
134
+ runtime.log(` echo "粘贴的URL或code" > ${codeTmpFile}`);
135
+ runtime.log("");
136
+ runtime.log(" 本窗口会自动检测并完成登录。");
137
+ runtime.log("=".repeat(60));
138
+
139
+ // 5. 轮询临时文件
140
+ const deadline = Date.now() + 300_000; // 5 分钟超时
141
+ while (Date.now() < deadline) {
142
+ await new Promise((r) => setTimeout(r, 1500));
143
+
144
+ if (!existsSync(codeTmpFile)) continue;
145
+
146
+ let raw = "";
147
+ try {
148
+ raw = readFileSync(codeTmpFile, "utf-8").trim();
149
+ unlinkSync(codeTmpFile); // 读完即删
150
+ } catch { continue; }
151
+
152
+ if (!raw) continue;
153
+
154
+ // 从 URL 或裸 code 中提取 code
155
+ let code = raw;
156
+ if (raw.includes("code=")) {
157
+ try {
158
+ const url = new URL(raw);
159
+ const c = url.searchParams.get("code");
160
+ if (c) code = c;
161
+ } catch {
162
+ const match = raw.match(/[?&#]code=([^&#]+)/);
163
+ if (match?.[1]) code = match[1];
164
+ }
165
+ }
166
+
167
+ if (!code) {
168
+ runtime.log("[wechat-access] 未能从输入中提取 code,请重试");
169
+ continue;
170
+ }
171
+
172
+ // 6. 用 code 换 token
173
+ runtime.log(`[wechat-access] 收到 code: ${code.substring(0, 10)}...,正在获取 token...`);
174
+ const loginResult = await api.wxLogin(code, state);
175
+ if (!loginResult.success) {
176
+ throw new Error(`登录失败: ${loginResult.message ?? "未知错误"}`);
177
+ }
178
+
179
+ const loginData = loginResult.data as Record<string, unknown>;
180
+ const jwtToken = (loginData.token as string) || "";
181
+ const channelToken = (loginData.openclaw_channel_token as string) || "";
182
+ const userInfo = (loginData.user_info as Record<string, unknown>) || {};
183
+
184
+ // 保存登录态
185
+ const persistedState: PersistedAuthState = {
186
+ jwtToken,
187
+ channelToken,
188
+ apiKey: "",
189
+ guid,
190
+ userInfo,
191
+ savedAt: Date.now(),
192
+ };
193
+ saveState(persistedState, authStatePath);
194
+
195
+ // 创建 API Key(非致命)
196
+ api.jwtToken = jwtToken;
197
+ api.userId = String(userInfo.user_id ?? "");
198
+ try {
199
+ const keyResult = await api.createApiKey();
200
+ if (keyResult.success) {
201
+ const apiKey =
202
+ (nested(keyResult.data, "key") as string) ??
203
+ (nested(keyResult.data, "resp", "data", "key") as string) ??
204
+ "";
205
+ if (apiKey) {
206
+ persistedState.apiKey = apiKey;
207
+ saveState(persistedState, authStatePath);
208
+ }
209
+ }
210
+ } catch { /* non-fatal */ }
211
+
212
+ const nickname = (userInfo.nickname as string) ?? "用户";
213
+ runtime.log(`[wechat-access] 登录成功! 欢迎 ${nickname},token 已保存。请重启 Gateway 生效。`);
214
+ return;
215
+ }
216
+ // 超时清理
217
+ try { unlinkSync(codeTmpFile); } catch { /* ignore */ }
218
+ throw new Error("登录超时(5 分钟),请重试");
219
+ },
220
+ },
221
+
84
222
  // 出站适配器(必需)
85
223
  outbound: {
86
224
  deliveryMode: "direct" as const,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryxiaoyang/wechat-access-unqclawed",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "description": "OpenClaw 微信通路插件 — 扫码登录 + AGP WebSocket 双向通信",
6
6
  "author": "HenryXiaoYang",