@xmanrui/dsh-im 0.9.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 +5 -3
- package/README.md +5 -3
- package/lib/client.js +1 -0
- package/lib/index.js +121 -119
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-api.mjs +82 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +156 -20
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/discord/discord-runtime.mjs +44 -1
- package/src/channels/feishu/bridge.mjs +39 -18
- 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 +100 -20
- package/src/channels/shared/harness-client.mjs +8 -2
- package/src/channels/shared/image-prompt.mjs +268 -0
- package/src/channels/shared/text-harness-bridge.mjs +43 -16
- 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 +156 -23
- package/src/channels/weixin/weixin-api.mjs +98 -2
- package/src/channels/weixin/weixin-bridge.mjs +48 -19
- package/src/channels/whatsapp/whatsapp-runtime.mjs +144 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
|
2
|
+
|
|
1
3
|
export function conversationKey(event) {
|
|
2
4
|
const chatType = event?.message?.chat_type;
|
|
3
5
|
if (chatType === 'p2p') {
|
|
@@ -10,19 +12,151 @@ export function conversationKey(event) {
|
|
|
10
12
|
return `group:${chatId}`;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
function parsedMessageContent(event) {
|
|
16
|
+
const value = event?.message?.content;
|
|
17
|
+
if (value && typeof value === 'object') return value;
|
|
18
|
+
if (typeof value !== 'string') return null;
|
|
16
19
|
try {
|
|
17
|
-
parsed = JSON.parse(
|
|
20
|
+
const parsed = JSON.parse(value);
|
|
21
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
18
22
|
} catch {
|
|
19
23
|
return null;
|
|
20
24
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function withoutMentions(text, event) {
|
|
28
|
+
let result = typeof text === 'string' ? text : '';
|
|
29
|
+
for (const mention of event?.message?.mentions ?? []) {
|
|
30
|
+
if (typeof mention?.key === 'string' && mention.key) {
|
|
31
|
+
result = result.replaceAll(mention.key, '');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result.trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function extractText(event) {
|
|
38
|
+
if (event?.message?.message_type !== 'text') return null;
|
|
39
|
+
const parsed = parsedMessageContent(event);
|
|
40
|
+
return parsed ? withoutMentions(parsed.text, event) : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nonEmptyString(value) {
|
|
44
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function postContent(event, parsed = parsedMessageContent(event)) {
|
|
48
|
+
if (event?.message?.message_type !== 'post') return null;
|
|
49
|
+
if (!parsed) return null;
|
|
50
|
+
|
|
51
|
+
const lines = [];
|
|
52
|
+
const title = nonEmptyString(withoutMentions(parsed.title, event));
|
|
53
|
+
if (title) lines.push(title);
|
|
54
|
+
const imageKeys = [];
|
|
55
|
+
for (const paragraph of Array.isArray(parsed.content) ? parsed.content : []) {
|
|
56
|
+
if (!Array.isArray(paragraph)) continue;
|
|
57
|
+
let visibleText = '';
|
|
58
|
+
for (const element of paragraph) {
|
|
59
|
+
const tag = String(element?.tag ?? '').toLowerCase();
|
|
60
|
+
if (tag === 'img') {
|
|
61
|
+
const key = nonEmptyString(element?.image_key);
|
|
62
|
+
if (key) imageKeys.push(key);
|
|
63
|
+
} else if (tag === 'text' || tag === 'a' || tag === 'link') {
|
|
64
|
+
if (typeof element?.text === 'string') visibleText += element.text;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const line = nonEmptyString(withoutMentions(visibleText, event));
|
|
68
|
+
if (line) lines.push(line);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
text: lines.join('\n'),
|
|
73
|
+
imageKeys,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function headerValue(headers, name) {
|
|
78
|
+
if (typeof headers?.get === 'function') return headers.get(name);
|
|
79
|
+
return headers?.[name] ?? headers?.[name.toLowerCase()] ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function declaredSize(headers) {
|
|
83
|
+
const header = headerValue(headers, 'content-length');
|
|
84
|
+
if (header === null || header === undefined || header === '') return null;
|
|
85
|
+
const value = Number(header);
|
|
86
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function readBoundedStream(stream, { signal, maxBytes }) {
|
|
90
|
+
if (!stream || typeof stream[Symbol.asyncIterator] !== 'function') {
|
|
91
|
+
throw new Error('Feishu image download returned no readable stream');
|
|
24
92
|
}
|
|
25
|
-
|
|
93
|
+
signal?.throwIfAborted();
|
|
94
|
+
const abort = () => stream.destroy?.(
|
|
95
|
+
signal.reason ?? new DOMException('Feishu image download aborted', 'AbortError'),
|
|
96
|
+
);
|
|
97
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
98
|
+
const chunks = [];
|
|
99
|
+
let size = 0;
|
|
100
|
+
try {
|
|
101
|
+
for await (const chunk of stream) {
|
|
102
|
+
signal?.throwIfAborted();
|
|
103
|
+
const data = Buffer.from(chunk);
|
|
104
|
+
size += data.length;
|
|
105
|
+
if (size > maxBytes) {
|
|
106
|
+
stream.destroy?.();
|
|
107
|
+
throw new ImagePromptError(
|
|
108
|
+
'image-too-large',
|
|
109
|
+
`Feishu image exceeds ${maxBytes} bytes`,
|
|
110
|
+
'图片超过 5 MB,请压缩后重试。',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
chunks.push(data);
|
|
114
|
+
}
|
|
115
|
+
signal?.throwIfAborted();
|
|
116
|
+
return Buffer.concat(chunks, size);
|
|
117
|
+
} finally {
|
|
118
|
+
signal?.removeEventListener('abort', abort);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function feishuImageSource(event, client, key) {
|
|
123
|
+
return {
|
|
124
|
+
async load({ signal, maxBytes }) {
|
|
125
|
+
signal?.throwIfAborted();
|
|
126
|
+
const resource = await client?.im?.v1?.messageResource?.get?.({
|
|
127
|
+
path: {
|
|
128
|
+
message_id: event.message.message_id,
|
|
129
|
+
file_key: key,
|
|
130
|
+
},
|
|
131
|
+
params: { type: 'image' },
|
|
132
|
+
});
|
|
133
|
+
signal?.throwIfAborted();
|
|
134
|
+
const size = declaredSize(resource?.headers);
|
|
135
|
+
if (size !== null && size > maxBytes) {
|
|
136
|
+
resource?.getReadableStream?.().destroy?.();
|
|
137
|
+
throw new ImagePromptError(
|
|
138
|
+
'image-too-large',
|
|
139
|
+
`Feishu image declares ${size} bytes; the limit is ${maxBytes}`,
|
|
140
|
+
'图片超过 5 MB,请压缩后重试。',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return readBoundedStream(resource?.getReadableStream?.(), { signal, maxBytes });
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function extractInboundMessage(event, client) {
|
|
149
|
+
const messageType = event?.message?.message_type;
|
|
150
|
+
const parsed = parsedMessageContent(event);
|
|
151
|
+
const post = postContent(event, parsed);
|
|
152
|
+
const standaloneImageKey = messageType === 'image'
|
|
153
|
+
? nonEmptyString(parsed?.image_key)
|
|
154
|
+
: null;
|
|
155
|
+
const imageKeys = standaloneImageKey ? [standaloneImageKey] : post?.imageKeys ?? [];
|
|
156
|
+
return {
|
|
157
|
+
content: messageType === 'text' ? extractText(event) ?? '' : post?.text ?? '',
|
|
158
|
+
images: imageKeys.map((key) => feishuImageSource(event, client, key)),
|
|
159
|
+
};
|
|
26
160
|
}
|
|
27
161
|
|
|
28
162
|
export function splitText(text, maxChars = 9000) {
|
|
@@ -5,6 +5,7 @@ export const FEISHU_SECRET_REF = 'DSH_FEISHU_APP_SECRET';
|
|
|
5
5
|
export const REQUIRED_TENANT_SCOPES = Object.freeze([
|
|
6
6
|
'im:message.p2p_msg:readonly',
|
|
7
7
|
'im:message.group_at_msg:readonly',
|
|
8
|
+
'im:message:readonly',
|
|
8
9
|
'im:message:send_as_bot',
|
|
9
10
|
'im:message.reactions:write_only',
|
|
10
11
|
'im:message:recall',
|
|
@@ -7,13 +7,30 @@ import {
|
|
|
7
7
|
} from '../shared/harness-question.mjs';
|
|
8
8
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
9
9
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
10
|
+
import {
|
|
11
|
+
fetchImageBuffer,
|
|
12
|
+
hasInboundImages,
|
|
13
|
+
imagePromptUserMessage,
|
|
14
|
+
promptContentForMessage,
|
|
15
|
+
} from '../shared/image-prompt.mjs';
|
|
10
16
|
|
|
11
17
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
12
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
|
+
|
|
13
30
|
const HELP_TEXT = [
|
|
14
31
|
'QQ 机器人已连接 DeepSeek Harness。',
|
|
15
32
|
'',
|
|
16
|
-
'
|
|
33
|
+
'直接发送文字或图片即可继续当前会话。',
|
|
17
34
|
'/new 开启一个全新会话',
|
|
18
35
|
'/compact 压缩当前会话的较早上下文',
|
|
19
36
|
'/workspace 工作区绝对路径 切换工作区',
|
|
@@ -32,6 +49,51 @@ function safeText(message) {
|
|
|
32
49
|
return typeof message?.content === 'string' ? message.content.trim() : '';
|
|
33
50
|
}
|
|
34
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
|
+
|
|
35
97
|
function nonEmptyString(value) {
|
|
36
98
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
37
99
|
}
|
|
@@ -40,6 +102,7 @@ function canClaimInteractionReply(message, pending) {
|
|
|
40
102
|
return pending.questions[pending.index]
|
|
41
103
|
&& nonEmptyString(message?.senderId) === pending.actor
|
|
42
104
|
&& (message.kind !== 'group' || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE')
|
|
105
|
+
&& !hasQqImageAttachments(message)
|
|
43
106
|
&& nonEmptyString(safeText(message));
|
|
44
107
|
}
|
|
45
108
|
|
|
@@ -64,6 +127,7 @@ export class QqHarnessBridge {
|
|
|
64
127
|
#logger;
|
|
65
128
|
#replyTimeoutMs;
|
|
66
129
|
#signal;
|
|
130
|
+
#fetchImpl;
|
|
67
131
|
#queues = new Map();
|
|
68
132
|
#pendingInteractions = new Map();
|
|
69
133
|
#interactionKeys = new Map();
|
|
@@ -80,10 +144,12 @@ export class QqHarnessBridge {
|
|
|
80
144
|
logger = console,
|
|
81
145
|
replyTimeoutMs = 600_000,
|
|
82
146
|
signal,
|
|
147
|
+
fetchImpl = fetch,
|
|
83
148
|
}) {
|
|
84
149
|
if (!bot || typeof bot.sendText !== 'function') throw new TypeError('QQ bot client is required');
|
|
85
150
|
if (!ownerUserOpenid) throw new TypeError('QQ scanner identity is required');
|
|
86
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');
|
|
87
153
|
this.#bot = bot;
|
|
88
154
|
this.#ownerUserOpenid = ownerUserOpenid;
|
|
89
155
|
this.#harness = harness;
|
|
@@ -92,6 +158,7 @@ export class QqHarnessBridge {
|
|
|
92
158
|
this.#logger = logger;
|
|
93
159
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
94
160
|
this.#signal = signal;
|
|
161
|
+
this.#fetchImpl = fetchImpl;
|
|
95
162
|
this.#approvals = new HarnessApprovalQueue({ label: 'qq', logger });
|
|
96
163
|
}
|
|
97
164
|
|
|
@@ -114,7 +181,7 @@ export class QqHarnessBridge {
|
|
|
114
181
|
key,
|
|
115
182
|
actor: sender,
|
|
116
183
|
messageId,
|
|
117
|
-
text: safeText(message),
|
|
184
|
+
text: hasQqImageAttachments(message) ? '' : safeText(message),
|
|
118
185
|
addressed: message.kind !== 'group' || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE',
|
|
119
186
|
hasPendingQuestion: Boolean(pending),
|
|
120
187
|
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
@@ -210,32 +277,37 @@ export class QqHarnessBridge {
|
|
|
210
277
|
if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
|
|
211
278
|
|
|
212
279
|
const target = message.replyTarget;
|
|
213
|
-
const
|
|
280
|
+
const promptMessage = qqInboundMessage(message, { fetchImpl: this.#fetchImpl });
|
|
281
|
+
const text = promptMessage.content;
|
|
282
|
+
const hasImages = hasInboundImages(promptMessage);
|
|
283
|
+
let stream = null;
|
|
214
284
|
try {
|
|
215
|
-
if (!text) {
|
|
216
|
-
await this.#bot.sendText(target, '
|
|
285
|
+
if (!text && !hasImages) {
|
|
286
|
+
await this.#bot.sendText(target, '目前支持文字和图片消息。');
|
|
217
287
|
await this.#state.markSeen(messageId);
|
|
218
288
|
return;
|
|
219
289
|
}
|
|
220
290
|
const command = text.toLowerCase();
|
|
221
|
-
if (command === '/help') {
|
|
291
|
+
if (!hasImages && command === '/help') {
|
|
222
292
|
await this.#bot.sendText(target, HELP_TEXT);
|
|
223
293
|
await this.#state.markSeen(messageId);
|
|
224
294
|
return;
|
|
225
295
|
}
|
|
226
|
-
if (command === '/status') {
|
|
296
|
+
if (!hasImages && command === '/status') {
|
|
227
297
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
228
298
|
await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
|
|
229
299
|
await this.#state.markSeen(messageId);
|
|
230
300
|
return;
|
|
231
301
|
}
|
|
232
|
-
if (command === '/new') {
|
|
302
|
+
if (!hasImages && command === '/new') {
|
|
233
303
|
await this.#state.clearSession(key);
|
|
234
304
|
await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
|
|
235
305
|
await this.#state.markSeen(messageId);
|
|
236
306
|
return;
|
|
237
307
|
}
|
|
238
|
-
const workspaceCommand =
|
|
308
|
+
const workspaceCommand = hasImages
|
|
309
|
+
? null
|
|
310
|
+
: await runWorkspaceCommand(text, this.#harness, key);
|
|
239
311
|
if (workspaceCommand) {
|
|
240
312
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
241
313
|
await this.#bot.sendText(target, reply);
|
|
@@ -243,20 +315,24 @@ export class QqHarnessBridge {
|
|
|
243
315
|
await this.#state.markSeen(messageId);
|
|
244
316
|
return;
|
|
245
317
|
}
|
|
246
|
-
const compactCommand =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
318
|
+
const compactCommand = hasImages
|
|
319
|
+
? null
|
|
320
|
+
: await runCompactCommand(
|
|
321
|
+
text,
|
|
322
|
+
this.#harness,
|
|
323
|
+
this.#state,
|
|
324
|
+
key,
|
|
325
|
+
{ signal: this.#signal },
|
|
326
|
+
);
|
|
253
327
|
if (compactCommand) {
|
|
254
328
|
await this.#bot.sendText(target, compactCommand.message);
|
|
255
329
|
await this.#state.markSeen(messageId);
|
|
256
330
|
return;
|
|
257
331
|
}
|
|
258
332
|
|
|
259
|
-
|
|
333
|
+
const content = hasImages
|
|
334
|
+
? await promptContentForMessage(promptMessage, { signal: this.#signal })
|
|
335
|
+
: undefined;
|
|
260
336
|
let streamFinished = false;
|
|
261
337
|
if (message.kind === 'c2c' && target?.msgId && typeof this.#bot.openStream === 'function') {
|
|
262
338
|
try {
|
|
@@ -271,7 +347,7 @@ export class QqHarnessBridge {
|
|
|
271
347
|
harness: this.#harness,
|
|
272
348
|
state: this.#state,
|
|
273
349
|
key,
|
|
274
|
-
text,
|
|
350
|
+
...(hasImages ? { content } : { text }),
|
|
275
351
|
createOptions: { signal: this.#signal },
|
|
276
352
|
existsOptions: { signal: this.#signal },
|
|
277
353
|
askOptions: {
|
|
@@ -316,11 +392,15 @@ export class QqHarnessBridge {
|
|
|
316
392
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
317
393
|
this.#status.lastError = null;
|
|
318
394
|
} catch (error) {
|
|
395
|
+
stream?.cancel?.();
|
|
319
396
|
if (this.#signal?.aborted) return;
|
|
320
397
|
this.#status.lastError = error?.message ?? String(error);
|
|
321
398
|
this.#logger.error?.('[dsh-im:qq] failed to process an inbound message:', error);
|
|
322
399
|
try {
|
|
323
|
-
await this.#bot.sendText(
|
|
400
|
+
await this.#bot.sendText(
|
|
401
|
+
target,
|
|
402
|
+
imagePromptUserMessage(error) ?? '消息处理失败,请稍后重试。',
|
|
403
|
+
);
|
|
324
404
|
await this.#state.markSeen(messageId);
|
|
325
405
|
} catch (sendError) {
|
|
326
406
|
this.#logger.error?.('[dsh-im:qq] failed to send the safe error reply:', sendError);
|
|
@@ -345,7 +425,7 @@ export class QqHarnessBridge {
|
|
|
345
425
|
|
|
346
426
|
if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
|
|
347
427
|
const text = nonEmptyString(safeText(message));
|
|
348
|
-
if (!text) {
|
|
428
|
+
if (!text || hasQqImageAttachments(message)) {
|
|
349
429
|
await this.#bot.sendText(message.replyTarget, '请用文字回答当前问题。');
|
|
350
430
|
return;
|
|
351
431
|
}
|
|
@@ -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
|
|