@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.
Files changed (68) hide show
  1. package/lib/client.js +951 -626
  2. package/lib/index.js +233 -233
  3. package/package.json +1 -1
  4. package/plugin-src/client/access-policy-settings.js +351 -0
  5. package/plugin-src/client/channel-card-meta.js +2 -1
  6. package/plugin-src/client/channels/dingtalk/api.js +5 -0
  7. package/plugin-src/client/channels/dingtalk/index.js +1 -0
  8. package/plugin-src/client/channels/feishu/api.js +5 -0
  9. package/plugin-src/client/channels/feishu/index.js +1 -0
  10. package/plugin-src/client/channels/qq/api.js +5 -0
  11. package/plugin-src/client/channels/qq/index.js +1 -0
  12. package/plugin-src/client/channels/shared/token-api.js +5 -0
  13. package/plugin-src/client/channels/shared/token-channel.js +1 -0
  14. package/plugin-src/client/channels/telegram/api.js +2 -17
  15. package/plugin-src/client/channels/telegram/index.js +0 -112
  16. package/plugin-src/client/channels/telegram/styles.js +0 -28
  17. package/plugin-src/client/channels/wecom/api.js +5 -0
  18. package/plugin-src/client/channels/wecom/index.js +1 -0
  19. package/plugin-src/client/channels/weixin/api.js +5 -0
  20. package/plugin-src/client/channels/weixin/index.js +1 -0
  21. package/plugin-src/client/channels/whatsapp/api.js +4 -10
  22. package/plugin-src/client/channels/whatsapp/index.js +1 -125
  23. package/plugin-src/client/channels/whatsapp/styles.js +0 -25
  24. package/plugin-src/client/delivery-settings.js +61 -9
  25. package/plugin-src/client/i18n.js +56 -1
  26. package/plugin-src/client/index.js +1 -0
  27. package/plugin-src/client/styles.js +50 -4
  28. package/plugin-src/host/channels/dingtalk/production.mjs +12 -1
  29. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  30. package/plugin-src/host/channels/feishu/production.mjs +39 -1
  31. package/plugin-src/host/channels/feishu/rpc.mjs +19 -2
  32. package/plugin-src/host/channels/qq/production.mjs +12 -1
  33. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  34. package/plugin-src/host/channels/shared/access-policy-production.mjs +137 -0
  35. package/plugin-src/host/channels/shared/access-policy-rpc.mjs +17 -0
  36. package/plugin-src/host/channels/shared/production.mjs +15 -1
  37. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  38. package/plugin-src/host/channels/slack/production.mjs +12 -1
  39. package/plugin-src/host/channels/slack/rpc.mjs +10 -0
  40. package/plugin-src/host/channels/telegram/rpc.mjs +2 -49
  41. package/plugin-src/host/channels/wecom/production.mjs +12 -1
  42. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  43. package/plugin-src/host/channels/weixin/production.mjs +12 -1
  44. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  45. package/plugin-src/host/channels/whatsapp/production.mjs +16 -2
  46. package/plugin-src/host/channels/whatsapp/rpc.mjs +10 -15
  47. package/src/channels/dingtalk/dingtalk-bridge.mjs +61 -9
  48. package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -0
  49. package/src/channels/discord/discord-runtime.mjs +29 -1
  50. package/src/channels/feishu/bridge.mjs +188 -80
  51. package/src/channels/feishu/feishu-runtime.mjs +4 -0
  52. package/src/channels/qq/qq-bridge.mjs +59 -17
  53. package/src/channels/qq/qq-runtime.mjs +20 -2
  54. package/src/channels/shared/access-policy.mjs +210 -0
  55. package/src/channels/shared/bot-workspace-store.mjs +176 -33
  56. package/src/channels/shared/command-permission.mjs +32 -0
  57. package/src/channels/shared/compact-command.mjs +5 -2
  58. package/src/channels/shared/i18n-en/shared-c.mjs +2 -0
  59. package/src/channels/shared/inbound-access.mjs +43 -0
  60. package/src/channels/shared/text-harness-bridge.mjs +47 -4
  61. package/src/channels/shared/workspace-command.mjs +10 -1
  62. package/src/channels/slack/slack-runtime.mjs +4 -0
  63. package/src/channels/telegram/telegram-runtime.mjs +5 -13
  64. package/src/channels/wecom/wecom-bridge.mjs +51 -4
  65. package/src/channels/wecom/wecom-runtime.mjs +4 -0
  66. package/src/channels/weixin/weixin-bridge.mjs +59 -16
  67. package/src/channels/weixin/weixin-runtime.mjs +4 -0
  68. package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -20
