@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
@@ -2,9 +2,10 @@ import * as React from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
 
4
4
  import {
5
+ CONTEXT_DIRECT_GUIDANCE_EXAMPLE,
5
6
  CONTEXT_ENHANCEMENT_FIELDS,
6
7
  CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH,
7
- CONTEXT_GUIDANCE_EXAMPLE,
8
+ CONTEXT_GROUP_GUIDANCE_EXAMPLE,
8
9
  normalizeContextEnhancementConfig,
9
10
  validateContextEnhancementConfig,
10
11
  } from '../../src/channels/shared/context-enhancement.mjs';
@@ -18,11 +19,34 @@ const FIELD_LABELS = Object.freeze({
18
19
  botId: '机器人标识',
19
20
  });
20
21
 
22
+ const SCOPE_COPY = Object.freeze({
23
+ group: Object.freeze({
24
+ title: '群聊',
25
+ enable: '启用',
26
+ fieldsHelpLabel: '查看群聊来源字段说明',
27
+ senderNameHelpLabel: '查看群聊发送者昵称字段说明',
28
+ guidanceLabel: '增强提示词',
29
+ guidanceHelpLabel: '查看群聊增强提示词使用说明',
30
+ guidanceUsage: '用于告诉模型如何使用当前群聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。',
31
+ guidanceBehavior: '仅在群聊开关开启时使用。清空并保存后不再附加群聊增强提示词;所选来源字段仍按当前场景设置发送。',
32
+ }),
33
+ direct: Object.freeze({
34
+ title: '私聊',
35
+ enable: '启用',
36
+ fieldsHelpLabel: '查看私聊来源字段说明',
37
+ senderNameHelpLabel: '查看私聊发送者昵称字段说明',
38
+ guidanceLabel: '增强提示词',
39
+ guidanceHelpLabel: '查看私聊增强提示词使用说明',
40
+ guidanceUsage: '用于告诉模型如何使用当前私聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。',
41
+ guidanceBehavior: '仅在私聊开关开启时使用。清空并保存后不再附加私聊增强提示词;所选来源字段仍按当前场景设置发送。',
42
+ }),
43
+ });
44
+
21
45
  export function contextEnhancementLabel(config) {
22
- const { groupEnabled, directEnabled } = normalizeContextEnhancementConfig(config);
23
- if (groupEnabled && directEnabled) return '群聊和私聊';
24
- if (groupEnabled) return '仅群聊';
25
- if (directEnabled) return '仅私聊';
46
+ const { group, direct } = normalizeContextEnhancementConfig(config);
47
+ if (group.enabled && direct.enabled) return '群聊和私聊';
48
+ if (group.enabled) return '仅群聊';
49
+ if (direct.enabled) return '仅私聊';
26
50
  return '未开启';
27
51
  }
28
52
 
@@ -37,27 +61,132 @@ function ContextIcon({ kind = 'sliders' }) {
37
61
  }, h('path', { d: path }));
38
62
  }
39
63
 
