@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
@@ -3,60 +3,13 @@ import {
3
3
  createTokenBotRpcHandler,
4
4
  } from '../shared/rpc.mjs';
5
5
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
6
- import { normalizeTelegramAccessPolicy } from '../../../../src/channels/telegram/config-store.mjs';
7
6
 
8
7
  export const TELEGRAM_RPC_CHANNEL = '/telegram';
9
- export const TELEGRAM_ENDPOINTS = Object.freeze({
10
- ...TOKEN_BOT_ENDPOINTS,
11
- setAccessPolicy: 'bot.access-policy.set',
12
- });
8
+ export const TELEGRAM_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
13
9
  export const TELEGRAM_RPC_ENDPOINTS = Object.freeze(Object.values(TELEGRAM_ENDPOINTS));
14
10
 
15
11
  export function createTelegramRpcHandler(controller) {
16
- if (typeof controller?.setAccessPolicy !== 'function') {
17
- throw new TypeError('A complete Telegram controller is required (setAccessPolicy)');
18
- }
19
- const sharedHandler = createTokenBotRpcHandler(controller, { channel: 'Telegram' });
20
- return async (endpoint, payload, signal) => {
21
- if (endpoint !== TELEGRAM_ENDPOINTS.setAccessPolicy) {
22
- return sharedHandler(endpoint, payload, signal);
23
- }
24
- if (signal?.aborted) {
25
- return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
26
- }
27
- const keys = payload && typeof payload === 'object' && !Array.isArray(payload)
28
- ? Object.keys(payload) : [];
29
- if (keys.length !== 3 || !keys.every((key) => (
30
- ['botId', 'accessMode', 'allowedUsers'].includes(key)
31
- )) || typeof payload.botId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) {
32
- return {
33
- ok: false,
34
- error: { code: 'bad-request', message: 'bot.access-policy.set requires a valid policy.' },
35
- };
36
- }
37
- let accessPolicy;
38
- try {
39
- accessPolicy = normalizeTelegramAccessPolicy(payload);
40
- } catch {
41
- return {
42
- ok: false,
43
- error: { code: 'bad-request', message: '请输入有效的 Telegram 访问模式和数字 User ID。' },
44
- };
45
- }
46
- try {
47
- const value = await controller.setAccessPolicy(payload.botId, accessPolicy);
48
- return signal?.aborted
49
- ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
50
- : { ok: true, value };
51
- } catch {
52
- return signal?.aborted
53
- ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
54
- : {
55
- ok: false,
56
- error: { code: 'telegram-operation-failed', message: 'Telegram 操作失败,请稍后重试。' },
57
- };
58
- }
59
- };
12
+ return createTokenBotRpcHandler(controller, { channel: 'Telegram' });
60
13
  }
61
14
 
62
15
  export function installTelegramRpc(ctx, controller, authority) {
@@ -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('wecom', bot),
57
62
  })));
58
63
  const observedConfigStore = typeof configStore.remove === 'function'
