@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,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
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
materializeOutboundArtifact,
|
|
3
|
+
releaseOutboundArtifact,
|
|
4
|
+
} from './artifact.mjs';
|
|
5
|
+
import {
|
|
6
|
+
createArtifactFailureReceipt,
|
|
7
|
+
createDeliveryReceipt,
|
|
8
|
+
mergeDeliveryReceipts,
|
|
9
|
+
providerMessageIdsFor,
|
|
10
|
+
} from './delivery.mjs';
|
|
11
|
+
|
|
12
|
+
function unavailableError() {
|
|
13
|
+
const error = new Error('Native file delivery is unavailable');
|
|
14
|
+
error.code = 'artifact-provider-unavailable';
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isAbort(error, signal) {
|
|
19
|
+
return signal?.aborted
|
|
20
|
+
|| error?.name === 'AbortError'
|
|
21
|
+
|| error?.code === 'ABORT_ERR';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function providerIds(value) {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return [...new Set(value
|
|
27
|
+
.filter((candidate) => (
|
|
28
|
+
(typeof candidate === 'string' && candidate.trim())
|
|
29
|
+
|| Number.isSafeInteger(candidate)
|
|
30
|
+
))
|
|
31
|
+
.map(String))];
|
|
32
|
+
}
|
|
33
|
+
return providerMessageIdsFor(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function sendMaterializedArtifact(file, {
|
|
37
|
+
sendFile,
|
|
38
|
+
sendImage,
|
|
39
|
+
signal,
|
|
40
|
+
}) {
|
|
41
|
+
if (file.mediaType?.startsWith('image/') && typeof sendImage === 'function') {
|
|
42
|
+
try {
|
|
43
|
+
return {
|
|
44
|
+
presentation: 'image',
|
|
45
|
+
result: await sendImage(file),
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (isAbort(error, signal) || error?.code === 'artifact-delivery-uncertain') {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
signal?.throwIfAborted();
|
|
54
|
+
if (typeof sendFile !== 'function') throw unavailableError();
|
|
55
|
+
return {
|
|
56
|
+
presentation: 'file',
|
|
57
|
+
result: await sendFile(file),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Deliver registered artifacts with one shared image-first policy while keeping
|
|
63
|
+
* provider protocol details inside the channel-supplied send closures.
|
|
64
|
+
*/
|
|
65
|
+
export async function deliverOutboundArtifacts({
|
|
66
|
+
artifacts = [],
|
|
67
|
+
baseReceipt = null,
|
|
68
|
+
deliveryId,
|
|
69
|
+
aggregatePresentation,
|
|
70
|
+
alwaysMerge = false,
|
|
71
|
+
channelKey,
|
|
72
|
+
signal,
|
|
73
|
+
sendFile,
|
|
74
|
+
sendImage,
|
|
75
|
+
sendFailureNotice,
|
|
76
|
+
logger,
|
|
77
|
+
}) {
|
|
78
|
+
const receipts = baseReceipt ? [baseReceipt] : [];
|
|
79
|
+
let userVisible = Boolean(baseReceipt) && baseReceipt.deliveryOutcome !== 'failed';
|
|
80
|
+
let failureNoticeVisible = false;
|
|
81
|
+
let artifactsSent = 0;
|
|
82
|
+
let artifactSendErrors = 0;
|
|
83
|
+
|
|
84
|
+
let artifactIndex = 0;
|
|
85
|
+
try {
|
|
86
|
+
while (artifactIndex < artifacts.length) {
|
|
87
|
+
const artifact = artifacts[artifactIndex];
|
|
88
|
+
artifactIndex += 1;
|
|
89
|
+
try {
|
|
90
|
+
signal?.throwIfAborted();
|
|
91
|
+
const file = await materializeOutboundArtifact(artifact, { signal });
|
|
92
|
+
signal?.throwIfAborted();
|
|
93
|
+
const sent = await sendMaterializedArtifact(file, {
|
|
94
|
+
sendFile,
|
|
95
|
+
sendImage,
|
|
96
|
+
signal,
|
|
97
|
+
});
|
|
98
|
+
signal?.throwIfAborted();
|
|
99
|
+
receipts.push(createDeliveryReceipt({
|
|
100
|
+
deliveryId: file.deliveryKey,
|
|
101
|
+
presentation: `${channelKey}-${sent.presentation}`,
|
|
102
|
+
providerMessageIds: providerIds(sent.result),
|
|
103
|
+
artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
|
|
104
|
+
}));
|
|
105
|
+
artifactsSent += 1;
|
|
106
|
+
userVisible = true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (isAbort(error, signal)) throw error;
|
|
109
|
+
artifactSendErrors += 1;
|
|
110
|
+
logger?.warn?.(
|
|
111
|
+
`[dsh-im:${channelKey}] result artifact delivery failed (${error?.code ?? 'unknown'})`,
|
|
112
|
+
);
|
|
113
|
+
let messageIds = [];
|
|
114
|
+
if (typeof sendFailureNotice === 'function') {
|
|
115
|
+
try {
|
|
116
|
+
signal?.throwIfAborted();
|
|
117
|
+
const notice = await sendFailureNotice(artifact, error);
|
|
118
|
+
signal?.throwIfAborted();
|
|
119
|
+
messageIds = providerIds(notice);
|
|
120
|
+
failureNoticeVisible = true;
|
|
121
|
+
} catch (noticeError) {
|
|
122
|
+
if (isAbort(noticeError, signal)) throw noticeError;
|
|
123
|
+
logger?.warn?.(
|
|
124
|
+
`[dsh-im:${channelKey}] unable to send the safe artifact failure notice`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const failureReceipt = createArtifactFailureReceipt({
|
|
129
|
+
artifactId: artifact?.artifactId ?? 'unknown',
|
|
130
|
+
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
131
|
+
error,
|
|
132
|
+
providerMessageIds: messageIds,
|
|
133
|
+
});
|
|
134
|
+
receipts.push(failureReceipt);
|
|
135
|
+
if (failureNoticeVisible || failureReceipt.artifacts[0]?.outcome === 'unknown') {
|
|
136
|
+
userVisible = true;
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
releaseOutboundArtifact(artifact);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
while (artifactIndex < artifacts.length) {
|
|
144
|
+
releaseOutboundArtifact(artifacts[artifactIndex]);
|
|
145
|
+
artifactIndex += 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let receipt = null;
|
|
150
|
+
if (receipts.length === 1 && !alwaysMerge) {
|
|
151
|
+
[receipt] = receipts;
|
|
152
|
+
} else if (receipts.length > 0) {
|
|
153
|
+
receipt = mergeDeliveryReceipts({
|
|
154
|
+
deliveryId: deliveryId
|
|
155
|
+
?? baseReceipt?.deliveryId
|
|
156
|
+
?? artifacts[0]?.deliveryKey,
|
|
157
|
+
presentation: aggregatePresentation
|
|
158
|
+
?? `${channelKey}-${baseReceipt ? 'text-and-files' : 'files'}`,
|
|
159
|
+
receipts,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
receipt,
|
|
165
|
+
userVisible,
|
|
166
|
+
failureNoticeVisible,
|
|
167
|
+
artifactsSent,
|
|
168
|
+
artifactSendErrors,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -576,7 +576,7 @@ export function createOutboundArtifactTool({ registry = outboundArtifactRegistry
|
|
|
576
576
|
};
|
|
577
577
|
const definition = Object.freeze({
|
|
578
578
|
name: OUTBOUND_ARTIFACT_TOOL,
|
|
579
|
-
description: 'Send a readable file to the user through the current conversation. Existing and newly created files are both valid.',
|
|
579
|
+
description: 'Send a readable file or generated image to the user through the current conversation. Existing and newly created files are both valid.',
|
|
580
580
|
parameters: {
|
|
581
581
|
type: 'object',
|
|
582
582
|
additionalProperties: false,
|
|
@@ -654,7 +654,7 @@ export function installOutboundArtifactTool(ctx, { registry = outboundArtifactRe
|
|
|
654
654
|
ctx.systemPrompt.section({
|
|
655
655
|
name: 'dsh-im:return-file',
|
|
656
656
|
order: 115,
|
|
657
|
-
text: `When the user asks to receive a file, call ${OUTBOUND_ARTIFACT_TOOL} with its path. Existing files can be sent directly; do not recreate or rename a file solely for delivery.`,
|
|
657
|
+
text: `When the user asks to receive a file or generated image, call ${OUTBOUND_ARTIFACT_TOOL} with its path. Existing files can be sent directly; do not recreate or rename a file solely for delivery.`,
|
|
658
658
|
});
|
|
659
659
|
return true;
|
|
660
660
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export const DELIVERY_RECEIPT_SCHEMA_VERSION = 1;
|
|
2
2
|
|
|
3
3
|
const ARTIFACT_OUTCOMES = new Set(['sent', 'rejected', 'failed', 'unknown']);
|
|
4
|
+
const DELIVERY_OUTCOMES = new Set(['sent', 'failed', 'unknown']);
|
|
5
|
+
const TEXT_FORMATS = new Set(['plain', 'markdown']);
|
|
4
6
|
const REJECTED_ARTIFACT_ERRORS = new Set([
|
|
5
7
|
'artifact-changed',
|
|
6
8
|
'artifact-context-required',
|
|
@@ -34,6 +36,8 @@ export function providerMessageIdsFor(value) {
|
|
|
34
36
|
value.message?.ts,
|
|
35
37
|
value.key?.id,
|
|
36
38
|
value.data?.message_id,
|
|
39
|
+
value.body?.msgid,
|
|
40
|
+
value.body?.message_id,
|
|
37
41
|
];
|
|
38
42
|
const id = candidates.find((candidate) => (
|
|
39
43
|
(typeof candidate === 'string' && candidate.trim())
|
|
@@ -50,6 +54,24 @@ function requiredString(value, name) {
|
|
|
50
54
|
return value;
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
export function createTextDeliveryBlock(value, format = 'plain') {
|
|
58
|
+
const block = typeof value === 'string'
|
|
59
|
+
? { kind: 'text', text: value, format }
|
|
60
|
+
: value;
|
|
61
|
+
if (!block || typeof block !== 'object' || Array.isArray(block)
|
|
62
|
+
|| block.kind !== 'text' || typeof block.text !== 'string' || !block.text.trim()) {
|
|
63
|
+
throw new TypeError('text delivery block must contain non-empty text');
|
|
64
|
+
}
|
|
65
|
+
if (!TEXT_FORMATS.has(block.format)) {
|
|
66
|
+
throw new TypeError('text delivery format must be plain or markdown');
|
|
67
|
+
}
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
kind: 'text',
|
|
70
|
+
text: block.text,
|
|
71
|
+
format: block.format,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
function providerIds(values) {
|
|
54
76
|
if (!Array.isArray(values)) throw new TypeError('providerMessageIds must be an array');
|
|
55
77
|
const ids = [];
|
|
@@ -96,12 +118,22 @@ export function createDeliveryReceipt({
|
|
|
96
118
|
presentation,
|
|
97
119
|
providerMessageIds = [],
|
|
98
120
|
artifacts = [],
|
|
121
|
+
deliveryOutcome,
|
|
122
|
+
reason,
|
|
99
123
|
}) {
|
|
124
|
+
if (deliveryOutcome !== undefined && !DELIVERY_OUTCOMES.has(deliveryOutcome)) {
|
|
125
|
+
throw new TypeError('deliveryOutcome must be sent, failed, or unknown');
|
|
126
|
+
}
|
|
127
|
+
const normalizedReason = reason === undefined
|
|
128
|
+
? undefined
|
|
129
|
+
: requiredString(reason, 'delivery reason');
|
|
100
130
|
return Object.freeze({
|
|
101
131
|
schemaVersion: DELIVERY_RECEIPT_SCHEMA_VERSION,
|
|
102
132
|
deliveryId: requiredString(deliveryId, 'deliveryId'),
|
|
103
133
|
presentation: requiredString(presentation, 'presentation'),
|
|
104
134
|
providerMessageIds: providerIds(providerMessageIds),
|
|
135
|
+
...(deliveryOutcome === undefined ? {} : { deliveryOutcome }),
|
|
136
|
+
...(normalizedReason === undefined ? {} : { reason: normalizedReason }),
|
|
105
137
|
artifacts: artifactResults(artifacts),
|
|
106
138
|
});
|
|
107
139
|
}
|
|
@@ -135,11 +167,17 @@ export function mergeDeliveryReceipts({ deliveryId, presentation, receipts }) {
|
|
|
135
167
|
}
|
|
136
168
|
const messageIds = [];
|
|
137
169
|
const artifacts = new Map();
|
|
170
|
+
let deliveryOutcome;
|
|
171
|
+
let reason;
|
|
138
172
|
for (const receipt of receipts) {
|
|
139
173
|
if (!receipt || receipt.schemaVersion !== DELIVERY_RECEIPT_SCHEMA_VERSION) {
|
|
140
174
|
throw new TypeError('receipt must use DeliveryReceipt schema version 1');
|
|
141
175
|
}
|
|
142
176
|
messageIds.push(...(receipt.providerMessageIds ?? []));
|
|
177
|
+
if (deliveryOutcome === undefined && receipt.deliveryOutcome !== undefined) {
|
|
178
|
+
deliveryOutcome = receipt.deliveryOutcome;
|
|
179
|
+
reason = receipt.reason;
|
|
180
|
+
}
|
|
143
181
|
for (const artifact of receipt.artifacts ?? []) {
|
|
144
182
|
artifacts.set(artifact.artifactId, artifact);
|
|
145
183
|
}
|
|
@@ -148,6 +186,8 @@ export function mergeDeliveryReceipts({ deliveryId, presentation, receipts }) {
|
|
|
148
186
|
deliveryId,
|
|
149
187
|
presentation,
|
|
150
188
|
providerMessageIds: messageIds,
|
|
189
|
+
deliveryOutcome,
|
|
190
|
+
reason,
|
|
151
191
|
artifacts: [...artifacts.values()],
|
|
152
192
|
});
|
|
153
193
|
}
|