@xmanrui/dsh-im 1.5.0 → 2.0.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 +16 -4
- package/README.md +16 -4
- package/lib/index.js +208 -178
- package/package.json +1 -1
- package/plugin-src/host/index.mjs +2 -0
- package/src/channels/dingtalk/config-store.mjs +5 -3
- package/src/channels/dingtalk/dingtalk-api.mjs +2 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +45 -40
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +3 -1
- package/src/channels/dingtalk/dingtalk-controller.mjs +24 -23
- package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -3
- package/src/channels/dingtalk/state-store.mjs +3 -1
- package/src/channels/discord/config-store.mjs +2 -1
- package/src/channels/discord/discord-api.mjs +25 -1
- package/src/channels/discord/discord-runtime.mjs +237 -11
- package/src/channels/feishu/bridge.mjs +131 -123
- package/src/channels/feishu/feishu-cards.mjs +62 -54
- package/src/channels/feishu/feishu-channel.mjs +5 -4
- package/src/channels/feishu/feishu-runtime.mjs +10 -9
- package/src/channels/feishu/message-utils.mjs +4 -3
- package/src/channels/office/office-job-executor.mjs +15 -14
- package/src/channels/office/office-runtime.mjs +6 -5
- package/src/channels/qq/config-store.mjs +3 -1
- package/src/channels/qq/markdown-reply.mjs +4 -2
- package/src/channels/qq/qq-bridge.mjs +57 -52
- package/src/channels/qq/qq-controller.mjs +17 -16
- package/src/channels/qq/qq-runtime.mjs +3 -2
- package/src/channels/shared/agent-preset.mjs +3 -1
- package/src/channels/shared/compact-command.mjs +15 -14
- package/src/channels/shared/connection-test.mjs +6 -4
- package/src/channels/shared/control-command.mjs +13 -11
- package/src/channels/shared/editable-message-stream.mjs +4 -2
- package/src/channels/shared/harness-approval.mjs +23 -21
- package/src/channels/shared/harness-client.mjs +3 -2
- package/src/channels/shared/harness-question.mjs +8 -6
- package/src/channels/shared/i18n-en/dingtalk.mjs +52 -0
- package/src/channels/shared/i18n-en/discord.mjs +8 -0
- package/src/channels/shared/i18n-en/feishu.mjs +217 -0
- package/src/channels/shared/i18n-en/office.mjs +23 -0
- package/src/channels/shared/i18n-en/qq.mjs +54 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +91 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +215 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +99 -0
- package/src/channels/shared/i18n-en/slack.mjs +27 -0
- package/src/channels/shared/i18n-en/telegram.mjs +21 -0
- package/src/channels/shared/i18n-en/wecom.mjs +38 -0
- package/src/channels/shared/i18n-en/weixin.mjs +80 -0
- package/src/channels/shared/i18n-en/whatsapp.mjs +17 -0
- package/src/channels/shared/i18n-en.mjs +35 -0
- package/src/channels/shared/i18n.mjs +35 -0
- package/src/channels/shared/image-prompt.mjs +17 -15
- package/src/channels/shared/inbound-file.mjs +5 -3
- package/src/channels/shared/model-command.mjs +35 -31
- package/src/channels/shared/preset-command.mjs +41 -38
- package/src/channels/shared/semantic/artifact-delivery.mjs +1 -1
- package/src/channels/shared/semantic/delivery.mjs +38 -0
- package/src/channels/shared/text-harness-bridge.mjs +130 -63
- package/src/channels/shared/token-bot-controller.mjs +32 -11
- package/src/channels/shared/workspace-command.mjs +69 -58
- package/src/channels/slack/config-store.mjs +3 -1
- package/src/channels/slack/slack-api.mjs +6 -5
- package/src/channels/slack/slack-controller.mjs +10 -9
- package/src/channels/slack/slack-runtime.mjs +16 -4
- package/src/channels/telegram/config-store.mjs +2 -1
- package/src/channels/telegram/telegram-api.mjs +72 -6
- package/src/channels/telegram/telegram-rich-message.mjs +147 -0
- package/src/channels/telegram/telegram-runtime.mjs +373 -5
- package/src/channels/wecom/config-store.mjs +3 -1
- package/src/channels/wecom/wecom-bridge.mjs +59 -55
- package/src/channels/wecom/wecom-controller.mjs +20 -19
- package/src/channels/wecom/wecom-runtime.mjs +2 -1
- package/src/channels/weixin/config-store.mjs +2 -1
- package/src/channels/weixin/weixin-bridge.mjs +50 -49
- package/src/channels/weixin/weixin-controller.mjs +22 -21
- package/src/channels/weixin/weixin-runtime.mjs +3 -2
- package/src/channels/whatsapp/config-store.mjs +3 -1
- package/src/channels/whatsapp/whatsapp-bridge.mjs +3 -1
- package/src/channels/whatsapp/whatsapp-controller.mjs +15 -14
- package/src/channels/whatsapp/whatsapp-runtime.mjs +5 -4
- package/src/channels/whatsapp/whatsapp-web-session.mjs +3 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// English translations for host-side user-facing text.
|
|
2
|
+
// Keys are the exact Chinese literals passed to t() in src/channels/**.
|
|
3
|
+
// Chinese output is the identity default and needs no entries here.
|
|
4
|
+
// Entries are maintained per area in ./i18n-en/*.mjs and merged here.
|
|
5
|
+
|
|
6
|
+
import sharedA from './i18n-en/shared-a.mjs';
|
|
7
|
+
import sharedB from './i18n-en/shared-b.mjs';
|
|
8
|
+
import sharedC from './i18n-en/shared-c.mjs';
|
|
9
|
+
import feishu from './i18n-en/feishu.mjs';
|
|
10
|
+
import dingtalk from './i18n-en/dingtalk.mjs';
|
|
11
|
+
import wecom from './i18n-en/wecom.mjs';
|
|
12
|
+
import qq from './i18n-en/qq.mjs';
|
|
13
|
+
import weixin from './i18n-en/weixin.mjs';
|
|
14
|
+
import slack from './i18n-en/slack.mjs';
|
|
15
|
+
import telegram from './i18n-en/telegram.mjs';
|
|
16
|
+
import discord from './i18n-en/discord.mjs';
|
|
17
|
+
import whatsapp from './i18n-en/whatsapp.mjs';
|
|
18
|
+
import office from './i18n-en/office.mjs';
|
|
19
|
+
|
|
20
|
+
export const EN = Object.freeze(Object.assign(
|
|
21
|
+
{},
|
|
22
|
+
sharedA,
|
|
23
|
+
sharedB,
|
|
24
|
+
sharedC,
|
|
25
|
+
feishu,
|
|
26
|
+
dingtalk,
|
|
27
|
+
wecom,
|
|
28
|
+
qq,
|
|
29
|
+
weixin,
|
|
30
|
+
slack,
|
|
31
|
+
telegram,
|
|
32
|
+
discord,
|
|
33
|
+
whatsapp,
|
|
34
|
+
office,
|
|
35
|
+
));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Host-side i18n for dsh-im. Mirrors the conventions of the settings-UI
|
|
2
|
+
// translator in plugin-src/client/i18n.js: dictionary keys are the exact
|
|
3
|
+
// Chinese source literals, and Chinese (zh) is the identity default, so
|
|
4
|
+
// untranslated or unrecognized text always falls back to the original
|
|
5
|
+
// Chinese output unchanged.
|
|
6
|
+
//
|
|
7
|
+
// The English dictionary lives in ./i18n-en.mjs to keep this module small.
|
|
8
|
+
|
|
9
|
+
import { EN } from './i18n-en.mjs';
|
|
10
|
+
|
|
11
|
+
let language = 'zh';
|
|
12
|
+
|
|
13
|
+
// Accepts 'en', 'en-US', 'english' (any case) as English; anything else
|
|
14
|
+
// (including undefined and unrecognized values) selects Chinese.
|
|
15
|
+
export function setImHostLanguage(lang) {
|
|
16
|
+
const normalized = typeof lang === 'string' ? lang.trim().toLowerCase() : '';
|
|
17
|
+
language = normalized === 'english' || /^en(?:[-_].*)?$/u.test(normalized) ? 'en' : 'zh';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getImHostLanguage() {
|
|
21
|
+
return language;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Translate a user-facing Chinese literal. In zh mode (the default) this is
|
|
25
|
+
// the identity function. Optional `params` fills `{name}` placeholders in
|
|
26
|
+
// both the Chinese key and its translation, e.g.
|
|
27
|
+
// t('共 {count} 个机器人', { count: 3 })
|
|
28
|
+
export function t(text, params) {
|
|
29
|
+
if (typeof text !== 'string') return text;
|
|
30
|
+
const translated = language === 'en' ? EN[text] ?? text : text;
|
|
31
|
+
if (params == null) return translated;
|
|
32
|
+
return translated.replace(/\{(\w+)\}/g, (match, name) =>
|
|
33
|
+
Object.hasOwn(params, name) ? String(params[name]) : match,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t } from './i18n.mjs';
|
|
2
|
+
|
|
1
3
|
const DEFAULT_MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
2
4
|
const DEFAULT_MAX_IMAGES = 20;
|
|
3
5
|
const DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
@@ -67,7 +69,7 @@ export async function fetchImageBuffer(url, {
|
|
|
67
69
|
throw new ImagePromptError(
|
|
68
70
|
'image-redirect-blocked',
|
|
69
71
|
`Image download redirect was blocked (HTTP ${response.status})`,
|
|
70
|
-
'图片下载地址发生了重定向,暂时无法读取。',
|
|
72
|
+
t('图片下载地址发生了重定向,暂时无法读取。'),
|
|
71
73
|
);
|
|
72
74
|
}
|
|
73
75
|
if (!response?.ok) {
|
|
@@ -75,7 +77,7 @@ export async function fetchImageBuffer(url, {
|
|
|
75
77
|
throw new ImagePromptError(
|
|
76
78
|
'image-http-error',
|
|
77
79
|
`Image download failed with HTTP ${response?.status ?? 'unknown'}`,
|
|
78
|
-
|
|
80
|
+
t('图片下载失败(HTTP {status}),请重新发送后再试。', { status: response?.status ?? 'unknown' }),
|
|
79
81
|
);
|
|
80
82
|
}
|
|
81
83
|
const declaredLength = Number(response.headers?.get?.('content-length'));
|
|
@@ -84,7 +86,7 @@ export async function fetchImageBuffer(url, {
|
|
|
84
86
|
throw new ImagePromptError(
|
|
85
87
|
'image-too-large',
|
|
86
88
|
`Image response declares ${declaredLength} bytes; the limit is ${maxBytes}`,
|
|
87
|
-
'图片超过 5 MB,请压缩后重试。',
|
|
89
|
+
t('图片超过 5 MB,请压缩后重试。'),
|
|
88
90
|
);
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -99,7 +101,7 @@ export async function fetchImageBuffer(url, {
|
|
|
99
101
|
throw new ImagePromptError(
|
|
100
102
|
'image-too-large',
|
|
101
103
|
`Image response exceeded ${maxBytes} bytes`,
|
|
102
|
-
'图片超过 5 MB,请压缩后重试。',
|
|
104
|
+
t('图片超过 5 MB,请压缩后重试。'),
|
|
103
105
|
);
|
|
104
106
|
}
|
|
105
107
|
chunks.push(data);
|
|
@@ -112,7 +114,7 @@ export async function fetchImageBuffer(url, {
|
|
|
112
114
|
throw new ImagePromptError(
|
|
113
115
|
'image-too-large',
|
|
114
116
|
`Image response contains ${data.length} bytes; the limit is ${maxBytes}`,
|
|
115
|
-
'图片超过 5 MB,请压缩后重试。',
|
|
117
|
+
t('图片超过 5 MB,请压缩后重试。'),
|
|
116
118
|
);
|
|
117
119
|
}
|
|
118
120
|
return data;
|
|
@@ -192,7 +194,7 @@ export async function promptContentForMessage(message, {
|
|
|
192
194
|
throw new ImagePromptError(
|
|
193
195
|
'too-many-images',
|
|
194
196
|
`Image message contains ${sources.length} images; the limit is ${maxImages}`,
|
|
195
|
-
|
|
197
|
+
t('一次最多只能处理 {maxImages} 张图片。', { maxImages }),
|
|
196
198
|
);
|
|
197
199
|
}
|
|
198
200
|
|
|
@@ -200,7 +202,7 @@ export async function promptContentForMessage(message, {
|
|
|
200
202
|
const content = [];
|
|
201
203
|
let totalImageBytes = 0;
|
|
202
204
|
if (text) content.push({ type: 'text', text });
|
|
203
|
-
else if (sources.length > 0) content.push({ type: 'text', text: DEFAULT_IMAGE_PROMPT });
|
|
205
|
+
else if (sources.length > 0) content.push({ type: 'text', text: t(DEFAULT_IMAGE_PROMPT) });
|
|
204
206
|
|
|
205
207
|
for (const [index, source] of sources.entries()) {
|
|
206
208
|
signal?.throwIfAborted();
|
|
@@ -208,14 +210,14 @@ export async function promptContentForMessage(message, {
|
|
|
208
210
|
throw new ImagePromptError(
|
|
209
211
|
'image-too-large',
|
|
210
212
|
`Image ${index + 1} declares ${source.size} bytes; the limit is ${maxImageBytes}`,
|
|
211
|
-
'图片超过 5 MB,请压缩后重试。',
|
|
213
|
+
t('图片超过 5 MB,请压缩后重试。'),
|
|
212
214
|
);
|
|
213
215
|
}
|
|
214
216
|
if (Number.isFinite(source?.size) && totalImageBytes + source.size > maxTotalImageBytes) {
|
|
215
217
|
throw new ImagePromptError(
|
|
216
218
|
'images-too-large',
|
|
217
219
|
`Images declare more than ${maxTotalImageBytes} bytes in total`,
|
|
218
|
-
'一次发送的图片总大小过大,请减少图片数量或压缩后重试。',
|
|
220
|
+
t('一次发送的图片总大小过大,请减少图片数量或压缩后重试。'),
|
|
219
221
|
);
|
|
220
222
|
}
|
|
221
223
|
|
|
@@ -230,7 +232,7 @@ export async function promptContentForMessage(message, {
|
|
|
230
232
|
throw new ImagePromptError(
|
|
231
233
|
'image-download-failed',
|
|
232
234
|
`Unable to download image ${index + 1}: ${error?.message ?? String(error)}`,
|
|
233
|
-
'图片下载失败,请重新发送后再试。',
|
|
235
|
+
t('图片下载失败,请重新发送后再试。'),
|
|
234
236
|
{ cause: error },
|
|
235
237
|
);
|
|
236
238
|
}
|
|
@@ -239,21 +241,21 @@ export async function promptContentForMessage(message, {
|
|
|
239
241
|
throw new ImagePromptError(
|
|
240
242
|
'invalid-image-data',
|
|
241
243
|
`Image ${index + 1} returned no data`,
|
|
242
|
-
'未能读取图片内容,请重新发送。',
|
|
244
|
+
t('未能读取图片内容,请重新发送。'),
|
|
243
245
|
);
|
|
244
246
|
}
|
|
245
247
|
if (loaded.data.length > maxImageBytes) {
|
|
246
248
|
throw new ImagePromptError(
|
|
247
249
|
'image-too-large',
|
|
248
250
|
`Image ${index + 1} contains ${loaded.data.length} bytes; the limit is ${maxImageBytes}`,
|
|
249
|
-
'图片超过 5 MB,请压缩后重试。',
|
|
251
|
+
t('图片超过 5 MB,请压缩后重试。'),
|
|
250
252
|
);
|
|
251
253
|
}
|
|
252
254
|
if (totalImageBytes + loaded.data.length > maxTotalImageBytes) {
|
|
253
255
|
throw new ImagePromptError(
|
|
254
256
|
'images-too-large',
|
|
255
257
|
`Images contain more than ${maxTotalImageBytes} bytes in total`,
|
|
256
|
-
'一次发送的图片总大小过大,请减少图片数量或压缩后重试。',
|
|
258
|
+
t('一次发送的图片总大小过大,请减少图片数量或压缩后重试。'),
|
|
257
259
|
);
|
|
258
260
|
}
|
|
259
261
|
totalImageBytes += loaded.data.length;
|
|
@@ -262,7 +264,7 @@ export async function promptContentForMessage(message, {
|
|
|
262
264
|
throw new ImagePromptError(
|
|
263
265
|
'unsupported-image-type',
|
|
264
266
|
`Image ${index + 1} is not JPEG, PNG, GIF, or WebP`,
|
|
265
|
-
'暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。',
|
|
267
|
+
t('暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。'),
|
|
266
268
|
);
|
|
267
269
|
}
|
|
268
270
|
content.push({
|
|
@@ -289,7 +291,7 @@ export function imagePromptDiagnostic(error) {
|
|
|
289
291
|
}
|
|
290
292
|
const reason = error.details.reason;
|
|
291
293
|
const userMessage = Object.hasOwn(HOST_ATTACHMENT_USER_MESSAGES, reason)
|
|
292
|
-
? HOST_ATTACHMENT_USER_MESSAGES[reason]
|
|
294
|
+
? t(HOST_ATTACHMENT_USER_MESSAGES[reason])
|
|
293
295
|
: null;
|
|
294
296
|
return userMessage ? { code: 'attachment-error', reason, userMessage } : null;
|
|
295
297
|
}
|
|
@@ -3,10 +3,12 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { pipeline } from 'node:stream/promises';
|
|
5
5
|
|
|
6
|
+
import { t } from './i18n.mjs';
|
|
7
|
+
|
|
6
8
|
const FILES_DIRECTORY = join('.dsh-im', 'inbound');
|
|
7
9
|
|
|
8
10
|
export class InboundFileError extends Error {
|
|
9
|
-
constructor(code, message, userMessage = '文件接收失败,请重新发送后再试。', options = {}) {
|
|
11
|
+
constructor(code, message, userMessage = t('文件接收失败,请重新发送后再试。'), options = {}) {
|
|
10
12
|
super(message, options);
|
|
11
13
|
this.name = 'InboundFileError';
|
|
12
14
|
this.code = code;
|
|
@@ -124,7 +126,7 @@ export async function stageInboundFiles(message, {
|
|
|
124
126
|
throw new InboundFileError(
|
|
125
127
|
'inbound-file-download-failed',
|
|
126
128
|
`Unable to download inbound file ${index + 1}: ${error?.message ?? String(error)}`,
|
|
127
|
-
'文件下载失败,请重新发送后再试。',
|
|
129
|
+
t('文件下载失败,请重新发送后再试。'),
|
|
128
130
|
{ cause: error },
|
|
129
131
|
);
|
|
130
132
|
}
|
|
@@ -152,7 +154,7 @@ export async function stageInboundFiles(message, {
|
|
|
152
154
|
throw new InboundFileError(
|
|
153
155
|
'inbound-file-download-failed',
|
|
154
156
|
`Unable to stream inbound file ${index + 1}: ${error?.message ?? String(error)}`,
|
|
155
|
-
'文件下载失败,请重新发送后再试。',
|
|
157
|
+
t('文件下载失败,请重新发送后再试。'),
|
|
156
158
|
{ cause: error },
|
|
157
159
|
);
|
|
158
160
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { splitWorkspaceCommandMessage } from './workspace-command.mjs';
|
|
2
|
+
import { t } from './i18n.mjs';
|
|
2
3
|
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
3
4
|
import { withSessionBindingLock } from './session-binding-lock.mjs';
|
|
4
5
|
|
|
@@ -109,9 +110,9 @@ function modelNumberRequest(requested) {
|
|
|
109
110
|
|
|
110
111
|
function invalidModelNumberMessage(requested) {
|
|
111
112
|
return [
|
|
112
|
-
|
|
113
|
+
t('模型序号无效:{input}', { input: safeDisplayText(requested) }),
|
|
113
114
|
'',
|
|
114
|
-
'请发送 /models 查看并输入有效的正整数序号。',
|
|
115
|
+
t('请发送 /models 查看并输入有效的正整数序号。'),
|
|
115
116
|
].join('\n');
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -119,43 +120,43 @@ function formatCatalog(catalog) {
|
|
|
119
120
|
const currentId = catalog.current
|
|
120
121
|
? modelId(catalog.current.provider, catalog.current.model)
|
|
121
122
|
: null;
|
|
122
|
-
const lines = ['可用模型:'];
|
|
123
|
+
const lines = [t('可用模型:')];
|
|
123
124
|
let index = 0;
|
|
124
|
-
if (catalog.groups.length === 0) lines.push('', '当前没有可用模型。');
|
|
125
|
+
if (catalog.groups.length === 0) lines.push('', t('当前没有可用模型。'));
|
|
125
126
|
for (const group of catalog.groups) {
|
|
126
127
|
lines.push('', safeDisplayText(group.name) || safeDisplayText(group.id));
|
|
127
128
|
for (const model of group.models) {
|
|
128
129
|
index += 1;
|
|
129
130
|
const id = modelId(group.id, model.id);
|
|
130
|
-
lines.push(`${index}. ${safeDisplayText(id)}${id === currentId ? '(当前)' : ''}`);
|
|
131
|
+
lines.push(`${index}. ${safeDisplayText(id)}${id === currentId ? t('(当前)') : ''}`);
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
if (catalog.failures.length > 0) {
|
|
134
|
-
lines.push('', '以下模型提供方暂时不可用:');
|
|
135
|
+
lines.push('', t('以下模型提供方暂时不可用:'));
|
|
135
136
|
for (const failure of catalog.failures) {
|
|
136
137
|
lines.push(`- ${safeDisplayText(failure.name) || safeDisplayText(failure.id)}`);
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
|
-
if (index > 0) lines.push('', '切换模型:/model <序号>');
|
|
140
|
+
if (index > 0) lines.push('', t('切换模型:/model <序号>'));
|
|
140
141
|
return lines.join('\n');
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
function currentModelMessage(current) {
|
|
144
145
|
return [
|
|
145
|
-
'当前模型:',
|
|
146
|
+
t('当前模型:'),
|
|
146
147
|
modelId(current.provider, current.model),
|
|
147
148
|
'',
|
|
148
|
-
'查看全部模型:/models',
|
|
149
|
-
'切换模型:/model <序号>',
|
|
149
|
+
t('查看全部模型:/models'),
|
|
150
|
+
t('切换模型:/model <序号>'),
|
|
150
151
|
].join('\n');
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
function noSessionMessage() {
|
|
154
155
|
return [
|
|
155
|
-
'当前聊天还没有会话。',
|
|
156
|
+
t('当前聊天还没有会话。'),
|
|
156
157
|
'',
|
|
157
|
-
'查看模型:/models',
|
|
158
|
-
'选择模型:/model <序号>',
|
|
158
|
+
t('查看模型:/models'),
|
|
159
|
+
t('选择模型:/model <序号>'),
|
|
159
160
|
].join('\n');
|
|
160
161
|
}
|
|
161
162
|
|
|
@@ -166,26 +167,26 @@ function errorCode(error) {
|
|
|
166
167
|
function modelErrorMessage(error, action) {
|
|
167
168
|
const code = errorCode(error);
|
|
168
169
|
if (code === 'agent-busy') {
|
|
169
|
-
return '当前任务正在运行,请等待完成或先发送 /stop。';
|
|
170
|
+
return t('当前任务正在运行,请等待完成或先发送 /stop。');
|
|
170
171
|
}
|
|
171
172
|
if (code === 'session-not-found') {
|
|
172
|
-
return '当前聊天绑定的会话已不存在,请重试。';
|
|
173
|
+
return t('当前聊天绑定的会话已不存在,请重试。');
|
|
173
174
|
}
|
|
174
175
|
if (code === 'model-unavailable') {
|
|
175
|
-
return '无法切换到该模型。模型当前不可用,或不支持当前会话中的图片。';
|
|
176
|
+
return t('无法切换到该模型。模型当前不可用,或不支持当前会话中的图片。');
|
|
176
177
|
}
|
|
177
178
|
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
|
|
178
|
-
return '工作区或机器人状态已发生变化,请重试。';
|
|
179
|
+
return t('工作区或机器人状态已发生变化,请重试。');
|
|
179
180
|
}
|
|
180
181
|
if (code === SESSION_BINDING_CHANGED) {
|
|
181
|
-
return '当前聊天绑定的会话已发生变化,请重试。';
|
|
182
|
+
return t('当前聊天绑定的会话已发生变化,请重试。');
|
|
182
183
|
}
|
|
183
184
|
if (code === 'cancelled' || error?.name === 'AbortError') {
|
|
184
|
-
return action === 'list' ? '获取模型列表已取消。' : '模型切换已取消。';
|
|
185
|
+
return action === 'list' ? t('获取模型列表已取消。') : t('模型切换已取消。');
|
|
185
186
|
}
|
|
186
187
|
return action === 'list'
|
|
187
|
-
? '暂时无法获取模型列表,请稍后重试。'
|
|
188
|
-
: '模型切换失败,请稍后重试。';
|
|
188
|
+
? t('暂时无法获取模型列表,请稍后重试。')
|
|
189
|
+
: t('模型切换失败,请稍后重试。');
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
async function boundSession(harness, state, key, options) {
|
|
@@ -243,12 +244,12 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
243
244
|
if (!isModelCommand(text)) return null;
|
|
244
245
|
const command = text.trim();
|
|
245
246
|
if (options.hasImages) {
|
|
246
|
-
return commandResult('模型命令仅支持纯文字,请移除图片后重试。');
|
|
247
|
+
return commandResult(t('模型命令仅支持纯文字,请移除图片后重试。'));
|
|
247
248
|
}
|
|
248
249
|
const requestOptions = rpcOptions(options.signal);
|
|
249
250
|
|
|
250
251
|
if (isModelsCommand(command)) {
|
|
251
|
-
if (!/^\/models[ \t]*$/iu.test(command)) return commandResult(MODELS_USAGE);
|
|
252
|
+
if (!/^\/models[ \t]*$/iu.test(command)) return commandResult(t(MODELS_USAGE));
|
|
252
253
|
try {
|
|
253
254
|
const bound = await boundSession(harness, state, key, requestOptions);
|
|
254
255
|
const catalog = bound
|
|
@@ -261,7 +262,7 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
const match = /^\/model(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(command);
|
|
264
|
-
if (!match) return commandResult(MODEL_USAGE);
|
|
265
|
+
if (!match) return commandResult(t(MODEL_USAGE));
|
|
265
266
|
const requested = match[1];
|
|
266
267
|
if (!requested) {
|
|
267
268
|
try {
|
|
@@ -279,13 +280,13 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
279
280
|
}
|
|
280
281
|
if (!numberRequest
|
|
281
282
|
&& (!requested.includes('/') || requested.startsWith('/') || requested.endsWith('/'))) {
|
|
282
|
-
return commandResult(MODEL_USAGE);
|
|
283
|
+
return commandResult(t(MODEL_USAGE));
|
|
283
284
|
}
|
|
284
285
|
if (options.pendingInteraction) {
|
|
285
286
|
return commandResult([
|
|
286
|
-
'当前任务正在等待你的回答或审批。',
|
|
287
|
+
t('当前任务正在等待你的回答或审批。'),
|
|
287
288
|
'',
|
|
288
|
-
'请先处理当前请求,或者发送 /stop 停止任务。',
|
|
289
|
+
t('请先处理当前请求,或者发送 /stop 停止任务。'),
|
|
289
290
|
].join('\n'));
|
|
290
291
|
}
|
|
291
292
|
|
|
@@ -293,7 +294,7 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
293
294
|
return await withSessionBindingLock(state, key, async () => {
|
|
294
295
|
const bound = await boundSession(harness, state, key, requestOptions);
|
|
295
296
|
if (bound && await sessionIsBusy(bound.session, options.control, requestOptions)) {
|
|
296
|
-
return commandResult('当前任务正在运行,请等待完成或先发送 /stop。');
|
|
297
|
+
return commandResult(t('当前任务正在运行,请等待完成或先发送 /stop。'));
|
|
297
298
|
}
|
|
298
299
|
|
|
299
300
|
const catalog = bound
|
|
@@ -305,9 +306,9 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
305
306
|
if (!selection) {
|
|
306
307
|
if (numberRequest) return commandResult(invalidModelNumberMessage(requested));
|
|
307
308
|
return commandResult([
|
|
308
|
-
|
|
309
|
+
t('没有找到模型:{model}', { model: safeDisplayText(requested) }),
|
|
309
310
|
'',
|
|
310
|
-
'请发送 /models 查看可用模型。',
|
|
311
|
+
t('请发送 /models 查看可用模型。'),
|
|
311
312
|
].join('\n'));
|
|
312
313
|
}
|
|
313
314
|
|
|
@@ -344,7 +345,10 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
344
345
|
throw stale;
|
|
345
346
|
}
|
|
346
347
|
}
|
|
347
|
-
return commandResult(
|
|
348
|
+
return commandResult(t(`模型已切换为:
|
|
349
|
+
{model}
|
|
350
|
+
|
|
351
|
+
后续消息将使用该模型。`, { model: modelId(selection.provider, selection.model) }));
|
|
348
352
|
});
|
|
349
353
|
} catch (error) {
|
|
350
354
|
return commandResult(modelErrorMessage(error, 'select'));
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
normalizeAgentPresetCatalog,
|
|
3
3
|
normalizeAgentPresetId,
|
|
4
4
|
} from './agent-preset.mjs';
|
|
5
|
+
import { t } from './i18n.mjs';
|
|
5
6
|
import { withSessionBindingLock } from './session-binding-lock.mjs';
|
|
6
7
|
import { splitWorkspaceCommandMessage } from './workspace-command.mjs';
|
|
7
8
|
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
@@ -59,7 +60,7 @@ function normalizeSettings(value) {
|
|
|
59
60
|
|
|
60
61
|
function presetItemText(item) {
|
|
61
62
|
const label = safeDisplayText(item.label) || item.id;
|
|
62
|
-
return
|
|
63
|
+
return t('{label}({id})', { label, id: item.id });
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
function itemFor(catalog, id) {
|
|
@@ -67,11 +68,11 @@ function itemFor(catalog, id) {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function defaultDescription(catalog) {
|
|
70
|
-
if (!catalog.defaultId) return '未设置或当前不可用';
|
|
71
|
+
if (!catalog.defaultId) return t('未设置或当前不可用');
|
|
71
72
|
const item = itemFor(catalog, catalog.defaultId);
|
|
72
73
|
return item
|
|
73
74
|
? presetItemText(item)
|
|
74
|
-
:
|
|
75
|
+
: t('{id}(当前不可用)', { id: catalog.defaultId });
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
function currentDescription(settings) {
|
|
@@ -79,63 +80,63 @@ function currentDescription(settings) {
|
|
|
79
80
|
if (agentPreset === null) {
|
|
80
81
|
const item = itemFor(catalog, catalog.defaultId);
|
|
81
82
|
return item
|
|
82
|
-
?
|
|
83
|
-
: '跟随 Host 默认(Host 默认当前不可用)';
|
|
83
|
+
? t('跟随 Host 默认:{preset}', { preset: presetItemText(item) })
|
|
84
|
+
: t('跟随 Host 默认(Host 默认当前不可用)');
|
|
84
85
|
}
|
|
85
86
|
const item = itemFor(catalog, agentPreset);
|
|
86
87
|
return item
|
|
87
88
|
? presetItemText(item)
|
|
88
|
-
:
|
|
89
|
+
: t('{id}(已不可用)', { id: agentPreset });
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
function formatCurrent(settings) {
|
|
92
93
|
return [
|
|
93
|
-
'当前机器人用于新会话的 Agent Preset:',
|
|
94
|
+
t('当前机器人用于新会话的 Agent Preset:'),
|
|
94
95
|
currentDescription(settings),
|
|
95
96
|
'',
|
|
96
|
-
'已有会话不会受此设置影响。',
|
|
97
|
-
'查看可用项:/presetlist',
|
|
98
|
-
'恢复跟随 Host 默认:/preset --default',
|
|
97
|
+
t('已有会话不会受此设置影响。'),
|
|
98
|
+
t('查看可用项:/presetlist'),
|
|
99
|
+
t('恢复跟随 Host 默认:/preset --default'),
|
|
99
100
|
].join('\n');
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
function formatList(settings) {
|
|
103
104
|
const { agentPreset, agentPresetCatalog: catalog } = settings;
|
|
104
105
|
const lines = [
|
|
105
|
-
'当前机器人用于新会话的 Agent Preset:',
|
|
106
|
+
t('当前机器人用于新会话的 Agent Preset:'),
|
|
106
107
|
currentDescription(settings),
|
|
107
108
|
'',
|
|
108
|
-
|
|
109
|
+
t('Host 默认:{preset}', { preset: defaultDescription(catalog) }),
|
|
109
110
|
'',
|
|
110
|
-
|
|
111
|
+
t('可用 Agent Preset({count}):', { count: catalog.items.length }),
|
|
111
112
|
];
|
|
112
113
|
if (catalog.items.length === 0) {
|
|
113
|
-
lines.push('当前没有可用 Agent Preset。');
|
|
114
|
+
lines.push(t('当前没有可用 Agent Preset。'));
|
|
114
115
|
} else {
|
|
115
116
|
catalog.items.forEach((item, index) => {
|
|
116
117
|
const markers = [];
|
|
117
|
-
if (item.id === catalog.defaultId) markers.push('Host 默认');
|
|
118
|
-
if (item.id === agentPreset) markers.push('当前选择');
|
|
119
|
-
if (agentPreset === null && item.id === catalog.defaultId) markers.push('当前生效');
|
|
118
|
+
if (item.id === catalog.defaultId) markers.push(t('Host 默认'));
|
|
119
|
+
if (item.id === agentPreset) markers.push(t('当前选择'));
|
|
120
|
+
if (agentPreset === null && item.id === catalog.defaultId) markers.push(t('当前生效'));
|
|
120
121
|
const annotation = markers.length > 0 ? `(${markers.join(',')})` : '';
|
|
121
122
|
lines.push(`${index + 1}. ${presetItemText(item)}${annotation}`);
|
|
122
123
|
});
|
|
123
124
|
}
|
|
124
125
|
lines.push(
|
|
125
126
|
'',
|
|
126
|
-
'选择:/preset <序号或 ID>',
|
|
127
|
-
'纯数字 ID:/preset id:<ID>',
|
|
128
|
-
'恢复跟随 Host 默认:/preset --default',
|
|
127
|
+
t('选择:/preset <序号或 ID>'),
|
|
128
|
+
t('纯数字 ID:/preset id:<ID>'),
|
|
129
|
+
t('恢复跟随 Host 默认:/preset --default'),
|
|
129
130
|
);
|
|
130
131
|
return lines.join('\n');
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
function formatUpdated(settings) {
|
|
134
135
|
return [
|
|
135
|
-
'当前机器人用于新会话的 Agent Preset 已设置为:',
|
|
136
|
+
t('当前机器人用于新会话的 Agent Preset 已设置为:'),
|
|
136
137
|
currentDescription(settings),
|
|
137
138
|
'',
|
|
138
|
-
'已有会话不变。若当前聊天已有会话,请先发送 /new,再发送普通消息,才会使用新设置创建会话。',
|
|
139
|
+
t('已有会话不变。若当前聊天已有会话,请先发送 /new,再发送普通消息,才会使用新设置创建会话。'),
|
|
139
140
|
].join('\n');
|
|
140
141
|
}
|
|
141
142
|
|
|
@@ -189,16 +190,16 @@ function presetFromSnapshot(state, key, requested) {
|
|
|
189
190
|
if (!/^\d+$/u.test(requested)) return { numeric: false, id: null };
|
|
190
191
|
const index = Number(requested);
|
|
191
192
|
if (!Number.isSafeInteger(index) || index < 1) {
|
|
192
|
-
return { numeric: true, error: 'Agent Preset 序号无效,请先执行 /presetlist。' };
|
|
193
|
+
return { numeric: true, error: t('Agent Preset 序号无效,请先执行 /presetlist。') };
|
|
193
194
|
}
|
|
194
195
|
const snapshot = loadSnapshot(state, key);
|
|
195
196
|
if (!snapshot) {
|
|
196
|
-
return { numeric: true, error: '请先执行 /presetlist,再按列表序号选择 Agent Preset。' };
|
|
197
|
+
return { numeric: true, error: t('请先执行 /presetlist,再按列表序号选择 Agent Preset。') };
|
|
197
198
|
}
|
|
198
199
|
const id = snapshot[index - 1];
|
|
199
200
|
return id
|
|
200
201
|
? { numeric: true, id }
|
|
201
|
-
: { numeric: true, error: 'Agent Preset 序号不存在,请重新执行 /presetlist。' };
|
|
202
|
+
: { numeric: true, error: t('Agent Preset 序号不存在,请重新执行 /presetlist。') };
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
function errorCode(error) {
|
|
@@ -208,22 +209,23 @@ function errorCode(error) {
|
|
|
208
209
|
function presetErrorMessage(error, action) {
|
|
209
210
|
const code = errorCode(error);
|
|
210
211
|
if (code === 'agent-preset-invalid') {
|
|
211
|
-
return `Agent Preset ID
|
|
212
|
+
return t(`Agent Preset ID 格式无效。
|
|
213
|
+
{usage}`, { usage: t(PRESET_USAGE) });
|
|
212
214
|
}
|
|
213
215
|
if (code === 'agent-preset-unavailable') {
|
|
214
|
-
return 'Agent Preset 不存在或当前不可用,请重新执行 /presetlist。';
|
|
216
|
+
return t('Agent Preset 不存在或当前不可用,请重新执行 /presetlist。');
|
|
215
217
|
}
|
|
216
218
|
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
|
|
217
|
-
return '工作区或机器人状态已发生变化,请重试。';
|
|
219
|
+
return t('工作区或机器人状态已发生变化,请重试。');
|
|
218
220
|
}
|
|
219
221
|
if (code === 'cancelled' || error?.name === 'AbortError') {
|
|
220
|
-
if (action === 'list') return '获取 Agent Preset 列表已取消。';
|
|
221
|
-
if (action === 'current') return '获取 Agent Preset 设置已取消。';
|
|
222
|
-
return 'Agent Preset 修改已取消。';
|
|
222
|
+
if (action === 'list') return t('获取 Agent Preset 列表已取消。');
|
|
223
|
+
if (action === 'current') return t('获取 Agent Preset 设置已取消。');
|
|
224
|
+
return t('Agent Preset 修改已取消。');
|
|
223
225
|
}
|
|
224
|
-
if (action === 'list') return '暂时无法获取 Agent Preset 列表,请稍后重试。';
|
|
225
|
-
if (action === 'current') return '暂时无法获取 Agent Preset 设置,请稍后重试。';
|
|
226
|
-
return 'Agent Preset 修改失败,请稍后重试。';
|
|
226
|
+
if (action === 'list') return t('暂时无法获取 Agent Preset 列表,请稍后重试。');
|
|
227
|
+
if (action === 'current') return t('暂时无法获取 Agent Preset 设置,请稍后重试。');
|
|
228
|
+
return t('Agent Preset 修改失败,请稍后重试。');
|
|
227
229
|
}
|
|
228
230
|
|
|
229
231
|
async function settings(harness, options) {
|
|
@@ -250,12 +252,12 @@ export async function runPresetCommand(text, harness, state, key, options = {})
|
|
|
250
252
|
if (!isPresetCommand(text)) return null;
|
|
251
253
|
const command = text.trim();
|
|
252
254
|
if (options.hasImages) {
|
|
253
|
-
return commandResult('Agent Preset 命令仅支持纯文字,请移除图片后重试。');
|
|
255
|
+
return commandResult(t('Agent Preset 命令仅支持纯文字,请移除图片后重试。'));
|
|
254
256
|
}
|
|
255
257
|
const requestOptions = rpcOptions(options.signal);
|
|
256
258
|
|
|
257
259
|
if (PRESET_LIST_COMMAND.test(command)) {
|
|
258
|
-
if (!/^\/presetlist[ \t]*$/iu.test(command)) return commandResult(PRESET_LIST_USAGE);
|
|
260
|
+
if (!/^\/presetlist[ \t]*$/iu.test(command)) return commandResult(t(PRESET_LIST_USAGE));
|
|
259
261
|
try {
|
|
260
262
|
const current = await settings(harness, requestOptions);
|
|
261
263
|
saveSnapshot(state, key, current.agentPresetCatalog.items);
|
|
@@ -266,7 +268,7 @@ export async function runPresetCommand(text, harness, state, key, options = {})
|
|
|
266
268
|
}
|
|
267
269
|
|
|
268
270
|
const match = /^\/preset(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(command);
|
|
269
|
-
if (!match) return commandResult(PRESET_USAGE);
|
|
271
|
+
if (!match) return commandResult(t(PRESET_USAGE));
|
|
270
272
|
const requested = match[1];
|
|
271
273
|
if (!requested) {
|
|
272
274
|
try {
|
|
@@ -290,7 +292,8 @@ export async function runPresetCommand(text, harness, state, key, options = {})
|
|
|
290
292
|
selected = fromSnapshot.id;
|
|
291
293
|
} else {
|
|
292
294
|
selected = normalizeAgentPresetId(requested);
|
|
293
|
-
if (!selected) return commandResult(`Agent Preset ID
|
|
295
|
+
if (!selected) return commandResult(t(`Agent Preset ID 格式无效。
|
|
296
|
+
{usage}`, { usage: t(PRESET_USAGE) }));
|
|
294
297
|
}
|
|
295
298
|
}
|
|
296
299
|
}
|
|
@@ -76,7 +76,7 @@ export async function deliverOutboundArtifacts({
|
|
|
76
76
|
logger,
|
|
77
77
|
}) {
|
|
78
78
|
const receipts = baseReceipt ? [baseReceipt] : [];
|
|
79
|
-
let userVisible = Boolean(baseReceipt);
|
|
79
|
+
let userVisible = Boolean(baseReceipt) && baseReceipt.deliveryOutcome !== 'failed';
|
|
80
80
|
let failureNoticeVisible = false;
|
|
81
81
|
let artifactsSent = 0;
|
|
82
82
|
let artifactSendErrors = 0;
|