@xmanrui/dsh-im 0.8.0 → 0.10.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 +8 -4
- package/README.md +8 -4
- package/lib/client.js +16 -14
- package/lib/index.js +123 -121
- package/package.json +1 -1
- package/plugin-src/client/styles.js +15 -14
- package/plugin-src/host/channels/dingtalk/index.mjs +1 -1
- package/plugin-src/host/channels/dingtalk/production.mjs +3 -0
- package/plugin-src/host/channels/discord/index.mjs +1 -1
- package/plugin-src/host/channels/feishu/index.mjs +1 -1
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/qq/index.mjs +1 -1
- package/plugin-src/host/channels/qq/production.mjs +3 -0
- package/plugin-src/host/channels/shared/production.mjs +3 -0
- package/plugin-src/host/channels/slack/index.mjs +1 -1
- package/plugin-src/host/channels/slack/production.mjs +3 -0
- package/plugin-src/host/channels/telegram/index.mjs +1 -1
- package/plugin-src/host/channels/wecom/index.mjs +1 -1
- package/plugin-src/host/channels/wecom/production.mjs +3 -0
- package/plugin-src/host/channels/weixin/index.mjs +1 -1
- package/plugin-src/host/channels/weixin/production.mjs +3 -0
- package/plugin-src/host/channels/whatsapp/index.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +3 -0
- package/plugin-src/host/harness-command-executor.mjs +21 -0
- package/plugin-src/host/index.mjs +1 -1
- package/src/channels/dingtalk/dingtalk-api.mjs +82 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +162 -13
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/discord/discord-runtime.mjs +44 -1
- package/src/channels/feishu/bridge.mjs +45 -11
- package/src/channels/feishu/message-utils.mjs +142 -8
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +107 -13
- package/src/channels/shared/bot-workspace-store.mjs +13 -0
- package/src/channels/shared/compact-command.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +32 -2
- package/src/channels/shared/image-prompt.mjs +268 -0
- package/src/channels/shared/text-harness-bridge.mjs +49 -9
- package/src/channels/shared/workspace-session.mjs +2 -1
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +95 -0
- package/src/channels/slack/slack-runtime.mjs +24 -3
- package/src/channels/telegram/telegram-api.mjs +37 -0
- package/src/channels/telegram/telegram-runtime.mjs +71 -9
- package/src/channels/wecom/wecom-bridge.mjs +163 -16
- package/src/channels/weixin/weixin-api.mjs +98 -2
- package/src/channels/weixin/weixin-bridge.mjs +55 -12
- package/src/channels/whatsapp/whatsapp-runtime.mjs +144 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
2
|
+
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
2
3
|
import {
|
|
3
4
|
harnessAnswerForQuestion,
|
|
4
5
|
harnessQuestionText,
|
|
@@ -6,14 +7,32 @@ import {
|
|
|
6
7
|
} from '../shared/harness-question.mjs';
|
|
7
8
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
8
9
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
10
|
+
import {
|
|
11
|
+
fetchImageBuffer,
|
|
12
|
+
hasInboundImages,
|
|
13
|
+
imagePromptUserMessage,
|
|
14
|
+
promptContentForMessage,
|
|
15
|
+
} from '../shared/image-prompt.mjs';
|
|
9
16
|
|
|
10
17
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
11
18
|
|
|
19
|
+
export const QQ_IMAGE_HOSTS = Object.freeze([
|
|
20
|
+
'.myqcloud.com',
|
|
21
|
+
'.qpic.cn',
|
|
22
|
+
'.qq.com',
|
|
23
|
+
'.qq.com.cn',
|
|
24
|
+
'.tencentcos.com',
|
|
25
|
+
'.ugcimg.cn',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const QQ_IMAGE_FILENAME = /\.(?:gif|jpe?g|png|webp)$/i;
|
|
29
|
+
|
|
12
30
|
const HELP_TEXT = [
|
|
13
31
|
'QQ 机器人已连接 DeepSeek Harness。',
|
|
14
32
|
'',
|
|
15
|
-
'
|
|
33
|
+
'直接发送文字或图片即可继续当前会话。',
|
|
16
34
|
'/new 开启一个全新会话',
|
|
35
|
+
'/compact 压缩当前会话的较早上下文',
|
|
17
36
|
'/workspace 工作区绝对路径 切换工作区',
|
|
18
37
|
'/workspacelist 列出工作区绝对路径',
|
|
19
38
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
@@ -30,6 +49,51 @@ function safeText(message) {
|
|
|
30
49
|
return typeof message?.content === 'string' ? message.content.trim() : '';
|
|
31
50
|
}
|
|
32
51
|
|
|
52
|
+
function attachmentMediaType(attachment) {
|
|
53
|
+
const value = nonEmptyString(attachment?.content_type ?? attachment?.contentType);
|
|
54
|
+
if (!value) return null;
|
|
55
|
+
return value.split(';', 1)[0].trim().toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isQqImageAttachment(attachment) {
|
|
59
|
+
const mediaType = attachmentMediaType(attachment);
|
|
60
|
+
return mediaType?.startsWith('image/') === true
|
|
61
|
+
|| QQ_IMAGE_FILENAME.test(nonEmptyString(attachment?.filename) ?? '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasQqImageAttachments(message) {
|
|
65
|
+
return Array.isArray(message?.attachments)
|
|
66
|
+
&& message.attachments.some(isQqImageAttachment);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Convert QQ's attachment metadata into lazily downloaded image references. */
|
|
70
|
+
export function qqInboundMessage(message, { fetchImpl = fetch } = {}) {
|
|
71
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('fetchImpl must be a function');
|
|
72
|
+
const images = [];
|
|
73
|
+
for (const attachment of message?.attachments ?? []) {
|
|
74
|
+
if (!isQqImageAttachment(attachment)) continue;
|
|
75
|
+
const url = nonEmptyString(attachment?.url);
|
|
76
|
+
const name = nonEmptyString(attachment?.filename) ?? undefined;
|
|
77
|
+
const mediaType = attachmentMediaType(attachment);
|
|
78
|
+
const declaredSize = Number(attachment?.size);
|
|
79
|
+
images.push({
|
|
80
|
+
...(name ? { name } : {}),
|
|
81
|
+
...(mediaType?.startsWith('image/') ? { mediaType } : {}),
|
|
82
|
+
...(Number.isFinite(declaredSize) && declaredSize >= 0 ? { size: declaredSize } : {}),
|
|
83
|
+
load: ({ signal, maxBytes }) => {
|
|
84
|
+
if (!url) throw new Error('QQ image attachment has no download URL');
|
|
85
|
+
return fetchImageBuffer(url, {
|
|
86
|
+
fetchImpl,
|
|
87
|
+
signal,
|
|
88
|
+
maxBytes,
|
|
89
|
+
allowedHosts: QQ_IMAGE_HOSTS,
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return { content: safeText(message), images };
|
|
95
|
+
}
|
|
96
|
+
|
|
33
97
|
function nonEmptyString(value) {
|
|
34
98
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
35
99
|
}
|
|
@@ -38,6 +102,7 @@ function canClaimInteractionReply(message, pending) {
|
|
|
38
102
|
return pending.questions[pending.index]
|
|
39
103
|
&& nonEmptyString(message?.senderId) === pending.actor
|
|
40
104
|
&& (message.kind !== 'group' || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE')
|
|
105
|
+
&& !hasQqImageAttachments(message)
|
|
41
106
|
&& nonEmptyString(safeText(message));
|
|
42
107
|
}
|
|
43
108
|
|
|
@@ -62,6 +127,7 @@ export class QqHarnessBridge {
|
|
|
62
127
|
#logger;
|
|
63
128
|
#replyTimeoutMs;
|
|
64
129
|
#signal;
|
|
130
|
+
#fetchImpl;
|
|
65
131
|
#queues = new Map();
|
|
66
132
|
#pendingInteractions = new Map();
|
|
67
133
|
#interactionKeys = new Map();
|
|
@@ -78,10 +144,12 @@ export class QqHarnessBridge {
|
|
|
78
144
|
logger = console,
|
|
79
145
|
replyTimeoutMs = 600_000,
|
|
80
146
|
signal,
|
|
147
|
+
fetchImpl = fetch,
|
|
81
148
|
}) {
|
|
82
149
|
if (!bot || typeof bot.sendText !== 'function') throw new TypeError('QQ bot client is required');
|
|
83
150
|
if (!ownerUserOpenid) throw new TypeError('QQ scanner identity is required');
|
|
84
151
|
if (!harness || !state) throw new TypeError('Harness client and state store are required');
|
|
152
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('fetchImpl must be a function');
|
|
85
153
|
this.#bot = bot;
|
|
86
154
|
this.#ownerUserOpenid = ownerUserOpenid;
|
|
87
155
|
this.#harness = harness;
|
|
@@ -90,6 +158,7 @@ export class QqHarnessBridge {
|
|
|
90
158
|
this.#logger = logger;
|
|
91
159
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
92
160
|
this.#signal = signal;
|
|
161
|
+
this.#fetchImpl = fetchImpl;
|
|
93
162
|
this.#approvals = new HarnessApprovalQueue({ label: 'qq', logger });
|
|
94
163
|
}
|
|
95
164
|
|
|
@@ -112,7 +181,7 @@ export class QqHarnessBridge {
|
|
|
112
181
|
key,
|
|
113
182
|
actor: sender,
|
|
114
183
|
messageId,
|
|
115
|
-
text: safeText(message),
|
|
184
|
+
text: hasQqImageAttachments(message) ? '' : safeText(message),
|
|
116
185
|
addressed: message.kind !== 'group' || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE',
|
|
117
186
|
hasPendingQuestion: Boolean(pending),
|
|
118
187
|
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
@@ -208,32 +277,37 @@ export class QqHarnessBridge {
|
|
|
208
277
|
if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
|
|
209
278
|
|
|
210
279
|
const target = message.replyTarget;
|
|
211
|
-
const
|
|
280
|
+
const promptMessage = qqInboundMessage(message, { fetchImpl: this.#fetchImpl });
|
|
281
|
+
const text = promptMessage.content;
|
|
282
|
+
const hasImages = hasInboundImages(promptMessage);
|
|
283
|
+
let stream = null;
|
|
212
284
|
try {
|
|
213
|
-
if (!text) {
|
|
214
|
-
await this.#bot.sendText(target, '
|
|
285
|
+
if (!text && !hasImages) {
|
|
286
|
+
await this.#bot.sendText(target, '目前支持文字和图片消息。');
|
|
215
287
|
await this.#state.markSeen(messageId);
|
|
216
288
|
return;
|
|
217
289
|
}
|
|
218
290
|
const command = text.toLowerCase();
|
|
219
|
-
if (command === '/help') {
|
|
291
|
+
if (!hasImages && command === '/help') {
|
|
220
292
|
await this.#bot.sendText(target, HELP_TEXT);
|
|
221
293
|
await this.#state.markSeen(messageId);
|
|
222
294
|
return;
|
|
223
295
|
}
|
|
224
|
-
if (command === '/status') {
|
|
296
|
+
if (!hasImages && command === '/status') {
|
|
225
297
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
226
298
|
await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
|
|
227
299
|
await this.#state.markSeen(messageId);
|
|
228
300
|
return;
|
|
229
301
|
}
|
|
230
|
-
if (command === '/new') {
|
|
302
|
+
if (!hasImages && command === '/new') {
|
|
231
303
|
await this.#state.clearSession(key);
|
|
232
304
|
await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
|
|
233
305
|
await this.#state.markSeen(messageId);
|
|
234
306
|
return;
|
|
235
307
|
}
|
|
236
|
-
const workspaceCommand =
|
|
308
|
+
const workspaceCommand = hasImages
|
|
309
|
+
? null
|
|
310
|
+
: await runWorkspaceCommand(text, this.#harness, key);
|
|
237
311
|
if (workspaceCommand) {
|
|
238
312
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
239
313
|
await this.#bot.sendText(target, reply);
|
|
@@ -241,8 +315,24 @@ export class QqHarnessBridge {
|
|
|
241
315
|
await this.#state.markSeen(messageId);
|
|
242
316
|
return;
|
|
243
317
|
}
|
|
318
|
+
const compactCommand = hasImages
|
|
319
|
+
? null
|
|
320
|
+
: await runCompactCommand(
|
|
321
|
+
text,
|
|
322
|
+
this.#harness,
|
|
323
|
+
this.#state,
|
|
324
|
+
key,
|
|
325
|
+
{ signal: this.#signal },
|
|
326
|
+
);
|
|
327
|
+
if (compactCommand) {
|
|
328
|
+
await this.#bot.sendText(target, compactCommand.message);
|
|
329
|
+
await this.#state.markSeen(messageId);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
244
332
|
|
|
245
|
-
|
|
333
|
+
const content = hasImages
|
|
334
|
+
? await promptContentForMessage(promptMessage, { signal: this.#signal })
|
|
335
|
+
: undefined;
|
|
246
336
|
let streamFinished = false;
|
|
247
337
|
if (message.kind === 'c2c' && target?.msgId && typeof this.#bot.openStream === 'function') {
|
|
248
338
|
try {
|
|
@@ -257,7 +347,7 @@ export class QqHarnessBridge {
|
|
|
257
347
|
harness: this.#harness,
|
|
258
348
|
state: this.#state,
|
|
259
349
|
key,
|
|
260
|
-
text,
|
|
350
|
+
...(hasImages ? { content } : { text }),
|
|
261
351
|
createOptions: { signal: this.#signal },
|
|
262
352
|
existsOptions: { signal: this.#signal },
|
|
263
353
|
askOptions: {
|
|
@@ -302,11 +392,15 @@ export class QqHarnessBridge {
|
|
|
302
392
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
303
393
|
this.#status.lastError = null;
|
|
304
394
|
} catch (error) {
|
|
395
|
+
stream?.cancel?.();
|
|
305
396
|
if (this.#signal?.aborted) return;
|
|
306
397
|
this.#status.lastError = error?.message ?? String(error);
|
|
307
398
|
this.#logger.error?.('[dsh-im:qq] failed to process an inbound message:', error);
|
|
308
399
|
try {
|
|
309
|
-
await this.#bot.sendText(
|
|
400
|
+
await this.#bot.sendText(
|
|
401
|
+
target,
|
|
402
|
+
imagePromptUserMessage(error) ?? '消息处理失败,请稍后重试。',
|
|
403
|
+
);
|
|
310
404
|
await this.#state.markSeen(messageId);
|
|
311
405
|
} catch (sendError) {
|
|
312
406
|
this.#logger.error?.('[dsh-im:qq] failed to send the safe error reply:', sendError);
|
|
@@ -331,7 +425,7 @@ export class QqHarnessBridge {
|
|
|
331
425
|
|
|
332
426
|
if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
|
|
333
427
|
const text = nonEmptyString(safeText(message));
|
|
334
|
-
if (!text) {
|
|
428
|
+
if (!text || hasQqImageAttachments(message)) {
|
|
335
429
|
await this.#bot.sendText(message.replyTarget, '请用文字回答当前问题。');
|
|
336
430
|
return;
|
|
337
431
|
}
|
|
@@ -668,6 +668,19 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|
|
668
668
|
return target.ask(sessionId, ...args);
|
|
669
669
|
};
|
|
670
670
|
}
|
|
671
|
+
if (property === 'executeCommand' && typeof target.executeCommand === 'function') {
|
|
672
|
+
return (sessionId, ...args) => {
|
|
673
|
+
const generation = sessionGenerations.get(sessionId);
|
|
674
|
+
sessionGenerations.delete(sessionId);
|
|
675
|
+
if (!isCurrentScope()
|
|
676
|
+
|| (generation !== undefined && generation !== workspaces.generationFor(botId))) {
|
|
677
|
+
const error = new Error('The bot workspace changed before this command started.');
|
|
678
|
+
error.code = WORKSPACE_SESSION_STALE;
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
return target.executeCommand(sessionId, ...args);
|
|
682
|
+
};
|
|
683
|
+
}
|
|
671
684
|
const value = Reflect.get(target, property, target);
|
|
672
685
|
return typeof value === 'function' ? value.bind(target) : value;
|
|
673
686
|
},
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
2
|
+
|
|
3
|
+
const COMPACT_COMMAND = /^\/compact(?=$|\s)([\s\S]*)$/i;
|
|
4
|
+
const COMPACT_USAGE = '用法:/compact(不带参数)';
|
|
5
|
+
|
|
6
|
+
const COMPACT_RESULT_TEXT = new Map([
|
|
7
|
+
['No compactable history yet.', '暂无可压缩的历史记录。'],
|
|
8
|
+
[
|
|
9
|
+
'Compaction is unavailable because this process has an active compaction, or the agent is not idle.',
|
|
10
|
+
'当前会话正在生成回复或执行压缩,请稍后重试。',
|
|
11
|
+
],
|
|
12
|
+
['Compaction cancelled.', '上下文压缩已取消。'],
|
|
13
|
+
[
|
|
14
|
+
'The history selected for compaction changed before it could be replaced. The conversation is unchanged; the attempt is recorded in the session log.',
|
|
15
|
+
'压缩期间会话历史发生变化,本次未修改会话,请重试。',
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
'Compaction could not produce a useful summary. The conversation is unchanged; the attempt is recorded in the session log.',
|
|
19
|
+
'未能生成有效的压缩摘要,本次未修改会话。',
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
'Compaction did not finish cleanly; some session history may have changed. Inspect the current session state before retrying.',
|
|
23
|
+
'上下文压缩未正常完成,部分会话历史可能已变化,请检查会话后再重试。',
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
'Compaction finished, but the session could not be saved.',
|
|
27
|
+
'上下文已压缩,但会话保存失败。',
|
|
28
|
+
],
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function commandResult(message) {
|
|
32
|
+
return { handled: true, message, messages: [message] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function compactResultText(result) {
|
|
36
|
+
if (!result || typeof result !== 'object'
|
|
37
|
+
|| !['success', 'error'].includes(result.kind)
|
|
38
|
+
|| (result.text !== undefined && typeof result.text !== 'string')) {
|
|
39
|
+
throw new TypeError('Harness returned an invalid /compact result');
|
|
40
|
+
}
|
|
41
|
+
const text = result.text?.trim() ?? '';
|
|
42
|
+
const compacted = /^Compacted (\d+) history items \(~(\d+) tokens\)\.$/u.exec(text);
|
|
43
|
+
if (compacted) {
|
|
44
|
+
return `已压缩 ${compacted[1]} 条历史记录(约 ${compacted[2]} 个 token)。`;
|
|
45
|
+
}
|
|
46
|
+
if (COMPACT_RESULT_TEXT.has(text)) return COMPACT_RESULT_TEXT.get(text);
|
|
47
|
+
if (text) return text;
|
|
48
|
+
return result.kind === 'success' ? '上下文压缩完成。' : '上下文压缩失败。';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function compactErrorMessage(error) {
|
|
52
|
+
const code = error?.code ?? error?.failure?.code;
|
|
53
|
+
if (code === 'session-not-found') {
|
|
54
|
+
return '当前聊天绑定的会话已不存在,请发送新消息开启会话。';
|
|
55
|
+
}
|
|
56
|
+
if (code === 'agent-busy') return '当前会话正在生成回复,请稍后重试。';
|
|
57
|
+
if (code === 'cancelled' || error?.name === 'AbortError') return '上下文压缩已取消。';
|
|
58
|
+
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
|
|
59
|
+
return '工作区或机器人状态已发生变化,请重试。';
|
|
60
|
+
}
|
|
61
|
+
if (code === 'commands-unavailable') {
|
|
62
|
+
return '当前 Harness 暂不支持从机器人执行上下文压缩。';
|
|
63
|
+
}
|
|
64
|
+
return '上下文压缩失败,请稍后重试。';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Execute the explicit Harness compaction command for an existing IM conversation Session.
|
|
69
|
+
* Unknown input returns null so the caller may continue ordinary message routing.
|
|
70
|
+
*/
|
|
71
|
+
export async function runCompactCommand(text, harness, state, conversationKey, options = {}) {
|
|
72
|
+
if (typeof text !== 'string') return null;
|
|
73
|
+
const match = COMPACT_COMMAND.exec(text.trim());
|
|
74
|
+
if (!match) return null;
|
|
75
|
+
if (match[1].trim()) return commandResult(COMPACT_USAGE);
|
|
76
|
+
if (typeof state?.sessionFor !== 'function') {
|
|
77
|
+
return commandResult('当前机器人没有可用的会话状态。');
|
|
78
|
+
}
|
|
79
|
+
const sessionId = state.sessionFor(conversationKey);
|
|
80
|
+
if (typeof sessionId !== 'string' || !sessionId) {
|
|
81
|
+
return commandResult('当前聊天还没有可压缩的会话,请先发送一条消息。');
|
|
82
|
+
}
|
|
83
|
+
if (typeof harness?.executeCommand !== 'function') {
|
|
84
|
+
return commandResult('当前机器人暂不支持上下文压缩。');
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const execution = await harness.executeCommand(sessionId, '/compact', options);
|
|
88
|
+
if (execution === undefined) {
|
|
89
|
+
return commandResult('当前 Harness 未注册 /compact 命令,请确认上下文压缩组件已启用。');
|
|
90
|
+
}
|
|
91
|
+
return commandResult(compactResultText(execution?.result));
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return commandResult(compactErrorMessage(error));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -318,6 +318,7 @@ export class HarnessClient {
|
|
|
318
318
|
#interactionReconnectDelayMs;
|
|
319
319
|
#rpcIdPrefix;
|
|
320
320
|
#logPrefix;
|
|
321
|
+
#commandExecutor;
|
|
321
322
|
#managedProcess = null;
|
|
322
323
|
#interactionRegistry;
|
|
323
324
|
#interactionOwnerships;
|
|
@@ -334,6 +335,7 @@ export class HarnessClient {
|
|
|
334
335
|
interactionReconnectDelayMs = 500,
|
|
335
336
|
rpcIdPrefix = 'im',
|
|
336
337
|
logPrefix = 'dsh-im',
|
|
338
|
+
commandExecutor,
|
|
337
339
|
}) {
|
|
338
340
|
if (typeof createWebSocket !== 'function') {
|
|
339
341
|
throw new TypeError('createWebSocket must be a function');
|
|
@@ -347,6 +349,9 @@ export class HarnessClient {
|
|
|
347
349
|
if (typeof logPrefix !== 'string' || !logPrefix.trim()) {
|
|
348
350
|
throw new TypeError('logPrefix must be a non-empty string');
|
|
349
351
|
}
|
|
352
|
+
if (commandExecutor !== undefined && typeof commandExecutor !== 'function') {
|
|
353
|
+
throw new TypeError('commandExecutor must be a function');
|
|
354
|
+
}
|
|
350
355
|
this.#baseUrl = new URL(baseUrl);
|
|
351
356
|
this.#workspace = workspace;
|
|
352
357
|
this.#agentPreset = agentPreset;
|
|
@@ -357,6 +362,7 @@ export class HarnessClient {
|
|
|
357
362
|
this.#interactionReconnectDelayMs = interactionReconnectDelayMs;
|
|
358
363
|
this.#rpcIdPrefix = rpcIdPrefix.trim();
|
|
359
364
|
this.#logPrefix = logPrefix.trim();
|
|
365
|
+
this.#commandExecutor = commandExecutor;
|
|
360
366
|
this.#interactionRegistry = interactionRegistry(this.#baseUrl.origin);
|
|
361
367
|
this.#interactionOwnerships = this.#interactionRegistry.ownerships;
|
|
362
368
|
this.#interactionClaims = this.#interactionRegistry.claims;
|
|
@@ -459,6 +465,24 @@ export class HarnessClient {
|
|
|
459
465
|
return created.sessionId;
|
|
460
466
|
}
|
|
461
467
|
|
|
468
|
+
async executeCommand(sessionId, line, options = {}) {
|
|
469
|
+
if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('sessionId is required');
|
|
470
|
+
if (typeof line !== 'string' || !line) throw new TypeError('command line is required');
|
|
471
|
+
if (!this.#commandExecutor) {
|
|
472
|
+
const error = new Error('Harness command execution is unavailable');
|
|
473
|
+
error.code = 'commands-unavailable';
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
return await this.#commandExecutor(sessionId, line, options);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (error?.failure && typeof error.failure === 'object') {
|
|
480
|
+
throw new HarnessRpcError('commands.execute', error.failure);
|
|
481
|
+
}
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
462
486
|
async sessionExists(sessionId, options = {}) {
|
|
463
487
|
try {
|
|
464
488
|
await this.rpc('session.history', { sessionId, maxMessages: 1 }, 30_000, options);
|
|
@@ -599,7 +623,7 @@ export class HarnessClient {
|
|
|
599
623
|
return ownership ? { ownership, recovered: true } : null;
|
|
600
624
|
}
|
|
601
625
|
|
|
602
|
-
async ask(sessionId,
|
|
626
|
+
async ask(sessionId, prompt, options = {}) {
|
|
603
627
|
if (typeof options === 'number') options = { timeoutMs: options };
|
|
604
628
|
const timeoutMs = options.timeoutMs ?? 600_000;
|
|
605
629
|
const signal = options.signal;
|
|
@@ -669,10 +693,16 @@ export class HarnessClient {
|
|
|
669
693
|
]);
|
|
670
694
|
}
|
|
671
695
|
|
|
696
|
+
const content = typeof prompt === 'string'
|
|
697
|
+
? [{ type: 'text', text: prompt }]
|
|
698
|
+
: prompt;
|
|
699
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
700
|
+
throw new TypeError('Harness prompt content is required');
|
|
701
|
+
}
|
|
672
702
|
await this.rpc('session.prompt', {
|
|
673
703
|
sessionId,
|
|
674
704
|
mode: 'queue',
|
|
675
|
-
content
|
|
705
|
+
content,
|
|
676
706
|
clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
677
707
|
}, 30_000, { rpcId: promptRpcId, signal });
|
|
678
708
|
|