@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.
Files changed (82) hide show
  1. package/README.en.md +18 -6
  2. package/README.md +18 -6
  3. package/lib/index.js +206 -176
  4. package/package.json +1 -1
  5. package/plugin-src/host/index.mjs +2 -0
  6. package/src/channels/dingtalk/config-store.mjs +5 -3
  7. package/src/channels/dingtalk/dingtalk-api.mjs +105 -82
  8. package/src/channels/dingtalk/dingtalk-bridge.mjs +79 -110
  9. package/src/channels/dingtalk/dingtalk-card-stream.mjs +3 -1
  10. package/src/channels/dingtalk/dingtalk-controller.mjs +24 -23
  11. package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -3
  12. package/src/channels/dingtalk/state-store.mjs +3 -1
  13. package/src/channels/discord/config-store.mjs +2 -1
  14. package/src/channels/discord/discord-api.mjs +25 -1
  15. package/src/channels/discord/discord-runtime.mjs +237 -11
  16. package/src/channels/feishu/bridge.mjs +169 -180
  17. package/src/channels/feishu/feishu-cards.mjs +62 -54
  18. package/src/channels/feishu/feishu-channel.mjs +57 -21
  19. package/src/channels/feishu/feishu-runtime.mjs +10 -9
  20. package/src/channels/feishu/message-utils.mjs +4 -3
  21. package/src/channels/office/office-job-executor.mjs +15 -14
  22. package/src/channels/office/office-runtime.mjs +6 -5
  23. package/src/channels/qq/config-store.mjs +3 -1
  24. package/src/channels/qq/markdown-reply.mjs +4 -2
  25. package/src/channels/qq/qq-bridge.mjs +149 -130
  26. package/src/channels/qq/qq-controller.mjs +17 -16
  27. package/src/channels/qq/qq-runtime.mjs +3 -2
  28. package/src/channels/shared/agent-preset.mjs +3 -1
  29. package/src/channels/shared/compact-command.mjs +15 -14
  30. package/src/channels/shared/connection-test.mjs +6 -4
  31. package/src/channels/shared/control-command.mjs +13 -11
  32. package/src/channels/shared/editable-message-stream.mjs +4 -2
  33. package/src/channels/shared/harness-approval.mjs +23 -21
  34. package/src/channels/shared/harness-client.mjs +3 -2
  35. package/src/channels/shared/harness-question.mjs +8 -6
  36. package/src/channels/shared/i18n-en/dingtalk.mjs +52 -0
  37. package/src/channels/shared/i18n-en/discord.mjs +8 -0
  38. package/src/channels/shared/i18n-en/feishu.mjs +217 -0
  39. package/src/channels/shared/i18n-en/office.mjs +23 -0
  40. package/src/channels/shared/i18n-en/qq.mjs +54 -0
  41. package/src/channels/shared/i18n-en/shared-a.mjs +91 -0
  42. package/src/channels/shared/i18n-en/shared-b.mjs +215 -0
  43. package/src/channels/shared/i18n-en/shared-c.mjs +99 -0
  44. package/src/channels/shared/i18n-en/slack.mjs +27 -0
  45. package/src/channels/shared/i18n-en/telegram.mjs +21 -0
  46. package/src/channels/shared/i18n-en/wecom.mjs +38 -0
  47. package/src/channels/shared/i18n-en/weixin.mjs +80 -0
  48. package/src/channels/shared/i18n-en/whatsapp.mjs +17 -0
  49. package/src/channels/shared/i18n-en.mjs +35 -0
  50. package/src/channels/shared/i18n.mjs +35 -0
  51. package/src/channels/shared/image-prompt.mjs +17 -15
  52. package/src/channels/shared/inbound-file.mjs +5 -3
  53. package/src/channels/shared/model-command.mjs +35 -31
  54. package/src/channels/shared/preset-command.mjs +41 -38
  55. package/src/channels/shared/semantic/artifact-delivery.mjs +170 -0
  56. package/src/channels/shared/semantic/artifact.mjs +2 -2
  57. package/src/channels/shared/semantic/delivery.mjs +40 -0
  58. package/src/channels/shared/text-harness-bridge.mjs +154 -136
  59. package/src/channels/shared/token-bot-controller.mjs +32 -11
  60. package/src/channels/shared/workspace-command.mjs +69 -58
  61. package/src/channels/slack/config-store.mjs +3 -1
  62. package/src/channels/slack/slack-api.mjs +6 -5
  63. package/src/channels/slack/slack-controller.mjs +10 -9
  64. package/src/channels/slack/slack-runtime.mjs +16 -4
  65. package/src/channels/telegram/config-store.mjs +2 -1
  66. package/src/channels/telegram/telegram-api.mjs +111 -19
  67. package/src/channels/telegram/telegram-rich-message.mjs +147 -0
  68. package/src/channels/telegram/telegram-runtime.mjs +383 -5
  69. package/src/channels/wecom/config-store.mjs +3 -1
  70. package/src/channels/wecom/wecom-bridge.mjs +154 -153
  71. package/src/channels/wecom/wecom-controller.mjs +20 -19
  72. package/src/channels/wecom/wecom-runtime.mjs +2 -1
  73. package/src/channels/weixin/config-store.mjs +2 -1
  74. package/src/channels/weixin/weixin-api.mjs +140 -107
  75. package/src/channels/weixin/weixin-bridge.mjs +86 -123
  76. package/src/channels/weixin/weixin-controller.mjs +22 -21
  77. package/src/channels/weixin/weixin-runtime.mjs +3 -2
  78. package/src/channels/whatsapp/config-store.mjs +3 -1
  79. package/src/channels/whatsapp/whatsapp-bridge.mjs +3 -1
  80. package/src/channels/whatsapp/whatsapp-controller.mjs +15 -14
  81. package/src/channels/whatsapp/whatsapp-runtime.mjs +47 -13
  82. 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
- `模型序号无效:${safeDisplayText(requested)}`,
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
- `没有找到模型:${safeDisplayText(requested)}`,
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(`模型已切换为:\n${modelId(selection.provider, selection.model)}\n\n后续消息将使用该模型。`);
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 `${label}(${item.id})`;
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
- : `${catalog.defaultId}(当前不可用)`;
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
- ? `跟随 Host 默认:${presetItemText(item)}`
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
- : `${agentPreset}(已不可用)`;
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
- `Host 默认:${defaultDescription(catalog)}`,
109
+ t('Host 默认:{preset}', { preset: defaultDescription(catalog) }),
109
110
  '',
110
- `可用 Agent Preset(${catalog.items.length}):`,
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 格式无效。\n${PRESET_USAGE}`;
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 格式无效。\n${PRESET_USAGE}`);
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
  }