@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "4.2.1",
3
+ "version": "4.4.0",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -0,0 +1,351 @@
1
+ import * as React from 'react';
2
+
3
+ import {
4
+ DEFAULT_ACCESS_POLICY,
5
+ normalizeAccessPolicy,
6
+ validateAccessPolicy,
7
+ } from '../../src/channels/shared/access-policy.mjs';
8
+ import { h, localizeText } from './i18n.js';
9
+
10
+ export const ACCESS_POLICY_ENDPOINT = 'bot.access-policy.set';
11
+
12
+ export const ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
13
+ weixin: Object.freeze({
14
+ directUserLabel: '微信用户 ID',
15
+ directPlaceholder: '填写微信用户 ID',
16
+ groupSupported: false,
17
+ }),
18
+ feishu: Object.freeze({
19
+ directUserLabel: '飞书 Open ID',
20
+ directPlaceholder: 'ou_xxx',
21
+ groupUserLabel: '群成员 Open ID',
22
+ groupPlaceholder: 'ou_xxx',
23
+ }),
24
+ dingtalk: Object.freeze({
25
+ directUserLabel: '钉钉用户 ID',
26
+ directPlaceholder: '填写 senderStaffId 或 senderId',
27
+ groupUserLabel: '群成员用户 ID',
28
+ groupPlaceholder: '填写 senderStaffId 或 senderId',
29
+ }),
30
+ wecom: Object.freeze({
31
+ directUserLabel: '企业微信用户 ID',
32
+ directPlaceholder: '填写 userid',
33
+ groupUserLabel: '群成员用户 ID',
34
+ groupPlaceholder: '填写 userid',
35
+ }),
36
+ qq: Object.freeze({
37
+ directUserLabel: 'QQ User Open ID',
38
+ directPlaceholder: '填写 user_openid',
39
+ groupUserLabel: '群成员 Open ID',
40
+ groupPlaceholder: '填写 member_openid',
41
+ }),
42
+ slack: Object.freeze({
43
+ directUserLabel: 'Slack User ID',
44
+ directPlaceholder: 'U0123456789',
45
+ groupUserLabel: '群成员 User ID',
46
+ groupPlaceholder: 'U0123456789',
47
+ }),
48
+ telegram: Object.freeze({
49
+ directUserLabel: 'Telegram User ID',
50
+ directPlaceholder: '填写数字 User ID',
51
+ groupUserLabel: '群成员 User ID',
52
+ groupPlaceholder: '填写数字 User ID',
53
+ }),
54
+ discord: Object.freeze({
55
+ directUserLabel: 'Discord User ID',
56
+ directPlaceholder: '填写数字 User ID',
57
+ groupUserLabel: '群成员 User ID',
58
+ groupPlaceholder: '填写数字 User ID',
59
+ }),
60
+ whatsapp: Object.freeze({
61
+ directUserLabel: 'WhatsApp 电话号码或 JID',
62
+ directPlaceholder: '8613800000000 或完整 JID',
63
+ groupUserLabel: '群成员电话号码或 JID',
64
+ groupPlaceholder: '8613800000000 或完整 JID',
65
+ }),
66
+ });
67
+
68
+ function clonePolicy(policy) {
69
+ const cloneScope = (scope) => ({
70
+ mode: scope.mode,
71
+ open: {
72
+ defaultCanExecuteCommands: scope.open.defaultCanExecuteCommands,
73
+ commandPermissionOverrides: scope.open.commandPermissionOverrides.map((user) => ({
74
+ ...user,
75
+ })),
76
+ },
77
+ allowlist: {
78
+ users: scope.allowlist.users.map((user) => ({ ...user })),
79
+ },
80
+ });
81
+ return {
82
+ direct: cloneScope(policy.direct),
83
+ group: cloneScope(policy.group),
84
+ };
85
+ }
86
+
87
+ function unwrapRpcResult(result) {
88
+ if (result?.ok === true) return result.value;
89
+ if (result?.ok === false) {
90
+ const error = new Error(result.error?.message || '访问设置保存失败,请稍后重试。');
91
+ error.code = result.error?.code;
92
+ throw error;
93
+ }
94
+ return result;
95
+ }
96
+
97
+ function policyFromSnapshot(value, botId) {
98
+ const source = value?.snapshot ?? value;
99
+ const bot = Array.isArray(source?.bots)
100
+ ? source.bots.find((entry) => entry?.botId === botId)
101
+ : null;
102
+ return normalizeAccessPolicy(bot?.accessPolicy ?? source?.accessPolicy ?? source?.policy);
103
+ }
104
+
105
+ function commandValue(value) {
106
+ return value === 'allow';
107
+ }
108
+
109
+ function ScenePolicyEditor({
110
+ scene,
111
+ title,
112
+ policy,
113
+ userLabel,
114
+ placeholder,
115
+ disabled = false,
116
+ unsupported = false,
117
+ onChange,
118
+ }) {
119
+ const ownerHelpId = React.useId();
120
+ const emptyAllowlistHelpId = React.useId();
121
+ const allowlist = policy.mode === 'allowlist';
122
+ const collectionKey = allowlist ? 'users' : 'commandPermissionOverrides';
123
+ const branchKey = allowlist ? 'allowlist' : 'open';
124
+ const users = policy[branchKey][collectionKey];
125
+ const emptyAllowlist = allowlist && users.length === 0;
126
+ const updateUsers = (nextUsers) => onChange({
127
+ ...policy,
128
+ [branchKey]: {
129
+ ...policy[branchKey],
130
+ [collectionKey]: nextUsers,
131
+ },
132
+ });
133
+ const updateUser = (index, patch) => updateUsers(users.map((user, userIndex) => (
134
+ userIndex === index ? { ...user, ...patch } : user
135
+ )));
136
+
137
+ return h('fieldset', {
138
+ className: 'dim-accessScene',
139
+ disabled,
140
+ 'data-scene': scene,
141
+ 'aria-label': localizeText(title),
142
+ },
143
+ h('legend', null,
144
+ h('span', { className: 'dim-accessLegendContent' },
145
+ h('span', null, title),
146
+ h('span', { className: 'dim-channelHelp dim-accessLegendHelp' },
147
+ h('button', {
148
+ type: 'button',
149
+ className: 'dim-channelHelpButton',
150
+ 'aria-label': [localizeText(title), localizeText('查看访问权限说明')].join(' '),
151
+ 'aria-describedby': ownerHelpId,
152
+ }, h('span', { 'aria-hidden': true }, '?')),
153
+ h('span', {
154
+ id: ownerHelpId,
155
+ className: 'dim-channelTooltip dim-accessHelpTooltip',
156
+ role: 'tooltip',
157
+ }, '原所有者或扫码接入者始终可以访问并执行命令;以下设置仅约束其他用户。')))),
158
+ unsupported
159
+ ? h('div', { className: 'dim-accessUnsupported', role: 'note' },
160
+ h('strong', null, '当前渠道不支持群聊'),
161
+ h('p', null, '此区域无需配置,保存私聊设置时会保留现有群聊策略。'))
162
+ : h(React.Fragment, null,
163
+ h('div', { className: 'dim-accessControls', 'data-mode': policy.mode },
164
+ h('label', { className: 'dim-accessField' },
165
+ h('span', null, '访问模式'),
166
+ h('select', {
167
+ value: policy.mode,
168
+ 'aria-label': [localizeText(title), localizeText('访问模式')].join(' '),
169
+ onChange: (event) => onChange({ ...policy, mode: event.target.value }),
170
+ },
171
+ h('option', { value: 'open' }, '允许所有用户'),
172
+ h('option', { value: 'allowlist' }, '仅白名单用户'))),
173
+ allowlist ? null : h('label', { className: 'dim-accessField' },
174
+ h('span', null, '默认命令权限'),
175
+ h('select', {
176
+ value: policy.open.defaultCanExecuteCommands ? 'allow' : 'deny',
177
+ 'aria-label': [localizeText(title), localizeText('默认命令权限')].join(' '),
178
+ onChange: (event) => onChange({
179
+ ...policy,
180
+ open: {
181
+ ...policy.open,
182
+ defaultCanExecuteCommands: commandValue(event.target.value),
183
+ },
184
+ }),
185
+ },
186
+ h('option', { value: 'allow' }, '可以执行命令'),
187
+ h('option', { value: 'deny' }, '不可以执行命令')))),
188
+ h('div', { className: 'dim-accessUsers' },
189
+ h('div', { className: 'dim-accessUsersHeading' },
190
+ h('div', { className: 'dim-accessUsersTitle' },
191
+ h('strong', null, allowlist ? '白名单用户' : '命令权限例外'),
192
+ emptyAllowlist
193
+ ? h('span', { className: 'dim-channelHelp dim-accessUsersHelp' },
194
+ h('button', {
195
+ type: 'button',
196
+ className: 'dim-channelHelpButton',
197
+ 'aria-label': [localizeText(title), localizeText('查看白名单说明')].join(' '),
198
+ 'aria-describedby': emptyAllowlistHelpId,
199
+ }, h('span', { 'aria-hidden': true }, '?')),
200
+ h('span', {
201
+ id: emptyAllowlistHelpId,
202
+ className: 'dim-channelTooltip dim-accessEmptyAllowlistTooltip',
203
+ role: 'tooltip',
204
+ }, '当前没有白名单用户,保存后普通用户将无法使用机器人。'))
205
+ : null),
206
+ h('button', {
207
+ type: 'button',
208
+ className: 'dim-deliveryButton dim-accessAddUser',
209
+ 'aria-label': [localizeText(title), localizeText('新增用户')].join(' '),
210
+ title: localizeText('新增用户'),
211
+ onClick: () => updateUsers([...users, {
212
+ id: '',
213
+ canExecuteCommands: allowlist
214
+ ? false
215
+ : !policy.open.defaultCanExecuteCommands,
216
+ }]),
217
+ }, h('span', { 'aria-hidden': true }, '+'))),
218
+ users.length === 0
219
+ ? h('div', { className: 'dim-accessUsersEmpty' }, '尚未添加用户')
220
+ : h('ul', { className: 'dim-accessUserList' }, users.map((user, index) =>
221
+ h('li', { key: `${scene}-${policy.mode}-${index}`, className: 'dim-accessUserRow' },
222
+ h('label', { className: 'dim-accessField dim-accessUserId' },
223
+ h('span', null, userLabel),
224
+ h('input', {
225
+ value: user.id,
226
+ maxLength: 256,
227
+ required: true,
228
+ autoCapitalize: 'none',
229
+ autoCorrect: 'off',
230
+ spellCheck: false,
231
+ placeholder,
232
+ 'aria-label': [localizeText(title), localizeText(userLabel), index + 1].join(' '),
233
+ onChange: (event) => updateUser(index, { id: event.target.value }),
234
+ })),
235
+ h('label', { className: 'dim-accessField dim-accessUserCommand' },
236
+ h('span', null, '命令权限'),
237
+ h('select', {
238
+ value: user.canExecuteCommands ? 'allow' : 'deny',
239
+ 'aria-label': [
240
+ localizeText(title), localizeText('用户'), index + 1,
241
+ localizeText('命令权限'),
242
+ ].join(' '),
243
+ onChange: (event) => updateUser(index, {
244
+ canExecuteCommands: commandValue(event.target.value),
245
+ }),
246
+ },
247
+ h('option', { value: 'allow' }, '可以执行命令'),
248
+ h('option', { value: 'deny' }, '不可以执行命令'))),
249
+ h('button', {
250
+ type: 'button',
251
+ className: 'dim-deliveryButton dim-accessDeleteUser',
252
+ 'data-kind': 'danger',
253
+ 'aria-label': [
254
+ localizeText(title), localizeText('删除'),
255
+ localizeText('用户'), index + 1,
256
+ ].join(' '),
257
+ onClick: () => updateUsers(users.filter((_, userIndex) => userIndex !== index)),
258
+ }, '删除')))))));
259
+ }
260
+
261
+ export function AccessPolicySettingsPage({ channel, account, rpcCall, onSaved }) {
262
+ const definition = ACCESS_CHANNEL_DEFINITIONS[channel];
263
+ const initialPolicy = normalizeAccessPolicy(account?.accessPolicy);
264
+ const initialKey = JSON.stringify(initialPolicy);
265
+ const [draft, setDraft] = React.useState(() => clonePolicy(
266
+ initialPolicy ?? DEFAULT_ACCESS_POLICY,
267
+ ));
268
+ const [saving, setSaving] = React.useState(false);
269
+ const [feedback, setFeedback] = React.useState(null);
270
+
271
+ React.useEffect(() => {
272
+ const next = normalizeAccessPolicy(account?.accessPolicy);
273
+ setDraft(clonePolicy(next ?? DEFAULT_ACCESS_POLICY));
274
+ }, [account?.botId, initialKey]);
275
+
276
+ React.useEffect(() => {
277
+ setFeedback(null);
278
+ }, [account?.botId]);
279
+
280
+ if (!definition) {
281
+ return h('div', { className: 'dim-accessState', role: 'alert' },
282
+ '当前渠道暂不支持访问设置。');
283
+ }
284
+
285
+ const save = async (event) => {
286
+ event.preventDefault();
287
+ setFeedback(null);
288
+ setSaving(true);
289
+ try {
290
+ const policy = validateAccessPolicy(draft);
291
+ if (typeof rpcCall !== 'function') throw new Error('访问设置暂不可用。');
292
+ const value = unwrapRpcResult(await rpcCall(ACCESS_POLICY_ENDPOINT, {
293
+ botId: account.botId,
294
+ policy,
295
+ }));
296
+ const saved = policyFromSnapshot(value, account.botId);
297
+ if (!saved) throw new Error('服务没有返回已保存的访问策略,请刷新后重试。');
298
+ setDraft(clonePolicy(saved));
299
+ onSaved?.(saved);
300
+ setFeedback({ tone: 'success', message: '访问设置已保存。' });
301
+ } catch (error) {
302
+ setFeedback({
303
+ tone: 'error',
304
+ message: error?.message || '访问设置保存失败,请稍后重试。',
305
+ });
306
+ } finally {
307
+ setSaving(false);
308
+ }
309
+ };
310
+
311
+ return h('form', {
312
+ className: 'dim-accessPage',
313
+ onSubmit: (event) => void save(event),
314
+ },
315
+ initialPolicy
316
+ ? null
317
+ : h('div', { className: 'dim-accessState', role: 'alert' },
318
+ '访问策略尚未就绪,请返回机器人列表刷新后重试。'),
319
+ h(ScenePolicyEditor, {
320
+ scene: 'direct',
321
+ title: '私聊',
322
+ policy: draft.direct,
323
+ userLabel: definition.directUserLabel,
324
+ placeholder: definition.directPlaceholder,
325
+ disabled: saving,
326
+ onChange: (direct) => { setDraft((current) => ({ ...current, direct })); setFeedback(null); },
327
+ }),
328
+ h(ScenePolicyEditor, {
329
+ scene: 'group',
330
+ title: '群聊',
331
+ policy: draft.group,
332
+ userLabel: definition.groupUserLabel ?? definition.directUserLabel,
333
+ placeholder: definition.groupPlaceholder ?? definition.directPlaceholder,
334
+ disabled: saving || definition.groupSupported === false,
335
+ unsupported: definition.groupSupported === false,
336
+ onChange: (group) => { setDraft((current) => ({ ...current, group })); setFeedback(null); },
337
+ }),
338
+ feedback ? h('p', {
339
+ className: 'dim-accessFeedback',
340
+ 'data-tone': feedback.tone,
341
+ role: feedback.tone === 'error' ? 'alert' : 'status',
342
+ 'aria-live': 'polite',
343
+ }, feedback.message) : null,
344
+ h('div', { className: 'dim-accessActions' },
345
+ h('button', {
346
+ type: 'submit',
347
+ className: 'dim-deliveryButton',
348
+ 'data-kind': 'primary',
349
+ disabled: saving || !initialPolicy,
350
+ }, saving ? '正在保存…' : '保存访问设置')));
351
+ }
@@ -22,7 +22,7 @@ function SettingsGlyph() {
22
22
  h('path', { d: 'M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06a1.7 1.7 0 0 0-1.88-.34 1.7 1.7 0 0 0-1.03 1.55V21h-4v-.08A1.7 1.7 0 0 0 8.97 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.52-1.03H3v-4h.08A1.7 1.7 0 0 0 4.6 8.97a1.7 1.7 0 0 0-.34-1.88l-.06-.06L7.03 4.2l.06.06a1.7 1.7 0 0 0 1.88.34A1.7 1.7 0 0 0 10 3.08V3h4v.08a1.7 1.7 0 0 0 1.03 1.52 1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83-.06.06a1.7 1.7 0 0 0-.34 1.88A1.7 1.7 0 0 0 20.92 10H21v4h-.08A1.7 1.7 0 0 0 19.4 15Z' }));
