oicq-icalingua-plus-plus 1.26.28 → 1.26.30

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
@@ -960,7 +960,13 @@ export interface FlashTransferMetadata {
960
960
  file_count?: number;
961
961
  }
962
962
 
963
- export type FlashTransferUploadInput = MediaFile | { file: MediaFile; name?: string };
963
+ export interface FlashTransferUploadDirectory {
964
+ name: string;
965
+ /** 虚拟目录的子项;文件仍从各自的源路径读取 */
966
+ children: FlashTransferUploadInput[];
967
+ }
968
+
969
+ export type FlashTransferUploadInput = MediaFile | { file: MediaFile; name?: string } | FlashTransferUploadDirectory;
964
970
 
965
971
  export interface FlashTransferUploadSource {
966
972
  name: string;
@@ -969,6 +975,22 @@ export interface FlashTransferUploadSource {
969
975
  sha1: Buffer | string;
970
976
  }
971
977
 
978
+ /** 客户端分片上传所需的凭证。 */
979
+ export interface FlashTransferUploadCredential {
980
+ ukey: string;
981
+ appid: number;
982
+ file_id: string;
983
+ }
984
+
985
+ /** 由远程 QQ 客户端执行控制操作,文件内容在上传方本地传输。 */
986
+ export interface FlashTransferUploadController<T extends FlashTransferUploadCredential = FlashTransferUploadCredential> {
987
+ create(name: string, options?: FlashTransferCreateOptions): Promise<FlashTransferTask>;
988
+ addFiles(task: FlashTransferTask, files: FlashTransferMetadata[]): Promise<FlashTransferFile[]>;
989
+ applyUpload(task: FlashTransferTask, file: FlashTransferFile, source: FlashTransferUploadSource): Promise<T>;
990
+ completeFile(ticket: T): Promise<void>;
991
+ submit(task_id: string): Promise<void>;
992
+ }
993
+
972
994
  /** 由 applyUpload 返回,保留服务端的 IndexNode、存储参数和业务透传信息。 */
973
995
  export interface FlashTransferUploadTicket {
974
996
  upload: Record<number, any>;
@@ -1027,6 +1049,8 @@ export interface FlashTransferDownload {
1027
1049
  /** 与 Gfs 一样,以下方法会 reject;控制接口保留官网的 snake_case 字段。 */
1028
1050
  export class FlashTransfer {
1029
1051
  private constructor();
1052
+ /** 文件在本机分片上传,创建、登记及完成提交由 control 执行;无需本机登录 QQ */
1053
+ static upload<T extends FlashTransferUploadCredential>(files: FlashTransferUploadInput | FlashTransferUploadInput[], control: FlashTransferUploadController<T>, name?: string, callback?: (percentage: string) => void, options?: FlashTransferCreateOptions & { timeout?: number }): Promise<FlashTransferUploadResult>;
1030
1054
  /** 分享码或 https://qfile.qq.com/q/... 链接转换为文件集 ID */
1031
1055
  resolve(code: string): Promise<string>;
1032
1056
  /** 原生创建文件集,需要在线客户端;保留 task_id 用于完成提交 */
@@ -1036,7 +1060,7 @@ export class FlashTransfer {
1036
1060
  /** 分批登记目录及已计算摘要的文件,返回顺序与请求一致 */
1037
1061
  addFiles(task: Pick<FlashTransferTask, "fileset_id" | "task_id">, files: FlashTransferMetadata[]): Promise<FlashTransferFile[]>;
1038
1062
  /** 底层上传申请;文件必须是 addFiles 返回的节点,source 提供其名称、大小及摘要 */
1039
- applyUpload(task: Pick<FlashTransferTask, "fileset_id">, file: FlashTransferFile, source: FlashTransferUploadSource): Promise<FlashTransferUploadTicket>;
1063
+ applyUpload(task: Pick<FlashTransferTask, "fileset_id">, file: FlashTransferFile, source: FlashTransferUploadSource): Promise<FlashTransferUploadTicket & FlashTransferUploadCredential>;
1040
1064
  /** 分片上传或秒传成功后同步单个文件,随后才能提交整个任务 */
1041
1065
  completeFile(ticket: FlashTransferUploadTicket): Promise<void>;
1042
1066
  /** 文件级完成同步之后提交整个任务,参数是 task_id */
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  const { Client } = require("./lib/oicq");
3
+ const { FlashTransfer } = require("./lib/message/qfile");
3
4
  const { segment, cqcode } = require("./util");
4
5
  const { checkUin, NOOP } = require("./lib/common");
5
6
  const pb = require("./lib/algo/pb");
@@ -20,7 +21,7 @@ const constants = {
20
21
  }
21
22
 
22
23
  module.exports = {
23
- Client, createClient,
24
+ Client, createClient, FlashTransfer,
24
25
  segment, cqcode,
25
26
  /** @deprecated */
26
27
  setGlobalConfig: NOOP,
@@ -236,10 +236,11 @@ class FlashTransfer {
236
236
  * @param {Pick<import("../../").FlashTransferTask, "fileset_id">} task
237
237
  * @param {import("../../").FlashTransferFile} file
238
238
  * @param {import("../../").FlashTransferUploadSource} source
239
- * @returns {Promise<import("../../").FlashTransferUploadTicket>}
239
+ * @returns {Promise<import("../../").FlashTransferUploadTicket & import("../../").FlashTransferUploadCredential>}
240
240
  */
241
- applyUpload(task, file, source) {
242
- return native.applyUpload(this.c, source, native.getBizTransInfo(task, file), file.is_cover ? 23 : 22);
241
+ async applyUpload(task, file, source) {
242
+ const ticket = await native.applyUpload(this.c, source, native.getBizTransInfo(task, file), file.is_cover ? 23 : 22);
243
+ return { ...ticket, file_id: String(ticket.info[1][2]) };
243
244
  }
244
245
 
245
246
  /**
@@ -251,7 +252,7 @@ class FlashTransfer {
251
252
  }
252
253
 
253
254
  /**
254
- * 本地文件、目录或 Buffer 的完整上传;直到文件同步和任务提交均成功才返回。
255
+ * 本地文件、目录、虚拟目录或 Buffer 的完整上传;直到文件同步和任务提交均成功才返回。
255
256
  * @param {import("../../").FlashTransferUploadInput | import("../../").FlashTransferUploadInput[]} files
256
257
  * @param {string} [name]
257
258
  * @param {(percentage: string) => void} [callback]
@@ -260,6 +261,39 @@ class FlashTransfer {
260
261
  */
261
262
  async upload(files, name, callback, options = {}) {
262
263
  native.checkOnline(this.c);
264
+ return this._upload(files, name, callback, options, this);
265
+ }
266
+
267
+ /**
268
+ * 文件在本机读取和上传,控制操作由调用方提供;无需在本机登录 QQ。
269
+ * @param {import("../../").FlashTransferUploadInput | import("../../").FlashTransferUploadInput[]} files
270
+ * @template {import("../../").FlashTransferUploadCredential} T
271
+ * @param {import("../../").FlashTransferUploadController<T>} control
272
+ * @param {string} [name]
273
+ * @param {(percentage: string) => void} [callback]
274
+ * @param {import("../../").FlashTransferCreateOptions & {timeout?: number}} [options]
275
+ * @returns {Promise<import("../../").FlashTransferUploadResult>}
276
+ */
277
+ static async upload(files, control, name, callback, options = {}) {
278
+ for (const method of ["create", "addFiles", "applyUpload", "completeFile", "submit"]) {
279
+ if (typeof control?.[method] !== "function")
280
+ throw new TypeError("upload controller has no " + method);
281
+ }
282
+ const { timeout, ...createOptions } = options;
283
+ return new FlashTransfer(undefined, { timeout })._upload(files, name, callback, createOptions, control);
284
+ }
285
+
286
+ /**
287
+ * @private
288
+ * @param {import("../../").FlashTransferUploadInput | import("../../").FlashTransferUploadInput[]} files
289
+ * @param {string} name
290
+ * @param {(percentage: string) => void} callback
291
+ * @param {import("../../").FlashTransferCreateOptions} options
292
+ * @template {import("../../").FlashTransferUploadCredential} T
293
+ * @param {import("../../").FlashTransferUploadController<T>} control
294
+ * @returns {Promise<import("../../").FlashTransferUploadResult>}
295
+ */
296
+ async _upload(files, name, callback, options, control) {
263
297
  if (callback !== undefined && typeof callback !== "function")
264
298
  throw new TypeError("upload callback must be a function");
265
299
  if (name !== undefined)
@@ -274,7 +308,7 @@ class FlashTransfer {
274
308
  let task;
275
309
  let stage = "hash";
276
310
  /**
277
- * @param {import("../../").MediaFile} file
311
+ * @param {import("../../").MediaFile | import("../../").FlashTransferUploadDirectory} file
278
312
  * @param {string} [file_name]
279
313
  * @param {string} [parent_id]
280
314
  * @param {number} [depth=1]
@@ -283,9 +317,15 @@ class FlashTransfer {
283
317
  const collect = async (file, file_name, parent_id = "", depth = 1) => {
284
318
  if (file_name !== undefined)
285
319
  file_name = checkString(file_name, "file name");
320
+ const virtual = !!file && typeof file === "object" && "children" in file;
321
+ if (virtual) {
322
+ if (!Array.isArray(file.children))
323
+ throw new TypeError("directory children must be an array");
324
+ file_name = checkString(file_name || file.name, "directory name");
325
+ }
286
326
  const binary = file instanceof Uint8Array || file instanceof ArrayBuffer || file instanceof SharedArrayBuffer;
287
327
  let stat;
288
- if (!binary) {
328
+ if (!virtual && !binary) {
289
329
  if (typeof file !== "string")
290
330
  throw new TypeError("upload source must be a local path or binary data");
291
331
  file = path.resolve(file);
@@ -298,17 +338,19 @@ class FlashTransfer {
298
338
  cli_fileid, parent_id, depth,
299
339
  name: file_name || (!binary ? path.basename(file) : ""),
300
340
  cli_file_index: metadata.length + 1,
301
- is_dir: !!stat?.isDirectory(),
341
+ is_dir: virtual || !!stat?.isDirectory(),
302
342
  file_size: 0,
303
343
  file_count: 0,
304
344
  };
305
345
  metadata.push(meta);
306
346
  if (meta.is_dir) {
307
347
  meta.file_type = 25;
308
- const children = await fs.promises.readdir(file, { withFileTypes: true });
309
- children.sort((a, b) => a.name.localeCompare(b.name));
348
+ const children = virtual ? file.children : (await fs.promises.readdir(file, { withFileTypes: true }))
349
+ .sort((a, b) => a.name.localeCompare(b.name))
350
+ .map(child => ({ file: path.join(file, child.name), name: child.name }));
310
351
  for (const child of children) {
311
- const entry = await collect(path.join(file, child.name), child.name, cli_fileid, depth + 1);
352
+ const input = child && typeof child === "object" && "file" in child ? child : { file: child };
353
+ const entry = await collect(input.file, input.name, cli_fileid, depth + 1);
312
354
  meta.file_size += entry.file_size;
313
355
  ++meta.file_count;
314
356
  }
@@ -333,14 +375,14 @@ class FlashTransfer {
333
375
  }
334
376
  stage = "create";
335
377
  const total = [...sources.values()].reduce((size, source) => size + source.size, 0);
336
- task = await this.create(name || metadata[0].name, {
378
+ task = await control.create(name || metadata[0].name, {
337
379
  ...options,
338
380
  total_file_count: sources.size,
339
381
  total_file_size: total,
340
382
  first_level_item_count: metadata.filter(file => !file.parent_id).length,
341
383
  });
342
384
  stage = "add_files";
343
- const added = await this.addFiles(task, metadata);
385
+ const added = await control.addFiles(task, metadata);
344
386
  let transferred = 0, completed = 0;
345
387
  for (const file of added) {
346
388
  if (file.is_dir)
@@ -349,7 +391,7 @@ class FlashTransfer {
349
391
  if (!source)
350
392
  throw new FlashTransferError(-1, "upload response has an unknown client file id");
351
393
  stage = "apply_upload";
352
- const ticket = await this.applyUpload(task, file, source);
394
+ const ticket = await control.applyUpload(task, file, source);
353
395
  stage = "upload";
354
396
  if (ticket.ukey && source.size) {
355
397
  await uploadFile(source, ticket.ukey, ticket.appid, this._http, callback ? percentage => {
@@ -358,15 +400,15 @@ class FlashTransfer {
358
400
  }
359
401
  await source.close();
360
402
  stage = "complete_file";
361
- await this.completeFile(ticket);
362
- file.physical = { id: String(ticket.info[1][2]), status: 2 };
403
+ await control.completeFile(ticket);
404
+ file.physical = { id: String(ticket.file_id), status: 2 };
363
405
  transferred += source.size;
364
406
  ++completed;
365
407
  if (callback)
366
408
  callback((total ? transferred / total * 100 : completed / sources.size * 100).toFixed(2));
367
409
  }
368
410
  stage = "submit";
369
- await this.submit(task.task_id);
411
+ await control.submit(task.task_id);
370
412
  return { ...task, files: added };
371
413
  } catch (error) {
372
414
  if (error instanceof Error) {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "oicq-icalingua-plus-plus",
3
3
  "app_name": "oicq-icalingua-plus-plus",
4
- "version": "1.26.28",
5
- "app_version": "1.26.28",
4
+ "version": "1.26.30",
5
+ "app_version": "1.26.30",
6
6
  "upday": "2026/11/14",
7
7
  "protocol_version": "v11",
8
8
  "description": "oicq(https://github.com/takayama-lily/oicq) v1 for Icalingua++",