@xmanrui/dsh-im 1.0.2 → 1.1.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 -0
- package/README.md +18 -0
- package/assets/logo-dsh-im-chinese-readme-3x2.png +0 -0
- package/assets/logo_cn.png +0 -0
- package/lib/client.js +603 -560
- package/lib/index.js +163 -163
- package/package.json +1 -1
- package/plugin-src/client/agent-preset.js +15 -6
- package/plugin-src/client/channel-card-meta.js +48 -0
- package/plugin-src/client/channels/dingtalk/index.js +25 -19
- package/plugin-src/client/channels/dingtalk/styles.js +0 -6
- package/plugin-src/client/channels/feishu/index.js +41 -35
- package/plugin-src/client/channels/feishu/styles.js +0 -5
- package/plugin-src/client/channels/qq/index.js +24 -16
- package/plugin-src/client/channels/shared/token-channel.js +32 -24
- package/plugin-src/client/channels/wecom/index.js +24 -16
- package/plugin-src/client/channels/weixin/index.js +29 -23
- package/plugin-src/client/channels/weixin/styles.js +0 -5
- package/plugin-src/client/channels/whatsapp/index.js +27 -23
- package/plugin-src/client/i18n.js +2 -0
- package/plugin-src/client/styles.js +23 -8
- package/plugin-src/host/index.mjs +14 -1
- package/src/channels/dingtalk/dingtalk-api.mjs +215 -2
- package/src/channels/dingtalk/dingtalk-bridge.mjs +155 -4
- package/src/channels/discord/discord-api.mjs +134 -6
- package/src/channels/discord/discord-runtime.mjs +15 -4
- package/src/channels/feishu/bridge.mjs +223 -15
- package/src/channels/feishu/feishu-channel.mjs +227 -1
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +217 -10
- package/src/channels/shared/editable-message-stream.mjs +18 -1
- package/src/channels/shared/harness-client.mjs +99 -7
- package/src/channels/shared/semantic/artifact.mjs +748 -0
- package/src/channels/shared/semantic/delivery.mjs +153 -0
- package/src/channels/shared/text-harness-bridge.mjs +149 -3
- package/src/channels/shared/workspace-session.mjs +15 -1
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +167 -4
- package/src/channels/slack/slack-runtime.mjs +21 -5
- package/src/channels/telegram/telegram-api.mjs +111 -5
- package/src/channels/telegram/telegram-runtime.mjs +18 -4
- package/src/channels/wecom/wecom-bridge.mjs +260 -12
- package/src/channels/weixin/weixin-api.mjs +268 -2
- package/src/channels/weixin/weixin-bridge.mjs +134 -3
- package/src/channels/weixin/weixin-controller.mjs +5 -1
- package/src/channels/weixin/weixin-runtime.mjs +5 -1
- package/src/channels/whatsapp/whatsapp-runtime.mjs +108 -5
|
@@ -30,6 +30,16 @@ import {
|
|
|
30
30
|
promptContentForMessage,
|
|
31
31
|
} from '../shared/image-prompt.mjs';
|
|
32
32
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
33
|
+
import {
|
|
34
|
+
materializeOutboundArtifact,
|
|
35
|
+
releaseOutboundArtifact,
|
|
36
|
+
} from '../shared/semantic/artifact.mjs';
|
|
37
|
+
import {
|
|
38
|
+
createArtifactFailureReceipt,
|
|
39
|
+
createDeliveryReceipt,
|
|
40
|
+
mergeDeliveryReceipts,
|
|
41
|
+
providerMessageIdsFor,
|
|
42
|
+
} from '../shared/semantic/delivery.mjs';
|
|
33
43
|
|
|
34
44
|
const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
35
45
|
const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
|
|
@@ -62,6 +72,13 @@ function nonEmptyString(value) {
|
|
|
62
72
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
function dingtalkFileProviderIds(result) {
|
|
76
|
+
const ids = providerMessageIdsFor(result);
|
|
77
|
+
const processQueryKey = nonEmptyString(result?.processQueryKey);
|
|
78
|
+
if (processQueryKey && !ids.includes(processQueryKey)) ids.push(processQueryKey);
|
|
79
|
+
return ids;
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
function safeErrorDiagnostic(error) {
|
|
66
83
|
const chain = [];
|
|
67
84
|
const seen = new Set();
|
|
@@ -195,6 +212,40 @@ function cardTarget(message, sender) {
|
|
|
195
212
|
return { type: 'user', userId: sender };
|
|
196
213
|
}
|
|
197
214
|
|
|
215
|
+
function fileTarget(message, sender, clientId) {
|
|
216
|
+
const robotCode = nonEmptyString(message?.robotCode) ?? clientId;
|
|
217
|
+
if (String(message?.conversationType) === '2') {
|
|
218
|
+
return {
|
|
219
|
+
type: 'group',
|
|
220
|
+
openConversationId: nonEmptyString(message?.conversationId),
|
|
221
|
+
robotCode,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return { type: 'user', userId: sender, robotCode };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function artifactFailureText(fileName, error) {
|
|
228
|
+
const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
|
|
229
|
+
switch (error?.code) {
|
|
230
|
+
case 'artifact-delivery-uncertain':
|
|
231
|
+
return `结果文件「${name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
|
|
232
|
+
case 'artifact-permission-required':
|
|
233
|
+
return `结果文件「${name}」已生成,但钉钉应用或机器人缺少文件消息权限。请开通应用 qyapi_base 权限,并确认机器人具备文件消息发送能力。`;
|
|
234
|
+
case 'artifact-too-large':
|
|
235
|
+
return `结果文件「${name}」超过当前钉钉机器人可发送的文件大小,未发送。`;
|
|
236
|
+
case 'artifact-rate-limited':
|
|
237
|
+
return `结果文件「${name}」暂时被钉钉限流,未能发送,请稍后重试。`;
|
|
238
|
+
case 'artifact-provider-rejected':
|
|
239
|
+
return `结果文件「${name}」已生成,但钉钉拒绝了该文件消息,请检查文件类型和机器人文件消息配置。`;
|
|
240
|
+
case 'artifact-invalid':
|
|
241
|
+
case 'artifact-changed':
|
|
242
|
+
case 'artifact-unavailable':
|
|
243
|
+
return `结果文件「${name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。`;
|
|
244
|
+
default:
|
|
245
|
+
return `结果文件「${name}」已生成,但暂时未能通过钉钉发送,请稍后重试。`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
198
249
|
function progressText(update) {
|
|
199
250
|
if (update?.type === 'text' && nonEmptyString(update.text)) return update.text;
|
|
200
251
|
if (update?.type === 'tool') {
|
|
@@ -596,7 +647,7 @@ export class DingtalkHarnessBridge {
|
|
|
596
647
|
});
|
|
597
648
|
cardStarted = await cardStream.start(CARD_INITIAL_TEXT);
|
|
598
649
|
}
|
|
599
|
-
const { answer } = await askInWorkspaceSession({
|
|
650
|
+
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
600
651
|
harness: this.#harness,
|
|
601
652
|
state: this.#state,
|
|
602
653
|
key,
|
|
@@ -619,11 +670,41 @@ export class DingtalkHarnessBridge {
|
|
|
619
670
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
620
671
|
},
|
|
621
672
|
});
|
|
622
|
-
const
|
|
623
|
-
|
|
673
|
+
const answerText = typeof answer === 'string' && answer.trim()
|
|
674
|
+
? answer
|
|
675
|
+
: artifacts.length > 0 ? '结果文件已生成。' : answer;
|
|
676
|
+
let textDeliveryError = null;
|
|
677
|
+
let textReceipt = null;
|
|
678
|
+
let streamed = false;
|
|
679
|
+
try {
|
|
680
|
+
streamed = cardStarted && await cardStream.finish(answerText);
|
|
681
|
+
if (streamed) {
|
|
682
|
+
textReceipt = createDeliveryReceipt({
|
|
683
|
+
deliveryId: messageId,
|
|
684
|
+
presentation: 'dingtalk-card',
|
|
685
|
+
});
|
|
686
|
+
} else {
|
|
687
|
+
textReceipt = createDeliveryReceipt({
|
|
688
|
+
deliveryId: messageId,
|
|
689
|
+
presentation: 'dingtalk-text',
|
|
690
|
+
providerMessageIds: await this.#send(sessionWebhook, answerText),
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
} catch (error) {
|
|
694
|
+
textDeliveryError = error;
|
|
695
|
+
}
|
|
696
|
+
const delivery = await this.#deliverArtifacts(
|
|
697
|
+
fileTarget(message, sender, this.#clientId),
|
|
698
|
+
sessionWebhook,
|
|
699
|
+
messageId,
|
|
700
|
+
artifacts,
|
|
701
|
+
textReceipt,
|
|
702
|
+
);
|
|
703
|
+
if (textDeliveryError && !delivery.userVisible) throw textDeliveryError;
|
|
624
704
|
increment(this.#status, 'messagesReplied');
|
|
625
705
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
626
706
|
this.#status.lastError = null;
|
|
707
|
+
return delivery.receipt;
|
|
627
708
|
} catch (error) {
|
|
628
709
|
if (error?.code === 'turn-stopped') {
|
|
629
710
|
if (cardStarted) await cardStream.finish('已停止。').catch(() => undefined);
|
|
@@ -940,16 +1021,86 @@ export class DingtalkHarnessBridge {
|
|
|
940
1021
|
}
|
|
941
1022
|
|
|
942
1023
|
async #send(sessionWebhook, text) {
|
|
1024
|
+
const providerMessageIds = [];
|
|
943
1025
|
for (const chunk of splitDingtalkText(text, this.#maxMessageChars)) {
|
|
944
1026
|
this.#signal?.throwIfAborted();
|
|
945
|
-
await this.#api.sendText({
|
|
1027
|
+
const result = await this.#api.sendText({
|
|
946
1028
|
clientId: this.#clientId,
|
|
947
1029
|
clientSecret: this.#clientSecret,
|
|
948
1030
|
sessionWebhook,
|
|
949
1031
|
text: chunk,
|
|
950
1032
|
signal: this.#signal,
|
|
951
1033
|
});
|
|
1034
|
+
providerMessageIds.push(...providerMessageIdsFor(result));
|
|
1035
|
+
}
|
|
1036
|
+
return providerMessageIds;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async #deliverArtifacts(target, sessionWebhook, replyTo, artifacts, baseReceipt) {
|
|
1040
|
+
const receipts = baseReceipt ? [baseReceipt] : [];
|
|
1041
|
+
let userVisible = Boolean(baseReceipt);
|
|
1042
|
+
for (const artifact of artifacts) {
|
|
1043
|
+
this.#signal?.throwIfAborted();
|
|
1044
|
+
try {
|
|
1045
|
+
if (typeof this.#api.sendFile !== 'function') {
|
|
1046
|
+
const unavailable = new Error('DingTalk file delivery is unavailable');
|
|
1047
|
+
unavailable.code = 'artifact-provider-unavailable';
|
|
1048
|
+
throw unavailable;
|
|
1049
|
+
}
|
|
1050
|
+
const file = await materializeOutboundArtifact(artifact, {
|
|
1051
|
+
signal: this.#signal,
|
|
1052
|
+
});
|
|
1053
|
+
const result = await this.#api.sendFile({
|
|
1054
|
+
clientId: this.#clientId,
|
|
1055
|
+
clientSecret: this.#clientSecret,
|
|
1056
|
+
target,
|
|
1057
|
+
file,
|
|
1058
|
+
signal: this.#signal,
|
|
1059
|
+
});
|
|
1060
|
+
receipts.push(createDeliveryReceipt({
|
|
1061
|
+
deliveryId: file.deliveryKey,
|
|
1062
|
+
presentation: 'dingtalk-file',
|
|
1063
|
+
providerMessageIds: dingtalkFileProviderIds(result),
|
|
1064
|
+
artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
|
|
1065
|
+
}));
|
|
1066
|
+
userVisible = true;
|
|
1067
|
+
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
if (this.#signal?.aborted) throw error;
|
|
1070
|
+
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
|
|
1071
|
+
this.#logger.warn?.(
|
|
1072
|
+
`[dsh-dingtalk] result file delivery failed (${error?.code ?? 'unknown'})`,
|
|
1073
|
+
);
|
|
1074
|
+
let noticeSent = false;
|
|
1075
|
+
const providerMessageIds = await this.#send(
|
|
1076
|
+
sessionWebhook,
|
|
1077
|
+
artifactFailureText(artifact?.fileName, error),
|
|
1078
|
+
).then((ids) => {
|
|
1079
|
+
noticeSent = true;
|
|
1080
|
+
return ids;
|
|
1081
|
+
}).catch(() => []);
|
|
1082
|
+
const failureReceipt = createArtifactFailureReceipt({
|
|
1083
|
+
artifactId: artifact?.artifactId ?? 'unknown',
|
|
1084
|
+
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
1085
|
+
error,
|
|
1086
|
+
providerMessageIds,
|
|
1087
|
+
});
|
|
1088
|
+
receipts.push(failureReceipt);
|
|
1089
|
+
if (noticeSent || failureReceipt.artifacts[0]?.outcome === 'unknown') userVisible = true;
|
|
1090
|
+
} finally {
|
|
1091
|
+
releaseOutboundArtifact(artifact);
|
|
1092
|
+
}
|
|
952
1093
|
}
|
|
1094
|
+
const receipt = receipts.length === 0
|
|
1095
|
+
? null
|
|
1096
|
+
: receipts.length === 1
|
|
1097
|
+
? receipts[0]
|
|
1098
|
+
: mergeDeliveryReceipts({
|
|
1099
|
+
deliveryId: replyTo,
|
|
1100
|
+
presentation: baseReceipt ? 'dingtalk-text-and-files' : 'dingtalk-files',
|
|
1101
|
+
receipts,
|
|
1102
|
+
});
|
|
1103
|
+
return { receipt, userVisible };
|
|
953
1104
|
}
|
|
954
1105
|
}
|
|
955
1106
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
1
3
|
const DEFAULT_BASE_URL = 'https://discord.com/api/v10/';
|
|
4
|
+
const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
|
5
|
+
const DISCORD_PERMISSION_ERRORS = new Set([50001, 50013]);
|
|
6
|
+
const DISCORD_TOO_LARGE_ERRORS = new Set([40005]);
|
|
2
7
|
|
|
3
8
|
function cleanString(value) {
|
|
4
9
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -9,6 +14,58 @@ function requestSignal(signal, timeoutMs) {
|
|
|
9
14
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
10
15
|
}
|
|
11
16
|
|
|
17
|
+
function abortReason(signal) {
|
|
18
|
+
return signal?.reason instanceof Error
|
|
19
|
+
? signal.reason
|
|
20
|
+
: new DOMException('The operation was aborted', 'AbortError');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function positiveTimeout(value, name) {
|
|
24
|
+
if (!Number.isInteger(value) || value < 1) throw new TypeError(`${name} must be a positive integer`);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function preserveProviderMetadata(target, source) {
|
|
29
|
+
if (source?.providerCode !== undefined) target.providerCode = source.providerCode;
|
|
30
|
+
if (source?.retry_after !== undefined) {
|
|
31
|
+
target.retry_after = source.retry_after;
|
|
32
|
+
target.retryAfter = source.retry_after;
|
|
33
|
+
}
|
|
34
|
+
if (Number.isInteger(source?.status)) target.status = source.status;
|
|
35
|
+
return target;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function discordArtifactProviderError(cause) {
|
|
39
|
+
const providerCode = Number(cause?.providerCode);
|
|
40
|
+
const status = Number(cause?.status);
|
|
41
|
+
const message = cleanString(cause?.message) ?? '';
|
|
42
|
+
let code = 'artifact-provider-rejected';
|
|
43
|
+
let summary = 'Discord rejected the attachment.';
|
|
44
|
+
if (status === 401 || status === 403 || DISCORD_PERMISSION_ERRORS.has(providerCode)) {
|
|
45
|
+
code = 'artifact-permission-required';
|
|
46
|
+
summary = 'Discord denied permission to send the attachment.';
|
|
47
|
+
} else if (status === 413 || DISCORD_TOO_LARGE_ERRORS.has(providerCode)
|
|
48
|
+
|| /(?:request|attachment|file).{0,24}too large/i.test(message)) {
|
|
49
|
+
code = 'artifact-too-large';
|
|
50
|
+
summary = 'The attachment exceeds Discord\'s size limit.';
|
|
51
|
+
} else if (status === 429) {
|
|
52
|
+
code = 'artifact-rate-limited';
|
|
53
|
+
summary = 'Discord rate-limited attachment delivery.';
|
|
54
|
+
} else if (status >= 500) {
|
|
55
|
+
code = 'artifact-delivery-uncertain';
|
|
56
|
+
summary = 'Discord attachment delivery result is uncertain.';
|
|
57
|
+
}
|
|
58
|
+
const error = new Error(summary, { cause });
|
|
59
|
+
error.code = code;
|
|
60
|
+
return preserveProviderMetadata(error, cause);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function uncertainDiscordDelivery(cause) {
|
|
64
|
+
const error = new Error('Discord attachment delivery result is uncertain', { cause });
|
|
65
|
+
error.code = 'artifact-delivery-uncertain';
|
|
66
|
+
return preserveProviderMetadata(error, cause);
|
|
67
|
+
}
|
|
68
|
+
|
|
12
69
|
function delay(ms, signal) {
|
|
13
70
|
return new Promise((resolve, reject) => {
|
|
14
71
|
if (signal?.aborted) {
|
|
@@ -39,13 +96,20 @@ export class DiscordApi {
|
|
|
39
96
|
#token;
|
|
40
97
|
#fetch;
|
|
41
98
|
#baseUrl;
|
|
99
|
+
#fileUploadTimeoutMs;
|
|
42
100
|
|
|
43
|
-
constructor({
|
|
101
|
+
constructor({
|
|
102
|
+
token,
|
|
103
|
+
fetchImpl = fetch,
|
|
104
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
105
|
+
fileUploadTimeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS,
|
|
106
|
+
}) {
|
|
44
107
|
if (!validDiscordToken(token)) throw new TypeError('Discord Bot Token is invalid');
|
|
45
108
|
if (typeof fetchImpl !== 'function') throw new TypeError('DiscordApi requires fetch');
|
|
46
109
|
this.#token = token.trim();
|
|
47
110
|
this.#fetch = fetchImpl;
|
|
48
111
|
this.#baseUrl = new URL(baseUrl);
|
|
112
|
+
this.#fileUploadTimeoutMs = positiveTimeout(fileUploadTimeoutMs, 'fileUploadTimeoutMs');
|
|
49
113
|
}
|
|
50
114
|
|
|
51
115
|
getCurrentUser(options = {}) {
|
|
@@ -74,6 +138,54 @@ export class DiscordApi {
|
|
|
74
138
|
});
|
|
75
139
|
}
|
|
76
140
|
|
|
141
|
+
async createFileMessage({ channelId, file, replyToMessageId, signal }) {
|
|
142
|
+
if (!file || typeof file !== 'object'
|
|
143
|
+
|| typeof file.fileName !== 'string' || !file.fileName
|
|
144
|
+
|| !Buffer.isBuffer(file.bytes)) {
|
|
145
|
+
throw new TypeError('A Discord attachment is required');
|
|
146
|
+
}
|
|
147
|
+
const deliverySeed = cleanString(file.deliveryKey) ?? cleanString(file.artifactId);
|
|
148
|
+
const nonce = deliverySeed
|
|
149
|
+
? createHash('sha256').update(deliverySeed).digest('hex').slice(0, 25)
|
|
150
|
+
: undefined;
|
|
151
|
+
const payload = new FormData();
|
|
152
|
+
payload.append('payload_json', JSON.stringify({
|
|
153
|
+
allowed_mentions: { parse: [], replied_user: false },
|
|
154
|
+
attachments: [{ id: 0, filename: file.fileName }],
|
|
155
|
+
...(nonce ? { nonce, enforce_nonce: true } : {}),
|
|
156
|
+
...(replyToMessageId ? {
|
|
157
|
+
message_reference: {
|
|
158
|
+
message_id: snowflake(replyToMessageId, 'message id'),
|
|
159
|
+
channel_id: snowflake(channelId, 'channel id'),
|
|
160
|
+
fail_if_not_exists: false,
|
|
161
|
+
},
|
|
162
|
+
} : {}),
|
|
163
|
+
}));
|
|
164
|
+
payload.append(
|
|
165
|
+
'files[0]',
|
|
166
|
+
new Blob([file.bytes], { type: file.mediaType ?? 'application/octet-stream' }),
|
|
167
|
+
file.fileName,
|
|
168
|
+
);
|
|
169
|
+
const targetChannelId = snowflake(channelId, 'channel id');
|
|
170
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
171
|
+
const uploadSignal = requestSignal(signal, this.#fileUploadTimeoutMs);
|
|
172
|
+
try {
|
|
173
|
+
return await this.#request(`channels/${targetChannelId}/messages`, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
signal: uploadSignal,
|
|
176
|
+
timeoutMs: this.#fileUploadTimeoutMs,
|
|
177
|
+
body: payload,
|
|
178
|
+
multipart: true,
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
182
|
+
if (error?.code?.startsWith?.('discord-')) {
|
|
183
|
+
throw discordArtifactProviderError(error);
|
|
184
|
+
}
|
|
185
|
+
throw uncertainDiscordDelivery(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
77
189
|
editMessage({ channelId, messageId, content, signal }) {
|
|
78
190
|
return this.#request(
|
|
79
191
|
`channels/${snowflake(channelId, 'channel id')}/messages/${snowflake(messageId, 'message id')}`,
|
|
@@ -100,6 +212,7 @@ export class DiscordApi {
|
|
|
100
212
|
timeoutMs = 15_000,
|
|
101
213
|
expectBody = true,
|
|
102
214
|
retry = true,
|
|
215
|
+
multipart = false,
|
|
103
216
|
}) {
|
|
104
217
|
let response;
|
|
105
218
|
try {
|
|
@@ -107,10 +220,10 @@ export class DiscordApi {
|
|
|
107
220
|
method,
|
|
108
221
|
headers: {
|
|
109
222
|
authorization: `Bot ${this.#token}`,
|
|
110
|
-
'content-type': 'application/json',
|
|
111
|
-
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 1.0
|
|
223
|
+
...(multipart ? {} : { 'content-type': 'application/json' }),
|
|
224
|
+
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 1.1.0)',
|
|
112
225
|
},
|
|
113
|
-
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
226
|
+
...(body === undefined ? {} : { body: multipart ? body : JSON.stringify(body) }),
|
|
114
227
|
signal: requestSignal(signal, timeoutMs),
|
|
115
228
|
redirect: 'error',
|
|
116
229
|
});
|
|
@@ -124,17 +237,32 @@ export class DiscordApi {
|
|
|
124
237
|
try {
|
|
125
238
|
parsed = await response.json();
|
|
126
239
|
} catch {
|
|
127
|
-
if (expectBody)
|
|
240
|
+
if (expectBody) {
|
|
241
|
+
const error = new Error(`Discord ${method} returned invalid JSON`);
|
|
242
|
+
error.status = response?.status;
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
128
245
|
}
|
|
129
246
|
}
|
|
130
247
|
if (response.status === 429 && retry) {
|
|
131
248
|
const retryAfterMs = Math.min(10_000, Math.max(50, Number(parsed?.retry_after) * 1_000 || 1_000));
|
|
132
249
|
await delay(retryAfterMs, signal);
|
|
133
|
-
return this.#request(path, {
|
|
250
|
+
return this.#request(path, {
|
|
251
|
+
method, body, signal, timeoutMs, expectBody, retry: false, multipart,
|
|
252
|
+
});
|
|
134
253
|
}
|
|
135
254
|
if (!response.ok) {
|
|
136
255
|
const error = new Error(cleanString(parsed?.message) ?? `Discord API failed with HTTP ${response.status}`);
|
|
137
256
|
error.code = `discord-${response.status}`;
|
|
257
|
+
error.status = response.status;
|
|
258
|
+
if (Number.isInteger(parsed?.code) || typeof parsed?.code === 'string') {
|
|
259
|
+
error.providerCode = parsed.code;
|
|
260
|
+
}
|
|
261
|
+
const retryAfter = Number(parsed?.retry_after);
|
|
262
|
+
if (Number.isFinite(retryAfter) && retryAfter >= 0) {
|
|
263
|
+
error.retry_after = retryAfter;
|
|
264
|
+
error.retryAfter = retryAfter;
|
|
265
|
+
}
|
|
138
266
|
throw error;
|
|
139
267
|
}
|
|
140
268
|
return expectBody ? parsed : null;
|
|
@@ -112,7 +112,7 @@ export function normalizeDiscordMessage(message, botId, { fetchImpl = fetch } =
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
class DiscordBotClient {
|
|
115
|
+
export class DiscordBotClient {
|
|
116
116
|
#api;
|
|
117
117
|
#signal;
|
|
118
118
|
|
|
@@ -123,22 +123,32 @@ class DiscordBotClient {
|
|
|
123
123
|
|
|
124
124
|
async sendText(target, text) {
|
|
125
125
|
const chunks = splitMessageText(text, 1_900);
|
|
126
|
-
|
|
126
|
+
const providerMessageIds = [];
|
|
127
127
|
for (const [index, chunk] of chunks.entries()) {
|
|
128
|
-
result = await this.#api.createMessage({
|
|
128
|
+
const result = await this.#api.createMessage({
|
|
129
129
|
channelId: target.channelId,
|
|
130
130
|
content: chunk,
|
|
131
131
|
replyToMessageId: index === 0 ? target.replyToMessageId : undefined,
|
|
132
132
|
signal: this.#signal,
|
|
133
133
|
});
|
|
134
|
+
if (typeof result?.id === 'string' && result.id) providerMessageIds.push(result.id);
|
|
134
135
|
}
|
|
135
|
-
return
|
|
136
|
+
return { providerMessageIds };
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
sendTyping(target) {
|
|
139
140
|
return this.#api.sendTyping({ channelId: target.channelId, signal: this.#signal });
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
sendFile(target, file) {
|
|
144
|
+
return this.#api.createFileMessage({
|
|
145
|
+
channelId: target.channelId,
|
|
146
|
+
file,
|
|
147
|
+
replyToMessageId: target.replyToMessageId,
|
|
148
|
+
signal: this.#signal,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
142
152
|
async openStream(target) {
|
|
143
153
|
const stream = createEditableMessageStream({
|
|
144
154
|
limit: 1_900,
|
|
@@ -162,6 +172,7 @@ class DiscordBotClient {
|
|
|
162
172
|
content,
|
|
163
173
|
signal: this.#signal,
|
|
164
174
|
}),
|
|
175
|
+
messageIdForResult: (message) => message?.id,
|
|
165
176
|
});
|
|
166
177
|
return stream.start();
|
|
167
178
|
}
|