23
23
  }
24
24
 
25
- export function BotSettingsButton({ channel, botId, botName, connected }) {
25
+ export function BotSettingsButton({ channel, botId, botName, connected, accessPolicy }) {
26
26
  const { openBotSettings } = React.useContext(BotSettingsContext);
27
27
  const tooltipId = React.useId();
28
28
  return h('span', { className: 'dim-botSettingsAction' },
@@ -37,6 +37,7 @@ export function BotSettingsButton({ channel, botId, botName, connected }) {
37
37
  botId,
38
38
  botName,
39
39
  connected: Boolean(connected),
40
+ accessPolicy,
40
41
  }),
41
42
  }, h(SettingsGlyph)),
42
43
  h('span', {
@@ -1,5 +1,6 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
2
  import { normalizeLastMessageError } from '../../last-message-error.js';
3
+ import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
3
4
  import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
4
5
 
5
6
  export const DINGTALK_RPC_CHANNEL = '/dingtalk';
@@ -15,6 +16,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
15
16
  setWorkspace: 'bot.workspace.set',
16
17
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
17
18
  setContextEnhancement: 'bot.context-enhancement.set',
19
+ setAccessPolicy: 'bot.access-policy.set',
18
20
  });
19
21
 
20
22
  const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
@@ -165,6 +167,9 @@ function normalizeBot(value) {
165
167
  workspace: optionalString(value.workspace, 4_096) ?? '',
166
168
  agentPreset: normalizeAgentPresetId(value.agentPreset),
167
169
  contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
170
+ ...(Object.hasOwn(value, 'accessPolicy')
171
+ ? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
172
+ : {}),
168
173
  bot: {
169
174
  name: optionalString(bot.name, 100) ?? '钉钉机器人',
170
175
  clientIdMasked: optionalString(bot.clientIdMasked, 140) ?? '已安全保存',
@@ -253,6 +253,7 @@ export function AccountCard({
253
253
  botId: account.botId,
254
254
  botName: account.bot.name,
255
255
  connected: account.connected,
256
+ accessPolicy: account.accessPolicy,
256
257
  }))),
