@henryxiaoyang/wechat-access-unqclawed 1.0.10 → 1.0.12

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 (3) hide show
  1. package/auth/qclaw-api.ts +20 -0
  2. package/index.ts +115 -12
  3. package/package.json +1 -1
package/auth/qclaw-api.ts CHANGED
@@ -126,4 +126,24 @@ export class QClawAPI {
126
126
  }
127
127
  return null;
128
128
  }
129
+
130
+ /** 生成企微客服专属链接 (cmd_id=4018) */
131
+ async generateContactLink(openKfId: string): Promise<QClawApiResponse> {
132
+ return this.post("data/4018/forward", {
133
+ guid: this.guid,
134
+ user_id: this.userId,
135
+ open_id: openKfId,
136
+ contact_type: "open_kfid",
137
+ });
138
+ }
139
+
140
+ /** 查询设备绑定状态 (cmd_id=4019) */
141
+ async queryDeviceByGuid(): Promise<QClawApiResponse> {
142
+ return this.post("data/4019/forward", { guid: this.guid });
143
+ }
144
+
145
+ /** 断开设备绑定 (cmd_id=4020) */
146
+ async disconnectDevice(): Promise<QClawApiResponse> {
147
+ return this.post("data/4020/forward", { guid: this.guid });
148
+ }
129
149
  }
package/index.ts CHANGED
@@ -3,7 +3,7 @@ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
3
  import { WechatAccessWebSocketClient, handlePrompt, handleCancel } from "./websocket/index.js";
4
4
  // import { handleSimpleWecomWebhook } from "./http/webhook.js";
5
5
  import { setWecomRuntime, getWecomRuntime } from "./common/runtime.js";
6
- import { performLogin, loadState, clearState, saveState, getDeviceGuid, getEnvironment, QClawAPI, buildAuthUrl, fetchQrUuid, fetchQrImageDataUrl, pollQrStatus } from "./auth/index.js";
6
+ import { performLogin, loadState, clearState, saveState, getDeviceGuid, getEnvironment, QClawAPI, TokenExpiredError, buildAuthUrl, fetchQrUuid, fetchQrImageDataUrl, pollQrStatus } from "./auth/index.js";
7
7
  import type { QClawEnvironment, PersistedAuthState } from "./auth/index.js";
8
8
  import { nested } from "./auth/utils.js";
9
9
 
@@ -199,6 +199,65 @@ const tencentAccessPlugin = {
199
199
  }
200
200
  } catch { /* non-fatal */ }
201
201
 
202
+ // 7. 生成企微客服链接,等待设备绑定
203
+ const OPEN_KFID = "wkzLlJLAAAfbxEV3ZcS-lHZxkaKmpejQ";
204
+ runtime.log("\n[wechat-access] 生成企微客服链接...");
205
+ const linkResult = await api.generateContactLink(OPEN_KFID);
206
+ let deviceBound = false;
207
+
208
+ if (!linkResult.success) {
209
+ runtime.log(`[wechat-access] 生成链接失败: ${linkResult.message ?? "未知错误"}(跳过设备绑定)`);
210
+ } else {
211
+ const linkData = linkResult.data as Record<string, unknown>;
212
+ const contactUrl =
213
+ (nested(linkData, "url") as string) ||
214
+ (nested(linkData, "data", "url") as string) ||
215
+ "";
216
+
217
+ if (!contactUrl) {
218
+ runtime.log("[wechat-access] 返回数据中没有 URL(跳过设备绑定)");
219
+ } else {
220
+ runtime.log("=".repeat(60));
221
+ runtime.log(" 用微信扫描下方二维码,进入客服对话完成设备绑定");
222
+ runtime.log("=".repeat(60));
223
+
224
+ try {
225
+ const qrterm = await import("qrcode-terminal");
226
+ const generate = qrterm.default?.generate ?? qrterm.generate;
227
+ generate(contactUrl, { small: true }, (qrcode: string) => {
228
+ runtime.log("\n" + qrcode);
229
+ });
230
+ } catch {
231
+ runtime.log("(qrcode-terminal 不可用)");
232
+ }
233
+ runtime.log(`\n或手动打开: ${contactUrl}\n`);
234
+
235
+ // 轮询等待绑定
236
+ runtime.log("[wechat-access] 等待微信扫码绑定 (超时 5 分钟)...");
237
+ const bindDeadline = Date.now() + 300_000;
238
+ while (Date.now() < bindDeadline) {
239
+ await new Promise((r) => setTimeout(r, 2000));
240
+ try {
241
+ const status = await api.queryDeviceByGuid();
242
+ if (status.success) {
243
+ const sd = status.data as Record<string, unknown>;
244
+ const nick =
245
+ (nested(sd, "nickname") as string) ||
246
+ (nested(sd, "data", "nickname") as string);
247
+ if (nick) {
248
+ runtime.log(`[wechat-access] 设备绑定成功! 微信昵称: ${nick}`);
249
+ deviceBound = true;
250
+ break;
251
+ }
252
+ }
253
+ } catch { /* continue polling */ }
254
+ }
255
+ if (!deviceBound) {
256
+ runtime.log("[wechat-access] 设备绑定超时,可稍后重新执行 channels login");
257
+ }
258
+ }
259
+ }
260
+
202
261
  // 写入 openclaw.json(统一存储)
