@xmanrui/dsh-im 4.3.0 → 4.5.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.
Files changed (81) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/lib/client.js +976 -634
  4. package/lib/index.js +241 -238
  5. package/package.json +1 -1
  6. package/plugin-src/client/access-policy-settings.js +351 -0
  7. package/plugin-src/client/channel-card-meta.js +2 -1
  8. package/plugin-src/client/channels/dingtalk/api.js +5 -0
  9. package/plugin-src/client/channels/dingtalk/index.js +1 -0
  10. package/plugin-src/client/channels/feishu/api.js +5 -0
  11. package/plugin-src/client/channels/feishu/index.js +1 -0
  12. package/plugin-src/client/channels/qq/api.js +5 -0
  13. package/plugin-src/client/channels/qq/index.js +1 -0
  14. package/plugin-src/client/channels/shared/token-api.js +5 -0
  15. package/plugin-src/client/channels/shared/token-channel.js +1 -0
  16. package/plugin-src/client/channels/telegram/api.js +2 -17
  17. package/plugin-src/client/channels/telegram/index.js +0 -112
  18. package/plugin-src/client/channels/telegram/styles.js +0 -28
  19. package/plugin-src/client/channels/wecom/api.js +5 -0
  20. package/plugin-src/client/channels/wecom/index.js +1 -0
  21. package/plugin-src/client/channels/weixin/api.js +5 -0
  22. package/plugin-src/client/channels/weixin/index.js +1 -0
  23. package/plugin-src/client/channels/whatsapp/api.js +4 -10
  24. package/plugin-src/client/channels/whatsapp/index.js +1 -125
  25. package/plugin-src/client/channels/whatsapp/styles.js +0 -25
  26. package/plugin-src/client/context-enhancement.js +19 -5
  27. package/plugin-src/client/delivery-settings.js +61 -9
  28. package/plugin-src/client/i18n.js +61 -2
  29. package/plugin-src/client/index.js +1 -0
  30. package/plugin-src/client/styles.js +50 -4
  31. package/plugin-src/host/channels/dingtalk/production.mjs +12 -1
  32. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  33. package/plugin-src/host/channels/feishu/production.mjs +39 -1
  34. package/plugin-src/host/channels/feishu/rpc.mjs +19 -2
  35. package/plugin-src/host/channels/qq/production.mjs +12 -1
  36. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  37. package/plugin-src/host/channels/shared/access-policy-production.mjs +137 -0
  38. package/plugin-src/host/channels/shared/access-policy-rpc.mjs +17 -0
  39. package/plugin-src/host/channels/shared/production.mjs +15 -1
  40. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  41. package/plugin-src/host/channels/slack/production.mjs +12 -1
  42. package/plugin-src/host/channels/slack/rpc.mjs +10 -0
  43. package/plugin-src/host/channels/telegram/rpc.mjs +2 -49
  44. package/plugin-src/host/channels/wecom/production.mjs +12 -1
  45. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  46. package/plugin-src/host/channels/weixin/production.mjs +12 -1
  47. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  48. package/plugin-src/host/channels/whatsapp/production.mjs +16 -2
  49. package/plugin-src/host/channels/whatsapp/rpc.mjs +10 -15
  50. package/plugin-src/host/modern-harness-api.mjs +3 -0
  51. package/src/channels/dingtalk/dingtalk-bridge.mjs +70 -12
  52. package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -0
  53. package/src/channels/discord/discord-runtime.mjs +29 -1
  54. package/src/channels/feishu/bridge.mjs +207 -82
  55. package/src/channels/feishu/feishu-cards.mjs +2 -2
  56. package/src/channels/feishu/feishu-channel.mjs +36 -6
  57. package/src/channels/feishu/feishu-runtime.mjs +4 -0
  58. package/src/channels/qq/qq-bridge.mjs +67 -20
  59. package/src/channels/qq/qq-runtime.mjs +20 -2
  60. package/src/channels/shared/access-policy.mjs +210 -0
  61. package/src/channels/shared/bot-workspace-store.mjs +181 -33
  62. package/src/channels/shared/command-permission.mjs +32 -0
  63. package/src/channels/shared/compact-command.mjs +5 -2
  64. package/src/channels/shared/context-enhancement.mjs +9 -4
  65. package/src/channels/shared/harness-client.mjs +6 -0
  66. package/src/channels/shared/i18n-en/feishu.mjs +3 -3
  67. package/src/channels/shared/i18n-en/shared-a.mjs +2 -1
  68. package/src/channels/shared/i18n-en/shared-b.mjs +6 -3
  69. package/src/channels/shared/i18n-en/shared-c.mjs +2 -0
  70. package/src/channels/shared/inbound-access.mjs +43 -0
  71. package/src/channels/shared/session-title.mjs +74 -0
  72. package/src/channels/shared/text-harness-bridge.mjs +54 -6
  73. package/src/channels/shared/workspace-command.mjs +26 -5
  74. package/src/channels/shared/workspace-session.mjs +28 -2
  75. package/src/channels/slack/slack-runtime.mjs +4 -0
  76. package/src/channels/telegram/telegram-runtime.mjs +6 -13
  77. package/src/channels/wecom/wecom-bridge.mjs +57 -6
  78. package/src/channels/wecom/wecom-runtime.mjs +4 -0
  79. package/src/channels/weixin/weixin-bridge.mjs +67 -19
  80. package/src/channels/weixin/weixin-runtime.mjs +4 -0
  81. package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -20
