oicq-icalingua-plus-plus 1.26.5 → 1.26.7

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/index.d.ts CHANGED
@@ -56,6 +56,23 @@ export interface ConfBot {
56
56
 
57
57
  /** 使用NT上线包,部分消息暂未支持 */
58
58
  useNT?: boolean,
59
+
60
+ /** 指定APK信息,覆盖默认的协议参数 */
61
+ apk_info?: {
62
+ id?: string,
63
+ name?: string,
64
+ version?: string,
65
+ ver?: string,
66
+ sign?: Buffer,
67
+ buildtime?: number,
68
+ appid?: number,
69
+ subid?: number,
70
+ bitmap?: number,
71
+ sigmap?: number,
72
+ sdkver?: string,
73
+ ssover?: number,
74
+ qua?: string,
75
+ }
59
76
  }
60
77
 
61
78
  export interface Statistics {
package/lib/client.js CHANGED
@@ -263,11 +263,15 @@ class Client extends EventEmitter {
263
263
  data_dir: path.join(require.main ? require.main.path : process.cwd(), "data"),
264
264
  ffmpeg_path: "ffmpeg",
265
265
  ffprobe_path: "ffprobe",
266
+ apk_info: {},
266
267
  ...config
267
268
  };
268
269
  this.config = config;
269
270
  this.dir = createDataDir(config.data_dir, uin);
270
271
  this.apk = getApkInfo(config.platform);
272
+ if (config.apk_info) {
273
+ Object.assign(this.apk, config.apk_info);
274
+ }
271
275
  if (this.config.useNT && this.apk.ssover < 20) this.apk.ssover = 20;
272
276
  this.logger = log4js.getLogger(`[${platforms[config.platform] ||
273
277
  ((config.platform % 2 === 1 ? "Android_" : "aPad_") + this.apk.ver)
@@ -85,13 +85,12 @@ async function initFL() {
85
85
  this.sync_finished = false;
86
86
  const tmp = this.fl;
87
87
  this.fl = new Map;
88
- let start = 0, limit = 150;
88
+ let start = 0, limit = 100;
89
89
  while (1) {
90
90
  try {
91
91
  const total = await _initFL.call(this, start, limit, tmp);
92
92
  start += limit;
93
- if (start + limit > total) limit = total - start;
94
- if (start >= total) break;
93
+ if (start > total) break;
95
94
  } catch (e) {
96
95
  this.logger.debug(e);
97
96
  this.logger.warn("加载好友列表出现异常,未加载完成。");
@@ -391,6 +391,18 @@ const map = {
391
391
  468: "/失眠",
392
392
  469: "/坚毅",
393
393
  470: "/马到成功",
394
+ 472: "/心动",
395
+ 474: "/给你一拳",
396
+ 475: "/干饭",
397
+ 476: "/不是吧",
398
+ 477: "/你懂的",
399
+ 478: "/对的对的",
400
+ 479: "/不对不对",
401
+ 480: "/散味儿",
402
+ 481: "/学习",
403
+ 482: "/热化了",
404
+ 483: "/略",
405
+ 484: "/比爱心",
394
406
  };
395
407
 
396
408
  const FACE_OLD_BUF = Buffer.from([0x00, 0x01, 0x00, 0x04, 0x52, 0xCC, 0xF5, 0xD0]);
@@ -612,28 +612,18 @@ class Parser {
612
612
 
613
613
  data.file = buildImageFileParam(md5, size, width, height, imgType);
614
614
 
615
- if (ntPath && storeId === 1) {
615
+ if (ntPath) {
616
616
  data.url = `https://${elem[1][2][3]}${ntPath}&spec=0`;
617
617
  } else {
618
618
  data.url = `https://gchat.qpic.cn/gchatpic_new/0/0-0-${md5.toUpperCase()}/0`;
619
619
  }
620
- if (timestamp() - time > 60 * 60 || !(await checkImgUrl(data.url))) {
621
- // 图片地址失效,尝试发包获取
622
- let newURL = null;
623
- if (dm) { //私聊
624
- newURL = await getNTPicURL.call(this.c, imgInfo, String(this.uid), true);
625
- } else { //群聊
626
- newURL = await getNTPicURL.call(this.c, imgInfo, this.gid);
627
- }
628
- if (newURL) {
629
- data.url = newURL;
630
- }
620
+ if (storeId === 1 && fileid) {
621
+ data.url = await getNTPicURLbyFileid.call(this.c, fileid);
622
+ } else if (storeId === 0) {
623
+ data.url = `https://gchat.qpic.cn/gchatpic_new/0/0-0-${md5.toUpperCase()}/0`;
631
624
  }
632
625
  // QQNT 图片容错,某些 QQNT 的机器人会发出 appid 异常的图片
633
626
  data.url = data.url.replace("multimedia.nt.qq.com.cn", "gchat.qpic.cn"); //这个host下面似乎不检查rkey有效期
634
- if (!(await checkImgUrl(data.url))) {
635
- data.url = await getNTPicURLbyFileid.call(this.c, fileid);
636
- }
637
627
  this.isNTImg = true;
638
628
  return data;
639
629
  }
@@ -300,8 +300,10 @@ async function _uploadPtt(target, buf) {
300
300
  4: md5,
301
301
  5: md5.toString("hex") + ".amr",
302
302
  6: buf.length,
303
+ 8: 0,
303
304
  11: 1,
304
305
  18: fid,
306
+ 19: seconds,
305
307
  29: codec,
306
308
  30: Buffer.from([8, 0, 40, 0, 56, 0]),
307
309
  });
