@qwen-code/channel-base 0.19.7 → 0.19.8
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/AcpBridge.d.ts +6 -0
- package/dist/AcpBridge.js +75 -10
- package/dist/AcpBridge.js.map +1 -1
- package/dist/ChannelAgentBridge.d.ts +13 -0
- package/dist/ChannelBase.d.ts +24 -1
- package/dist/ChannelBase.js +326 -0
- package/dist/ChannelBase.js.map +1 -1
- package/dist/DmGate.d.ts +19 -0
- package/dist/DmGate.js +25 -0
- package/dist/DmGate.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/ChannelBase.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { basename, join } from 'node:path';
|
|
2
2
|
import { BlockStreamer } from './BlockStreamer.js';
|
|
3
3
|
import { GroupGate } from './GroupGate.js';
|
|
4
|
+
import { DmGate } from './DmGate.js';
|
|
4
5
|
import { GroupHistoryStore } from './group-history-store.js';
|
|
5
6
|
import { SenderGate } from './SenderGate.js';
|
|
6
7
|
import { PairingStore } from './PairingStore.js';
|
|
@@ -53,6 +54,7 @@ export class ChannelBase {
|
|
|
53
54
|
config;
|
|
54
55
|
bridge;
|
|
55
56
|
groupGate;
|
|
57
|
+
dmGate;
|
|
56
58
|
gate;
|
|
57
59
|
router;
|
|
58
60
|
name;
|
|
@@ -88,6 +90,16 @@ export class ChannelBase {
|
|
|
88
90
|
bridgeSessionDiedListener = (event) => {
|
|
89
91
|
this.onSessionDied(event.sessionId);
|
|
90
92
|
};
|
|
93
|
+
bridgePermissionRequestListener = (event) => {
|
|
94
|
+
void this.dispatchPermissionRequest(event).catch((err) => {
|
|
95
|
+
process.stderr.write(`[${this.name}] permission relay failed for request ${sanitizeLogText(event.requestId, 128)}: ${this.lifecycleError(err)}\n`);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
bridgePermissionResolvedListener = (event) => {
|
|
99
|
+
this.dispatchPermissionResolved(event);
|
|
100
|
+
};
|
|
101
|
+
pendingPermissions = new Map();
|
|
102
|
+
pendingPermissionsByChat = new Map();
|
|
91
103
|
channelLoopToolHandler = {
|
|
92
104
|
canHandle: (sessionId) => this.router.getTarget(sessionId)?.channelName === this.name,
|
|
93
105
|
create: (sessionId, input) => this.createLoopFromTool(sessionId, input),
|
|
@@ -119,6 +131,83 @@ export class ChannelBase {
|
|
|
119
131
|
}
|
|
120
132
|
this.onToolCall(chatId, event);
|
|
121
133
|
}
|
|
134
|
+
async dispatchPermissionRequest(event) {
|
|
135
|
+
const target = this.permissionTargetForEvent(event);
|
|
136
|
+
if (!target) {
|
|
137
|
+
try {
|
|
138
|
+
await this.bridge.respondToPermission?.(event.requestId, {
|
|
139
|
+
outcome: { outcome: 'cancelled' },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (respondErr) {
|
|
143
|
+
process.stderr.write(`[${this.name}] permission cancellation failed for request ${sanitizeLogText(event.requestId, 128)}: ${this.lifecycleError(respondErr)}\n`);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.removePendingPermission(event.requestId);
|
|
148
|
+
const pending = {
|
|
149
|
+
requestId: event.requestId,
|
|
150
|
+
sessionId: event.sessionId,
|
|
151
|
+
target,
|
|
152
|
+
request: event.request,
|
|
153
|
+
};
|
|
154
|
+
this.pendingPermissions.set(event.requestId, pending);
|
|
155
|
+
const chatKey = this.permissionChatKey(target);
|
|
156
|
+
const requestIds = this.pendingPermissionsByChat.get(chatKey) ?? [];
|
|
157
|
+
requestIds.push(event.requestId);
|
|
158
|
+
this.pendingPermissionsByChat.set(chatKey, requestIds);
|
|
159
|
+
try {
|
|
160
|
+
const text = this.formatPermissionRequest(pending);
|
|
161
|
+
if (target.threadId !== undefined &&
|
|
162
|
+
this.supportsProactiveSend() &&
|
|
163
|
+
this.supportsProactiveTarget(target)) {
|
|
164
|
+
await this.pushProactive(target, text);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
await this.sendMessage(target.chatId, text);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
this.removePendingPermission(event.requestId);
|
|
172
|
+
try {
|
|
173
|
+
await this.bridge.respondToPermission?.(event.requestId, {
|
|
174
|
+
outcome: { outcome: 'cancelled' },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (respondErr) {
|
|
178
|
+
process.stderr.write(`[${this.name}] permission cancellation failed for request ${sanitizeLogText(event.requestId, 128)}: ${this.lifecycleError(respondErr)}\n`);
|
|
179
|
+
}
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
permissionTargetForEvent(event) {
|
|
184
|
+
const routeTarget = this.router.getTarget(event.sessionId);
|
|
185
|
+
if (!routeTarget || routeTarget.channelName !== this.name) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const active = this.activePrompts.get(event.sessionId);
|
|
189
|
+
if (!active) {
|
|
190
|
+
return routeTarget;
|
|
191
|
+
}
|
|
192
|
+
const target = {
|
|
193
|
+
channelName: routeTarget.channelName,
|
|
194
|
+
senderId: active.senderId ?? routeTarget.senderId,
|
|
195
|
+
chatId: active.chatId,
|
|
196
|
+
};
|
|
197
|
+
if (active.threadId !== undefined) {
|
|
198
|
+
target.threadId = active.threadId;
|
|
199
|
+
}
|
|
200
|
+
if (active.isGroup !== undefined) {
|
|
201
|
+
target.isGroup = active.isGroup;
|
|
202
|
+
}
|
|
203
|
+
else if (routeTarget.isGroup !== undefined) {
|
|
204
|
+
target.isGroup = routeTarget.isGroup;
|
|
205
|
+
}
|
|
206
|
+
return target;
|
|
207
|
+
}
|
|
208
|
+
dispatchPermissionResolved(event) {
|
|
209
|
+
this.removePendingPermission(event.requestId);
|
|
210
|
+
}
|
|
122
211
|
constructor(name, config, bridge, options) {
|
|
123
212
|
this.name = name;
|
|
124
213
|
this.config = config;
|
|
@@ -132,6 +221,7 @@ export class ChannelBase {
|
|
|
132
221
|
join(getGlobalQwenDir(), 'channels', `${encodeURIComponent(name)}-group-history.jsonl`));
|
|
133
222
|
this.loopController = options?.loopController;
|
|
134
223
|
this.groupGate = new GroupGate(config.groupPolicy, config.groups);
|
|
224
|
+
this.dmGate = new DmGate(config.dmPolicy);
|
|
135
225
|
const pairingStore = config.senderPolicy === 'pairing' ? new PairingStore(name) : undefined;
|
|
136
226
|
this.gate = new SenderGate(config.senderPolicy, config.allowedUsers, pairingStore);
|
|
137
227
|
this.router =
|
|
@@ -261,6 +351,7 @@ export class ChannelBase {
|
|
|
261
351
|
if (this.registerBridgeEvents) {
|
|
262
352
|
this.detachBridgeEvents(this.bridge);
|
|
263
353
|
}
|
|
354
|
+
this.clearPendingPermissions();
|
|
264
355
|
this.router.setBridge(bridge);
|
|
265
356
|
this.bridge = bridge;
|
|
266
357
|
if (this.loopController) {
|
|
@@ -360,6 +451,8 @@ export class ChannelBase {
|
|
|
360
451
|
done,
|
|
361
452
|
resolve: doneResolve,
|
|
362
453
|
chatId: job.target.chatId,
|
|
454
|
+
threadId: job.target.threadId,
|
|
455
|
+
isGroup: job.target.isGroup,
|
|
363
456
|
messageId: job.id,
|
|
364
457
|
senderId: job.target.senderId,
|
|
365
458
|
senderName: job.createdBy,
|
|
@@ -667,14 +760,19 @@ export class ChannelBase {
|
|
|
667
760
|
onSessionDied(sessionId) {
|
|
668
761
|
this.router.removeSessionId(sessionId);
|
|
669
762
|
this.instructedSessions.delete(sessionId);
|
|
763
|
+
this.removePendingPermissionsForSession(sessionId);
|
|
670
764
|
}
|
|
671
765
|
attachBridgeEvents(bridge) {
|
|
672
766
|
bridge.on('toolCall', this.bridgeToolCallListener);
|
|
673
767
|
bridge.on('sessionDied', this.bridgeSessionDiedListener);
|
|
768
|
+
bridge.on('permissionRequest', this.bridgePermissionRequestListener);
|
|
769
|
+
bridge.on('permissionResolved', this.bridgePermissionResolvedListener);
|
|
674
770
|
}
|
|
675
771
|
detachBridgeEvents(bridge) {
|
|
676
772
|
bridge.off('toolCall', this.bridgeToolCallListener);
|
|
677
773
|
bridge.off('sessionDied', this.bridgeSessionDiedListener);
|
|
774
|
+
bridge.off('permissionRequest', this.bridgePermissionRequestListener);
|
|
775
|
+
bridge.off('permissionResolved', this.bridgePermissionResolvedListener);
|
|
678
776
|
}
|
|
679
777
|
/**
|
|
680
778
|
* Called when a prompt actually begins processing (inside the session queue).
|
|
@@ -741,6 +839,217 @@ export class ChannelBase {
|
|
|
741
839
|
return true;
|
|
742
840
|
});
|
|
743
841
|
}
|
|
842
|
+
permissionChatKey(target) {
|
|
843
|
+
return `${target.chatId}\0${target.threadId ?? ''}`;
|
|
844
|
+
}
|
|
845
|
+
pendingPermissionIdsForChatKey(chatKey) {
|
|
846
|
+
const requestIds = this.pendingPermissionsByChat.get(chatKey);
|
|
847
|
+
if (!requestIds) {
|
|
848
|
+
return [];
|
|
849
|
+
}
|
|
850
|
+
const live = requestIds.filter((id) => this.pendingPermissions.has(id));
|
|
851
|
+
if (live.length === 0) {
|
|
852
|
+
this.pendingPermissionsByChat.delete(chatKey);
|
|
853
|
+
}
|
|
854
|
+
else if (live.length !== requestIds.length) {
|
|
855
|
+
this.pendingPermissionsByChat.set(chatKey, live);
|
|
856
|
+
}
|
|
857
|
+
return live;
|
|
858
|
+
}
|
|
859
|
+
removePendingPermission(requestId) {
|
|
860
|
+
const pending = this.pendingPermissions.get(requestId);
|
|
861
|
+
if (!pending) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
this.pendingPermissions.delete(requestId);
|
|
865
|
+
const chatKey = this.permissionChatKey(pending.target);
|
|
866
|
+
const requestIds = this.pendingPermissionsByChat.get(chatKey);
|
|
867
|
+
if (!requestIds) {
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const remaining = requestIds.filter((id) => id !== requestId);
|
|
871
|
+
if (remaining.length === 0) {
|
|
872
|
+
this.pendingPermissionsByChat.delete(chatKey);
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
this.pendingPermissionsByChat.set(chatKey, remaining);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
removePendingPermissionsForSession(sessionId) {
|
|
879
|
+
const requestIds = Array.from(this.pendingPermissions)
|
|
880
|
+
.filter(([, pending]) => pending.sessionId === sessionId)
|
|
881
|
+
.map(([requestId]) => requestId);
|
|
882
|
+
for (const requestId of requestIds) {
|
|
883
|
+
this.removePendingPermission(requestId);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
clearPendingPermissions() {
|
|
887
|
+
this.pendingPermissions.clear();
|
|
888
|
+
this.pendingPermissionsByChat.clear();
|
|
889
|
+
}
|
|
890
|
+
pendingPermissionForEnvelope(envelope, args) {
|
|
891
|
+
const trimmed = args.trim();
|
|
892
|
+
if (trimmed) {
|
|
893
|
+
const explicit = this.pendingPermissions.get(trimmed);
|
|
894
|
+
if (explicit &&
|
|
895
|
+
this.canEnvelopeAnswerPendingPermission(envelope, explicit)) {
|
|
896
|
+
return { kind: 'found', pending: explicit };
|
|
897
|
+
}
|
|
898
|
+
return { kind: 'none', explicit: true };
|
|
899
|
+
}
|
|
900
|
+
const requestIds = this.pendingPermissionIdsForChatKey(this.permissionChatKey(envelope));
|
|
901
|
+
if (requestIds.length === 0) {
|
|
902
|
+
return { kind: 'none', explicit: false };
|
|
903
|
+
}
|
|
904
|
+
const matching = requestIds
|
|
905
|
+
.map((id) => this.pendingPermissions.get(id))
|
|
906
|
+
.filter((pending) => pending !== undefined &&
|
|
907
|
+
this.canEnvelopeAnswerPendingPermission(envelope, pending));
|
|
908
|
+
if (matching.length === 0) {
|
|
909
|
+
return { kind: 'none', explicit: false };
|
|
910
|
+
}
|
|
911
|
+
if (matching.length > 1) {
|
|
912
|
+
return {
|
|
913
|
+
kind: 'ambiguous',
|
|
914
|
+
requestIds: matching.map((pending) => pending.requestId),
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return { kind: 'found', pending: matching[0] };
|
|
918
|
+
}
|
|
919
|
+
canEnvelopeAnswerPendingPermission(envelope, pending) {
|
|
920
|
+
return (pending.target.chatId === envelope.chatId &&
|
|
921
|
+
pending.target.threadId === envelope.threadId &&
|
|
922
|
+
(this.isSharedSessionTarget(pending.target) ||
|
|
923
|
+
pending.target.senderId === envelope.senderId));
|
|
924
|
+
}
|
|
925
|
+
formatPermissionRequest(pending) {
|
|
926
|
+
const { toolCall } = pending.request;
|
|
927
|
+
const title = sanitizeQuotedText(toolCall.title || 'Tool use', 160);
|
|
928
|
+
const alwaysOption = this.approvalAlwaysOption(pending);
|
|
929
|
+
const replies = [
|
|
930
|
+
'/approve allow once',
|
|
931
|
+
...(alwaysOption ? [`/approve-always ${alwaysOption.label}`] : []),
|
|
932
|
+
'/deny deny',
|
|
933
|
+
];
|
|
934
|
+
const lines = [
|
|
935
|
+
'Permission required to run a tool',
|
|
936
|
+
'',
|
|
937
|
+
'Command:',
|
|
938
|
+
title,
|
|
939
|
+
'',
|
|
940
|
+
'Reply with:',
|
|
941
|
+
...replies,
|
|
942
|
+
];
|
|
943
|
+
return lines.join('\n');
|
|
944
|
+
}
|
|
945
|
+
approvalOptionId(pending) {
|
|
946
|
+
const options = pending.request.options;
|
|
947
|
+
return (options.find((option) => option.kind === 'allow_once')?.optionId ??
|
|
948
|
+
options.find((option) => option.optionId === 'proceed_once' &&
|
|
949
|
+
option.kind === undefined)?.optionId);
|
|
950
|
+
}
|
|
951
|
+
approvalAlwaysOption(pending) {
|
|
952
|
+
const options = pending.request.options.filter((option) => option.kind === 'allow_always');
|
|
953
|
+
const option = this.findScopedAlwaysOption(options, 'project') ??
|
|
954
|
+
this.findScopedAlwaysOption(options, 'user') ??
|
|
955
|
+
options[0];
|
|
956
|
+
if (!option) {
|
|
957
|
+
return undefined;
|
|
958
|
+
}
|
|
959
|
+
return {
|
|
960
|
+
optionId: option.optionId,
|
|
961
|
+
label: this.approvalAlwaysLabel(option),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
findScopedAlwaysOption(options, scope) {
|
|
965
|
+
return options.find((option) => option.optionId === `proceed_always_${scope}`);
|
|
966
|
+
}
|
|
967
|
+
approvalAlwaysLabel(option) {
|
|
968
|
+
if (option.optionId === 'proceed_always_project') {
|
|
969
|
+
return 'always allow for this project';
|
|
970
|
+
}
|
|
971
|
+
if (option.optionId === 'proceed_always_user') {
|
|
972
|
+
return 'always allow for this user';
|
|
973
|
+
}
|
|
974
|
+
return 'always allow';
|
|
975
|
+
}
|
|
976
|
+
denialResponse(pending) {
|
|
977
|
+
const option = pending.request.options.find((candidate) => candidate.kind === 'reject_once') ??
|
|
978
|
+
pending.request.options.find((candidate) => candidate.optionId === 'cancel' &&
|
|
979
|
+
candidate.kind === undefined);
|
|
980
|
+
if (option) {
|
|
981
|
+
return { outcome: { outcome: 'selected', optionId: option.optionId } };
|
|
982
|
+
}
|
|
983
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
984
|
+
}
|
|
985
|
+
async handlePermissionResponseCommand(envelope, args, decision) {
|
|
986
|
+
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
987
|
+
await this.sendMessage(envelope.chatId, 'Only authorized members can answer permission requests in this shared session.');
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
const lookup = this.pendingPermissionForEnvelope(envelope, args);
|
|
991
|
+
if (lookup.kind === 'ambiguous') {
|
|
992
|
+
const requestList = lookup.requestIds
|
|
993
|
+
.slice(0, 6)
|
|
994
|
+
.map((id) => {
|
|
995
|
+
const pending = this.pendingPermissions.get(id);
|
|
996
|
+
const title = pending
|
|
997
|
+
? `: ${sanitizeQuotedText(pending.request.toolCall.title || 'Tool use', 160)}`
|
|
998
|
+
: '';
|
|
999
|
+
return `- ${sanitizeQuotedText(id, 128)}${title}`;
|
|
1000
|
+
})
|
|
1001
|
+
.join('\n');
|
|
1002
|
+
await this.sendMessage(envelope.chatId, `Multiple permission requests are pending for this chat. Reply with /${decision} <request-id>.\n${requestList}`);
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
if (lookup.kind === 'none') {
|
|
1006
|
+
await this.sendMessage(envelope.chatId, lookup.explicit
|
|
1007
|
+
? 'No pending permission request with that id for this chat.'
|
|
1008
|
+
: 'No pending permission request for this chat.');
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
if (!this.bridge.respondToPermission) {
|
|
1012
|
+
await this.sendMessage(envelope.chatId, 'Permission relay is not available for this session.');
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
const { pending } = lookup;
|
|
1016
|
+
const response = (() => {
|
|
1017
|
+
if (decision === 'deny') {
|
|
1018
|
+
return this.denialResponse(pending);
|
|
1019
|
+
}
|
|
1020
|
+
const optionId = decision === 'approve'
|
|
1021
|
+
? this.approvalOptionId(pending)
|
|
1022
|
+
: this.approvalAlwaysOption(pending)?.optionId;
|
|
1023
|
+
return optionId
|
|
1024
|
+
? { outcome: { outcome: 'selected', optionId } }
|
|
1025
|
+
: undefined;
|
|
1026
|
+
})();
|
|
1027
|
+
if (!response) {
|
|
1028
|
+
await this.sendMessage(envelope.chatId, decision === 'approve-always'
|
|
1029
|
+
? 'This permission request has no always-allow option.'
|
|
1030
|
+
: 'This permission request has no approvable option.');
|
|
1031
|
+
return true;
|
|
1032
|
+
}
|
|
1033
|
+
let accepted;
|
|
1034
|
+
try {
|
|
1035
|
+
accepted = await this.bridge.respondToPermission(pending.requestId, response);
|
|
1036
|
+
}
|
|
1037
|
+
catch (err) {
|
|
1038
|
+
this.removePendingPermission(pending.requestId);
|
|
1039
|
+
process.stderr.write(`[${this.name}] permission response failed for request ${sanitizeLogText(pending.requestId, 128)}: ${this.lifecycleError(err)}\n`);
|
|
1040
|
+
await this.sendMessage(envelope.chatId, 'Failed to answer the permission request.');
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
this.removePendingPermission(pending.requestId);
|
|
1044
|
+
await this.sendMessage(envelope.chatId, accepted
|
|
1045
|
+
? decision === 'approve'
|
|
1046
|
+
? 'Permission approved.'
|
|
1047
|
+
: decision === 'approve-always'
|
|
1048
|
+
? 'Permission approved always.'
|
|
1049
|
+
: 'Permission denied.'
|
|
1050
|
+
: 'Permission request is no longer pending.');
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
744
1053
|
/** Register shared slash commands. Called from constructor. */
|
|
745
1054
|
registerSharedCommands() {
|
|
746
1055
|
const doClear = async (envelope) => {
|
|
@@ -760,6 +1069,7 @@ export class ChannelBase {
|
|
|
760
1069
|
// already queued onto this session sees a stale generation and bails
|
|
761
1070
|
// instead of running bridge.prompt() against the cleared session.
|
|
762
1071
|
this.sessionGenerations.set(id, (this.sessionGenerations.get(id) ?? 0) + 1);
|
|
1072
|
+
this.removePendingPermissionsForSession(id);
|
|
763
1073
|
// Cancel an in-flight turn (and drop its buffered follow-ups) before
|
|
764
1074
|
// purging, so a running prompt can't deliver a stale response into —
|
|
765
1075
|
// or resurrect via collect-drain — the just-cleared session.
|
|
@@ -858,6 +1168,9 @@ export class ChannelBase {
|
|
|
858
1168
|
this.registerCommand('clear', clearHandler);
|
|
859
1169
|
this.registerCommand('reset', clearHandler);
|
|
860
1170
|
this.registerCommand('new', clearHandler);
|
|
1171
|
+
this.registerCommand('approve', (envelope, args) => this.handlePermissionResponseCommand(envelope, args, 'approve'));
|
|
1172
|
+
this.registerCommand('approve-always', (envelope, args) => this.handlePermissionResponseCommand(envelope, args, 'approve-always'));
|
|
1173
|
+
this.registerCommand('deny', (envelope, args) => this.handlePermissionResponseCommand(envelope, args, 'deny'));
|
|
861
1174
|
this.registerCommand('remember-channel', async (envelope, args) => {
|
|
862
1175
|
const text = args.trim();
|
|
863
1176
|
if (text === '') {
|
|
@@ -936,6 +1249,9 @@ export class ChannelBase {
|
|
|
936
1249
|
: '/clear — Clear your session (aliases: /reset, /new)',
|
|
937
1250
|
'/who — Show current session & workspace',
|
|
938
1251
|
'/status — Show session info',
|
|
1252
|
+
'/approve [request-id] — Approve a pending permission request',
|
|
1253
|
+
'/approve-always [request-id] — Always approve a pending permission request',
|
|
1254
|
+
'/deny [request-id] — Deny a pending permission request',
|
|
939
1255
|
];
|
|
940
1256
|
// Platform-specific commands (registered by adapters, not shared ones)
|
|
941
1257
|
const sharedCmds = new Set([
|
|
@@ -943,6 +1259,9 @@ export class ChannelBase {
|
|
|
943
1259
|
'clear',
|
|
944
1260
|
'reset',
|
|
945
1261
|
'new',
|
|
1262
|
+
'approve',
|
|
1263
|
+
'approve-always',
|
|
1264
|
+
'deny',
|
|
946
1265
|
'remember-channel',
|
|
947
1266
|
'channel-memory',
|
|
948
1267
|
'forget-channel',
|
|
@@ -1309,6 +1628,7 @@ export class ChannelBase {
|
|
|
1309
1628
|
isReplyToBot: true,
|
|
1310
1629
|
};
|
|
1311
1630
|
return (this.groupGate.check(envelope).allowed &&
|
|
1631
|
+
this.dmGate.check(envelope).allowed &&
|
|
1312
1632
|
this.gate.isAllowed(normalizedTarget.senderId) &&
|
|
1313
1633
|
this.isAuthorizedForSharedSession(envelope));
|
|
1314
1634
|
}
|
|
@@ -1800,6 +2120,10 @@ export class ChannelBase {
|
|
|
1800
2120
|
}
|
|
1801
2121
|
return false;
|
|
1802
2122
|
}
|
|
2123
|
+
const dmResult = this.dmGate.check(envelope);
|
|
2124
|
+
if (!dmResult.allowed) {
|
|
2125
|
+
return false;
|
|
2126
|
+
}
|
|
1803
2127
|
const result = this.gate.check(envelope.senderId, envelope.senderName);
|
|
1804
2128
|
if (!result.allowed) {
|
|
1805
2129
|
if (result.pairingCode !== undefined) {
|
|
@@ -2170,6 +2494,8 @@ export class ChannelBase {
|
|
|
2170
2494
|
done,
|
|
2171
2495
|
resolve: doneResolve,
|
|
2172
2496
|
chatId: envelope.chatId,
|
|
2497
|
+
threadId: envelope.threadId,
|
|
2498
|
+
isGroup: envelope.isGroup,
|
|
2173
2499
|
messageId: envelope.messageId,
|
|
2174
2500
|
senderId: envelope.senderId,
|
|
2175
2501
|
senderName: envelope.senderName,
|