257
258
  h(WorkspaceEditor, {
258
259
  workspace: account.workspace,
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId } from "../../agent-preset.js";
10
10
  import { normalizeLastMessageError } from "../../last-message-error.js";
11
+ import { normalizeAccessPolicy } from "../../../../src/channels/shared/access-policy.mjs";
11
12
  import { normalizeContextEnhancementConfig } from "../../../../src/channels/shared/context-enhancement.mjs";
12
13
 
13
14
  export const FEISHU_RPC_CHANNEL = "/feishu";
@@ -26,6 +27,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
26
27
  setWorkspace: "bot.workspace.set",
27
28
  setAgentPreset: "bot.preset.set",
28
29
  setContextEnhancement: "bot.context-enhancement.set",
30
+ setAccessPolicy: "bot.access-policy.set",
29
31
  setGroupResponseMode: "bot.group-response-mode.set",
30
32
  // Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
31
33
  testConnection: "connection.test",
@@ -206,6 +208,9 @@ export function normalizeBotConnection(value, fallbackBotId) {
206
208
  workspace: optionalString(value.workspace)?.slice(0, 4_096) ?? "",
207
209
  agentPreset: normalizeAgentPresetId(value.agentPreset),
208
210
  contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
211
+ ...(Object.hasOwn(value, "accessPolicy")
212
+ ? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
213
+ : {}),
209
214
  groupResponseMode: normalizeGroupResponseMode(value.groupResponseMode),
210
215
  groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
211
216
  bot: normalizeBot(value.bot),
@@ -609,6 +609,7 @@ export function BotCard({
609
609
  botId: connection.botId,
610
610
  botName: bot.name,
611
611
  connected,
612
+ accessPolicy: connection.accessPolicy,
612
613
  })),
613
614
  ),
