@xmanrui/dsh-im 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +951 -626
- package/lib/index.js +233 -233
- package/package.json +1 -1
- package/plugin-src/client/access-policy-settings.js +351 -0
- package/plugin-src/client/channel-card-meta.js +2 -1
- package/plugin-src/client/channels/dingtalk/api.js +5 -0
- package/plugin-src/client/channels/dingtalk/index.js +1 -0
- package/plugin-src/client/channels/feishu/api.js +5 -0
- package/plugin-src/client/channels/feishu/index.js +1 -0
- package/plugin-src/client/channels/qq/api.js +5 -0
- package/plugin-src/client/channels/qq/index.js +1 -0
- package/plugin-src/client/channels/shared/token-api.js +5 -0
- package/plugin-src/client/channels/shared/token-channel.js +1 -0
- package/plugin-src/client/channels/telegram/api.js +2 -17
- package/plugin-src/client/channels/telegram/index.js +0 -112
- package/plugin-src/client/channels/telegram/styles.js +0 -28
- package/plugin-src/client/channels/wecom/api.js +5 -0
- package/plugin-src/client/channels/wecom/index.js +1 -0
- package/plugin-src/client/channels/weixin/api.js +5 -0
- package/plugin-src/client/channels/weixin/index.js +1 -0
- package/plugin-src/client/channels/whatsapp/api.js +4 -10
- package/plugin-src/client/channels/whatsapp/index.js +1 -125
- package/plugin-src/client/channels/whatsapp/styles.js +0 -25
- package/plugin-src/client/delivery-settings.js +61 -9
- package/plugin-src/client/i18n.js +56 -1
- package/plugin-src/client/index.js +1 -0
- package/plugin-src/client/styles.js +50 -4
- package/plugin-src/host/channels/dingtalk/production.mjs +12 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/feishu/production.mjs +39 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +19 -2
- package/plugin-src/host/channels/qq/production.mjs +12 -1
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/access-policy-production.mjs +137 -0
- package/plugin-src/host/channels/shared/access-policy-rpc.mjs +17 -0
- package/plugin-src/host/channels/shared/production.mjs +15 -1
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/slack/production.mjs +12 -1
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- package/plugin-src/host/channels/telegram/rpc.mjs +2 -49
- package/plugin-src/host/channels/wecom/production.mjs +12 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +12 -1
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +16 -2
- package/plugin-src/host/channels/whatsapp/rpc.mjs +10 -15
- package/src/channels/dingtalk/dingtalk-bridge.mjs +61 -9
- package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -0
- package/src/channels/discord/discord-runtime.mjs +29 -1
- package/src/channels/feishu/bridge.mjs +188 -80
- package/src/channels/feishu/feishu-runtime.mjs +4 -0
- package/src/channels/qq/qq-bridge.mjs +59 -17
- package/src/channels/qq/qq-runtime.mjs +20 -2
- package/src/channels/shared/access-policy.mjs +210 -0
- package/src/channels/shared/bot-workspace-store.mjs +176 -33
- package/src/channels/shared/command-permission.mjs +32 -0
- package/src/channels/shared/compact-command.mjs +5 -2
- package/src/channels/shared/i18n-en/shared-c.mjs +2 -0
- package/src/channels/shared/inbound-access.mjs +43 -0
- package/src/channels/shared/text-harness-bridge.mjs +47 -4
- package/src/channels/shared/workspace-command.mjs +10 -1
- package/src/channels/slack/slack-runtime.mjs +4 -0
- package/src/channels/telegram/telegram-runtime.mjs +5 -13
- package/src/channels/wecom/wecom-bridge.mjs +51 -4
- package/src/channels/wecom/wecom-runtime.mjs +4 -0
- package/src/channels/weixin/weixin-bridge.mjs +59 -16
- package/src/channels/weixin/weixin-runtime.mjs +4 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -20
package/package.json
CHANGED
|
@@ -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) ?? '已安全保存',
|
|
@@ -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),
|
|
@@ -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),
|
|
@@ -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 =
|
|
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;
|