@xmanrui/dsh-im 0.9.0 → 0.11.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 +7 -3
- package/README.md +7 -3
- package/lib/client.js +378 -27
- package/lib/index.js +134 -131
- package/package.json +1 -1
- package/plugin-src/client/channels/dingtalk/api.js +19 -0
- package/plugin-src/client/channels/dingtalk/index.js +67 -10
- package/plugin-src/client/channels/feishu/index.js +35 -5
- package/plugin-src/client/channels/qq/api.js +21 -0
- package/plugin-src/client/channels/qq/index.js +39 -1
- package/plugin-src/client/channels/shared/token-channel.js +29 -3
- package/plugin-src/client/channels/wecom/api.js +11 -0
- package/plugin-src/client/channels/wecom/index.js +71 -1
- package/plugin-src/client/channels/weixin/api.js +11 -0
- package/plugin-src/client/channels/weixin/index.js +40 -4
- package/plugin-src/client/channels/whatsapp/index.js +41 -2
- package/plugin-src/client/i18n.js +13 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +1 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +29 -3
- package/plugin-src/host/channels/feishu/production.mjs +1 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +34 -3
- package/plugin-src/host/channels/qq/production.mjs +1 -1
- package/plugin-src/host/channels/qq/rpc.mjs +35 -2
- package/plugin-src/host/channels/shared/production.mjs +1 -1
- package/plugin-src/host/channels/shared/rpc.mjs +25 -1
- package/plugin-src/host/channels/slack/production.mjs +1 -1
- package/plugin-src/host/channels/slack/rpc.mjs +28 -2
- package/plugin-src/host/channels/wecom/production.mjs +1 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +31 -2
- package/plugin-src/host/channels/weixin/production.mjs +1 -1
- package/plugin-src/host/channels/weixin/rpc.mjs +29 -3
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/rpc.mjs +31 -3
- package/src/channels/dingtalk/dingtalk-api.mjs +82 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +160 -20
- package/src/channels/dingtalk/dingtalk-controller.mjs +18 -0
- package/src/channels/dingtalk/dingtalk-runtime.mjs +21 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/discord/discord-runtime.mjs +54 -1
- package/src/channels/feishu/bridge.mjs +45 -18
- package/src/channels/feishu/feishu-runtime.mjs +45 -0
- package/src/channels/feishu/message-utils.mjs +227 -6
- package/src/channels/feishu/multi-bot-controller.mjs +21 -0
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +107 -20
- package/src/channels/qq/qq-controller.mjs +18 -0
- package/src/channels/qq/qq-runtime.mjs +26 -0
- package/src/channels/shared/bot-workspace-store.mjs +2 -0
- package/src/channels/shared/connection-test.mjs +54 -0
- package/src/channels/shared/harness-client.mjs +14 -8
- package/src/channels/shared/image-prompt.mjs +268 -0
- package/src/channels/shared/text-harness-bridge.mjs +62 -16
- package/src/channels/shared/token-bot-controller.mjs +21 -0
- 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-controller.mjs +19 -0
- package/src/channels/slack/slack-runtime.mjs +34 -3
- package/src/channels/telegram/telegram-api.mjs +37 -0
- package/src/channels/telegram/telegram-runtime.mjs +81 -9
- package/src/channels/wecom/wecom-bridge.mjs +160 -23
- package/src/channels/wecom/wecom-controller.mjs +18 -0
- package/src/channels/wecom/wecom-runtime.mjs +18 -0
- package/src/channels/weixin/weixin-api.mjs +98 -2
- package/src/channels/weixin/weixin-bridge.mjs +52 -19
- package/src/channels/weixin/weixin-controller.mjs +18 -0
- package/src/channels/weixin/weixin-runtime.mjs +25 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +20 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +160 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
2
2
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
3
|
+
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
3
4
|
import {
|
|
4
5
|
harnessAnswerForQuestion,
|
|
5
6
|
harnessQuestionText,
|
|
@@ -7,13 +8,30 @@ import {
|
|
|
7
8
|
} from '../shared/harness-question.mjs';
|
|
8
9
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
9
10
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
11
|
+
import {
|
|
12
|
+
fetchImageBuffer,
|
|
13
|
+
hasInboundImages,
|
|
14
|
+
imagePromptUserMessage,
|
|
15
|
+
promptContentForMessage,
|
|
16
|
+
} from '../shared/image-prompt.mjs';
|
|
10
17
|
|
|
11
18
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
12
19
|
|
|
20
|
+
export const QQ_IMAGE_HOSTS = Object.freeze([
|
|
21
|
+
'.myqcloud.com',
|
|
22
|
+
'.qpic.cn',
|
|
23
|
+
'.qq.com',
|
|
24
|
+
'.qq.com.cn',
|
|
25
|
+
'.tencentcos.com',
|
|
26
|
+
'.ugcimg.cn',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const QQ_IMAGE_FILENAME = /\.(?:gif|jpe?g|png|webp)$/i;
|
|
30
|
+
|
|
13
31
|
const HELP_TEXT = [
|
|
14
32
|
'QQ 机器人已连接 DeepSeek Harness。',
|
|
15
33
|
'',
|
|
16
|
-
'
|
|
34
|
+
'直接发送文字或图片即可继续当前会话。',
|
|
17
35
|
'/new 开启一个全新会话',
|
|
18
36
|
'/compact 压缩当前会话的较早上下文',
|
|
19
37
|
'/workspace 工作区绝对路径 切换工作区',
|
|
@@ -32,6 +50,51 @@ function safeText(message) {
|
|
|
32
50
|
return typeof message?.content === 'string' ? message.content.trim() : '';
|
|
33
51
|
}
|
|
34
52
|
|
|
53
|
+
function attachmentMediaType(attachment) {
|
|
54
|
+
const value = nonEmptyString(attachment?.content_type ?? attachment?.contentType);
|
|
55
|
+
if (!value) return null;
|
|
56
|
+
return value.split(';', 1)[0].trim().toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isQqImageAttachment(attachment) {
|
|
60
|
+
const mediaType = attachmentMediaType(attachment);
|
|
61
|
+
return mediaType?.startsWith('image/') === true
|
|
62
|
+
|| QQ_IMAGE_FILENAME.test(nonEmptyString(attachment?.filename) ?? '');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hasQqImageAttachments(message) {
|
|
66
|
+
return Array.isArray(message?.attachments)
|
|
67
|
+
&& message.attachments.some(isQqImageAttachment);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Convert QQ's attachment metadata into lazily downloaded image references. */
|
|
71
|
+
export function qqInboundMessage(message, { fetchImpl = fetch } = {}) {
|
|
72
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('fetchImpl must be a function');
|
|
73
|
+
const images = [];
|
|
74
|
+
for (const attachment of message?.attachments ?? []) {
|
|
75
|
+
if (!isQqImageAttachment(attachment)) continue;
|
|
76
|
+
const url = nonEmptyString(attachment?.url);
|
|
77
|
+
const name = nonEmptyString(attachment?.filename) ?? undefined;
|
|
78
|
+
const mediaType = attachmentMediaType(attachment);
|
|
79
|
+
const declaredSize = Number(attachment?.size);
|
|
80
|
+
images.push({
|
|
81
|
+
...(name ? { name } : {}),
|
|
82
|
+
...(mediaType?.startsWith('image/') ? { mediaType } : {}),
|
|
83
|
+
...(Number.isFinite(declaredSize) && declaredSize >= 0 ? { size: declaredSize } : {}),
|
|
84
|
+
load: ({ signal, maxBytes }) => {
|
|
85
|
+
if (!url) throw new Error('QQ image attachment has no download URL');
|
|
86
|
+
return fetchImageBuffer(url, {
|
|
87
|
+
fetchImpl,
|
|
88
|
+
signal,
|
|
89
|
+
maxBytes,
|
|
90
|
+
allowedHosts: QQ_IMAGE_HOSTS,
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return { content: safeText(message), images };
|
|
96
|
+
}
|
|
97
|
+
|
|
35
98
|
function nonEmptyString(value) {
|
|
36
99
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
37
100
|
}
|
|
@@ -40,6 +103,7 @@ function canClaimInteractionReply(message, pending) {
|
|
|
40
103
|
return pending.questions[pending.index]
|
|
41
104
|
&& nonEmptyString(message?.senderId) === pending.actor
|
|
42
105
|
&& (message.kind !== 'group' || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE')
|
|
106
|
+
&& !hasQqImageAttachments(message)
|
|
43
107
|
&& nonEmptyString(safeText(message));
|
|
44
108
|
}
|
|
45
109
|
|
|
@@ -64,6 +128,7 @@ export class QqHarnessBridge {
|
|
|
64
128
|
#logger;
|
|
65
129
|
#replyTimeoutMs;
|
|
66
130
|
#signal;
|
|
131
|
+
#fetchImpl;
|
|
67
132
|
#queues = new Map();
|
|
68
133
|
#pendingInteractions = new Map();
|
|
69
134
|
#interactionKeys = new Map();
|
|
@@ -80,10 +145,12 @@ export class QqHarnessBridge {
|
|
|
80
145
|
logger = console,
|
|
81
146
|
replyTimeoutMs = 600_000,
|
|
82
147
|
signal,
|
|
148
|
+
fetchImpl = fetch,
|
|
83
149
|
}) {
|
|
84
150
|
if (!bot || typeof bot.sendText !== 'function') throw new TypeError('QQ bot client is required');
|
|
85
151
|
if (!ownerUserOpenid) throw new TypeError('QQ scanner identity is required');
|
|
86
152
|
if (!harness || !state) throw new TypeError('Harness client and state store are required');
|
|
153
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('fetchImpl must be a function');
|
|
87
154
|
this.#bot = bot;
|
|
88
155
|
this.#ownerUserOpenid = ownerUserOpenid;
|
|
89
156
|
this.#harness = harness;
|
|
@@ -92,6 +159,7 @@ export class QqHarnessBridge {
|
|
|
92
159
|
this.#logger = logger;
|
|
93
160
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
94
161
|
this.#signal = signal;
|
|
162
|
+
this.#fetchImpl = fetchImpl;
|
|
95
163
|
this.#approvals = new HarnessApprovalQueue({ label: 'qq', logger });
|
|
96
164
|
}
|
|
97
165
|
|
|
@@ -109,12 +177,18 @@ export class QqHarnessBridge {
|
|
|
109
177
|
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|
110
178
|
const key = conversationKey(message);
|
|
111
179
|
this.#acceptedMessageIds.add(messageId);
|
|
180
|
+
if (message.kind === 'c2c'
|
|
181
|
+
&& (this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid)
|
|
182
|
+
&& message.replyTarget?.scope === 'c2c'
|
|
183
|
+
&& nonEmptyString(message.replyTarget.targetId) === sender) {
|
|
184
|
+
rememberConnectionTestTarget(this.#state, message.replyTarget);
|
|
185
|
+
}
|
|
112
186
|
const pending = this.#pendingInteractions.get(key);
|
|
113
187
|
const approval = this.#approvals.claimReply({
|
|
114
188
|
key,
|
|
115
189
|
actor: sender,
|
|
116
190
|
messageId,
|
|
117
|
-
text: safeText(message),
|
|
191
|
+
text: hasQqImageAttachments(message) ? '' : safeText(message),
|
|
118
192
|
addressed: message.kind !== 'group' || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE',
|
|
119
193
|
hasPendingQuestion: Boolean(pending),
|
|
120
194
|
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
@@ -210,32 +284,37 @@ export class QqHarnessBridge {
|
|
|
210
284
|
if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
|
|
211
285
|
|
|
212
286
|
const target = message.replyTarget;
|
|
213
|
-
const
|
|
287
|
+
const promptMessage = qqInboundMessage(message, { fetchImpl: this.#fetchImpl });
|
|
288
|
+
const text = promptMessage.content;
|
|
289
|
+
const hasImages = hasInboundImages(promptMessage);
|
|
290
|
+
let stream = null;
|
|
214
291
|
try {
|
|
215
|
-
if (!text) {
|
|
216
|
-
await this.#bot.sendText(target, '
|
|
292
|
+
if (!text && !hasImages) {
|
|
293
|
+
await this.#bot.sendText(target, '目前支持文字和图片消息。');
|
|
217
294
|
await this.#state.markSeen(messageId);
|
|
218
295
|
return;
|
|
219
296
|
}
|
|
220
297
|
const command = text.toLowerCase();
|
|
221
|
-
if (command === '/help') {
|
|
298
|
+
if (!hasImages && command === '/help') {
|
|
222
299
|
await this.#bot.sendText(target, HELP_TEXT);
|
|
223
300
|
await this.#state.markSeen(messageId);
|
|
224
301
|
return;
|
|
225
302
|
}
|
|
226
|
-
if (command === '/status') {
|
|
303
|
+
if (!hasImages && command === '/status') {
|
|
227
304
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
228
305
|
await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
|
|
229
306
|
await this.#state.markSeen(messageId);
|
|
230
307
|
return;
|
|
231
308
|
}
|
|
232
|
-
if (command === '/new') {
|
|
309
|
+
if (!hasImages && command === '/new') {
|
|
233
310
|
await this.#state.clearSession(key);
|
|
234
311
|
await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
|
|
235
312
|
await this.#state.markSeen(messageId);
|
|
236
313
|
return;
|
|
237
314
|
}
|
|
238
|
-
const workspaceCommand =
|
|
315
|
+
const workspaceCommand = hasImages
|
|
316
|
+
? null
|
|
317
|
+
: await runWorkspaceCommand(text, this.#harness, key);
|
|
239
318
|
if (workspaceCommand) {
|
|
240
319
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
241
320
|
await this.#bot.sendText(target, reply);
|
|
@@ -243,20 +322,24 @@ export class QqHarnessBridge {
|
|
|
243
322
|
await this.#state.markSeen(messageId);
|
|
244
323
|
return;
|
|
245
324
|
}
|
|
246
|
-
const compactCommand =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
325
|
+
const compactCommand = hasImages
|
|
326
|
+
? null
|
|
327
|
+
: await runCompactCommand(
|
|
328
|
+
text,
|
|
329
|
+
this.#harness,
|
|
330
|
+
this.#state,
|
|
331
|
+
key,
|
|
332
|
+
{ signal: this.#signal },
|
|
333
|
+
);
|
|
253
334
|
if (compactCommand) {
|
|
254
335
|
await this.#bot.sendText(target, compactCommand.message);
|
|
255
336
|
await this.#state.markSeen(messageId);
|
|
256
337
|
return;
|
|
257
338
|
}
|
|
258
339
|
|
|
259
|
-
|
|
340
|
+
const content = hasImages
|
|
341
|
+
? await promptContentForMessage(promptMessage, { signal: this.#signal })
|
|
342
|
+
: undefined;
|
|
260
343
|
let streamFinished = false;
|
|
261
344
|
if (message.kind === 'c2c' && target?.msgId && typeof this.#bot.openStream === 'function') {
|
|
262
345
|
try {
|
|
@@ -271,7 +354,7 @@ export class QqHarnessBridge {
|
|
|
271
354
|
harness: this.#harness,
|
|
272
355
|
state: this.#state,
|
|
273
356
|
key,
|
|
274
|
-
text,
|
|
357
|
+
...(hasImages ? { content } : { text }),
|
|
275
358
|
createOptions: { signal: this.#signal },
|
|
276
359
|
existsOptions: { signal: this.#signal },
|
|
277
360
|
askOptions: {
|
|
@@ -316,11 +399,15 @@ export class QqHarnessBridge {
|
|
|
316
399
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
317
400
|
this.#status.lastError = null;
|
|
318
401
|
} catch (error) {
|
|
402
|
+
stream?.cancel?.();
|
|
319
403
|
if (this.#signal?.aborted) return;
|
|
320
404
|
this.#status.lastError = error?.message ?? String(error);
|
|
321
405
|
this.#logger.error?.('[dsh-im:qq] failed to process an inbound message:', error);
|
|
322
406
|
try {
|
|
323
|
-
await this.#bot.sendText(
|
|
407
|
+
await this.#bot.sendText(
|
|
408
|
+
target,
|
|
409
|
+
imagePromptUserMessage(error) ?? '消息处理失败,请稍后重试。',
|
|
410
|
+
);
|
|
324
411
|
await this.#state.markSeen(messageId);
|
|
325
412
|
} catch (sendError) {
|
|
326
413
|
this.#logger.error?.('[dsh-im:qq] failed to send the safe error reply:', sendError);
|
|
@@ -345,7 +432,7 @@ export class QqHarnessBridge {
|
|
|
345
432
|
|
|
346
433
|
if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
|
|
347
434
|
const text = nonEmptyString(safeText(message));
|
|
348
|
-
if (!text) {
|
|
435
|
+
if (!text || hasQqImageAttachments(message)) {
|
|
349
436
|
await this.#bot.sendText(message.replyTarget, '请用文字回答当前问题。');
|
|
350
437
|
return;
|
|
351
438
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
3
4
|
import { deriveQqBotIdentity, maskQqAppId } from './config-store.mjs';
|
|
4
5
|
|
|
5
6
|
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting']);
|
|
@@ -248,6 +249,23 @@ export class QqController {
|
|
|
248
249
|
return this.status();
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
async sendConnectionTest(botId) {
|
|
253
|
+
const config = this.#configStore.get(botId);
|
|
254
|
+
if (!config) throw new Error('Unknown QQ bot');
|
|
255
|
+
return this.#withBotTransition(botId, async () => {
|
|
256
|
+
const runtime = this.#runtimes.get(botId);
|
|
257
|
+
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
258
|
+
const error = new Error('QQ机器人尚未连接');
|
|
259
|
+
error.code = 'test-target-unavailable';
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
await runtime.sendConnectionTest(connectionTestMessage(
|
|
263
|
+
`QQ 机器人(${maskQqAppId(config.appId)})`,
|
|
264
|
+
));
|
|
265
|
+
return { sent: true };
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
251
269
|
async deleteBot(botId) {
|
|
252
270
|
const config = this.#configStore.get(botId);
|
|
253
271
|
if (!config) throw new Error('Unknown QQ bot');
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { QQBot, typingIndicator } from '@tencent-connect/qqbot-nodejs';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
connectionTestTarget,
|
|
5
|
+
connectionTestTargetUnavailable,
|
|
6
|
+
} from '../shared/connection-test.mjs';
|
|
3
7
|
import { createQqBridgeStatus, QqHarnessBridge } from './qq-bridge.mjs';
|
|
4
8
|
|
|
5
9
|
function timeoutError() {
|
|
@@ -67,6 +71,28 @@ export class QqRuntime {
|
|
|
67
71
|
return structuredClone(this.#status);
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
async sendConnectionTest(text) {
|
|
75
|
+
if (!this.#status.ready || !this.#bot) {
|
|
76
|
+
throw connectionTestTargetUnavailable('QQ机器人');
|
|
77
|
+
}
|
|
78
|
+
const ownerUserOpenid = typeof this.#config.ownerUserOpenid === 'string'
|
|
79
|
+
? this.#config.ownerUserOpenid.trim()
|
|
80
|
+
: '';
|
|
81
|
+
const remembered = connectionTestTarget(this.#state);
|
|
82
|
+
const rememberedUserOpenid = remembered?.scope === 'c2c'
|
|
83
|
+
&& typeof remembered.targetId === 'string'
|
|
84
|
+
? remembered.targetId.trim()
|
|
85
|
+
: '';
|
|
86
|
+
const target = rememberedUserOpenid
|
|
87
|
+
? { scope: 'c2c', targetId: rememberedUserOpenid }
|
|
88
|
+
: (ownerUserOpenid && ownerUserOpenid !== '*'
|
|
89
|
+
? { scope: 'c2c', targetId: ownerUserOpenid }
|
|
90
|
+
: null);
|
|
91
|
+
if (!target) throw connectionTestTargetUnavailable('QQ机器人');
|
|
92
|
+
await this.#bot.sendText(target, text);
|
|
93
|
+
return { sent: true };
|
|
94
|
+
}
|
|
95
|
+
|
|
70
96
|
async start() {
|
|
71
97
|
if (this.#status.ready && this.#bot) return this.status;
|
|
72
98
|
if (this.#starting) return this.#starting;
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from 'node:fs/promises';
|
|
10
10
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
11
11
|
|
|
12
|
+
import { CONNECTION_TEST_STATE_IDENTITY } from './connection-test.mjs';
|
|
12
13
|
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
13
14
|
|
|
14
15
|
const EMPTY_DOCUMENT = Object.freeze({ version: 1, workspaces: Object.freeze({}) });
|
|
@@ -687,6 +688,7 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|
|
687
688
|
});
|
|
688
689
|
const scopedState = new Proxy(state, {
|
|
689
690
|
get(target, property) {
|
|
691
|
+
if (property === CONNECTION_TEST_STATE_IDENTITY) return target;
|
|
690
692
|
if (property === 'sessionFor') {
|
|
691
693
|
return (key, ...args) => {
|
|
692
694
|
if (!isCurrentScope()) return null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const targets = new WeakMap();
|
|
2
|
+
|
|
3
|
+
export const CONNECTION_TEST_STATE_IDENTITY = Symbol('dsh-im.connection-test-state-identity');
|
|
4
|
+
|
|
5
|
+
function stateIdentity(state) {
|
|
6
|
+
return state?.[CONNECTION_TEST_STATE_IDENTITY] ?? state;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function cleanText(value) {
|
|
10
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function rememberConnectionTestTarget(state, target) {
|
|
14
|
+
if (!state || !target || typeof target !== 'object') return false;
|
|
15
|
+
try {
|
|
16
|
+
targets.set(stateIdentity(state), structuredClone(target));
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function connectionTestTarget(state) {
|
|
24
|
+
const target = state ? targets.get(stateIdentity(state)) : null;
|
|
25
|
+
return target ? structuredClone(target) : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function connectionTestMessage(botName, channelLabel = '机器人') {
|
|
29
|
+
const name = cleanText(botName) ?? channelLabel;
|
|
30
|
+
return `✅ DeepSeek Harness 连接测试成功\n这条消息由插件页面中的“${name}”机器人卡片发出。`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function connectionTestTargetUnavailable(channelLabel = '机器人') {
|
|
34
|
+
const error = new Error(`${channelLabel}尚未收到可用于测试的私聊消息。`);
|
|
35
|
+
error.code = 'test-target-unavailable';
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function sendRememberedConnectionTest({ state, send, text, channelLabel }) {
|
|
40
|
+
const target = connectionTestTarget(state);
|
|
41
|
+
if (!target) throw connectionTestTargetUnavailable(channelLabel);
|
|
42
|
+
await send(target, text);
|
|
43
|
+
return { sent: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function publicConnectionTestResult(error) {
|
|
47
|
+
if (!error) return Object.freeze({ sent: true });
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
sent: false,
|
|
50
|
+
code: error?.code === 'test-target-unavailable'
|
|
51
|
+
? 'test-target-unavailable'
|
|
52
|
+
: 'test-message-failed',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -327,7 +327,7 @@ export class HarnessClient {
|
|
|
327
327
|
constructor({
|
|
328
328
|
baseUrl,
|
|
329
329
|
workspace,
|
|
330
|
-
agentPreset
|
|
330
|
+
agentPreset,
|
|
331
331
|
autostart = false,
|
|
332
332
|
dshBin = 'dsh',
|
|
333
333
|
fetchImpl = fetch,
|
|
@@ -354,7 +354,8 @@ export class HarnessClient {
|
|
|
354
354
|
}
|
|
355
355
|
this.#baseUrl = new URL(baseUrl);
|
|
356
356
|
this.#workspace = workspace;
|
|
357
|
-
|
|
357
|
+
// Keep an omitted preset absent so session.create resolves the Host's current default.
|
|
358
|
+
this.#agentPreset = agentPreset ?? undefined;
|
|
358
359
|
this.#autostart = autostart;
|
|
359
360
|
this.#dshBin = dshBin;
|
|
360
361
|
this.#fetch = fetchImpl;
|
|
@@ -458,10 +459,9 @@ export class HarnessClient {
|
|
|
458
459
|
async createSession(options = {}) {
|
|
459
460
|
await this.ensureRunning(options);
|
|
460
461
|
const workspaceId = await this.workspaceId(options);
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}, 30_000, options);
|
|
462
|
+
const payload = { workspaceId };
|
|
463
|
+
if (this.#agentPreset !== undefined) payload.agentPreset = this.#agentPreset;
|
|
464
|
+
const created = await this.rpc('session.create', payload, 30_000, options);
|
|
465
465
|
return created.sessionId;
|
|
466
466
|
}
|
|
467
467
|
|
|
@@ -623,7 +623,7 @@ export class HarnessClient {
|
|
|
623
623
|
return ownership ? { ownership, recovered: true } : null;
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
-
async ask(sessionId,
|
|
626
|
+
async ask(sessionId, prompt, options = {}) {
|
|
627
627
|
if (typeof options === 'number') options = { timeoutMs: options };
|
|
628
628
|
const timeoutMs = options.timeoutMs ?? 600_000;
|
|
629
629
|
const signal = options.signal;
|
|
@@ -693,10 +693,16 @@ export class HarnessClient {
|
|
|
693
693
|
]);
|
|
694
694
|
}
|
|
695
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
|
+
}
|
|
696
702
|
await this.rpc('session.prompt', {
|
|
697
703
|
sessionId,
|
|
698
704
|
mode: 'queue',
|
|
699
|
-
content
|
|
705
|
+
content,
|
|
700
706
|
clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
701
707
|
}, 30_000, { rpcId: promptRpcId, signal });
|
|
702
708
|
|