@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
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* without button callbacks.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { t } from '../shared/i18n.mjs';
|
|
18
|
+
|
|
17
19
|
export const MENU_PAGE_SIZE = 10;
|
|
18
20
|
|
|
19
21
|
function plainText(content) {
|
|
@@ -56,7 +58,7 @@ function buttonElement(content, actionValue) {
|
|
|
56
58
|
|
|
57
59
|
function safeTitle(value) {
|
|
58
60
|
const title = String(value ?? '').replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu, ' ').replace(/\s+/gu, ' ').trim();
|
|
59
|
-
return title || '暂无标题';
|
|
61
|
+
return title || t('暂无标题');
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
function cardWith(headerText, elements) {
|
|
@@ -69,18 +71,18 @@ function cardWith(headerText, elements) {
|
|
|
69
71
|
|
|
70
72
|
/** The main command menu (buttons + number-reply fallback). */
|
|
71
73
|
export function menuCard() {
|
|
72
|
-
return cardWith('🤖 助手菜单', [
|
|
73
|
-
{ tag: 'div', text: markdown('**点击按钮或直接回复数字**') },
|
|
74
|
-
button('1 · 会话列表', 'sessions'),
|
|
75
|
-
button('2 · 工作区', 'workspaces'),
|
|
76
|
-
button('3 · 新会话', 'new'),
|
|
77
|
-
button('4 · 状态', 'status'),
|
|
78
|
-
button('5 · 帮助', 'help'),
|
|
74
|
+
return cardWith(t('🤖 助手菜单'), [
|
|
75
|
+
{ tag: 'div', text: markdown(t('**点击按钮或直接回复数字**')) },
|
|
76
|
+
button(t('1 · 会话列表'), 'sessions'),
|
|
77
|
+
button(t('2 · 工作区'), 'workspaces'),
|
|
78
|
+
button(t('3 · 新会话'), 'new'),
|
|
79
|
+
button(t('4 · 状态'), 'status'),
|
|
80
|
+
button(t('5 · 帮助'), 'help'),
|
|
79
81
|
// Repair must remain number-driven. Apps that need this command do not
|
|
80
82
|
// have card.action.trigger yet, so rendering it as a callback button would
|
|
81
83
|
// send the user straight back to Feishu's broken callback setup popup.
|
|
82
|
-
{ tag: 'div', text: markdown('**6 · 修复卡片按钮**(请直接回复数字 **6**)') },
|
|
83
|
-
button('7 · 关注列表', 'watchlist'),
|
|
84
|
+
{ tag: 'div', text: markdown(t('**6 · 修复卡片按钮**(请直接回复数字 **6**)')) },
|
|
85
|
+
button(t('7 · 关注列表'), 'watchlist'),
|
|
84
86
|
]);
|
|
85
87
|
}
|
|
86
88
|
|
|
@@ -89,10 +91,10 @@ export function cardActionProbeCard(nonce) {
|
|
|
89
91
|
if (typeof nonce !== 'string' || !/^[A-Za-z0-9_-]{16,128}$/.test(nonce)) {
|
|
90
92
|
throw new TypeError('A safe card-action probe nonce is required');
|
|
91
93
|
}
|
|
92
|
-
return cardWith('🧪 验证卡片按钮', [
|
|
94
|
+
return cardWith(t('🧪 验证卡片按钮'), [
|
|
93
95
|
{
|
|
94
96
|
tag: 'div',
|
|
95
|
-
text: markdown('授权已提交。请点击下方按钮;机器人真实收到回调后才会判定修复成功。'),
|
|
97
|
+
text: markdown(t('授权已提交。请点击下方按钮;机器人真实收到回调后才会判定修复成功。')),
|
|
96
98
|
},
|
|
97
99
|
{
|
|
98
100
|
tag: 'column_set',
|
|
@@ -103,7 +105,7 @@ export function cardActionProbeCard(nonce) {
|
|
|
103
105
|
weight: 1,
|
|
104
106
|
elements: [{
|
|
105
107
|
tag: 'button',
|
|
106
|
-
text: plainText('完成验证'),
|
|
108
|
+
text: plainText(t('完成验证')),
|
|
107
109
|
type: 'primary',
|
|
108
110
|
width: 'fill',
|
|
109
111
|
behaviors: [{
|
|
@@ -140,74 +142,80 @@ export function sessionListCard(workspace, sessions, page, total, watchedSession
|
|
|
140
142
|
],
|
|
141
143
|
});
|
|
142
144
|
const elements = [
|
|
143
|
-
{ tag: 'div', text: markdown(
|
|
145
|
+
{ tag: 'div', text: markdown(t('**工作区**:{workspace}\n共 **{total}** 个会话{paging}', {
|
|
146
|
+
workspace: `\`${workspace}\``,
|
|
147
|
+
total,
|
|
148
|
+
paging: total > MENU_PAGE_SIZE
|
|
149
|
+
? t('(第 {page}/{pageCount} 页)', { page: page + 1, pageCount })
|
|
150
|
+
: '',
|
|
151
|
+
})) },
|
|
144
152
|
...slice.map((session, offset) => {
|
|
145
153
|
// Page-local numbering: number replies resolve against this page.
|
|
146
|
-
const label = `${offset + 1}. ${safeTitle(session.title)}${session.archived === true ? '(已归档)' : ''}`;
|
|
154
|
+
const label = `${offset + 1}. ${safeTitle(session.title)}${session.archived === true ? t('(已归档)') : ''}`;
|
|
147
155
|
const watching = watched(session.sessionId);
|
|
148
156
|
return row(
|
|
149
|
-
buttonElement(watching ? '⭐取关' : '⭐关注', watching ? `unwatch:${session.sessionId}` : `watch:${session.sessionId}`),
|
|
157
|
+
buttonElement(watching ? t('⭐取关') : t('⭐关注'), watching ? `unwatch:${session.sessionId}` : `watch:${session.sessionId}`),
|
|
150
158
|
buttonElement(label, `use:${session.sessionId}`),
|
|
151
159
|
);
|
|
152
160
|
}),
|
|
153
161
|
];
|
|
154
|
-
if (page > 0) elements.push(button('◀ 上一页', `sessions:${page - 1}`));
|
|
155
|
-
if (page + 1 < pageCount) elements.push(button('下一页 ▶', `sessions:${page + 1}`));
|
|
156
|
-
elements.push({ tag: 'div', text: markdown('回复数字(1~N)绑定本页会话。') });
|
|
157
|
-
return cardWith('📂 会话列表', elements);
|
|
162
|
+
if (page > 0) elements.push(button(t('◀ 上一页'), `sessions:${page - 1}`));
|
|
163
|
+
if (page + 1 < pageCount) elements.push(button(t('下一页 ▶'), `sessions:${page + 1}`));
|
|
164
|
+
elements.push({ tag: 'div', text: markdown(t('回复数字(1~N)绑定本页会话。')) });
|
|
165
|
+
return cardWith(t('📂 会话列表'), elements);
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
/** The workspace list card (switch-workspace buttons + reply fallback). */
|
|
161
169
|
export function workspaceListCard(paths, current) {
|
|
162
170
|
const elements = paths.length === 0
|
|
163
|
-
? [{ tag: 'div', text: markdown('当前 Host 上没有已登记的工作区。') }]
|
|
171
|
+
? [{ tag: 'div', text: markdown(t('当前 Host 上没有已登记的工作区。')) }]
|
|
164
172
|
: [
|
|
165
|
-
{ tag: 'div', text: markdown(
|
|
173
|
+
{ tag: 'div', text: markdown(t('回复数字切换工作区,或点击按钮:')) },
|
|
166
174
|
...paths.map((path, index) => button(
|
|
167
|
-
`${index + 1}. ${path}${path === current ? '(当前)' : ''}`,
|
|
175
|
+
`${index + 1}. ${path}${path === current ? t('(当前)') : ''}`,
|
|
168
176
|
`workspace:${path}`,
|
|
169
177
|
)),
|
|
170
178
|
];
|
|
171
|
-
return cardWith('🗂 工作区', elements);
|
|
179
|
+
return cardWith(t('🗂 工作区'), elements);
|
|
172
180
|
}
|
|
173
181
|
|
|
174
182
|
/** The card-menu help text (number-driven, no command memorization). */
|
|
175
183
|
export function menuHelpText() {
|
|
176
184
|
return [
|
|
177
|
-
'🤖 助手菜单(回复数字即可,无需记命令)',
|
|
185
|
+
t('🤖 助手菜单(回复数字即可,无需记命令)'),
|
|
178
186
|
'',
|
|
179
|
-
'1 · /sessionlist 列出会话(回复数字绑定)',
|
|
180
|
-
'2 · /workspacelist 列出工作区(回复数字切换)',
|
|
181
|
-
'3 · /new 开启新会话',
|
|
182
|
-
'4 · /status 连接状态',
|
|
183
|
-
'5 · /help 本帮助',
|
|
184
|
-
'6 · /repair 修复卡片按钮(请回复数字 6)',
|
|
185
|
-
'7 · /watchlist 关注列表',
|
|
187
|
+
t('1 · /sessionlist 列出会话(回复数字绑定)'),
|
|
188
|
+
t('2 · /workspacelist 列出工作区(回复数字切换)'),
|
|
189
|
+
t('3 · /new 开启新会话'),
|
|
190
|
+
t('4 · /status 连接状态'),
|
|
191
|
+
t('5 · /help 本帮助'),
|
|
192
|
+
t('6 · /repair 修复卡片按钮(请回复数字 6)'),
|
|
193
|
+
t('7 · /watchlist 关注列表'),
|
|
186
194
|
'',
|
|
187
|
-
'直接发送文字/图片即继续当前会话。',
|
|
188
|
-
'/session ID 或序号 绑定已有会话',
|
|
189
|
-
'/watch ID 或序号 关注会话(完成后推送)',
|
|
190
|
-
'/compact 压缩上下文',
|
|
191
|
-
'/workspace 绝对路径 切换工作区',
|
|
192
|
-
'/presetlist 列出可用 Agent Preset',
|
|
193
|
-
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
|
|
194
|
-
'纯数字 ID:/preset id:<ID>',
|
|
195
|
-
'/preset --default 跟随 Host 默认',
|
|
195
|
+
t('直接发送文字/图片即继续当前会话。'),
|
|
196
|
+
t('/session ID 或序号 绑定已有会话'),
|
|
197
|
+
t('/watch ID 或序号 关注会话(完成后推送)'),
|
|
198
|
+
t('/compact 压缩上下文'),
|
|
199
|
+
t('/workspace 绝对路径 切换工作区'),
|
|
200
|
+
t('/presetlist 列出可用 Agent Preset'),
|
|
201
|
+
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
|
202
|
+
t('纯数字 ID:/preset id:<ID>'),
|
|
203
|
+
t('/preset --default 跟随 Host 默认'),
|
|
196
204
|
].join('\n');
|
|
197
205
|
}
|
|
198
206
|
|
|
199
207
|
/** The watch list for one conversation (unwatch buttons + reply fallback). */
|
|
200
208
|
export function watchListCard(entries) {
|
|
201
209
|
const elements = entries.length === 0
|
|
202
|
-
? [{ tag: 'div', text: markdown('当前没有关注的会话。\n`/watch <ID|序号>` 关注后,任务完成会自动推送。') }]
|
|
210
|
+
? [{ tag: 'div', text: markdown(t('当前没有关注的会话。\n`/watch <ID|序号>` 关注后,任务完成会自动推送。')) }]
|
|
203
211
|
: [
|
|
204
|
-
{ tag: 'div', text: markdown('任务完成会自动推送,回复数字或点按钮取消关注:') },
|
|
212
|
+
{ tag: 'div', text: markdown(t('任务完成会自动推送,回复数字或点按钮取消关注:')) },
|
|
205
213
|
...entries.map((entry, index) => button(
|
|
206
214
|
`${index + 1}. ${safeTitle(entry.title)}`,
|
|
207
215
|
`unwatch:${entry.sessionId}`,
|
|
208
216
|
)),
|
|
209
217
|
];
|
|
210
|
-
return cardWith('👁 关注列表', elements);
|
|
218
|
+
return cardWith(t('👁 关注列表'), elements);
|
|
211
219
|
}
|
|
212
220
|
|
|
213
221
|
/**
|
|
@@ -216,19 +224,19 @@ export function watchListCard(entries) {
|
|
|
216
224
|
*/
|
|
217
225
|
export function completionCard(sessionId, title, reason) {
|
|
218
226
|
const reasonText = reason === 'completed'
|
|
219
|
-
? '已完成'
|
|
227
|
+
? t('已完成')
|
|
220
228
|
: reason === 'stopped'
|
|
221
|
-
? '已停止'
|
|
229
|
+
? t('已停止')
|
|
222
230
|
: reason === 'aborted'
|
|
223
|
-
? '已中止'
|
|
231
|
+
? t('已中止')
|
|
224
232
|
: reason === 'cancelled'
|
|
225
|
-
? '已取消'
|
|
226
|
-
: '已结束';
|
|
227
|
-
return cardWith('✅ 任务完成', [
|
|
233
|
+
? t('已取消')
|
|
234
|
+
: t('已结束');
|
|
235
|
+
return cardWith(t('✅ 任务完成'), [
|
|
228
236
|
{ tag: 'div', text: markdown(`**${safeTitle(title)}**\n\`${sessionId}\``) },
|
|
229
|
-
{ tag: 'div', text: markdown(
|
|
230
|
-
button('打开会话列表', 'sessions'),
|
|
231
|
-
button('工作区', 'workspaces'),
|
|
232
|
-
{ tag: 'div', text: markdown('绑定该会话后可继续追问,输入文字即可。') },
|
|
237
|
+
{ tag: 'div', text: markdown(t('**状态**:{reason}', { reason: reasonText })) },
|
|
238
|
+
button(t('打开会话列表'), 'sessions'),
|
|
239
|
+
button(t('工作区'), 'workspaces'),
|
|
240
|
+
{ tag: 'div', text: markdown(t('绑定该会话后可继续追问,输入文字即可。')) },
|
|
233
241
|
]);
|
|
234
242
|
}
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
|
|
3
3
|
import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
|
|
4
4
|
import { createDeliveryReceipt } from '../shared/semantic/delivery.mjs';
|
|
5
|
+
import { t } from '../shared/i18n.mjs';
|
|
5
6
|
|
|
6
7
|
const STREAM_ELEMENT_ID = 'stream_md';
|
|
7
8
|
const DEFAULT_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
@@ -114,9 +115,10 @@ function waitForFileOperation(operation, { signal, timeoutMs, stage }) {
|
|
|
114
115
|
});
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
function deliveryUuid(file, chatId) {
|
|
118
|
+
function deliveryUuid(file, chatId, messageType) {
|
|
119
|
+
const seed = `${file.deliveryKey}\u0000${chatId}`;
|
|
118
120
|
const digest = createHash('sha256')
|
|
119
|
-
.update(`${
|
|
121
|
+
.update(messageType === 'file' ? seed : `${seed}\u0000${messageType}`)
|
|
120
122
|
.digest('hex')
|
|
121
123
|
.slice(0, 40);
|
|
122
124
|
return `dshim_${digest}`;
|
|
@@ -132,7 +134,7 @@ function streamingCard(initialText) {
|
|
|
132
134
|
schema: '2.0',
|
|
133
135
|
config: {
|
|
134
136
|
streaming_mode: true,
|
|
135
|
-
summary: { content: '正在生成…' },
|
|
137
|
+
summary: { content: t('正在生成…') },
|
|
136
138
|
streaming_config: {
|
|
137
139
|
print_frequency_ms: { default: 70 },
|
|
138
140
|
print_step: { default: 1 },
|
|
@@ -157,12 +159,12 @@ export class VerifiedFeishuChannel {
|
|
|
157
159
|
|
|
158
160
|
constructor({
|
|
159
161
|
client,
|
|
160
|
-
initialText
|
|
162
|
+
initialText,
|
|
161
163
|
fileUploadTimeoutMs = MAX_FILE_OPERATION_TIMEOUT_MS,
|
|
162
164
|
fileMessageTimeoutMs = MAX_FILE_OPERATION_TIMEOUT_MS,
|
|
163
165
|
}) {
|
|
164
166
|
this.#client = client;
|
|
165
|
-
this.#initialText = initialText;
|
|
167
|
+
this.#initialText = initialText ?? t(DEFAULT_INITIAL_TEXT);
|
|
166
168
|
this.#fileUploadTimeoutMs = boundedFileTimeout(fileUploadTimeoutMs, 'fileUploadTimeoutMs');
|
|
167
169
|
this.#fileMessageTimeoutMs = boundedFileTimeout(fileMessageTimeoutMs, 'fileMessageTimeoutMs');
|
|
168
170
|
}
|
|
@@ -214,7 +216,7 @@ export class VerifiedFeishuChannel {
|
|
|
214
216
|
settings: JSON.stringify({
|
|
215
217
|
config: {
|
|
216
218
|
streaming_mode: false,
|
|
217
|
-
summary: { content: summaryOf(lastContent) || '回答完成' },
|
|
219
|
+
summary: { content: summaryOf(lastContent) || t('回答完成') },
|
|
218
220
|
},
|
|
219
221
|
}),
|
|
220
222
|
sequence: ++sequence,
|
|
@@ -230,6 +232,29 @@ export class VerifiedFeishuChannel {
|
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
async sendFile(chatId, file, { replyTo, signal } = {}) {
|
|
235
|
+
return this.#sendArtifact(chatId, file, {
|
|
236
|
+
replyTo,
|
|
237
|
+
signal,
|
|
238
|
+
messageType: 'file',
|
|
239
|
+
presentation: 'feishu-file',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async sendImage(chatId, file, { replyTo, signal } = {}) {
|
|
244
|
+
return this.#sendArtifact(chatId, file, {
|
|
245
|
+
replyTo,
|
|
246
|
+
signal,
|
|
247
|
+
messageType: 'image',
|
|
248
|
+
presentation: 'feishu-image',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async #sendArtifact(chatId, file, {
|
|
253
|
+
replyTo,
|
|
254
|
+
signal,
|
|
255
|
+
messageType,
|
|
256
|
+
presentation,
|
|
257
|
+
}) {
|
|
233
258
|
signal?.throwIfAborted();
|
|
234
259
|
if (typeof chatId !== 'string' || !chatId) throw new TypeError('chatId is required');
|
|
235
260
|
if (!file || typeof file !== 'object'
|
|
@@ -243,13 +268,20 @@ export class VerifiedFeishuChannel {
|
|
|
243
268
|
try {
|
|
244
269
|
uploaded = await waitForFileOperation((operationSignal) => {
|
|
245
270
|
operationSignal.throwIfAborted();
|
|
246
|
-
const pending =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
271
|
+
const pending = messageType === 'image'
|
|
272
|
+
? this.#client.im.v1.image.create({
|
|
273
|
+
data: {
|
|
274
|
+
image_type: 'message',
|
|
275
|
+
image: file.bytes,
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
: this.#client.im.v1.file.create({
|
|
279
|
+
data: {
|
|
280
|
+
file_type: 'stream',
|
|
281
|
+
file_name: file.fileName,
|
|
282
|
+
file: file.bytes,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
253
285
|
trackOutboundArtifactProviderPromise(file, pending);
|
|
254
286
|
return pending;
|
|
255
287
|
}, {
|
|
@@ -262,21 +294,25 @@ export class VerifiedFeishuChannel {
|
|
|
262
294
|
throw fileDeliveryError('upload', error);
|
|
263
295
|
}
|
|
264
296
|
signal?.throwIfAborted();
|
|
265
|
-
const
|
|
266
|
-
|
|
297
|
+
const resourceKey = messageType === 'image'
|
|
298
|
+
? uploaded?.image_key ?? uploaded?.data?.image_key
|
|
299
|
+
: uploaded?.file_key ?? uploaded?.data?.file_key;
|
|
300
|
+
if (typeof resourceKey !== 'string' || !resourceKey) {
|
|
267
301
|
throw fileDeliveryError('upload', undefined, uploaded?.code);
|
|
268
302
|
}
|
|
269
303
|
|
|
270
|
-
const uuid = deliveryUuid(file, chatId);
|
|
271
|
-
const content = JSON.stringify(
|
|
304
|
+
const uuid = deliveryUuid(file, chatId, messageType);
|
|
305
|
+
const content = JSON.stringify(messageType === 'image'
|
|
306
|
+
? { image_key: resourceKey }
|
|
307
|
+
: { file_key: resourceKey });
|
|
272
308
|
const request = replyTo
|
|
273
309
|
? {
|
|
274
310
|
path: { message_id: replyTo },
|
|
275
|
-
data: { msg_type:
|
|
311
|
+
data: { msg_type: messageType, content, uuid },
|
|
276
312
|
}
|
|
277
313
|
: {
|
|
278
314
|
params: { receive_id_type: 'chat_id' },
|
|
279
|
-
data: { receive_id: chatId, msg_type:
|
|
315
|
+
data: { receive_id: chatId, msg_type: messageType, content, uuid },
|
|
280
316
|
};
|
|
281
317
|
const send = () => {
|
|
282
318
|
const pending = replyTo
|
|
@@ -300,7 +336,7 @@ export class VerifiedFeishuChannel {
|
|
|
300
336
|
operationSignal.throwIfAborted();
|
|
301
337
|
|
|
302
338
|
// Feishu documents 230049 as an uncertain asynchronous send result.
|
|
303
|
-
// Reuse the same
|
|
339
|
+
// Reuse the same resource key and UUID once so the provider can deduplicate.
|
|
304
340
|
if (Number(result?.code) === 230049) {
|
|
305
341
|
result = await send();
|
|
306
342
|
operationSignal.throwIfAborted();
|
|
@@ -324,7 +360,7 @@ export class VerifiedFeishuChannel {
|
|
|
324
360
|
}
|
|
325
361
|
return createDeliveryReceipt({
|
|
326
362
|
deliveryId: file.deliveryKey,
|
|
327
|
-
presentation
|
|
363
|
+
presentation,
|
|
328
364
|
providerMessageIds: [messageId],
|
|
329
365
|
artifacts: [{
|
|
330
366
|
artifactId: file.artifactId,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
connectionTestTargetUnavailable,
|
|
8
8
|
sendRememberedConnectionTest,
|
|
9
9
|
} from '../shared/connection-test.mjs';
|
|
10
|
+
import { t } from '../shared/i18n.mjs';
|
|
10
11
|
|
|
11
12
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
12
13
|
const CALLBACK_PROBE_SUCCESS_NOTICE = '✅ 修复完成:已实测收到 card.action.trigger,菜单按钮现在可用。';
|
|
@@ -206,7 +207,7 @@ export class FeishuRuntime {
|
|
|
206
207
|
this.#client = new this.#lark.Client(larkConfig);
|
|
207
208
|
const channel = new VerifiedFeishuChannel({
|
|
208
209
|
client: this.#client,
|
|
209
|
-
initialText: '已连接 DeepSeek Harness,正在思考…',
|
|
210
|
+
initialText: t('已连接 DeepSeek Harness,正在思考…'),
|
|
210
211
|
});
|
|
211
212
|
this.#bridge = new FeishuHarnessBridge({
|
|
212
213
|
client: this.#client,
|
|
@@ -340,7 +341,7 @@ export class FeishuRuntime {
|
|
|
340
341
|
} catch {
|
|
341
342
|
void this.#sendCardActionProbeNotice(
|
|
342
343
|
operatorOpenId,
|
|
343
|
-
CALLBACK_PROBE_SEND_FAILURE_NOTICE,
|
|
344
|
+
t(CALLBACK_PROBE_SEND_FAILURE_NOTICE),
|
|
344
345
|
'failure',
|
|
345
346
|
);
|
|
346
347
|
throw probeError('card_action_probe_send_failed', '无法发送飞书卡片回调测试');
|
|
@@ -348,7 +349,7 @@ export class FeishuRuntime {
|
|
|
348
349
|
if (response?.code && response.code !== 0) {
|
|
349
350
|
void this.#sendCardActionProbeNotice(
|
|
350
351
|
operatorOpenId,
|
|
351
|
-
CALLBACK_PROBE_SEND_FAILURE_NOTICE,
|
|
352
|
+
t(CALLBACK_PROBE_SEND_FAILURE_NOTICE),
|
|
352
353
|
'failure',
|
|
353
354
|
);
|
|
354
355
|
throw probeError('card_action_probe_send_failed', '无法发送飞书卡片回调测试');
|
|
@@ -358,7 +359,7 @@ export class FeishuRuntime {
|
|
|
358
359
|
if (!messageId) {
|
|
359
360
|
void this.#sendCardActionProbeNotice(
|
|
360
361
|
operatorOpenId,
|
|
361
|
-
CALLBACK_PROBE_SEND_FAILURE_NOTICE,
|
|
362
|
+
t(CALLBACK_PROBE_SEND_FAILURE_NOTICE),
|
|
362
363
|
'failure',
|
|
363
364
|
);
|
|
364
365
|
throw probeError('card_action_probe_send_failed', '飞书未返回测试卡片的消息 ID');
|
|
@@ -371,7 +372,7 @@ export class FeishuRuntime {
|
|
|
371
372
|
this.#pendingCardActionProbes.delete(messageId);
|
|
372
373
|
void this.#sendCardActionProbeNotice(
|
|
373
374
|
operatorOpenId,
|
|
374
|
-
CALLBACK_PROBE_TIMEOUT_NOTICE,
|
|
375
|
+
t(CALLBACK_PROBE_TIMEOUT_NOTICE),
|
|
375
376
|
'timeout',
|
|
376
377
|
);
|
|
377
378
|
reject(probeError(
|
|
@@ -413,7 +414,7 @@ export class FeishuRuntime {
|
|
|
413
414
|
// the callback proof itself.
|
|
414
415
|
void this.#sendCardActionProbeNotice(
|
|
415
416
|
operatorOpenId,
|
|
416
|
-
CALLBACK_PROBE_SUCCESS_NOTICE,
|
|
417
|
+
t(CALLBACK_PROBE_SUCCESS_NOTICE),
|
|
417
418
|
'success',
|
|
418
419
|
).finally(() => {
|
|
419
420
|
probe.resolve({
|
|
@@ -482,10 +483,10 @@ export class FeishuRuntime {
|
|
|
482
483
|
return sendRememberedConnectionTest({
|
|
483
484
|
state: this.#state,
|
|
484
485
|
text,
|
|
485
|
-
channelLabel: '飞书机器人',
|
|
486
|
+
channelLabel: t('飞书机器人'),
|
|
486
487
|
send: async (target, content) => {
|
|
487
488
|
const chatId = typeof target?.chatId === 'string' ? target.chatId.trim() : '';
|
|
488
|
-
if (!chatId) throw connectionTestTargetUnavailable('飞书机器人');
|
|
489
|
+
if (!chatId) throw connectionTestTargetUnavailable(t('飞书机器人'));
|
|
489
490
|
await send('chat_id', chatId, content);
|
|
490
491
|
},
|
|
491
492
|
});
|
|
@@ -500,7 +501,7 @@ export class FeishuRuntime {
|
|
|
500
501
|
clearTimeout(probe.timeout);
|
|
501
502
|
void this.#sendCardActionProbeNotice(
|
|
502
503
|
probe.expectedOperatorOpenId,
|
|
503
|
-
CALLBACK_PROBE_ABORT_NOTICE,
|
|
504
|
+
t(CALLBACK_PROBE_ABORT_NOTICE),
|
|
504
505
|
'abort',
|
|
505
506
|
);
|
|
506
507
|
probe.reject(probeError('abort', '飞书运行时已停止'));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
|
2
|
+
import { t } from '../shared/i18n.mjs';
|
|
2
3
|
|
|
3
4
|
const FEISHU_MISSING_MESSAGE_SCOPE_CODE = 99991672;
|
|
4
5
|
const FEISHU_ERROR_BODY_LIMIT = 64 * 1024;
|
|
@@ -113,7 +114,7 @@ async function readBoundedStream(stream, { signal, maxBytes }) {
|
|
|
113
114
|
throw new ImagePromptError(
|
|
114
115
|
'image-too-large',
|
|
115
116
|
`Feishu image exceeds ${maxBytes} bytes`,
|
|
116
|
-
'图片超过 5 MB,请压缩后重试。',
|
|
117
|
+
t('图片超过 5 MB,请压缩后重试。'),
|
|
117
118
|
);
|
|
118
119
|
}
|
|
119
120
|
chunks.push(data);
|
|
@@ -221,7 +222,7 @@ async function feishuImageDownloadError(error, signal) {
|
|
|
221
222
|
return new ImagePromptError(
|
|
222
223
|
'feishu-image-permission-required',
|
|
223
224
|
'Feishu image download requires the im:message:readonly tenant scope',
|
|
224
|
-
FEISHU_IMAGE_PERMISSION_MESSAGE,
|
|
225
|
+
t(FEISHU_IMAGE_PERMISSION_MESSAGE),
|
|
225
226
|
{ cause: error },
|
|
226
227
|
);
|
|
227
228
|
}
|
|
@@ -249,7 +250,7 @@ function feishuImageSource(event, client, key) {
|
|
|
249
250
|
throw new ImagePromptError(
|
|
250
251
|
'image-too-large',
|
|
251
252
|
`Feishu image declares ${size} bytes; the limit is ${maxBytes}`,
|
|
252
|
-
'图片超过 5 MB,请压缩后重试。',
|
|
253
|
+
t('图片超过 5 MB,请压缩后重试。'),
|
|
253
254
|
);
|
|
254
255
|
}
|
|
255
256
|
return readBoundedStream(resource?.getReadableStream?.(), { signal, maxBytes });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
2
|
|
|
3
|
+
import { t } from '../shared/i18n.mjs';
|
|
3
4
|
import { harnessApprovalText } from '../shared/harness-approval.mjs';
|
|
4
5
|
import {
|
|
5
6
|
harnessAnswerForQuestion,
|
|
@@ -21,23 +22,23 @@ function clean(value, max = 8_000) {
|
|
|
21
22
|
|
|
22
23
|
function safeFailure(error) {
|
|
23
24
|
if (error?.code === 'office-job-alias-invalid') return error.message;
|
|
24
|
-
if (error?.code === 'turn-stopped') return '本机 Harness 已停止这次执行。';
|
|
25
|
-
if (error?.code === 'office-job-conflict') return 'Office Job 已被领取、取消或结束。';
|
|
26
|
-
return '本机 Harness 未能完成任务;请检查 Harness 会话后重试。';
|
|
25
|
+
if (error?.code === 'turn-stopped') return t('本机 Harness 已停止这次执行。');
|
|
26
|
+
if (error?.code === 'office-job-conflict') return t('Office Job 已被领取、取消或结束。');
|
|
27
|
+
return t('本机 Harness 未能完成任务;请检查 Harness 会话后重试。');
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function renderPrompt(job, preset) {
|
|
30
31
|
return [
|
|
31
32
|
'# AI Office Handoff',
|
|
32
33
|
'',
|
|
33
|
-
'你正在本机 DeepSeek Harness 中继续一个来自 AI Office 的任务。',
|
|
34
|
-
'只在当前 Workspace 内行动。完成后必须返回:结果摘要、改动文件、验证证据、未解决风险。',
|
|
34
|
+
t('你正在本机 DeepSeek Harness 中继续一个来自 AI Office 的任务。'),
|
|
35
|
+
t('只在当前 Workspace 内行动。完成后必须返回:结果摘要、改动文件、验证证据、未解决风险。'),
|
|
35
36
|
'',
|
|
36
|
-
'## 本机 Instruction Preset',
|
|
37
|
+
t('## 本机 Instruction Preset'),
|
|
37
38
|
preset,
|
|
38
39
|
'',
|
|
39
|
-
...(clean(job.instruction) ? ['## 本轮补充指令', clean(job.instruction), ''] : []),
|
|
40
|
-
'## Office 时间线',
|
|
40
|
+
...(clean(job.instruction) ? [t('## 本轮补充指令'), clean(job.instruction), ''] : []),
|
|
41
|
+
t('## Office 时间线'),
|
|
41
42
|
clean(job.markdown, 200_000),
|
|
42
43
|
].join('\n');
|
|
43
44
|
}
|
|
@@ -54,7 +55,7 @@ function approvalRequest(interaction) {
|
|
|
54
55
|
return {
|
|
55
56
|
id,
|
|
56
57
|
kind: 'approval',
|
|
57
|
-
title:
|
|
58
|
+
title: t('批准 {tool}', { tool: clean(interaction.payload?.toolName, 120) || t('Harness 工具') }),
|
|
58
59
|
prompt,
|
|
59
60
|
toolName: clean(interaction.payload?.toolName, 120),
|
|
60
61
|
};
|
|
@@ -66,7 +67,7 @@ function approvalRequest(interaction) {
|
|
|
66
67
|
return {
|
|
67
68
|
id,
|
|
68
69
|
kind: 'question',
|
|
69
|
-
title: questions.length > 1 ?
|
|
70
|
+
title: questions.length > 1 ? t('Harness 需要 {count} 项补充信息', { count: questions.length }) : clean(questions[0].header || questions[0].question, 160),
|
|
70
71
|
prompt: questions.map((question, index) => harnessQuestionText(question, index, questions.length)).join('\n\n---\n\n'),
|
|
71
72
|
questions,
|
|
72
73
|
};
|
|
@@ -216,14 +217,14 @@ export class OfficeJobExecutor {
|
|
|
216
217
|
const workspace = this.#config.workspaces[job.workspaceAlias];
|
|
217
218
|
const preset = this.#config.instructionPresets[job.instructionPreset];
|
|
218
219
|
if (!workspace || !preset) {
|
|
219
|
-
const error = new Error('Office Job 引用了本机未配置的 Workspace/Preset alias。');
|
|
220
|
+
const error = new Error(t('Office Job 引用了本机未配置的 Workspace/Preset alias。'));
|
|
220
221
|
error.code = 'office-job-alias-invalid';
|
|
221
222
|
throw error;
|
|
222
223
|
}
|
|
223
224
|
entry.harness = this.#createHarness({ workspace });
|
|
224
|
-
await this.#progress(jobId, entry, { kind: 'status', message:
|
|
225
|
+
await this.#progress(jobId, entry, { kind: 'status', message: t('已领取 Job,准备 Workspace alias:{alias}', { alias: job.workspaceAlias }) }, true);
|
|
225
226
|
entry.sessionId = await entry.harness.createSession({ signal, workspace });
|
|
226
|
-
await this.#progress(jobId, entry, { kind: 'status', message: 'Harness Session 已创建。', sessionId: entry.sessionId }, true);
|
|
227
|
+
await this.#progress(jobId, entry, { kind: 'status', message: t('Harness Session 已创建。'), sessionId: entry.sessionId }, true);
|
|
227
228
|
const answer = await entry.harness.ask(entry.sessionId, renderPrompt(job, preset), {
|
|
228
229
|
timeoutMs: 30 * 60_000,
|
|
229
230
|
signal,
|
|
@@ -300,7 +301,7 @@ export class OfficeJobExecutor {
|
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
async #handleUpdate(jobId, entry, update) {
|
|
303
|
-
const message = update?.type === 'tool' ?
|
|
304
|
+
const message = update?.type === 'tool' ? t('正在使用 {tool}…', { tool: clean(update.name, 160) || t('Harness 工具') })
|
|
304
305
|
: clean(update?.text, 4_000);
|
|
305
306
|
if (!message) return;
|
|
306
307
|
const key = `${update.type}:${message}`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
2
|
|
|
3
|
+
import { t } from '../shared/i18n.mjs';
|
|
3
4
|
import { OfficeTransport } from './office-transport.mjs';
|
|
4
5
|
import { OFFICE_PROTOCOL_VERSION } from './protocol.mjs';
|
|
5
6
|
import { OfficeJobExecutor } from './office-job-executor.mjs';
|
|
@@ -9,12 +10,12 @@ const RETRY_DELAYS = Object.freeze([1_000, 3_000, 10_000, 30_000]);
|
|
|
9
10
|
function safeConnectionError(error) {
|
|
10
11
|
const code = typeof error?.code === 'string' ? error.code : 'office-connection-failed';
|
|
11
12
|
const messages = {
|
|
12
|
-
'invalid-device-token': 'AI Office 拒绝了 Device Token。',
|
|
13
|
-
'office-hook-unavailable': 'AI Office Connector Hook 尚未就绪。',
|
|
14
|
-
'office-protocol-mismatch': 'AI Office Connector 协议版本不兼容。',
|
|
15
|
-
'office-transport-failed': '本机暂时无法访问 AI Office。',
|
|
13
|
+
'invalid-device-token': t('AI Office 拒绝了 Device Token。'),
|
|
14
|
+
'office-hook-unavailable': t('AI Office Connector Hook 尚未就绪。'),
|
|
15
|
+
'office-protocol-mismatch': t('AI Office Connector 协议版本不兼容。'),
|
|
16
|
+
'office-transport-failed': t('本机暂时无法访问 AI Office。'),
|
|
16
17
|
};
|
|
17
|
-
return { code, message: messages[code] ?? 'AI Office 连接已中断。' };
|
|
18
|
+
return { code, message: messages[code] ?? t('AI Office 连接已中断。') };
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export class OfficeRuntime {
|
|
@@ -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
|
|
|
7
9
|
function cleanString(value) {
|
|
@@ -30,7 +32,7 @@ export function deriveQqBotIdentity(appId) {
|
|
|
30
32
|
|
|
31
33
|
export function maskQqAppId(appId) {
|
|
32
34
|
const value = cleanString(appId) ?? '';
|
|
33
|
-
if (value.length <= 10) return value ? `${value.slice(0, 3)}•••` : 'QQ机器人';
|
|
35
|
+
if (value.length <= 10) return value ? `${value.slice(0, 3)}•••` : t('QQ机器人');
|
|
34
36
|
return `${value.slice(0, 6)}••••${value.slice(-4)}`;
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// QQ markdown 回复投递:长文尽量按结构边界切分,以 msg_type=2 发送,
|
|
2
2
|
// 平台拒绝 markdown 时逐条回退纯文本。
|
|
3
3
|
|
|
4
|
+
import { t } from '../shared/i18n.mjs';
|
|
5
|
+
|
|
4
6
|
const DEFAULT_CHUNK_LIMIT = 4_500;
|
|
5
7
|
const CODE_FENCE_OPEN = /^```/;
|
|
6
8
|
const GFM_TABLE_LINE = /^\|.+\|$/;
|
|
7
9
|
const PASSIVE_REPLY_LIMIT = Object.freeze({ c2c: 4, group: 5 });
|
|
8
|
-
const PARTIAL_REPLY_NOTICE = '回答较长,后续内容未能通过 QQ 完整发送,请回复“继续”。';
|
|
10
|
+
const PARTIAL_REPLY_NOTICE = () => t('回答较长,后续内容未能通过 QQ 完整发送,请回复“继续”。');
|
|
9
11
|
|
|
10
12
|
function safeSliceIndex(value, limit) {
|
|
11
13
|
let index = Math.min(limit, value.length);
|
|
@@ -141,7 +143,7 @@ export async function sendMarkdownReply(bot, target, text, { logger } = {}) {
|
|
|
141
143
|
if (partialNoticeSent || !target?.msgId) return;
|
|
142
144
|
partialNoticeSent = true;
|
|
143
145
|
try {
|
|
144
|
-
results.push(await bot.sendText(target, PARTIAL_REPLY_NOTICE));
|
|
146
|
+
results.push(await bot.sendText(target, PARTIAL_REPLY_NOTICE()));
|
|
145
147
|
} catch (error) {
|
|
146
148
|
logger?.warn?.('[dsh-im:qq] unable to send partial reply notice:', error);
|
|
147
149
|
}
|