@wu529778790/open-im 1.0.3 → 1.1.0
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 +59 -3
- package/dist/commands/handler.d.ts +1 -1
- package/dist/config.d.ts +19 -1
- package/dist/config.js +69 -4
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/index.js +59 -12
- package/dist/setup.js +140 -29
- package/dist/shared/active-chats.d.ts +2 -2
- package/dist/wechat/auth/device-bind.d.ts +13 -0
- package/dist/wechat/auth/device-bind.js +76 -0
- package/dist/wechat/auth/device-guid.d.ts +5 -0
- package/dist/wechat/auth/device-guid.js +28 -0
- package/dist/wechat/auth/environments.d.ts +5 -0
- package/dist/wechat/auth/environments.js +21 -0
- package/dist/wechat/auth/index.d.ts +7 -0
- package/dist/wechat/auth/index.js +5 -0
- package/dist/wechat/auth/qclaw-api.d.ts +26 -0
- package/dist/wechat/auth/qclaw-api.js +100 -0
- package/dist/wechat/auth/types.d.ts +18 -0
- package/dist/wechat/auth/types.js +4 -0
- package/dist/wechat/auth/wechat-login.d.ts +17 -0
- package/dist/wechat/auth/wechat-login.js +172 -0
- package/dist/wechat/client.js +11 -2
- package/dist/wework/client.d.ts +46 -0
- package/dist/wework/client.js +356 -0
- package/dist/wework/event-handler.d.ts +12 -0
- package/dist/wework/event-handler.js +245 -0
- package/dist/wework/message-sender.d.ts +57 -0
- package/dist/wework/message-sender.js +258 -0
- package/dist/wework/types.d.ts +156 -0
- package/dist/wework/types.js +6 -0
- package/package.json +4 -1
package/dist/setup.js
CHANGED
|
@@ -26,6 +26,7 @@ function getConfiguredPlatforms(existing) {
|
|
|
26
26
|
{ k: "telegram", label: "Telegram" },
|
|
27
27
|
{ k: "feishu", label: "飞书" },
|
|
28
28
|
{ k: "wechat", label: "微信" },
|
|
29
|
+
{ k: "wework", label: "企业微信" },
|
|
29
30
|
];
|
|
30
31
|
return names
|
|
31
32
|
.filter(({ k }) => {
|
|
@@ -36,8 +37,11 @@ function getConfiguredPlatforms(existing) {
|
|
|
36
37
|
return !!p.botToken;
|
|
37
38
|
if (k === "feishu")
|
|
38
39
|
return !!(p.appId && p.appSecret);
|
|
40
|
+
// 微信支持 AGP 协议(token + guid + userId)或标准协议(appId + appSecret)
|
|
39
41
|
if (k === "wechat")
|
|
40
|
-
return !!(p.appId && p.appSecret);
|
|
42
|
+
return !!(p.token && p.guid && p.userId) || !!(p.appId && p.appSecret);
|
|
43
|
+
if (k === "wework")
|
|
44
|
+
return !!(p.corpId && p.secret);
|
|
41
45
|
return false;
|
|
42
46
|
})
|
|
43
47
|
.map(({ label }) => label);
|
|
@@ -78,6 +82,13 @@ function printManualInstructions(configPath) {
|
|
|
78
82
|
"appSecret": "你的微信 App Secret(可选)",
|
|
79
83
|
"wsUrl": "AGP WebSocket URL(可选,默认使用官方服务)",
|
|
80
84
|
"allowedUserIds": ["允许访问的微信用户 ID(可选)"]
|
|
85
|
+
},
|
|
86
|
+
"wework": {
|
|
87
|
+
"enabled": false,
|
|
88
|
+
"corpId": "你的企业微信 Corp ID(可选)",
|
|
89
|
+
"agentId": "你的企业微信 Agent ID(可选)",
|
|
90
|
+
"secret": "你的企业微信 Secret(可选)",
|
|
91
|
+
"allowedUserIds": ["允许访问的企业微信用户 ID(可选)"]
|
|
81
92
|
}
|
|
82
93
|
},
|
|
83
94
|
"claudeWorkDir": "${process.cwd().replace(/\\/g, "/")}",
|
|
@@ -85,8 +96,8 @@ function printManualInstructions(configPath) {
|
|
|
85
96
|
"aiCommand": "claude"
|
|
86
97
|
}`);
|
|
87
98
|
console.log("");
|
|
88
|
-
console.log("提示:至少需要配置 Telegram、Feishu 或
|
|
89
|
-
console.log("或设置环境变量: TELEGRAM_BOT_TOKEN=xxx、FEISHU_APP_ID=xxx 或
|
|
99
|
+
console.log("提示:至少需要配置 Telegram、Feishu、WeChat 或 WeWork 其中一个平台");
|
|
100
|
+
console.log("或设置环境变量: TELEGRAM_BOT_TOKEN=xxx、FEISHU_APP_ID=xxx、WECHAT_APP_ID=xxx 或 WEWORK_CORP_ID=xxx 后再运行");
|
|
90
101
|
console.log("");
|
|
91
102
|
}
|
|
92
103
|
export async function runInteractiveSetup() {
|
|
@@ -111,7 +122,9 @@ export async function runInteractiveSetup() {
|
|
|
111
122
|
};
|
|
112
123
|
const hasTg = !!existing?.platforms?.telegram?.botToken;
|
|
113
124
|
const hasFs = !!(existing?.platforms?.feishu?.appId && existing?.platforms?.feishu?.appSecret);
|
|
114
|
-
const
|
|
125
|
+
const wc = existing?.platforms?.wechat;
|
|
126
|
+
const hasWc = !!(wc?.token && wc?.guid && wc?.userId) || !!(wc?.appId && wc?.appSecret);
|
|
127
|
+
const hasWw = !!(existing?.platforms?.wework?.corpId && existing?.platforms?.wework?.secret);
|
|
115
128
|
// 第一步:选择平台(在选项和提示中显示已配置项)
|
|
116
129
|
const configuredHint = configured.length > 0 ? `(当前已配置: ${configured.join("、")})` : "";
|
|
117
130
|
const platformResp = await prompts({
|
|
@@ -129,10 +142,15 @@ export async function runInteractiveSetup() {
|
|
|
129
142
|
value: "feishu",
|
|
130
143
|
},
|
|
131
144
|
{
|
|
132
|
-
title: "微信 (WeChat) -
|
|
145
|
+
title: "微信 (WeChat) - 扫码登录获取 token(QClaw/AGP 协议)" +
|
|
133
146
|
(hasWc ? " ✓已配置" : ""),
|
|
134
147
|
value: "wechat",
|
|
135
148
|
},
|
|
149
|
+
{
|
|
150
|
+
title: "企业微信 (WeCom/WeWork) - 需要 Bot ID 和 Secret" +
|
|
151
|
+
(hasWw ? " ✓已配置" : ""),
|
|
152
|
+
value: "wework",
|
|
153
|
+
},
|
|
136
154
|
{ title: "配置多个平台", value: "multi" },
|
|
137
155
|
],
|
|
138
156
|
initial: 0,
|
|
@@ -153,6 +171,7 @@ export async function runInteractiveSetup() {
|
|
|
153
171
|
{ title: "Telegram" + (hasTg ? " ✓已配置" : ""), value: "telegram", selected: hasTg },
|
|
154
172
|
{ title: "飞书 (Feishu)" + (hasFs ? " ✓已配置" : ""), value: "feishu", selected: hasFs },
|
|
155
173
|
{ title: "微信 (WeChat)" + (hasWc ? " ✓已配置" : ""), value: "wechat", selected: hasWc },
|
|
174
|
+
{ title: "企业微信 (WeWork)" + (hasWw ? " ✓已配置" : ""), value: "wework", selected: hasWw },
|
|
156
175
|
],
|
|
157
176
|
}, { onCancel });
|
|
158
177
|
if (!multiResp.platforms || multiResp.platforms.length === 0) {
|
|
@@ -211,39 +230,95 @@ export async function runInteractiveSetup() {
|
|
|
211
230
|
}
|
|
212
231
|
}
|
|
213
232
|
if (selectedPlatforms.includes("wechat")) {
|
|
214
|
-
const
|
|
215
|
-
|
|
233
|
+
const wc = existing?.platforms?.wechat;
|
|
234
|
+
const hasToken = !!(wc?.token && wc?.guid && wc?.userId);
|
|
235
|
+
const wechatModeResp = await prompts({
|
|
236
|
+
type: "select",
|
|
237
|
+
name: "mode",
|
|
238
|
+
message: "微信登录方式",
|
|
239
|
+
choices: [
|
|
240
|
+
{
|
|
241
|
+
title: "扫码登录(推荐)- 用微信扫描二维码,自动获取 token",
|
|
242
|
+
value: "qr",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
title: "使用已有配置" + (hasToken ? " ✓" : "(需已通过扫码登录获取)"),
|
|
246
|
+
value: "keep",
|
|
247
|
+
disabled: !hasToken,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
initial: hasToken ? 1 : 0,
|
|
251
|
+
}, { onCancel });
|
|
252
|
+
if (wechatModeResp.mode === "qr") {
|
|
253
|
+
console.log("\n正在启动微信扫码登录...\n");
|
|
254
|
+
const appIdResp = await prompts({
|
|
216
255
|
type: "text",
|
|
217
256
|
name: "appId",
|
|
218
|
-
message: "
|
|
219
|
-
initial:
|
|
220
|
-
validate: (v) => (v.trim() ? true : "
|
|
221
|
-
},
|
|
257
|
+
message: "请输入微信 AppID",
|
|
258
|
+
initial: wc?.appId ?? "",
|
|
259
|
+
validate: (v) => (v.trim() ? true : "AppID 不能为空"),
|
|
260
|
+
}, { onCancel });
|
|
261
|
+
try {
|
|
262
|
+
const { performWeChatLogin } = await import("./wechat/auth/index.js");
|
|
263
|
+
const credentials = await performWeChatLogin({
|
|
264
|
+
envName: "production",
|
|
265
|
+
appId: appIdResp.appId?.trim() || wc?.appId || "",
|
|
266
|
+
});
|
|
267
|
+
config.platforms.wechat = {
|
|
268
|
+
appId: appIdResp.appId?.trim() || wc?.appId,
|
|
269
|
+
enabled: true,
|
|
270
|
+
token: credentials.channelToken,
|
|
271
|
+
jwtToken: credentials.jwtToken,
|
|
272
|
+
loginKey: credentials.loginKey,
|
|
273
|
+
guid: credentials.guid,
|
|
274
|
+
userId: credentials.userId,
|
|
275
|
+
wsUrl: "wss://mmgrcalltoken.3g.qq.com/agentwss",
|
|
276
|
+
};
|
|
277
|
+
console.log("\n✅ 微信登录成功,配置已获取");
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
console.error("\n❌ 微信登录失败:", err instanceof Error ? err.message : String(err));
|
|
281
|
+
if (platform === "wechat")
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else if (hasToken) {
|
|
286
|
+
config.platforms.wechat = {
|
|
287
|
+
...wc,
|
|
288
|
+
enabled: true,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
else if (platform === "wechat") {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (selectedPlatforms.includes("wework")) {
|
|
296
|
+
const weworkResp = await prompts([
|
|
222
297
|
{
|
|
223
298
|
type: "text",
|
|
224
|
-
name: "
|
|
225
|
-
message: "
|
|
226
|
-
initial: existing?.platforms?.
|
|
227
|
-
validate: (v) => (v.trim() ? true : "
|
|
299
|
+
name: "corpId",
|
|
300
|
+
message: "企业微信 Bot ID(从企业微信管理后台获取)",
|
|
301
|
+
initial: existing?.platforms?.wework?.corpId ?? "",
|
|
302
|
+
validate: (v) => (v.trim() ? true : "Bot ID 不能为空"),
|
|
228
303
|
},
|
|
229
304
|
{
|
|
230
305
|
type: "text",
|
|
231
|
-
name: "
|
|
232
|
-
message: "
|
|
233
|
-
initial: existing?.platforms?.
|
|
306
|
+
name: "secret",
|
|
307
|
+
message: "企业微信 Secret(从企业微信管理后台获取)",
|
|
308
|
+
initial: existing?.platforms?.wework?.secret ?? "",
|
|
309
|
+
validate: (v) => (v.trim() ? true : "Secret 不能为空"),
|
|
234
310
|
},
|
|
235
311
|
], { onCancel });
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
config.platforms.
|
|
312
|
+
const wwCorpId = weworkResp.corpId?.trim() || existing?.platforms?.wework?.corpId;
|
|
313
|
+
const wwSecret = weworkResp.secret?.trim() || existing?.platforms?.wework?.secret;
|
|
314
|
+
if (wwCorpId && wwSecret) {
|
|
315
|
+
config.platforms.wework = {
|
|
240
316
|
enabled: true,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
wsUrl: wechatResp.wsUrl?.trim() || existing?.platforms?.wechat?.wsUrl || undefined,
|
|
317
|
+
corpId: wwCorpId,
|
|
318
|
+
secret: wwSecret,
|
|
244
319
|
};
|
|
245
320
|
}
|
|
246
|
-
else if (platform === "
|
|
321
|
+
else if (platform === "wework") {
|
|
247
322
|
return false;
|
|
248
323
|
}
|
|
249
324
|
}
|
|
@@ -251,6 +326,7 @@ export async function runInteractiveSetup() {
|
|
|
251
326
|
const tgIds = existing?.platforms?.telegram?.allowedUserIds?.join(", ") ?? "";
|
|
252
327
|
const fsIds = existing?.platforms?.feishu?.allowedUserIds?.join(", ") ?? "";
|
|
253
328
|
const wcIds = existing?.platforms?.wechat?.allowedUserIds?.join(", ") ?? "";
|
|
329
|
+
const wwIds = existing?.platforms?.wework?.allowedUserIds?.join(", ") ?? "";
|
|
254
330
|
const aiIdx = ["claude", "codex", "cursor"].indexOf(existing?.aiCommand ?? "claude");
|
|
255
331
|
const commonPrompts = [];
|
|
256
332
|
if (selectedPlatforms.includes("telegram")) {
|
|
@@ -277,6 +353,14 @@ export async function runInteractiveSetup() {
|
|
|
277
353
|
initial: wcIds,
|
|
278
354
|
});
|
|
279
355
|
}
|
|
356
|
+
if (selectedPlatforms.includes("wework")) {
|
|
357
|
+
commonPrompts.push({
|
|
358
|
+
type: "text",
|
|
359
|
+
name: "weworkAllowedUserIds",
|
|
360
|
+
message: "企业微信白名单用户 ID(可选,逗号分隔,留空=所有人可访问)",
|
|
361
|
+
initial: wwIds,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
280
364
|
commonPrompts.push({
|
|
281
365
|
type: "select",
|
|
282
366
|
name: "aiCommand",
|
|
@@ -310,6 +394,9 @@ export async function runInteractiveSetup() {
|
|
|
310
394
|
const wechatIds = selectedPlatforms.includes("wechat")
|
|
311
395
|
? parseIds(commonResp.wechatAllowedUserIds)
|
|
312
396
|
: parseIds(existing?.platforms?.wechat?.allowedUserIds?.join(", "));
|
|
397
|
+
const weworkIds = selectedPlatforms.includes("wework")
|
|
398
|
+
? parseIds(commonResp.weworkAllowedUserIds)
|
|
399
|
+
: parseIds(existing?.platforms?.wework?.allowedUserIds?.join(", "));
|
|
313
400
|
// 增量合并:以已有配置为底,只覆盖本次选中的平台(不写入根级旧字段 telegramBotToken 等)
|
|
314
401
|
const base = existing
|
|
315
402
|
? JSON.parse(JSON.stringify(existing))
|
|
@@ -358,12 +445,18 @@ export async function runInteractiveSetup() {
|
|
|
358
445
|
out.platforms.feishu = { enabled: false, allowedUserIds: feishuIds };
|
|
359
446
|
}
|
|
360
447
|
if (selectedPlatforms.includes("wechat")) {
|
|
448
|
+
const wcConfig = config.platforms?.wechat;
|
|
361
449
|
out.platforms.wechat = {
|
|
362
450
|
...base?.platforms?.wechat,
|
|
363
451
|
enabled: true,
|
|
364
|
-
appId:
|
|
365
|
-
appSecret:
|
|
366
|
-
|
|
452
|
+
appId: wcConfig?.appId ?? base?.platforms?.wechat?.appId,
|
|
453
|
+
appSecret: wcConfig?.appSecret ?? base?.platforms?.wechat?.appSecret,
|
|
454
|
+
token: wcConfig?.token ?? base?.platforms?.wechat?.token,
|
|
455
|
+
jwtToken: wcConfig?.jwtToken ?? base?.platforms?.wechat?.jwtToken,
|
|
456
|
+
loginKey: wcConfig?.loginKey ?? base?.platforms?.wechat?.loginKey,
|
|
457
|
+
guid: wcConfig?.guid ?? base?.platforms?.wechat?.guid,
|
|
458
|
+
userId: wcConfig?.userId ?? base?.platforms?.wechat?.userId,
|
|
459
|
+
wsUrl: wcConfig?.wsUrl ?? base?.platforms?.wechat?.wsUrl,
|
|
367
460
|
allowedUserIds: wechatIds,
|
|
368
461
|
};
|
|
369
462
|
}
|
|
@@ -376,6 +469,24 @@ export async function runInteractiveSetup() {
|
|
|
376
469
|
else {
|
|
377
470
|
out.platforms.wechat = { enabled: false, allowedUserIds: wechatIds };
|
|
378
471
|
}
|
|
472
|
+
if (selectedPlatforms.includes("wework")) {
|
|
473
|
+
out.platforms.wework = {
|
|
474
|
+
...base?.platforms?.wework,
|
|
475
|
+
enabled: true,
|
|
476
|
+
corpId: config.platforms.wework?.corpId,
|
|
477
|
+
secret: config.platforms.wework?.secret,
|
|
478
|
+
allowedUserIds: weworkIds,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
else if (base?.platforms?.wework) {
|
|
482
|
+
out.platforms.wework = {
|
|
483
|
+
...base.platforms.wework,
|
|
484
|
+
allowedUserIds: weworkIds.length > 0 ? weworkIds : base.platforms.wework.allowedUserIds,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
out.platforms.wework = { enabled: false, allowedUserIds: weworkIds };
|
|
489
|
+
}
|
|
379
490
|
const dir = dirname(configPath);
|
|
380
491
|
if (!existsSync(dir)) {
|
|
381
492
|
mkdirSync(dir, { recursive: true });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare function loadActiveChats(): void;
|
|
2
|
-
export declare function getActiveChatId(platform: 'feishu' | 'telegram' | 'wechat'): string | undefined;
|
|
3
|
-
export declare function setActiveChatId(platform: 'feishu' | 'telegram' | 'wechat', chatId: string): void;
|
|
2
|
+
export declare function getActiveChatId(platform: 'feishu' | 'telegram' | 'wechat' | 'wework'): string | undefined;
|
|
3
|
+
export declare function setActiveChatId(platform: 'feishu' | 'telegram' | 'wechat' | 'wework', chatId: string): void;
|
|
4
4
|
export declare function flushActiveChats(): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设备绑定:生成企微客服链接,用户在微信中打开后才有对话入口
|
|
3
|
+
*/
|
|
4
|
+
import type { QClawAPI } from './qclaw-api.js';
|
|
5
|
+
export interface DeviceBindResult {
|
|
6
|
+
success: boolean;
|
|
7
|
+
contactUrl?: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function performDeviceBinding(api: QClawAPI, options?: {
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
showQr?: (url: string) => void | Promise<void>;
|
|
13
|
+
}): Promise<DeviceBindResult>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设备绑定:生成企微客服链接,用户在微信中打开后才有对话入口
|
|
3
|
+
*/
|
|
4
|
+
function nested(obj, ...keys) {
|
|
5
|
+
let cur = obj;
|
|
6
|
+
for (const k of keys) {
|
|
7
|
+
if (cur == null || typeof cur !== 'object')
|
|
8
|
+
return undefined;
|
|
9
|
+
cur = cur[k];
|
|
10
|
+
}
|
|
11
|
+
return cur;
|
|
12
|
+
}
|
|
13
|
+
const DEFAULT_OPEN_KFID = 'wkzLlJLAAAfbxEV3ZcS-lHZxkaKmpejQ';
|
|
14
|
+
const POLL_INTERVAL_MS = 2000;
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 180_000; // 3 分钟
|
|
16
|
+
export async function performDeviceBinding(api, options) {
|
|
17
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
18
|
+
const showQr = options?.showQr;
|
|
19
|
+
console.log('[微信登录] 正在调用 4018 接口生成绑定链接...');
|
|
20
|
+
let linkResult;
|
|
21
|
+
try {
|
|
22
|
+
linkResult = await Promise.race([
|
|
23
|
+
api.generateContactLink(DEFAULT_OPEN_KFID),
|
|
24
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('4018 接口超时(15秒)')), 15_000)),
|
|
25
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29
|
+
return { success: false, message: `生成绑定链接失败: ${msg}` };
|
|
30
|
+
}
|
|
31
|
+
if (!linkResult.success) {
|
|
32
|
+
return { success: false, message: `生成绑定链接失败: ${linkResult.message}` };
|
|
33
|
+
}
|
|
34
|
+
const linkData = linkResult.data;
|
|
35
|
+
const bindUrl = nested(linkData, 'url') ||
|
|
36
|
+
nested(linkData, 'data', 'url') ||
|
|
37
|
+
nested(linkData, 'resp', 'url') ||
|
|
38
|
+
nested(linkData, 'resp', 'data', 'url') ||
|
|
39
|
+
'';
|
|
40
|
+
if (!bindUrl) {
|
|
41
|
+
console.warn('[微信登录] 4018 响应结构:', JSON.stringify(linkData, null, 2).slice(0, 500));
|
|
42
|
+
return { success: false, message: '生成绑定链接失败,未返回 URL。服务端响应结构可能已变更' };
|
|
43
|
+
}
|
|
44
|
+
if (showQr) {
|
|
45
|
+
await showQr(bindUrl);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log('\n' + '='.repeat(64));
|
|
49
|
+
console.log('【设备绑定】请复制下方链接,在企微/微信中打开:');
|
|
50
|
+
console.log(' → 打开后会进入客服会话,后续发消息必须在此会话中进行');
|
|
51
|
+
console.log('='.repeat(64));
|
|
52
|
+
console.log(bindUrl);
|
|
53
|
+
console.log('='.repeat(64) + '\n');
|
|
54
|
+
}
|
|
55
|
+
const deadline = Date.now() + timeoutMs;
|
|
56
|
+
while (Date.now() < deadline) {
|
|
57
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
58
|
+
const queryResult = await api.queryDeviceByGuid();
|
|
59
|
+
if (!queryResult.success)
|
|
60
|
+
continue;
|
|
61
|
+
const data = queryResult.data;
|
|
62
|
+
const inner = nested(data, 'data');
|
|
63
|
+
const isBind = nested(data, 'is_bind') ?? inner?.is_bind;
|
|
64
|
+
const nickname = (nested(data, 'nickname') ?? inner?.nickname);
|
|
65
|
+
const externalUserId = (nested(data, 'external_user_id') ?? inner?.external_user_id);
|
|
66
|
+
// 与 wechat-access 一致:nickname 或 external_user_id 表示已绑定
|
|
67
|
+
if (isBind === true || isBind === 1 || !!nickname || !!externalUserId) {
|
|
68
|
+
return { success: true, contactUrl: bindUrl, message: '设备绑定成功' };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
contactUrl: bindUrl,
|
|
74
|
+
message: '绑定超时,请稍后重新登录并完成绑定',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设备唯一标识生成
|
|
3
|
+
* 首次运行时随机生成并持久化到 ~/.open-im/wechat-guid
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { join, dirname } from 'node:path';
|
|
8
|
+
import { APP_HOME } from '../../constants.js';
|
|
9
|
+
const GUID_FILE = join(APP_HOME, 'wechat-guid');
|
|
10
|
+
export function getDeviceGuid() {
|
|
11
|
+
try {
|
|
12
|
+
const existing = readFileSync(GUID_FILE, 'utf-8').trim();
|
|
13
|
+
if (existing)
|
|
14
|
+
return existing;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* 文件不存在 */
|
|
18
|
+
}
|
|
19
|
+
const guid = createHash('md5').update(randomUUID()).digest('hex');
|
|
20
|
+
try {
|
|
21
|
+
mkdirSync(dirname(GUID_FILE), { recursive: true });
|
|
22
|
+
writeFileSync(GUID_FILE, guid, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* 写入失败不致命 */
|
|
26
|
+
}
|
|
27
|
+
return guid;
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QClaw 环境配置(生产/测试)
|
|
3
|
+
*/
|
|
4
|
+
const ENVIRONMENT_CONFIGS = {
|
|
5
|
+
production: {
|
|
6
|
+
jprxGateway: 'https://jprx.m.qq.com/',
|
|
7
|
+
wxLoginRedirectUri: 'https://security.guanjia.qq.com/login',
|
|
8
|
+
wechatWsUrl: 'wss://mmgrcalltoken.3g.qq.com/agentwss',
|
|
9
|
+
},
|
|
10
|
+
test: {
|
|
11
|
+
jprxGateway: 'https://jprx.sparta.html5.qq.com/',
|
|
12
|
+
wxLoginRedirectUri: 'https://security-test.guanjia.qq.com/login',
|
|
13
|
+
wechatWsUrl: 'wss://jprx.sparta.html5.qq.com/agentwss',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
export function getEnvironment(name, appId) {
|
|
17
|
+
const config = ENVIRONMENT_CONFIGS[name];
|
|
18
|
+
if (!config)
|
|
19
|
+
throw new Error(`未知环境: ${name},可选: ${Object.keys(ENVIRONMENT_CONFIGS).join(', ')}`);
|
|
20
|
+
return { ...config, wxAppId: appId };
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微信扫码登录(QClaw 体系)
|
|
3
|
+
*/
|
|
4
|
+
export { performWeChatLogin, getEnvironment } from './wechat-login.js';
|
|
5
|
+
export type { LoginCredentials, QClawEnvironment } from './types.js';
|
|
6
|
+
export type { PerformWeChatLoginOptions } from './wechat-login.js';
|
|
7
|
+
export { getDeviceGuid } from './device-guid.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QClaw JPRX 网关 API 客户端
|
|
3
|
+
* 用于微信扫码登录、token 刷新、设备绑定
|
|
4
|
+
*/
|
|
5
|
+
import type { QClawEnvironment } from './types.js';
|
|
6
|
+
export interface QClawApiResponse {
|
|
7
|
+
success: boolean;
|
|
8
|
+
data?: Record<string, unknown>;
|
|
9
|
+
message?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class QClawAPI {
|
|
12
|
+
private env;
|
|
13
|
+
private guid;
|
|
14
|
+
loginKey: string;
|
|
15
|
+
jwtToken: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
constructor(env: QClawEnvironment, guid: string, jwtToken?: string);
|
|
18
|
+
private headers;
|
|
19
|
+
private post;
|
|
20
|
+
getWxLoginState(): Promise<QClawApiResponse>;
|
|
21
|
+
wxLogin(code: string, state: string): Promise<QClawApiResponse>;
|
|
22
|
+
generateContactLink(openKfId: string): Promise<QClawApiResponse>;
|
|
23
|
+
queryDeviceByGuid(): Promise<QClawApiResponse>;
|
|
24
|
+
/** 刷新渠道 token(4058),连接前调用以获取最新 channel_token */
|
|
25
|
+
refreshChannelToken(): Promise<string | null>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QClaw JPRX 网关 API 客户端
|
|
3
|
+
* 用于微信扫码登录、token 刷新、设备绑定
|
|
4
|
+
*/
|
|
5
|
+
function nested(obj, ...keys) {
|
|
6
|
+
let cur = obj;
|
|
7
|
+
for (const k of keys) {
|
|
8
|
+
if (cur == null || typeof cur !== 'object')
|
|
9
|
+
return undefined;
|
|
10
|
+
cur = cur[k];
|
|
11
|
+
}
|
|
12
|
+
return cur;
|
|
13
|
+
}
|
|
14
|
+
export class QClawAPI {
|
|
15
|
+
env;
|
|
16
|
+
guid;
|
|
17
|
+
loginKey = 'm83qdao0AmE5';
|
|
18
|
+
jwtToken = '';
|
|
19
|
+
userId = '';
|
|
20
|
+
constructor(env, guid, jwtToken = '') {
|
|
21
|
+
this.env = env;
|
|
22
|
+
this.guid = guid;
|
|
23
|
+
this.jwtToken = jwtToken;
|
|
24
|
+
}
|
|
25
|
+
headers() {
|
|
26
|
+
const h = {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
'X-Version': '1',
|
|
29
|
+
'X-Token': this.loginKey,
|
|
30
|
+
'X-Guid': this.guid,
|
|
31
|
+
'X-Account': this.userId || '1',
|
|
32
|
+
'X-Session': '',
|
|
33
|
+
};
|
|
34
|
+
if (this.jwtToken)
|
|
35
|
+
h['X-OpenClaw-Token'] = this.jwtToken;
|
|
36
|
+
return h;
|
|
37
|
+
}
|
|
38
|
+
async post(path, body = {}) {
|
|
39
|
+
const url = `${this.env.jprxGateway}${path}`;
|
|
40
|
+
const payload = { ...body, web_version: '1.4.0', web_env: 'release' };
|
|
41
|
+
const res = await fetch(url, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: this.headers(),
|
|
44
|
+
body: JSON.stringify(payload),
|
|
45
|
+
signal: AbortSignal.timeout(30_000),
|
|
46
|
+
});
|
|
47
|
+
const newToken = res.headers.get('X-New-Token');
|
|
48
|
+
if (newToken)
|
|
49
|
+
this.jwtToken = newToken;
|
|
50
|
+
const data = (await res.json());
|
|
51
|
+
const ret = data.ret;
|
|
52
|
+
const commonCode = nested(data, 'data', 'resp', 'common', 'code') ??
|
|
53
|
+
nested(data, 'data', 'common', 'code') ??
|
|
54
|
+
nested(data, 'resp', 'common', 'code') ??
|
|
55
|
+
nested(data, 'common', 'code');
|
|
56
|
+
if (ret === 0 || commonCode === 0) {
|
|
57
|
+
const respData = nested(data, 'data', 'resp', 'data') ??
|
|
58
|
+
nested(data, 'data', 'data') ??
|
|
59
|
+
data.data ??
|
|
60
|
+
data;
|
|
61
|
+
return { success: true, data: respData };
|
|
62
|
+
}
|
|
63
|
+
const message = nested(data, 'data', 'common', 'message') ??
|
|
64
|
+
nested(data, 'resp', 'common', 'message') ??
|
|
65
|
+
nested(data, 'common', 'message') ??
|
|
66
|
+
'请求失败';
|
|
67
|
+
return { success: false, message, data: data };
|
|
68
|
+
}
|
|
69
|
+
async getWxLoginState() {
|
|
70
|
+
return this.post('data/4050/forward', { guid: this.guid });
|
|
71
|
+
}
|
|
72
|
+
async wxLogin(code, state) {
|
|
73
|
+
return this.post('data/4026/forward', { guid: this.guid, code, state });
|
|
74
|
+
}
|
|
75
|
+
async generateContactLink(openKfId) {
|
|
76
|
+
return this.post('data/4018/forward', {
|
|
77
|
+
guid: this.guid,
|
|
78
|
+
user_id: Number(this.userId),
|
|
79
|
+
open_id: openKfId,
|
|
80
|
+
contact_type: 'open_kfid',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async queryDeviceByGuid() {
|
|
84
|
+
return this.post('data/4019/forward', { guid: this.guid });
|
|
85
|
+
}
|
|
86
|
+
/** 刷新渠道 token(4058),连接前调用以获取最新 channel_token */
|
|
87
|
+
async refreshChannelToken() {
|
|
88
|
+
const result = await this.post('data/4058/forward', {});
|
|
89
|
+
if (result.success && result.data) {
|
|
90
|
+
const d = result.data;
|
|
91
|
+
const token = nested(d, 'openclaw_channel_token') ??
|
|
92
|
+
nested(d, 'data', 'openclaw_channel_token') ??
|
|
93
|
+
nested(d, 'resp', 'openclaw_channel_token') ??
|
|
94
|
+
nested(d, 'resp', 'data', 'openclaw_channel_token') ??
|
|
95
|
+
null;
|
|
96
|
+
return token;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微信扫码登录(QClaw 体系)类型定义
|
|
3
|
+
*/
|
|
4
|
+
export interface QClawEnvironment {
|
|
5
|
+
jprxGateway: string;
|
|
6
|
+
wxLoginRedirectUri: string;
|
|
7
|
+
wechatWsUrl: string;
|
|
8
|
+
wxAppId: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LoginCredentials {
|
|
11
|
+
channelToken: string;
|
|
12
|
+
jwtToken: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
guid: string;
|
|
15
|
+
/** 4058 刷新 token 时需用此值作为 X-Token header */
|
|
16
|
+
loginKey?: string;
|
|
17
|
+
userInfo?: Record<string, unknown>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微信扫码登录流程
|
|
3
|
+
* 1. 获取 state → 2. 显示二维码 → 3. 等待 code → 4. 换 token → 5. 设备绑定
|
|
4
|
+
*/
|
|
5
|
+
import type { QClawEnvironment, LoginCredentials } from './types.js';
|
|
6
|
+
import { getEnvironment } from './environments.js';
|
|
7
|
+
export interface PerformWeChatLoginOptions {
|
|
8
|
+
envName?: string;
|
|
9
|
+
appId?: string;
|
|
10
|
+
bypassInvite?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 执行微信扫码登录,返回 token、guid、userId
|
|
14
|
+
*/
|
|
15
|
+
export declare function performWeChatLogin(options?: PerformWeChatLoginOptions): Promise<LoginCredentials>;
|
|
16
|
+
export { getEnvironment };
|
|
17
|
+
export type { QClawEnvironment, LoginCredentials };
|