59
64
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
@@ -95,7 +100,10 @@ export async function createProductionController(ctx, config = {}, internals = {
95
100
  logger,
96
101
  createRuntime: async ({ botId, config: botConfig, secret }) => {
97
102
  const state = await stateFor(botId);
98
- await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
103
+ await workspaces.ensure(botId, {
104
+ defaultAgentPreset: config.agentPreset,
105
+ initialAccessPolicy: initialAccessPolicyFor('wecom', botConfig),
106
+ });
99
107
  const workspaceScope = createBotWorkspaceScope(harness, {
100
108
  botId, workspaces, state, agentPresetCatalog,
101
109
  });
@@ -105,6 +113,9 @@ export async function createProductionController(ctx, config = {}, internals = {
105
113
  harness: workspaceScope.harness,
106
114
  state: workspaceScope.state,
107
115
  contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
116
+ accessPolicy: accessPolicyProvider(workspaces, botId, {
117
+ channel: 'wecom', config: botConfig,
118
+ }),
108
119
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
109
120
  connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
110
121
  maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
@@ -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 WECOM_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 WECOM_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_ENDPOINTS));
25
27
 
@@ -82,6 +84,10 @@ function payloadFailure(endpoint, payload) {
82
84
  return validContextEnhancementPayload(payload)
83
85
  ? null : '请提交有效的上下文增强设置。';
84
86
  }
87
+ if (endpoint === WECOM_ENDPOINTS.setAccessPolicy) {
88
+ return validAccessPolicyPayload(payload)
89
+ ? null : '请提交有效的访问设置。';
90
+ }
85
91
  return 'Unknown Enterprise WeChat endpoint.';
86
92
  }
87
93
 
@@ -183,6 +189,11 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
183
189
  value = await controller.updateContextEnhancement(
184
190
  payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
185
191
  );
192
+ } else if (endpoint === WECOM_ENDPOINTS.setAccessPolicy) {
193
+ if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
194
+ value = await controller.updateAccessPolicy(
195
+ payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
196
+ );
186
197
  } else if (endpoint === WECOM_ENDPOINTS.setAgentPreset) {
187
198
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
188
199
  value = await publicStatus(
@@ -23,6 +23,10 @@ import { createConnectionSupervisor } from './connection-supervisor.mjs';
23
23
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
24
24
  import { harnessConnection } from '../../harness-connection.mjs';
25
25
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
26
+ import {
27
+ accessPolicyProvider,
28
+ initialAccessPolicyFor,
29
+ } from '../shared/access-policy-production.mjs';
26
30
 
27
31
  function pluginPaths(config) {
28
32
  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
  await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
61
65
  await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
62
66
  defaultAgentPreset: config.agentPreset,
67
+ initialAccessPolicy: initialAccessPolicyFor('weixin', bot),
63
68
  })));
64
69
  const observedConfigStore = typeof configStore.remove === 'function'