@@ -57,6 +57,10 @@ import {
57
57
  messageFailureText,
58
58
  setLastMessageFailure,
59
59
  } from '../shared/message-failure.mjs';
60
+ import {
61
+ COMMAND_PERMISSION_DENIED_MESSAGE,
62
+ evaluateInboundAccess,
63
+ } from '../shared/inbound-access.mjs';
60
64
  import { t } from '../shared/i18n.mjs';
61
65
 
62
66
  const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
@@ -71,7 +75,7 @@ const HELP_TEXT = () => [
71
75
  t('/new 开启一个全新会话'),
72
76
  t('/compact 压缩当前会话的较早上下文'),
73
77
  t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
74
- t('/workspace 工作区绝对路径 切换工作区'),
78
+ t('/workspace 工作区序号或绝对路径 切换工作区'),
75
79
  t('/workspacelist 列出工作区绝对路径'),
76
80
  t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
77
81
  t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
@@ -260,6 +264,7 @@ export class WeixinHarnessBridge {
260
264
  #harness;
261
265
  #state;
262
266
  #contextEnhancement;
267
+ #accessPolicy;
263
268
  #status;
264
269
  #logger;
265
270
  #replyTimeoutMs;
@@ -292,6 +297,7 @@ export class WeixinHarnessBridge {
292
297
  harness,
293
298
  state,
294
299
  contextEnhancement,
300
+ accessPolicy,
295
301
  status = createWeixinBridgeStatus(),
296
302
  logger = console,
297
303
  replyTimeoutMs = 600_000,
@@ -312,6 +318,7 @@ export class WeixinHarnessBridge {
312
318
  this.#harness = harness;
313
319
  this.#state = state;
314
320
  this.#contextEnhancement = contextEnhancement;
321
+ this.#accessPolicy = accessPolicy;
315
322
  this.#status = status;
316
323
  this.#logger = logger;
317
324
  this.#replyTimeoutMs = replyTimeoutMs;
@@ -332,6 +339,22 @@ export class WeixinHarnessBridge {
332
339
  const sender = nonEmptyString(message?.from_user_id);
333
340
  if (!messageId || !sender || this.#state.hasSeen(messageId)
334
341
  || this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
342
+ const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
343
+ const access = this.#accessPolicy
344
+ ? evaluateInboundAccess(this.#accessPolicy, {
345
+ conversationType: 'direct',
346
+ senderIds: sender,
347
+ text: commandText,
348
+ hasImages: hasWeixinImageItems(message),
349
+ hasFiles: hasWeixinFileItems(message),
350
+ })
351
+ : sender === this.#ownerUserId
352
+ ? { allowed: true, reason: 'legacy-owner' }
353
+ : { allowed: false, reason: 'sender-not-allowed' };
354
+ if (!access.allowed) {
355
+ this.#acceptedMessageIds.set(messageId, null);
356
+ return this.#finishAccessDecision(messageId, sender, message, access);
357
+ }
335
358
  this.#acceptedMessageIds.set(messageId, captureContextEnhancement(
336
359
  this.#contextEnhancement,
337
360
  'direct',
@@ -343,11 +366,9 @@ export class WeixinHarnessBridge {
343
366
  const contextToken = nonEmptyString(message?.context_token) ?? undefined;
344
367
  const runId = nonEmptyString(message?.run_id) ?? undefined;
345
368
  const pending = this.#pendingInteractions.get(key);
346
- const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
347
369
  const batchCommand = isBatchInputCommand(commandText);
348
370
  const batchStatus = this.#batchInputs.status(key);
349
- if (sender === this.#ownerUserId
350
- && (batchCommand || batchStatus.phase === 'collecting')) {
371
+ if (batchCommand || batchStatus.phase === 'collecting') {
351
372
  const exactBatchStart = /^\/batch$/iu.test(commandText);
352
373
  const result = exactBatchStart
353
374
  && batchStatus.phase === 'idle'
@@ -380,7 +401,7 @@ export class WeixinHarnessBridge {
380
401
  : (isModelCommand(commandText)
381
402
  ? runModelCommand
382
403
  : (isPresetCommand(commandText) ? runPresetCommand : null));
383
- if (commandRunner && sender === this.#ownerUserId) {
404
+ if (commandRunner) {
384
405
  let task;
385
406
  task = this.#processFastCommand(
386
407
  message,
@@ -467,12 +488,10 @@ export class WeixinHarnessBridge {
467
488
  alreadyRecorded = false,
468
489
  batchSubmission = null,
469
490
  } = {}) {
470
- const preparedMessage = message.from_user_id === this.#ownerUserId
471
- ? prefetchInboundFiles(
472
- weixinInboundMessage(message, this.#api),
473
- { signal: this.#signal },
474
- )
475
- : undefined;
491
+ const preparedMessage = prefetchInboundFiles(
492
+ weixinInboundMessage(message, this.#api),
493
+ { signal: this.#signal },
494
+ );
476
495
  const previous = this.#queues.get(key) ?? Promise.resolve();
477
496
  const current = previous
478
497
  .catch(() => undefined)
@@ -518,6 +537,36 @@ export class WeixinHarnessBridge {
518
537
  return task;
519
538
  }
520
539
 
540
+ #finishAccessDecision(messageId, sender, message, access) {
541
+ const contextToken = nonEmptyString(message?.context_token) ?? undefined;
542
+ const runId = nonEmptyString(message?.run_id) ?? undefined;
543
+ let task;
544
+ task = Promise.resolve().then(async () => {
545
+ if (this.#state.hasSeen(messageId)) return;
546
+ await this.#state.markSeen(messageId);
547
+ if (access.reason === 'command-not-allowed') {
548
+ this.#status.messagesReceived += 1;
549
+ this.#status.lastMessageAt = new Date().toISOString();
550
+ await this.#send(sender, t(COMMAND_PERMISSION_DENIED_MESSAGE), contextToken, runId);
551
+ this.#status.messagesReplied += 1;
552
+ this.#status.lastReplyAt = new Date().toISOString();
553
+ } else {
554
+ this.#status.messagesRejected += 1;
555
+ this.#status.lastRejectedAt = new Date().toISOString();
556
+ }
557
+ this.#status.lastError = null;
558
+ }).catch((error) => {
559
+ if (this.#signal?.aborted) return;
560
+ this.#status.lastError = error?.message ?? String(error);
561
+ this.#logger.error?.('[dsh-weixin] failed to apply inbound access policy:', error);
562
+ }).finally(() => {
563
+ this.#acceptedMessageIds.delete(messageId);
564
+ this.#commandTasks.delete(task);
565
+ });
566
+ this.#commandTasks.add(task);
567
+ return task;
568
+ }
569
+
521
570
  async waitForIdle() {
522
571
  await Promise.allSettled([
523
572
  ...this.#queues.values(),
@@ -590,12 +639,6 @@ export class WeixinHarnessBridge {
590
639
  this.#status.messagesReceived += 1;
591
640
  this.#status.lastMessageAt = new Date().toISOString();
592
641
  }
593
- if (sender !== this.#ownerUserId) {
594
- this.#status.messagesRejected += 1;
595
- this.#status.lastRejectedAt = new Date().toISOString();
596
- return;
597
- }
598
-
599
642
  const contextToken = typeof message.context_token === 'string' ? message.context_token : undefined;
600
643
  const runId = typeof message.run_id === 'string' ? message.run_id : undefined;
601
644
  let batchSettled = batchSubmission === null;
@@ -662,11 +705,14 @@ export class WeixinHarnessBridge {
662
705
  ? await promptContentForMessage(promptMessage, { signal: this.#signal })
663
706
  : undefined;
664
707
  const snapshot = this.#acceptedMessageIds.get(messageId);
708
+ let contextEnhanced = false;
665
709
  if (snapshot) {
666
- content = enhanceContextContent(content ?? text, snapshot, () => ({
710
+ const originalContent = content ?? text;
711
+ content = enhanceContextContent(originalContent, snapshot, () => ({
667
712
  channel: 'weixin',
668
713
  senderId: sender,
669
714
  }));
715
+ contextEnhanced = content !== originalContent;
670
716
  }
671
717
  await this.#state.markSeen(messageId);
672
718
  promptRecorded = true;
@@ -674,7 +720,9 @@ export class WeixinHarnessBridge {
674
720
  harness: this.#harness,
675
721
  state: this.#state,
676
722
  key,
677
- ...(content !== undefined ? { content } : { text }),
723
+ text,
724
+ content,
725
+ contextEnhanced,
678
726
  createOptions: { signal: this.#signal },
679
727
  existsOptions: { signal: this.#signal },
680
728
  askOptions: {
@@ -109,6 +109,7 @@ export class WeixinRuntime {
109
109
  #harness;
110
110
  #state;
111
111
  #contextEnhancement;
112
+ #accessPolicy;
112
113
  #logger;
113
114
  #replyTimeoutMs;
114
115
  #maxMessageChars;
@@ -126,6 +127,7 @@ export class WeixinRuntime {
126
127
  harness,
127
128
  state,
128
129
  contextEnhancement,
130
+ accessPolicy,
129
131
  logger = console,
130
132
  replyTimeoutMs = 600_000,
131
133
  maxMessageChars = DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
@@ -140,6 +142,7 @@ export class WeixinRuntime {
140
142
  this.#harness = harness;
141
143
  this.#state = state;
142
144
  this.#contextEnhancement = contextEnhancement;
145
+ this.#accessPolicy = accessPolicy;
143
146
  this.#logger = logger;
144
147
  this.#replyTimeoutMs = replyTimeoutMs;
145
148
  this.#maxMessageChars = maxMessageChars;
@@ -182,6 +185,7 @@ export class WeixinRuntime {
182
185
  harness: this.#harness,
183
186
  state: this.#state,
184
187
  contextEnhancement: this.#contextEnhancement,
188
+ accessPolicy: this.#accessPolicy,
185
189
  status: this.#status,
186
190
  logger: this.#logger,
187
191
  replyTimeoutMs: this.#replyTimeoutMs,
@@ -3,6 +3,7 @@ import { createHash, randomBytes } from 'node:crypto';
3
3
  import {
4
4
  areJidsSameUser,
5
5
  downloadMediaMessage,
6
+ jidDecode,
6
7
  normalizeMessageContent,
7
8
  } from '@whiskeysockets/baileys';
8
9
 
@@ -13,7 +14,6 @@ import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifac
13
14
  import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
14
15
  import {
15
16
  WHATSAPP_ACCESS_MODES,
16
- normalizeWhatsappAccessPolicy,
17
17
  } from './config-store.mjs';
18
18
  import { createWhatsappWebSession } from './whatsapp-web-session.mjs';
19
19
 
@@ -37,6 +37,36 @@ const VIEW_ONCE_WRAPPER_KEYS = new Set([
37
37
  'viewOnceMessageV2',
38
38
  'viewOnceMessageV2Extension',
39
39
  ]);
40
+ const WHATSAPP_ACCESS_POLICY_USER_SERVERS = new Set([
41
+ 's.whatsapp.net',
42
+ 'c.us',
43
+ 'lid',
44
+ 'hosted',
45
+ 'hosted.lid',
46
+ ]);
47
+
48
+ function normalizeWhatsappAccessPolicyId(value) {
49
+ if (typeof value !== 'string') return null;
50
+ const candidate = value.trim();
51
+ if (/^\+?\d+$/.test(candidate)) {
52
+ return `${candidate.replace(/^\+/, '')}@s.whatsapp.net`;
53
+ }
54
+ const decoded = jidDecode(candidate);
55
+ if (!decoded || !/^\d+$/.test(decoded.user)
56
+ || !WHATSAPP_ACCESS_POLICY_USER_SERVERS.has(decoded.server)) return null;
57
+ return candidate;
58
+ }
59
+
60
+ export function whatsappAccessPolicyIdsEqual(left, right) {
61
+ const normalizedLeft = normalizeWhatsappAccessPolicyId(left);
62
+ const normalizedRight = normalizeWhatsappAccessPolicyId(right);
63
+ if (!normalizedLeft || !normalizedRight) return false;
64
+ try {
65
+ return areJidsSameUser(normalizedLeft, normalizedRight) === true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
40
70
 
41
71
  function hasViewOnceWrapper(content) {
42
72
  let current = content;
@@ -538,12 +568,11 @@ export class WhatsappRuntime {
538
568
  #harness;
539
569
  #state;
540
570
  #contextEnhancement;
571
+ #accessPolicy;
541
572
  #logger;
542
573
  #replyTimeoutMs;
543
574
  #connectTimeoutMs;
544
575
  #mediaUploadTimeoutMs;
545
- #accessMode;
546
- #allowedPrivateNumbers;
547
576
  #createSession;
548
577
  #status = createWhatsappRuntimeStatus();
549
578
  #abortController = null;
@@ -558,6 +587,7 @@ export class WhatsappRuntime {
558
587
  harness,
559
588
  state,
560
589
  contextEnhancement,
590
+ accessPolicy,
561
591
  logger = console,
562
592
  replyTimeoutMs = 600_000,
563
593
  connectTimeoutMs = 30_000,
@@ -572,6 +602,7 @@ export class WhatsappRuntime {
572
602
  this.#harness = harness;
573
603
  this.#state = state;
574
604
  this.#contextEnhancement = contextEnhancement;
605
+ this.#accessPolicy = accessPolicy;
575
606
  this.#logger = logger;
576
607
  this.#replyTimeoutMs = replyTimeoutMs;
577
608
  this.#connectTimeoutMs = connectTimeoutMs;
@@ -583,21 +614,12 @@ export class WhatsappRuntime {
583
614
  WHATSAPP_MEDIA_UPLOAD_TIMEOUT_MS,
584
615
  );
585
616
  this.#createSession = createSession;
586
- this.setAccessPolicy(config);
587
617
  }
588
618
 
589
619
  get status() {
590
620
  return structuredClone(this.#status);
591
621
  }
592
622
 
593
- setAccessPolicy(value) {
594
- const policy = normalizeWhatsappAccessPolicy(value);
595
- this.#accessMode = policy.accessMode;
596
- this.#allowedPrivateNumbers = new Set(policy.allowedNumbers);
597
- this.#config = { ...this.#config, ...policy };
598
- return policy;
599
- }
600
-
601
623
  async start() {
602
624
  if (this.#status.ready && this.#session) return this.status;
603
625
  if (this.#starting) return this.#starting;
@@ -636,14 +658,6 @@ export class WhatsappRuntime {
636
658
  });
637
659
  if (!message || outboundIds.has(message.providerMessageId) || !this.#bridge) return;
638
660
  this.#status.lastCheckedAt = Date.now();
639
- if (!whatsappInboundAllowed(message, {
640
- accessMode: this.#accessMode,
641
- allowedNumbers: this.#allowedPrivateNumbers,
642
- })) {
643
- this.#status.messagesRejected += 1;
644
- this.#status.lastRejectedAt = new Date().toISOString();
645
- return;
646
- }
647
661
  await this.#bridge.accept(message);
648
662
  },
649
663
  onDisconnect: ({ error }) => {
@@ -678,6 +692,14 @@ export class WhatsappRuntime {
678
692
  harness: this.#harness,
679
693
  state: this.#state,
680
694
  contextEnhancement: this.#contextEnhancement,
695
+ accessPolicy: this.#accessPolicy ? {
696
+ botId: this.#accessPolicy.botId,
697
+ getSettings: (...args) => this.#accessPolicy.getSettings(...args),
698
+ ...(typeof this.#accessPolicy.isPrivileged === 'function' ? {
699
+ isPrivileged: (...args) => this.#accessPolicy.isPrivileged(...args),
700
+ } : {}),
701
+ equals: whatsappAccessPolicyIdsEqual,
702
+ } : undefined,
681
703
  status: this.#status,
682
704
  logger: this.#logger,
683
705
  replyTimeoutMs: this.#replyTimeoutMs,