@xmanrui/dsh-im 4.9.1 → 4.11.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/PROACTIVE_DELIVERY.en.md +23 -1
- package/PROACTIVE_DELIVERY.md +23 -1
- package/README.en.md +5 -1
- package/README.md +6 -2
- package/lib/client.js +2077 -673
- package/lib/index.js +277 -272
- package/package.json +13 -1
- package/plugin-src/client/channel-card-meta.js +9 -1
- package/plugin-src/client/channels/dingtalk/api.js +4 -0
- package/plugin-src/client/channels/dingtalk/index.js +25 -2
- package/plugin-src/client/channels/feishu/api.js +6 -0
- package/plugin-src/client/channels/feishu/group-settings.js +581 -0
- package/plugin-src/client/channels/feishu/index.js +26 -150
- package/plugin-src/client/channels/qq/api.js +4 -0
- package/plugin-src/client/channels/qq/index.js +25 -2
- package/plugin-src/client/channels/shared/token-api.js +4 -0
- package/plugin-src/client/channels/shared/token-channel.js +25 -3
- package/plugin-src/client/channels/wecom/api.js +4 -0
- package/plugin-src/client/channels/wecom/index.js +25 -2
- package/plugin-src/client/channels/weixin/api.js +4 -0
- package/plugin-src/client/channels/weixin/index.js +25 -2
- package/plugin-src/client/channels/whatsapp/api.js +4 -0
- package/plugin-src/client/channels/whatsapp/index.js +24 -2
- package/plugin-src/client/delivery-settings.js +73 -5
- package/plugin-src/client/global-settings.js +341 -0
- package/plugin-src/client/i18n.js +49 -0
- package/plugin-src/client/index.js +47 -6
- package/plugin-src/client/model-setting.js +104 -0
- package/plugin-src/client/styles.js +99 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +15 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/feishu/production.mjs +17 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +32 -0
- package/plugin-src/host/channels/qq/production.mjs +15 -0
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/model-setting-rpc.mjs +19 -0
- package/plugin-src/host/channels/shared/production.mjs +15 -0
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
- package/plugin-src/host/channels/slack/production.mjs +15 -0
- package/plugin-src/host/channels/slack/rpc.mjs +9 -0
- package/plugin-src/host/channels/wecom/production.mjs +15 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +15 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +15 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
- package/plugin-src/host/delivery-adapter.mjs +142 -3
- package/plugin-src/host/delivery-rpc.mjs +12 -0
- package/plugin-src/host/delivery-service.mjs +125 -3
- package/plugin-src/host/delivery-suggestions.mjs +46 -0
- package/plugin-src/host/harness-session-coordinator.mjs +32 -10
- package/plugin-src/host/inbound-ttl-rpc.mjs +83 -0
- package/plugin-src/host/inbound-ttl-runtime.mjs +85 -0
- package/plugin-src/host/inbound-ttl-service.mjs +179 -0
- package/plugin-src/host/index.mjs +26 -1
- package/plugin-src/host/modern-harness-api.mjs +58 -2
- package/plugin-src/host/session-sync-coordinator.mjs +217 -0
- package/scripts/verify-package.mjs +12 -6
- package/src/channels/feishu/bridge.mjs +164 -4
- package/src/channels/feishu/feishu-channel.mjs +45 -10
- package/src/channels/feishu/feishu-runtime.mjs +9 -0
- package/src/channels/feishu/message-utils.mjs +13 -0
- package/src/channels/feishu/multi-bot-controller.mjs +16 -0
- package/src/channels/feishu/plugin-config-store.mjs +1 -0
- package/src/channels/feishu/state-store.mjs +25 -0
- package/src/channels/shared/bot-workspace-store.mjs +291 -38
- package/src/channels/shared/harness-client.mjs +124 -43
- package/src/channels/shared/inbound-file.mjs +117 -2
- package/src/channels/shared/inbound-ttl-store.mjs +110 -0
- package/src/channels/shared/inbound-ttl.mjs +22 -0
- package/src/channels/shared/model-setting.mjs +110 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_INBOUND_TTL_HOURS,
|
|
5
|
+
INBOUND_TTL_MAX_HOURS,
|
|
6
|
+
normalizeInboundTtlHours,
|
|
7
|
+
} from '../../src/channels/shared/inbound-ttl.mjs';
|
|
8
|
+
import { h } from './i18n.js';
|
|
9
|
+
|
|
10
|
+
export const GLOBAL_SETTINGS_RPC_CHANNEL = '/dsh-im-settings';
|
|
11
|
+
|
|
12
|
+
export const GLOBAL_SETTINGS_TAB_ID = 'global-settings';
|
|
13
|
+
|
|
14
|
+
const ATTACHMENTS_TAB_ID = 'dim-general-settings-tab-attachments';
|
|
15
|
+
const ATTACHMENTS_PANEL_ID = 'dim-general-settings-panel-attachments';
|
|
16
|
+
|
|
17
|
+
export const GLOBAL_SETTINGS_ENDPOINTS = Object.freeze({
|
|
18
|
+
getTtl: 'settings.inbound-ttl.get',
|
|
19
|
+
setTtl: 'settings.inbound-ttl.set',
|
|
20
|
+
sweep: 'settings.inbound-ttl.sweep',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function presentError(error, fallback) {
|
|
24
|
+
return error?.message || fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function unwrapRpcResult(result) {
|
|
28
|
+
if (result?.ok === true) return result.value;
|
|
29
|
+
if (result?.ok === false) {
|
|
30
|
+
const error = new Error(result.error?.message || '请求失败,请稍后重试。');
|
|
31
|
+
error.code = result.error?.code;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function GlobalSettingsLogoGlyph({ size } = {}) {
|
|
38
|
+
return h('svg', {
|
|
39
|
+
...(size === undefined ? {} : { width: size, height: size }),
|
|
40
|
+
viewBox: '0 0 24 24',
|
|
41
|
+
focusable: 'false',
|
|
42
|
+
'aria-hidden': 'true',
|
|
43
|
+
'data-im-icon': 'global-settings',
|
|
44
|
+
}, h('path', {
|
|
45
|
+
fill: 'currentColor',
|
|
46
|
+
d: 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.488.488 0 0 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58ZM12 15.6A3.61 3.61 0 0 1 8.4 12c0-1.98 1.62-3.6 3.6-3.6s3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6Z',
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function GlobalButton({ children, kind = 'secondary', className = '', ...props }) {
|
|
51
|
+
return h('button', {
|
|
52
|
+
...props,
|
|
53
|
+
type: props.type ?? 'button',
|
|
54
|
+
className: `dim-deliveryButton ${className}`.trim(),
|
|
55
|
+
'data-kind': kind,
|
|
56
|
+
}, children);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function GlobalSettingsPanel({ rpcCall }) {
|
|
60
|
+
const [phase, setPhase] = React.useState('loading');
|
|
61
|
+
const [loadError, setLoadError] = React.useState(null);
|
|
62
|
+
const [ttlInput, setTtlInput] = React.useState('');
|
|
63
|
+
const [savedTtl, setSavedTtl] = React.useState(null);
|
|
64
|
+
const [ttlError, setTtlError] = React.useState(false);
|
|
65
|
+
const [saveError, setSaveError] = React.useState(null);
|
|
66
|
+
const [saveSucceeded, setSaveSucceeded] = React.useState(false);
|
|
67
|
+
const [isSaving, setIsSaving] = React.useState(false);
|
|
68
|
+
const [sweepConfirming, setSweepConfirming] = React.useState(false);
|
|
69
|
+
const [sweeping, setSweeping] = React.useState(false);
|
|
70
|
+
const ttlErrorId = React.useId();
|
|
71
|
+
const ttlHintsId = React.useId();
|
|
72
|
+
const sweepTriggerId = React.useId();
|
|
73
|
+
const sweepConfirmId = React.useId();
|
|
74
|
+
const sweepConfirmTextId = React.useId();
|
|
75
|
+
const sweepConfirmButtonId = React.useId();
|
|
76
|
+
const mounted = React.useRef(true);
|
|
77
|
+
const saving = React.useRef(false);
|
|
78
|
+
|
|
79
|
+
const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
|
|
80
|
+
if (typeof rpcCall !== 'function') throw new Error('通用设置暂不可用。');
|
|
81
|
+
return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
|
|
82
|
+
}, [rpcCall]);
|
|
83
|
+
|
|
84
|
+
const loadSettings = React.useCallback(async ({ signal } = {}) => {
|
|
85
|
+
setPhase('loading');
|
|
86
|
+
setLoadError(null);
|
|
87
|
+
try {
|
|
88
|
+
const value = await invoke(GLOBAL_SETTINGS_ENDPOINTS.getTtl, {}, signal);
|
|
89
|
+
if (signal?.aborted || !mounted.current) return;
|
|
90
|
+
const ttlHours = normalizeInboundTtlHours(value?.ttlHours);
|
|
91
|
+
if (ttlHours === null) {
|
|
92
|
+
setLoadError('通用设置返回了无法识别的响应。');
|
|
93
|
+
setPhase('error');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
setSavedTtl(ttlHours);
|
|
97
|
+
setTtlInput(String(ttlHours));
|
|
98
|
+
setTtlError(false);
|
|
99
|
+
setSaveError(null);
|
|
100
|
+
setSaveSucceeded(false);
|
|
101
|
+
setPhase('ready');
|
|
102
|
+
} catch (caught) {
|
|
103
|
+
if (signal?.aborted || caught?.name === 'AbortError' || !mounted.current) return;
|
|
104
|
+
setLoadError(presentError(caught, '无法读取通用设置,请稍后重试。'));
|
|
105
|
+
setPhase('error');
|
|
106
|
+
}
|
|
107
|
+
}, [invoke]);
|
|
108
|
+
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
mounted.current = true;
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
void loadSettings({ signal: controller.signal });
|
|
113
|
+
return () => {
|
|
114
|
+
mounted.current = false;
|
|
115
|
+
controller.abort();
|
|
116
|
+
};
|
|
117
|
+
}, [loadSettings]);
|
|
118
|
+
|
|
119
|
+
React.useEffect(() => {
|
|
120
|
+
if (!sweepConfirming) return;
|
|
121
|
+
globalThis.document?.getElementById(sweepConfirmButtonId)?.focus();
|
|
122
|
+
}, [sweepConfirmButtonId, sweepConfirming]);
|
|
123
|
+
|
|
124
|
+
const commitTtl = async () => {
|
|
125
|
+
if (phase === 'loading' || saving.current) return;
|
|
126
|
+
const proposed = normalizeInboundTtlHours(ttlInput.trim());
|
|
127
|
+
if (proposed === null) {
|
|
128
|
+
setSaveError(null);
|
|
129
|
+
setSaveSucceeded(false);
|
|
130
|
+
setTtlError(true);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
setTtlError(false);
|
|
134
|
+
if (proposed === savedTtl) return;
|
|
135
|
+
saving.current = true;
|
|
136
|
+
setIsSaving(true);
|
|
137
|
+
setSaveError(null);
|
|
138
|
+
setSaveSucceeded(false);
|
|
139
|
+
try {
|
|
140
|
+
const value = await invoke(GLOBAL_SETTINGS_ENDPOINTS.setTtl, { ttlHours: proposed });
|
|
141
|
+
if (!mounted.current) return;
|
|
142
|
+
const confirmed = normalizeInboundTtlHours(value?.ttlHours);
|
|
143
|
+
const finalTtl = confirmed === null ? proposed : confirmed;
|
|
144
|
+
setSavedTtl(finalTtl);
|
|
145
|
+
// Never clobber the field if the user kept typing while the save was in flight.
|
|
146
|
+
setTtlInput((current) => current.trim() === String(proposed) ? String(finalTtl) : current);
|
|
147
|
+
setLoadError(null);
|
|
148
|
+
setSaveSucceeded(true);
|
|
149
|
+
setPhase('ready');
|
|
150
|
+
} catch (caught) {
|
|
151
|
+
if (mounted.current) {
|
|
152
|
+
setSaveError(presentError(caught, '设置保存失败,请稍后重试。'));
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
saving.current = false;
|
|
156
|
+
if (mounted.current) setIsSaving(false);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const requestSweep = () => {
|
|
161
|
+
if (sweeping) return;
|
|
162
|
+
setSweepConfirming(true);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const cancelSweep = ({ restoreFocus = false } = {}) => {
|
|
166
|
+
setSweepConfirming(false);
|
|
167
|
+
if (restoreFocus) globalThis.document?.getElementById(sweepTriggerId)?.focus();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const runSweep = async () => {
|
|
171
|
+
setSweepConfirming(false);
|
|
172
|
+
setSweeping(true);
|
|
173
|
+
try {
|
|
174
|
+
// Manual sweeps are silent by design: the busy label on the button is
|
|
175
|
+
// the only feedback, and RPC results or failures are not announced.
|
|
176
|
+
await invoke(GLOBAL_SETTINGS_ENDPOINTS.sweep, {});
|
|
177
|
+
} catch {
|
|
178
|
+
// Intentionally swallowed — no result feedback for manual sweeps.
|
|
179
|
+
} finally {
|
|
180
|
+
if (mounted.current) setSweeping(false);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const inlineStatus = phase === 'loading'
|
|
185
|
+
? { role: 'status', message: '正在读取通用设置…' }
|
|
186
|
+
: ttlError
|
|
187
|
+
? { role: 'alert', message: `请输入 -1、0 或 1~${INBOUND_TTL_MAX_HOURS} 之间的整数。` }
|
|
188
|
+
: saveError
|
|
189
|
+
? { role: 'alert', message: saveError }
|
|
190
|
+
: phase === 'error' && loadError
|
|
191
|
+
? { role: 'alert', message: loadError }
|
|
192
|
+
: saveSucceeded
|
|
193
|
+
? { role: 'status', message: '已保存' }
|
|
194
|
+
: null;
|
|
195
|
+
|
|
196
|
+
const proposedTtl = normalizeInboundTtlHours(ttlInput.trim());
|
|
197
|
+
const canSave = phase !== 'loading'
|
|
198
|
+
&& !isSaving
|
|
199
|
+
&& proposedTtl !== null
|
|
200
|
+
&& proposedTtl !== savedTtl;
|
|
201
|
+
|
|
202
|
+
return h('section', { className: 'dim-generalSettingsPage', 'aria-label': '通用设置' },
|
|
203
|
+
h('header', { className: 'dim-generalSettingsHeader' },
|
|
204
|
+
h('h2', null, '通用设置')),
|
|
205
|
+
h('div', { className: 'dim-generalSettingsTabsBar' },
|
|
206
|
+
h('div', {
|
|
207
|
+
className: 'dim-generalSettingsTabs',
|
|
208
|
+
role: 'tablist',
|
|
209
|
+
'aria-label': '通用设置分类',
|
|
210
|
+
},
|
|
211
|
+
h('button', {
|
|
212
|
+
type: 'button',
|
|
213
|
+
id: ATTACHMENTS_TAB_ID,
|
|
214
|
+
className: 'dim-generalSettingsTab',
|
|
215
|
+
role: 'tab',
|
|
216
|
+
'aria-selected': true,
|
|
217
|
+
'aria-controls': ATTACHMENTS_PANEL_ID,
|
|
218
|
+
}, '附件'))),
|
|
219
|
+
h('div', {
|
|
220
|
+
id: ATTACHMENTS_PANEL_ID,
|
|
221
|
+
className: 'dim-generalSettingsTabPanel',
|
|
222
|
+
role: 'tabpanel',
|
|
223
|
+
'aria-labelledby': ATTACHMENTS_TAB_ID,
|
|
224
|
+
},
|
|
225
|
+
h('section', {
|
|
226
|
+
className: 'dim-globalSection',
|
|
227
|
+
'aria-label': '附件',
|
|
228
|
+
'aria-busy': phase === 'loading',
|
|
229
|
+
},
|
|
230
|
+
h('div', { className: 'dim-globalHead' },
|
|
231
|
+
h('div', { className: 'dim-globalHeadTitle' },
|
|
232
|
+
h('h3', { id: 'dim-globalTtlTitle' }, '附件保留时长 (小时)'),
|
|
233
|
+
h('div', { className: 'dim-globalTtlHelp' },
|
|
234
|
+
h('button', {
|
|
235
|
+
type: 'button',
|
|
236
|
+
className: 'dim-channelHelpButton dim-globalTtlHelpButton',
|
|
237
|
+
'aria-label': '查看附件保留时长说明',
|
|
238
|
+
'aria-describedby': ttlHintsId,
|
|
239
|
+
}, h('span', { 'aria-hidden': 'true' }, '?')),
|
|
240
|
+
h('div', {
|
|
241
|
+
id: ttlHintsId,
|
|
242
|
+
className: 'dim-globalTtlTooltip',
|
|
243
|
+
role: 'tooltip',
|
|
244
|
+
},
|
|
245
|
+
h('ul', { className: 'dim-globalTtlHints' },
|
|
246
|
+
h('li', null,
|
|
247
|
+
h('code', null, '-1'),
|
|
248
|
+
h('span', null, '永久保留,不会自动清理')),
|
|
249
|
+
h('li', null,
|
|
250
|
+
h('code', null, '0'),
|
|
251
|
+
h('span', null, '每 Turn 结束后立即清理')),
|
|
252
|
+
h('li', null,
|
|
253
|
+
h('code', null, `1~${INBOUND_TTL_MAX_HOURS}`),
|
|
254
|
+
h('span', null, '小时后自动清理'))))))),
|
|
255
|
+
h('form', {
|
|
256
|
+
className: 'dim-globalTtlRow',
|
|
257
|
+
onSubmit: (event) => {
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
void commitTtl();
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
h('input', {
|
|
263
|
+
id: 'dim-globalTtlInput',
|
|
264
|
+
className: 'dim-globalTtlInput',
|
|
265
|
+
type: 'text',
|
|
266
|
+
value: ttlInput,
|
|
267
|
+
placeholder: String(DEFAULT_INBOUND_TTL_HOURS),
|
|
268
|
+
autoComplete: 'off',
|
|
269
|
+
disabled: phase === 'loading' || isSaving,
|
|
270
|
+
'aria-labelledby': 'dim-globalTtlTitle',
|
|
271
|
+
'aria-invalid': ttlError ? 'true' : undefined,
|
|
272
|
+
'aria-describedby': ttlError ? ttlErrorId : ttlHintsId,
|
|
273
|
+
onBlur: () => {
|
|
274
|
+
setTtlError(normalizeInboundTtlHours(ttlInput.trim()) === null);
|
|
275
|
+
},
|
|
276
|
+
onKeyDown: (event) => {
|
|
277
|
+
if (event.key !== 'Enter') return;
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
void commitTtl();
|
|
280
|
+
},
|
|
281
|
+
onChange: (event) => {
|
|
282
|
+
setTtlInput(event.target.value);
|
|
283
|
+
setTtlError(false);
|
|
284
|
+
setSaveError(null);
|
|
285
|
+
setSaveSucceeded(false);
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
h(GlobalButton, {
|
|
289
|
+
type: 'submit',
|
|
290
|
+
kind: 'primary',
|
|
291
|
+
className: 'dim-globalSaveButton',
|
|
292
|
+
disabled: !canSave,
|
|
293
|
+
}, isSaving ? '保存中…' : '保存'),
|
|
294
|
+
h('div', {
|
|
295
|
+
className: 'dim-globalSweepAction',
|
|
296
|
+
onBlur: (event) => {
|
|
297
|
+
if (sweepConfirming && !event.currentTarget.contains(event.relatedTarget)) cancelSweep();
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
h(GlobalButton, {
|
|
301
|
+
id: sweepTriggerId,
|
|
302
|
+
className: 'dim-globalSweepButton',
|
|
303
|
+
onClick: () => (sweepConfirming ? cancelSweep() : requestSweep()),
|
|
304
|
+
disabled: sweeping,
|
|
305
|
+
'aria-haspopup': 'dialog',
|
|
306
|
+
'aria-expanded': sweepConfirming,
|
|
307
|
+
'aria-controls': sweepConfirmId,
|
|
308
|
+
}, sweeping ? '正在清理…' : '清理过期附件'),
|
|
309
|
+
sweepConfirming
|
|
310
|
+
? h('div', {
|
|
311
|
+
id: sweepConfirmId,
|
|
312
|
+
className: 'dim-globalSweepConfirm',
|
|
313
|
+
role: 'alertdialog',
|
|
314
|
+
'aria-label': '确认清理过期附件',
|
|
315
|
+
'aria-describedby': sweepConfirmTextId,
|
|
316
|
+
onKeyDown: (event) => {
|
|
317
|
+
if (event.key !== 'Escape') return;
|
|
318
|
+
event.preventDefault();
|
|
319
|
+
cancelSweep({ restoreFocus: true });
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
h('p', { id: sweepConfirmTextId }, '确认清理当前已过期的附件?'),
|
|
323
|
+
h('div', { className: 'dim-globalSweepConfirmActions' },
|
|
324
|
+
h(GlobalButton, { onClick: () => cancelSweep({ restoreFocus: true }) }, '取消'),
|
|
325
|
+
h(GlobalButton, {
|
|
326
|
+
id: sweepConfirmButtonId,
|
|
327
|
+
kind: 'danger',
|
|
328
|
+
className: 'dim-globalSweepConfirmButton',
|
|
329
|
+
onClick: () => void runSweep(),
|
|
330
|
+
}, '确认清理')))
|
|
331
|
+
: null),
|
|
332
|
+
inlineStatus
|
|
333
|
+
? h('p', {
|
|
334
|
+
id: ttlErrorId,
|
|
335
|
+
className: 'dim-globalInline',
|
|
336
|
+
'data-tone': inlineStatus.role === 'alert' ? 'error' : undefined,
|
|
337
|
+
role: inlineStatus.role,
|
|
338
|
+
'aria-live': inlineStatus.role === 'status' ? 'polite' : undefined,
|
|
339
|
+
}, inlineStatus.message)
|
|
340
|
+
: null))));
|
|
341
|
+
}
|
|
@@ -215,6 +215,11 @@ const EN = Object.freeze({
|
|
|
215
215
|
'需在飞书为该机器人开通“获取群组中所有消息”权限(im:message.group_msg);开通后,机器人会处理群聊中的所有可见消息。': 'Grant this bot the “Read all messages in associated group chat” Feishu scope (im:message.group_msg); once granted, it will process every visible group message.',
|
|
216
216
|
'群聊响应方式修改失败,请重试。': 'Could not update the group response mode. Try again.',
|
|
217
217
|
'群消息权限授权失败,请重试。': 'Could not authorize group-message permission. Try again.',
|
|
218
|
+
'群聊以话题方式回复': 'Reply to group chats as Feishu topics',
|
|
219
|
+
'群聊以话题方式回复设置保存失败,请重试。': 'Could not save the topic reply setting. Try again.',
|
|
220
|
+
'关闭(群内直接回复)': 'Off (reply in the group feed)',
|
|
221
|
+
'开启(自动开启独立飞书话题)': 'On (auto-open a dedicated Feishu topic)',
|
|
222
|
+
'开启后,群聊中向机器人提问会自动开启独立飞书话题,回复落在话题内;每个话题是 dsh 会话列表里一条独立会话,上下文互不串。私聊不受影响。': 'When enabled, questions to the bot in a group auto-open a dedicated Feishu topic and replies stay inside it. Each topic is an independent conversation in dsh with its own context. Private chats are unaffected.',
|
|
218
223
|
'钉钉设置': 'DingTalk settings',
|
|
219
224
|
'企业微信设置': 'WeCom settings',
|
|
220
225
|
'扫码接入机器人': 'Scan QR code',
|
|
@@ -346,6 +351,13 @@ const EN = Object.freeze({
|
|
|
346
351
|
'请选择 Agent Preset。': 'Choose an Agent Preset.',
|
|
347
352
|
'Agent Preset 无效。': 'The Agent Preset is invalid.',
|
|
348
353
|
'Agent Preset 不存在或不可用。': 'The Agent Preset does not exist or is unavailable.',
|
|
354
|
+
'模型': 'Model',
|
|
355
|
+
'查看模型设置说明': 'View model setting help',
|
|
356
|
+
'跟随默认模型': 'Follow the default model',
|
|
357
|
+
'模型修改失败,请重试。': 'Could not update the model. Try again.',
|
|
358
|
+
'模型无效。': 'The model is invalid.',
|
|
359
|
+
'模型不存在或不可用。': 'The model does not exist or is unavailable.',
|
|
360
|
+
'当前模型已不可用,请选择其他模型或跟随默认模型。': 'The current model is unavailable. Choose another model or follow the default.',
|
|
349
361
|
'尚未检查': 'Not checked yet',
|
|
350
362
|
'刚刚': 'Just now',
|
|
351
363
|
'检查连接': 'Check connection',
|
|
@@ -761,6 +773,10 @@ const EN = Object.freeze({
|
|
|
761
773
|
'填写接收消息的用户 ID': 'Enter the recipient user ID',
|
|
762
774
|
'私聊': 'Direct message',
|
|
763
775
|
'群聊': 'Group',
|
|
776
|
+
'群聊设置': 'Group settings',
|
|
777
|
+
'当前机器人': 'Current bot',
|
|
778
|
+
'飞书群聊设置暂不可用。': 'Feishu group settings are currently unavailable.',
|
|
779
|
+
'飞书服务没有返回当前机器人的设置': 'Feishu did not return settings for this bot',
|
|
764
780
|
'群 Chat ID': 'Group Chat ID',
|
|
765
781
|
'用户 ID': 'User ID',
|
|
766
782
|
'填写钉钉用户 ID': 'Enter the DingTalk user ID',
|
|
@@ -823,6 +839,15 @@ const EN = Object.freeze({
|
|
|
823
839
|
'?使用这个 targetId 的外部调用将返回 unknown-target。': '? External calls using this targetId will return unknown-target.',
|
|
824
840
|
'正在删除…': 'Deleting…',
|
|
825
841
|
'确认删除': 'Delete target',
|
|
842
|
+
'会话双向同步': 'Two-way Session sync',
|
|
843
|
+
'自动跟随该私聊的当前会话': 'Automatically follow this direct message’s current Session',
|
|
844
|
+
'等待该私聊建立新会话': 'Waiting for this direct message to establish a new Session',
|
|
845
|
+
'关闭时不发送 DSH 会话消息': 'DSH Session messages are not sent while disabled',
|
|
846
|
+
'仅支持已经聊过、已建立当前会话且使用当前 Host Harness 的私聊目标': 'Only a known direct message with a current Session on this Host Harness is supported',
|
|
847
|
+
'会话同步仅支持已经聊过、已建立当前会话且使用当前 Host Harness 的私聊目标。': 'Session sync requires a known direct message with a current Session on this Host Harness.',
|
|
848
|
+
'会话同步设置失败,请稍后重试。': 'Could not update Session sync. Try again later.',
|
|
849
|
+
'修改接收位置会关闭会话双向同步。': 'Changing the destination disables two-way Session sync.',
|
|
850
|
+
' 会话同步设置也会一并删除。': ' Its Session sync setting will also be deleted.',
|
|
826
851
|
'投递目标设置暂不可用。': 'Delivery target settings are unavailable.',
|
|
827
852
|
'无法读取投递目标,请稍后重试。': 'Could not load delivery targets. Try again later.',
|
|
828
853
|
'← 返回机器人列表': '← Back to bots',
|
|
@@ -837,6 +862,28 @@ const EN = Object.freeze({
|
|
|
837
862
|
'正在读取投递目标…': 'Loading delivery targets…',
|
|
838
863
|
'尚未配置投递目标': 'No delivery targets yet',
|
|
839
864
|
'点击“新建目标”可从已聊过的会话选择,也可手动填写。': 'Select New target to choose a conversation or enter the details manually.',
|
|
865
|
+
'全局设置': 'Global settings',
|
|
866
|
+
'通用设置': 'General settings',
|
|
867
|
+
'通用设置分类': 'General settings categories',
|
|
868
|
+
'附件': 'Attachments',
|
|
869
|
+
'IM 设置导航': 'IM settings navigation',
|
|
870
|
+
'附件保留时长 (小时)': 'Attachment retention (hours)',
|
|
871
|
+
'查看附件保留时长说明': 'View attachment retention details',
|
|
872
|
+
'永久保留,不会自动清理': 'Keep forever; nothing is removed automatically',
|
|
873
|
+
'每 Turn 结束后立即清理': 'Cleaned up as soon as each turn ends',
|
|
874
|
+
'小时后自动清理': 'hours until automatic cleanup',
|
|
875
|
+
'请输入 -1、0 或 1~${INBOUND_TTL_MAX_HOURS} 之间的整数。': 'Enter -1, 0, or an integer from 1 to ${INBOUND_TTL_MAX_HOURS}.',
|
|
876
|
+
'设置保存失败,请稍后重试。': 'Could not save the setting. Try again later.',
|
|
877
|
+
'已保存': 'Saved',
|
|
878
|
+
'通用设置暂不可用。': 'General settings are unavailable.',
|
|
879
|
+
'正在读取通用设置…': 'Loading general settings…',
|
|
880
|
+
'无法读取通用设置,请稍后重试。': 'Could not load general settings. Try again later.',
|
|
881
|
+
'通用设置返回了无法识别的响应。': 'General settings returned an unrecognized response.',
|
|
882
|
+
'清理过期附件': 'Clean up expired attachments',
|
|
883
|
+
'确认清理过期附件': 'Confirm expired attachment cleanup',
|
|
884
|
+
'确认清理当前已过期的附件?': 'Clean up the attachments that are currently expired?',
|
|
885
|
+
'确认清理': 'Confirm cleanup',
|
|
886
|
+
'正在清理…': 'Cleaning up…',
|
|
840
887
|
});
|
|
841
888
|
|
|
842
889
|
export const en = EN;
|
|
@@ -865,6 +912,8 @@ function translateDynamic(text) {
|
|
|
865
912
|
if (match) return `${match[1]}/${match[2]} online`;
|
|
866
913
|
match = /^已接入 (\d+) 个机器人,其中 (\d+) 个在线$/.exec(text);
|
|
867
914
|
if (match) return `${match[2]} of ${match[1]} bots online`;
|
|
915
|
+
match = /^请输入 -1、0 或 1~(\d+) 之间的整数。$/.exec(text);
|
|
916
|
+
if (match) return `Enter -1, 0, or an integer from 1 to ${match[1]}.`;
|
|
868
917
|
match = /^正在读取\s*(.+?)\s*机器人状态…$/.exec(text);
|
|
869
918
|
if (match) return `Loading ${channelName(match[1])} bot status…`;
|
|
870
919
|
match = /^无法读取\s*(.+?)\s*机器人状态$/.exec(text);
|
|
@@ -48,6 +48,12 @@ import {
|
|
|
48
48
|
DELIVERY_RPC_CHANNEL,
|
|
49
49
|
DeliveryTargetSettingsPage,
|
|
50
50
|
} from './delivery-settings.js';
|
|
51
|
+
import {
|
|
52
|
+
GLOBAL_SETTINGS_RPC_CHANNEL,
|
|
53
|
+
GLOBAL_SETTINGS_TAB_ID,
|
|
54
|
+
GlobalSettingsLogoGlyph,
|
|
55
|
+
GlobalSettingsPanel,
|
|
56
|
+
} from './global-settings.js';
|
|
51
57
|
import {
|
|
52
58
|
createLoopbackAwareRpcCalls,
|
|
53
59
|
replacePageLocation,
|
|
@@ -173,6 +179,7 @@ export function IMSettingsTab({
|
|
|
173
179
|
officeRpcCall,
|
|
174
180
|
updateRpcCall,
|
|
175
181
|
deliveryRpcCall,
|
|
182
|
+
globalSettingsRpcCall,
|
|
176
183
|
workspaceDirectoryPicker,
|
|
177
184
|
browserLocation = globalThis.location,
|
|
178
185
|
navigateToRecoveryUrl = replacePageLocation,
|
|
@@ -182,7 +189,15 @@ export function IMSettingsTab({
|
|
|
182
189
|
const [runningVersion, setRunningVersion] = React.useState(IM_PLUGIN_VERSION);
|
|
183
190
|
const [deliverySettings, setDeliverySettings] = React.useState(null);
|
|
184
191
|
const githubTooltipId = React.useId();
|
|
192
|
+
const generalSettingsTooltipId = React.useId();
|
|
193
|
+
const globalSettingsSelected = selected === GLOBAL_SETTINGS_TAB_ID;
|
|
185
194
|
const active = CHANNELS.find((channel) => channel.id === selected) ?? CHANNELS[0];
|
|
195
|
+
const activeTabId = globalSettingsSelected
|
|
196
|
+
? 'dim-general-settings-trigger'
|
|
197
|
+
: `dim-tab-${active.id}`;
|
|
198
|
+
const activePanelId = globalSettingsSelected
|
|
199
|
+
? `dim-panel-${GLOBAL_SETTINGS_TAB_ID}`
|
|
200
|
+
: `dim-panel-${active.id}`;
|
|
186
201
|
const reportLoopbackRecovery = React.useCallback((recovery) => {
|
|
187
202
|
setLoopbackRecovery((current) => current?.url === recovery.url ? current : recovery);
|
|
188
203
|
}, []);
|
|
@@ -202,6 +217,7 @@ export function IMSettingsTab({
|
|
|
202
217
|
officeRpcCall,
|
|
203
218
|
updateRpcCall,
|
|
204
219
|
deliveryRpcCall,
|
|
220
|
+
globalSettingsRpcCall,
|
|
205
221
|
}, {
|
|
206
222
|
location: browserLocation,
|
|
207
223
|
onRecovery: reportLoopbackRecovery,
|
|
@@ -211,6 +227,7 @@ export function IMSettingsTab({
|
|
|
211
227
|
discordRpcCall,
|
|
212
228
|
deliveryRpcCall,
|
|
213
229
|
feishuRpcCall,
|
|
230
|
+
globalSettingsRpcCall,
|
|
214
231
|
officeRpcCall,
|
|
215
232
|
qqRpcCall,
|
|
216
233
|
reportLoopbackRecovery,
|
|
@@ -253,17 +270,36 @@ export function IMSettingsTab({
|
|
|
253
270
|
id: githubTooltipId,
|
|
254
271
|
className: 'dim-githubTooltip',
|
|
255
272
|
role: 'tooltip',
|
|
256
|
-
}, '帮助与反馈 · 前往 GitHub'))
|
|
273
|
+
}, '帮助与反馈 · 前往 GitHub')),
|
|
274
|
+
h('span', { className: 'dim-generalSettingsAction' },
|
|
275
|
+
h('button', {
|
|
276
|
+
type: 'button',
|
|
277
|
+
id: 'dim-general-settings-trigger',
|
|
278
|
+
className: 'dim-generalSettingsButton',
|
|
279
|
+
'aria-label': '通用设置',
|
|
280
|
+
'aria-describedby': generalSettingsTooltipId,
|
|
281
|
+
'aria-controls': `dim-panel-${GLOBAL_SETTINGS_TAB_ID}`,
|
|
282
|
+
'aria-current': globalSettingsSelected ? 'page' : undefined,
|
|
283
|
+
onClick: () => {
|
|
284
|
+
setSelected(GLOBAL_SETTINGS_TAB_ID);
|
|
285
|
+
setDeliverySettings(null);
|
|
286
|
+
},
|
|
287
|
+
}, h(GlobalSettingsLogoGlyph, { size: 17 })),
|
|
288
|
+
h('span', {
|
|
289
|
+
id: generalSettingsTooltipId,
|
|
290
|
+
className: 'dim-generalSettingsTooltip',
|
|
291
|
+
role: 'tooltip',
|
|
292
|
+
}, '通用设置'))),
|
|
257
293
|
),
|
|
258
294
|
h('div', { className: 'dim-layout' },
|
|
259
|
-
h('nav', { className: 'dim-rail', role: 'tablist', 'aria-label': 'IM
|
|
295
|
+
h('nav', { className: 'dim-rail', role: 'tablist', 'aria-label': 'IM 设置导航' },
|
|
260
296
|
CHANNELS.map((channel) => h('button', {
|
|
261
297
|
key: channel.id,
|
|
262
298
|
type: 'button',
|
|
263
299
|
role: 'tab',
|
|
264
300
|
id: `dim-tab-${channel.id}`,
|
|
265
301
|
className: 'dim-channel',
|
|
266
|
-
'aria-selected': channel.id === active.id,
|
|
302
|
+
'aria-selected': !globalSettingsSelected && channel.id === active.id,
|
|
267
303
|
'aria-controls': `dim-panel-${channel.id}`,
|
|
268
304
|
onClick: () => {
|
|
269
305
|
setSelected(channel.id);
|
|
@@ -279,8 +315,8 @@ export function IMSettingsTab({
|
|
|
279
315
|
h('main', {
|
|
280
316
|
className: 'dim-panel',
|
|
281
317
|
role: 'tabpanel',
|
|
282
|
-
id:
|
|
283
|
-
'aria-labelledby':
|
|
318
|
+
id: activePanelId,
|
|
319
|
+
'aria-labelledby': activeTabId,
|
|
284
320
|
},
|
|
285
321
|
loopbackRecovery
|
|
286
322
|
? h(LoopbackRecoveryNotice, {
|
|
@@ -289,7 +325,9 @@ export function IMSettingsTab({
|
|
|
289
325
|
})
|
|
290
326
|
: null,
|
|
291
327
|
h(BotSettingsContext.Provider, { value: botSettingsContext },
|
|
292
|
-
|
|
328
|
+
globalSettingsSelected
|
|
329
|
+
? h(GlobalSettingsPanel, { rpcCall: rpcCalls.globalSettingsRpcCall })
|
|
330
|
+
: deliverySettings?.channel === active.id
|
|
293
331
|
? h(DeliveryTargetSettingsPage, {
|
|
294
332
|
channel: active.id,
|
|
295
333
|
account: deliverySettings,
|
|
@@ -370,6 +408,8 @@ export function apply(ctx) {
|
|
|
370
408
|
ctx.connection.rpc.call(UPDATE_RPC_CHANNEL, endpoint, payload, signal);
|
|
371
409
|
const deliveryRpcCall = (endpoint, payload, signal) =>
|
|
372
410
|
ctx.connection.rpc.call(DELIVERY_RPC_CHANNEL, endpoint, payload, signal);
|
|
411
|
+
const globalSettingsRpcCall = (endpoint, payload, signal) =>
|
|
412
|
+
ctx.connection.rpc.call(GLOBAL_SETTINGS_RPC_CHANNEL, endpoint, payload, signal);
|
|
373
413
|
const workspaceDirectoryPicker = Object.freeze({
|
|
374
414
|
listDirectory: (path, signal) =>
|
|
375
415
|
callWorkspaceDirectoryApi(ctx, 'listDirectory', path, signal),
|
|
@@ -395,6 +435,7 @@ export function apply(ctx) {
|
|
|
395
435
|
officeRpcCall,
|
|
396
436
|
updateRpcCall,
|
|
397
437
|
deliveryRpcCall,
|
|
438
|
+
globalSettingsRpcCall,
|
|
398
439
|
workspaceDirectoryPicker,
|
|
399
440
|
}),
|
|
400
441
|
}, IMSettingsTab));
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EMPTY_MODEL_CATALOG,
|
|
5
|
+
modelSelectionId,
|
|
6
|
+
normalizeModelCatalog,
|
|
7
|
+
normalizeModelSelection,
|
|
8
|
+
} from '../../src/channels/shared/model-setting.mjs';
|
|
9
|
+
import { h } from './i18n.js';
|
|
10
|
+
|
|
11
|
+
export const SET_MODEL_ENDPOINT = 'bot.model.set';
|
|
12
|
+
export { EMPTY_MODEL_CATALOG, normalizeModelCatalog, normalizeModelSelection };
|
|
13
|
+
|
|
14
|
+
export const ModelCatalogContext = React.createContext(EMPTY_MODEL_CATALOG);
|
|
15
|
+
|
|
16
|
+
function optionValue(selection) {
|
|
17
|
+
return JSON.stringify([selection.provider, selection.model]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ModelEditor({ model = null, disabled = false, onSave }) {
|
|
21
|
+
const catalog = normalizeModelCatalog(
|
|
22
|
+
React.useContext(ModelCatalogContext) ?? EMPTY_MODEL_CATALOG,
|
|
23
|
+
);
|
|
24
|
+
const helpId = React.useId();
|
|
25
|
+
const current = normalizeModelSelection(model);
|
|
26
|
+
const [saving, setSaving] = React.useState(false);
|
|
27
|
+
const [error, setError] = React.useState(null);
|
|
28
|
+
const currentValue = current ? optionValue(current) : '';
|
|
29
|
+
const currentAvailable = !current || catalog.groups.some(
|
|
30
|
+
(group) => group.id === current.provider
|
|
31
|
+
&& group.models.some((entry) => entry.id === current.model),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const selections = new Map();
|
|
35
|
+
for (const group of catalog.groups) {
|
|
36
|
+
for (const entry of group.models) {
|
|
37
|
+
const selection = { provider: group.id, model: entry.id };
|
|
38
|
+
selections.set(optionValue(selection), selection);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (current && !currentAvailable) selections.set(currentValue, current);
|
|
42
|
+
|
|
43
|
+
const change = async (event) => {
|
|
44
|
+
const nextValue = event.target.value;
|
|
45
|
+
if (nextValue === currentValue || saving || disabled) return;
|
|
46
|
+
const next = nextValue ? selections.get(nextValue) : null;
|
|
47
|
+
if (nextValue && !next) return;
|
|
48
|
+
setSaving(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
try {
|
|
51
|
+
await onSave?.(next);
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
setError(cause?.message ?? '模型修改失败,请重试。');
|
|
54
|
+
} finally {
|
|
55
|
+
setSaving(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return h('div', { className: 'dim-preset dim-modelSetting' },
|
|
60
|
+
h('div', { className: 'dim-presetHeader' },
|
|
61
|
+
h('span', { className: 'dim-presetTitle' },
|
|
62
|
+
h('span', null, '模型'),
|
|
63
|
+
h('span', { className: 'dim-presetHelp' },
|
|
64
|
+
h('button', {
|
|
65
|
+
type: 'button',
|
|
66
|
+
className: 'dim-presetHelpButton',
|
|
67
|
+
'aria-label': '查看模型设置说明',
|
|
68
|
+
'aria-describedby': helpId,
|
|
69
|
+
}, h('span', { 'aria-hidden': 'true' }, '?')),
|
|
70
|
+
h('span', {
|
|
71
|
+
id: helpId,
|
|
72
|
+
className: 'dim-presetTooltip',
|
|
73
|
+
role: 'tooltip',
|
|
74
|
+
}, '只影响新建会话;若当前聊天已有会话,先发送 /new,再发送普通消息生效。'))),
|
|
75
|
+
saving ? h('span', { className: 'dim-presetStatus' }, '保存中…') : null),
|
|
76
|
+
h('select', {
|
|
77
|
+
className: 'dim-presetSelect dim-modelSelect',
|
|
78
|
+
value: currentValue,
|
|
79
|
+
disabled: disabled || saving,
|
|
80
|
+
'aria-label': '模型',
|
|
81
|
+
onChange: (event) => { void change(event); },
|
|
82
|
+
},
|
|
83
|
+
h('option', { value: '' }, '跟随默认模型'),
|
|
84
|
+
...catalog.groups.map((group) => h(
|
|
85
|
+
'optgroup',
|
|
86
|
+
{ key: group.id, label: group.name },
|
|
87
|
+
...group.models.map((entry) => {
|
|
88
|
+
const selection = { provider: group.id, model: entry.id };
|
|
89
|
+
const id = modelSelectionId(selection);
|
|
90
|
+
return h('option', { key: id, value: optionValue(selection) },
|
|
91
|
+
entry.name && entry.name !== entry.id ? `${entry.name}(${id})` : id);
|
|
92
|
+
}),
|
|
93
|
+
)),
|
|
94
|
+
current && !currentAvailable
|
|
95
|
+
? h('option', { value: currentValue }, [modelSelectionId(current), '(已不可用)'])
|
|
96
|
+
: null,
|
|
97
|
+
),
|
|
98
|
+
error || !currentAvailable ? h(
|
|
99
|
+
'p',
|
|
100
|
+
{ className: 'dim-presetError', role: error ? 'alert' : 'status' },
|
|
101
|
+
error ?? '当前模型已不可用,请选择其他模型或跟随默认模型。',
|
|
102
|
+
) : null,
|
|
103
|
+
);
|
|
104
|
+
}
|