203
262
  try {
204
263
  const wRuntime = getWecomRuntime();
@@ -216,7 +275,7 @@ const tencentAccessPlugin = {
216
275
  models.providers = providers;
217
276
  nextCfg.models = models;
218
277
  }
219
- await runtime.config.writeConfigFile(nextCfg);
278
+ await wRuntime.config.writeConfigFile(nextCfg);
220
279
  } catch { /* non-fatal: fallback to state file */ }
221
280
 
222
281
  // 备份到独立文件(兜底)
@@ -279,22 +338,66 @@ const tencentAccessPlugin = {
279
338
  });
280
339
 
281
340
  // Token 获取策略:配置 > 已保存的登录态 > 提示用户手动登录
282
- if (!token) {
283
- const savedState = loadState(authStatePath);
284
- if (savedState?.channelToken) {
285
- token = savedState.channelToken;
286
- log?.info(`[wechat-access] 使用已保存的 token: ${token.substring(0, 6)}...`);
287
- } else {
288
- log?.warn(`[wechat-access] 未找到 token,请运行 "openclaw channels login --channel wechat-access-unqclawed" 完成扫码登录,然后重启 Gateway`);
289
- return;
290
- }
341
+ const savedState = loadState(authStatePath);
342
+ if (!token && savedState?.channelToken) {
343
+ token = savedState.channelToken;
344
+ log?.info(`[wechat-access] 使用已保存的 token: ${token.substring(0, 6)}...`);
291
345
  }
292
346
 
293
347
  if (!token) {
294
- log?.warn(`[wechat-access] token 为空,跳过 WebSocket 连接`);
348
+ log?.warn(`[wechat-access] 未找到 token,请运行 "openclaw channels login --channel wechat-access-unqclawed" 完成扫码登录,然后重启 Gateway`);
295
349
  return;
296
350
  }
297
351
 
352
+ // Token 刷新:用 jwt_token 调 4058 获取最新 channel_token(QClaw 客户端每次打开都会刷新)
353
+ const jwtToken = savedState?.jwtToken || "";
354
+ if (jwtToken) {
355
+ const api = new QClawAPI(env, guid, jwtToken);
356
+ api.userId = String((savedState?.userInfo as Record<string, unknown>)?.user_id ?? "");
357
+ const savedLoginKey = (savedState?.userInfo as Record<string, unknown>)?.loginKey as string | undefined;
358
+ if (savedLoginKey) api.loginKey = savedLoginKey;
359
+
360
+ let refreshed = false;
361
+ for (let attempt = 0; attempt < 3; attempt++) {
362
+ try {
363
+ const newToken = await api.refreshChannelToken();
364
+ if (newToken) {
365
+ token = newToken;
366
+ log?.info(`[wechat-access] channel_token 已刷新: ${token.substring(0, 6)}...`);
367
+ // 更新保存的状态和配置
368
+ if (savedState) {
369
+ savedState.channelToken = newToken;
370
+ savedState.savedAt = Date.now();
371
+ saveState(savedState, authStatePath);
372
+ }
373
+ try {
374
+ const wRuntime = getWecomRuntime();
375
+ const fullCfg = wRuntime.config.loadConfig();
376
+ const channels = { ...(fullCfg.channels ?? {}) } as Record<string, any>;
377
+ channels["wechat-access-unqclawed"] = {
378
+ ...(channels["wechat-access-unqclawed"] ?? {}),
379
+ token: newToken,
380
+ };
381
+ await wRuntime.config.writeConfigFile({ ...fullCfg, channels });
382
+ } catch { /* non-fatal */ }
383
+ refreshed = true;
384
+ break;
385
+ }
386
+ } catch (e) {
387
+ if (e instanceof TokenExpiredError) {
388
+ clearState(authStatePath);
389
+ log?.warn(`[wechat-access] jwt_token 已过期,请重新运行 "openclaw channels login --channel wechat-access-unqclawed"`);
390
+ return;
391
+ }
392
+ log?.warn(`[wechat-access] token 刷新失败 (${attempt + 1}/3): ${e instanceof Error ? e.message : String(e)}`);
393
+ }
394
+ if (attempt < 2) await new Promise(r => setTimeout(r, 1500));
395
+ }
396
+ if (!refreshed) {
397
+ log?.info(`[wechat-access] token 刷新失败,使用旧 token 尝试连接`);
398
+ }
399
+ }
400
+
298
401
  const wsConfig = {
299
402
  url: wsUrl,
300
403
  token,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryxiaoyang/wechat-access-unqclawed",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "type": "module",
5
5
  "description": "OpenClaw 微信通路插件 — 扫码登录 + AGP WebSocket 双向通信",
6
6
  "author": "HenryXiaoYang",