@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/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 或 WeChat 其中一个平台");
89
- console.log("或设置环境变量: TELEGRAM_BOT_TOKEN=xxx、FEISHU_APP_ID=xxx 或 WECHAT_APP_ID=xxx 后再运行");
99
+ console.log("提示:至少需要配置 Telegram、Feishu、WeChatWeWork 其中一个平台");
100
+ console.log("或设置环境变量: TELEGRAM_BOT_TOKEN=xxx、FEISHU_APP_ID=xxx、WECHAT_APP_ID=xxxWEWORK_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 hasWc = !!(existing?.platforms?.wechat?.appId && existing?.platforms?.wechat?.appSecret);
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) - 需要 App ID 和 App Secret(AGP 协议)" +
145
+ title: "微信 (WeChat) - 扫码登录获取 tokenQClaw/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 wechatResp = await prompts([
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: "微信 App ID(从微信开放平台获取)",
219
- initial: existing?.platforms?.wechat?.appId ?? "",
220
- validate: (v) => (v.trim() ? true : "App ID 不能为空"),
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: "appSecret",
225
- message: "微信 App Secret(从微信开放平台获取)",
226
- initial: existing?.platforms?.wechat?.appSecret ?? "",
227
- validate: (v) => (v.trim() ? true : "App Secret 不能为空"),
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: "wsUrl",
232
- message: "AGP WebSocket URL(可选,留空使用默认)",
233
- initial: existing?.platforms?.wechat?.wsUrl ?? "",
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 wcAppId = wechatResp.appId?.trim() || existing?.platforms?.wechat?.appId;
237
- const wcAppSecret = wechatResp.appSecret?.trim() || existing?.platforms?.wechat?.appSecret;
238
- if (wcAppId && wcAppSecret) {
239
- config.platforms.wechat = {
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
- appId: wcAppId,
242
- appSecret: wcAppSecret,
243
- wsUrl: wechatResp.wsUrl?.trim() || existing?.platforms?.wechat?.wsUrl || undefined,
317
+ corpId: wwCorpId,
318
+ secret: wwSecret,
244
319
  };
245
320
  }
246
- else if (platform === "wechat") {
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: config.platforms.wechat?.appId,
365
- appSecret: config.platforms.wechat?.appSecret,
366
- wsUrl: config.platforms.wechat?.wsUrl,
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,5 @@
1
+ /**
2
+ * 设备唯一标识生成
3
+ * 首次运行时随机生成并持久化到 ~/.open-im/wechat-guid
4
+ */
5
+ export declare function getDeviceGuid(): string;
@@ -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,5 @@
1
+ /**
2
+ * QClaw 环境配置(生产/测试)
3
+ */
4
+ import type { QClawEnvironment } from './types.js';
5
+ export declare function getEnvironment(name: string, appId: string): QClawEnvironment;
@@ -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,5 @@
1
+ /**
2
+ * 微信扫码登录(QClaw 体系)
3
+ */
4
+ export { performWeChatLogin, getEnvironment } from './wechat-login.js';
5
+ 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,4 @@
1
+ /**
2
+ * 微信扫码登录(QClaw 体系)类型定义
3
+ */
4
+ export {};
@@ -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 };