@xmanrui/dsh-im 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.en.md +4 -2
  2. package/README.md +4 -2
  3. package/lib/client.js +1267 -787
  4. package/lib/index.js +237 -235
  5. package/package.json +1 -1
  6. package/plugin-src/client/access-policy-settings.js +351 -0
  7. package/plugin-src/client/channel-card-meta.js +2 -1
  8. package/plugin-src/client/channels/dingtalk/api.js +5 -0
  9. package/plugin-src/client/channels/dingtalk/index.js +1 -0
  10. package/plugin-src/client/channels/feishu/api.js +5 -0
  11. package/plugin-src/client/channels/feishu/index.js +1 -0
  12. package/plugin-src/client/channels/qq/api.js +5 -0
  13. package/plugin-src/client/channels/qq/index.js +1 -0
  14. package/plugin-src/client/channels/shared/token-api.js +5 -0
  15. package/plugin-src/client/channels/shared/token-channel.js +1 -0
  16. package/plugin-src/client/channels/telegram/api.js +2 -17
  17. package/plugin-src/client/channels/telegram/index.js +0 -112
  18. package/plugin-src/client/channels/telegram/styles.js +0 -28
  19. package/plugin-src/client/channels/wecom/api.js +5 -0
  20. package/plugin-src/client/channels/wecom/index.js +1 -0
  21. package/plugin-src/client/channels/weixin/api.js +5 -0
  22. package/plugin-src/client/channels/weixin/index.js +1 -0
  23. package/plugin-src/client/channels/whatsapp/api.js +4 -10
  24. package/plugin-src/client/channels/whatsapp/index.js +1 -125
  25. package/plugin-src/client/channels/whatsapp/styles.js +0 -25
  26. package/plugin-src/client/context-enhancement.js +207 -102
  27. package/plugin-src/client/delivery-settings.js +61 -9
  28. package/plugin-src/client/i18n.js +71 -3
  29. package/plugin-src/client/index.js +1 -0
  30. package/plugin-src/client/styles.js +59 -8
  31. package/plugin-src/host/channels/dingtalk/production.mjs +12 -1
  32. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  33. package/plugin-src/host/channels/feishu/production.mjs +39 -1
  34. package/plugin-src/host/channels/feishu/rpc.mjs +19 -2
  35. package/plugin-src/host/channels/qq/production.mjs +12 -1
  36. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  37. package/plugin-src/host/channels/shared/access-policy-production.mjs +137 -0
  38. package/plugin-src/host/channels/shared/access-policy-rpc.mjs +17 -0
  39. package/plugin-src/host/channels/shared/production.mjs +15 -1
  40. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  41. package/plugin-src/host/channels/slack/production.mjs +12 -1
  42. package/plugin-src/host/channels/slack/rpc.mjs +10 -0
  43. package/plugin-src/host/channels/telegram/rpc.mjs +2 -49
  44. package/plugin-src/host/channels/wecom/production.mjs +12 -1
  45. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  46. package/plugin-src/host/channels/weixin/production.mjs +12 -1
  47. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  48. package/plugin-src/host/channels/whatsapp/production.mjs +16 -2
  49. package/plugin-src/host/channels/whatsapp/rpc.mjs +10 -15
  50. package/scripts/verify-package.mjs +4 -3
  51. package/src/channels/dingtalk/dingtalk-bridge.mjs +61 -9
  52. package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -0
  53. package/src/channels/discord/discord-runtime.mjs +29 -1
  54. package/src/channels/feishu/bridge.mjs +286 -120
  55. package/src/channels/feishu/feishu-runtime.mjs +4 -0
  56. package/src/channels/feishu/message-utils.mjs +5 -0
  57. package/src/channels/qq/qq-bridge.mjs +59 -17
  58. package/src/channels/qq/qq-runtime.mjs +25 -3
  59. package/src/channels/shared/access-policy.mjs +210 -0
  60. package/src/channels/shared/bot-workspace-store.mjs +176 -33
  61. package/src/channels/shared/command-permission.mjs +32 -0
  62. package/src/channels/shared/compact-command.mjs +5 -2
  63. package/src/channels/shared/context-enhancement.mjs +73 -20
  64. package/src/channels/shared/i18n-en/shared-c.mjs +2 -0
  65. package/src/channels/shared/inbound-access.mjs +43 -0
  66. package/src/channels/shared/text-harness-bridge.mjs +47 -4
  67. package/src/channels/shared/workspace-command.mjs +10 -1
  68. package/src/channels/slack/slack-runtime.mjs +4 -0
  69. package/src/channels/telegram/telegram-runtime.mjs +5 -13
  70. package/src/channels/wecom/wecom-bridge.mjs +51 -4
  71. package/src/channels/wecom/wecom-runtime.mjs +4 -0
  72. package/src/channels/weixin/weixin-bridge.mjs +59 -16
  73. package/src/channels/weixin/weixin-runtime.mjs +4 -0
  74. package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -20
@@ -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);
@@ -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);
@@ -142,12 +142,13 @@ if (client.includes('settings.plugins.tab') || clientSources.includes('settings.
142
142
  throw new Error('client source or bundle still contains the legacy Plugins-tab settings entry');
143
143
  }
144
144
  // Connections still have no channel-enable toggle. Only the shared context
145
- // editor owns checkable inputs: two scope switches and one mapped field input.
145
+ // editor owns checkable inputs. Its reusable scope component contains one
146
+ // switch template and one mapped field-input template; it renders both twice.
146
147
  const contextEditorSource = await readFile(resolve(root, 'plugin-src/client/context-enhancement.js'), 'utf8');
147
148
  const otherClientSources = clientSources.replace(contextEditorSource, '');
148
149
  if (/role:\s*["']switch|type:\s*["']checkbox/.test(otherClientSources)
149
- || (client.match(/role:\s*["']switch["']/g) ?? []).length !== 2
150
- || (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 3) {
150
+ || (client.match(/role:\s*["']switch["']/g) ?? []).length !== 1
151
+ || (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 2) {
151
152
  throw new Error('checkable inputs must be limited to the context-enhancement editor');
152
153
  }
153
154
  for (const marker of ['bot.context-enhancement.set', '<dsh_im_source>', '<dsh_im_source_guidance>']) {
@@ -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,