64
+ function ContextEnhancementScopeEditor({
65
+ kind,
66
+ scope,
67
+ example,
68
+ supported,
69
+ busy,
70
+ idPrefix,
71
+ onChange,
72
+ }) {
73
+ const copy = SCOPE_COPY[kind];
74
+ const unavailableId = `${idPrefix}-${kind}-unavailable`;
75
+ const fieldsHelpId = `${idPrefix}-${kind}-fields-help`;
76
+ const senderNameHelpId = `${idPrefix}-${kind}-sender-name-help`;
77
+ const guidanceId = `${idPrefix}-${kind}-guidance`;
78
+ const guidanceHelpId = `${idPrefix}-${kind}-guidance-help`;
79
+ const disabled = busy || !supported;
80
+
81
+ return h('fieldset', {
82
+ className: 'dim-contextSection dim-contextScope',
83
+ 'data-context-kind': kind,
84
+ 'aria-label': copy.title,
85
+ disabled,
86
+ },
87
+ h('label', { className: 'dim-contextSwitchRow' },
88
+ h('span', { className: 'dim-contextSwitchLabel' },
89
+ h('span', null, copy.enable),
90
+ !supported ? h('span', {
91
+ id: unavailableId, className: 'dim-contextUnavailable',
92
+ }, '(当前渠道不支持群聊)') : null),
93
+ h('input', {
94
+ type: 'checkbox', role: 'switch', className: 'dim-contextSwitch',
95
+ checked: supported && scope.enabled,
96
+ disabled,
97
+ 'aria-describedby': !supported ? unavailableId : undefined,
98
+ onChange: (event) => { if (supported) onChange('enabled', event.target.checked); },
99
+ })),
100
+ h('div', { className: 'dim-contextScopeBlock' },
101
+ h('div', { className: 'dim-contextLegend' },
102
+ h('span', null, '来源字段'),
103
+ h('span', { className: 'dim-contextHelp dim-contextLegendHelp' },
104
+ h('button', {
105
+ type: 'button', className: 'dim-contextHelpButton', disabled,
106
+ 'aria-label': copy.fieldsHelpLabel, 'aria-describedby': fieldsHelpId,
107
+ }, h('span', { 'aria-hidden': 'true' }, '?')),
108
+ h('span', { id: fieldsHelpId, className: 'dim-contextTooltip dim-contextLegendTooltip', role: 'tooltip' },
109
+ '增强提示词中请使用字段名(如 senderId、conversationType)引用这些信息。只发送当前会话中勾选且可用的字段,不会额外查询或补全。'))),
110
+ h('div', { className: 'dim-contextFields' }, CONTEXT_ENHANCEMENT_FIELDS.map((field) => {
111
+ const fieldId = `${idPrefix}-${kind}-field-${field}`;
112
+ return h('div', { key: field, className: 'dim-contextField' },
113
+ h('input', {
114
+ id: fieldId, type: 'checkbox', name: `${kind}-${field}`,
115
+ checked: scope.fields.includes(field), disabled,
116
+ onChange: (event) => onChange('fields', event.target.checked
117
+ ? [...scope.fields, field] : scope.fields.filter((value) => value !== field)),
118
+ }),
119
+ h('span', { className: 'dim-contextFieldText' },
120
+ h('label', { className: 'dim-contextFieldName', htmlFor: fieldId }, FIELD_LABELS[field]),
121
+ field === 'senderName' ? h('span', { className: 'dim-contextHelp dim-contextFieldHelp' },
122
+ h('button', {
123
+ type: 'button', className: 'dim-contextHelpButton dim-contextFieldHelpButton', disabled,
124
+ 'aria-label': copy.senderNameHelpLabel, 'aria-describedby': senderNameHelpId,
125
+ }, h('span', { 'aria-hidden': 'true' }, '?')),
126
+ h('span', {
127
+ id: senderNameHelpId,
128
+ className: 'dim-contextTooltip dim-contextFieldTooltip',
129
+ role: 'tooltip',
130
+ }, '该字段不是每个渠道都能提供。当前消息没有发送者昵称时,即使已选择该字段,<dsh_im_source> 中也会省略 senderName。')) : null,
131
+ h('label', { className: 'dim-contextFieldKey', htmlFor: fieldId }, field)));
132
+ }))),
133
+ h('div', { className: 'dim-contextGuidance dim-contextScopeBlock' },
134
+ h('div', { className: 'dim-contextEditorHeader' },
135
+ h('span', { className: 'dim-contextEditorTitle' },
136
+ h('label', { htmlFor: guidanceId }, copy.guidanceLabel),
137
+ h('span', { className: 'dim-contextHelp' },
138
+ h('button', {
139
+ type: 'button', className: 'dim-contextHelpButton', disabled,
140
+ 'aria-label': copy.guidanceHelpLabel, 'aria-describedby': guidanceHelpId,
141
+ }, h('span', { 'aria-hidden': 'true' }, '?')),
142
+ h('span', { id: guidanceHelpId, className: 'dim-contextTooltip dim-contextGuidanceTooltip', role: 'tooltip' },
143
+ h('strong', null, '使用说明'),
144
+ h('span', null, copy.guidanceUsage),
145
+ h('strong', null, '生效规则'),
146
+ h('span', null, copy.guidanceBehavior),
147
+ h('strong', null, '隐私提示'),
148
+ h('span', null, '发送者标识可能包含平台用户 ID 或电话号码形式的标识。关闭开关不会删除已经写入会话历史的信息。'),
149
+ h('strong', null, '使用示例'),
150
+ h('span', { className: 'dim-contextTooltipExample' }, example)))),
151
+ h('div', { className: 'dim-contextTextActions' },
152
+ h('button', { type: 'button', disabled, onClick: () => onChange('guidance', example) }, '填入示例'),
153
+ h('button', { type: 'button', disabled, onClick: () => onChange('guidance', '') }, '清空'))),
154
+ h('textarea', {
155
+ id: guidanceId, value: scope.guidance, placeholder: example, rows: 4, disabled,
156
+ 'data-context-kind': kind,
157
+ maxLength: CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH,
158
+ 'aria-describedby': guidanceHelpId,
159
+ onChange: (event) => onChange('guidance', event.target.value),
160
+ })));
161
+ }
162
+
40
163
  function ContextEnhancementDialog({ config, groupSupported, disabled, onSave, onClose, returnFocusRef, id }) {
41
164
  // A mounted dialog owns its draft; status refreshes must not replace unsaved edits.
42
- const [draft, setDraft] = React.useState(() => ({
43
- ...normalizeContextEnhancementConfig(config),
44
- ...(!groupSupported ? { groupEnabled: false } : {}),
45
- }));
165
+ const [draft, setDraft] = React.useState(() => {
166
+ const normalized = normalizeContextEnhancementConfig(config);
167
+ return {
168
+ ...normalized,
169
+ ...(!groupSupported ? {
170
+ group: { ...normalized.group, enabled: false },
171
+ } : {}),
172
+ };
173
+ });
46
174
  const [saving, setSaving] = React.useState(false);
47
175
  const [error, setError] = React.useState(null);
176
+ const [activeScope, setActiveScope] = React.useState('direct');
48
177
  const savingRef = React.useRef(false);
49
178
  const dialogRef = React.useRef(null);
50
179
  const mountedRef = React.useRef(true);
180
+ const groupTabRef = React.useRef(null);
181
+ const directTabRef = React.useRef(null);
51
182
  const titleId = React.useId();
52
183
  const descriptionId = React.useId();
53
- const groupNoticeId = React.useId();
54
- const guidanceId = React.useId();
55
- const guidanceHelpId = React.useId();
56
- const fieldIdPrefix = React.useId();
57
- const fieldsHelpId = React.useId();
58
- const senderNameHelpId = React.useId();
59
- const guidanceExample = localizeText(CONTEXT_GUIDANCE_EXAMPLE);
184
+ const scopeIdPrefix = React.useId();
185
+ const groupGuidanceExample = localizeText(CONTEXT_GROUP_GUIDANCE_EXAMPLE);
186
+ const directGuidanceExample = localizeText(CONTEXT_DIRECT_GUIDANCE_EXAMPLE);
60
187
  const busy = disabled || saving;
188
+ const scopeKinds = ['direct', 'group'];
189
+ const tabRefs = { group: groupTabRef, direct: directTabRef };
61
190
 
62
191
  React.useEffect(() => {
63
192
  mountedRef.current = true;
@@ -73,9 +202,12 @@ function ContextEnhancementDialog({ config, groupSupported, disabled, onSave, on
73
202
  };
74
203
  }, [returnFocusRef]);
75
204
 
76
- const change = (key, value) => {
205
+ const changeScope = (kind, key, value) => {
77
206
  if (busy || savingRef.current) return;
78
- setDraft((current) => ({ ...current, [key]: value }));
207
+ setDraft((current) => ({
208
+ ...current,
209
+ [kind]: { ...current[kind], [key]: value },
210
+ }));
79
211
  setError(null);
80
212
  };
81
213
 
@@ -83,6 +215,25 @@ function ContextEnhancementDialog({ config, groupSupported, disabled, onSave, on
83
215
  if (!savingRef.current) onClose();
84
216
  };
85
217
 
218
+ const activateScope = (kind, focus = false) => {
219
+ if (busy || !scopeKinds.includes(kind)) return;
220
+ setActiveScope(kind);
221
+ if (focus) tabRefs[kind].current?.focus?.();
222
+ };
223
+
224
+ const handleTabKeyDown = (event, kind) => {
225
+ let next;
226
+ if (event.key === 'Home') next = scopeKinds[0];
227
+ else if (event.key === 'End') next = scopeKinds.at(-1);
228
+ else if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
229
+ const offset = event.key === 'ArrowRight' ? 1 : -1;
230
+ next = scopeKinds[(scopeKinds.indexOf(kind) + offset + scopeKinds.length) % scopeKinds.length];
231
+ }
232
+ if (!next) return;
233
+ event.preventDefault();
234
+ activateScope(next, true);
235
+ };
236
+
86
237
  const save = async () => {
87
238
  if (busy || savingRef.current) return;
88
239
  savingRef.current = true;
@@ -155,90 +306,42 @@ function ContextEnhancementDialog({ config, groupSupported, disabled, onSave, on
155
306
  type: 'button', className: 'dim-contextClose', 'aria-label': '关闭弹窗',
156
307
  disabled: saving, onClick: cancel,
157
308
  }, h(ContextIcon, { kind: 'close' }))),
158
- h('fieldset', { className: 'dim-contextSection', disabled: busy },
159
- h('legend', null, '启用范围'),
160
- h('div', { className: 'dim-contextSwitches' },
161
- h('label', { className: 'dim-contextSwitchRow' },
162
- h('span', { className: 'dim-contextSwitchLabel' },
163
- h('span', null, '群聊中启用'),
164
- !groupSupported ? h('span', {
165
- id: groupNoticeId, className: 'dim-contextUnavailable',
166
- }, '(当前渠道不支持群聊)') : null),
167
- h('input', {
168
- type: 'checkbox', role: 'switch', className: 'dim-contextSwitch',
169
- checked: groupSupported && draft.groupEnabled,
170
- disabled: busy || !groupSupported,
171
- 'aria-describedby': !groupSupported ? groupNoticeId : undefined,
172
- onChange: (event) => { if (groupSupported) change('groupEnabled', event.target.checked); },
173
- })),
174
- h('label', { className: 'dim-contextSwitchRow' },
175
- h('span', null, '私聊中启用'),
176
- h('input', {
177
- type: 'checkbox', role: 'switch', className: 'dim-contextSwitch',
178
- checked: draft.directEnabled, disabled: busy,
179
- onChange: (event) => change('directEnabled', event.target.checked),
180
- }))),
181
- ),
182
- h('fieldset', { className: 'dim-contextSection', disabled: busy },
183
- h('legend', { className: 'dim-contextLegend' },
184
- h('span', null, '来源字段'),
185
- h('span', { className: 'dim-contextHelp dim-contextLegendHelp' },
186
- h('button', {
187
- type: 'button', className: 'dim-contextHelpButton', disabled: busy,
188
- 'aria-label': '查看来源字段说明', 'aria-describedby': fieldsHelpId,
189
- }, h('span', { 'aria-hidden': 'true' }, '?')),
190
- h('span', { id: fieldsHelpId, className: 'dim-contextTooltip dim-contextLegendTooltip', role: 'tooltip' },
191
- '增强提示词中请使用字段名(如 senderId、conversationType)引用这些信息。只发送勾选且当前消息中可用的字段,不会额外查询或补全。'))),
192
- h('div', { className: 'dim-contextFields' }, CONTEXT_ENHANCEMENT_FIELDS.map((field) => {
193
- const fieldId = `${fieldIdPrefix}-${field}`;
194
- return h('div', { key: field, className: 'dim-contextField' },
195
- h('input', {
196
- id: fieldId, type: 'checkbox', name: field,
197
- checked: draft.fields.includes(field), disabled: busy,
198
- onChange: (event) => change('fields', event.target.checked
199
- ? [...draft.fields, field] : draft.fields.filter((value) => value !== field)),
200
- }),
201
- h('span', { className: 'dim-contextFieldText' },
202
- h('label', { className: 'dim-contextFieldName', htmlFor: fieldId }, FIELD_LABELS[field]),
203
- field === 'senderName' ? h('span', { className: 'dim-contextHelp dim-contextFieldHelp' },
204
- h('button', {
205
- type: 'button', className: 'dim-contextHelpButton dim-contextFieldHelpButton', disabled: busy,
206
- 'aria-label': '查看发送者昵称字段说明', 'aria-describedby': senderNameHelpId,
207
- }, h('span', { 'aria-hidden': 'true' }, '?')),
208
- h('span', {
209
- id: senderNameHelpId,
210
- className: 'dim-contextTooltip dim-contextFieldTooltip',
211
- role: 'tooltip',
212
- }, '该字段不是每个渠道都能提供。当前消息没有发送者昵称时,即使已选择该字段,<dsh_im_source> 中也会省略 senderName。')) : null,
213
- h('label', { className: 'dim-contextFieldKey', htmlFor: fieldId }, field)));
214
- }))),
215
- h('div', { className: 'dim-contextGuidance' },
216
- h('div', { className: 'dim-contextEditorHeader' },
217
- h('span', { className: 'dim-contextEditorTitle' },
218
- h('label', { htmlFor: guidanceId }, '增强提示词'),
219
- h('span', { className: 'dim-contextHelp' },
220
- h('button', {
221
- type: 'button', className: 'dim-contextHelpButton', disabled: busy,
222
- 'aria-label': '查看增强提示词使用说明', 'aria-describedby': guidanceHelpId,
223
- }, h('span', { 'aria-hidden': 'true' }, '?')),
224
- h('span', { id: guidanceHelpId, className: 'dim-contextTooltip dim-contextGuidanceTooltip', role: 'tooltip' },
225
- h('strong', null, '使用说明'),
226
- h('span', null, '用于告诉模型如何使用 <dsh_im_source> 中已选择的来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。'),
227
- h('strong', null, '生效规则'),
228
- h('span', null, '只需填写正文,插件自动添加 <dsh_im_source_guidance> 成对标签。清空并保存后,不再附加增强提示词,已选来源字段仍按开关设置发送。'),
229
- h('strong', null, '隐私提示'),
230
- h('span', null, '发送者标识可能包含平台用户 ID 或电话号码形式的标识。关闭开关不会删除已经写入会话历史的信息。'),
231
- h('strong', null, '使用示例'),
232
- h('span', { className: 'dim-contextTooltipExample' }, guidanceExample)))),
233
- h('div', { className: 'dim-contextTextActions' },
234
- h('button', { type: 'button', disabled: busy, onClick: () => change('guidance', guidanceExample) }, '填入示例'),
235
- h('button', { type: 'button', disabled: busy, onClick: () => change('guidance', '') }, '清空'))),
236
- h('textarea', {
237
- id: guidanceId, value: draft.guidance, placeholder: guidanceExample, rows: 4, disabled: busy,
238
- maxLength: CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH,
239
- 'aria-describedby': guidanceHelpId,
240
- onChange: (event) => change('guidance', event.target.value),
309
+ h('div', { className: 'dim-contextTabs', role: 'tablist', 'aria-label': '上下文增强范围' },
310
+ scopeKinds.map((kind) => {
311
+ const selected = activeScope === kind;
312
+ return h('button', {
313
+ key: kind,
314
+ id: `${scopeIdPrefix}-${kind}-tab`,
315
+ ref: tabRefs[kind],
316
+ type: 'button',
317
+ role: 'tab',
318
+ className: 'dim-contextTab',
319
+ 'data-context-kind': kind,
320
+ 'aria-selected': selected,
321
+ 'aria-controls': `${scopeIdPrefix}-${kind}-panel`,
322
+ tabIndex: selected ? 0 : -1,
323
+ disabled: busy,
324
+ onClick: () => activateScope(kind),
325
+ onKeyDown: (event) => handleTabKeyDown(event, kind),
326
+ }, SCOPE_COPY[kind].title);
241
327
  })),
328
+ scopeKinds.map((kind) => h('div', {
329
+ key: kind,
330
+ id: `${scopeIdPrefix}-${kind}-panel`,
331
+ className: 'dim-contextTabPanel',
332
+ role: 'tabpanel',
333
+ 'data-context-kind': kind,
334
+ 'aria-labelledby': `${scopeIdPrefix}-${kind}-tab`,
335
+ hidden: activeScope !== kind,
336
+ }, h(ContextEnhancementScopeEditor, {
337
+ kind,
338
+ scope: draft[kind],
339
+ example: kind === 'group' ? groupGuidanceExample : directGuidanceExample,
340
+ supported: kind === 'group' ? groupSupported : true,
341
+ busy,
342
+ idPrefix: scopeIdPrefix,
343
+ onChange: (key, value) => changeScope(kind, key, value),
344
+ }))),
242
345
  error ? h('p', { className: 'dim-contextError', role: 'alert' }, error) : null,
243
346
  h('footer', { className: 'dim-contextFooter' },
244
347
  h('button', { type: 'button', disabled: saving, onClick: cancel }, '取消'),
@@ -256,7 +359,9 @@ export function ContextEnhancementEditor({ config, groupSupported = true, disabl
256
359
  const dialogId = React.useId();
257
360
  const statusId = React.useId();
258
361
  const saved = normalizeContextEnhancementConfig(config);
259
- const label = contextEnhancementLabel(groupSupported ? saved : { ...saved, groupEnabled: false });
362
+ const label = contextEnhancementLabel(groupSupported ? saved : {
363
+ ...saved, group: { ...saved.group, enabled: false },
364
+ });
260
365
 
261
366
  return h(React.Fragment, null,
262
367
  h('button', {
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
 
3
+ import { AccessPolicySettingsPage } from './access-policy-settings.js';
3
4
  import { h, isEnglish, localizeText } from './i18n.js';
4
5
 
5
6
  export const DELIVERY_RPC_CHANNEL = '/dsh-im-delivery';
@@ -18,6 +19,11 @@ export const DELIVERY_ENDPOINTS = Object.freeze({
18
19
  test: 'target.test',
19
20
  });
20
21
 
22
+ export const BOT_SETTINGS_TABS = Object.freeze([
23
+ Object.freeze({ id: 'delivery', label: '投递设置' }),
24
+ Object.freeze({ id: 'access', label: '访问设置' }),
25
+ ]);
26
+
21
27
  const CHANNEL_DEFINITIONS = Object.freeze({
22
28
  weixin: {
23
29
  label: '微信',
@@ -532,8 +538,15 @@ function TargetRow({ definition, target, botId, connected, rpcCall, onChanged, o
532
538
  : null);
533
539
  }
534
540
 
535
- export function DeliveryTargetSettingsPage({ channel, account, rpcCall, onBack }) {
541
+ export function DeliveryTargetSettingsPage({
542
+ channel,
543
+ account,
544
+ rpcCall,
545
+ accessRpcCall,
546
+ onBack,
547
+ }) {
536
548
  const definition = CHANNEL_DEFINITIONS[channel];
549
+ const [activeTabId, setActiveTabId] = React.useState(BOT_SETTINGS_TABS[0].id);
537
550
  const [phase, setPhase] = React.useState('loading');
538
551
  const [targets, setTargets] = React.useState([]);
539
552
  const [suggestionPhase, setSuggestionPhase] = React.useState('idle');
@@ -543,8 +556,13 @@ export function DeliveryTargetSettingsPage({ channel, account, rpcCall, onBack }
543
556
  const [editor, setEditor] = React.useState(null);
544
557
  const [saving, setSaving] = React.useState(false);
545
558
  const [botCopyState, setBotCopyState] = React.useState(null);
559
+ const [accessPolicy, setAccessPolicy] = React.useState(account.accessPolicy);
546
560
  const mounted = React.useRef(true);
547
561
 
562
+ React.useEffect(() => {
563
+ setAccessPolicy(account.accessPolicy);
564
+ }, [account.botId, account.accessPolicy]);
565
+
548
566
  const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
549
567
  if (typeof rpcCall !== 'function') throw new Error('投递目标设置暂不可用。');
550
568
  return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
@@ -654,13 +672,51 @@ export function DeliveryTargetSettingsPage({ channel, account, rpcCall, onBack }
654
672
  }
655
673
  };
656
674
 
675
+ const activeTab = BOT_SETTINGS_TABS.find((tab) => tab.id === activeTabId)
676
+ ?? BOT_SETTINGS_TABS[0];
677
+ const activeTabDomId = `dim-bot-settings-${activeTab.id}-tab`;
678
+ const activePanelId = `dim-bot-settings-${activeTab.id}-panel`;
679
+
657
680
  return h('section', {
658
681
  className: 'dim-deliveryPage',
659
682
  'aria-label': `${account.botName || definition.label}机器人设置`,
660
683
  },
661
684
  h('header', { className: 'dim-deliveryHeader' },
662
- h(DeliveryButton, { className: 'dim-deliveryBack', onClick: onBack }, '← 返回机器人列表'),
663
- h('div', { className: 'dim-deliveryHeaderMeta' },
685
+ h(DeliveryButton, { className: 'dim-deliveryBack', onClick: onBack }, '← 返回机器人列表')),
686
+ h('div', { className: 'dim-botSettingsTabsBar' },
687
+ h('nav', {
688
+ className: 'dim-botSettingsTabs',
689
+ role: 'tablist',
690
+ 'aria-label': '机器人设置页签',
691
+ }, BOT_SETTINGS_TABS.map((tab) => h('button', {
692
+ key: tab.id,
693
+ id: `dim-bot-settings-${tab.id}-tab`,
694
+ type: 'button',
695
+ role: 'tab',
696
+ className: 'dim-botSettingsTab',
697
+ 'aria-selected': tab.id === activeTab.id,
698
+ 'aria-controls': `dim-bot-settings-${tab.id}-panel`,
699
+ tabIndex: tab.id === activeTab.id ? 0 : -1,
700
+ onClick: () => setActiveTabId(tab.id),
701
+ }, tab.label)))),
702
+ h('div', {
703
+ id: activePanelId,
704
+ className: 'dim-botSettingsTabPanel',
705
+ role: 'tabpanel',
706
+ 'aria-labelledby': activeTabDomId,
707
+ },
708
+ activeTab.id === 'access'
709
+ ? h(AccessPolicySettingsPage, {
710
+ channel,
711
+ account: { ...account, accessPolicy },
712
+ rpcCall: accessRpcCall,
713
+ onSaved: setAccessPolicy,
714
+ })
715
+ : h(React.Fragment, null,
716
+ h('section', { className: 'dim-deliveryIdentity', 'aria-labelledby': 'dim-delivery-bot-title' },
717
+ h('div', { className: 'dim-deliveryIdentityHeading' },
718
+ h('h2', { id: 'dim-delivery-bot-title', className: 'dim-deliveryBotName' },
719
+ account.botName || '机器人设置'),
664
720
  h('a', {
665
721
  className: 'dim-deliveryDocsLink',
666
722
  href: isEnglish() ? DELIVERY_DOCS_URL.en : DELIVERY_DOCS_URL.zh,
@@ -669,11 +725,7 @@ export function DeliveryTargetSettingsPage({ channel, account, rpcCall, onBack }
669
725
  'aria-label': '打开主动投递使用文档',
670
726
  },
671
727
  h('span', null, '使用文档'),
672
- h('span', { 'aria-hidden': 'true' }, '↗')))),
673
- h('section', { className: 'dim-deliveryIdentity', 'aria-labelledby': 'dim-delivery-bot-title' },
674
- h('div', null,
675
- h('h2', { id: 'dim-delivery-bot-title', className: 'dim-deliveryBotName' },
676
- account.botName || '机器人设置')),
728
+ h('span', { 'aria-hidden': 'true' }, '↗'))),
677
729
  h('div', { className: 'dim-deliveryBotId' },
678
730
  h('span', null, 'Bot ID'),
679
731
  h('code', { title: account.botId }, account.botId),
@@ -745,5 +797,5 @@ export function DeliveryTargetSettingsPage({ channel, account, rpcCall, onBack }
745
797
  rpcCall: invoke,
746
798
  onChanged: () => loadTargets({ silent: true }),
747
799
  onEdit: () => setEditor({ mode: 'edit', target, source: 'edit' }),
748
- })))));
800
+ })))))));
749
801
  }
@@ -7,6 +7,62 @@ const EN = Object.freeze({
7
7
  'IM机器人': 'IM bots',
8
8
  'IM机器人设置': 'IM bot settings',
9
9
  '更多机器人设置': 'More bot settings',
10
+ '机器人设置': 'Bot settings',
11
+ '机器人设置页签': 'Bot settings tabs',
12
+ '投递设置': 'Delivery settings',
13
+ '访问设置': 'Access settings',
14
+ '查看访问权限说明': 'View access permission details',
15
+ '允许所有用户': 'Allow all users',
16
+ '仅白名单用户': 'Allowlisted users only',
17
+ '默认命令权限': 'Default command permission',
18
+ '命令权限': 'Command permission',
19
+ '可以执行命令': 'Can run commands',
20
+ '不可以执行命令': 'Cannot run commands',
21
+ '白名单用户': 'Allowlisted users',
22
+ '查看白名单说明': 'View allowlist details',
23
+ '命令权限例外': 'Command permission exceptions',
24
+ '当前没有白名单用户,保存后普通用户将无法使用机器人。': 'There are currently no allowlisted users. After saving, regular users will not be able to use the bot.',
25
+ '新增用户': 'Add user',
26
+ '尚未添加用户': 'No users added',
27
+ '当前渠道不支持群聊': 'Group chat is not supported by this channel',
28
+ '原所有者或扫码接入者始终可以访问并执行命令;以下设置仅约束其他用户。': 'The original owner or QR-code operator can always access the bot and run commands; the settings below apply only to other users.',
29
+ '此区域无需配置,保存私聊设置时会保留现有群聊策略。': 'No setup is needed here. Saving direct-message settings keeps the existing group policy.',
30
+ '访问设置已保存。': 'Access settings saved.',
31
+ '访问设置暂不可用。': 'Access settings are currently unavailable.',
32
+ '访问设置保存失败,请稍后重试。': 'Could not save access settings. Try again later.',
33
+ '服务没有返回已保存的访问策略,请刷新后重试。': 'The service did not return the saved access policy. Refresh and try again.',
34
+ '访问策略尚未就绪,请返回机器人列表刷新后重试。': 'The access policy is not ready. Return to the bot list, refresh, and try again.',
35
+ '当前渠道暂不支持访问设置。': 'Access settings are not supported by this channel yet.',
36
+ '用户标识无效。': 'The user ID is invalid.',
37
+ '用户标识必须是字符串。': 'The user ID must be a string.',
38
+ '用户标识不能为空、包含控制字符或超过 256 个字符。': 'The user ID cannot be empty, contain control characters, or exceed 256 characters.',
39
+ '用户条目必须包含用户标识和命令权限。': 'Each user entry must include a user ID and command permission.',
40
+ '命令权限必须是布尔值。': 'Command permission must be a boolean.',
41
+ '访问模式只能是 open 或 allowlist。': 'Access mode must be open or allowlist.',
42
+ '开放模式设置必须完整。': 'Open-mode settings must be complete.',
43
+ '开放模式默认命令权限必须是布尔值。': 'The open-mode default command permission must be a boolean.',
44
+ '开放模式命令权限覆盖用户必须是数组。': 'Open-mode command permission overrides must be an array.',
45
+ '开放模式命令权限覆盖用户不能包含重复的用户标识。': 'Open-mode command permission overrides cannot contain duplicate user IDs.',
46
+ '白名单模式设置必须完整。': 'Allowlist-mode settings must be complete.',
47
+ '白名单模式用户必须是数组。': 'Allowlist-mode users must be an array.',
48
+ '白名单模式用户不能包含重复的用户标识。': 'Allowlist-mode users cannot contain duplicate user IDs.',
49
+ '访问场景设置必须同时包含模式、开放模式设置和白名单模式设置。': 'Each access context must include its mode, open-mode settings, and allowlist-mode settings.',
50
+ '请同时提交完整的私聊和群聊访问设置。': 'Submit complete direct-message and group access settings together.',
51
+ '填写微信用户 ID': 'Enter a WeChat user ID',
52
+ '飞书 Open ID': 'Feishu Open ID',
53
+ '群成员 Open ID': 'Group member Open ID',
54
+ '钉钉用户 ID': 'DingTalk user ID',
55
+ '填写 senderStaffId 或 senderId': 'Enter senderStaffId or senderId',
56
+ '群成员用户 ID': 'Group member user ID',
57
+ '企业微信用户 ID': 'WeCom user ID',
58
+ '填写 userid': 'Enter userid',
59
+ '填写 member_openid': 'Enter member_openid',
60
+ 'QQ User Open ID': 'QQ User Open ID',
61
+ '群成员 User ID': 'Group member User ID',
62
+ '填写数字 User ID': 'Enter a numeric user ID',
63
+ 'WhatsApp 电话号码或 JID': 'WhatsApp phone number or JID',
64
+ '群成员电话号码或 JID': 'Group member phone number or JID',
65
+ '8613800000000 或完整 JID': '8613800000000 or a full JID',
10
66
  'IM 渠道': 'IM channels',
11
67
  '让 DeepSeek Harness 触手可及': 'Connecting DeepSeek Harness',
12
68
  '当前版本': 'Current version',
@@ -216,31 +272,44 @@ const EN = Object.freeze({
216
272
  '群聊和私聊': 'Groups and direct',
217
273
  '关闭弹窗': 'Close dialog',
218
274
  '查看上下文增强说明': 'View context enhancement details',
275
+ '上下文增强范围': 'Context enhancement scope',
219
276
  '启用范围': 'Enable in',
220
- '群聊中启用': 'Enable in group chats',
221
- '私聊中启用': 'Enable in direct chats',
277
+ '启用': 'Enabled',
222
278
  '(当前渠道不支持群聊)': '(This channel does not support group chats)',
223
279
  '来源字段': 'Source fields',
224
280
  '查看来源字段说明': 'View source field details',
281
+ '查看群聊来源字段说明': 'View group source field details',
282
+ '查看私聊来源字段说明': 'View direct source field details',
225
283
  '渠道': 'Channel',
226
284
  '会话类型': 'Conversation type',
227
285
  '发送者标识': 'Sender ID',
228
286
  '发送者昵称': 'Sender name',
229
287
  '查看发送者昵称字段说明': 'View sender name field details',
288
+ '查看群聊发送者昵称字段说明': 'View group sender name field details',
289
+ '查看私聊发送者昵称字段说明': 'View direct sender name field details',
230
290
  '该字段不是每个渠道都能提供。当前消息没有发送者昵称时,即使已选择该字段,<dsh_im_source> 中也会省略 senderName。': 'This field is not available on every channel. If the current message has no sender name, <dsh_im_source> omits senderName even when the field is selected.',
231
291
  '机器人标识': 'Bot ID',
232
292
  '增强提示词': 'Guidance',
233
293
  '查看增强提示词使用说明': 'View guidance instructions',
294
+ '查看群聊增强提示词使用说明': 'View group guidance instructions',
295
+ '查看私聊增强提示词使用说明': 'View direct guidance instructions',
234
296
  '使用说明': 'How to use',
235
297
  '用于告诉模型如何使用 <dsh_im_source> 中已选择的来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。': 'Tell the model how to use the selected fields in <dsh_im_source>. Enter only the body; the plugin adds paired <dsh_im_source_guidance> tags.',
236
298
  '生效规则': 'Behavior',
237
299
  '隐私提示': 'Privacy',
238
300
  '使用示例': 'Example',
239
301
  '仅依据当前消息的 <dsh_im_source> 中实际提供的字段理解来源;没有提供的字段不要猜测或补全。\nconversationType是群聊时回复严肃一点,conversationType是私聊时回复一定要幽默搞笑,像周星驰的电影一样搞笑': 'Understand the source only from fields actually present in <dsh_im_source> for the current message; do not guess or fill in missing fields.\nWhen conversationType is group, reply more seriously. When conversationType is direct, make the reply humorous and funny.',
302
+ '仅依据当前消息的 <dsh_im_source> 中实际提供的字段理解来源;没有提供的字段不要猜测或补全。\n当前消息来自群聊,请使用严肃、克制、简洁的表达方式。': 'Understand the source only from fields actually present in <dsh_im_source> for the current message; do not guess or fill in missing fields.\nThis message is from a group chat. Use a serious, restrained, and concise style.',
303
+ '仅依据当前消息的 <dsh_im_source> 中实际提供的字段理解来源;没有提供的字段不要猜测或补全。\n当前消息来自私聊,可以使用更轻松、幽默、详细的表达方式。': 'Understand the source only from fields actually present in <dsh_im_source> for the current message; do not guess or fill in missing fields.\nThis message is from a direct chat. You may use a lighter, more humorous, and detailed style.',
240
304
  '填入示例': 'Use example',
241
305
  '清空': 'Clear',
242
306
  '选择在哪些会话中启用、提供哪些来源字段,以及如何使用这些信息。仅使用已有消息元数据,不查询平台 API。': 'Choose which conversations to enhance, which source fields to include, and how to use them. Only existing message metadata is used; no platform APIs are queried.',
243
307
  '增强提示词中请使用字段名(如 senderId、conversationType)引用这些信息。只发送勾选且当前消息中可用的字段,不会额外查询或补全。': 'Reference these values by field name (such as senderId or conversationType) in the guidance. Only selected fields available in the current message are sent; no extra lookups are made.',
308
+ '增强提示词中请使用字段名(如 senderId、conversationType)引用这些信息。只发送当前会话中勾选且可用的字段,不会额外查询或补全。': 'Reference these values by field name (such as senderId or conversationType) in the guidance. Only fields selected and available in the current conversation are sent; no extra lookups are made.',
309
+ '用于告诉模型如何使用当前群聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。': 'Tell the model how to use <dsh_im_source> fields for the current group message. Enter only the body; the plugin adds paired <dsh_im_source_guidance> tags.',
310
+ '用于告诉模型如何使用当前私聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。': 'Tell the model how to use <dsh_im_source> fields for the current direct message. Enter only the body; the plugin adds paired <dsh_im_source_guidance> tags.',
311
+ '仅在群聊开关开启时使用。清空并保存后不再附加群聊增强提示词;所选来源字段仍按当前场景设置发送。': 'Used only when group enhancement is enabled. Clear and save to omit group guidance; selected source fields still follow the group settings.',
312
+ '仅在私聊开关开启时使用。清空并保存后不再附加私聊增强提示词;所选来源字段仍按当前场景设置发送。': 'Used only when direct enhancement is enabled. Clear and save to omit direct guidance; selected source fields still follow the direct settings.',
244
313
  '只需填写正文,插件自动添加 <dsh_im_source_guidance> 成对标签。清空并保存后,不再附加增强提示词,已选来源字段仍按开关设置发送。': 'Enter only the body; the plugin adds paired <dsh_im_source_guidance> tags. Clear and save to omit guidance. Selected source fields still follow the conversation switches.',
245
314
  '发送者标识可能包含平台用户 ID 或电话号码形式的标识。关闭开关不会删除已经写入会话历史的信息。': 'Sender IDs may contain platform user IDs or phone-number-like identifiers. Turning this off does not remove information already saved in conversation history.',
246
315
  '上下文增强保存失败,请重试。': 'Could not save context enhancement. Try again.',
@@ -508,7 +577,6 @@ const EN = Object.freeze({
508
577
  '先通过 @BotFather 获取 Bot Token,再在这里完成接入。': 'Get a Bot Token from @BotFather, then connect it here.',
509
578
  '填写 @BotFather 生成的 Bot Token': 'Enter the Bot Token from @BotFather',
510
579
  '访问模式': 'Access mode',
511
- '访问设置': 'Access settings',
512
580
  'Telegram 访问模式': 'Telegram access mode',
513
581
  '查看 Telegram 访问模式说明': 'View Telegram access mode details',
514
582
  '群聊全部忽略,私聊仅允许白名单用户。': 'All group messages are ignored; only allowlisted users may send DMs.',
@@ -294,6 +294,7 @@ export function IMSettingsTab({
294
294
  channel: active.id,
295
295
  account: deliverySettings,
296
296
  rpcCall: rpcCalls.deliveryRpcCall,
297
+ accessRpcCall: rpcCalls[`${active.id}RpcCall`],
297
298
  onBack: () => setDeliverySettings(null),
298
299
  })
299
300
  : active.id === 'weixin'