@xmanrui/dsh-im 4.3.0 → 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/lib/client.js +951 -626
- package/lib/index.js +233 -233
- 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/delivery-settings.js +61 -9
- package/plugin-src/client/i18n.js +56 -1
- package/plugin-src/client/index.js +1 -0
- package/plugin-src/client/styles.js +50 -4
- 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/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 +188 -80
- package/src/channels/feishu/feishu-runtime.mjs +4 -0
- package/src/channels/qq/qq-bridge.mjs +59 -17
- package/src/channels/qq/qq-runtime.mjs +20 -2
- 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/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
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
normalizeAgentPresetCatalog,
|
|
14
14
|
validateAgentPresetId,
|
|
15
15
|
} from './agent-preset.mjs';
|
|
16
|
+
import {
|
|
17
|
+
normalizeAccessPolicy,
|
|
18
|
+
validateAccessPolicy,
|
|
19
|
+
} from './access-policy.mjs';
|
|
16
20
|
import { CONNECTION_TEST_STATE_IDENTITY } from './connection-test.mjs';
|
|
17
21
|
import {
|
|
18
22
|
DEFAULT_CONTEXT_ENHANCEMENT_CONFIG,
|
|
@@ -131,6 +135,26 @@ function normalizeDeliveryTargets(value) {
|
|
|
131
135
|
return deliveryTargets;
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
function normalizeAccessPolicies(value, workspaces) {
|
|
139
|
+
const accessPolicies = Object.create(null);
|
|
140
|
+
if (value === undefined) return accessPolicies;
|
|
141
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
142
|
+
// Preserve the distinction between a missing policy (eligible for startup
|
|
143
|
+
// initialization) and damaged persisted data (fail closed).
|
|
144
|
+
for (const botId of Object.keys(workspaces)) accessPolicies[botId] = null;
|
|
145
|
+
return accessPolicies;
|
|
146
|
+
}
|
|
147
|
+
for (const [botId, policy] of Object.entries(value)) {
|
|
148
|
+
try {
|
|
149
|
+
botIdOf(botId);
|
|
150
|
+
accessPolicies[botId] = normalizeAccessPolicy(policy);
|
|
151
|
+
} catch {
|
|
152
|
+
// An invalid key cannot identify a bot, so it is isolated and ignored.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return accessPolicies;
|
|
156
|
+
}
|
|
157
|
+
|
|
134
158
|
function normalizeDocument(value) {
|
|
135
159
|
if (!value || ![1, 2].includes(value.version) || !value.workspaces
|
|
136
160
|
|| typeof value.workspaces !== 'object' || Array.isArray(value.workspaces)) return null;
|
|
@@ -168,15 +192,52 @@ function normalizeDocument(value) {
|
|
|
168
192
|
if (value.version === 1 && value.deliveryTargets !== undefined) return null;
|
|
169
193
|
const deliveryTargets = normalizeDeliveryTargets(value.deliveryTargets);
|
|
170
194
|
if (!deliveryTargets) return null;
|
|
195
|
+
const accessPolicies = normalizeAccessPolicies(value.accessPolicies, workspaces);
|
|
196
|
+
const version = value.accessPolicies === undefined ? value.version : 2;
|
|
171
197
|
return {
|
|
172
|
-
|
|
198
|
+
// A v1 file cannot be emitted with this optional v2 section. If one is
|
|
199
|
+
// recovered from an interrupted/manual edit, retain it on the next write.
|
|
200
|
+
version,
|
|
173
201
|
workspaces,
|
|
174
202
|
agentPresets,
|
|
175
203
|
contextEnhancement,
|
|
176
204
|
deliveryTargets,
|
|
205
|
+
accessPolicies,
|
|
177
206
|
};
|
|
178
207
|
}
|
|
179
208
|
|
|
209
|
+
function storedDocument({
|
|
210
|
+
version,
|
|
211
|
+
workspaces,
|
|
212
|
+
agentPresets,
|
|
213
|
+
contextEnhancement,
|
|
214
|
+
deliveryTargets,
|
|
215
|
+
accessPolicies,
|
|
216
|
+
}) {
|
|
217
|
+
const document = { version, workspaces };
|
|
218
|
+
if (Object.keys(agentPresets).length > 0) document.agentPresets = agentPresets;
|
|
219
|
+
if (Object.keys(contextEnhancement).length > 0) {
|
|
220
|
+
document.contextEnhancement = contextEnhancement;
|
|
221
|
+
}
|
|
222
|
+
if (version >= 2 && Object.keys(deliveryTargets).length > 0) {
|
|
223
|
+
document.deliveryTargets = deliveryTargets;
|
|
224
|
+
}
|
|
225
|
+
if (version >= 2 && Object.keys(accessPolicies).length > 0) {
|
|
226
|
+
document.accessPolicies = accessPolicies;
|
|
227
|
+
}
|
|
228
|
+
return document;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function writeStoredDocument(path, document) {
|
|
232
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
233
|
+
const temporary = `${path}.tmp`;
|
|
234
|
+
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
|
|
235
|
+
encoding: 'utf8',
|
|
236
|
+
mode: 0o600,
|
|
237
|
+
});
|
|
238
|
+
await rename(temporary, path);
|
|
239
|
+
}
|
|
240
|
+
|
|
180
241
|
export async function validateWorkspacePath(value) {
|
|
181
242
|
if (typeof value !== 'string' || !value.trim() || !isAbsolute(value.trim())) {
|
|
182
243
|
const error = new Error('工作区必须是绝对路径。');
|
|
@@ -208,6 +269,7 @@ export class BotWorkspaceStore {
|
|
|
208
269
|
#agentPresets = {};
|
|
209
270
|
#contextEnhancement = {};
|
|
210
271
|
#deliveryTargets = Object.create(null);
|
|
272
|
+
#accessPolicies = Object.create(null);
|
|
211
273
|
#generations = new Map();
|
|
212
274
|
#nextGeneration = 1;
|
|
213
275
|
#incarnations = new Map();
|
|
@@ -233,6 +295,7 @@ export class BotWorkspaceStore {
|
|
|
233
295
|
this.#agentPresets = normalized.agentPresets;
|
|
234
296
|
this.#contextEnhancement = normalized.contextEnhancement;
|
|
235
297
|
this.#deliveryTargets = normalized.deliveryTargets;
|
|
298
|
+
this.#accessPolicies = normalized.accessPolicies;
|
|
236
299
|
} catch (error) {
|
|
237
300
|
if (error?.code !== 'ENOENT') throw error;
|
|
238
301
|
this.#version = 1;
|
|
@@ -240,6 +303,7 @@ export class BotWorkspaceStore {
|
|
|
240
303
|
this.#agentPresets = {};
|
|
241
304
|
this.#contextEnhancement = {};
|
|
242
305
|
this.#deliveryTargets = Object.create(null);
|
|
306
|
+
this.#accessPolicies = Object.create(null);
|
|
243
307
|
}
|
|
244
308
|
this.#generations.clear();
|
|
245
309
|
this.#nextGeneration = 1;
|
|
@@ -278,6 +342,13 @@ export class BotWorkspaceStore {
|
|
|
278
342
|
: DEFAULT_CONTEXT_ENHANCEMENT_CONFIG;
|
|
279
343
|
}
|
|
280
344
|
|
|
345
|
+
accessPolicyFor(botId) {
|
|
346
|
+
const id = botIdOf(botId);
|
|
347
|
+
return this.has(id) && Object.hasOwn(this.#accessPolicies, id)
|
|
348
|
+
? this.#accessPolicies[id]
|
|
349
|
+
: null;
|
|
350
|
+
}
|
|
351
|
+
|
|
281
352
|
listDeliveryTargets(botId) {
|
|
282
353
|
const id = botIdOf(botId);
|
|
283
354
|
if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
|
|
@@ -373,28 +444,53 @@ export class BotWorkspaceStore {
|
|
|
373
444
|
}
|
|
374
445
|
}
|
|
375
446
|
|
|
376
|
-
async ensure(botId, {
|
|
447
|
+
async ensure(botId, {
|
|
448
|
+
workspace = this.#defaultWorkspace,
|
|
449
|
+
defaultAgentPreset,
|
|
450
|
+
initialAccessPolicy,
|
|
451
|
+
} = {}) {
|
|
377
452
|
const id = botIdOf(botId);
|
|
378
453
|
const initialWorkspace = resolve(workspace);
|
|
379
454
|
return this.#enqueue(id, async () => {
|
|
380
|
-
|
|
381
|
-
|
|
455
|
+
const createsBot = !this.#workspaces[id];
|
|
456
|
+
const initializesAccessPolicy = initialAccessPolicy !== undefined
|
|
457
|
+
&& !Object.hasOwn(this.#accessPolicies, id);
|
|
458
|
+
if (createsBot || initializesAccessPolicy) {
|
|
459
|
+
const accessPolicy = initializesAccessPolicy
|
|
460
|
+
? validateAccessPolicy(initialAccessPolicy)
|
|
461
|
+
: undefined;
|
|
462
|
+
const agentPreset = createsBot ? validateAgentPresetId(defaultAgentPreset) : null;
|
|
382
463
|
const hadAgentPreset = Object.hasOwn(this.#agentPresets, id);
|
|
383
464
|
const previousAgentPreset = this.#agentPresets[id];
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
465
|
+
const nextAccessPolicies = initializesAccessPolicy
|
|
466
|
+
? { ...this.#accessPolicies, [id]: accessPolicy }
|
|
467
|
+
: this.#accessPolicies;
|
|
468
|
+
if (createsBot) {
|
|
469
|
+
this.#workspaces[id] = initialWorkspace;
|
|
470
|
+
if (agentPreset) this.#agentPresets[id] = agentPreset;
|
|
471
|
+
this.#generations.set(id, this.#freshGeneration());
|
|
472
|
+
this.#incarnations.set(id, this.#freshIncarnation());
|
|
473
|
+
}
|
|
474
|
+
const nextVersion = initializesAccessPolicy ? 2 : this.#version;
|
|
388
475
|
try {
|
|
389
|
-
await this.#persist(
|
|
476
|
+
await this.#persist(
|
|
477
|
+
this.#contextEnhancement,
|
|
478
|
+
this.#deliveryTargets,
|
|
479
|
+
nextVersion,
|
|
480
|
+
nextAccessPolicies,
|
|
481
|
+
);
|
|
390
482
|
} catch (error) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
483
|
+
if (createsBot) {
|
|
484
|
+
delete this.#workspaces[id];
|
|
485
|
+
if (hadAgentPreset) this.#agentPresets[id] = previousAgentPreset;
|
|
486
|
+
else delete this.#agentPresets[id];
|
|
487
|
+
this.#generations.delete(id);
|
|
488
|
+
this.#incarnations.delete(id);
|
|
489
|
+
}
|
|
396
490
|
throw error;
|
|
397
491
|
}
|
|
492
|
+
this.#accessPolicies = nextAccessPolicies;
|
|
493
|
+
this.#version = nextVersion;
|
|
398
494
|
} else if (!this.#generations.has(id)) {
|
|
399
495
|
this.#generations.set(id, this.#freshGeneration());
|
|
400
496
|
}
|
|
@@ -487,6 +583,30 @@ export class BotWorkspaceStore {
|
|
|
487
583
|
});
|
|
488
584
|
}
|
|
489
585
|
|
|
586
|
+
async setAccessPolicy(botId, value, { incarnation } = {}) {
|
|
587
|
+
const id = botIdOf(botId);
|
|
588
|
+
const expectedIncarnation = incarnation === undefined ? this.incarnationFor(id) : incarnation;
|
|
589
|
+
const policy = validateAccessPolicy(value);
|
|
590
|
+
return this.#enqueue(id, async () => {
|
|
591
|
+
if (!this.has(id) || expectedIncarnation !== this.incarnationFor(id)) {
|
|
592
|
+
const error = new Error('找不到要修改的机器人。');
|
|
593
|
+
error.code = 'workspace-bot-not-found';
|
|
594
|
+
throw error;
|
|
595
|
+
}
|
|
596
|
+
const next = { ...this.#accessPolicies, [id]: policy };
|
|
597
|
+
// Inbound messages keep the previous committed snapshot until rename succeeds.
|
|
598
|
+
await this.#persist(
|
|
599
|
+
this.#contextEnhancement,
|
|
600
|
+
this.#deliveryTargets,
|
|
601
|
+
2,
|
|
602
|
+
next,
|
|
603
|
+
);
|
|
604
|
+
this.#accessPolicies = next;
|
|
605
|
+
this.#version = 2;
|
|
606
|
+
return policy;
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
490
610
|
async bindWorkspaceSession(botId, value, {
|
|
491
611
|
conversationKey,
|
|
492
612
|
sessionId,
|
|
@@ -646,6 +766,7 @@ export class BotWorkspaceStore {
|
|
|
646
766
|
...Object.keys(this.#agentPresets),
|
|
647
767
|
...Object.keys(this.#contextEnhancement),
|
|
648
768
|
...Object.keys(this.#deliveryTargets),
|
|
769
|
+
...Object.keys(this.#accessPolicies),
|
|
649
770
|
...this.#dirtyRemovals,
|
|
650
771
|
]);
|
|
651
772
|
for (const botId of candidates) {
|
|
@@ -663,6 +784,7 @@ export class BotWorkspaceStore {
|
|
|
663
784
|
workspace: this.workspaceFor(bot.botId),
|
|
664
785
|
agentPreset: this.agentPresetFor(bot.botId),
|
|
665
786
|
contextEnhancement: this.contextEnhancementFor(bot.botId),
|
|
787
|
+
accessPolicy: this.accessPolicyFor(bot.botId),
|
|
666
788
|
}
|
|
667
789
|
: bot),
|
|
668
790
|
};
|
|
@@ -694,12 +816,14 @@ export class BotWorkspaceStore {
|
|
|
694
816
|
const hadPreset = Object.hasOwn(this.#agentPresets, id);
|
|
695
817
|
const hadContextEnhancement = Object.hasOwn(this.#contextEnhancement, id);
|
|
696
818
|
const hadDeliveryTargets = Object.hasOwn(this.#deliveryTargets, id);
|
|
819
|
+
const hadAccessPolicy = Object.hasOwn(this.#accessPolicies, id);
|
|
697
820
|
const needsCleanup = hadWorkspace || hadPreset || hadContextEnhancement
|
|
698
|
-
|| hadDeliveryTargets || this.#dirtyRemovals.has(id);
|
|
821
|
+
|| hadDeliveryTargets || hadAccessPolicy || this.#dirtyRemovals.has(id);
|
|
699
822
|
delete this.#workspaces[id];
|
|
700
823
|
delete this.#agentPresets[id];
|
|
701
824
|
delete this.#contextEnhancement[id];
|
|
702
825
|
delete this.#deliveryTargets[id];
|
|
826
|
+
delete this.#accessPolicies[id];
|
|
703
827
|
this.#generations.delete(id);
|
|
704
828
|
this.#incarnations.delete(id);
|
|
705
829
|
if (!needsCleanup) return {
|
|
@@ -733,24 +857,16 @@ export class BotWorkspaceStore {
|
|
|
733
857
|
contextEnhancement = this.#contextEnhancement,
|
|
734
858
|
deliveryTargets = this.#deliveryTargets,
|
|
735
859
|
version = this.#version,
|
|
860
|
+
accessPolicies = this.#accessPolicies,
|
|
736
861
|
) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
document.deliveryTargets = deliveryTargets;
|
|
746
|
-
}
|
|
747
|
-
await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
|
|
748
|
-
const temporary = `${this.#path}.tmp`;
|
|
749
|
-
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
|
|
750
|
-
encoding: 'utf8',
|
|
751
|
-
mode: 0o600,
|
|
752
|
-
});
|
|
753
|
-
await rename(temporary, this.#path);
|
|
862
|
+
await writeStoredDocument(this.#path, storedDocument({
|
|
863
|
+
version,
|
|
864
|
+
workspaces: this.#workspaces,
|
|
865
|
+
agentPresets: this.#agentPresets,
|
|
866
|
+
contextEnhancement,
|
|
867
|
+
deliveryTargets,
|
|
868
|
+
accessPolicies,
|
|
869
|
+
}));
|
|
754
870
|
this.#dirtyRemovals.clear();
|
|
755
871
|
}
|
|
756
872
|
|
|
@@ -758,7 +874,8 @@ export class BotWorkspaceStore {
|
|
|
758
874
|
if (Object.keys(this.#workspaces).length > 0
|
|
759
875
|
|| Object.keys(this.#agentPresets).length > 0
|
|
760
876
|
|| Object.keys(this.#contextEnhancement).length > 0
|
|
761
|
-
|| Object.keys(this.#deliveryTargets).length > 0
|
|
877
|
+
|| Object.keys(this.#deliveryTargets).length > 0
|
|
878
|
+
|| Object.keys(this.#accessPolicies).length > 0) {
|
|
762
879
|
await this.#persist();
|
|
763
880
|
return;
|
|
764
881
|
}
|
|
@@ -1280,6 +1397,31 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
|
|
1280
1397
|
return result;
|
|
1281
1398
|
});
|
|
1282
1399
|
};
|
|
1400
|
+
const updateAccessPolicy = (botId, value, projectStatus) => {
|
|
1401
|
+
const incarnation = workspaces.incarnationFor(botId);
|
|
1402
|
+
const policy = validateAccessPolicy(value);
|
|
1403
|
+
return withBotTransition(botId, async () => {
|
|
1404
|
+
const snapshot = await controller.status();
|
|
1405
|
+
if (!snapshot?.bots?.some((bot) => bot?.botId === botId)) {
|
|
1406
|
+
const error = new Error('找不到要修改的机器人。');
|
|
1407
|
+
error.code = 'workspace-bot-not-found';
|
|
1408
|
+
throw error;
|
|
1409
|
+
}
|
|
1410
|
+
const catalog = await resolveAgentPresetCatalog(agentPresetCatalog);
|
|
1411
|
+
const decorated = workspaces.decorateStatus(snapshot);
|
|
1412
|
+
const updated = {
|
|
1413
|
+
...decorated,
|
|
1414
|
+
bots: decorated.bots.map((bot) => bot?.botId === botId
|
|
1415
|
+
? { ...bot, accessPolicy: policy } : bot),
|
|
1416
|
+
...(catalog ? { agentPresetCatalog: catalog } : {}),
|
|
1417
|
+
};
|
|
1418
|
+
// Prepare the complete channel-specific response before commit. Failed
|
|
1419
|
+
// projections and disk writes must leave the live policy unchanged.
|
|
1420
|
+
const result = projectStatus ? await projectStatus(updated) : updated;
|
|
1421
|
+
await workspaces.setAccessPolicy(botId, policy, { incarnation });
|
|
1422
|
+
return result;
|
|
1423
|
+
});
|
|
1424
|
+
};
|
|
1283
1425
|
const deleteWithWorkspace = (botId, invokeDelete) => withBotTransition(botId, async () => {
|
|
1284
1426
|
// Fence the old runtime without changing the durable mapping. A crash
|
|
1285
1427
|
// before the controller removes its config therefore keeps the bot's
|
|
@@ -1320,6 +1462,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
|
|
1320
1462
|
if (property === 'updateWorkspace') return updateWorkspace;
|
|
1321
1463
|
if (property === 'updateAgentPreset') return updateAgentPreset;
|
|
1322
1464
|
if (property === 'updateContextEnhancement') return updateContextEnhancement;
|
|
1465
|
+
if (property === 'updateAccessPolicy') return updateAccessPolicy;
|
|
1323
1466
|
const value = Reflect.get(target, property, target);
|
|
1324
1467
|
if (typeof value !== 'function') return value;
|
|
1325
1468
|
if (property === 'deleteBot') {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isBatchInputCommand } from './batch-input.mjs';
|
|
2
|
+
import { isCompactCommand } from './compact-command.mjs';
|
|
3
|
+
import { isControlCommand } from './control-command.mjs';
|
|
4
|
+
import { isHistoryCommand } from './history-command.mjs';
|
|
5
|
+
import { isModelCommand } from './model-command.mjs';
|
|
6
|
+
import { isPresetCommand } from './preset-command.mjs';
|
|
7
|
+
import { isWorkspaceCommand } from './workspace-command.mjs';
|
|
8
|
+
|
|
9
|
+
const SIMPLE_TEXT_COMMANDS = new Set(['/help', '/status', '/new']);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Match only commands that the shared bridges already execute locally.
|
|
13
|
+
* Unknown slash-prefixed text remains an ordinary prompt.
|
|
14
|
+
*/
|
|
15
|
+
export function isSharedLocalCommand(text, {
|
|
16
|
+
hasImages = false,
|
|
17
|
+
hasFiles = false,
|
|
18
|
+
} = {}) {
|
|
19
|
+
if (typeof text !== 'string') return false;
|
|
20
|
+
const command = text.trim();
|
|
21
|
+
if (!command) return false;
|
|
22
|
+
if (isBatchInputCommand(command) || isHistoryCommand(command)) return true;
|
|
23
|
+
if (!hasFiles && (
|
|
24
|
+
isControlCommand(command)
|
|
25
|
+
|| isModelCommand(command)
|
|
26
|
+
|| isPresetCommand(command)
|
|
27
|
+
)) return true;
|
|
28
|
+
if (hasImages || hasFiles) return false;
|
|
29
|
+
return SIMPLE_TEXT_COMMANDS.has(command.toLowerCase())
|
|
30
|
+
|| isWorkspaceCommand(command)
|
|
31
|
+
|| isCompactCommand(command);
|
|
32
|
+
}
|
|
@@ -70,9 +70,8 @@ function compactErrorMessage(error) {
|
|
|
70
70
|
* Unknown input returns null so the caller may continue ordinary message routing.
|
|
71
71
|
*/
|
|
72
72
|
export async function runCompactCommand(text, harness, state, conversationKey, options = {}) {
|
|
73
|
-
if (
|
|
73
|
+
if (!isCompactCommand(text)) return null;
|
|
74
74
|
const match = COMPACT_COMMAND.exec(text.trim());
|
|
75
|
-
if (!match) return null;
|
|
76
75
|
if (match[1].trim()) return commandResult(t(COMPACT_USAGE));
|
|
77
76
|
if (typeof state?.sessionFor !== 'function') {
|
|
78
77
|
return commandResult(t('当前机器人没有可用的会话状态。'));
|
|
@@ -94,3 +93,7 @@ export async function runCompactCommand(text, harness, state, conversationKey, o
|
|
|
94
93
|
return commandResult(compactErrorMessage(error));
|
|
95
94
|
}
|
|
96
95
|
}
|
|
96
|
+
|
|
97
|
+
export function isCompactCommand(text) {
|
|
98
|
+
return typeof text === 'string' && COMPACT_COMMAND.test(text.trim());
|
|
99
|
+
}
|
|
@@ -185,4 +185,6 @@ export default {
|
|
|
185
185
|
'Collected {count}/{limit} messages. The batch is full; send /send or /cancel.',
|
|
186
186
|
'批量内容提交失败,已保留 {count} 条消息。\n请再次发送 /send 重试或 /cancel 取消。':
|
|
187
187
|
'Batch submission failed; {count} messages were retained.\nSend /send to retry or /cancel to cancel.',
|
|
188
|
+
'你可以发送普通消息,但没有执行命令的权限。':
|
|
189
|
+
'You can send regular messages, but you do not have permission to run commands.',
|
|
188
190
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { evaluateAccessPolicy } from './access-policy.mjs';
|
|
2
|
+
import { isSharedLocalCommand } from './command-permission.mjs';
|
|
3
|
+
|
|
4
|
+
export const COMMAND_PERMISSION_DENIED_MESSAGE = '你可以发送普通消息,但没有执行命令的权限。';
|
|
5
|
+
|
|
6
|
+
const POLICY_NOT_CONFIGURED = Object.freeze({ allowed: true, reason: 'policy-not-configured' });
|
|
7
|
+
const PRIVILEGED_SENDER = Object.freeze({ allowed: true, reason: 'privileged-sender' });
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read one committed policy snapshot and decide a single inbound event.
|
|
11
|
+
* A missing provider is kept backward-compatible for direct bridge fixtures;
|
|
12
|
+
* production always injects a provider, whose missing/damaged value fails closed.
|
|
13
|
+
*/
|
|
14
|
+
export function evaluateInboundAccess(accessPolicy, {
|
|
15
|
+
conversationType,
|
|
16
|
+
senderIds,
|
|
17
|
+
text = '',
|
|
18
|
+
hasImages = false,
|
|
19
|
+
hasFiles = false,
|
|
20
|
+
isCommand = isSharedLocalCommand(text, { hasImages, hasFiles }),
|
|
21
|
+
} = {}) {
|
|
22
|
+
if (!accessPolicy) return POLICY_NOT_CONFIGURED;
|
|
23
|
+
try {
|
|
24
|
+
if (typeof accessPolicy.isPrivileged === 'function'
|
|
25
|
+
&& accessPolicy.isPrivileged(senderIds, conversationType) === true) {
|
|
26
|
+
return PRIVILEGED_SENDER;
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// A broken privilege lookup must not bypass the persisted policy.
|
|
30
|
+
}
|
|
31
|
+
let policy = null;
|
|
32
|
+
try {
|
|
33
|
+
policy = accessPolicy.getSettings();
|
|
34
|
+
} catch {
|
|
35
|
+
// Provider failures are equivalent to an unavailable persisted policy.
|
|
36
|
+
}
|
|
37
|
+
return evaluateAccessPolicy(policy, {
|
|
38
|
+
conversationType,
|
|
39
|
+
senderIds,
|
|
40
|
+
isCommand,
|
|
41
|
+
...(typeof accessPolicy.equals === 'function' ? { equals: accessPolicy.equals } : {}),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -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);
|