65
70
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
@@ -98,7 +103,10 @@ export async function createProductionController(ctx, config = {}, internals = {
98
103
  logger,
99
104
  createRuntime: async ({ botId, config: accountConfig, token }) => {
100
105
  const state = await stateFor(botId);
101
- await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
106
+ await workspaces.ensure(botId, {
107
+ defaultAgentPreset: config.agentPreset,
108
+ initialAccessPolicy: initialAccessPolicyFor('weixin', accountConfig),
109
+ });
102
110
  const workspaceScope = createBotWorkspaceScope(harness, {
103
111
  botId, workspaces, state, agentPresetCatalog,
104
112
  });
@@ -109,6 +117,9 @@ export async function createProductionController(ctx, config = {}, internals = {
109
117
  harness: workspaceScope.harness,
110
118
  state: workspaceScope.state,
111
119
  contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
120
+ accessPolicy: accessPolicyProvider(workspaces, botId, {
121
+ channel: 'weixin', config: accountConfig,
122
+ }),
112
123
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
113
124
  maxMessageChars: config.maxMessageChars ?? DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
114
125
  logger: {
@@ -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 {
5
6
  publicWorkspaceError,
@@ -27,6 +28,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
27
28
  setWorkspace: SET_WORKSPACE_ENDPOINT,
28
29
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
29
30
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
31
+ setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
30
32
  });
31
33
  export const WEIXIN_RPC_ENDPOINTS = Object.freeze(Object.values(WEIXIN_ENDPOINTS));
32
34
 
@@ -89,6 +91,10 @@ function payloadFailure(endpoint, payload) {
89
91
  return validContextEnhancementPayload(payload)
90
92
  ? null : '请提交有效的上下文增强设置。';
91
93
  }
94
+ if (endpoint === WEIXIN_ENDPOINTS.setAccessPolicy) {
95
+ return validAccessPolicyPayload(payload)
96
+ ? null : '请提交有效的访问设置。';
97
+ }
92
98
  return 'Unknown Weixin endpoint.';
93
99
  }
94
100
 
@@ -218,6 +224,11 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
218
224
  value = await controller.updateContextEnhancement(
219
225
  payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
220
226
  );
227
+ } else if (endpoint === WEIXIN_ENDPOINTS.setAccessPolicy) {
228
+ if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
229
+ value = await controller.updateAccessPolicy(
230
+ payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
231
+ );
221
232
  } else if (endpoint === WEIXIN_ENDPOINTS.setAgentPreset) {
222
233
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
223
234
  value = await publicStatus(
@@ -6,7 +6,10 @@ import { WhatsappConfigStore } from '../../../../src/channels/whatsapp/config-st
6
6
  import { WhatsappHarnessClient } from '../../../../src/channels/whatsapp/harness-client.mjs';
7
7
  import { WhatsappStateStore } from '../../../../src/channels/whatsapp/state-store.mjs';
8
8
  import { WhatsappController } from '../../../../src/channels/whatsapp/whatsapp-controller.mjs';
9
- import { WhatsappRuntime } from '../../../../src/channels/whatsapp/whatsapp-runtime.mjs';
9
+ import {
10
+ WhatsappRuntime,
11
+ whatsappAccessPolicyIdsEqual,
12
+ } from '../../../../src/channels/whatsapp/whatsapp-runtime.mjs';
10
13
  import { createWhatsappWebSession } from '../../../../src/channels/whatsapp/whatsapp-web-session.mjs';
11
14
  import {
12
15
  BotWorkspaceStore,
@@ -20,6 +23,10 @@ import { createTokenConnectionSupervisor } from '../shared/connection-supervisor
20
23
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
21
24
  import { harnessConnection } from '../../harness-connection.mjs';
22
25
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
26
+ import {
27
+ accessPolicyProvider,
28
+ initialAccessPolicyFor,
29
+ } from '../shared/access-policy-production.mjs';
23
30
 
24
31
  const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
25
32
 
@@ -61,6 +68,7 @@ export async function createProductionController(ctx, config = {}, internals = {
61
68
  await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
62
69
  await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
63
70
  defaultAgentPreset: config.agentPreset,
71
+ initialAccessPolicy: initialAccessPolicyFor('whatsapp', bot),
64
72
  })));
65
73
  const observedConfigStore = typeof configStore.remove === 'function'
66
74
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
@@ -98,7 +106,10 @@ export async function createProductionController(ctx, config = {}, internals = {
98
106
  logger,
99
107
  createRuntime: async ({ botId, config: botConfig, authDir }) => {
100
108
  const state = await stateFor(botId);
101
- await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
109
+ await workspaces.ensure(botId, {
110
+ defaultAgentPreset: config.agentPreset,
111
+ initialAccessPolicy: initialAccessPolicyFor('whatsapp', botConfig),
112
+ });
102
113
  const workspaceScope = createBotWorkspaceScope(harness, {
103
114
  botId, workspaces, state, agentPresetCatalog,
104
115
  });
@@ -108,6 +119,9 @@ export async function createProductionController(ctx, config = {}, internals = {
108
119
  harness: workspaceScope.harness,
109
120
  state: workspaceScope.state,
110
121
  contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
122
+ accessPolicy: accessPolicyProvider(workspaces, botId, {
123
+ channel: 'whatsapp', config: botConfig, equals: whatsappAccessPolicyIdsEqual,
124
+ }),
111
125
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
112
126
  connectTimeoutMs: config.connectTimeoutMs ?? 30_000,
113
127
  createSession,
@@ -1,7 +1,7 @@
1
1
  import QRCode from 'qrcode';
2
2
 
3
3
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
4
- import { normalizeWhatsappAccessPolicy } from '../../../../src/channels/whatsapp/config-store.mjs';
4
+ import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
5
5
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
6
6
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
7
7
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
@@ -15,7 +15,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
15
15
  cancelProvisioning: 'provision.cancel',
16
16
  reconnectBot: 'bot.reconnect',
17
17
  deleteBot: 'bot.delete',
18
- setAccessPolicy: 'bot.access-policy.set',
18
+ setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
19
19
  setWorkspace: SET_WORKSPACE_ENDPOINT,
20
20
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
21
21
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
@@ -55,15 +55,8 @@ function payloadFailure(endpoint, payload) {
55
55
  && payload.confirm === true ? null : 'bot.delete requires a botId and confirm=true.';
56
56
  }
57
57
  if (endpoint === WHATSAPP_ENDPOINTS.setAccessPolicy) {
58
- if (!exactKeys(payload, ['botId', 'accessMode', 'allowedNumbers'])
59
- || Object.keys(payload).length !== 3
60
- || !validId(payload.botId)) return '请输入有效的 WhatsApp 访问模式和电话号码。';
61
- try {
62
- normalizeWhatsappAccessPolicy(payload);
63
- return null;
64
- } catch {
65
- return '请输入有效的 WhatsApp 访问模式和电话号码。';
66
- }
58
+ return validAccessPolicyPayload(payload)
59
+ ? null : '请提交有效的访问设置。';
67
60
  }
68
61
  if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
69
62
  return validWorkspacePayload(payload)
@@ -111,7 +104,7 @@ async function publicStatus(value, encodeQr) {
111
104
  }
112
105
 
113
106
  export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
114
- for (const method of ['status', 'startProvisioning', 'registrationStatus', 'cancelProvisioning', 'reconnectBot', 'deleteBot', 'setAccessPolicy']) {
107
+ for (const method of ['status', 'startProvisioning', 'registrationStatus', 'cancelProvisioning', 'reconnectBot', 'deleteBot']) {
115
108
  if (typeof controller?.[method] !== 'function') {
116
109
  throw new TypeError(`A complete WhatsApp controller is required (${method})`);
117
110
  }
@@ -191,9 +184,11 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
191
184
  cachedEncode,
192
185
  );
193
186
  } else if (endpoint === WHATSAPP_ENDPOINTS.setAccessPolicy) {
194
- value = await publicStatus(
195
- await controller.setAccessPolicy(payload.botId, normalizeWhatsappAccessPolicy(payload)),
196
- cachedEncode,
187
+ if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
188
+ value = await controller.updateAccessPolicy(
189
+ payload.botId,
190
+ payload.policy,
191
+ (status) => publicStatus(status, cachedEncode),
197
192
  );
198
193
  } else {
199
194
  value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
@@ -58,6 +58,10 @@ import {
58
58
  messageFailureText,
59
59
  setLastMessageFailure,
60
60
  } from '../shared/message-failure.mjs';
61
+ import {
62
+ COMMAND_PERMISSION_DENIED_MESSAGE,
63
+ evaluateInboundAccess,
64
+ } from '../shared/inbound-access.mjs';
61
65
  import { t } from '../shared/i18n.mjs';
62
66
 
63
67
  const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
@@ -374,6 +378,7 @@ export class DingtalkHarnessBridge {
374
378
  #harness;
375
379
  #state;
376
380
  #contextEnhancement;
381
+ #accessPolicy;
377
382
  #status;
378
383
  #logger;
379
384
  #replyTimeoutMs;
@@ -397,6 +402,7 @@ export class DingtalkHarnessBridge {
397
402
  harness,
398
403
  state,
399
404
  contextEnhancement,
405
+ accessPolicy,
400
406
  status = createDingtalkBridgeStatus(),
401
407
  logger = console,
402
408
  replyTimeoutMs = 600_000,
@@ -415,6 +421,7 @@ export class DingtalkHarnessBridge {
415
421
  this.#harness = harness;
416
422
  this.#state = state;
417
423
  this.#contextEnhancement = contextEnhancement;
424
+ this.#accessPolicy = accessPolicy;
418
425
  this.#status = status;
419
426
  this.#logger = logger;
420
427
  this.#approvals = new HarnessApprovalQueue({ label: 'DingTalk', logger });
@@ -439,17 +446,13 @@ export class DingtalkHarnessBridge {
439
446
  const sender = senderStaffId(message);
440
447
  if (!messageId || !sender || this.#state.hasSeen(messageId)
441
448
  || this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
442
- this.#acceptedMessageIds.set(messageId, contextSnapshot === undefined ? captureContextEnhancement(
443
- this.#contextEnhancement,
444
- message.conversationType === '1' || message.conversationType === 1 ? 'direct'
445
- : message.conversationType === '2' || message.conversationType === 2 ? 'group' : null,
446
- ) : contextSnapshot);
449
+ const conversationType = String(message.conversationType) === '2' ? 'group'
450
+ : String(message.conversationType) === '1' ? 'direct' : null;
447
451
 
448
452
  let key;
449
453
  try {
450
454
  key = conversationKey(message, sender);
451
455
  } catch {
452
- this.#acceptedMessageIds.delete(messageId);
453
456
  increment(this.#status, 'messagesRejected');
454
457
  this.#status.lastRejectedAt = new Date().toISOString();
455
458
  return Promise.resolve();
@@ -461,9 +464,6 @@ export class DingtalkHarnessBridge {
461
464
  } catch {
462
465
  // An unsafe reply route must never be able to submit an approval.
463
466
  }
464
- if (sessionWebhook && String(message.conversationType) !== '2') {
465
- rememberConnectionTestTarget(this.#state, { sessionWebhook });
466
- }
467
467
  const pending = this.#pendingInteractions.get(key);
468
468
  const promptMessage = dingtalkInboundMessage(message, {
469
469
  api: this.#api,
@@ -473,6 +473,26 @@ export class DingtalkHarnessBridge {
473
473
  const commandText = nonEmptyString(promptMessage.content) ?? '';
474
474
  const addressed = String(message.conversationType) !== '2' || message?.isInAtList === true;
475
475
  const direct = String(message.conversationType) !== '2';
476
+ if (addressed) {
477
+ const access = evaluateInboundAccess(this.#accessPolicy, {
478
+ conversationType,
479
+ senderIds: sender,
480
+ text: commandText,
481
+ hasImages: hasInboundImages(promptMessage),
482
+ hasFiles: hasInboundFiles(promptMessage),
483
+ });
484
+ if (!access.allowed) {
485
+ this.#acceptedMessageIds.set(messageId, null);
486
+ return this.#finishAccessDecision(message, messageId, sessionWebhook, access);
487
+ }
488
+ }
489
+ this.#acceptedMessageIds.set(messageId, contextSnapshot === undefined ? captureContextEnhancement(
490
+ this.#contextEnhancement,
491
+ conversationType,
492
+ ) : contextSnapshot);
493
+ if (sessionWebhook && direct) {
494
+ rememberConnectionTestTarget(this.#state, { sessionWebhook });
495
+ }
476
496
  const statusReaction = sessionWebhook && addressed ? this.#startStatusReaction(message) : null;
477
497
  const finish = (task) => Promise.resolve(task).then(
478
498
  (value) => {
@@ -855,6 +875,38 @@ export class DingtalkHarnessBridge {
855
875
  return task;
856
876
  }
857
877
 
878
+ #finishAccessDecision(message, messageId, sessionWebhook, access) {
879
+ let task;
880
+ task = Promise.resolve().then(async () => {
881
+ if (this.#state.hasSeen(messageId)) return;
882
+ await this.#state.markSeen(messageId);
883
+ if (access.reason === 'command-not-allowed' && sessionWebhook) {
884
+ increment(this.#status, 'messagesReceived');
885
+ this.#status.lastMessageAt = new Date().toISOString();
886
+ await this.#send(
887
+ sessionWebhook,
888
+ t(COMMAND_PERMISSION_DENIED_MESSAGE),
889
+ this.#atUsersFor(message),
890
+ );
891
+ increment(this.#status, 'messagesReplied');
892
+ this.#status.lastReplyAt = new Date().toISOString();
893
+ } else {
894
+ increment(this.#status, 'messagesRejected');
895
+ this.#status.lastRejectedAt = new Date().toISOString();
896
+ }
897
+ this.#status.lastError = null;
898
+ }).catch((error) => {
899
+ if (this.#signal?.aborted) return;
900
+ this.#status.lastError = error?.message ?? String(error);
901
+ this.#logger.error?.('[dsh-dingtalk] failed to apply inbound access policy', error);
902
+ }).finally(() => {
903
+ this.#acceptedMessageIds.delete(messageId);
904
+ this.#commandTasks.delete(task);
905
+ });
906
+ this.#commandTasks.add(task);
907
+ return task;
908
+ }
909
+
858
910
  async #process(message, messageId, sender, key, {
859
911
  alreadyRecorded = false,
860
912
  preparedMessage,
@@ -130,6 +130,7 @@ export class DingtalkRuntime {
130
130
  #harness;
131
131
  #state;
132
132
  #contextEnhancement;
133
+ #accessPolicy;
133
134
  #logger;
134
135
  #replyTimeoutMs;
135
136
  #maxMessageChars;
@@ -152,6 +153,7 @@ export class DingtalkRuntime {
152
153
  harness,
153
154
  state,
154
155
  contextEnhancement,
156
+ accessPolicy,
155
157
  logger = console,
156
158
  replyTimeoutMs = 600_000,
157
159
  maxMessageChars = 4_000,
@@ -170,6 +172,7 @@ export class DingtalkRuntime {
170
172
  this.#harness = harness;
171
173
  this.#state = state;
172
174
  this.#contextEnhancement = contextEnhancement;
175
+ this.#accessPolicy = accessPolicy;
173
176
  this.#logger = logger;
174
177
  this.#replyTimeoutMs = replyTimeoutMs;
175
178
  this.#maxMessageChars = maxMessageChars;
@@ -238,6 +241,7 @@ export class DingtalkRuntime {
238
241
  harness: this.#harness,
239
242
  state: this.#state,
240
243
  contextEnhancement: this.#contextEnhancement,
244
+ accessPolicy: this.#accessPolicy,
241
245
  status: this.#status,
242
246
  logger: this.#logger,
243
247
  replyTimeoutMs: this.#replyTimeoutMs,
@@ -3,6 +3,7 @@ import { fetchFileStream } from '../shared/file-download.mjs';
3
3
  import { fetchImageBuffer } from '../shared/image-prompt.mjs';
4
4
  import { t } from '../shared/i18n.mjs';
5
5
  import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
6
+ import { evaluateInboundAccess } from '../shared/inbound-access.mjs';
6
7
  import { DiscordApi } from './discord-api.mjs';
7
8
  import { createDiscordBridgeStatus, DiscordHarnessBridge } from './discord-bridge.mjs';
8
9
 
@@ -433,6 +434,7 @@ export class DiscordRuntime {
433
434
  #harness;
434
435
  #state;
435
436
  #contextEnhancement;
437
+ #accessPolicy;
436
438
  #logger;
437
439
  #replyTimeoutMs;
438
440
  #connectTimeoutMs;
@@ -464,6 +466,7 @@ export class DiscordRuntime {
464
466
  harness,
465
467
  state,
466
468
  contextEnhancement,
469
+ accessPolicy,
467
470
  logger = console,
468
471
  replyTimeoutMs = 600_000,
469
472
  connectTimeoutMs = 20_000,
@@ -480,6 +483,7 @@ export class DiscordRuntime {
480
483
  this.#harness = harness;
481
484
  this.#state = state;
482
485
  this.#contextEnhancement = contextEnhancement;
486
+ this.#accessPolicy = accessPolicy;
483
487
  this.#logger = logger;
484
488
  this.#replyTimeoutMs = replyTimeoutMs;
485
489
  this.#connectTimeoutMs = connectTimeoutMs;
@@ -559,6 +563,7 @@ export class DiscordRuntime {
559
563
  harness: this.#harness,
560
564
  state: this.#state,
561
565
  contextEnhancement: this.#contextEnhancement,
566
+ accessPolicy: this.#accessPolicy,
562
567
  status: this.#status,
563
568
  logger: this.#logger,
564
569
  replyTimeoutMs: this.#replyTimeoutMs,
@@ -730,6 +735,24 @@ export class DiscordRuntime {
730
735
  async #acceptMessage(message, bridge) {
731
736
  const messageId = String(message?.id ?? '');
732
737
  if (!messageId || this.#state.hasSeen(messageId)) return;
738
+ const preflight = normalizeDiscordMessage(message, this.#config.platformId);
739
+ let accessDecision;
740
+ if (preflight?.kind === 'group' && preflight.addressed === true
741
+ && preflight.senderIsBot !== true) {
742
+ accessDecision = evaluateInboundAccess(this.#accessPolicy, {
743
+ conversationType: 'group',
744
+ senderIds: [preflight.senderId],
745
+ text: preflight.content,
746
+ hasImages: preflight.images.length > 0,
747
+ hasFiles: preflight.files.length > 0,
748
+ });
749
+ if (!accessDecision.allowed) {
750
+ // Let the shared bridge apply its normal silent/command-denial behavior,
751
+ // but do so against the source channel before creating a Thread.
752
+ await bridge.accept(preflight, { accessDecision });
753
+ return;
754
+ }
755
+ }
733
756
  let route = this.#routing.get(messageId);
734
757
  if (!route) {
735
758
  const contextSnapshot = captureContextEnhancement(
@@ -750,7 +773,12 @@ export class DiscordRuntime {
750
773
  }
751
774
  try {
752
775
  const normalized = await route.pendingRoute;
753
- if (normalized) await bridge.accept(normalized, { contextSnapshot: route.contextSnapshot });
776
+ if (normalized) {
777
+ await bridge.accept(normalized, {
778
+ contextSnapshot: route.contextSnapshot,
779
+ ...(accessDecision ? { accessDecision } : {}),
780
+ });
781
+ }
754
782
  } catch (error) {
755
783
  if (error?.code === 'discord-thread-create-uncertain') {
756
784
  await this.#state.markSeen(messageId);