@xmanrui/dsh-im 1.4.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 +18 -6
- package/README.md +18 -6
- package/lib/index.js +206 -176
- 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 +105 -82
- package/src/channels/dingtalk/dingtalk-bridge.mjs +79 -110
- 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 +169 -180
- package/src/channels/feishu/feishu-cards.mjs +62 -54
- package/src/channels/feishu/feishu-channel.mjs +57 -21
- 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 +149 -130
- 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 +170 -0
- package/src/channels/shared/semantic/artifact.mjs +2 -2
- package/src/channels/shared/semantic/delivery.mjs +40 -0
- package/src/channels/shared/text-harness-bridge.mjs +154 -136
- 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 +111 -19
- package/src/channels/telegram/telegram-rich-message.mjs +147 -0
- package/src/channels/telegram/telegram-runtime.mjs +383 -5
- package/src/channels/wecom/config-store.mjs +3 -1
- package/src/channels/wecom/wecom-bridge.mjs +154 -153
- 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-api.mjs +140 -107
- package/src/channels/weixin/weixin-bridge.mjs +86 -123
- 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 +47 -13
- package/src/channels/whatsapp/whatsapp-web-session.mjs +3 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { realpath, stat } from 'node:fs/promises';
|
|
2
2
|
import { isAbsolute, resolve } from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { t } from './i18n.mjs';
|
|
4
5
|
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
5
6
|
|
|
6
7
|
const WORKSPACE_COMMAND = /^\/workspace(?:\s+([\s\S]+))?$/i;
|
|
@@ -60,28 +61,34 @@ async function existingWorkspacePaths(values) {
|
|
|
60
61
|
|
|
61
62
|
async function selectedWorkspacePath(value) {
|
|
62
63
|
if (typeof value !== 'string' || !isAbsolute(value.trim())) {
|
|
63
|
-
return { error:
|
|
64
|
+
return { error: t(`工作区必须是绝对路径。
|
|
65
|
+
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
|
64
66
|
}
|
|
65
67
|
const workspace = normalizedWorkspacePath(value.trim());
|
|
66
68
|
if (!workspace) {
|
|
67
|
-
return { error:
|
|
69
|
+
return { error: t(`工作区路径包含不支持的字符或长度超过限制。
|
|
70
|
+
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
|
68
71
|
}
|
|
69
72
|
let info;
|
|
70
73
|
try {
|
|
71
74
|
info = await stat(workspace);
|
|
72
75
|
} catch {
|
|
73
|
-
return { error:
|
|
76
|
+
return { error: t(`工作区路径不存在。
|
|
77
|
+
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
|
74
78
|
}
|
|
75
79
|
if (!info.isDirectory()) {
|
|
76
|
-
return { error:
|
|
80
|
+
return { error: t(`工作区路径必须指向一个目录。
|
|
81
|
+
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
|
77
82
|
}
|
|
78
83
|
try {
|
|
79
84
|
const canonical = normalizedWorkspacePath(await realpath(workspace));
|
|
80
85
|
return canonical
|
|
81
86
|
? { workspace: canonical }
|
|
82
|
-
: { error:
|
|
87
|
+
: { error: t(`工作区路径包含不支持的字符或长度超过限制。
|
|
88
|
+
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
|
83
89
|
} catch {
|
|
84
|
-
return { error:
|
|
90
|
+
return { error: t(`工作区路径不存在。
|
|
91
|
+
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
|
85
92
|
}
|
|
86
93
|
}
|
|
87
94
|
|
|
@@ -120,38 +127,38 @@ export function splitWorkspaceCommandMessage(message) {
|
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
async function runWorkspaceListCommand(match, harness) {
|
|
123
|
-
if (match[1]?.trim()) return commandResult('用法:/workspacelist');
|
|
130
|
+
if (match[1]?.trim()) return commandResult(t('用法:/workspacelist'));
|
|
124
131
|
if (typeof harness?.listWorkspaces !== 'function') {
|
|
125
|
-
return commandResult('当前机器人暂不支持列出工作区。');
|
|
132
|
+
return commandResult(t('当前机器人暂不支持列出工作区。'));
|
|
126
133
|
}
|
|
127
134
|
try {
|
|
128
135
|
const { current, paths } = await workspacePathSnapshot(harness);
|
|
129
136
|
if (paths.length === 0) {
|
|
130
|
-
return commandResult('当前 Harness Host 上没有仍然存在的已登记工作区。');
|
|
137
|
+
return commandResult(t('当前 Harness Host 上没有仍然存在的已登记工作区。'));
|
|
131
138
|
}
|
|
132
139
|
const lines = [
|
|
133
|
-
|
|
140
|
+
t('当前 Harness Host 上存在的工作区({count}):', { count: paths.length }),
|
|
134
141
|
...paths.map((workspace, index) => (
|
|
135
|
-
`${index + 1}. ${workspace}${workspace === current ? '(当前)' : ''}`
|
|
142
|
+
`${index + 1}. ${workspace}${workspace === current ? t('(当前)') : ''}`
|
|
136
143
|
)),
|
|
137
144
|
'',
|
|
138
|
-
'切换用法:/workspace 工作区绝对路径',
|
|
139
|
-
'查看会话:/sessionlist 工作区序号或绝对路径',
|
|
145
|
+
t('切换用法:/workspace 工作区绝对路径'),
|
|
146
|
+
t('查看会话:/sessionlist 工作区序号或绝对路径'),
|
|
140
147
|
];
|
|
141
148
|
const message = lines.join('\n');
|
|
142
149
|
return commandResult(message, splitWorkspaceCommandMessage(message));
|
|
143
150
|
} catch (error) {
|
|
144
151
|
if (error?.code === 'workspace-bot-not-found') {
|
|
145
|
-
return commandResult('机器人正在移除或已重新接入,无法列出原会话的工作区。');
|
|
152
|
+
return commandResult(t('机器人正在移除或已重新接入,无法列出原会话的工作区。'));
|
|
146
153
|
}
|
|
147
|
-
return commandResult('暂时无法获取工作区列表,请稍后重试。');
|
|
154
|
+
return commandResult(t('暂时无法获取工作区列表,请稍后重试。'));
|
|
148
155
|
}
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
export async function resolveSessionListWorkspace(selector, harness) {
|
|
152
159
|
if (!selector) {
|
|
153
160
|
if (typeof harness?.currentWorkspace !== 'function') {
|
|
154
|
-
return { error: '当前机器人没有可用的工作区。' };
|
|
161
|
+
return { error: t('当前机器人没有可用的工作区。') };
|
|
155
162
|
}
|
|
156
163
|
const selected = await selectedWorkspacePath(harness.currentWorkspace());
|
|
157
164
|
harness.assertWorkspaceScope?.();
|
|
@@ -160,12 +167,12 @@ export async function resolveSessionListWorkspace(selector, harness) {
|
|
|
160
167
|
|
|
161
168
|
if (/^\d+$/u.test(selector)) {
|
|
162
169
|
if (typeof harness?.listWorkspaces !== 'function') {
|
|
163
|
-
return { error: '当前机器人暂不支持按序号选择工作区。' };
|
|
170
|
+
return { error: t('当前机器人暂不支持按序号选择工作区。') };
|
|
164
171
|
}
|
|
165
172
|
const { paths } = await workspacePathSnapshot(harness);
|
|
166
173
|
const position = Number(selector);
|
|
167
174
|
if (!Number.isSafeInteger(position) || position < 1 || position > paths.length) {
|
|
168
|
-
return { error: '工作区序号不存在,请先执行 /workspacelist。' };
|
|
175
|
+
return { error: t('工作区序号不存在,请先执行 /workspacelist。') };
|
|
169
176
|
}
|
|
170
177
|
return { workspace: paths[position - 1] };
|
|
171
178
|
}
|
|
@@ -185,13 +192,13 @@ function formatSessionRelativeTime(value) {
|
|
|
185
192
|
const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
186
193
|
const now = new Date();
|
|
187
194
|
const dayDiff = Math.round((startOfDay(now) - startOfDay(date)) / 86_400_000);
|
|
188
|
-
if (dayDiff === 0) return
|
|
189
|
-
if (dayDiff === 1) return
|
|
190
|
-
if (dayDiff === 2) return
|
|
195
|
+
if (dayDiff === 0) return t('今天 {time}', { time: hm });
|
|
196
|
+
if (dayDiff === 1) return t('昨天 {time}', { time: hm });
|
|
197
|
+
if (dayDiff === 2) return t('前天 {time}', { time: hm });
|
|
191
198
|
if (date.getFullYear() === now.getFullYear()) {
|
|
192
|
-
return
|
|
199
|
+
return t('{month}月{day}日 {time}', { month: date.getMonth() + 1, day: date.getDate(), time: hm });
|
|
193
200
|
}
|
|
194
|
-
return
|
|
201
|
+
return t('{year}年{month}月{day}日', { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() });
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
function sessionListMessage(workspace, sessions, { currentWorkspace = false } = {}) {
|
|
@@ -199,22 +206,24 @@ function sessionListMessage(workspace, sessions, { currentWorkspace = false } =
|
|
|
199
206
|
const sessionId = safeDisplayText(session?.sessionId);
|
|
200
207
|
if (!sessionId) throw new TypeError('Harness returned an invalid session id');
|
|
201
208
|
const title = session?.summaryAvailable === false
|
|
202
|
-
? '标题暂不可用'
|
|
203
|
-
: safeDisplayText(session?.title) || '暂无标题';
|
|
209
|
+
? t('标题暂不可用')
|
|
210
|
+
: safeDisplayText(session?.title) || t('暂无标题');
|
|
204
211
|
const timeText = formatSessionRelativeTime(session?.time);
|
|
205
|
-
const annotation = `${timeText ? ` · ${timeText}` : ''}${session?.archived === true ? '(已归档)' : ''}`;
|
|
212
|
+
const annotation = `${timeText ? ` · ${timeText}` : ''}${session?.archived === true ? t('(已归档)') : ''}`;
|
|
206
213
|
return `${title}${annotation}\n ID: ${sessionId}`;
|
|
207
214
|
});
|
|
208
|
-
if (rows.length === 0) return
|
|
215
|
+
if (rows.length === 0) return t(`工作区:{workspace}
|
|
216
|
+
该工作区暂无会话。`, { workspace });
|
|
209
217
|
return [
|
|
210
|
-
|
|
211
|
-
|
|
218
|
+
t('工作区:{workspace}', { workspace }),
|
|
219
|
+
t('会话({count}):', { count: rows.length }),
|
|
212
220
|
'',
|
|
213
221
|
...rows.map((row, index) => `${index + 1}. ${row}`),
|
|
214
222
|
'',
|
|
215
223
|
currentWorkspace
|
|
216
|
-
? '绑定用法:/session Session ID 或当前工作区序号(/session N)'
|
|
217
|
-
:
|
|
224
|
+
? t('绑定用法:/session Session ID 或当前工作区序号(/session N)')
|
|
225
|
+
: t(`绑定用法:/session Session ID
|
|
226
|
+
提示:/session N 只按机器人当前工作区的序号绑定。`),
|
|
218
227
|
].join('\n');
|
|
219
228
|
}
|
|
220
229
|
|
|
@@ -227,7 +236,7 @@ async function currentSessionListWorkspace(harness) {
|
|
|
227
236
|
|
|
228
237
|
async function runSessionListCommand(match, harness) {
|
|
229
238
|
if (typeof harness?.listWorkspaceSessions !== 'function') {
|
|
230
|
-
return commandResult('当前机器人暂不支持列出工作区会话。');
|
|
239
|
+
return commandResult(t('当前机器人暂不支持列出工作区会话。'));
|
|
231
240
|
}
|
|
232
241
|
const selector = match[1]?.trim() ?? '';
|
|
233
242
|
try {
|
|
@@ -246,36 +255,37 @@ async function runSessionListCommand(match, harness) {
|
|
|
246
255
|
return commandResult(message, splitWorkspaceCommandMessage(message));
|
|
247
256
|
} catch (error) {
|
|
248
257
|
if (error?.code === 'workspace-bot-not-found') {
|
|
249
|
-
return commandResult('机器人正在移除或已重新接入,无法列出原会话的工作区会话。');
|
|
258
|
+
return commandResult(t('机器人正在移除或已重新接入,无法列出原会话的工作区会话。'));
|
|
250
259
|
}
|
|
251
|
-
return commandResult('暂时无法获取工作区会话列表,请稍后重试。');
|
|
260
|
+
return commandResult(t('暂时无法获取工作区会话列表,请稍后重试。'));
|
|
252
261
|
}
|
|
253
262
|
}
|
|
254
263
|
|
|
255
264
|
function sessionBindErrorMessage(error) {
|
|
256
265
|
if (error?.code === 'session-id-invalid') {
|
|
257
|
-
return `Session ID
|
|
266
|
+
return t(`Session ID 格式无效。
|
|
267
|
+
{usage}`, { usage: t(SESSION_BIND_USAGE) });
|
|
258
268
|
}
|
|
259
269
|
if (['session-not-registered', 'session-not-found'].includes(error?.code)) {
|
|
260
|
-
return '未找到该会话,请先执行 /sessionlist 确认 Session ID。';
|
|
270
|
+
return t('未找到该会话,请先执行 /sessionlist 确认 Session ID。');
|
|
261
271
|
}
|
|
262
272
|
if (error?.code === 'session-subagent-unsupported') {
|
|
263
|
-
return '子代理会话不能绑定到机器人对话,请选择普通会话。';
|
|
273
|
+
return t('子代理会话不能绑定到机器人对话,请选择普通会话。');
|
|
264
274
|
}
|
|
265
275
|
if (error?.code === 'session-workspace-ambiguous') {
|
|
266
|
-
return '该会话的工作区归属不明确,暂时无法绑定。';
|
|
276
|
+
return t('该会话的工作区归属不明确,暂时无法绑定。');
|
|
267
277
|
}
|
|
268
278
|
if (error?.code === 'session-summary-unavailable') {
|
|
269
|
-
return '暂时无法读取该会话的信息,请稍后重试。';
|
|
279
|
+
return t('暂时无法读取该会话的信息,请稍后重试。');
|
|
270
280
|
}
|
|
271
281
|
if (error?.code === 'workspace-bot-not-found') {
|
|
272
|
-
return '机器人正在移除或已重新接入,无法绑定原对话的会话。';
|
|
282
|
+
return t('机器人正在移除或已重新接入,无法绑定原对话的会话。');
|
|
273
283
|
}
|
|
274
284
|
if ([WORKSPACE_SESSION_STALE, 'agent-busy', 'session-conflict', 'workspace-conflict']
|
|
275
285
|
.includes(error?.code)) {
|
|
276
|
-
return '工作区或会话状态已发生变化,请重试。';
|
|
286
|
+
return t('工作区或会话状态已发生变化,请重试。');
|
|
277
287
|
}
|
|
278
|
-
return '暂时无法绑定会话,请稍后重试。';
|
|
288
|
+
return t('暂时无法绑定会话,请稍后重试。');
|
|
279
289
|
}
|
|
280
290
|
|
|
281
291
|
async function runSessionBindCommand(command, harness, conversationKey) {
|
|
@@ -285,7 +295,7 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|
|
285
295
|
// 序号模式:把 /session N 解析成当前工作区会话列表中的第 N 个会话
|
|
286
296
|
if (typeof harness?.listWorkspaceSessions !== 'function'
|
|
287
297
|
|| typeof harness?.currentWorkspace !== 'function') {
|
|
288
|
-
return commandResult('当前机器人暂不支持按序号绑定,请使用 /session Session ID。');
|
|
298
|
+
return commandResult(t('当前机器人暂不支持按序号绑定,请使用 /session Session ID。'));
|
|
289
299
|
}
|
|
290
300
|
try {
|
|
291
301
|
const selected = await selectedWorkspacePath(harness.currentWorkspace());
|
|
@@ -298,7 +308,7 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|
|
298
308
|
const position = Number(sessionId);
|
|
299
309
|
if (!Number.isSafeInteger(position) || position < 1
|
|
300
310
|
|| position > listed.sessions.length) {
|
|
301
|
-
return commandResult('会话序号不存在,请先执行 /sessionlist 查看序号。');
|
|
311
|
+
return commandResult(t('会话序号不存在,请先执行 /sessionlist 查看序号。'));
|
|
302
312
|
}
|
|
303
313
|
const selectedSessionId = listed.sessions[position - 1]?.sessionId;
|
|
304
314
|
if (!validSessionId(selectedSessionId)) {
|
|
@@ -309,15 +319,15 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|
|
309
319
|
if (error?.code === 'workspace-bot-not-found') {
|
|
310
320
|
return commandResult(sessionBindErrorMessage(error));
|
|
311
321
|
}
|
|
312
|
-
return commandResult('暂时无法获取会话列表,请稍后重试。');
|
|
322
|
+
return commandResult(t('暂时无法获取会话列表,请稍后重试。'));
|
|
313
323
|
}
|
|
314
324
|
}
|
|
315
|
-
if (!validSessionId(sessionId)) return commandResult(SESSION_BIND_USAGE);
|
|
325
|
+
if (!validSessionId(sessionId)) return commandResult(t(SESSION_BIND_USAGE));
|
|
316
326
|
if (typeof harness?.bindWorkspaceSession !== 'function') {
|
|
317
|
-
return commandResult('当前机器人暂不支持绑定已有会话。');
|
|
327
|
+
return commandResult(t('当前机器人暂不支持绑定已有会话。'));
|
|
318
328
|
}
|
|
319
329
|
if (typeof conversationKey !== 'string' || !conversationKey) {
|
|
320
|
-
return commandResult('当前消息缺少可绑定的会话上下文。');
|
|
330
|
+
return commandResult(t('当前消息缺少可绑定的会话上下文。'));
|
|
321
331
|
}
|
|
322
332
|
try {
|
|
323
333
|
const bound = await harness.bindWorkspaceSession(conversationKey, sessionId);
|
|
@@ -327,13 +337,13 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|
|
327
337
|
if (!workspace || !boundSessionId) {
|
|
328
338
|
throw new TypeError('Harness returned an invalid bound session');
|
|
329
339
|
}
|
|
330
|
-
const title = safeDisplayText(bound?.title) || '暂无标题';
|
|
340
|
+
const title = safeDisplayText(bound?.title) || t('暂无标题');
|
|
331
341
|
const message = [
|
|
332
|
-
'当前聊天已绑定会话:',
|
|
333
|
-
|
|
334
|
-
|
|
342
|
+
t('当前聊天已绑定会话:'),
|
|
343
|
+
t('工作区:{workspace}', { workspace }),
|
|
344
|
+
t('标题:{title}', { title }),
|
|
335
345
|
`ID:${boundSessionId}`,
|
|
336
|
-
|
|
346
|
+
t('归档:{archived}', { archived: bound?.archived === true ? t('是') : t('否') }),
|
|
337
347
|
].join('\n');
|
|
338
348
|
return commandResult(message, splitWorkspaceCommandMessage(message));
|
|
339
349
|
} catch (error) {
|
|
@@ -355,20 +365,21 @@ export async function runWorkspaceCommand(text, harness, conversationKey) {
|
|
|
355
365
|
if (!match) return null;
|
|
356
366
|
const workspace = match[1]?.trim();
|
|
357
367
|
if (!workspace) {
|
|
358
|
-
return commandResult('用法:/workspace 工作区绝对路径');
|
|
368
|
+
return commandResult(t('用法:/workspace 工作区绝对路径'));
|
|
359
369
|
}
|
|
360
370
|
if (typeof harness?.switchWorkspace !== 'function') {
|
|
361
|
-
return commandResult('当前机器人暂不支持切换工作区。');
|
|
371
|
+
return commandResult(t('当前机器人暂不支持切换工作区。'));
|
|
362
372
|
}
|
|
363
373
|
try {
|
|
364
374
|
const current = await harness.switchWorkspace(workspace);
|
|
365
|
-
return commandResult(
|
|
375
|
+
return commandResult(t('工作区已切换为:{workspace}', { workspace: current }));
|
|
366
376
|
} catch (error) {
|
|
367
377
|
if (['workspace-not-absolute', 'workspace-not-found', 'workspace-not-directory'].includes(error?.code)) {
|
|
368
|
-
return commandResult(
|
|
378
|
+
return commandResult(t(`{message}
|
|
379
|
+
用法:/workspace 工作区绝对路径`, { message: error.message }));
|
|
369
380
|
}
|
|
370
381
|
if (error?.code === 'workspace-bot-not-found') {
|
|
371
|
-
return commandResult('机器人正在移除或已重新接入,无法切换原会话的工作区。');
|
|
382
|
+
return commandResult(t('机器人正在移除或已重新接入,无法切换原会话的工作区。'));
|
|
372
383
|
}
|
|
373
384
|
throw error;
|
|
374
385
|
}
|
|
@@ -2,6 +2,8 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
|
|
5
|
+
import { t } from '../shared/i18n.mjs';
|
|
6
|
+
|
|
5
7
|
const EMPTY_DOCUMENT = Object.freeze({ version: 1, bots: Object.freeze([]) });
|
|
6
8
|
const BOT_ID_PATTERN = /^slack_[a-f0-9]{24}$/;
|
|
7
9
|
const BOT_TOKEN_REF_PATTERN = /^DSH_SLACK_BOT_TOKEN_[A-F0-9]{24}$/;
|
|
@@ -27,7 +29,7 @@ export function maskSlackBotId(platformId) {
|
|
|
27
29
|
const value = cleanString(platformId) ?? '';
|
|
28
30
|
const [teamId, userId] = value.split(':');
|
|
29
31
|
if (teamId && userId) return `${teamId.slice(0, 5)}••• · ${userId.slice(0, 5)}•••`;
|
|
30
|
-
return value ? `${value.slice(0, 6)}••••` : 'Slack机器人';
|
|
32
|
+
return value ? `${value.slice(0, 6)}••••` : t('Slack机器人');
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export class SlackConfigStore {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchFileStream } from '../shared/file-download.mjs';
|
|
2
2
|
import { fetchImageBuffer, ImagePromptError } from '../shared/image-prompt.mjs';
|
|
3
|
+
import { t } from '../shared/i18n.mjs';
|
|
3
4
|
|
|
4
5
|
const DEFAULT_BASE_URL = 'https://slack.com/api/';
|
|
5
6
|
const SLACK_FILE_HOST = 'files.slack.com';
|
|
@@ -430,7 +431,7 @@ export class SlackApi {
|
|
|
430
431
|
throw new ImagePromptError(
|
|
431
432
|
'slack-file-access-required',
|
|
432
433
|
'Slack redirected a private file request to the workspace because file access was not granted',
|
|
433
|
-
'Slack 未授权机器人读取该文件。请为应用添加 files:read 后重新安装,再重新发送图片。',
|
|
434
|
+
t('Slack 未授权机器人读取该文件。请为应用添加 files:read 后重新安装,再重新发送图片。'),
|
|
434
435
|
);
|
|
435
436
|
}
|
|
436
437
|
return response;
|
|
@@ -505,19 +506,19 @@ export class SlackApi {
|
|
|
505
506
|
|
|
506
507
|
export async function inspectSlackCredentials({ botToken, appToken }, options = {}) {
|
|
507
508
|
if (!validSlackBotToken(botToken)) {
|
|
508
|
-
const error = new TypeError('Slack Bot Token 必须以 xoxb- 开头。');
|
|
509
|
+
const error = new TypeError(t('Slack Bot Token 必须以 xoxb- 开头。'));
|
|
509
510
|
error.code = 'slack-invalid-bot-token';
|
|
510
511
|
throw error;
|
|
511
512
|
}
|
|
512
513
|
if (!validSlackAppToken(appToken)) {
|
|
513
|
-
const error = new TypeError('Slack App Token 必须以 xapp- 开头。');
|
|
514
|
+
const error = new TypeError(t('Slack App Token 必须以 xapp- 开头。'));
|
|
514
515
|
error.code = 'slack-invalid-app-token';
|
|
515
516
|
throw error;
|
|
516
517
|
}
|
|
517
518
|
const api = new SlackApi({ botToken, appToken, ...options });
|
|
518
519
|
const [identity, connection] = await Promise.all([api.authTest(), api.openConnection()]);
|
|
519
520
|
if (!identity?.team_id || !identity?.user_id || !identity?.bot_id) {
|
|
520
|
-
throw new Error('Slack Bot Token 没有返回完整的机器人身份。');
|
|
521
|
+
throw new Error(t('Slack Bot Token 没有返回完整的机器人身份。'));
|
|
521
522
|
}
|
|
522
523
|
let socketUrl;
|
|
523
524
|
try {
|
|
@@ -526,7 +527,7 @@ export async function inspectSlackCredentials({ botToken, appToken }, options =
|
|
|
526
527
|
socketUrl = null;
|
|
527
528
|
}
|
|
528
529
|
if (!socketUrl || socketUrl.protocol !== 'wss:') {
|
|
529
|
-
const error = new Error('Slack App Token 无法创建 Socket Mode 连接,请确认已启用 Socket Mode 和 connections:write。');
|
|
530
|
+
const error = new Error(t('Slack App Token 无法创建 Socket Mode 连接,请确认已启用 Socket Mode 和 connections:write。'));
|
|
530
531
|
error.code = 'slack-socket-mode';
|
|
531
532
|
throw error;
|
|
532
533
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
2
|
+
import { t } from '../shared/i18n.mjs';
|
|
2
3
|
import { deriveSlackBotIdentity, maskSlackBotId } from './config-store.mjs';
|
|
3
4
|
import { inspectSlackCredentials } from './slack-api.mjs';
|
|
4
5
|
import { SLACK_DESCRIPTOR } from './slack-bridge.mjs';
|
|
@@ -60,7 +61,7 @@ export class SlackController {
|
|
|
60
61
|
if (!resolved) {
|
|
61
62
|
this.#errors.set(config.botId, safeError(
|
|
62
63
|
'missing-token',
|
|
63
|
-
'Slack机器人凭据缺失,请移除后重新接入。',
|
|
64
|
+
t('Slack机器人凭据缺失,请移除后重新接入。'),
|
|
64
65
|
));
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
@@ -70,7 +71,7 @@ export class SlackController {
|
|
|
70
71
|
} catch (error) {
|
|
71
72
|
this.#errors.set(config.botId, safeError(
|
|
72
73
|
'connection-failed',
|
|
73
|
-
'Slack Socket Mode 连接未就绪,插件会自动重试。',
|
|
74
|
+
t('Slack Socket Mode 连接未就绪,插件会自动重试。'),
|
|
74
75
|
));
|
|
75
76
|
this.#logger.warn?.(
|
|
76
77
|
`[dsh-im:slack] bot ${config.botId} failed to initialize:`,
|
|
@@ -135,7 +136,7 @@ export class SlackController {
|
|
|
135
136
|
} catch (error) {
|
|
136
137
|
this.#errors.set(identity.botId, safeError(
|
|
137
138
|
'connection-failed',
|
|
138
|
-
'Slack机器人已接入,Socket Mode 连接暂未就绪。',
|
|
139
|
+
t('Slack机器人已接入,Socket Mode 连接暂未就绪。'),
|
|
139
140
|
));
|
|
140
141
|
this.#logger.warn?.(
|
|
141
142
|
`[dsh-im:slack] bot ${identity.botId} credential connection failed:`,
|
|
@@ -159,7 +160,7 @@ export class SlackController {
|
|
|
159
160
|
} catch (error) {
|
|
160
161
|
this.#errors.set(botId, safeError(
|
|
161
162
|
'connection-failed',
|
|
162
|
-
'Slack Socket Mode 连接仍未就绪,请检查两个 Token。',
|
|
163
|
+
t('Slack Socket Mode 连接仍未就绪,请检查两个 Token。'),
|
|
163
164
|
));
|
|
164
165
|
throw error;
|
|
165
166
|
} finally {
|
|
@@ -175,13 +176,13 @@ export class SlackController {
|
|
|
175
176
|
return this.#withBotTransition(botId, async () => {
|
|
176
177
|
const runtime = this.#runtimes.get(botId);
|
|
177
178
|
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
178
|
-
const error = new Error('Slack机器人尚未连接');
|
|
179
|
+
const error = new Error(t('Slack机器人尚未连接'));
|
|
179
180
|
error.code = 'test-target-unavailable';
|
|
180
181
|
throw error;
|
|
181
182
|
}
|
|
182
183
|
await runtime.sendConnectionTest(connectionTestMessage(
|
|
183
184
|
`${config.name}(${maskSlackBotId(config.platformId)})`,
|
|
184
|
-
'Slack机器人',
|
|
185
|
+
t('Slack机器人'),
|
|
185
186
|
));
|
|
186
187
|
return { sent: true };
|
|
187
188
|
});
|
|
@@ -243,9 +244,9 @@ export class SlackController {
|
|
|
243
244
|
},
|
|
244
245
|
health: {
|
|
245
246
|
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
|
|
246
|
-
summary: connected ? `Slack${SLACK_DESCRIPTOR.connectionLabel}运行正常`
|
|
247
|
-
: state === 'error' ? 'Slack连接未就绪,插件会自动重试'
|
|
248
|
-
: 'Slack连接当前离线',
|
|
247
|
+
summary: connected ? t(`Slack${SLACK_DESCRIPTOR.connectionLabel}运行正常`)
|
|
248
|
+
: state === 'error' ? t('Slack连接未就绪,插件会自动重试')
|
|
249
|
+
: t('Slack连接当前离线'),
|
|
249
250
|
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
|
|
250
251
|
lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
|
|
251
252
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { splitMessageText } from '../shared/editable-message-stream.mjs';
|
|
2
|
+
import { t } from '../shared/i18n.mjs';
|
|
2
3
|
import { SlackApi } from './slack-api.mjs';
|
|
3
4
|
import { createSlackBridgeStatus, SlackHarnessBridge } from './slack-bridge.mjs';
|
|
4
5
|
|
|
@@ -135,8 +136,18 @@ export function normalizeSlackEvent(payload, botUserId, {
|
|
|
135
136
|
};
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
function
|
|
139
|
-
|
|
139
|
+
export function isSlackToolProgress(text) {
|
|
140
|
+
if (typeof text !== 'string') return false;
|
|
141
|
+
const marker = '__DSH_IM_TOOL_NAME__';
|
|
142
|
+
const template = t('正在使用{name}…', { name: marker });
|
|
143
|
+
const markerIndex = template.indexOf(marker);
|
|
144
|
+
if (markerIndex < 0) return false;
|
|
145
|
+
const prefix = template.slice(0, markerIndex);
|
|
146
|
+
const suffix = template.slice(markerIndex + marker.length);
|
|
147
|
+
const source = text.trim();
|
|
148
|
+
return source.startsWith(prefix)
|
|
149
|
+
&& source.endsWith(suffix)
|
|
150
|
+
&& source.length > prefix.length + suffix.length;
|
|
140
151
|
}
|
|
141
152
|
|
|
142
153
|
async function appendInChunks(api, target, ts, text, signal) {
|
|
@@ -204,7 +215,8 @@ async function createSlackMessageStream({ api, target, signal, logger }) {
|
|
|
204
215
|
return [...providerMessageIds];
|
|
205
216
|
},
|
|
206
217
|
update(text) {
|
|
207
|
-
if (closed || broken || typeof text !== 'string' || !text.trim()
|
|
218
|
+
if (closed || broken || typeof text !== 'string' || !text.trim()
|
|
219
|
+
|| isSlackToolProgress(text)) return;
|
|
208
220
|
pending = text;
|
|
209
221
|
schedule();
|
|
210
222
|
},
|
|
@@ -217,7 +229,7 @@ async function createSlackMessageStream({ api, target, signal, logger }) {
|
|
|
217
229
|
await inFlight?.catch(() => undefined);
|
|
218
230
|
|
|
219
231
|
const chunks = splitMessageText(text, SLACK_MESSAGE_LIMIT);
|
|
220
|
-
const first = chunks[0] ?? '处理完成。';
|
|
232
|
+
const first = chunks[0] ?? t('处理完成。');
|
|
221
233
|
if (!broken && first.startsWith(appended)) {
|
|
222
234
|
await appendInChunks(api, target, ts, first.slice(appended.length), signal);
|
|
223
235
|
await api.stopStream({ channelId: target.channelId, ts, signal });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t } from '../shared/i18n.mjs';
|
|
1
2
|
import {
|
|
2
3
|
deriveTokenBotIdentity,
|
|
3
4
|
maskPlatformId,
|
|
@@ -66,7 +67,7 @@ export function deriveTelegramBotIdentity(platformId) {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export function maskTelegramBotId(platformId) {
|
|
69
|
-
return maskPlatformId(platformId, 'Telegram机器人');
|
|
70
|
+
return maskPlatformId(platformId, t('Telegram机器人'));
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export class TelegramConfigStore extends TokenBotConfigStore {
|