qq-bot-roost 0.9.10 → 0.9.11
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.md +1 -1
- package/package.json +1 -1
- package/src/api.js +45 -6
- package/src/message.js +10 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="零依赖">
|
|
11
11
|
<img src="https://img.shields.io/badge/tests-679%20passed-brightgreen" alt="测试 679 项">
|
|
12
12
|
<img src="https://img.shields.io/badge/plugins-JS%20%7C%20Python%20%7C%20Go%20%7C%20any-blue" alt="多语言插件">
|
|
13
|
-
<img src="https://img.shields.io/badge/version-0.9.
|
|
13
|
+
<img src="https://img.shields.io/badge/version-0.9.11-blueviolet" alt="版本 0.9.11">
|
|
14
14
|
<img src="https://img.shields.io/badge/license-Non--Commercial-red" alt="禁止商用">
|
|
15
15
|
<a href="https://github.com/tdcq9594/qq-bot-roost/actions"><img src="https://img.shields.io/github/actions/workflow/status/tdcq9594/qq-bot-roost/ci.yml?branch=master" alt="CI"></a>
|
|
16
16
|
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -284,14 +284,22 @@ export class QQApi {
|
|
|
284
284
|
const body = { file_type: fileType, srv_send_msg: false };
|
|
285
285
|
if (url) body.url = url;
|
|
286
286
|
if (fileData) body.file_data = fileData;
|
|
287
|
+
/**
|
|
288
|
+
* `file_name` **不只分片合并要**。
|
|
289
|
+
*
|
|
290
|
+
* 官方字段说明:「文件名(分片上传或者二进制上传时必填)」。
|
|
291
|
+
* 以前只在 `upload_id` 那条路带上,于是小文件走 base64 直传时是**无名文件**——
|
|
292
|
+
* QQ 客户端拿不到名字,显示出来就没有扩展名(`报告` 而不是 `报告.docx`),
|
|
293
|
+
* 对方还得自己猜怎么打开。2026-11-02 线上真踩过。
|
|
294
|
+
*
|
|
295
|
+
* URL 上传不用带:那种路径平台自己下载转存,名字从 URL 里取。
|
|
296
|
+
*/
|
|
297
|
+
if (fileName) body.file_name = fileName;
|
|
287
298
|
/**
|
|
288
299
|
* 分片合并:`upload_id` 一进,服务端就走「合并已上传分片」那条路,
|
|
289
|
-
* `url`
|
|
300
|
+
* `url` 可以为空。
|
|
290
301
|
*/
|
|
291
|
-
if (uploadId)
|
|
292
|
-
body.upload_id = uploadId;
|
|
293
|
-
if (fileName) body.file_name = fileName;
|
|
294
|
-
}
|
|
302
|
+
if (uploadId) body.upload_id = uploadId;
|
|
295
303
|
const r = await this.#request('POST', path, body);
|
|
296
304
|
|
|
297
305
|
if (cacheKey && r?.file_info) {
|
|
@@ -348,6 +356,11 @@ export class QQApi {
|
|
|
348
356
|
const up = await this.uploadMedia(scene, target, {
|
|
349
357
|
fileType,
|
|
350
358
|
fileData: buf.toString('base64'),
|
|
359
|
+
/**
|
|
360
|
+
* 二进制直传**必须带文件名**(官方字段说明:分片上传或二进制上传时必填)。
|
|
361
|
+
* 少了它 QQ 侧就是个无名文件,客户端连扩展名都显示不出来。
|
|
362
|
+
*/
|
|
363
|
+
fileName: filePath.split(/[\\/]/).pop() || 'file',
|
|
351
364
|
reuse: false,
|
|
352
365
|
});
|
|
353
366
|
if (cacheKey && up?.file_info) {
|
|
@@ -967,7 +980,21 @@ export class QQApi {
|
|
|
967
980
|
content_raw: String(p.contentRaw ?? ''),
|
|
968
981
|
};
|
|
969
982
|
if (p.streamMsgId) payload.stream_msg_id = p.streamMsgId;
|
|
970
|
-
if (p.msgId)
|
|
983
|
+
if (p.msgId) {
|
|
984
|
+
payload.msg_id = p.msgId;
|
|
985
|
+
/**
|
|
986
|
+
* `msg_seq` 必须显式给,而且**所有分片用同一个值**。
|
|
987
|
+
*
|
|
988
|
+
* 流式消息是「一条消息在长」,不是多条——官方示例里首片、续片、结束片
|
|
989
|
+
* 都是同一个 `msg_seq`。不传的话服务端按 1 处理,而我们自己的普通回复
|
|
990
|
+
* 也从 1 开始递增,于是「先流式、再发图/文件」会撞
|
|
991
|
+
* `40054005 消息被去重`。
|
|
992
|
+
*
|
|
993
|
+
* 取值由调用方先算好传进来(`replyStream` 会在首片前调 `takeSeq`)。
|
|
994
|
+
*/
|
|
995
|
+
if (p.msgSeq != null) payload.msg_seq = Number(p.msgSeq);
|
|
996
|
+
else payload.msg_seq = this.#nextSeq(p.msgId);
|
|
997
|
+
}
|
|
971
998
|
if (p.eventId) payload.event_id = p.eventId;
|
|
972
999
|
if (p.isWakeup) payload.is_wakeup = true;
|
|
973
1000
|
return this.#request('POST', `/v2/users/${enc(openid)}/stream_messages`, payload);
|
|
@@ -1198,6 +1225,18 @@ export class QQApi {
|
|
|
1198
1225
|
|
|
1199
1226
|
/* ---------------- 内部 ---------------- */
|
|
1200
1227
|
|
|
1228
|
+
/**
|
|
1229
|
+
* 先占一个 `msg_seq`,供流式消息这种「一条消息发好几次」的场景用。
|
|
1230
|
+
*
|
|
1231
|
+
* 为什么要有:流式接口不走 `#payload`,所以以前完全不占序号——首片按默认 1 算,
|
|
1232
|
+
* 而我们自己后续的普通回复也从 1 开始递增,两边撞上就是
|
|
1233
|
+
* `40054005 消息被去重`(用户那边表现为「发了但没出现」)。
|
|
1234
|
+
* 流式所有分片共用这一个值(一条消息一个序号)。
|
|
1235
|
+
*/
|
|
1236
|
+
takeSeq(msgId) {
|
|
1237
|
+
return this.#nextSeq(msgId);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1201
1240
|
#payload(opts) {
|
|
1202
1241
|
return buildSendPayload(opts, (id) => this.#nextSeq(id));
|
|
1203
1242
|
}
|
package/src/message.js
CHANGED
|
@@ -812,6 +812,14 @@ export function buildMessage(eventName, d, { api, cfg, outbound, session, traceI
|
|
|
812
812
|
let index = 0;
|
|
813
813
|
let streamId = opts.streamMsgId || '';
|
|
814
814
|
const target = userId;
|
|
815
|
+
/**
|
|
816
|
+
* **一条流式消息只占一个 `msg_seq`**,所有分片共用。
|
|
817
|
+
*
|
|
818
|
+
* 这一步必须在首片之前做:流式接口不走 `#payload`,不主动占号的话
|
|
819
|
+
* 服务端按默认 1 算,而我们后续的普通回复也从 1 开始递增,
|
|
820
|
+
* 两边一撞就是 `40054005 消息被去重`(用户看到「发了但没出现」)。
|
|
821
|
+
*/
|
|
822
|
+
const seq = typeof api.takeSeq === 'function' ? api.takeSeq(msg.id) : 1;
|
|
815
823
|
|
|
816
824
|
return {
|
|
817
825
|
/** 发一片。返回服务端生成的 stream_msg_id(第一片才有) */
|
|
@@ -823,6 +831,7 @@ export function buildMessage(eventName, d, { api, cfg, outbound, session, traceI
|
|
|
823
831
|
mode: 'replace',
|
|
824
832
|
streamMsgId: streamId || undefined,
|
|
825
833
|
msgId: msg.id,
|
|
834
|
+
msgSeq: seq,
|
|
826
835
|
isWakeup: opts.isWakeup,
|
|
827
836
|
});
|
|
828
837
|
if (r?.id && !streamId) streamId = r.id;
|
|
@@ -837,6 +846,7 @@ export function buildMessage(eventName, d, { api, cfg, outbound, session, traceI
|
|
|
837
846
|
mode: 'replace',
|
|
838
847
|
streamMsgId: streamId || undefined,
|
|
839
848
|
msgId: msg.id,
|
|
849
|
+
msgSeq: seq,
|
|
840
850
|
isWakeup: opts.isWakeup,
|
|
841
851
|
});
|
|
842
852
|
},
|