@henryxiaoyang/wechat-access-unqclawed 1.0.7 → 1.0.9
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.
- package/README.md +12 -21
- package/index.ts +57 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,13 +16,19 @@ openclaw config set channels.wechat-access-unqclawed.enabled true
|
|
|
16
16
|
|
|
17
17
|
## 首次登录
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
```bash
|
|
20
|
+
openclaw channels login --channel wechat-access-unqclawed
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
终端会显示微信二维码(或浏览器链接),用微信扫码并确认后,浏览器会跳转到新页面。
|
|
24
|
+
|
|
25
|
+
在**另一个终端窗口**中,将浏览器地址栏的完整 URL 或 `code` 参数值写入临时文件:
|
|
20
26
|
|
|
21
27
|
```bash
|
|
22
|
-
openclaw
|
|
28
|
+
echo "粘贴的URL或code" > ~/.openclaw/wechat-auth-code.tmp
|
|
23
29
|
```
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
原窗口会自动检测并完成登录,token 自动保存。然后重启 Gateway:
|
|
26
32
|
|
|
27
33
|
```bash
|
|
28
34
|
openclaw gateway restart
|
|
@@ -36,22 +42,6 @@ openclaw gateway restart
|
|
|
36
42
|
- 邀请码验证(可配置跳过)
|
|
37
43
|
- 支持生产/测试环境切换
|
|
38
44
|
|
|
39
|
-
## 命令
|
|
40
|
-
|
|
41
|
-
### CLI 命令(终端)
|
|
42
|
-
|
|
43
|
-
| 命令 | 说明 |
|
|
44
|
-
|------|------|
|
|
45
|
-
| `openclaw wechat login` | 交互式微信扫码登录 |
|
|
46
|
-
| `openclaw wechat logout` | 清除已保存的登录态 |
|
|
47
|
-
|
|
48
|
-
### 聊天命令(渠道内)
|
|
49
|
-
|
|
50
|
-
| 命令 | 说明 |
|
|
51
|
-
|------|------|
|
|
52
|
-
| `/wechat-login` | 触发扫码登录(QR 码输出到 Gateway 日志) |
|
|
53
|
-
| `/wechat-logout` | 清除已保存的登录态 |
|
|
54
|
-
|
|
55
45
|
## 配置
|
|
56
46
|
|
|
57
47
|
在 OpenClaw 配置文件的 `channels.wechat-access-unqclawed` 下:
|
|
@@ -83,7 +73,7 @@ openclaw gateway restart
|
|
|
83
73
|
|
|
84
74
|
1. 读取配置中的 `token` — 如果有,直接使用
|
|
85
75
|
2. 读取本地保存的登录态(`~/.openclaw/wechat-access-auth.json`)
|
|
86
|
-
3. 以上都没有 — 运行 `openclaw
|
|
76
|
+
3. 以上都没有 — 运行 `openclaw channels login --channel wechat-access-unqclawed` 手动登录
|
|
87
77
|
|
|
88
78
|
## 项目结构
|
|
89
79
|
|
|
@@ -95,7 +85,8 @@ auth/
|
|
|
95
85
|
device-guid.ts # 设备 GUID 生成(随机,持久化)
|
|
96
86
|
qclaw-api.ts # QClaw JPRX 网关 API 客户端
|
|
97
87
|
state-store.ts # Token 持久化
|
|
98
|
-
wechat-login.ts #
|
|
88
|
+
wechat-login.ts # 扫码登录流程编排(交互式)
|
|
89
|
+
wechat-qr-poll.ts # QR 码生成与轮询
|
|
99
90
|
websocket/
|
|
100
91
|
types.ts # AGP 协议类型
|
|
101
92
|
websocket-client.ts # WebSocket 客户端(连接、心跳、重连)
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
2
2
|
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
|
-
import { setWecomRuntime } from "./common/runtime.js";
|
|
5
|
+
import { setWecomRuntime, getWecomRuntime } from "./common/runtime.js";
|
|
6
6
|
import { performLogin, loadState, clearState, saveState, getDeviceGuid, getEnvironment, QClawAPI, 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";
|
|
@@ -181,34 +181,46 @@ const tencentAccessPlugin = {
|
|
|
181
181
|
const channelToken = (loginData.openclaw_channel_token as string) || "";
|
|
182
182
|
const userInfo = (loginData.user_info as Record<string, unknown>) || {};
|
|
183
183
|
|
|
184
|
-
//
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
channelToken,
|
|
188
|
-
apiKey: "",
|
|
189
|
-
guid,
|
|
190
|
-
userInfo,
|
|
191
|
-
savedAt: Date.now(),
|
|
192
|
-
};
|
|
193
|
-
saveState(persistedState, authStatePath);
|
|
184
|
+
// 更新 loginKey(服务端可能返回新值,后续 API 调用需要)
|
|
185
|
+
const loginKey = userInfo.loginKey as string | undefined;
|
|
186
|
+
if (loginKey) api.loginKey = loginKey;
|
|
194
187
|
|
|
195
188
|
// 创建 API Key(非致命)
|
|
196
189
|
api.jwtToken = jwtToken;
|
|
197
190
|
api.userId = String(userInfo.user_id ?? "");
|
|
191
|
+
let apiKey = "";
|
|
198
192
|
try {
|
|
199
193
|
const keyResult = await api.createApiKey();
|
|
200
194
|
if (keyResult.success) {
|
|
201
|
-
|
|
195
|
+
apiKey =
|
|
202
196
|
(nested(keyResult.data, "key") as string) ??
|
|
203
197
|
(nested(keyResult.data, "resp", "data", "key") as string) ??
|
|
204
198
|
"";
|
|
205
|
-
if (apiKey) {
|
|
206
|
-
persistedState.apiKey = apiKey;
|
|
207
|
-
saveState(persistedState, authStatePath);
|
|
208
|
-
}
|
|
209
199
|
}
|
|
210
200
|
} catch { /* non-fatal */ }
|
|
211
201
|
|
|
202
|
+
// 写入 openclaw.json(统一存储)
|
|
203
|
+
try {
|
|
204
|
+
const fullCfg = runtime.config?.loadConfig?.() ?? cfg;
|
|
205
|
+
const channels = { ...(fullCfg.channels ?? {}) } as Record<string, any>;
|
|
206
|
+
channels["wechat-access-unqclawed"] = {
|
|
207
|
+
...(channels["wechat-access-unqclawed"] ?? {}),
|
|
208
|
+
token: channelToken,
|
|
209
|
+
};
|
|
210
|
+
const nextCfg: Record<string, unknown> = { ...fullCfg, channels };
|
|
211
|
+
if (apiKey) {
|
|
212
|
+
const models = { ...(fullCfg.models ?? {}) } as Record<string, any>;
|
|
213
|
+
const providers = { ...(models.providers ?? {}) } as Record<string, any>;
|
|
214
|
+
providers.qclaw = { ...(providers.qclaw ?? {}), apiKey };
|
|
215
|
+
models.providers = providers;
|
|
216
|
+
nextCfg.models = models;
|
|
217
|
+
}
|
|
218
|
+
await runtime.config.writeConfigFile(nextCfg);
|
|
219
|
+
} catch { /* non-fatal: fallback to state file */ }
|
|
220
|
+
|
|
221
|
+
// 备份到独立文件(兜底)
|
|
222
|
+
saveState({ jwtToken, channelToken, apiKey, guid, userInfo, savedAt: Date.now() }, authStatePath);
|
|
223
|
+
|
|
212
224
|
const nickname = (userInfo.nickname as string) ?? "用户";
|
|
213
225
|
runtime.log(`[wechat-access] 登录成功! 欢迎 ${nickname},token 已保存。请重启 Gateway 生效。`);
|
|
214
226
|
return;
|
|
@@ -272,7 +284,7 @@ const tencentAccessPlugin = {
|
|
|
272
284
|
token = savedState.channelToken;
|
|
273
285
|
log?.info(`[wechat-access] 使用已保存的 token: ${token.substring(0, 6)}...`);
|
|
274
286
|
} else {
|
|
275
|
-
log?.warn(`[wechat-access] 未找到 token
|
|
287
|
+
log?.warn(`[wechat-access] 未找到 token,请运行 "openclaw channels login --channel wechat-access-unqclawed" 完成扫码登录,然后重启 Gateway`);
|
|
276
288
|
return;
|
|
277
289
|
}
|
|
278
290
|
}
|
|
@@ -426,34 +438,47 @@ const tencentAccessPlugin = {
|
|
|
426
438
|
const channelToken = (loginData.openclaw_channel_token as string) || "";
|
|
427
439
|
const userInfo = (loginData.user_info as Record<string, unknown>) || {};
|
|
428
440
|
|
|
429
|
-
//
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
channelToken,
|
|
433
|
-
apiKey: "",
|
|
434
|
-
guid,
|
|
435
|
-
userInfo,
|
|
436
|
-
savedAt: Date.now(),
|
|
437
|
-
};
|
|
438
|
-
saveState(persistedState, authStatePath);
|
|
441
|
+
// 更新 loginKey(服务端可能返回新值,后续 API 调用需要)
|
|
442
|
+
const loginKey = userInfo.loginKey as string | undefined;
|
|
443
|
+
if (loginKey) api.loginKey = loginKey;
|
|
439
444
|
|
|
440
445
|
// 创建 API Key(非致命)
|
|
441
446
|
api.jwtToken = jwtToken;
|
|
442
447
|
api.userId = String(userInfo.user_id ?? "");
|
|
448
|
+
let apiKey = "";
|
|
443
449
|
try {
|
|
444
450
|
const keyResult = await api.createApiKey();
|
|
445
451
|
if (keyResult.success) {
|
|
446
|
-
|
|
452
|
+
apiKey =
|
|
447
453
|
(nested(keyResult.data, "key") as string) ??
|
|
448
454
|
(nested(keyResult.data, "resp", "data", "key") as string) ??
|
|
449
455
|
"";
|
|
450
|
-
if (apiKey) {
|
|
451
|
-
persistedState.apiKey = apiKey;
|
|
452
|
-
saveState(persistedState, authStatePath);
|
|
453
|
-
}
|
|
454
456
|
}
|
|
455
457
|
} catch { /* non-fatal */ }
|
|
456
458
|
|
|
459
|
+
// 写入 openclaw.json(统一存储)
|
|
460
|
+
try {
|
|
461
|
+
const wRuntime = getWecomRuntime();
|
|
462
|
+
const fullCfg = wRuntime.config.loadConfig();
|
|
463
|
+
const channels = { ...(fullCfg.channels ?? {}) } as Record<string, any>;
|
|
464
|
+
channels["wechat-access-unqclawed"] = {
|
|
465
|
+
...(channels["wechat-access-unqclawed"] ?? {}),
|
|
466
|
+
token: channelToken,
|
|
467
|
+
};
|
|
468
|
+
const nextCfg: Record<string, unknown> = { ...fullCfg, channels };
|
|
469
|
+
if (apiKey) {
|
|
470
|
+
const models = { ...(fullCfg.models ?? {}) } as Record<string, any>;
|
|
471
|
+
const providers = { ...(models.providers ?? {}) } as Record<string, any>;
|
|
472
|
+
providers.qclaw = { ...(providers.qclaw ?? {}), apiKey };
|
|
473
|
+
models.providers = providers;
|
|
474
|
+
nextCfg.models = models;
|
|
475
|
+
}
|
|
476
|
+
await wRuntime.config.writeConfigFile(nextCfg);
|
|
477
|
+
} catch { /* non-fatal */ }
|
|
478
|
+
|
|
479
|
+
// 备份到独立文件(兜底)
|
|
480
|
+
saveState({ jwtToken, channelToken, apiKey, guid, userInfo, savedAt: Date.now() }, authStatePath);
|
|
481
|
+
|
|
457
482
|
pendingQrLogin = null;
|
|
458
483
|
const nickname = (userInfo.nickname as string) ?? "用户";
|
|
459
484
|
return { connected: true, message: `登录成功! 欢迎 ${nickname},请重启 Gateway 生效。` };
|