@xmanrui/dsh-im 4.19.1 → 4.20.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/README.en.md +96 -4
- package/README.md +96 -4
- package/lib/client.js +666 -217
- package/lib/index.js +277 -273
- package/package.json +13 -1
- package/plugin-src/client/channels/weixin/api.js +35 -17
- package/plugin-src/client/channels/weixin/connection-error.js +68 -0
- package/plugin-src/client/channels/weixin/index.js +36 -12
- package/plugin-src/client/i18n.js +2 -0
- package/plugin-src/client/index.js +15 -0
- package/plugin-src/client/interface-language.js +89 -0
- package/plugin-src/host/channels/shared/startup.mjs +30 -4
- package/plugin-src/host/channels/weixin/connection-supervisor.mjs +13 -1
- package/plugin-src/host/channels/weixin/index.mjs +12 -3
- package/plugin-src/host/channels/weixin/production.mjs +53 -3
- package/plugin-src/host/channels/weixin/rpc.mjs +22 -17
- package/plugin-src/host/host-language-rpc.mjs +71 -0
- package/plugin-src/host/host-language.mjs +157 -0
- package/plugin-src/host/index.mjs +15 -2
- package/scripts/verify-interface-language.mjs +333 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-menu.mjs +8 -4
- package/src/channels/discord/discord-runtime.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +44 -13
- package/src/channels/qq/qq-bridge.mjs +12 -4
- package/src/channels/qq/qq-menu.mjs +11 -8
- package/src/channels/shared/bot-workspace-store.mjs +532 -40
- package/src/channels/shared/command-catalog.mjs +5 -0
- package/src/channels/shared/compact-command.mjs +14 -4
- package/src/channels/shared/control-command.mjs +1 -1
- package/src/channels/shared/deferred-delivery-coordinator.mjs +1 -1
- package/src/channels/shared/history-command.mjs +1 -1
- package/src/channels/shared/i18n-en/discord.mjs +2 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +41 -0
- package/src/channels/shared/i18n-en/telegram.mjs +5 -0
- package/src/channels/shared/i18n-en/weixin.mjs +2 -0
- package/src/channels/shared/i18n.mjs +46 -3
- package/src/channels/shared/interface-language-store.mjs +127 -0
- package/src/channels/shared/interface-language.mjs +51 -0
- package/src/channels/shared/model-command.mjs +5 -3
- package/src/channels/shared/token-bot-controller.mjs +26 -0
- package/src/channels/shared/workspace-command.mjs +114 -9
- package/src/channels/shared/workspace-session.mjs +55 -5
- package/src/channels/telegram/telegram-runtime.mjs +76 -14
- package/src/channels/wecom/wecom-bridge.mjs +2 -2
- package/src/channels/weixin/connection-error.en.mjs +116 -0
- package/src/channels/weixin/connection-error.mjs +204 -0
- package/src/channels/weixin/diagnostic-details.mjs +40 -0
- package/src/channels/weixin/state-store.mjs +4 -3
- package/src/channels/weixin/weixin-api.mjs +20 -8
- package/src/channels/weixin/weixin-bridge.mjs +3 -2
- package/src/channels/weixin/weixin-controller.mjs +133 -104
- package/src/channels/weixin/weixin-runtime.mjs +35 -24
|
@@ -6,6 +6,7 @@ import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
|
6
6
|
|
|
7
7
|
const WORKSPACE_COMMAND = /^\/(?:workspace|ws)(?:\s+([\s\S]+))?$/i;
|
|
8
8
|
const WORKSPACE_LIST_COMMAND = /^\/(?:workspacelist|workspaces|wsl)(?:\s+([\s\S]+))?$/i;
|
|
9
|
+
const THREAD_WORKSPACE_COMMAND = /^\/(?:conv|conversation|thread)(?:\s+([\s\S]+))?$/i;
|
|
9
10
|
const SESSION_LIST_COMMAND = /^\/(?:sessionlist|sessions)(?:\s+([\s\S]+))?$/i;
|
|
10
11
|
const SESSION_BIND_PREFIX = /^\/session(?=$|\s)/i;
|
|
11
12
|
const SESSION_BIND_COMMAND = /^\/session[ \t]+([^\s]+)$/i;
|
|
@@ -21,6 +22,7 @@ const SESSION_LIST_USAGE = [
|
|
|
21
22
|
'/sessionlist --limit N 列出当前工作区前 N 个会话(N 为正整数)',
|
|
22
23
|
'/sessionlist 工作区序号 按 /workspacelist 序号列出会话',
|
|
23
24
|
'/sessionlist 工作区绝对路径 列出指定工作区会话',
|
|
25
|
+
'不带工作区参数时,默认列出当前对话的有效工作区(未设置对话专属工作区时即 bot 默认工作区)。',
|
|
24
26
|
].join('\n');
|
|
25
27
|
|
|
26
28
|
function commandResult(message, messages = [message]) {
|
|
@@ -158,7 +160,9 @@ async function runWorkspaceListCommand(match, harness) {
|
|
|
158
160
|
)),
|
|
159
161
|
'',
|
|
160
162
|
t('切换用法:/workspace 工作区序号或绝对路径'),
|
|
163
|
+
t('对话专属:/conv 工作区序号或绝对路径(仅影响当前对话)'),
|
|
161
164
|
t('查看会话:/sessionlist 工作区序号或绝对路径'),
|
|
165
|
+
t('不带工作区参数时,/sessionlist 默认列出当前对话的有效工作区。'),
|
|
162
166
|
];
|
|
163
167
|
const message = lines.join('\n');
|
|
164
168
|
return commandResult(message, splitWorkspaceCommandMessage(message));
|
|
@@ -170,8 +174,22 @@ async function runWorkspaceListCommand(match, harness) {
|
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
176
|
|
|
177
|
+
async function conversationEffectiveWorkspace(harness, conversationKey) {
|
|
178
|
+
if (typeof harness?.currentConversationWorkspace !== 'function'
|
|
179
|
+
|| typeof conversationKey !== 'string' || !conversationKey) return null;
|
|
180
|
+
const [current] = await existingWorkspacePaths([harness.currentConversationWorkspace(conversationKey)]);
|
|
181
|
+
harness.assertWorkspaceScope?.();
|
|
182
|
+
return current ?? null;
|
|
183
|
+
}
|
|
184
|
+
|
|
173
185
|
export async function resolveSessionListWorkspace(selector, harness, options = {}) {
|
|
174
186
|
if (!selector) {
|
|
187
|
+
// With no explicit selector the listing follows the conversation's effective
|
|
188
|
+
// workspace, so /conv B makes /sessionlist list B instead of the bot default.
|
|
189
|
+
if (options.conversationKey) {
|
|
190
|
+
const effective = await conversationEffectiveWorkspace(harness, options.conversationKey);
|
|
191
|
+
if (effective) return { workspace: effective };
|
|
192
|
+
}
|
|
175
193
|
if (typeof harness?.currentWorkspace !== 'function') {
|
|
176
194
|
return { error: t('当前机器人没有可用的工作区。') };
|
|
177
195
|
}
|
|
@@ -242,21 +260,23 @@ function sessionListMessage(workspace, sessions, { currentWorkspace = false } =
|
|
|
242
260
|
].join('\n');
|
|
243
261
|
}
|
|
244
262
|
|
|
245
|
-
async function currentSessionListWorkspace(harness) {
|
|
263
|
+
async function currentSessionListWorkspace(harness, conversationKey) {
|
|
264
|
+
const effective = await conversationEffectiveWorkspace(harness, conversationKey);
|
|
265
|
+
if (effective) return effective;
|
|
246
266
|
if (typeof harness?.currentWorkspace !== 'function') return null;
|
|
247
267
|
const [current] = await existingWorkspacePaths([harness.currentWorkspace()]);
|
|
248
268
|
harness.assertWorkspaceScope?.();
|
|
249
269
|
return current ?? null;
|
|
250
270
|
}
|
|
251
271
|
|
|
252
|
-
async function runSessionListCommand(match, harness) {
|
|
272
|
+
async function runSessionListCommand(match, harness, conversationKey) {
|
|
253
273
|
const request = parseSessionListArgument(match[1]);
|
|
254
274
|
if (request.error) return commandResult(request.error);
|
|
255
275
|
if (typeof harness?.listWorkspaceSessions !== 'function') {
|
|
256
276
|
return commandResult(t('当前机器人暂不支持列出工作区会话。'));
|
|
257
277
|
}
|
|
258
278
|
try {
|
|
259
|
-
const resolved = await resolveSessionListWorkspace(request.selector, harness);
|
|
279
|
+
const resolved = await resolveSessionListWorkspace(request.selector, harness, { conversationKey });
|
|
260
280
|
if (resolved.error) return commandResult(resolved.error);
|
|
261
281
|
const listed = await harness.listWorkspaceSessions(resolved.workspace);
|
|
262
282
|
if (!listed || !Array.isArray(listed.sessions)) {
|
|
@@ -264,7 +284,7 @@ async function runSessionListCommand(match, harness) {
|
|
|
264
284
|
}
|
|
265
285
|
harness.assertWorkspaceScope?.();
|
|
266
286
|
const workspace = normalizedWorkspacePath(listed.workspace) ?? resolved.workspace;
|
|
267
|
-
const currentWorkspace = await currentSessionListWorkspace(harness);
|
|
287
|
+
const currentWorkspace = await currentSessionListWorkspace(harness, conversationKey);
|
|
268
288
|
const sessions = request.limit === null
|
|
269
289
|
? listed.sessions
|
|
270
290
|
: listed.sessions.slice(0, request.limit);
|
|
@@ -311,13 +331,15 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|
|
311
331
|
const match = SESSION_BIND_COMMAND.exec(command);
|
|
312
332
|
let sessionId = match?.[1];
|
|
313
333
|
if (typeof sessionId === 'string' && /^\d+$/u.test(sessionId)) {
|
|
314
|
-
//
|
|
334
|
+
// Use the same effective-workspace resolver as /sessionlist, so its indexes
|
|
335
|
+
// cannot select a session from the bot default after /conv sets an override.
|
|
315
336
|
if (typeof harness?.listWorkspaceSessions !== 'function'
|
|
316
|
-
|| typeof harness?.currentWorkspace !== 'function'
|
|
337
|
+
|| (typeof harness?.currentWorkspace !== 'function'
|
|
338
|
+
&& typeof harness?.currentConversationWorkspace !== 'function')) {
|
|
317
339
|
return commandResult(t('当前机器人暂不支持按序号绑定,请使用 /session Session ID。'));
|
|
318
340
|
}
|
|
319
341
|
try {
|
|
320
|
-
const selected = await
|
|
342
|
+
const selected = await resolveSessionListWorkspace('', harness, { conversationKey });
|
|
321
343
|
if (selected.error) return commandResult(selected.error);
|
|
322
344
|
const listed = await harness.listWorkspaceSessions(selected.workspace);
|
|
323
345
|
if (!listed || !Array.isArray(listed.sessions)) {
|
|
@@ -371,14 +393,96 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|
|
371
393
|
}
|
|
372
394
|
}
|
|
373
395
|
|
|
396
|
+
async function runConversationWorkspaceCommand(command, harness, conversationKey) {
|
|
397
|
+
if (typeof harness?.currentConversationWorkspace !== 'function') {
|
|
398
|
+
return commandResult(t('当前机器人暂不支持按对话设置专属工作区。'));
|
|
399
|
+
}
|
|
400
|
+
if (typeof conversationKey !== 'string' || !conversationKey) {
|
|
401
|
+
return commandResult(t('当前消息缺少可设置的对话上下文。'));
|
|
402
|
+
}
|
|
403
|
+
const argument = THREAD_WORKSPACE_COMMAND.exec(command)?.[1]?.trim() ?? '';
|
|
404
|
+
if (!argument) {
|
|
405
|
+
try {
|
|
406
|
+
const current = harness.currentConversationWorkspace(conversationKey);
|
|
407
|
+
harness.assertWorkspaceScope?.();
|
|
408
|
+
const lines = [t('当前对话工作区:{workspace}', { workspace: current })];
|
|
409
|
+
// 明确区分「显式绑定」与「回落 bot 默认」,否则用户无法判断之后改默认值会不会影响本对话。
|
|
410
|
+
const bound = typeof harness?.hasConversationWorkspaceOverride === 'function'
|
|
411
|
+
&& harness.hasConversationWorkspaceOverride(conversationKey) === true;
|
|
412
|
+
lines.push(bound
|
|
413
|
+
? t('状态:已为该对话显式绑定,之后修改 bot 默认工作区不会影响本对话。')
|
|
414
|
+
: t('状态:未显式绑定,当前跟随 bot 默认工作区。'));
|
|
415
|
+
// 顺带列出可切换的工作区,省得先跑一次 /workspacelist 再回来切。
|
|
416
|
+
try {
|
|
417
|
+
const { paths } = await workspacePathSnapshot(harness);
|
|
418
|
+
if (paths.length > 0) {
|
|
419
|
+
lines.push('', t('可切换的工作区({count}):', { count: paths.length }));
|
|
420
|
+
for (const [index, workspace] of paths.entries()) {
|
|
421
|
+
lines.push(`${index + 1}. ${workspace}${workspace === current ? t('(当前)') : ''}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} catch { /* 列表拿不到不影响主信息 */ }
|
|
425
|
+
lines.push('', t('用法:/conv 工作区序号或绝对路径'), t('清除:/conv clear'));
|
|
426
|
+
const message = lines.join('\n');
|
|
427
|
+
return commandResult(message, splitWorkspaceCommandMessage(message));
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (error?.code === 'workspace-bot-not-found') {
|
|
430
|
+
return commandResult(t('机器人正在移除或已重新接入,无法读取对话工作区。'));
|
|
431
|
+
}
|
|
432
|
+
return commandResult(t('暂时无法读取当前对话工作区,请稍后重试。'));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (/^(?:--default|clear)$/iu.test(argument)) {
|
|
436
|
+
try {
|
|
437
|
+
const current = await harness.clearConversationWorkspace(conversationKey);
|
|
438
|
+
harness.assertWorkspaceScope?.();
|
|
439
|
+
return commandResult(t('已清除对话专属工作区,当前使用 bot 默认工作区:{workspace}(之后默认工作区的变化会同步到本对话)', { workspace: current }));
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (error?.code === 'workspace-bot-not-found') {
|
|
442
|
+
return commandResult(t('机器人正在移除或已重新接入,无法清除对话工作区。'));
|
|
443
|
+
}
|
|
444
|
+
return commandResult(t('暂时无法清除对话工作区,请稍后重试。'));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (typeof harness?.switchConversationWorkspace !== 'function') {
|
|
448
|
+
return commandResult(t('当前机器人暂不支持设置对话工作区。'));
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
let selected = argument;
|
|
452
|
+
if (/^\d+$/u.test(argument)) {
|
|
453
|
+
const { paths } = await workspacePathSnapshot(harness);
|
|
454
|
+
const position = Number(argument);
|
|
455
|
+
if (!Number.isSafeInteger(position) || position < 1 || position > paths.length) {
|
|
456
|
+
return commandResult(t('工作区序号不存在,请先执行 /workspacelist。'));
|
|
457
|
+
}
|
|
458
|
+
selected = paths[position - 1];
|
|
459
|
+
}
|
|
460
|
+
const current = await harness.switchConversationWorkspace(conversationKey, selected);
|
|
461
|
+
return commandResult(t('当前对话工作区已切换为:{workspace}', { workspace: current }));
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if (['workspace-not-absolute', 'workspace-not-found', 'workspace-not-directory'].includes(error?.code)) {
|
|
464
|
+
return commandResult(t(`{message}
|
|
465
|
+
用法:/conv 工作区绝对路径`, { message: error.message }));
|
|
466
|
+
}
|
|
467
|
+
if (error?.code === 'workspace-bot-not-found') {
|
|
468
|
+
return commandResult(t('机器人正在移除或已重新接入,无法切换对话工作区。'));
|
|
469
|
+
}
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
374
474
|
export async function runWorkspaceCommand(text, harness, conversationKey) {
|
|
375
475
|
if (!isWorkspaceCommand(text)) return null;
|
|
376
476
|
const command = text.trim();
|
|
477
|
+
const threadMatch = THREAD_WORKSPACE_COMMAND.exec(command);
|
|
478
|
+
if (threadMatch) {
|
|
479
|
+
return runConversationWorkspaceCommand(command, harness, conversationKey);
|
|
480
|
+
}
|
|
377
481
|
if (SESSION_BIND_PREFIX.test(command)) {
|
|
378
482
|
return runSessionBindCommand(command, harness, conversationKey);
|
|
379
483
|
}
|
|
380
484
|
const sessionListMatch = SESSION_LIST_COMMAND.exec(command);
|
|
381
|
-
if (sessionListMatch) return runSessionListCommand(sessionListMatch, harness);
|
|
485
|
+
if (sessionListMatch) return runSessionListCommand(sessionListMatch, harness, conversationKey);
|
|
382
486
|
const listMatch = WORKSPACE_LIST_COMMAND.exec(command);
|
|
383
487
|
if (listMatch) return runWorkspaceListCommand(listMatch, harness);
|
|
384
488
|
const match = WORKSPACE_COMMAND.exec(command);
|
|
@@ -423,5 +527,6 @@ export function isWorkspaceCommand(text) {
|
|
|
423
527
|
return SESSION_BIND_PREFIX.test(command)
|
|
424
528
|
|| SESSION_LIST_COMMAND.test(command)
|
|
425
529
|
|| WORKSPACE_LIST_COMMAND.test(command)
|
|
426
|
-
|| WORKSPACE_COMMAND.test(command)
|
|
530
|
+
|| WORKSPACE_COMMAND.test(command)
|
|
531
|
+
|| THREAD_WORKSPACE_COMMAND.test(command);
|
|
427
532
|
}
|
|
@@ -3,9 +3,45 @@ import { initialSessionTitle } from './session-title.mjs';
|
|
|
3
3
|
|
|
4
4
|
export const WORKSPACE_SESSION_STALE = 'workspace-session-stale';
|
|
5
5
|
|
|
6
|
-
function
|
|
6
|
+
function workspaceSessionStaleError() {
|
|
7
|
+
const error = new Error('The conversation workspace changed before the prompt was sent.');
|
|
8
|
+
error.code = WORKSPACE_SESSION_STALE;
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read the conversation's effective-workspace generation. A bot-scoped Harness
|
|
14
|
+
* exposes it; anything else (plain fixtures, older Harnesses) yields null and
|
|
15
|
+
* disables the conversation-level fence without changing existing behavior.
|
|
16
|
+
*/
|
|
17
|
+
function readConversationGeneration(harness, conversationKey) {
|
|
18
|
+
return typeof harness?.conversationWorkspaceGeneration === 'function'
|
|
19
|
+
? harness.conversationWorkspaceGeneration(conversationKey) ?? null
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function conversationGenerationMoved(harness, conversationKey, generation) {
|
|
24
|
+
if (generation === null) return false;
|
|
25
|
+
return readConversationGeneration(harness, conversationKey) !== generation;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wait for a conversation-level workspace switch that is still committing.
|
|
30
|
+
* An explicit /conv publishes its fence before it persists, so a message that
|
|
31
|
+
* is already in flight must settle on the new workspace instead of resolving a
|
|
32
|
+
* session in the one being left behind.
|
|
33
|
+
*/
|
|
34
|
+
async function awaitPendingConversationSwitch(harness, conversationKey) {
|
|
35
|
+
if (typeof harness?.pendingConversationWorkspaceSwitch !== 'function') return;
|
|
36
|
+
const pending = harness.pendingConversationWorkspaceSwitch(conversationKey);
|
|
37
|
+
if (pending && typeof pending.then === 'function') await pending.catch(() => undefined);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function workspaceSession(harness, sessionId, conversationKey) {
|
|
7
41
|
if (typeof harness.workspaceSession === 'function') {
|
|
8
|
-
return
|
|
42
|
+
return conversationKey
|
|
43
|
+
? harness.workspaceSession(sessionId, conversationKey)
|
|
44
|
+
: harness.workspaceSession(sessionId);
|
|
9
45
|
}
|
|
10
46
|
const session = {
|
|
11
47
|
sessionId,
|
|
@@ -67,12 +103,20 @@ export async function askInWorkspaceSession({
|
|
|
67
103
|
while (true) {
|
|
68
104
|
try {
|
|
69
105
|
const binding = await withSessionBindingLock(state, key, async () => {
|
|
106
|
+
await awaitPendingConversationSwitch(harness, key);
|
|
70
107
|
let sessionId = state.sessionFor(key);
|
|
71
|
-
let session = sessionId ? workspaceSession(harness, sessionId) : null;
|
|
108
|
+
let session = sessionId ? workspaceSession(harness, sessionId, key) : null;
|
|
72
109
|
if (!session || !(await sessionExists(session, existsOptions))) {
|
|
73
|
-
sessionId = await createSession(harness,
|
|
110
|
+
sessionId = await createSession(harness, {
|
|
111
|
+
conversationKey: key,
|
|
112
|
+
...(createOptions ?? {}),
|
|
113
|
+
});
|
|
74
114
|
if (await state.setSession(key, sessionId) === false) return null;
|
|
75
|
-
|
|
115
|
+
// Binding committed: capture the conversation's effective-workspace
|
|
116
|
+
// generation together with the session, so the prompt below is fenced
|
|
117
|
+
// against a conversation switch that only commits after the bind.
|
|
118
|
+
const conversationGeneration = readConversationGeneration(harness, key);
|
|
119
|
+
session = workspaceSession(harness, sessionId, key);
|
|
76
120
|
if (initialTitle && typeof session.renameTitle === 'function') {
|
|
77
121
|
try {
|
|
78
122
|
await session.renameTitle(initialTitle, renameOptions);
|
|
@@ -83,6 +127,12 @@ export async function askInWorkspaceSession({
|
|
|
83
127
|
console.warn('[dsh-im] unable to set the initial Session title:', error?.message ?? error);
|
|
84
128
|
}
|
|
85
129
|
}
|
|
130
|
+
// A switch that committed while the title was being set has already
|
|
131
|
+
// cleared the mapping and must not receive this prompt.
|
|
132
|
+
if (conversationGenerationMoved(harness, key, conversationGeneration)) {
|
|
133
|
+
throw workspaceSessionStaleError();
|
|
134
|
+
}
|
|
135
|
+
return { sessionId, session };
|
|
86
136
|
}
|
|
87
137
|
return { sessionId, session };
|
|
88
138
|
});
|
|
@@ -524,10 +524,10 @@ export class TelegramBotClient {
|
|
|
524
524
|
|
|
525
525
|
const fallback = await this.#sendPlain(target, text);
|
|
526
526
|
const terminalText = fallback.deliveryOutcome === 'sent'
|
|
527
|
-
? '回复已发送。'
|
|
527
|
+
? t('回复已发送。')
|
|
528
528
|
: fallback.deliveryOutcome === 'unknown'
|
|
529
|
-
? '回复发送结果未能确认。'
|
|
530
|
-
: '消息发送失败,请稍后重试。';
|
|
529
|
+
? t('回复发送结果未能确认。')
|
|
530
|
+
: t('消息发送失败,请稍后重试。');
|
|
531
531
|
try {
|
|
532
532
|
await this.#api.editMessageText({
|
|
533
533
|
chatId: target.chatId,
|
|
@@ -717,13 +717,13 @@ export class TelegramBotClient {
|
|
|
717
717
|
keepalive: true,
|
|
718
718
|
logger: this.#logger,
|
|
719
719
|
});
|
|
720
|
-
await stream.update(createTextDeliveryBlock('正在处理…', 'plain'));
|
|
720
|
+
await stream.update(createTextDeliveryBlock(t('正在处理…'), 'plain'));
|
|
721
721
|
return stream;
|
|
722
722
|
}
|
|
723
723
|
|
|
724
724
|
const placeholder = await this.#api.sendMessage({
|
|
725
725
|
chatId: target.chatId,
|
|
726
|
-
text: '正在处理…',
|
|
726
|
+
text: t('正在处理…'),
|
|
727
727
|
replyToMessageId: target.replyToMessageId,
|
|
728
728
|
messageThreadId: target.messageThreadId,
|
|
729
729
|
signal: this.#signal,
|
|
@@ -824,6 +824,14 @@ export class TelegramRuntime {
|
|
|
824
824
|
#abortController = null;
|
|
825
825
|
#pollTask = null;
|
|
826
826
|
#starting = null;
|
|
827
|
+
// True only while #start() is between "started connecting" and "ready",
|
|
828
|
+
// which is the one window a skipped refresh must be reconciled. A refresh on
|
|
829
|
+
// a never-started or already-stopped runtime stays a no-op.
|
|
830
|
+
#connecting = false;
|
|
831
|
+
// True when a language switch arrived while the runtime was connecting, so a
|
|
832
|
+
// refreshCommandMenu() had nothing to push. Reconciled once startup
|
|
833
|
+
// completes, so the platform always ends up with the latest language.
|
|
834
|
+
#menuDirty = false;
|
|
827
835
|
|
|
828
836
|
constructor({
|
|
829
837
|
config,
|
|
@@ -902,6 +910,7 @@ export class TelegramRuntime {
|
|
|
902
910
|
|
|
903
911
|
async #start() {
|
|
904
912
|
await this.stop();
|
|
913
|
+
this.#connecting = true;
|
|
905
914
|
this.#status.startedAt = new Date().toISOString();
|
|
906
915
|
this.#status.connectionState = 'connecting';
|
|
907
916
|
this.#status.lastError = null;
|
|
@@ -930,14 +939,7 @@ export class TelegramRuntime {
|
|
|
930
939
|
throw error;
|
|
931
940
|
}
|
|
932
941
|
try {
|
|
933
|
-
|
|
934
|
-
// Both operations use the existing default scope and language. Sending
|
|
935
|
-
// the full list replaces old entries; an empty catalog clears that list.
|
|
936
|
-
if (commands.length > 0) {
|
|
937
|
-
await api.setMyCommands({ commands, signal: controller.signal });
|
|
938
|
-
} else {
|
|
939
|
-
await api.deleteMyCommands({ signal: controller.signal });
|
|
940
|
-
}
|
|
942
|
+
await this.#sendCommandMenu(api, controller.signal);
|
|
941
943
|
await api.setChatMenuButton({ menuButton: COMMANDS_MENU_BUTTON, signal: controller.signal });
|
|
942
944
|
} catch (error) {
|
|
943
945
|
this.#logger.warn?.(
|
|
@@ -973,6 +975,21 @@ export class TelegramRuntime {
|
|
|
973
975
|
this.#status.connectionState = 'connected';
|
|
974
976
|
this.#status.lastCheckedAt = now;
|
|
975
977
|
this.#status.lastConnectedAt = now;
|
|
978
|
+
// A language switch that landed while we were connecting was skipped by
|
|
979
|
+
// refreshCommandMenu(); re-send the menu now that the API is usable, so
|
|
980
|
+
// the platform ends with the latest language instead of the one that was
|
|
981
|
+
// current when #sendCommandMenu first ran.
|
|
982
|
+
if (this.#menuDirty) {
|
|
983
|
+
this.#menuDirty = false;
|
|
984
|
+
try {
|
|
985
|
+
await this.#sendCommandMenu(api, controller.signal);
|
|
986
|
+
} catch (error) {
|
|
987
|
+
this.#logger.warn?.(
|
|
988
|
+
`[dsh-im:telegram] bot ${this.#config.botId} command menu catch-up failed:`,
|
|
989
|
+
error,
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
976
993
|
this.#pollTask = this.#poll(cursor, controller.signal);
|
|
977
994
|
this.#pollTask.catch((error) => {
|
|
978
995
|
if (controller.signal.aborted) return;
|
|
@@ -988,6 +1005,51 @@ export class TelegramRuntime {
|
|
|
988
1005
|
this.#status.lastError = error?.message ?? String(error);
|
|
989
1006
|
await this.stop();
|
|
990
1007
|
throw error;
|
|
1008
|
+
} finally {
|
|
1009
|
+
// Whether the bot is now ready or the attempt failed, the connecting
|
|
1010
|
+
// window is over. A failed attempt also drops any unreconciled dirty
|
|
1011
|
+
// flag: a later retry re-sends the menu from scratch in the current
|
|
1012
|
+
// language anyway.
|
|
1013
|
+
this.#connecting = false;
|
|
1014
|
+
this.#menuDirty = false;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Both operations use the existing default scope and language. Sending the
|
|
1019
|
+
// full list replaces old entries; an empty catalog clears that list.
|
|
1020
|
+
async #sendCommandMenu(api, signal) {
|
|
1021
|
+
const commands = telegramCommandMenu(this.#commandCatalog);
|
|
1022
|
+
if (commands.length > 0) await api.setMyCommands({ commands, signal });
|
|
1023
|
+
else await api.deleteMyCommands({ signal });
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Re-send the command menu in the current host message language.
|
|
1028
|
+
*
|
|
1029
|
+
* The menu Telegram shows is registered once per connection, so a language
|
|
1030
|
+
* change would otherwise stay invisible until the bot reconnected. This
|
|
1031
|
+
* replaces the text only: the chat menu button is owned by connect.
|
|
1032
|
+
* @returns whether a connected bot accepted the refreshed menu.
|
|
1033
|
+
*/
|
|
1034
|
+
async refreshCommandMenu() {
|
|
1035
|
+
const api = this.#api;
|
|
1036
|
+
const signal = this.#abortController?.signal;
|
|
1037
|
+
if (!this.#status.ready || !api || !signal || signal.aborted) {
|
|
1038
|
+
// A refresh during an in-progress connection must not be lost: mark the
|
|
1039
|
+
// menu dirty so #start() reconciles it the moment the bot is ready. A
|
|
1040
|
+
// never-started or stopped runtime stays a no-op, as before.
|
|
1041
|
+
if (this.#connecting) this.#menuDirty = true;
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
await this.#sendCommandMenu(api, signal);
|
|
1046
|
+
return true;
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
this.#logger.warn?.(
|
|
1049
|
+
`[dsh-im:telegram] bot ${this.#config.botId} command menu refresh failed:`,
|
|
1050
|
+
error,
|
|
1051
|
+
);
|
|
1052
|
+
return false;
|
|
991
1053
|
}
|
|
992
1054
|
}
|
|
993
1055
|
|
|
@@ -1000,7 +1062,7 @@ export class TelegramRuntime {
|
|
|
1000
1062
|
}
|
|
1001
1063
|
const sessionId = this.#state.sessionFor(key);
|
|
1002
1064
|
const session = typeof sessionId === 'string' && sessionId
|
|
1003
|
-
? this.#harness.workspaceSession?.(sessionId)
|
|
1065
|
+
? this.#harness.workspaceSession?.(sessionId, key)
|
|
1004
1066
|
: null;
|
|
1005
1067
|
const text = await recoverAssistantTextByTimestamp({ session, quotedAt, signal });
|
|
1006
1068
|
return text ? { content: text } : { unavailableReason: 'not-delivered' };
|
|
@@ -628,7 +628,7 @@ export class WecomHarnessBridge {
|
|
|
628
628
|
workspacePathSnapshot(this.#harness, options),
|
|
629
629
|
this.#harness.listWorkspaceSessions?.(workspace, options),
|
|
630
630
|
(async () => {
|
|
631
|
-
const session = sessionId ? this.#harness.workspaceSession?.(sessionId) : null;
|
|
631
|
+
const session = sessionId ? this.#harness.workspaceSession?.(sessionId, key) : null;
|
|
632
632
|
return typeof session?.models === 'function'
|
|
633
633
|
? session.models(options) : this.#harness.listModels?.(options);
|
|
634
634
|
})(),
|
|
@@ -852,7 +852,7 @@ export class WecomHarnessBridge {
|
|
|
852
852
|
} else if (menu.section === 'models') {
|
|
853
853
|
title = t('🧠 切换模型');
|
|
854
854
|
const sessionId = this.#state.sessionFor(key);
|
|
855
|
-
const session = sessionId ? this.#harness.workspaceSession?.(sessionId) : null;
|
|
855
|
+
const session = sessionId ? this.#harness.workspaceSession?.(sessionId, key) : null;
|
|
856
856
|
const catalog = typeof session?.models === 'function'
|
|
857
857
|
? await session.models(options) : await this.#harness.listModels(options);
|
|
858
858
|
entries = catalog.groups.flatMap((group) => group.models.map((model) => [
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Shared diagnostic copy for the WeChat Host and settings client.
|
|
2
|
+
export default {
|
|
3
|
+
"插件版本": "Plugin version",
|
|
4
|
+
"DSH 微信管理接口返回了无法识别的响应,请重新读取状态。": "The DSH WeChat management endpoint returned an invalid response. Refresh the status.",
|
|
5
|
+
"无法读取现有登录凭据。请检查 DSH 凭据存储。": "Could not read the saved login credential. Check the DSH credential store.",
|
|
6
|
+
"登录凭据无法写入 DSH 凭据存储。请检查凭据存储是否可写。": "Could not save the login credential to DSH. Check whether the credential store is writable.",
|
|
7
|
+
"账号配置无法写入本机。请检查 DSH_HOME 目录权限。": "Could not save the account configuration. Check permissions on DSH_HOME.",
|
|
8
|
+
"无法初始化账号状态或工作区。请检查 DSH_HOME 和工作区目录。": "Could not prepare the account state or workspace. Check DSH_HOME and the workspace directory.",
|
|
9
|
+
"插件无法连接当前 Harness,请检查宿主是否已就绪。": "The plugin could not connect to this Harness. Check whether the host is ready.",
|
|
10
|
+
"Harness 健康检查超时。请确认 dsh web 未阻塞。": "The Harness health check timed out. Check whether the host is responsive.",
|
|
11
|
+
"Harness 健康检查需要身份认证。请检查代理、网关或自定义鉴权配置。": "The Harness health check requires authentication. Check proxy, gateway or custom authentication settings.",
|
|
12
|
+
"本机 Harness 请求被代理要求认证。请让回环地址绕过代理,并检查 NO_PROXY 配置。": "The proxy requires authentication for the local Harness request. Bypass the proxy for loopback addresses and check NO_PROXY.",
|
|
13
|
+
"Harness 异常拒绝了回环地址的健康检查。请检查 HTTP 代理、Harness 源码版本和构建产物。": "Harness rejected a loopback health check. Check the HTTP proxy and the running Harness build.",
|
|
14
|
+
"Harness 的 Host 信任检查拒绝了非回环地址请求。请检查 harnessBaseUrl 与 trustedHosts 配置。": "Harness rejected the non-loopback Host. Check harnessBaseUrl and trustedHosts.",
|
|
15
|
+
"健康检查收到了非 Harness 标准的 403 拒绝响应。请检查代理或网关配置。": "The health check received a non-standard HTTP 403 response. Check proxy or gateway settings.",
|
|
16
|
+
"找不到 Harness 健康检查接口。请确认 Harness 与插件版本兼容。": "The Harness health check endpoint was not found. Check Harness and plugin compatibility.",
|
|
17
|
+
"Harness 健康检查返回服务错误。请查看 DSH 日志。": "The Harness health check returned a service error. Check the DSH logs.",
|
|
18
|
+
"Harness 返回了无法识别的响应。请确认 Harness 与插件版本兼容。": "Harness returned an unrecognized response. Check Harness and plugin compatibility.",
|
|
19
|
+
"Harness 拒绝了健康检查请求。请查看 DSH 日志。": "Harness rejected the health check. Check the DSH logs.",
|
|
20
|
+
"Harness 健康检查发生未知错误。请查看 DSH 日志。": "The Harness health check failed with an unknown error. Check the DSH logs.",
|
|
21
|
+
"消息连接初始化失败。请查看 DSH 日志后重试。": "The message connection could not start. Check the DSH logs and retry.",
|
|
22
|
+
"暂时无法访问微信服务。": "The WeChat service is temporarily unreachable.",
|
|
23
|
+
"微信服务请求超时。": "The WeChat service request timed out.",
|
|
24
|
+
"微信服务请求失败(HTTP {status})。": "The WeChat service request failed (HTTP {status}).",
|
|
25
|
+
"微信服务返回了无法解析的响应。": "The WeChat service returned an unparseable response.",
|
|
26
|
+
"微信服务没有返回有效二维码。": "The WeChat service did not return a valid QR code.",
|
|
27
|
+
"微信服务返回了不受信任的扫码地址。": "The WeChat service returned an untrusted QR-code URL.",
|
|
28
|
+
"微信服务返回了无效的连接地址。": "The WeChat service returned an invalid connection URL.",
|
|
29
|
+
"微信服务返回了不受信任的连接地址。": "The WeChat service returned an untrusted connection URL.",
|
|
30
|
+
"拒绝访问不受信任的微信服务地址。": "Refusing to access an untrusted WeChat service URL.",
|
|
31
|
+
"微信服务返回了无法识别的扫码状态。": "The WeChat service returned an unrecognized QR-code status.",
|
|
32
|
+
"微信授权成功,但返回的账号凭据不完整。": "WeChat authorized the account but returned incomplete credentials.",
|
|
33
|
+
"微信服务拒绝了二维码申请。": "The WeChat service rejected the QR-code request.",
|
|
34
|
+
"已取得扫码地址,但本机二维码图片生成失败。": "The login URL was obtained, but the QR-code image could not be generated locally.",
|
|
35
|
+
"无法生成微信二维码。": "Could not generate a WeChat QR code.",
|
|
36
|
+
"这次微信绑定任务已不存在,请重新读取状态或生成二维码。": "This WeChat setup attempt no longer exists. Refresh the status or generate a new QR code.",
|
|
37
|
+
"这次微信绑定任务当前不能提交配对码,请重新读取状态。": "This setup attempt is not waiting for a pairing code. Refresh the status.",
|
|
38
|
+
"登录凭据缺失,请移除账号后重新扫码。": "The login credential is missing. Remove the account and scan a new QR code.",
|
|
39
|
+
"微信登录凭据已失效,请移除账号后重新扫码。": "The WeChat login credential has expired. Remove the account and scan a new QR code.",
|
|
40
|
+
"无法从 DSH 凭据存储移除微信登录凭据。": "Could not remove the WeChat login credential from the DSH credential store.",
|
|
41
|
+
"微信账号配置移除失败。": "Could not remove the WeChat account configuration.",
|
|
42
|
+
"无法读取微信账号状态文件。": "Could not read the WeChat account state file.",
|
|
43
|
+
"无法保存微信账号状态文件。": "Could not save the WeChat account state file.",
|
|
44
|
+
"微信账号已移除,但本机状态文件清理失败。": "The WeChat account was removed, but its local state file could not be cleaned up.",
|
|
45
|
+
"无法保存微信工作区设置。": "Could not save the WeChat workspace settings.",
|
|
46
|
+
"微信账号已移除,但工作区设置清理失败。": "The WeChat account was removed, but its workspace settings could not be cleaned up.",
|
|
47
|
+
"微信账号连接启动失败。": "The WeChat account connection could not start.",
|
|
48
|
+
"微信消息同步请求被拒绝。": "WeChat rejected the message sync request.",
|
|
49
|
+
"微信服务未确认停止通知。": "WeChat did not confirm the stop notification.",
|
|
50
|
+
"微信消息连接停止时发生错误。": "An error occurred while stopping the WeChat connection.",
|
|
51
|
+
"无法读取微信连接状态,请重新读取;之前的操作可能已完成。": "Could not read the WeChat connection status. Refresh it; the previous operation may have completed.",
|
|
52
|
+
"微信激活过程中发生未知错误。": "An unknown error occurred while activating the WeChat account.",
|
|
53
|
+
"微信操作发生未知错误。": "The WeChat operation failed with an unknown error.",
|
|
54
|
+
"微信配置格式错误,请检查 config.json 和 workspaces.json 后重启 DSH。": "The WeChat configuration is invalid. Check config.json and workspaces.json, then restart DSH.",
|
|
55
|
+
"微信配置访问权限不足,请检查数据目录权限后重启 DSH。": "The WeChat configuration could not be accessed. Check the data directory permissions and restart DSH.",
|
|
56
|
+
"微信初始化失败,请查看 DSH 启动日志。": "WeChat initialization failed. Check the DSH startup logs.",
|
|
57
|
+
"微信连接未就绪。": "The WeChat connection is not ready.",
|
|
58
|
+
"微信操作失败后,原账号状态恢复失败。": "The previous account state could not be restored after the failed WeChat operation.",
|
|
59
|
+
"原账号状态尚未确认,请重新读取状态,并反馈诊断信息。": "The previous account state has not been confirmed. Refresh the status and share the diagnostic information.",
|
|
60
|
+
"操作未完成,已恢复之前的本机状态。请处理上述问题后重试。": "The operation did not complete. The previous local state was restored. Resolve the problem above before retrying.",
|
|
61
|
+
"凭据由只读来源提供,请在启动 DSH 的配置中检查该来源。": "The credential comes from a read-only source. Check that source in the configuration used to launch DSH.",
|
|
62
|
+
"微信服务域名解析失败,请检查运行 DSH 的机器的网络和 DNS 设置后重试。": "The WeChat service domain could not be resolved. Check the network and DNS settings on the machine running DSH and retry.",
|
|
63
|
+
"请检查 DSH 数据目录和对应文件的读写权限。": "Check read and write permissions on the DSH data directory and the affected file.",
|
|
64
|
+
"磁盘空间不足,请释放空间后重试。": "The disk is full. Free up space and retry.",
|
|
65
|
+
"本机文件格式无效,请检查对应配置或状态文件;不要清空登录凭据。": "The local file format is invalid. Check the affected configuration or state file; do not clear login credentials.",
|
|
66
|
+
"请检查运行 DSH 的机器的系统时间、证书和网络设置。": "Check the system time, certificates and network settings on the machine running DSH.",
|
|
67
|
+
"请移除失效接入并重新扫码绑定。": "Remove the invalid connection and scan a new QR code to connect.",
|
|
68
|
+
"请确认当前 DSH 宿主已就绪,并检查 Harness 与插件实际运行版本及宿主日志。": "Check that this DSH host is ready, and check the running Harness and plugin versions and the host logs.",
|
|
69
|
+
"微信服务限流,请稍后重试。": "The WeChat service is rate-limiting requests. Try again later.",
|
|
70
|
+
"访问被拒绝,请检查服务访问限制;HTTP 状态本身不能确认登录凭据已失效。": "Access was denied. Check service access restrictions; the HTTP status alone does not establish that the login credential expired.",
|
|
71
|
+
"请检查运行 DSH 的机器能否访问微信服务,然后重试。": "Check whether the machine running DSH can access the WeChat service, then retry.",
|
|
72
|
+
"请按参考号查看 DSH 日志,并复制诊断信息反馈。": "Find this reference in the DSH logs and copy the diagnostic information when reporting the issue.",
|
|
73
|
+
"无法完成微信管理请求,请检查 DSH 连接后重新读取状态。": "The WeChat management request could not be completed. Check the DSH connection and refresh the status.",
|
|
74
|
+
"查询微信扫码状态失败": "Could not query WeChat QR-code status",
|
|
75
|
+
"无法完成微信管理请求": "Could not complete the WeChat management request",
|
|
76
|
+
"无法生成微信二维码": "Could not generate a WeChat QR code",
|
|
77
|
+
"状态读取失败,以下是上次读取的状态。": "Status refresh failed. The last known status is shown below.",
|
|
78
|
+
"诊断详情": "Diagnostic details",
|
|
79
|
+
"诊断信息": "Diagnostic information",
|
|
80
|
+
"复制诊断信息": "Copy diagnostic information",
|
|
81
|
+
"诊断信息已复制": "Diagnostic information copied",
|
|
82
|
+
"未取得 Host 诊断参考号。": "No diagnostic reference was received from the host.",
|
|
83
|
+
"无法访问剪贴板,请选择并复制以下诊断信息。": "Clipboard access is unavailable. Select and copy the diagnostic information below.",
|
|
84
|
+
"错误码": "Error code",
|
|
85
|
+
"失败阶段": "Failed step",
|
|
86
|
+
"底层原因": "Underlying cause",
|
|
87
|
+
"HTTP 状态": "HTTP status",
|
|
88
|
+
"微信返回码": "WeChat return code",
|
|
89
|
+
"参考号": "Reference",
|
|
90
|
+
"发生时间": "Time",
|
|
91
|
+
"加载微信配置": "Load WeChat configuration",
|
|
92
|
+
"申请二维码": "Request QR code",
|
|
93
|
+
"生成二维码图片": "Generate QR-code image",
|
|
94
|
+
"查询扫码状态": "Query QR-code status",
|
|
95
|
+
"提交配对码": "Submit pairing code",
|
|
96
|
+
"取消绑定": "Cancel setup",
|
|
97
|
+
"读取登录凭据": "Read login credential",
|
|
98
|
+
"保存登录凭据": "Save login credential",
|
|
99
|
+
"移除登录凭据": "Remove login credential",
|
|
100
|
+
"保存账号配置": "Save account configuration",
|
|
101
|
+
"移除账号配置": "Remove account configuration",
|
|
102
|
+
"读取账号状态": "Read account state",
|
|
103
|
+
"保存账号状态": "Save account state",
|
|
104
|
+
"清理账号状态": "Clean up account state",
|
|
105
|
+
"保存工作区设置": "Save workspace settings",
|
|
106
|
+
"清理工作区设置": "Clean up workspace settings",
|
|
107
|
+
"准备消息连接": "Prepare message connection",
|
|
108
|
+
"激活微信账号": "Activate WeChat account",
|
|
109
|
+
"检查 DSH 宿主": "Check DSH host",
|
|
110
|
+
"启动微信连接": "Start WeChat connection",
|
|
111
|
+
"同步微信消息": "Sync WeChat messages",
|
|
112
|
+
"停止微信连接": "Stop WeChat connection",
|
|
113
|
+
"读取连接状态": "Read connection status",
|
|
114
|
+
"恢复原账号状态": "Restore previous account state",
|
|
115
|
+
"访问 DSH 管理接口": "Access DSH management API"
|
|
116
|
+
};
|