@@ -20,6 +20,10 @@ import { createConnectionSupervisor } from './connection-supervisor.mjs';
20
20
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
21
21
  import { harnessConnection } from '../../harness-connection.mjs';
22
22
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
23
+ import {
24
+ accessPolicyProvider,
25
+ initialAccessPolicyFor,
26
+ } from '../shared/access-policy-production.mjs';
23
27
 
24
28
  function pluginPaths(config) {
25
29
  const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
@@ -60,6 +64,7 @@ export async function createProductionController(ctx, config = {}, internals = {
60
64
  }
61
65
  await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
62
66
  defaultAgentPreset: config.agentPreset,
67
+ initialAccessPolicy: initialAccessPolicyFor('dingtalk', bot),
63
68
  })));
64
69
  const observedConfigStore = typeof configStore.remove === 'function'
65
70
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
@@ -101,7 +106,10 @@ export async function createProductionController(ctx, config = {}, internals = {
101
106
  logger,
102
107
  createRuntime: async ({ botId, config: botConfig, clientSecret }) => {
103
108
  const state = await stateFor(botId);
104
- await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
109
+ await workspaces.ensure(botId, {
110
+ defaultAgentPreset: config.agentPreset,
111
+ initialAccessPolicy: initialAccessPolicyFor('dingtalk', botConfig),
112
+ });
105
113
  const workspaceScope = createBotWorkspaceScope(harness, {
106
114
  botId, workspaces, state, agentPresetCatalog,
107
115
  });
@@ -111,6 +119,9 @@ export async function createProductionController(ctx, config = {}, internals = {
111
119
  harness: workspaceScope.harness,
112
120
  state: workspaceScope.state,
113
121
  contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
122
+ accessPolicy: accessPolicyProvider(workspaces, botId, {
123
+ channel: 'dingtalk', config: botConfig,
124
+ }),
114
125
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
115
126
  maxMessageChars: config.maxMessageChars ?? 4_000,
116
127
  connectTimeoutMs: config.connectTimeoutMs ?? 15_000,
@@ -1,5 +1,6 @@
1
1
  import QRCode from 'qrcode';
2
2
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
3
+ import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
3
4
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
4
5
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
5
6
  import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
@@ -20,6 +21,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
20
21
  setWorkspace: SET_WORKSPACE_ENDPOINT,
21
22
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
22
23
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
24
+ setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
23
25
  approveSender: 'bot.sender.approve',
24
26
  revokeSender: 'bot.sender.revoke',
25
27
  });
@@ -100,6 +102,10 @@ function payloadFailure(endpoint, payload) {
100
102
  return validContextEnhancementPayload(payload)
101
103
  ? null : '请提交有效的上下文增强设置。';
102
104
  }
105
+ if (endpoint === DINGTALK_ENDPOINTS.setAccessPolicy) {
106
+ return validAccessPolicyPayload(payload)
107
+ ? null : '请提交有效的访问设置。';
108
+ }
103
109
  if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
104
110
  return exactKeys(payload, ['botId', 'requestId', 'confirm'])
105
111
  && validId(payload.botId)
@@ -262,6 +268,11 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
262
268
  value = await controller.updateContextEnhancement(
263
269
  payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
264
270
  );
271
+ } else if (endpoint === DINGTALK_ENDPOINTS.setAccessPolicy) {
272
+ if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
273
+ value = await controller.updateAccessPolicy(
274
+ payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
275
+ );
265
276
  } else if (endpoint === DINGTALK_ENDPOINTS.setAgentPreset) {
266
277
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
267
278
  value = await publicStatus(
@@ -24,8 +24,39 @@ import {
24
24
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
25
25
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
26
26
  import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
27
+ import {
28
+ accessPolicyProvider,
29
+ initialAccessPolicyFor,
30
+ } from '../shared/access-policy-production.mjs';
31
+
32
+ // The WebSocket agent built here is only used for the Feishu long connection,
33
+ // whose endpoint is open.feishu.cn (Feishu) or open.larksuite.com (Lark).
34
+ // Honor NO_PROXY/no_proxy for those hosts so users with blanket proxy
35
+ // environments exported in their shell (Clash/V2Ray etc.) keep a direct
36
+ // connection to Feishu instead of routing the WSS handshake through the proxy.
37
+ const LONG_CONNECTION_HOSTS = ['open.feishu.cn', 'open.larksuite.com'];
38
+
39
+ function hostExcludedByNoProxyEntry(host, entry) {
40
+ if (entry === '*') return true;
41
+ const bare = entry.startsWith('.') ? entry.slice(1) : entry;
42
+ if (!bare) return false;
43
+ return host === bare || host.endsWith(`.${bare}`);
44
+ }
45
+
46
+ function longConnectionExcludedByNoProxy(env) {
47
+ for (const key of ['no_proxy', 'NO_PROXY']) {
48
+ const value = env?.[key];
49
+ if (typeof value !== 'string' || !value.trim()) continue;
50
+ const entries = value.split(',').map((entry) => entry.trim().toLowerCase()).filter(Boolean);
51
+ if (LONG_CONNECTION_HOSTS.some((host) => entries.some((entry) => hostExcludedByNoProxyEntry(host, entry)))) {
52
+ return true;
53
+ }
54
+ }
55
+ return false;
56
+ }
27
57
 
28
58
  function webSocketProxyUrl(env) {
59
+ if (longConnectionExcludedByNoProxy(env)) return undefined;
29
60
  for (const key of ['https_proxy', 'HTTPS_PROXY', 'http_proxy', 'HTTP_PROXY']) {
30
61
  const value = env?.[key];
31
62
  if (typeof value === 'string' && value.trim()) return value.trim();
@@ -93,6 +124,7 @@ export async function createProductionController(ctx, config = {}, internals = {
93
124
  }
94
125
  await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.id, {
95
126
  defaultAgentPreset: config.agentPreset,
127
+ initialAccessPolicy: initialAccessPolicyFor('feishu', bot),
96
128
  })));
97
129
  const observedConfigStore = typeof configStore.removeBot === 'function'
98
130
  ? observeBotWorkspaceRemovals(configStore, {
@@ -154,7 +186,10 @@ export async function createProductionController(ctx, config = {}, internals = {
154
186
  createRuntime: async ({ botId, config: botConfig, appSecret, repair }) => {
155
187
  const state = await stateFor(botConfig);
156
188
  const id = botId ?? botConfig.id ?? botConfig.appId;
157
- await workspaces.ensure(id, { defaultAgentPreset: config.agentPreset });
189
+ await workspaces.ensure(id, {
190
+ defaultAgentPreset: config.agentPreset,
191
+ initialAccessPolicy: initialAccessPolicyFor('feishu', botConfig),
192
+ });
158
193
  const workspaceScope = createBotWorkspaceScope(harness, {
159
194
  botId: id, workspaces, state, agentPresetCatalog,
160
195
  });
@@ -171,6 +206,9 @@ export async function createProductionController(ctx, config = {}, internals = {
171
206
  harness: workspaceScope.harness,
172
207
  state: workspaceScope.state,
173
208
  contextEnhancement: { botId: id, getSettings: () => workspaces.contextEnhancementFor(id) },
209
+ accessPolicy: accessPolicyProvider(workspaces, id, {
210
+ channel: 'feishu', config: botConfig,
211
+ }),
174
212
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
175
213
  slashCommands: config.slashCommands !== false,
176
214
  ...(wsAgent ? { wsAgent } : {}),
@@ -5,21 +5,27 @@ import {
5
5
  } from '../../../../src/channels/shared/agent-preset.mjs';
6
6
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
7
7
  import { publicMessageFailure } from '../../../../src/channels/shared/message-failure.mjs';
8
+ import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
8
9
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
9
10
  import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
10
11
  import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
11
12
  import { validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
13
+ import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
12
14
  import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
13
15
  import {
14
16
  isFeishuGroupResponseMode,
15
17
  normalizeFeishuGroupResponseMode,
16
18
  } from '../../../../src/channels/feishu/group-response-mode.mjs';
17
19
  import {
18
- FEISHU_ENDPOINTS,
20
+ FEISHU_ENDPOINTS as FEISHU_CLIENT_ENDPOINTS,
19
21
  FEISHU_RPC_CHANNEL,
20
22
  } from '../../../client/channels/feishu/api.js';
21
23
 
22
- export { FEISHU_ENDPOINTS, FEISHU_RPC_CHANNEL };
24
+ export const FEISHU_ENDPOINTS = Object.freeze({
25
+ ...FEISHU_CLIENT_ENDPOINTS,
26
+ setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
27
+ });
28
+ export { FEISHU_RPC_CHANNEL };
23
29
  export const FEISHU_MULTI_ENDPOINTS = Object.freeze({
24
30
  reconnectBot: 'bot.reconnect',
25
31
  disconnectBot: 'bot.disconnect',
@@ -279,6 +285,7 @@ function publicBotEntry(entry) {
279
285
  configured: source.configured === true,
280
286
  agentPreset: normalizeAgentPresetId(source.agentPreset),
281
287
  contextEnhancement: normalizeContextEnhancementConfig(source.contextEnhancement),
288
+ accessPolicy: normalizeAccessPolicy(source.accessPolicy),
282
289
  groupResponseMode: normalizeFeishuGroupResponseMode(source.groupResponseMode),
283
290
  groupMessagePermissionGranted: source.groupMessagePermissionGranted === true,
284
291
  bot: publicBot(source.bot),
@@ -422,6 +429,10 @@ function validPayload(endpoint, payload) {
422
429
  return validContextEnhancementPayload(payload)
423
430
  ? null : '请提交有效的上下文增强设置。';
424
431
  }
432
+ if (endpoint === FEISHU_ENDPOINTS.setAccessPolicy) {
433
+ return validAccessPolicyPayload(payload)
434
+ ? null : '请提交有效的访问设置。';
435
+ }
425
436
  if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
426
437
  return hasOnlyKeys(payload, new Set(['botId', 'groupResponseMode']))
427
438
  && safeOpaqueId(payload.botId)
@@ -679,6 +690,12 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
679
690
  payload.botId, payload.config,
680
691
  (status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
681
692
  );
693
+ } else if (endpoint === FEISHU_ENDPOINTS.setAccessPolicy) {
694
+ if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
695
+ value = await controller.updateAccessPolicy(
696
+ payload.botId, payload.policy,
697
+ (status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
698
+ );
682
699
  } else if (endpoint === FEISHU_ENDPOINTS.setAgentPreset) {
683
700
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
684
701
  value = await toPublicFeishuStatus(
@@ -20,6 +20,10 @@ import { createConnectionSupervisor } from './connection-supervisor.mjs';
20
20
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
21
21
  import { harnessConnection } from '../../harness-connection.mjs';
22
22
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
23
+ import {
24
+ accessPolicyProvider,
25
+ initialAccessPolicyFor,
26
+ } from '../shared/access-policy-production.mjs';
23
27
 
24
28
  function pluginPaths(config) {
25
29
  const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
@@ -54,6 +58,7 @@ export async function createProductionController(ctx, config = {}, internals = {
54
58
  await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
55
59
  await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
56
60
  defaultAgentPreset: config.agentPreset,
61
+ initialAccessPolicy: initialAccessPolicyFor('qq', bot),
57
62
  })));
58
63
  const observedConfigStore = typeof configStore.remove === 'function'
59
64
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
@@ -92,7 +97,10 @@ export async function createProductionController(ctx, config = {}, internals = {
92
97
  logger,
93
98
  createRuntime: async ({ botId, config: botConfig, appSecret }) => {
94
99
  const state = await stateFor(botId);
95
- await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
100
+ await workspaces.ensure(botId, {
101
+ defaultAgentPreset: config.agentPreset,
102
+ initialAccessPolicy: initialAccessPolicyFor('qq', botConfig),
103
+ });
96
104
  const workspaceScope = createBotWorkspaceScope(harness, {
97
105
  botId, workspaces, state, agentPresetCatalog,
98
106
  });
@@ -102,6 +110,9 @@ export async function createProductionController(ctx, config = {}, internals = {
102
110
  harness: workspaceScope.harness,
103
111
  state: workspaceScope.state,
104
112
  contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
113
+ accessPolicy: accessPolicyProvider(workspaces, botId, {
114
+ channel: 'qq', config: botConfig,
115
+ }),
105
116
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
106
117
  connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
107
118
  logger: {
@@ -4,6 +4,7 @@ import {
4
4
  publicConnectionTestResult,
5
5
  } from '../../../../src/channels/shared/connection-test.mjs';
6
6
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
7
+ import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
7
8
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
8
9
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
9
10
  import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
@@ -20,6 +21,7 @@ export const QQ_ENDPOINTS = Object.freeze({
20
21
  setWorkspace: SET_WORKSPACE_ENDPOINT,
21
22
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
22
23
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
24
+ setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
23
25
  });
24
26
  export const QQ_RPC_ENDPOINTS = Object.freeze(Object.values(QQ_ENDPOINTS));
25
27
 
@@ -82,6 +84,10 @@ function payloadFailure(endpoint, payload) {
82
84
  return validContextEnhancementPayload(payload)
83
85
  ? null : '请提交有效的上下文增强设置。';
84
86
  }
87
+ if (endpoint === QQ_ENDPOINTS.setAccessPolicy) {
88
+ return validAccessPolicyPayload(payload)
89
+ ? null : '请提交有效的访问设置。';
90
+ }
85
91
  return 'Unknown QQ endpoint.';
86
92
  }
87
93
 
@@ -182,6 +188,11 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
182
188
  value = await controller.updateContextEnhancement(
183
189
  payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
184
190
  );
191
+ } else if (endpoint === QQ_ENDPOINTS.setAccessPolicy) {
192
+ if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
193
+ value = await controller.updateAccessPolicy(
194
+ payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
195
+ );
185
196
  } else if (endpoint === QQ_ENDPOINTS.setAgentPreset) {
186
197
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
187
198
  value = await publicStatus(
@@ -0,0 +1,137 @@
1
+ import {
2
+ createAccessPolicy,
3
+ createAccessPolicyScope,
4
+ } from '../../../../src/channels/shared/access-policy.mjs';
5
+
6
+ function policyUsers(users) {
7
+ return users.map((id) => ({ id, canExecuteCommands: true }));
8
+ }
9
+
10
+ function openScope(allowlistUsers = []) {
11
+ return createAccessPolicyScope({
12
+ mode: 'open',
13
+ open: {
14
+ defaultCanExecuteCommands: true,
15
+ commandPermissionOverrides: [],
16
+ },
17
+ allowlist: { users: policyUsers(allowlistUsers) },
18
+ });
19
+ }
20
+
21
+ function allowlistScope(users = []) {
22
+ return createAccessPolicyScope({
23
+ mode: 'allowlist',
24
+ open: {
25
+ defaultCanExecuteCommands: false,
26
+ commandPermissionOverrides: [],
27
+ },
28
+ allowlist: { users: policyUsers(users) },
29
+ });
30
+ }
31
+
32
+ function cleanIds(values) {
33
+ return [...new Set((Array.isArray(values) ? values : [values])
34
+ .filter((value) => typeof value === 'string' || typeof value === 'number'
35
+ || typeof value === 'bigint')
36
+ .map((value) => String(value).trim())
37
+ .filter(Boolean))];
38
+ }
39
+
40
+ function whatsappNumberJids(values) {
41
+ return cleanIds(values).map((value) => `${value.replace(/^\+/, '')}@s.whatsapp.net`);
42
+ }
43
+
44
+ /**
45
+ * Build the one-time, backwards-compatible seed for a bot whose workspace
46
+ * document does not yet contain an access policy.
47
+ */
48
+ export function initialAccessPolicyFor(channel, config = {}) {
49
+ const key = String(channel ?? '').trim().toLowerCase();
50
+ if (key === 'weixin') {
51
+ return createAccessPolicy({
52
+ direct: allowlistScope(),
53
+ group: allowlistScope(),
54
+ });
55
+ }
56
+ if (key === 'feishu') {
57
+ const owners = cleanIds(config.ownerOpenIds ?? config.ownerOpenId);
58
+ const scope = owners.includes('*') ? openScope() : allowlistScope();
59
+ return createAccessPolicy({ direct: scope, group: scope });
60
+ }
61
+ if (key === 'qq') {
62
+ const owners = cleanIds(config.ownerUserOpenid);
63
+ return createAccessPolicy({
64
+ direct: owners.includes('*') ? openScope() : allowlistScope(),
65
+ group: openScope(),
66
+ });
67
+ }
68
+ if (key === 'telegram') {
69
+ const users = cleanIds(config.allowedUsers);
70
+ if ((config.accessMode ?? 'compatible') === 'private-allowlist') {
71
+ return createAccessPolicy({
72
+ direct: allowlistScope(users),
73
+ group: allowlistScope(),
74
+ });
75
+ }
76
+ return createAccessPolicy({
77
+ direct: openScope(users),
78
+ group: openScope(),
79
+ });
80
+ }
81
+ if (key === 'whatsapp') {
82
+ const mode = config.accessMode ?? 'self-only';
83
+ const allowed = whatsappNumberJids(config.allowedNumbers);
84
+ if (mode === 'open') {
85
+ return createAccessPolicy({ direct: openScope(allowed), group: openScope() });
86
+ }
87
+ return createAccessPolicy({
88
+ direct: allowlistScope(mode === 'private-allowlist'
89
+ ? allowed
90
+ : []),
91
+ group: allowlistScope(),
92
+ });
93
+ }
94
+ if (['dingtalk', 'wecom', 'slack', 'discord'].includes(key)) {
95
+ return createAccessPolicy({ direct: openScope(), group: openScope() });
96
+ }
97
+ throw new TypeError(`Unsupported access-policy channel: ${channel}`);
98
+ }
99
+
100
+ export function privilegedSenderIdsFor(channel, config = {}) {
101
+ const key = String(channel ?? '').trim().toLowerCase();
102
+ if (key === 'weixin') return cleanIds(config.ownerUserId);
103
+ if (key === 'feishu') {
104
+ return cleanIds(config.ownerOpenIds ?? config.ownerOpenId).filter((id) => id !== '*');
105
+ }
106
+ if (key === 'dingtalk') {
107
+ const approved = Array.isArray(config.approvedSenders) ? config.approvedSenders : [];
108
+ return cleanIds(approved.map((entry) => entry?.staffId));
109
+ }
110
+ if (key === 'qq') return cleanIds(config.ownerUserOpenid).filter((id) => id !== '*');
111
+ if (key === 'whatsapp') return cleanIds(config.accountJid);
112
+ return [];
113
+ }
114
+
115
+ export function accessPolicyProvider(workspaces, botId, { channel, config, equals } = {}) {
116
+ if (!workspaces || typeof workspaces.accessPolicyFor !== 'function') {
117
+ throw new TypeError('A workspace store with access policies is required');
118
+ }
119
+ const privilegedSenderIds = new Set(privilegedSenderIdsFor(channel, config));
120
+ const sameSender = typeof equals === 'function' ? equals : (left, right) => left === right;
121
+ return Object.freeze({
122
+ botId,
123
+ getSettings: () => workspaces.accessPolicyFor(botId),
124
+ isPrivileged(senderIds, conversationType) {
125
+ if (!['direct', 'group'].includes(conversationType)) return false;
126
+ const candidates = Array.isArray(senderIds) ? senderIds : [senderIds];
127
+ try {
128
+ return candidates.some((senderId) => typeof senderId === 'string'
129
+ && [...privilegedSenderIds].some((privilegedId) => (
130
+ sameSender(senderId.trim(), privilegedId) === true
131
+ )));
132
+ } catch {
133
+ return false;
134
+ }
135
+ },
136
+ });
137
+ }
@@ -0,0 +1,17 @@
1
+ import { validateAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
2
+
3
+ export const SET_ACCESS_POLICY_ENDPOINT = 'bot.access-policy.set';
4
+
5
+ export function validAccessPolicyPayload(payload) {
6
+ try {
7
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)
8
+ || Reflect.ownKeys(payload).length !== 2
9
+ || !Object.hasOwn(payload, 'botId') || !Object.hasOwn(payload, 'policy')
10
+ || typeof payload.botId !== 'string'
11
+ || !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) return false;
12
+ validateAccessPolicy(payload.policy);
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
@@ -17,6 +17,10 @@ import {
17
17
  createDeliveryAdapter,
18
18
  supportsDeliveryChannel,
19
19
  } from '../../delivery-adapter.mjs';
20
+ import {
21
+ accessPolicyProvider,
22
+ initialAccessPolicyFor,
23
+ } from './access-policy-production.mjs';
20
24
 
21
25
  export function pluginPaths(config, channel) {
22
26
  const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
@@ -46,6 +50,11 @@ export async function createTokenProductionController(ctx, config, internals, de
46
50
  throw new TypeError(`dsh-im ${channel} runtimeOptions must return an object`);
47
51
  }
48
52
  const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
53
+ const seedAccessPolicy = typeof definitions.initialAccessPolicyForBot === 'function'
54
+ ? definitions.initialAccessPolicyForBot
55
+ // Telegram is the only token channel with a legacy access model. Other
56
+ // current token channels preserve their fully-open baseline.
57
+ : (bot) => initialAccessPolicyFor(channel === 'telegram' ? 'telegram' : 'discord', bot);
49
58
  const logger = typeof ctx.logger === 'function'
50
59
  ? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
51
60
  const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
@@ -59,6 +68,7 @@ export async function createTokenProductionController(ctx, config, internals, de
59
68
  await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
60
69
  await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
61
70
  defaultAgentPreset: config.agentPreset,
71
+ initialAccessPolicy: seedAccessPolicy(bot),
62
72
  })));
63
73
  const observedConfigStore = typeof configStore.remove === 'function'
64
74
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
@@ -96,7 +106,10 @@ export async function createTokenProductionController(ctx, config, internals, de
96
106
  ...(internals.inspectToken ? { inspectToken: internals.inspectToken } : {}),
97
107
  createRuntime: async ({ botId, config: botConfig, token }) => {
98
108
  const state = await stateFor(botId);
99
- await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
109
+ await workspaces.ensure(botId, {
110
+ defaultAgentPreset: config.agentPreset,
111
+ initialAccessPolicy: seedAccessPolicy(botConfig),
112
+ });
100
113
  const workspaceScope = createBotWorkspaceScope(harness, {
101
114
  botId, workspaces, state, agentPresetCatalog,
102
115
  });
@@ -107,6 +120,7 @@ export async function createTokenProductionController(ctx, config, internals, de
107
120
  harness: workspaceScope.harness,
108
121
  state: workspaceScope.state,
109
122
  contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
123
+ accessPolicy: accessPolicyProvider(workspaces, botId, { channel, config: botConfig }),
110
124
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
111
125
  connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
112
126
  logger: {
@@ -1,4 +1,5 @@
1
1
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
2
+ import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from './access-policy-rpc.mjs';
2
3
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
4
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
4
5
  import {
@@ -19,6 +20,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
19
20
  setWorkspace: SET_WORKSPACE_ENDPOINT,
20
21
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
21
22
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
23
+ setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
22
24
  });
23
25
 
24
26
  const ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS));
@@ -77,6 +79,10 @@ function payloadFailure(endpoint, payload) {
77
79
  return validContextEnhancementPayload(payload)
78
80
  ? null : '请提交有效的上下文增强设置。';
79
81
  }
82
+ if (endpoint === TOKEN_BOT_ENDPOINTS.setAccessPolicy) {
83
+ return validAccessPolicyPayload(payload)
84
+ ? null : '请提交有效的访问设置。';
85
+ }
80
86
  return 'Unknown bot endpoint.';
81
87
  }
82
88
 
@@ -161,6 +167,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
161
167
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setContextEnhancement) {
162
168
  if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
163
169
  value = await controller.updateContextEnhancement(payload.botId, payload.config);
170
+ } else if (endpoint === TOKEN_BOT_ENDPOINTS.setAccessPolicy) {
171
+ if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
172
+ value = await controller.updateAccessPolicy(payload.botId, payload.policy);
164
173
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setAgentPreset) {
165
174
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
166
175
  value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);
@@ -19,6 +19,10 @@ import { pluginPaths } from '../shared/production.mjs';
19
19
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
20
20
  import { harnessConnection } from '../../harness-connection.mjs';
21
21
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
22
+ import {
23
+ accessPolicyProvider,
24
+ initialAccessPolicyFor,
25
+ } from '../shared/access-policy-production.mjs';
22
26
 
23
27
  export async function createProductionController(ctx, config = {}, internals = {}) {
24
28
  if (!ctx?.credentials) throw new TypeError('dsh-im slack requires ctx.credentials');
@@ -43,6 +47,7 @@ export async function createProductionController(ctx, config = {}, internals = {
43
47
  await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
44
48
  await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
45
49
  defaultAgentPreset: config.agentPreset,
50
+ initialAccessPolicy: initialAccessPolicyFor('slack', bot),
46
51
  })));
47
52
  const observedConfigStore = typeof configStore.remove === 'function'
48
53
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
@@ -80,7 +85,10 @@ export async function createProductionController(ctx, config = {}, internals = {
80
85
  ...(internals.inspectCredentials ? { inspectCredentials: internals.inspectCredentials } : {}),
81
86
  createRuntime: async ({ botId, config: botConfig, botToken, appToken }) => {
82
87
  const state = await stateFor(botId);
83
- await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
88
+ await workspaces.ensure(botId, {
89
+ defaultAgentPreset: config.agentPreset,
90
+ initialAccessPolicy: initialAccessPolicyFor('slack', botConfig),
91
+ });
84
92
  const workspaceScope = createBotWorkspaceScope(harness, {
85
93
  botId, workspaces, state, agentPresetCatalog,
86
94
  });
@@ -91,6 +99,9 @@ export async function createProductionController(ctx, config = {}, internals = {
91
99
  harness: workspaceScope.harness,
92
100
  state: workspaceScope.state,
93
101
  contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
102
+ accessPolicy: accessPolicyProvider(workspaces, botId, {
103
+ channel: 'slack', config: botConfig,
104
+ }),
94
105
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
95
106
  connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
96
107
  logger: {
@@ -1,4 +1,5 @@
1
1
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
2
+ import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
2
3
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
4
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
4
5
  import {
@@ -20,6 +21,7 @@ export const SLACK_ENDPOINTS = Object.freeze({
20
21
  setWorkspace: SET_WORKSPACE_ENDPOINT,
21
22
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
22
23
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
24
+ setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
23
25
  });
24
26
  export const SLACK_RPC_ENDPOINTS = Object.freeze(Object.values(SLACK_ENDPOINTS));
25
27
 
@@ -81,6 +83,10 @@ function payloadFailure(endpoint, payload) {
81
83
  return validContextEnhancementPayload(payload)
82
84
  ? null : '请提交有效的上下文增强设置。';
83
85
  }
86
+ if (endpoint === SLACK_ENDPOINTS.setAccessPolicy) {
87
+ return validAccessPolicyPayload(payload)
88
+ ? null : '请提交有效的访问设置。';
89
+ }
84
90
  return 'Unknown Slack endpoint.';
85
91
  }
86
92
 
@@ -164,6 +170,10 @@ export function createSlackRpcHandler(controller) {
164
170
  if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
165
171
  value = await controller.updateContextEnhancement(payload.botId, payload.config);
166
172
  }
173
+ else if (endpoint === SLACK_ENDPOINTS.setAccessPolicy) {
174
+ if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
175
+ value = await controller.updateAccessPolicy(payload.botId, payload.policy);
176
+ }
167
177
  else if (endpoint === SLACK_ENDPOINTS.setAgentPreset) {
168
178
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
169
179
  value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);