@xmanrui/dsh-im 4.2.1 → 4.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "4.2.1",
3
+ "version": "4.3.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",
@@ -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', {
@@ -216,31 +216,44 @@ const EN = Object.freeze({
216
216
  '群聊和私聊': 'Groups and direct',
217
217
  '关闭弹窗': 'Close dialog',
218
218
  '查看上下文增强说明': 'View context enhancement details',
219
+ '上下文增强范围': 'Context enhancement scope',
219
220
  '启用范围': 'Enable in',
220
- '群聊中启用': 'Enable in group chats',
221
- '私聊中启用': 'Enable in direct chats',
221
+ '启用': 'Enabled',
222
222
  '(当前渠道不支持群聊)': '(This channel does not support group chats)',
223
223
  '来源字段': 'Source fields',
224
224
  '查看来源字段说明': 'View source field details',
225
+ '查看群聊来源字段说明': 'View group source field details',
226
+ '查看私聊来源字段说明': 'View direct source field details',
225
227
  '渠道': 'Channel',
226
228
  '会话类型': 'Conversation type',
227
229
  '发送者标识': 'Sender ID',
228
230
  '发送者昵称': 'Sender name',
229
231
  '查看发送者昵称字段说明': 'View sender name field details',
232
+ '查看群聊发送者昵称字段说明': 'View group sender name field details',
233
+ '查看私聊发送者昵称字段说明': 'View direct sender name field details',
230
234
  '该字段不是每个渠道都能提供。当前消息没有发送者昵称时,即使已选择该字段,<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
235
  '机器人标识': 'Bot ID',
232
236
  '增强提示词': 'Guidance',
233
237
  '查看增强提示词使用说明': 'View guidance instructions',
238
+ '查看群聊增强提示词使用说明': 'View group guidance instructions',
239
+ '查看私聊增强提示词使用说明': 'View direct guidance instructions',
234
240
  '使用说明': 'How to use',
235
241
  '用于告诉模型如何使用 <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
242
  '生效规则': 'Behavior',
237
243
  '隐私提示': 'Privacy',
238
244
  '使用示例': 'Example',
239
245
  '仅依据当前消息的 <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.',
246
+ '仅依据当前消息的 <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.',
247
+ '仅依据当前消息的 <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
248
  '填入示例': 'Use example',
241
249
  '清空': 'Clear',
242
250
  '选择在哪些会话中启用、提供哪些来源字段,以及如何使用这些信息。仅使用已有消息元数据,不查询平台 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
251
  '增强提示词中请使用字段名(如 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.',
252
+ '增强提示词中请使用字段名(如 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.',
253
+ '用于告诉模型如何使用当前群聊消息的 <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.',
254
+ '用于告诉模型如何使用当前私聊消息的 <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.',
255
+ '仅在群聊开关开启时使用。清空并保存后不再附加群聊增强提示词;所选来源字段仍按当前场景设置发送。': 'Used only when group enhancement is enabled. Clear and save to omit group guidance; selected source fields still follow the group settings.',
256
+ '仅在私聊开关开启时使用。清空并保存后不再附加私聊增强提示词;所选来源字段仍按当前场景设置发送。': 'Used only when direct enhancement is enabled. Clear and save to omit direct guidance; selected source fields still follow the direct settings.',
244
257
  '只需填写正文,插件自动添加 <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
258
  '发送者标识可能包含平台用户 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
259
  '上下文增强保存失败,请重试。': 'Could not save context enhancement. Try again.',
@@ -228,10 +228,15 @@ const CSS = String.raw`
228
228
  .dim-contextHeaderTooltip { width: min(340px, calc(100vw - 72px)); }
229
229
  .dim-contextClose { width: 30px; height: 30px; flex: none; display: grid; place-items: center; padding: 0; border: 0; border-radius: 6px; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; cursor: pointer; }
230
230
  .dim-contextClose:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, #eef0f3); }
231
- .dim-contextSection { min-width: 0; margin: 14px 0; padding: 0; border: 0; }
232
- .dim-contextSection legend { margin-bottom: 8px; padding: 0; font-weight: 500; }
231
+ .dim-contextTabs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3px; margin-top: 14px; padding: 3px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 8px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
232
+ .dim-contextTab { min-width: 0; min-height: 34px; padding: 5px 12px; border: 0; border-radius: 6px; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-weight: 500; cursor: pointer; transition: color .15s ease, background .15s ease, box-shadow .15s ease; }
233
+ .dim-contextTab:hover:not(:disabled):not([aria-selected="true"]) { color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #eef0f3); }
234
+ .dim-contextTab[aria-selected="true"] { color: var(--dsw-alias-state-business-primary, #3370ff); background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 3px rgb(31 35 41 / 12%); }
235
+ .dim-contextTabPanel[hidden] { display: none; }
236
+ .dim-contextSection { min-width: 0; margin: 0; padding: 0; border: 0; }
237
+ .dim-contextScope { margin-top: 10px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; background: var(--dsw-alias-bg-layer-1, #fff); }
238
+ .dim-contextScopeBlock { margin-top: 12px; }
233
239
  .dim-contextLegend { position: relative; display: inline-flex; align-items: center; gap: 6px; }
234
- .dim-contextSwitches { display: grid; grid-template-columns: 1fr; gap: 2px; }
235
240
  .dim-contextSwitchRow { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 34px; cursor: pointer; }
236
241
  .dim-contextSwitchLabel { min-width: 0; display: inline-flex; align-items: baseline; gap: 5px; flex-wrap: wrap; }
237
242
  .dim-contextUnavailable { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 16px; font-weight: 400; }
@@ -274,7 +279,7 @@ const CSS = String.raw`
274
279
  .dim-contextEntry:focus-visible, .dim-contextDialog button:focus-visible, .dim-contextDialog input:focus-visible, .dim-contextDialog textarea:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; }
275
280
  .dim-contextEntry:disabled, .dim-contextDialog button:disabled, .dim-contextDialog input:disabled, .dim-contextDialog textarea:disabled { opacity: .55; cursor: not-allowed; }
276
281
  @media (pointer: coarse) {
277
- .dim-contextEntry, .dim-contextClose, .dim-contextFooter button, .dim-contextTextActions button, .dim-contextField, .dim-contextSwitchRow { min-height: 44px; }
282
+ .dim-contextEntry, .dim-contextTab, .dim-contextClose, .dim-contextFooter button, .dim-contextTextActions button, .dim-contextField, .dim-contextSwitchRow { min-height: 44px; }
278
283
  .dim-contextClose, .dim-contextTextActions button { min-width: 44px; }
279
284
  .dim-contextGuidance textarea { font-size: 16px; }
280
285
  }
@@ -142,12 +142,13 @@ if (client.includes('settings.plugins.tab') || clientSources.includes('settings.
142
142
  throw new Error('client source or bundle still contains the legacy Plugins-tab settings entry');
143
143
  }
144
144
  // Connections still have no channel-enable toggle. Only the shared context
145
- // editor owns checkable inputs: two scope switches and one mapped field input.
145
+ // editor owns checkable inputs. Its reusable scope component contains one
146
+ // switch template and one mapped field-input template; it renders both twice.
146
147
  const contextEditorSource = await readFile(resolve(root, 'plugin-src/client/context-enhancement.js'), 'utf8');
147
148
  const otherClientSources = clientSources.replace(contextEditorSource, '');
148
149
  if (/role:\s*["']switch|type:\s*["']checkbox/.test(otherClientSources)
149
- || (client.match(/role:\s*["']switch["']/g) ?? []).length !== 2
150
- || (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 3) {
150
+ || (client.match(/role:\s*["']switch["']/g) ?? []).length !== 1
151
+ || (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 2) {
151
152
  throw new Error('checkable inputs must be limited to the context-enhancement editor');
152
153
  }
153
154
  for (const marker of ['bot.context-enhancement.set', '<dsh_im_source>', '<dsh_im_source_guidance>']) {