package/lib/service.js CHANGED
@@ -176,117 +176,145 @@ function highwayUploadStream(readable, obj, ip, port) {
176
176
  }
177
177
 
178
178
  const __ = Buffer.from([41])
179
- // TODO: 优化内存占用
180
179
  function highwayHttpUpload(readable, obj) {
181
- const agent = new http.Agent({ maxSockets: 10 })
180
+ const agent = new http.Agent({ maxSockets: 1 })
182
181
  const ip = this.storage.ip
183
182
  const port = this.storage.port
184
183
  if (!port) throw new Error("没有上传通道,如果你刚刚登录,请等待几秒")
185
184
 
186
185
  console.log(`highway(http) ip:${ip} port:${port}`)
187
186
  const url = "http://" + ip + ":" + port + "/cgi-bin/httpconn?htcmd=0x6FF0087&uin=" + this.uin
188
- let seq = 1
189
- let offset = 0, limit = 524288
187
+ const limit = 524288
190
188
  obj.ticket = this.storage.sig_session
191
189
 
192
- const bufs = new Set()
193
- const controller = new AbortController();
194
- require("events").setMaxListeners(233, controller.signal);
195
- let finished = 0
190
+ const MAX_QUEUED = 3 // 队列中最多保留的分块数,控制内存上限(3×512KB≈1.5MB)
191
+ let seq = 1
192
+ let offset = 0
193
+ let readEnded = false
194
+
195
+ // 顺序队列:每读到一个完整分块就推入,drain 循环逐个串行上传
196
+ const queue = []
197
+ let notify = null // 用于队列非空时唤醒 drain
198
+ let errFlag = null // 记录首个错误,终止后续上传
199
+ let quickDone = false
200
+
201
+ const buildBuf = (chunk, chunkOffset) => {
202
+ const head = pb.encode({
203
+ 1: {
204
+ 1: 1,
205
+ 2: String(this.uin),
206
+ 3: "PicUp.DataUp",
207
+ 4: seq++,
208
+ 5: 0,
209
+ 6: this.apk.subid,
210
+ 8: obj.cmdid,
211
+ },
212
+ 2: {
213
+ 1: 0,
214
+ 2: obj.size,
215
+ 3: chunkOffset,
216
+ 4: chunk.length,
217
+ 6: obj.ticket,
218
+ 8: md5(chunk),
219
+ 9: obj.md5,
220
+ 10: 0,
221
+ 13: 0,
222
+ },
223
+ 3: obj.ext,
224
+ 4: Date.now()
225
+ })
226
+ const header = Buffer.allocUnsafe(9)
227
+ header.writeUInt8(40)
228
+ header.writeUInt32BE(head.length, 1)
229
+ header.writeUInt32BE(chunk.length, 5)
230
+ return Buffer.concat([header, head, chunk, __])
231
+ }
196
232
 
233
+ // 读取流 → 分块 → 入队(带背压:队列满时暂停读取)
197
234
  readable.on("data", data => {
235
+ if (errFlag || quickDone) return
198
236
  let _offset = 0
199
237
  while (_offset < data.length) {
200
238
  const chunk = data.slice(_offset, limit + _offset)
201
- const head = pb.encode({
202
- 1: {
203
- 1: 1,
204
- 2: String(this.uin),
205
- 3: "PicUp.DataUp",
206
- 4: seq++,
207
- 5: 0,
208
- 6: this.apk.subid,
209
- 8: obj.cmdid,
210
- },
211
- 2: {
212
- 1: 0,
213
- 2: obj.size,
214
- 3: offset + _offset,
215
- 4: chunk.length,
216
- 6: obj.ticket,
217
- 8: md5(chunk),
218
- 9: obj.md5,
219
- 10: 0,
220
- 13: 0,
221
- },
222
- 3: obj.ext,
223
- 4: Date.now()
224
- })
225
239
  _offset += chunk.length
226
- const _ = Buffer.allocUnsafe(9)
227
- _.writeUInt8(40)
228
- _.writeUInt32BE(head.length, 1)
229
- _.writeUInt32BE(chunk.length, 5)
230
- const buf = Buffer.concat([_, head, chunk, __])
231
- bufs.add(buf)
240
+ const buf = buildBuf(chunk, offset + _offset - chunk.length)
241
+ queue.push({ buf, chunkLen: chunk.length })
242
+ if (notify) { notify(); notify = null }
232
243
  }
233
244
  offset += data.length
245
+ // 队列积压过多,暂停读取,等上传消化后再恢复
246
+ if (queue.length >= MAX_QUEUED) {
247
+ readable.pause()
248
+ }
234
249
  })
235
250
 
251
+ readable.on("err", err => { errFlag = err })
252
+
253
+ // 串行 drain 循环:逐个从队列取分块上传
254
+ const drain = async () => {
255
+ let uploadedBytes = 0
256
+ while (true) {
257
+ while (queue.length === 0) {
258
+ if (errFlag) return
259
+ if (readEnded) {
260
+ // 流已结束且队列为空,检查是否全部完成
261
+ if (uploadedBytes >= obj.size) return
262
+ }
263
+ // 等待新分块入队
264
+ await new Promise(r => { notify = r })
265
+ }
266
+ const { buf, chunkLen } = queue.shift()
267
+ const r = await axios.post(url, buf, {
268
+ responseType: "arraybuffer",
269
+ httpAgent: agent,
270
+ headers: {
271
+ "Content-Length": String(buf.length),
272
+ "Content-Type": "application/octet-stream"
273
+ }
274
+ })
275
+ let rsp
276
+ try {
277
+ const respBuf = Buffer.from(r?.data)
278
+ const header = respBuf.slice(9, respBuf.length - 1)
279
+ rsp = pb.decode(header)
280
+ } catch (err) {
281
+ throw err
282
+ }
283
+ if (rsp?.[3] !== 0) {
284
+ throw new Error(`${rsp[3]}, unknown highway error`)
285
+ }
286
+ uploadedBytes += chunkLen
287
+ const percentage = (uploadedBytes / obj.size * 100).toFixed(2)
288
+ this.logger.debug(`highway(http) chunk uploaded (${percentage}%)`)
289
+ if (typeof obj.callback === "function")
290
+ obj.callback(percentage)
291
+ // 快传:服务端已持有全部数据,跳过后续分块
292
+ if (rsp[7]?.toBuffer().length > 0) {
293
+ quickDone = true
294
+ this.logger.debug(`highway(http) can quick upload`)
295
+ this.logger.debug(`highway(http) chunk uploaded (100.00%)`)
296
+ if (typeof obj.callback === "function")
297
+ obj.callback("100.00")
298
+ return
299
+ }
300
+ // 队列消化后恢复读取
301
+ if (queue.length < MAX_QUEUED && readable.isPaused()) {
302
+ readable.resume()
303
+ }
304
+ }
305
+ }
306
+
236
307
  return new Promise((resolve, reject) => {
237
308
  readable.on("err", reject)
238
- .on("end", async() => {
239
- let flag = false
240
- for (const buf of bufs) {
241
- if (flag) break
242
- await axios.post(url, buf, {
243
- responseType: "arraybuffer",
244
- httpAgent: agent,
245
- signal: controller.signal,
246
- headers: {
247
- "Content-Length": String(buf.length),
248
- "Content-Type": "application/octet-stream"
249
- }
250
- }).then(r => {
251
- let percentage, rsp
252
- try {
253
- const buf = Buffer.from(r?.data)
254
- const header = buf.slice(9, buf.length - 1)
255
- rsp = pb.decode(header)
256
- } catch (err) {
257
- this.logger.error(err)
258
- reject(err)
259
- return
260
- }
261
- if (rsp?.[3] !== 0) {
262
- controller.abort()
263
- reject(new Error(`${rsp[3]}, unknown highway error"`))
264
- return
265
- }
266
- ++finished
267
- percentage = (finished / bufs.size * 100).toFixed(2)
268
- this.logger.debug(`highway(http) chunk uploaded (${percentage}%)`)
269
- if (typeof obj.callback === "function" && percentage)
270
- obj.callback(percentage)
271
- if (finished < bufs.size && rsp[7]?.toBuffer().length > 0) {
272
- controller.abort()
273
- flag = true
274
- this.logger.debug(`highway(http) can quick upload`)
275
- this.logger.debug(`highway(http) chunk uploaded (100.00%)`)
276
- if (typeof obj.callback === "function")
277
- obj.callback("100.00")
278
- }
279
- if (finished >= bufs.size && !rsp[7]?.toBuffer().length)
280
- reject("文件校验未通过,上传失败")
281
- }).catch(err => {
282
- if (err instanceof axios.Cancel === false) {
283
- reject(err)
284
- }
285
- flag = true
286
- })
287
- }
288
- resolve(undefined)
309
+ readable.on("end", () => {
310
+ readEnded = true
311
+ // 流结束时唤醒 drain(如果它正在等待)
312
+ if (notify) { notify(); notify = null }
289
313
  })
314
+ drain().then(() => {
315
+ if (errFlag) reject(errFlag)
316
+ else resolve(undefined)
317
+ }).catch(reject)
290
318
  })
291
319
  }
292
320
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "oicq-icalingua-plus-plus",
3
3
  "app_name": "oicq-icalingua-plus-plus",
4
- "version": "1.26.5",
5
- "app_version": "1.26.5",
6
- "upday": "2026/06/30",
4
+ "version": "1.26.7",
5
+ "app_version": "1.26.7",
6
+ "upday": "2026/07/18",
7
7
  "protocol_version": "v11",
8
8
  "description": "oicq(https://github.com/takayama-lily/oicq) v1 for Icalingua++",
9
9
  "main": "index.js",