614
615
  h(WorkspaceEditor, {
@@ -1,5 +1,6 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
2
  import { normalizeLastMessageError } from '../../last-message-error.js';
3
+ import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
3
4
  import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
4
5
 
5
6
  export const QQ_RPC_CHANNEL = '/qq';
@@ -15,6 +16,7 @@ export const QQ_ENDPOINTS = Object.freeze({
15
16
  setWorkspace: 'bot.workspace.set',
16
17
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
17
18
  setContextEnhancement: 'bot.context-enhancement.set',
19
+ setAccessPolicy: 'bot.access-policy.set',
18
20
  });
19
21
 
20
22
  const PROVISION_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting', 'connected', 'failed', 'cancelled']);
@@ -90,6 +92,9 @@ function normalizeBot(value) {
90
92
  workspace: text(value.workspace, '', 4_096),
91
93
  agentPreset: normalizeAgentPresetId(value.agentPreset),
92
94
  contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
95
+ ...(Object.hasOwn(value, 'accessPolicy')
96
+ ? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
97
+ : {}),
93
98
  bot: {
94
99
  name: text(value.bot?.name, 'QQ机器人', 100),
95
100
  appIdMasked: text(value.bot?.appIdMasked, '应用标识已安全保存', 140),
@@ -197,6 +197,7 @@ export function AccountCard({
197
197
  botId: account.botId,
198
198
  botName: account.bot.name,
199
199
  connected: account.connected,
200
+ accessPolicy: account.accessPolicy,
200
201
  }))),
201
202
  h(WorkspaceEditor, {
202
203
  workspace: account.workspace,
@@ -1,5 +1,6 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
2
  import { normalizeLastMessageError } from '../../last-message-error.js';
3
+ import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
3
4
  import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
4
5
 
5
6
  const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
@@ -31,6 +32,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
31
32
  setWorkspace: 'bot.workspace.set',
32
33
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
33
34
  setContextEnhancement: 'bot.context-enhancement.set',
35
+ setAccessPolicy: 'bot.access-policy.set',
34
36
  });
35
37
 
36
38
  export function createTokenChannelApi(channel, connectionSummary, {
@@ -60,6 +62,9 @@ export function createTokenChannelApi(channel, connectionSummary, {
60
62
  workspace: text(value.workspace, '', 4_096),
61
63
  agentPreset: normalizeAgentPresetId(value.agentPreset),
62
64
  contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
65
+ ...(Object.hasOwn(value, 'accessPolicy')
66
+ ? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
67
+ : {}),
63
68
  bot: {
64
69
  name: text(value.bot?.name, `${channel}机器人`, 100),
65
70
  username: text(value.bot?.username, '', 100),
@@ -103,6 +103,7 @@ export function createTokenChannelSettings(definition) {
103
103
  botId: account.botId,
104
104
  botName: account.bot.name,
105
105
  connected: account.connected,
106
+ accessPolicy: account.accessPolicy,
106
107
  }))),
107
108
  h(WorkspaceEditor, {
108
109
  workspace: account.workspace,
@@ -1,24 +1,9 @@
1
1
  import { TOKEN_BOT_ENDPOINTS, createTokenChannelApi } from '../shared/token-api.js';
2
2
 
3
3
  export const TELEGRAM_RPC_CHANNEL = '/telegram';
4
- export const TELEGRAM_ENDPOINTS = Object.freeze({
5
- ...TOKEN_BOT_ENDPOINTS,
6
- setAccessPolicy: 'bot.access-policy.set',
7
- });
4
+ export const TELEGRAM_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
8
5
 
9
- const api = createTokenChannelApi('Telegram', ' Bot API 长轮询', {
10
- normalizeBotExtension: (value) => {
11
- const source = value?.accessPolicy;
12
- const accessMode = source?.accessMode === 'private-allowlist'
13
- ? 'private-allowlist' : 'compatible';
14
- const allowedUsers = Array.isArray(source?.allowedUsers)
15
- ? [...new Set(source.allowedUsers.filter((entry) => (
16
- typeof entry === 'string' && /^[1-9]\d{0,15}$/.test(entry)
17
- )))]
18
- : [];
19
- return { accessPolicy: { accessMode, allowedUsers } };
20
- },
21
- });
6
+ const api = createTokenChannelApi('Telegram', ' Bot API 长轮询');
22
7
 
23
8
  export const unwrapRpcResult = api.unwrapRpcResult;
24
9
  export const normalizeSnapshot = api.normalizeSnapshot;
@@ -1,121 +1,11 @@
1
- import * as React from 'react';
2
-
3
1
  import { TelegramLogoGlyph } from '../../channel-logos.js';
4
2
  import { createTokenChannelSettings } from '../shared/token-channel.js';
5
- import { h } from '../../i18n.js';
6
3
  import {
7
4
  TELEGRAM_ENDPOINTS,
8
5
  telegramClientApi,
9
6
  } from './api.js';
10
7
  import { installTelegramStyles } from './styles.js';
11
8
 
12
- function policyFor(account) {
13
- return {
14
- accessMode: account?.accessPolicy?.accessMode === 'private-allowlist'
15
- ? 'private-allowlist' : 'compatible',
16
- allowedUsers: Array.isArray(account?.accessPolicy?.allowedUsers)
17
- ? account.accessPolicy.allowedUsers : [],
18
- };
19
- }
20
-
21
- function allowedUsersFromText(value) {
22
- const entries = value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
23
- if (entries.some((entry) => !/^[1-9]\d{0,15}$/.test(entry))) {
24
- throw new TypeError('User ID 必须是 1–16 位正整数,每行一个。');
25
- }
26
- return [...new Set(entries)];
27
- }
28
-
29
- export function TelegramAccessSettings({ account, busy = false, onSave }) {
30
- const policy = policyFor(account);
31
- const sourceUsers = policy.allowedUsers.join('\n');
32
- const accessHelpId = React.useId();
33
- const [accessMode, setAccessMode] = React.useState(policy.accessMode);
34
- const [allowedUsers, setAllowedUsers] = React.useState(sourceUsers);
35
- const [error, setError] = React.useState(null);
36
-
37
- React.useEffect(() => {
38
- setAccessMode(policy.accessMode);
39
- setAllowedUsers(sourceUsers);
40
- setError(null);
41
- }, [policy.accessMode, sourceUsers]);
42
-
43
- const save = async (event) => {
44
- event.preventDefault();
45
- setError(null);
46
- try {
47
- const normalized = allowedUsersFromText(allowedUsers);
48
- if (typeof onSave !== 'function') throw new Error('Telegram 访问设置暂不可用。');
49
- await onSave({ accessMode, allowedUsers: normalized });
50
- } catch (caught) {
51
- setError(caught?.message ?? 'Telegram 访问设置保存失败。');
52
- }
53
- };
54
-
55
- const privateAllowlist = accessMode === 'private-allowlist';
56
- const savedPrivateAllowlist = policy.accessMode === 'private-allowlist';
57
- const emptyAllowlist = privateAllowlist && allowedUsers.trim() === '';
58
- return h('form', { className: 'dtg-access', onSubmit: save },
59
- h('div', { className: 'dtg-accessHeading' },
60
- h('strong', null, '访问设置'),
61
- h('span', { className: 'dtg-accessStatus' },
62
- h('span', { className: 'dtg-accessBadge', 'data-mode': policy.accessMode },
63
- savedPrivateAllowlist ? '已生效:安全模式' : '已生效:兼容模式'),
64
- h('span', { className: 'dtg-accessHelp' },
65
- h('button', {
66
- type: 'button',
67
- className: 'dtg-accessHelpButton',
68
- 'aria-label': '查看 Telegram 访问模式说明',
69
- 'aria-describedby': accessHelpId,
70
- }, h('span', { 'aria-hidden': 'true' }, '?')),
71
- h('span', {
72
- id: accessHelpId,
73
- className: 'dtg-accessTooltip',
74
- role: 'tooltip',
75
- },
76
- h('span', { className: 'dtg-accessTooltipItem' },
77
- h('strong', null, '兼容模式'),
78
- h('span', null, '保持原有行为:私聊直接响应,群聊在被提及或回复时响应。')),
79
- h('span', { className: 'dtg-accessTooltipItem' },
80
- h('strong', null, '安全模式'),
81
- h('span', null, '群聊全部忽略,私聊仅允许白名单用户。')))))),
82
- h('label', { className: 'dtg-accessField' },
83
- h('span', null, '模式'),
84
- h('select', {
85
- value: accessMode,
86
- disabled: busy,
87
- 'aria-label': 'Telegram 访问模式',
88
- onChange: (event) => { setAccessMode(event.target.value); setError(null); },
89
- },
90
- h('option', { value: 'compatible' }, '兼容模式(默认)'),
91
- h('option', { value: 'private-allowlist' }, '安全模式(私聊白名单)'))),
92
- h('label', { className: 'dtg-accessField' },
93
- h('span', null, '允许私聊的 Telegram User ID'),
94
- h('textarea', {
95
- value: allowedUsers,
96
- disabled: busy || !privateAllowlist,
97
- rows: 3,
98
- placeholder: '每行一个数字 User ID',
99
- 'aria-label': '允许私聊的 Telegram User ID',
100
- onChange: (event) => { setAllowedUsers(event.target.value); setError(null); },
101
- }),
102
- h('small', null, privateAllowlist
103
- ? '白名单仅属于当前机器人。'
104
- : '兼容模式下暂不使用白名单,切换模式时会保留。')),
105
- emptyAllowlist
106
- ? h('p', { className: 'dtg-accessWarning', role: 'status' },
107
- '白名单为空;保存后该机器人会拒绝所有入站消息。')
108
- : null,
109
- error ? h('p', { className: 'dtg-accessError', role: 'alert' }, error) : null,
110
- h('div', { className: 'dtg-accessActions' },
111
- h('button', {
112
- type: 'submit',
113
- className: 'ddt-button',
114
- 'data-kind': 'secondary',
115
- disabled: busy,
116
- }, busy ? '正在保存…' : '保存访问设置')));
117
- }
118
-
119
9
  const channel = createTokenChannelSettings({
120
10
  channel: 'Telegram',
121
11
  endpoints: TELEGRAM_ENDPOINTS,
@@ -129,8 +19,6 @@ const channel = createTokenChannelSettings({
129
19
  emptyTitle: '接入 Telegram 机器人',
130
20
  emptyDescription: '先通过 @BotFather 获取 Bot Token,再在这里完成接入。',
131
21
  platformLabel: 'Telegram',
132
- AccountSettings: TelegramAccessSettings,
133
- accountSettingsEndpoint: TELEGRAM_ENDPOINTS.setAccessPolicy,
134
22
  });
135
23
 
136
24
  export const TelegramSettingsTab = channel.SettingsTab;