@sidleo3/dsh-chat 0.0.4

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 (47) hide show
  1. package/client/bot-list.js +243 -0
  2. package/client/bot-settings.js +175 -0
  3. package/client/bot-shared-settings.js +561 -0
  4. package/client/chat-ui.js +134 -0
  5. package/client/context-enhancement.js +435 -0
  6. package/client/delivery-targets.js +334 -0
  7. package/client/diagnostics.js +160 -0
  8. package/client/i18n.js +371 -0
  9. package/client/index.js +77 -0
  10. package/client/list-order.js +144 -0
  11. package/client/rpc.js +52 -0
  12. package/client/scoped-mode-editor.js +111 -0
  13. package/client/section.js +250 -0
  14. package/client/session-badges.js +263 -0
  15. package/client/styles.js +960 -0
  16. package/client/version-panel.js +97 -0
  17. package/cordis.patch.yml +5 -0
  18. package/host/bot-model.mjs +53 -0
  19. package/host/bot-settings.mjs +247 -0
  20. package/host/channel-registry.mjs +237 -0
  21. package/host/commands.mjs +857 -0
  22. package/host/deferred.mjs +291 -0
  23. package/host/delivery.mjs +377 -0
  24. package/host/file-log.mjs +169 -0
  25. package/host/guidance.mjs +73 -0
  26. package/host/index.mjs +7 -0
  27. package/host/interactions.mjs +330 -0
  28. package/host/json-store.mjs +144 -0
  29. package/host/log-tail.mjs +63 -0
  30. package/host/panel.mjs +1012 -0
  31. package/host/paths.mjs +50 -0
  32. package/host/plugin.mjs +873 -0
  33. package/host/prompt-context.mjs +70 -0
  34. package/host/rpc.mjs +147 -0
  35. package/host/session-keys.mjs +25 -0
  36. package/host/session-store.mjs +187 -0
  37. package/host/sessions.mjs +1348 -0
  38. package/host/tools.mjs +283 -0
  39. package/lib/client.js +4431 -0
  40. package/lib/index.js +5676 -0
  41. package/package.json +63 -0
  42. package/shared/access-policy.mjs +263 -0
  43. package/shared/channel-rail.mjs +156 -0
  44. package/shared/context-enhancement.mjs +415 -0
  45. package/shared/contract.mjs +120 -0
  46. package/shared/panel-sections.mjs +76 -0
  47. package/shared/reply-reference.mjs +115 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * hub 提供给各渠道插件的共享 UI 组件。
