@xmanrui/dsh-im 4.2.1 → 4.4.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.en.md +4 -2
- package/README.md +4 -2
- package/lib/client.js +1267 -787
- package/lib/index.js +237 -235
- package/package.json +1 -1
- package/plugin-src/client/access-policy-settings.js +351 -0
- package/plugin-src/client/channel-card-meta.js +2 -1
- package/plugin-src/client/channels/dingtalk/api.js +5 -0
- package/plugin-src/client/channels/dingtalk/index.js +1 -0
- package/plugin-src/client/channels/feishu/api.js +5 -0
- package/plugin-src/client/channels/feishu/index.js +1 -0
- package/plugin-src/client/channels/qq/api.js +5 -0
- package/plugin-src/client/channels/qq/index.js +1 -0
- package/plugin-src/client/channels/shared/token-api.js +5 -0
- package/plugin-src/client/channels/shared/token-channel.js +1 -0
- package/plugin-src/client/channels/telegram/api.js +2 -17
- package/plugin-src/client/channels/telegram/index.js +0 -112
- package/plugin-src/client/channels/telegram/styles.js +0 -28
- package/plugin-src/client/channels/wecom/api.js +5 -0
- package/plugin-src/client/channels/wecom/index.js +1 -0
- package/plugin-src/client/channels/weixin/api.js +5 -0
- package/plugin-src/client/channels/weixin/index.js +1 -0
- package/plugin-src/client/channels/whatsapp/api.js +4 -10
- package/plugin-src/client/channels/whatsapp/index.js +1 -125
- package/plugin-src/client/channels/whatsapp/styles.js +0 -25
- package/plugin-src/client/context-enhancement.js +207 -102
- package/plugin-src/client/delivery-settings.js +61 -9
- package/plugin-src/client/i18n.js +71 -3
- package/plugin-src/client/index.js +1 -0
- package/plugin-src/client/styles.js +59 -8
- package/plugin-src/host/channels/dingtalk/production.mjs +12 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/feishu/production.mjs +39 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +19 -2
- package/plugin-src/host/channels/qq/production.mjs +12 -1
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/access-policy-production.mjs +137 -0
- package/plugin-src/host/channels/shared/access-policy-rpc.mjs +17 -0
- package/plugin-src/host/channels/shared/production.mjs +15 -1
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/slack/production.mjs +12 -1
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- package/plugin-src/host/channels/telegram/rpc.mjs +2 -49
- package/plugin-src/host/channels/wecom/production.mjs +12 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +12 -1
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +16 -2
- package/plugin-src/host/channels/whatsapp/rpc.mjs +10 -15
- package/scripts/verify-package.mjs +4 -3
- package/src/channels/dingtalk/dingtalk-bridge.mjs +61 -9
- package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -0
- package/src/channels/discord/discord-runtime.mjs +29 -1
- package/src/channels/feishu/bridge.mjs +286 -120
- package/src/channels/feishu/feishu-runtime.mjs +4 -0
- package/src/channels/feishu/message-utils.mjs +5 -0
- package/src/channels/qq/qq-bridge.mjs +59 -17
- package/src/channels/qq/qq-runtime.mjs +25 -3
- package/src/channels/shared/access-policy.mjs +210 -0
- package/src/channels/shared/bot-workspace-store.mjs +176 -33
- package/src/channels/shared/command-permission.mjs +32 -0
- package/src/channels/shared/compact-command.mjs +5 -2
- package/src/channels/shared/context-enhancement.mjs +73 -20
- package/src/channels/shared/i18n-en/shared-c.mjs +2 -0
- package/src/channels/shared/inbound-access.mjs +43 -0
- package/src/channels/shared/text-harness-bridge.mjs +47 -4
- package/src/channels/shared/workspace-command.mjs +10 -1
- package/src/channels/slack/slack-runtime.mjs +4 -0
- package/src/channels/telegram/telegram-runtime.mjs +5 -13
- package/src/channels/wecom/wecom-bridge.mjs +51 -4
- package/src/channels/wecom/wecom-runtime.mjs +4 -0
- package/src/channels/weixin/weixin-bridge.mjs +59 -16
- package/src/channels/weixin/weixin-runtime.mjs +4 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -20
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { t } from './i18n.mjs';
|
|
2
|
+
import {
|
|
3
|
+
COMMAND_PERMISSION_DENIED_MESSAGE,
|
|
4
|
+
evaluateInboundAccess,
|
|
5
|
+
} from './inbound-access.mjs';
|
|
2
6
|
import { captureContextEnhancement, enhanceContextContent } from './context-enhancement.mjs';
|
|
3
7
|
import { runWorkspaceCommand } from './workspace-command.mjs';
|
|
4
8
|
import { runCompactCommand } from './compact-command.mjs';
|
|
@@ -129,6 +133,7 @@ export class TextHarnessBridge {
|
|
|
129
133
|
#harness;
|
|
130
134
|
#state;
|
|
131
135
|
#contextEnhancement;
|
|
136
|
+
#accessPolicy;
|
|
132
137
|
#status;
|
|
133
138
|
#logger;
|
|
134
139
|
#replyTimeoutMs;
|
|
@@ -149,6 +154,7 @@ export class TextHarnessBridge {
|
|
|
149
154
|
harness,
|
|
150
155
|
state,
|
|
151
156
|
contextEnhancement,
|
|
157
|
+
accessPolicy,
|
|
152
158
|
status = createTextBridgeStatus(),
|
|
153
159
|
logger = console,
|
|
154
160
|
replyTimeoutMs = 600_000,
|
|
@@ -162,6 +168,7 @@ export class TextHarnessBridge {
|
|
|
162
168
|
this.#harness = harness;
|
|
163
169
|
this.#state = state;
|
|
164
170
|
this.#contextEnhancement = contextEnhancement;
|
|
171
|
+
this.#accessPolicy = accessPolicy;
|
|
165
172
|
this.#status = status;
|
|
166
173
|
this.#logger = logger;
|
|
167
174
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
@@ -176,7 +183,7 @@ export class TextHarnessBridge {
|
|
|
176
183
|
return structuredClone(this.#status);
|
|
177
184
|
}
|
|
178
185
|
|
|
179
|
-
accept(message, { contextSnapshot } = {}) {
|
|
186
|
+
accept(message, { contextSnapshot, accessDecision } = {}) {
|
|
180
187
|
if (this.#signal?.aborted) return Promise.resolve();
|
|
181
188
|
const conversationId = cleanText(message?.conversationId);
|
|
182
189
|
const kind = message?.kind === 'group' ? 'group' : 'direct';
|
|
@@ -187,6 +194,40 @@ export class TextHarnessBridge {
|
|
|
187
194
|
|| this.#state.hasSeen(messageId) || this.#acceptedMessageIds.has(messageId)) {
|
|
188
195
|
return Promise.resolve();
|
|
189
196
|
}
|
|
197
|
+
// Preserve the channel trigger boundary. Access policy never turns an
|
|
198
|
+
// unaddressed group message into a denial reply.
|
|
199
|
+
if (kind === 'group' && normalized.addressed !== true) {
|
|
200
|
+
this.#status.messagesRejected += 1;
|
|
201
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
202
|
+
this.#acceptedMessageIds.set(messageId, null);
|
|
203
|
+
return this.#finishLocalMessage(normalized, messageId, null);
|
|
204
|
+
}
|
|
205
|
+
if (this.#accessPolicy || accessDecision) {
|
|
206
|
+
const hasImages = hasInboundImages(normalized);
|
|
207
|
+
const hasFiles = hasInboundFiles(normalized);
|
|
208
|
+
const decision = accessDecision ?? evaluateInboundAccess(this.#accessPolicy, {
|
|
209
|
+
conversationType: kind,
|
|
210
|
+
senderIds: [senderId, cleanText(normalized.senderAlternateId)].filter(Boolean),
|
|
211
|
+
text: normalized.content,
|
|
212
|
+
hasImages,
|
|
213
|
+
hasFiles,
|
|
214
|
+
});
|
|
215
|
+
if (!decision.allowed) {
|
|
216
|
+
this.#status.messagesRejected += 1;
|
|
217
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
218
|
+
// Mark policy denials so a webhook replay cannot repeat local work or
|
|
219
|
+
// a command-permission notice.
|
|
220
|
+
this.#acceptedMessageIds.set(messageId, null);
|
|
221
|
+
return this.#finishLocalMessage(
|
|
222
|
+
normalized,
|
|
223
|
+
messageId,
|
|
224
|
+
decision.reason === 'command-not-allowed'
|
|
225
|
+
? t(COMMAND_PERMISSION_DENIED_MESSAGE)
|
|
226
|
+
: null,
|
|
227
|
+
{ recordReceived: decision.reason === 'command-not-allowed' },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
190
231
|
this.#acceptedMessageIds.set(messageId, contextSnapshot === undefined
|
|
191
232
|
? captureContextEnhancement(this.#contextEnhancement, message?.kind)
|
|
192
233
|
: contextSnapshot);
|
|
@@ -350,13 +391,15 @@ export class TextHarnessBridge {
|
|
|
350
391
|
return this.#enqueueMessage(normalized, messageId, senderId, key);
|
|
351
392
|
}
|
|
352
393
|
|
|
353
|
-
#finishLocalMessage(message, messageId, reply) {
|
|
394
|
+
#finishLocalMessage(message, messageId, reply, { recordReceived = true } = {}) {
|
|
354
395
|
let task;
|
|
355
396
|
task = (async () => {
|
|
356
397
|
if (this.#state.hasSeen(messageId)) return;
|
|
357
398
|
await this.#state.markSeen(messageId);
|
|
358
|
-
|
|
359
|
-
|
|
399
|
+
if (recordReceived) {
|
|
400
|
+
this.#status.messagesReceived += 1;
|
|
401
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
402
|
+
}
|
|
360
403
|
if (reply) await this.#bot.sendText(message.replyTarget, reply);
|
|
361
404
|
this.#status.lastError = null;
|
|
362
405
|
})().catch(async (error) => {
|
|
@@ -353,7 +353,7 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
export async function runWorkspaceCommand(text, harness, conversationKey) {
|
|
356
|
-
if (
|
|
356
|
+
if (!isWorkspaceCommand(text)) return null;
|
|
357
357
|
const command = text.trim();
|
|
358
358
|
if (SESSION_BIND_PREFIX.test(command)) {
|
|
359
359
|
return runSessionBindCommand(command, harness, conversationKey);
|
|
@@ -385,3 +385,12 @@ export async function runWorkspaceCommand(text, harness, conversationKey) {
|
|
|
385
385
|
throw error;
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
|
+
|
|
389
|
+
export function isWorkspaceCommand(text) {
|
|
390
|
+
if (typeof text !== 'string') return false;
|
|
391
|
+
const command = text.trim();
|
|
392
|
+
return SESSION_BIND_PREFIX.test(command)
|
|
393
|
+
|| SESSION_LIST_COMMAND.test(command)
|
|
394
|
+
|| WORKSPACE_LIST_COMMAND.test(command)
|
|
395
|
+
|| WORKSPACE_COMMAND.test(command);
|
|
396
|
+
}
|
|
@@ -347,6 +347,7 @@ export class SlackRuntime {
|
|
|
347
347
|
#harness;
|
|
348
348
|
#state;
|
|
349
349
|
#contextEnhancement;
|
|
350
|
+
#accessPolicy;
|
|
350
351
|
#logger;
|
|
351
352
|
#replyTimeoutMs;
|
|
352
353
|
#connectTimeoutMs;
|
|
@@ -371,6 +372,7 @@ export class SlackRuntime {
|
|
|
371
372
|
harness,
|
|
372
373
|
state,
|
|
373
374
|
contextEnhancement,
|
|
375
|
+
accessPolicy,
|
|
374
376
|
logger = console,
|
|
375
377
|
replyTimeoutMs = 600_000,
|
|
376
378
|
connectTimeoutMs = 20_000,
|
|
@@ -387,6 +389,7 @@ export class SlackRuntime {
|
|
|
387
389
|
this.#harness = harness;
|
|
388
390
|
this.#state = state;
|
|
389
391
|
this.#contextEnhancement = contextEnhancement;
|
|
392
|
+
this.#accessPolicy = accessPolicy;
|
|
390
393
|
this.#logger = logger;
|
|
391
394
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
392
395
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
@@ -464,6 +467,7 @@ export class SlackRuntime {
|
|
|
464
467
|
harness: this.#harness,
|
|
465
468
|
state: this.#state,
|
|
466
469
|
contextEnhancement: this.#contextEnhancement,
|
|
470
|
+
accessPolicy: this.#accessPolicy,
|
|
467
471
|
status: this.#status,
|
|
468
472
|
logger: this.#logger,
|
|
469
473
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from './telegram-rich-message.mjs';
|
|
15
15
|
import {
|
|
16
16
|
TELEGRAM_ACCESS_MODES,
|
|
17
|
-
normalizeTelegramAccessPolicy,
|
|
18
17
|
} from './config-store.mjs';
|
|
19
18
|
|
|
20
19
|
export const TELEGRAM_COMMAND_MENU = Object.freeze([
|
|
@@ -669,12 +668,11 @@ export class TelegramRuntime {
|
|
|
669
668
|
#harness;
|
|
670
669
|
#state;
|
|
671
670
|
#contextEnhancement;
|
|
671
|
+
#accessPolicy;
|
|
672
672
|
#logger;
|
|
673
673
|
#replyTimeoutMs;
|
|
674
674
|
#createApi;
|
|
675
675
|
#createHttpTransport;
|
|
676
|
-
#accessMode;
|
|
677
|
-
#allowedPrivateUserIds;
|
|
678
676
|
#status = createTelegramRuntimeStatus();
|
|
679
677
|
#httpTransport = null;
|
|
680
678
|
#api = null;
|
|
@@ -689,6 +687,7 @@ export class TelegramRuntime {
|
|
|
689
687
|
harness,
|
|
690
688
|
state,
|
|
691
689
|
contextEnhancement,
|
|
690
|
+
accessPolicy,
|
|
692
691
|
logger = console,
|
|
693
692
|
replyTimeoutMs = 600_000,
|
|
694
693
|
createApi = (options) => new TelegramApi(options),
|
|
@@ -702,13 +701,11 @@ export class TelegramRuntime {
|
|
|
702
701
|
this.#harness = harness;
|
|
703
702
|
this.#state = state;
|
|
704
703
|
this.#contextEnhancement = contextEnhancement;
|
|
704
|
+
this.#accessPolicy = accessPolicy;
|
|
705
705
|
this.#logger = logger;
|
|
706
706
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
707
707
|
this.#createApi = createApi;
|
|
708
708
|
this.#createHttpTransport = createHttpTransport;
|
|
709
|
-
const accessPolicy = normalizeTelegramAccessPolicy(config);
|
|
710
|
-
this.#accessMode = accessPolicy.accessMode;
|
|
711
|
-
this.#allowedPrivateUserIds = new Set(accessPolicy.allowedUsers);
|
|
712
709
|
}
|
|
713
710
|
|
|
714
711
|
get status() {
|
|
@@ -805,6 +802,7 @@ export class TelegramRuntime {
|
|
|
805
802
|
harness: this.#harness,
|
|
806
803
|
state: this.#state,
|
|
807
804
|
contextEnhancement: this.#contextEnhancement,
|
|
805
|
+
accessPolicy: this.#accessPolicy,
|
|
808
806
|
status: this.#status,
|
|
809
807
|
logger: this.#logger,
|
|
810
808
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
@@ -865,10 +863,7 @@ export class TelegramRuntime {
|
|
|
865
863
|
loadFile: (fileId, options) => this.#api.downloadFile({ fileId, ...options }),
|
|
866
864
|
loadFileStream: (fileId, options) => this.#api.downloadFileStream({ fileId, ...options }),
|
|
867
865
|
});
|
|
868
|
-
if (message
|
|
869
|
-
accessMode: this.#accessMode,
|
|
870
|
-
allowedPrivateUserIds: this.#allowedPrivateUserIds,
|
|
871
|
-
})) {
|
|
866
|
+
if (message) {
|
|
872
867
|
void this.#bridge.accept(message, { contextSnapshot }).catch((error) => {
|
|
873
868
|
if (signal.aborted) return;
|
|
874
869
|
this.#logger.error?.(
|
|
@@ -876,9 +871,6 @@ export class TelegramRuntime {
|
|
|
876
871
|
error,
|
|
877
872
|
);
|
|
878
873
|
});
|
|
879
|
-
} else if (message) {
|
|
880
|
-
this.#status.messagesRejected += 1;
|
|
881
|
-
this.#status.lastRejectedAt = new Date().toISOString();
|
|
882
874
|
}
|
|
883
875
|
cursor = update.update_id + 1;
|
|
884
876
|
await this.#state.setCursor(cursor);
|
|
@@ -51,6 +51,10 @@ import {
|
|
|
51
51
|
messageFailureText,
|
|
52
52
|
setLastMessageFailure,
|
|
53
53
|
} from '../shared/message-failure.mjs';
|
|
54
|
+
import {
|
|
55
|
+
COMMAND_PERMISSION_DENIED_MESSAGE,
|
|
56
|
+
evaluateInboundAccess,
|
|
57
|
+
} from '../shared/inbound-access.mjs';
|
|
54
58
|
import { t } from '../shared/i18n.mjs';
|
|
55
59
|
|
|
56
60
|
const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
|
@@ -490,6 +494,7 @@ export class WecomHarnessBridge {
|
|
|
490
494
|
#harness;
|
|
491
495
|
#state;
|
|
492
496
|
#contextEnhancement;
|
|
497
|
+
#accessPolicy;
|
|
493
498
|
#status;
|
|
494
499
|
#logger;
|
|
495
500
|
#replyTimeoutMs;
|
|
@@ -512,6 +517,7 @@ export class WecomHarnessBridge {
|
|
|
512
517
|
harness,
|
|
513
518
|
state,
|
|
514
519
|
contextEnhancement,
|
|
520
|
+
accessPolicy,
|
|
515
521
|
status = createWecomBridgeStatus(),
|
|
516
522
|
logger = console,
|
|
517
523
|
replyTimeoutMs = 600_000,
|
|
@@ -530,6 +536,7 @@ export class WecomHarnessBridge {
|
|
|
530
536
|
this.#harness = harness;
|
|
531
537
|
this.#state = state;
|
|
532
538
|
this.#contextEnhancement = contextEnhancement;
|
|
539
|
+
this.#accessPolicy = accessPolicy;
|
|
533
540
|
this.#status = status;
|
|
534
541
|
this.#logger = logger;
|
|
535
542
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
@@ -557,16 +564,28 @@ export class WecomHarnessBridge {
|
|
|
557
564
|
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|
558
565
|
|
|
559
566
|
const key = conversationKey(frame);
|
|
567
|
+
const pending = this.#pendingInteractions.get(key);
|
|
568
|
+
const commandMessage = wecomInboundMessage(frame, this.#client);
|
|
569
|
+
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
570
|
+
const conversationType = body.chattype === 'single' ? 'direct' : 'group';
|
|
571
|
+
const access = evaluateInboundAccess(this.#accessPolicy, {
|
|
572
|
+
conversationType,
|
|
573
|
+
senderIds: senderId,
|
|
574
|
+
text: commandText,
|
|
575
|
+
hasImages: hasInboundImages(commandMessage),
|
|
576
|
+
hasFiles: hasInboundFiles(commandMessage),
|
|
577
|
+
});
|
|
578
|
+
if (!access.allowed) {
|
|
579
|
+
this.#acceptedMessageIds.set(messageId, null);
|
|
580
|
+
return this.#finishAccessDecision(frame, messageId, chatId, access);
|
|
581
|
+
}
|
|
560
582
|
this.#acceptedMessageIds.set(messageId, captureContextEnhancement(
|
|
561
583
|
this.#contextEnhancement,
|
|
562
|
-
|
|
584
|
+
conversationType,
|
|
563
585
|
));
|
|
564
586
|
if (body.chattype === 'single') {
|
|
565
587
|
rememberConnectionTestTarget(this.#state, { chatId });
|
|
566
588
|
}
|
|
567
|
-
const pending = this.#pendingInteractions.get(key);
|
|
568
|
-
const commandMessage = wecomInboundMessage(frame, this.#client);
|
|
569
|
-
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
570
589
|
const batchCommand = isBatchInputCommand(commandText);
|
|
571
590
|
const batchStatus = this.#batchInputs.status(key);
|
|
572
591
|
if (batchCommand && body.chattype === 'group') {
|
|
@@ -760,6 +779,34 @@ export class WecomHarnessBridge {
|
|
|
760
779
|
return task;
|
|
761
780
|
}
|
|
762
781
|
|
|
782
|
+
#finishAccessDecision(frame, messageId, chatId, access) {
|
|
783
|
+
let task;
|
|
784
|
+
task = Promise.resolve().then(async () => {
|
|
785
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
786
|
+
await this.#state.markSeen(messageId);
|
|
787
|
+
if (access.reason === 'command-not-allowed') {
|
|
788
|
+
this.#status.messagesReceived += 1;
|
|
789
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
790
|
+
await this.#sendImmediate(frame, chatId, t(COMMAND_PERMISSION_DENIED_MESSAGE));
|
|
791
|
+
this.#status.messagesReplied += 1;
|
|
792
|
+
this.#status.lastReplyAt = new Date().toISOString();
|
|
793
|
+
} else {
|
|
794
|
+
this.#status.messagesRejected += 1;
|
|
795
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
796
|
+
}
|
|
797
|
+
this.#status.lastError = null;
|
|
798
|
+
}).catch((error) => {
|
|
799
|
+
if (this.#signal?.aborted) return;
|
|
800
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
801
|
+
this.#logger.error?.('[dsh-im:wecom] failed to apply inbound access policy', error);
|
|
802
|
+
}).finally(() => {
|
|
803
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
804
|
+
this.#commandTasks.delete(task);
|
|
805
|
+
});
|
|
806
|
+
this.#commandTasks.add(task);
|
|
807
|
+
return task;
|
|
808
|
+
}
|
|
809
|
+
|
|
763
810
|
async waitForIdle() {
|
|
764
811
|
await Promise.allSettled([
|
|
765
812
|
...this.#queues.values(),
|
|
@@ -29,6 +29,7 @@ export class WecomRuntime {
|
|
|
29
29
|
#harness;
|
|
30
30
|
#state;
|
|
31
31
|
#contextEnhancement;
|
|
32
|
+
#accessPolicy;
|
|
32
33
|
#logger;
|
|
33
34
|
#replyTimeoutMs;
|
|
34
35
|
#connectTimeoutMs;
|
|
@@ -47,6 +48,7 @@ export class WecomRuntime {
|
|
|
47
48
|
harness,
|
|
48
49
|
state,
|
|
49
50
|
contextEnhancement,
|
|
51
|
+
accessPolicy,
|
|
50
52
|
logger = console,
|
|
51
53
|
replyTimeoutMs = 600_000,
|
|
52
54
|
connectTimeoutMs = 20_000,
|
|
@@ -61,6 +63,7 @@ export class WecomRuntime {
|
|
|
61
63
|
this.#harness = harness;
|
|
62
64
|
this.#state = state;
|
|
63
65
|
this.#contextEnhancement = contextEnhancement;
|
|
66
|
+
this.#accessPolicy = accessPolicy;
|
|
64
67
|
this.#logger = logger;
|
|
65
68
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
66
69
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
@@ -111,6 +114,7 @@ export class WecomRuntime {
|
|
|
111
114
|
harness: this.#harness,
|
|
112
115
|
state: this.#state,
|
|
113
116
|
contextEnhancement: this.#contextEnhancement,
|
|
117
|
+
accessPolicy: this.#accessPolicy,
|
|
114
118
|
status: this.#status,
|
|
115
119
|
logger: this.#logger,
|
|
116
120
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
@@ -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('这个问题已在其他客户端处理,无需再次回答。');
|
|
@@ -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 (
|
|
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
|
|
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 =
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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;
|
|
@@ -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,
|