3
+ *
4
+ * 渠道页只需要自己的协议相关内容与渠道卡片,工作区/模型/预设/**上下文增强**等
5
+ * 渠道无关面板直接复用这里的组件(实现只在 hub 一份)。
6
+ *
7
+ * @module dsh-chat/client/chat-ui
8
+ */
9
+
10
+ import * as React from 'react';
11
+
12
+ import { CONTRACT_VERSION } from '../shared/contract.mjs';
13
+ import { useBotSettings, useConversations } from './bot-settings.js';
14
+ import {
15
+ AccessPolicyEditor, ModelEditor, OwnerEditor, PanelSectionsEditor, PresetEditor, WorkspaceEditor,
16
+ } from './bot-shared-settings.js';
17
+ import { ContextEnhancementEditor } from './context-enhancement.js';
18
+ import { DeliveryTargetsEditor } from './delivery-targets.js';
19
+ import { callChatRpc, callControlRpc, unwrapRpc } from './rpc.js';
20
+ import { ScopedModeEditor } from './scoped-mode-editor.js';
21
+ import { installChatStyles } from './styles.js';
22
+
23
+ const h = React.createElement;
24
+
25
+ /**
26
+ * 通用卡片。
27
+ *
28
+ * @param props - { title, description, actions, children }。
29
+ * @returns React 元素。
30
+ */
31
+ export function Panel({ title, description, actions, children }) {
32
+ return h('section', { className: 'dchat-card' },
33
+ title || description || actions
34
+ ? h('div', { className: 'dchat-cardHeader' },
35
+ h('div', { className: 'dchat-cardHeading' },
36
+ title ? h('h3', { className: 'dchat-cardTitle' }, title) : null,
37
+ description ? h('p', { className: 'dchat-cardDescription' }, description) : null),
38
+ actions ? h('div', { className: 'dchat-actions' }, actions) : null)
39
+ : null,
40
+ children);
41
+ }
42
+
43
+ /**
44
+ * 空态提示。
45
+ *
46
+ * @param props - { title, description, children }。
47
+ * @returns React 元素。
48
+ */
49
+ export function EmptyState({ title, description, children }) {
50
+ return h('div', { className: 'dchat-empty' },
51
+ h('span', { className: 'dchat-emptyTitle' }, title),
52
+ description ? h('span', null, description) : null,
53
+ children);
54
+ }
55
+
56
+ const TONES = Object.freeze({
57
+ starting: 'warning',
58
+ running: 'success',
59
+ failed: 'error',
60
+ stopped: '',
61
+ });
62
+
63
+ /**
64
+ * 渠道/机器人状态点。
65
+ *
66
+ * @param props - { status, label }。
67
+ * @returns React 元素。
68
+ */
69
+ export function StatusPill({ status, label }) {
70
+ return h('span', {
71
+ className: 'dchat-status',
72
+ 'data-tone': TONES[status] ?? '',
73
+ 'data-status': status,
74
+ }, label ?? status);
75
+ }
76
+
77
+ /**
78
+ * 创建 hub UI 套件(作为 client 服务 `chatUi` 发布)。
79
+ *
80
+ * @param options - { ctx, translate }。
81
+ * @returns 冻结的 UI 套件。
82
+ */
83
+ export function createChatUi({ ctx, translate } = {}) {
84
+ const t = typeof translate === 'function' ? translate : (key) => key;
85
+ return Object.freeze({
86
+ version: CONTRACT_VERSION,
87
+ components: Object.freeze({
88
+ Panel,
89
+ EmptyState,
90
+ StatusPill,
91
+ /** 上下文增强(群聊/私聊全局 + 指定用户/指定群 + 是否叠加全局提示词)。 */
92
+ ContextEnhancementEditor,
93
+ /** 通用"两作用域 × 多选项"设置块(如飞书任务过程展示)。 */
94
+ ScopedModeEditor,
95
+ /** 机器人跑在哪个目录(只对新建会话生效)。 */
96
+ WorkspaceEditor,
97
+ /** 用哪套 Agent 预设(只对新建会话生效)。 */
98
+ PresetEditor,
99
+ /** 机器人默认模型(还没有会话时用它,只对新建会话生效)。 */
100
+ ModelEditor,
101
+ /** 谁能跟机器人说话、谁能执行命令(立即生效)。 */
102
+ AccessPolicyEditor,
103
+ /** 控制面板卡片显示哪些项(私聊/群聊分开)。 */
104
+ PanelSectionsEditor,
105
+ /** 属主:绕过所有策略的人(改完渠道会重连一次)。 */
106
+ OwnerEditor,
107
+ /** 主动投递目标:清单、候选收编、测试发送(数据经 hub 控制端点)。 */
108
+ DeliveryTargetsEditor,
109
+ }),
110
+ hooks: Object.freeze({
111
+ /** 读取/保存 hub 持有的每机器人共享设置。 */
112
+ useBotSettings,
113
+ /** 该机器人聊过的会话(带名字),给"指定用户/指定群"这类选择器用。 */
114
+ useConversations,
115
+ }),
116
+ installStyles: () => installChatStyles(),
117
+ /** 调用本渠道自己的 RPC。 */
118
+ callChannelRpc: (connection, channelId, method, payload, signal) => (
119
+ callChatRpc(connection, channelId, method, payload, signal)
120
+ ),
121
+ /** 调用 hub 控制端点(渠道无关设置,如上下文增强)。 */
122
+ callControlRpc: (connection, method, payload, signal) => (
123
+ callControlRpc(connection, method, payload, signal)
124
+ ),
125
+ unwrapRpc,
126
+ translate: t,
127
+ /** 供渠道页复用的 React 运行时(渠道包只 external react/react-dom,无需各写一份)。 */
128
+ react: React,
129
+ createElement: h,
130
+ /** hub 当前提供的契约版本,渠道页可据此显示兼容信息。 */
131
+ contractVersion: CONTRACT_VERSION,
132
+ context: Object.freeze({ has: () => typeof ctx === 'object' }),
133
+ });
134
+ }
@@ -0,0 +1,435 @@
1
+ /**
2
+ * 上下文增强编辑器(渠道页共用的共享组件)。
3
+ *
4
+ * 两级设置:
5
+ * - 全局:私聊 / 群聊 各自的启用开关、来源字段、增强提示词;
6
+ * - 指定设置:指定用户(只在私聊命中)/ 指定群(只在群聊命中),
7
+ * 各自可独立填提示词并选择"叠加全局提示词"或"只用自己的提示词"。
8
+ *
9
+ * @module dsh-chat/client/context-enhancement
10
+ */
11
+
12
+ import * as React from 'react';
13
+ import { createPortal } from 'react-dom';
14
+
15
+ import {
16
+ CONTEXT_FIELDS,
17
+ DIRECT_GUIDANCE_EXAMPLE,
18
+ GROUP_GUIDANCE_EXAMPLE,
19
+ GUIDANCE_MAX_LENGTH,
20
+ TARGET_ID_MAX_LENGTH,
21
+ TARGET_LABEL_MAX_LENGTH,
22
+ TARGET_LIMIT,
23
+ contextStatusLabel,
24
+ normalizeContextConfig,
25
+ validateContextConfig,
26
+ } from '../shared/context-enhancement.mjs';
27
+
28
+ const h = React.createElement;
29
+
30
+ const FIELD_LABELS = Object.freeze({
31
+ channel: '渠道',
32
+ conversationType: '会话类型',
33
+ senderId: '发送者标识',
34
+ senderName: '发送者昵称',
35
+ conversationTitle: '会话标题',
36
+ chatId: '会话标识',
37
+ threadId: '话题标识',
38
+ botId: '机器人标识',
39
+ });
40
+
41
+ const FIELD_HELP = Object.freeze({
42
+ senderName: '不是每个渠道都能提供;当前消息没有发送者昵称时会省略该字段。',
43
+ conversationTitle: '不是每个渠道都能提供;当前消息没有会话标题时会省略该字段。',
44
+ chatId: '用于区分不同群组或私聊;当前消息没有会话标识时会省略该字段。',
45
+ threadId: '飞书话题群的消息会带上话题标识,用于区分同一群组内的不同话题。',
46
+ });
47
+
48
+ const SCOPE_TEXT = Object.freeze({
49
+ direct: Object.freeze({
50
+ title: '私聊',
51
+ targetTitle: '指定用户',
52
+ targetHint: '只在该用户与机器人的私聊中生效(按发送者标识匹配)。',
53
+ idPlaceholder: 'ou_xxx(发送者标识)',
54
+ idLabel: '用户标识',
55
+ }),
56
+ group: Object.freeze({
57
+ title: '群聊',
58
+ targetTitle: '指定群',
59
+ targetHint: '只在该群中生效(按会话标识匹配)。',
60
+ idPlaceholder: 'oc_xxx(会话标识)',
61
+ idLabel: '群标识',
62
+ }),
63
+ });
64
+
65
+ function t_of(translate) {
66
+ return typeof translate === 'function' ? translate : (key) => key;
67
+ }
68
+
69
+ function FieldPicker({ scopeKey, scope, disabled, onChange, t }) {
70
+ return h('div', { className: 'dchat-contextFields' }, CONTEXT_FIELDS.map((field) => {
71
+ const inputId = `dchat-field-${scopeKey}-${field}`;
72
+ return h('div', { key: field, className: 'dchat-contextField' },
73
+ h('input', {
74
+ id: inputId,
75
+ type: 'checkbox',
76
+ checked: scope.fields.includes(field),
77
+ disabled,
78
+ onChange: (event) => onChange(event.target.checked
79
+ ? [...scope.fields, field]
80
+ : scope.fields.filter((value) => value !== field)),
81
+ }),
82
+ h('label', { htmlFor: inputId, title: FIELD_HELP[field] ? t(FIELD_HELP[field]) : '' },
83
+ h('span', null, t(FIELD_LABELS[field])),
84
+ h('code', null, field)));
85
+ }));
86
+ }
87
+
88
+ function GuidanceEditor({ idPrefix, value, example, disabled, onChange, t }) {
89
+ const id = `${idPrefix}-guidance`;
90
+ return h('div', { className: 'dchat-contextGuidance' },
91
+ h('div', { className: 'dchat-contextGuidanceHeader' },
92
+ h('label', { htmlFor: id, className: 'dchat-contextLegend' }, t('增强提示词')),
93
+ h('div', { className: 'dchat-actions' },
94
+ h('button', {
95
+ type: 'button', className: 'dchat-button', disabled,
96
+ onClick: () => onChange(example),
97
+ }, t('填入示例')),
98
+ h('button', {
99
+ type: 'button', className: 'dchat-button', disabled,
100
+ onClick: () => onChange(''),
101
+ }, t('清空')))),
102
+ h('p', { className: 'dchat-cardDescription' },
103
+ t('告诉模型如何使用来源字段。只填正文,插件会自动包成来源增强块。')),
104
+ h('textarea', {
105
+ id,
106
+ className: 'dchat-textarea',
107
+ rows: 4,
108
+ value,
109
+ placeholder: example,
110
+ maxLength: GUIDANCE_MAX_LENGTH,
111
+ disabled,
112
+ onChange: (event) => onChange(event.target.value),
113
+ }));
114
+ }
115
+
116
+ function GlobalScopePanel({ kind, scope, disabled, onChange, t }) {
117
+ const text = SCOPE_TEXT[kind];
118
+ const example = kind === 'group' ? GROUP_GUIDANCE_EXAMPLE : DIRECT_GUIDANCE_EXAMPLE;
119
+ const switchId = `dchat-enable-${kind}`;
120
+ return h('div', { className: 'dchat-contextGlobal' },
121
+ h('div', { className: 'dchat-contextSwitchRow' },
122
+ h('label', { htmlFor: switchId, className: 'dchat-contextSwitchLabel' },
123
+ `启用${text.title}全局增强`),
124
+ h('input', {
125
+ id: switchId,
126
+ type: 'checkbox',
127
+ role: 'switch',
128
+ checked: scope.enabled,
129
+ disabled,
130
+ onChange: (event) => onChange({ ...scope, enabled: event.target.checked }),
131
+ })),
132
+ h('div', { className: 'dchat-contextLegendRow' }, t('来源字段')),
133
+ h(FieldPicker, {
134
+ scopeKey: `${kind}-global`,
135
+ scope,
136
+ disabled,
137
+ onChange: (fields) => onChange({ ...scope, fields }),
138
+ t,
139
+ }),
140
+ h(GuidanceEditor, {
141
+ idPrefix: `dchat-${kind}-global`,
142
+ value: scope.guidance,
143
+ example,
144
+ disabled,
145
+ onChange: (guidance) => onChange({ ...scope, guidance }),
146
+ t,
147
+ }));
148
+ }
149
+
150
+ /** 作用域 → 指定设置的命中类型:私聊按发送者,群聊按会话。 */
151
+ function targetKindOf(scope) {
152
+ return scope === 'direct' ? 'user' : 'group';
153
+ }
154
+
155
+ function TargetRow({ scope, target, index, disabled, onChange, onRemove, t, conversations }) {
156
+ const text = SCOPE_TEXT[scope];
157
+ const prefix = `dchat-target-${scope}-${index}`;
158
+ return h('li', { className: 'dchat-targetRow' },
159
+ h('div', { className: 'dchat-targetHead' },
160
+ h('label', { className: 'dchat-targetEnable' },
161
+ h('input', {
162
+ type: 'checkbox',
163
+ checked: target.enabled,
164
+ disabled,
165
+ 'aria-label': `启用第 ${index + 1} 条${text.targetTitle}`,
166
+ onChange: (event) => onChange({ ...target, enabled: event.target.checked }),
167
+ }),
168
+ t('启用')),
169
+ h('button', {
170
+ type: 'button',
171
+ className: 'dchat-button',
172
+ disabled,
173
+ 'aria-label': `删除第 ${index + 1} 条${text.targetTitle}`,
174
+ onClick: onRemove,
175
+ }, t('删除'))),
176
+ h('div', { className: 'dchat-targetGrid' },
177
+ h('label', { className: 'dchat-targetField' },
178
+ h('span', null, text.idLabel),
179
+ // 能选就别让人填 id:下拉里是这台机器人聊过的会话(带群名/人名),
180
+ // 手填输入框仍然保留,兼容还没聊过的会话与直接粘贴 id 的场合。
181
+ (conversations ?? []).length > 0
182
+ ? h('select', {
183
+ className: 'dchat-select',
184
+ value: (conversations ?? []).some((item) => item.id === target.id) ? target.id : '',
185
+ disabled,
186
+ 'aria-label': text.idLabel,
187
+ onChange: (event) => {
188
+ if (event.target.value) onChange({ ...target, id: event.target.value });
189
+ },
190
+ },
191
+ h('option', { value: '' }, t('从会话里选…')),
192
+ (conversations ?? []).map((item) => h('option', { key: item.id, value: item.id }, item.name)))
193
+ : null,
194
+ h('input', {
195
+ type: 'text',
196
+ value: target.id,
197
+ maxLength: TARGET_ID_MAX_LENGTH,
198
+ placeholder: text.idPlaceholder,
199
+ disabled,
200
+ onChange: (event) => onChange({ ...target, id: event.target.value }),
201
+ })),
202
+ h('label', { className: 'dchat-targetField' },
203
+ h('span', null, t('备注名(可选)')),
204
+ h('input', {
205
+ type: 'text',
206
+ value: target.label,
207
+ maxLength: TARGET_LABEL_MAX_LENGTH,
208
+ placeholder: t('张三'),
209
+ disabled,
210
+ onChange: (event) => onChange({ ...target, label: event.target.value }),
211
+ }))),
212
+ h('div', { className: 'dchat-contextLegendRow' }, t('来源字段')),
213
+ h(FieldPicker, {
214
+ scopeKey: `${prefix}`,
215
+ scope: target,
216
+ disabled,
217
+ onChange: (fields) => onChange({ ...target, fields }),
218
+ t,
219
+ }),
220
+ h(GuidanceEditor, {
221
+ idPrefix: prefix,
222
+ value: target.guidance,
223
+ example: scope === 'group' ? GROUP_GUIDANCE_EXAMPLE : DIRECT_GUIDANCE_EXAMPLE,
224
+ disabled,
225
+ onChange: (guidance) => onChange({ ...target, guidance }),
226
+ t,
227
+ }),
228
+ h('label', { className: 'dchat-targetMerge' },
229
+ h('input', {
230
+ type: 'checkbox',
231
+ checked: target.merge === 'append',
232
+ disabled,
233
+ onChange: (event) => onChange({
234
+ ...target,
235
+ merge: event.target.checked ? 'append' : 'replace',
236
+ }),
237
+ }),
238
+ t('叠加全局提示词(不勾选则只使用上面的专属提示词)')));
239
+ }
240
+
241
+ function TargetPanel({ scope, targets, disabled, onChange, t, conversations }) {
242
+ const kind = targetKindOf(scope);
243
+ const text = SCOPE_TEXT[scope];
244
+ const rows = targets
245
+ .map((target, index) => ({ target, index }))
246
+ .filter((entry) => entry.target.kind === kind);
247
+
248
+ const add = () => onChange([...targets, {
249
+ kind,
250
+ id: '',
251
+ label: '',
252
+ enabled: true,
253
+ fields: ['senderId'],
254
+ guidance: '',
255
+ merge: 'append',
256
+ }]);
257
+
258
+ const replace = (index, next) => onChange(targets.map((item, at) => (at === index ? next : item)));
259
+ const remove = (index) => onChange(targets.filter((_, at) => at !== index));
260
+
261
+ return h('div', { className: 'dchat-contextTargets' },
262
+ h('div', { className: 'dchat-cardHeader' },
263
+ h('div', null,
264
+ h('h4', { className: 'dchat-cardTitle' }, text.targetTitle),
265
+ h('p', { className: 'dchat-cardDescription' }, text.targetHint)),
266
+ h('button', {
267
+ type: 'button',
268
+ className: 'dchat-button',
269
+ disabled: disabled || targets.length >= TARGET_LIMIT,
270
+ onClick: add,
271
+ }, t('新增'))),
272
+ rows.length === 0
273
+ ? h('p', { className: 'dchat-cardDescription' }, t('还没有指定设置。'))
274
+ : h('ul', { className: 'dchat-targetList' }, rows.map(({ target, index }) => h(TargetRow, {
275
+ key: index,
276
+ scope,
277
+ target,
278
+ index,
279
+ disabled,
280
+ onChange: (next) => replace(index, next),
281
+ onRemove: () => remove(index),
282
+ t,
283
+ // 只给这一类作用域挑:私聊给"人",群聊给"群"。
284
+ conversations: (conversations ?? []).filter((item) => (
285
+ kind === 'group' ? item.kind === 'group' : item.kind === 'direct')),
286
+ }))));;
287
+ }
288
+
289
+ function ContextEnhancementDialog({ config, disabled, translate, onSave, onClose, conversations }) {
290
+ const t = t_of(translate);
291
+ const [draft, setDraft] = React.useState(() => normalizeContextConfig(config));
292
+ const [activeScope, setActiveScope] = React.useState('direct');
293
+ const [saving, setSaving] = React.useState(false);
294
+ const [error, setError] = React.useState(null);
295
+ const dialogRef = React.useRef(null);
296
+ const titleId = React.useId();
297
+
298
+ React.useEffect(() => {
299
+ dialogRef.current?.focus?.();
300
+ }, []);
301
+
302
+ const busy = disabled || saving;
303
+
304
+ const save = async () => {
305
+ if (busy) return;
306
+ setSaving(true);
307
+ setError(null);
308
+ try {
309
+ const next = validateContextConfig(draft);
310
+ await onSave(next);
311
+ onClose();
312
+ } catch (cause) {
313
+ setError(cause?.message ?? t('保存失败,请重试。'));
314
+ } finally {
315
+ setSaving(false);
316
+ }
317
+ };
318
+
319
+ const content = h('div', {
320
+ className: 'dchat-backdrop',
321
+ onMouseDown: (event) => {
322
+ if (event.target === event.currentTarget && !saving) onClose();
323
+ },
324
+ }, h('section', {
325
+ ref: dialogRef,
326
+ className: 'dchat-dialog',
327
+ role: 'dialog',
328
+ 'aria-modal': 'true',
329
+ 'aria-labelledby': titleId,
330
+ tabIndex: -1,
331
+ onKeyDown: (event) => {
332
+ if (event.key === 'Escape' && !saving) {
333
+ event.preventDefault();
334
+ onClose();
335
+ }
336
+ },
337
+ },
338
+ h('header', { className: 'dchat-dialogHeader' },
339
+ h('h3', { id: titleId, className: 'dchat-cardTitle' }, t('上下文增强')),
340
+ h('button', {
341
+ type: 'button',
342
+ className: 'dchat-button',
343
+ disabled: saving,
344
+ 'aria-label': t('关闭'),
345
+ onClick: onClose,
346
+ }, t('关闭'))),
347
+ h('p', { className: 'dchat-cardDescription' },
348
+ t('来源字段只在当前消息已提供时才会发送,不会额外查询平台接口。')),
349
+ h('div', { className: 'dchat-tabs', role: 'tablist', 'aria-label': t('上下文增强范围') },
350
+ ['direct', 'group'].map((kind) => h('button', {
351
+ key: kind,
352
+ type: 'button',
353
+ role: 'tab',
354
+ className: 'dchat-tab',
355
+ 'aria-selected': activeScope === kind,
356
+ 'data-scope': kind,
357
+ onClick: () => setActiveScope(kind),
358
+ }, t(SCOPE_TEXT[kind].title)))),
359
+ ['direct', 'group'].map((kind) => h('div', {
360
+ key: kind,
361
+ role: 'tabpanel',
362
+ className: 'dchat-tabPanel',
363
+ hidden: activeScope !== kind,
364
+ 'data-scope': kind,
365
+ },
366
+ h(GlobalScopePanel, {
367
+ kind,
368
+ scope: draft[kind],
369
+ disabled: busy,
370
+ t,
371
+ onChange: (scope) => setDraft((current) => ({ ...current, [kind]: scope })),
372
+ }),
373
+ h(TargetPanel, {
374
+ scope: kind,
375
+ targets: draft.targets,
376
+ disabled: busy,
377
+ t,
378
+ conversations,
379
+ onChange: (targets) => setDraft((current) => ({ ...current, targets })),
380
+ }))),
381
+ error ? h('p', { className: 'dchat-error', role: 'alert' }, error) : null,
382
+ h('footer', { className: 'dchat-dialogFooter' },
383
+ h('button', {
384
+ type: 'button', className: 'dchat-button', disabled: saving, onClick: onClose,
385
+ }, t('取消')),
386
+ h('button', {
387
+ type: 'button',
388
+ className: 'dchat-button dchat-buttonPrimary',
389
+ disabled: busy,
390
+ onClick: () => {
391
+ void save();
392
+ },
393
+ }, saving ? t('保存中…') : t('保存')))));
394
+
395
+ return globalThis.document?.body ? createPortal(content, globalThis.document.body) : content;
396
+ }
397
+
398
+ /**
399
+ * 上下文增强入口 + 弹窗。
400
+ *
401
+ * @param props - { config, disabled, translate, onSave }。
402
+ * @returns React 元素。
403
+ */
404
+ export function ContextEnhancementEditor({
405
+ config, disabled = false, translate, onSave, conversations = [],
406
+ }) {
407
+ const t = t_of(translate);
408
+ const [open, setOpen] = React.useState(false);
409
+ const status = contextStatusLabel(config);
410
+ /**
411
+ * 「指定用户/指定群」的 id 必须是**平台 id**(`ou_…` / `oc_…`),因为它是拿消息里的
412
+ * `senderId` / `chatId` 去匹配的。**由渠道**把会话映射成平台 id 后传进来
413
+ * (`route` 的字段名是平台概念,hub 不认识);没传就只留手填输入框。
414
+ */
415
+ return h(React.Fragment, null,
416
+ h('button', {
417
+ type: 'button',
418
+ className: 'dchat-entry',
419
+ disabled,
420
+ 'aria-haspopup': 'dialog',
421
+ 'aria-expanded': open,
422
+ onClick: () => setOpen(true),
423
+ },
424
+ h('span', { className: 'dchat-entryLabel' }, t('上下文增强')),
425
+ h('span', { className: 'dchat-entryStatus', 'data-active': status !== '未开启' }, t(status)),
426
+ h('span', { className: 'dchat-entryArrow', 'aria-hidden': 'true' }, '›')),
427
+ open ? h(ContextEnhancementDialog, {
428
+ config,
429
+ disabled,
430
+ translate: t,
431
+ onSave,
432
+ conversations,
433
+ onClose: () => setOpen(false),
434
+ }) : null);
435
+ }