@wahaha216/koishi-plugin-jmcomic 0.0.6 → 0.1.1

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.
@@ -1,3 +1,5 @@
1
+ import { Config } from "..";
2
+ import { Logger, HTTP } from "koishi";
1
3
  import { IJMUser } from "../types/JMClient";
2
4
  import { JMClientAbstract } from "../abstract/JMClientAbstract";
3
5
  import { JMAppPhoto } from "./JMAppPhoto";
@@ -8,7 +10,19 @@ export declare class JMAppClient extends JMClientAbstract {
8
10
  static APP_TOKEN_SECRET: string;
9
11
  static APP_TOKEN_SECRET_2: string;
10
12
  static APP_DATA_SECRET: string;
11
- constructor(root: string);
13
+ /**
14
+ * koishi 配置项
15
+ */
16
+ private config;
17
+ /**
18
+ * koishi 日志
19
+ */
20
+ private logger;
21
+ /**
22
+ * koishi http
23
+ */
24
+ private http;
25
+ constructor(root: string, http: HTTP, config: Config, logger: Logger);
12
26
  /**
13
27
  * 登录,未完成
14
28
  * @param username 用户名
@@ -1,9 +1,23 @@
1
+ import { Config } from "..";
2
+ import { Logger, HTTP } from "koishi";
1
3
  import { JMClientAbstract } from "../abstract/JMClientAbstract";
2
4
  import { IJMUser } from "../types/JMClient";
3
5
  import { JMHtmlAlbum } from "./JMHtmlAlbum";
4
6
  import { JMHtmlPhoto } from "./JMHtmlPhoto";
5
7
  export declare class JMHtmlClient extends JMClientAbstract {
6
- constructor(root: string);
8
+ /**
9
+ * koishi 配置项
10
+ */
11
+ private config;
12
+ /**
13
+ * koishi 日志
14
+ */
15
+ private logger;
16
+ /**
17
+ * koishi http
18
+ */
19
+ private http;
20
+ constructor(root: string, http: HTTP, config: Config, logger: Logger);
7
21
  login(username: string, password: string): Promise<IJMUser>;
8
22
  getAlbumById(id: string): Promise<JMHtmlAlbum>;
9
23
  getPhotoById(id: string): Promise<JMHtmlPhoto>;
@@ -0,0 +1,6 @@
1
+ import { AlbumNotExistError } from "../error/albumNotExist.error";
2
+ import { PhotoNotExistError } from "../error/photoNotExist.error";
3
+ import { MySqlError } from "../error/mysql.error";
4
+ import { EmptyBufferError } from "../error/emptybuffer.error";
5
+ import { OverRetryError } from "../error/overRetry.error";
6
+ export { AlbumNotExistError, PhotoNotExistError, MySqlError, EmptyBufferError, OverRetryError, };
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Context, HTTP, Logger, Schema } from "koishi";
1
+ import { Context, Schema } from "koishi";
2
2
  export declare const name = "jmcomic";
3
3
  export interface Config {
4
4
  sendMethod?: "zip" | "pdf";
@@ -6,6 +6,9 @@ export interface Config {
6
6
  retryCount?: number;
7
7
  password?: string;
8
8
  fileName?: string;
9
+ concurrentDownloadLimit?: number;
10
+ concurrentDecodeLimit?: number;
11
+ concurrentQueueLimit?: number;
9
12
  level?: number;
10
13
  cache?: boolean;
11
14
  autoDelete?: boolean;
@@ -19,8 +22,4 @@ export declare const inject: {
19
22
  required: string[];
20
23
  optional: string[];
21
24
  };
22
- export declare let http: HTTP;
23
- export declare let logger: Logger;
24
- export declare let retryCount: number;
25
- export declare let debug: boolean;
26
25
  export declare function apply(ctx: Context, config: Config): Promise<void>;
package/lib/index.js CHANGED
@@ -33,14 +33,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
33
33
  // src/locales/zh-CN.yml
34
34
  var require_zh_CN = __commonJS({
35
35
  "src/locales/zh-CN.yml"(exports2, module2) {
36
- module2.exports = { commands: { jm: { description: "下载JM漫画,无需python!", examples: "jm album 本子数字ID\njm album info 本子数字ID\njm photo 本子章节数字ID", album: { examples: "jm album 数字ID", messages: { notExistError: "找不到该ID对应的本子", mysqlError: "已尝试所有备用地址,但是JM坏掉了" }, info: { examples: "jm album info 本子数字ID", messages: { notExistError: "找不到该ID对应的本子", mysqlError: "已尝试所有备用地址,但是JM坏掉了" } } }, photo: { examples: "jm photo 本子章节数字ID", messages: { notExistError: "找不到该ID对应的章节", mysqlError: "已尝试所有备用地址,但是JM坏掉了" } } } }, _config: [{ $desc: "基础设置", url: "JM域名", sendMethod: "发送方式", fileMethod: "文件获取方式<br>`buffer`: 读取成buffer后发送给bot实现端<br>`file`: 以`file:///` 本地路径形式发送,如docker环境,请在bot实现端同时映射/koishi目录", retryCount: "重试次数限制", password: "密码,留空则不加密", fileName: "文件名定义<br>`{{name}}`:标题<br>`{{id}}`:章节或者本子ID<br>`{{index}}`:多章节本子自动填充`1` 、 `2`" }, { level: "压缩级别,0~9,0为仅存储" }, { $desc: "缓存设置", cache: "缓存文件" }, { autoDelete: "自动删除缓存,**依赖cron服务**" }, { cron: "5位cron表达式", deleteInStart: "启动时检测", keepDays: "缓存保留时间(天)" }, { $desc: "开发者选项", debug: "调试模式,输出更多信息" }] };
36
+ module2.exports = { commands: { jm: { description: "下载JM漫画,无需python!", examples: "jm album 本子数字ID\njm album info 本子数字ID\njm photo 本子章节数字ID", album: { examples: "jm album 数字ID", messages: { addedToQueue: "已将 {id} 添加到处理队列,请稍候。", queueFirst: "已将 {id} 添加到处理队列,即将开始处理", queuePosition: "已将 {id} 添加到处理队列\n前面还有 {ahead} 个任务等待或正在处理", queueProcessing: "已将 {id} 添加到处理队列\n当前任务状态:{status}", notExistError: "找不到该ID对应的本子", mysqlError: "已尝试所有备用地址,但是JM坏掉了" }, info: { examples: "jm album info 本子数字ID", messages: { notExistError: "找不到该ID对应的本子", mysqlError: "已尝试所有备用地址,但是JM坏掉了" } } }, photo: { examples: "jm photo 本子章节数字ID", messages: { addedToQueue: "已将 {id} 添加到处理队列,请稍候。", queueFirst: "已将 {id} 添加到处理队列,即将开始处理", queuePosition: "已将 {id} 添加到处理队列\n前面还有 {ahead} 个任务等待或正在处理", queueProcessing: "已将 {id} 添加到处理队列\n当前任务状态:{status}", notExistError: "找不到该ID对应的章节", mysqlError: "已尝试所有备用地址,但是JM坏掉了" } }, queue: { examples: "jm queue", messages: { emptyQueue: "当前没有正在处理或者等待处理的任务", msgFormat: "ID: {id}, 类型: {type}, 状态: {status}\n", task: { pending: "等待中...", processing: "处理中...", failed: "发生未知错误", completed: "已完成", unknown: "未定义状态" }, type: { album: "本子", photo: "章节" } } } } }, _config: [{ $desc: "基础设置", sendMethod: "发送方式", fileMethod: "文件获取方式<br>`buffer`: 读取成buffer后发送给bot实现端<br>`file`: 以`file:///` 本地路径形式发送,如docker环境,请在bot实现端同时映射/koishi目录", password: "密码,留空则不加密", fileName: "文件名定义<br>`{{name}}`:标题<br>`{{id}}`:章节或者本子ID<br>`{{index}}`:多章节本子自动填充`1` 、 `2`" }, { level: "压缩级别,0~9,0为仅存储" }, { $desc: "限制相关设置", retryCount: "重试次数限制", concurrentDownloadLimit: "同时下载数量限制", concurrentDecodeLimit: "同时解密数量限制", concurrentQueueLimit: "同时处理数量限制" }, { $desc: "缓存设置", cache: "缓存文件" }, { autoDelete: "自动删除缓存,**依赖cron服务**" }, { cron: "5位cron表达式", deleteInStart: "启动时检测", keepDays: "缓存保留时间(天)" }, { $desc: "开发者选项", debug: "调试模式,输出更多信息" }] };
37
37
  }
38
38
  });
39
39
 
40
40
  // src/locales/en-US.yml
41
41
  var require_en_US = __commonJS({
42
42
  "src/locales/en-US.yml"(exports2, module2) {
43
- module2.exports = { commands: { jm: { description: "Download JM comics without python!", examples: "jm album albumID\njm album info albumID\njm photo photoID", album: { examples: "jm album albumID", messages: { notExistError: "albumID not found", mysqlError: "All alternate addresses have been tried. But no response." }, info: { examples: "jm album info albumID", messages: { notExistError: "albumID not found", mysqlError: "All alternate addresses have been tried. But no response." } } }, photo: { examples: "jm photo photoID", messages: { notExistError: "photoID not found", mysqlError: "All alternate addresses have been tried. But no response." } } } }, _config: [{ $desc: "Basic settings", url: "JM domain name", sendMethod: "Send method", fileMethod: "File acquisition method<br>`buffer`: Read as buffer and send it to the bot implementation.<br>`file`: Send it in the local path of `file:///`. For example, if in the docker environment, please map the `/koishi` directory at the bot implementation.", retryCount: "Retry limit", password: "Password, leave blank without encryption", fileName: "File name definition<br>`{{name}}`: Title<br>`{{id}}`: Chapter or Book ID<br>`{{index}}`: Multi-chapter book auto-filling `_1` `_2`" }, { level: "Compression level, 0~9, 0 is only stores" }, { $desc: "Cache settings", cache: "Cache files" }, { autoDelete: "Automatically delete cache, **need cron services**" }, { cron: "5-bit cron expression", deleteInStart: "Detection on startup", keepDays: "Cache retention time (days)" }, { $desc: "Developer Options", debug: "Debug mode, output more information" }] };
43
+ module2.exports = { commands: { jm: { description: "Download JM comics without python!", examples: "jm album albumID\njm album info albumID\njm photo photoID", album: { examples: "jm album albumID", messages: { addedToQueue: "Album {id} has been added to the processing queue. Please wait.", queueFirst: "Added {id} to the processing queue, starting shortly.", queuePosition: "Added {id} to the processing queue.\nThere are {ahead} tasks ahead or currently processing.", queueProcessing: "Added {id} to the processing queue.\nCurrent task status: {status}", notExistError: "albumID not found", mysqlError: "All alternate addresses have been tried. But no response." }, info: { examples: "jm album info albumID", messages: { notExistError: "albumID not found", mysqlError: "All alternate addresses have been tried. But no response." } } }, photo: { examples: "jm photo photoID", messages: { addedToQueue: "Photo {id} has been added to the processing queue. Please wait.", queueFirst: "Added {id} to the processing queue, starting shortly.", queuePosition: "Added {id} to the processing queue.\nThere are {ahead} tasks ahead or currently processing.", queueProcessing: "Added {id} to the processing queue.\nCurrent task status: {status}", notExistError: "photoID not found", mysqlError: "All alternate addresses have been tried. But no response." } }, queue: { examples: "jm queue", messages: { emptyQueue: "There are currently no tasks being processed or waiting.", msgFormat: "ID: {id}, type: {type}, status: {status}\n", task: { Pending: "Pending...", Processing: "Processing...", failed: "Failed (Unknown Error)", completed: "Completed", unknown: "Unknown Status" }, type: { album: "Album", photo: "Photo" } } } } }, _config: [{ $desc: "Basic settings", sendMethod: "Send method", fileMethod: "File acquisition method<br>`buffer`: Read as buffer and send it to the bot implementation.<br>`file`: Send it in the local path of `file:///`. For example, if in the docker environment, please map the `/koishi` directory at the bot implementation.", retryCount: "Retry limit", password: "Password, leave blank without encryption", fileName: "File name definition<br>`{{name}}`: Title<br>`{{id}}`: Chapter or Book ID<br>`{{index}}`: Multi-chapter book auto-filling `_1` `_2`" }, { $desc: "Limit settings", retryCount: "Retry limit", concurrentDownloadLimit: "Concurrent Download Limit", concurrentDecodeLimit: "Concurrent Decode Limit", concurrentQueueLimit: "Concurrent Queue Limit" }, { level: "Compression level, 0~9, 0 is only stores" }, { $desc: "Cache settings", cache: "Cache files" }, { autoDelete: "Automatically delete cache, **need cron services**" }, { cron: "5-bit cron expression", deleteInStart: "Detection on startup", keepDays: "Cache retention time (days)" }, { $desc: "Developer Options", debug: "Debug mode, output more information" }] };
44
44
  }
45
45
  });
46
46
 
@@ -49,17 +49,12 @@ var src_exports = {};
49
49
  __export(src_exports, {
50
50
  Config: () => Config,
51
51
  apply: () => apply,
52
- debug: () => debug,
53
- http: () => http,
54
52
  inject: () => inject,
55
- logger: () => logger,
56
- name: () => name,
57
- retryCount: () => retryCount
53
+ name: () => name
58
54
  });
59
55
  module.exports = __toCommonJS(src_exports);
60
- var import_koishi = require("koishi");
56
+ var import_koishi2 = require("koishi");
61
57
  var import_path3 = require("path");
62
- var import_promises3 = require("fs/promises");
63
58
 
64
59
  // src/utils/Utils.ts
65
60
  var import_fs = require("fs");
@@ -169,7 +164,7 @@ async function limitPromiseAll(promises, limit) {
169
164
  return results;
170
165
  }
171
166
  __name(limitPromiseAll, "limitPromiseAll");
172
- async function requestWithRetry(url, method, config = {}, retryIndex = 0) {
167
+ async function requestWithRetry(url, method, config = {}, http, pluginsConfig, logger, retryIndex = 0) {
173
168
  try {
174
169
  const res = await http(method, url, config);
175
170
  if (typeof res.data === "string" && res.data.includes("Could not connect to mysql")) {
@@ -179,18 +174,26 @@ async function requestWithRetry(url, method, config = {}, retryIndex = 0) {
179
174
  } catch (error) {
180
175
  if (error instanceof MySqlError) {
181
176
  throw new MySqlError();
182
- } else if (retryIndex < retryCount) {
177
+ } else if (retryIndex < pluginsConfig.retryCount) {
183
178
  logger.info(
184
- `${url} 请求失败,正在重试... ${retryIndex + 1}/${retryCount}`
179
+ `${url} 请求失败,正在重试... ${retryIndex + 1}/${pluginsConfig.retryCount}`
180
+ );
181
+ return await requestWithRetry(
182
+ url,
183
+ method,
184
+ config,
185
+ http,
186
+ pluginsConfig,
187
+ logger,
188
+ retryIndex + 1
185
189
  );
186
- return await requestWithRetry(url, method, config, retryIndex + 1);
187
190
  } else {
188
191
  throw new OverRetryError(`请求失败,超过最大重试次数: ${url}`);
189
192
  }
190
193
  }
191
194
  }
192
195
  __name(requestWithRetry, "requestWithRetry");
193
- async function requestWithUrlSwitch(url, method, config = {}, type = "CLIENT", urlIndex = 0) {
196
+ async function requestWithUrlSwitch(url, method, config = {}, http, pluginsConfig, logger, type = "CLIENT", urlIndex = 0) {
194
197
  const list = type === "CLIENT" ? JM_CLIENT_URL_LIST : JM_IMAGE_URL_LIST;
195
198
  const urlCount = list.length;
196
199
  const url_bak = url;
@@ -199,7 +202,14 @@ async function requestWithUrlSwitch(url, method, config = {}, type = "CLIENT", u
199
202
  }
200
203
  try {
201
204
  if (urlIndex < urlCount) {
202
- const res = await requestWithRetry(url, method, config);
205
+ const res = await requestWithRetry(
206
+ url,
207
+ method,
208
+ config,
209
+ http,
210
+ pluginsConfig,
211
+ logger
212
+ );
203
213
  if (res instanceof ArrayBuffer && res.byteLength === 0) {
204
214
  throw new EmptyBufferError();
205
215
  }
@@ -210,12 +220,16 @@ async function requestWithUrlSwitch(url, method, config = {}, type = "CLIENT", u
210
220
  } catch (error) {
211
221
  const isMysqlError = error instanceof MySqlError;
212
222
  const isEmptyBuffer = error instanceof EmptyBufferError;
213
- if (isMysqlError || isEmptyBuffer) {
223
+ const isOverRetryError = error instanceof OverRetryError;
224
+ if (isMysqlError || isEmptyBuffer || isOverRetryError) {
214
225
  logger.info(`请求失败,尝试切换域名... ${urlIndex + 1}/${urlCount}`);
215
226
  return await requestWithUrlSwitch(
216
227
  url_bak,
217
228
  method,
218
229
  config,
230
+ http,
231
+ pluginsConfig,
232
+ logger,
219
233
  type,
220
234
  urlIndex + 1
221
235
  );
@@ -734,8 +748,23 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
734
748
  static APP_TOKEN_SECRET = "18comicAPP";
735
749
  static APP_TOKEN_SECRET_2 = "18comicAPPContent";
736
750
  static APP_DATA_SECRET = "185Hcomic3PAPP7R";
737
- constructor(root) {
751
+ /**
752
+ * koishi 配置项
753
+ */
754
+ config;
755
+ /**
756
+ * koishi 日志
757
+ */
758
+ logger;
759
+ /**
760
+ * koishi http
761
+ */
762
+ http;
763
+ constructor(root, http, config, logger) {
738
764
  super(root);
765
+ this.config = config;
766
+ this.logger = logger;
767
+ this.http = http;
739
768
  }
740
769
  /**
741
770
  * 登录,未完成
@@ -755,7 +784,7 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
755
784
  const formData = new import_form_data.default();
756
785
  formData.append("username", username);
757
786
  formData.append("password", password);
758
- const res = await http.post(
787
+ const res = await this.http.post(
759
788
  "https://www.cdnmhws.cc/login",
760
789
  formData,
761
790
  { headers, responseType: "json" }
@@ -763,14 +792,21 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
763
792
  return this.decodeBase64(res.data, timestamp);
764
793
  }
765
794
  async getAlbumById(id) {
766
- if (debug) logger.info(`获取本子(${id})信息`);
795
+ if (this.config.debug) this.logger.info(`获取本子(${id})信息`);
767
796
  const timestamp = this.getTimeStamp();
768
797
  const { token, tokenparam } = this.getTokenAndTokenParam(timestamp);
769
- const res = await requestWithUrlSwitch("/album", "POST", {
770
- params: { id },
771
- headers: { token, tokenparam },
772
- responseType: "json"
773
- });
798
+ const res = await requestWithUrlSwitch(
799
+ "/album",
800
+ "POST",
801
+ {
802
+ params: { id },
803
+ headers: { token, tokenparam },
804
+ responseType: "json"
805
+ },
806
+ this.http,
807
+ this.config,
808
+ this.logger
809
+ );
774
810
  const album_json = this.decodeBase64(res.data, timestamp);
775
811
  if (!album_json.name) throw new AlbumNotExistError();
776
812
  const album = JMAppAlbum.fromJson(album_json);
@@ -789,14 +825,21 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
789
825
  return album;
790
826
  }
791
827
  async getPhotoById(id) {
792
- if (debug) logger.info(`获取章节(${id})信息`);
828
+ if (this.config.debug) this.logger.info(`获取章节(${id})信息`);
793
829
  const timestamp = this.getTimeStamp();
794
830
  const { token, tokenparam } = this.getTokenAndTokenParam(timestamp);
795
- const res = await requestWithUrlSwitch("/chapter", "POST", {
796
- params: { id },
797
- headers: { token, tokenparam },
798
- responseType: "json"
799
- });
831
+ const res = await requestWithUrlSwitch(
832
+ "/chapter",
833
+ "POST",
834
+ {
835
+ params: { id },
836
+ headers: { token, tokenparam },
837
+ responseType: "json"
838
+ },
839
+ this.http,
840
+ this.config,
841
+ this.logger
842
+ );
800
843
  const photo_json = this.decodeBase64(res.data, timestamp);
801
844
  if (!photo_json.name) throw new PhotoNotExistError();
802
845
  const photo = JMAppPhoto.fromJson(photo_json);
@@ -818,12 +861,12 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
818
861
  const images = photo.getImages();
819
862
  const id = photo.getId();
820
863
  let path = `${this.root}/${type}/${id}/origin`;
821
- if (debug) {
822
- logger.info(`开始下载: ${id}`);
864
+ if (this.config.debug) {
865
+ this.logger.info(`开始下载: ${id}`);
823
866
  if (type === "album") {
824
- logger.info(`单章节: ${single ? "是" : "否"}`);
825
- logger.info(`子章节: ${albumId ? "是" : "否"}`);
826
- logger.info(`本子ID: ${albumId}`);
867
+ this.logger.info(`单章节: ${single ? "是" : "否"}`);
868
+ this.logger.info(`子章节: ${albumId ? "是" : "否"}`);
869
+ this.logger.info(`本子ID: ${albumId}`);
827
870
  }
828
871
  }
829
872
  if (type === "album") {
@@ -833,7 +876,7 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
833
876
  path = `${this.root}/${type}/${albumId}/origin/${id}`;
834
877
  }
835
878
  }
836
- if (debug) logger.info(`存储目录:${path}`);
879
+ if (this.config.debug) this.logger.info(`存储目录:${path}`);
837
880
  await (0, import_promises2.mkdir)(path, { recursive: true });
838
881
  await limitPromiseAll(
839
882
  images.filter((image) => {
@@ -843,18 +886,21 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
843
886
  return !fileExists || !fileSize;
844
887
  }).map((image) => async () => {
845
888
  const url = `/media/photos/${id}/${image}`;
846
- if (debug) logger.info(`下载图片:${url}`);
889
+ if (this.config.debug) this.logger.info(`下载图片:${url}`);
847
890
  const res = await requestWithUrlSwitch(
848
891
  url,
849
892
  "GET",
850
893
  { responseType: "arraybuffer" },
894
+ this.http,
895
+ this.config,
896
+ this.logger,
851
897
  "IMAGE"
852
898
  );
853
899
  await saveImage(res, `${path}/${image}`);
854
900
  }),
855
- 5
901
+ this.config.concurrentDownloadLimit
856
902
  );
857
- if (debug) logger.info(`${id} 下载完成,开始解密图片`);
903
+ if (this.config.debug) this.logger.info(`${id} 下载完成,开始解密图片`);
858
904
  await this.decodeByPhoto(photo, type, albumId, single);
859
905
  }
860
906
  async decodeByPhoto(photo, type = "photo", albumId = "", single = false) {
@@ -879,14 +925,14 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
879
925
  return !fileExists || !fileSize;
880
926
  }).map((image, index) => async () => {
881
927
  const imagePath = `${path}/${image}`;
882
- if (debug) logger.info(`解密图片:${imagePath}`);
928
+ if (this.config.debug) this.logger.info(`解密图片:${imagePath}`);
883
929
  const decodedImagePath = `${decodedPath}/${image}`;
884
930
  const imageBuffer = await (0, import_promises2.readFile)(imagePath);
885
931
  await decodeImage(imageBuffer, splitNumbers[index], decodedImagePath);
886
932
  }),
887
- 10
933
+ this.config.concurrentDecodeLimit
888
934
  );
889
- logger.info(`${id} 解密完成`);
935
+ this.logger.info(`${id} 解密完成`);
890
936
  }
891
937
  async albumToPdf(album, password) {
892
938
  const id = album.getId();
@@ -920,7 +966,7 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
920
966
  async photoToPdf(photo, pdfName, type = "photo", albumId = "", single = false, password) {
921
967
  const images = photo.getImages();
922
968
  const id = photo.getId();
923
- if (debug) logger.info(`开始生成PDF ${pdfName}.pdf`);
969
+ if (this.config.debug) this.logger.info(`开始生成PDF ${pdfName}.pdf`);
924
970
  pdfName = sanitizeFileName(pdfName);
925
971
  let path = (0, import_path2.join)(this.root, type, `${id}`);
926
972
  if (type === "album") {
@@ -960,7 +1006,7 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
960
1006
  }
961
1007
  try {
962
1008
  pdfDoc.endPDF(() => {
963
- if (debug) logger.info(`PDF ${pdfName}.pdf 生成完成`);
1009
+ if (this.config.debug) this.logger.info(`PDF ${pdfName}.pdf 生成完成`);
964
1010
  });
965
1011
  } catch (error) {
966
1012
  throw new Error(error);
@@ -986,7 +1032,7 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
986
1032
  directorys.push({ directory: `${path}/decoded`, destpath: false });
987
1033
  }
988
1034
  await archiverImage(directorys, `${path}/${zipName}.zip`, password, level);
989
- if (debug) logger.info(`ZIP ${zipName}.zip 生成完成`);
1035
+ if (this.config.debug) this.logger.info(`ZIP ${zipName}.zip 生成完成`);
990
1036
  return `${path}/${zipName}.zip`;
991
1037
  }
992
1038
  async photoToZip(photo, zipName, password, level = 6) {
@@ -999,7 +1045,7 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
999
1045
  password,
1000
1046
  level
1001
1047
  );
1002
- if (debug) logger.info(`ZIP ${zipName}.zip 生成完成`);
1048
+ if (this.config.debug) this.logger.info(`ZIP ${zipName}.zip 生成完成`);
1003
1049
  return `${path}/${zipName}.zip`;
1004
1050
  }
1005
1051
  /**
@@ -1023,7 +1069,10 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
1023
1069
  const html = await requestWithUrlSwitch(
1024
1070
  "/chapter_view_template",
1025
1071
  "POST",
1026
- { params: { id }, headers: { token, tokenparam }, responseType: "text" }
1072
+ { params: { id }, headers: { token, tokenparam }, responseType: "text" },
1073
+ this.http,
1074
+ this.config,
1075
+ this.logger
1027
1076
  );
1028
1077
  return parseInt(html.match(JM_SCRAMBLE_ID)[1]);
1029
1078
  }
@@ -1059,45 +1108,317 @@ var JMAppClient = class _JMAppClient extends JMClientAbstract {
1059
1108
  }
1060
1109
  };
1061
1110
 
1111
+ // src/processors/jmProcessor.ts
1112
+ var import_koishi = require("koishi");
1113
+ var import_promises3 = require("node:fs/promises");
1114
+ var createJmProcessor = /* @__PURE__ */ __name((processorConfig, http, config, logger) => {
1115
+ const {
1116
+ root,
1117
+ sendMethod,
1118
+ password,
1119
+ level,
1120
+ fileName,
1121
+ fileMethod,
1122
+ cache,
1123
+ debug
1124
+ } = processorConfig;
1125
+ return async (payload) => {
1126
+ const { id, session, messageId } = payload;
1127
+ try {
1128
+ const jmClient = new JMAppClient(root, http, config, logger);
1129
+ let filePath;
1130
+ let name2;
1131
+ let ext;
1132
+ let dir = "";
1133
+ if (payload.type === "album") {
1134
+ const album = await jmClient.getAlbumById(id);
1135
+ await jmClient.downloadByAlbum(album);
1136
+ if (sendMethod === "zip") {
1137
+ filePath = await jmClient.albumToZip(album, password, level);
1138
+ } else {
1139
+ filePath = await jmClient.albumToPdf(album, password);
1140
+ }
1141
+ if (typeof filePath === "string") {
1142
+ const fileInfo = getFileInfo(filePath);
1143
+ name2 = formatFileName(fileName, fileInfo.fileName, id);
1144
+ ext = fileInfo.ext;
1145
+ dir = fileInfo.dir;
1146
+ } else {
1147
+ const firstPath = filePath[0];
1148
+ const fileInfo = getFileInfo(firstPath);
1149
+ name2 = formatFileName(fileName, fileInfo.fileName, id);
1150
+ ext = fileInfo.ext;
1151
+ dir = fileInfo.dir;
1152
+ }
1153
+ } else if (payload.type === "photo") {
1154
+ const photo = await jmClient.getPhotoById(id);
1155
+ await jmClient.downloadByPhoto(photo);
1156
+ const photoName = photo.getName();
1157
+ if (sendMethod === "zip") {
1158
+ filePath = await jmClient.photoToZip(
1159
+ photo,
1160
+ photoName,
1161
+ password,
1162
+ level
1163
+ );
1164
+ } else {
1165
+ filePath = await jmClient.photoToPdf(photo, photoName);
1166
+ }
1167
+ const fileInfo = getFileInfo(filePath);
1168
+ name2 = formatFileName(fileName, fileInfo.fileName, id);
1169
+ ext = fileInfo.ext;
1170
+ dir = fileInfo.dir;
1171
+ } else {
1172
+ throw new Error(`未知任务类型: ${payload.type}`);
1173
+ }
1174
+ if (typeof filePath === "string") {
1175
+ const {
1176
+ fileName: baseFileName,
1177
+ ext: fileExt,
1178
+ dir: fileDir
1179
+ } = getFileInfo(filePath);
1180
+ const finalName = formatFileName(fileName, baseFileName, id);
1181
+ if (debug) logger.info(`文件名:${finalName}.${fileExt}`);
1182
+ if (fileMethod === "buffer") {
1183
+ const buffer = await (0, import_promises3.readFile)(filePath);
1184
+ await session.send([
1185
+ import_koishi.h.file(buffer, fileExt, { title: `${finalName}.${fileExt}` })
1186
+ ]);
1187
+ } else {
1188
+ await session.send([
1189
+ import_koishi.h.file(`file:///${filePath}`, { title: `${finalName}.${fileExt}` })
1190
+ ]);
1191
+ }
1192
+ if (!cache) (0, import_promises3.rm)(fileDir, { recursive: true });
1193
+ } else {
1194
+ let currentFileDir = "";
1195
+ for (const [index, p] of filePath.entries()) {
1196
+ const {
1197
+ fileName: baseFileName,
1198
+ ext: fileExt,
1199
+ dir: fileDir
1200
+ } = getFileInfo(p);
1201
+ const finalName = formatFileName(
1202
+ fileName,
1203
+ baseFileName,
1204
+ id,
1205
+ index + 1
1206
+ );
1207
+ if (debug) logger.info(`文件名:${finalName}.${fileExt}`);
1208
+ if (fileMethod === "buffer") {
1209
+ const buffer = await (0, import_promises3.readFile)(p);
1210
+ await session.send([
1211
+ import_koishi.h.file(buffer, fileExt, { title: `${finalName}.${fileExt}` })
1212
+ ]);
1213
+ } else {
1214
+ await session.send([
1215
+ import_koishi.h.file(`file:///${p}`, { title: `${finalName}.${fileExt}` })
1216
+ ]);
1217
+ }
1218
+ currentFileDir = fileDir;
1219
+ }
1220
+ if (!cache) {
1221
+ if (currentFileDir) {
1222
+ (0, import_promises3.rm)(currentFileDir, { recursive: true });
1223
+ } else {
1224
+ logger.warn(`无法删除目录,fileDir 未定义。任务 ID: ${id}`);
1225
+ }
1226
+ }
1227
+ }
1228
+ } catch (error) {
1229
+ if (error instanceof AlbumNotExistError || error instanceof PhotoNotExistError) {
1230
+ await session.send([
1231
+ import_koishi.h.quote(messageId),
1232
+ import_koishi.h.text(session.text(".notExistError"))
1233
+ // 假设 .notExistError 可以通用
1234
+ ]);
1235
+ } else if (error instanceof MySqlError) {
1236
+ await session.send([
1237
+ import_koishi.h.quote(messageId),
1238
+ import_koishi.h.text(session.text(".mysqlError"))
1239
+ ]);
1240
+ } else {
1241
+ await session.send([
1242
+ import_koishi.h.quote(messageId),
1243
+ import_koishi.h.text(`处理 ID 为 ${id} 的本子/章节时发生错误`)
1244
+ ]);
1245
+ throw error;
1246
+ }
1247
+ }
1248
+ };
1249
+ }, "createJmProcessor");
1250
+
1251
+ // src/utils/Queue.ts
1252
+ var import_crypto4 = require("crypto");
1253
+ var Queue = class {
1254
+ static {
1255
+ __name(this, "Queue");
1256
+ }
1257
+ tasks = [];
1258
+ processor;
1259
+ concurrency;
1260
+ activeTasks = 0;
1261
+ /**
1262
+ * koishi 配置项
1263
+ */
1264
+ config;
1265
+ /**
1266
+ * koishi 日志
1267
+ */
1268
+ logger;
1269
+ constructor(processor, options = {}, config, logger) {
1270
+ this.config = config;
1271
+ this.logger = logger;
1272
+ this.processor = processor;
1273
+ this.concurrency = options.concurrency || 1;
1274
+ }
1275
+ /**
1276
+ * 向队列添加一个新任务
1277
+ */
1278
+ add(payload) {
1279
+ const task = {
1280
+ id: (0, import_crypto4.randomUUID)(),
1281
+ payload,
1282
+ status: "pending",
1283
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1284
+ };
1285
+ this.tasks.push(task);
1286
+ if (this.config.debug)
1287
+ this.logger.info(
1288
+ `[任务添加] 任务ID: ${payload} 类型: ${payload.type} ID: ${payload.id}`
1289
+ );
1290
+ const { pendingAhead, queuePosition } = this.getTaskQueuePosition(task.id);
1291
+ setTimeout(() => this._processQueue(), 0);
1292
+ return { task, pendingAhead, queuePosition };
1293
+ }
1294
+ /**
1295
+ * 获取任务状态
1296
+ */
1297
+ getTask(id) {
1298
+ return this.tasks.find((t) => t.id === id);
1299
+ }
1300
+ /**
1301
+ * 获取所有任务的只读列表
1302
+ */
1303
+ getAllTasks() {
1304
+ return this.tasks;
1305
+ }
1306
+ /**
1307
+ * 获取指定任务在队列中的位置信息
1308
+ * @param taskId 任务ID
1309
+ * @returns {pendingAhead: number, queuePosition: number}
1310
+ * pendingAhead: 在此任务之前有多少个待处理任务
1311
+ * queuePosition: 此任务在所有待处理任务中的位置(从1开始)
1312
+ */
1313
+ getTaskQueuePosition(taskId) {
1314
+ let pendingAhead = 0;
1315
+ let queuePosition = 0;
1316
+ let found = false;
1317
+ for (let i = 0; i < this.tasks.length; i++) {
1318
+ const currentTask = this.tasks[i];
1319
+ if (currentTask.id === taskId) {
1320
+ found = true;
1321
+ if (currentTask.status === "pending" || currentTask.status === "processing") {
1322
+ queuePosition = pendingAhead + 1;
1323
+ }
1324
+ break;
1325
+ }
1326
+ if (currentTask.status === "pending" || currentTask.status === "processing") {
1327
+ pendingAhead++;
1328
+ }
1329
+ }
1330
+ if (!found) {
1331
+ return { pendingAhead: -1, queuePosition: -1 };
1332
+ }
1333
+ return { pendingAhead, queuePosition };
1334
+ }
1335
+ /**
1336
+ * 检查并处理队列中的任务
1337
+ */
1338
+ _processQueue() {
1339
+ while (this.activeTasks < this.concurrency) {
1340
+ const task = this.tasks.find((t) => t.status === "pending");
1341
+ if (!task) break;
1342
+ this.activeTasks++;
1343
+ task.status = "processing";
1344
+ if (this.config.debug)
1345
+ this.logger.info(
1346
+ `[任务开始] 任务ID: ${task.id} 类型: ${task.payload.type} ID: ${task.payload.id}`
1347
+ );
1348
+ this._runTask(task);
1349
+ }
1350
+ }
1351
+ /**
1352
+ * 执行单个任务
1353
+ */
1354
+ async _runTask(task) {
1355
+ try {
1356
+ await this.processor(task.payload);
1357
+ task.status = "completed";
1358
+ if (this.config.debug)
1359
+ this.logger.info(
1360
+ `[任务成功] 任务ID: ${task.id} 类型: ${task.payload.type} ID: ${task.payload.id}`
1361
+ );
1362
+ } catch (error) {
1363
+ const errorMessage = error instanceof Error ? error.message : String(error);
1364
+ if (this.config.debug)
1365
+ this.logger.error(
1366
+ `[任务失败] 任务ID: ${task.id} 类型: ${task.payload.type} ID: ${task.payload.id}, 错误: ${errorMessage}`
1367
+ );
1368
+ task.status = "failed";
1369
+ task.error = errorMessage;
1370
+ } finally {
1371
+ task.processedAt = (/* @__PURE__ */ new Date()).toISOString();
1372
+ this.activeTasks--;
1373
+ this._processQueue();
1374
+ }
1375
+ }
1376
+ };
1377
+
1062
1378
  // src/index.ts
1063
1379
  var name = "jmcomic";
1064
- var Config = import_koishi.Schema.intersect([
1065
- import_koishi.Schema.object({
1066
- retryCount: import_koishi.Schema.number().min(1).max(5).default(5),
1067
- sendMethod: import_koishi.Schema.union(["zip", "pdf"]).default("pdf"),
1068
- fileMethod: import_koishi.Schema.union(["buffer", "file"]).default("buffer"),
1069
- password: import_koishi.Schema.string(),
1070
- fileName: import_koishi.Schema.string().default("{{name}} ({{id}})_{{index}}")
1380
+ var Config = import_koishi2.Schema.intersect([
1381
+ import_koishi2.Schema.object({
1382
+ fileMethod: import_koishi2.Schema.union(["buffer", "file"]).default("buffer"),
1383
+ password: import_koishi2.Schema.string(),
1384
+ fileName: import_koishi2.Schema.string().default("{{name}} ({{id}})_{{index}}"),
1385
+ sendMethod: import_koishi2.Schema.union(["zip", "pdf"]).default("pdf")
1071
1386
  }),
1072
- import_koishi.Schema.union([
1073
- import_koishi.Schema.object({
1074
- sendMethod: import_koishi.Schema.const("zip").required(),
1075
- level: import_koishi.Schema.number().min(0).max(9).default(6).role("slider")
1387
+ import_koishi2.Schema.union([
1388
+ import_koishi2.Schema.object({
1389
+ sendMethod: import_koishi2.Schema.const("zip").required(),
1390
+ level: import_koishi2.Schema.number().min(0).max(9).default(6).role("slider")
1076
1391
  }),
1077
- import_koishi.Schema.object({})
1392
+ import_koishi2.Schema.object({})
1078
1393
  ]),
1079
- import_koishi.Schema.object({
1080
- cache: import_koishi.Schema.boolean().default(false)
1394
+ import_koishi2.Schema.object({
1395
+ retryCount: import_koishi2.Schema.number().min(1).max(5).default(5),
1396
+ concurrentDownloadLimit: import_koishi2.Schema.number().min(0).max(20).default(10),
1397
+ concurrentDecodeLimit: import_koishi2.Schema.number().min(0).max(20).default(5),
1398
+ concurrentQueueLimit: import_koishi2.Schema.number().min(0).max(10).default(1)
1399
+ }),
1400
+ import_koishi2.Schema.object({
1401
+ cache: import_koishi2.Schema.boolean().default(false)
1081
1402
  }),
1082
- import_koishi.Schema.union([
1083
- import_koishi.Schema.object({
1084
- cache: import_koishi.Schema.const(true).required(),
1085
- autoDelete: import_koishi.Schema.boolean().default(false)
1403
+ import_koishi2.Schema.union([
1404
+ import_koishi2.Schema.object({
1405
+ cache: import_koishi2.Schema.const(true).required(),
1406
+ autoDelete: import_koishi2.Schema.boolean().default(false)
1086
1407
  }),
1087
- import_koishi.Schema.object({})
1408
+ import_koishi2.Schema.object({})
1088
1409
  ]),
1089
- import_koishi.Schema.union([
1090
- import_koishi.Schema.object({
1091
- cache: import_koishi.Schema.const(true).required(),
1092
- autoDelete: import_koishi.Schema.const(true).required(),
1093
- cron: import_koishi.Schema.string().default("0 0 * * *"),
1094
- deleteInStart: import_koishi.Schema.boolean().default(false),
1095
- keepDays: import_koishi.Schema.number().min(1).default(7)
1410
+ import_koishi2.Schema.union([
1411
+ import_koishi2.Schema.object({
1412
+ cache: import_koishi2.Schema.const(true).required(),
1413
+ autoDelete: import_koishi2.Schema.const(true).required(),
1414
+ cron: import_koishi2.Schema.string().default("0 0 * * *"),
1415
+ deleteInStart: import_koishi2.Schema.boolean().default(false),
1416
+ keepDays: import_koishi2.Schema.number().min(1).default(7)
1096
1417
  }),
1097
- import_koishi.Schema.object({})
1418
+ import_koishi2.Schema.object({})
1098
1419
  ]),
1099
- import_koishi.Schema.object({
1100
- debug: import_koishi.Schema.boolean().default(false)
1420
+ import_koishi2.Schema.object({
1421
+ debug: import_koishi2.Schema.boolean().default(false)
1101
1422
  })
1102
1423
  ]).i18n({
1103
1424
  "zh-CN": require_zh_CN()._config,
@@ -1107,17 +1428,10 @@ var inject = {
1107
1428
  required: ["http"],
1108
1429
  optional: ["notifier", "cron"]
1109
1430
  };
1110
- var http;
1111
- var logger;
1112
- var retryCount;
1113
- var debug;
1114
1431
  async function apply(ctx, config) {
1115
- http = ctx.http;
1116
- retryCount = config.retryCount;
1117
- debug = config.debug;
1118
1432
  ctx.i18n.define("en-US", require_en_US());
1119
1433
  ctx.i18n.define("zh-CN", require_zh_CN());
1120
- logger = ctx.logger("jmcomic");
1434
+ const logger = ctx.logger("jmcomic");
1121
1435
  const root = (0, import_path3.join)(ctx.baseDir, "data", "jmcomic");
1122
1436
  const scheduleFn = /* @__PURE__ */ __name(async () => {
1123
1437
  const albumPath = (0, import_path3.join)(ctx.baseDir, "data", "jmcomic", "album");
@@ -1135,194 +1449,148 @@ async function apply(ctx, config) {
1135
1449
  content: "据JMComic-Crawler-Python源码可知JM图片还有gif形式,目前尚未支持"
1136
1450
  });
1137
1451
  }
1138
- ctx.command("jm.album <albumId:string>").alias("本子").action(async ({ session, options }, albumId) => {
1452
+ const processorConfig = {
1453
+ root,
1454
+ sendMethod: config.sendMethod,
1455
+ password: config.password,
1456
+ level: config.level,
1457
+ fileName: config.fileName,
1458
+ fileMethod: config.fileMethod,
1459
+ cache: config.cache,
1460
+ debug: config.debug || false
1461
+ };
1462
+ const jmProcessor = createJmProcessor(
1463
+ processorConfig,
1464
+ ctx.http,
1465
+ config,
1466
+ logger
1467
+ );
1468
+ const queue = new Queue(
1469
+ jmProcessor,
1470
+ { concurrency: config.concurrentQueueLimit || 1 },
1471
+ config,
1472
+ logger
1473
+ );
1474
+ const handleAlbumOrPhoto = /* @__PURE__ */ __name(async (session, id) => {
1139
1475
  const messageId = session.messageId;
1140
- if (!/^\d+$/.test(albumId)) {
1476
+ if (!/^\d+$/.test(id)) {
1141
1477
  await session.send([
1142
- import_koishi.h.quote(messageId),
1143
- import_koishi.h.text("输入的ID不合法,请检查")
1478
+ import_koishi2.h.quote(messageId),
1479
+ import_koishi2.h.text("输入的ID不合法,请检查")
1144
1480
  ]);
1145
1481
  return;
1146
1482
  }
1147
- try {
1148
- const jmClient = new JMAppClient(root);
1149
- const album = await jmClient.getAlbumById(albumId);
1150
- await jmClient.downloadByAlbum(album);
1151
- let filePath;
1152
- if (config.sendMethod === "zip") {
1153
- filePath = await jmClient.albumToZip(
1154
- album,
1155
- config.password,
1156
- config.level
1157
- );
1158
- } else {
1159
- filePath = await jmClient.albumToPdf(album, config.password);
1160
- }
1161
- if (typeof filePath === "string") {
1162
- const { fileName, ext, dir } = getFileInfo(filePath);
1163
- const name2 = formatFileName(config.fileName, fileName, albumId);
1164
- if (debug) logger.info(`文件名:${name2}.${ext}`);
1165
- if (config.fileMethod === "buffer") {
1166
- const buffer = await (0, import_promises3.readFile)(filePath);
1167
- await session.send([
1168
- import_koishi.h.file(buffer, ext, { title: `${name2}.${ext}` })
1169
- ]);
1170
- } else {
1171
- await session.send([
1172
- import_koishi.h.file(`file:///${filePath}`, { title: `${name2}.${ext}` })
1173
- ]);
1174
- }
1175
- if (!config.cache) (0, import_promises3.rm)(dir, { recursive: true });
1176
- } else {
1177
- let fileDir;
1178
- for (const [index, p] of filePath.entries()) {
1179
- const { fileName, ext, dir } = getFileInfo(p);
1180
- const name2 = formatFileName(
1181
- config.fileName,
1182
- fileName,
1183
- albumId,
1184
- index + 1
1185
- );
1186
- if (debug) logger.info(`文件名:${name2}.${ext}`);
1187
- if (config.fileMethod === "buffer") {
1188
- const buffer = await (0, import_promises3.readFile)(p);
1189
- await session.send([
1190
- import_koishi.h.file(buffer, ext, { title: `${name2}.${ext}` })
1191
- ]);
1192
- } else {
1193
- await session.send([
1194
- import_koishi.h.file(`file:///${p}`, { title: `${name2}.${ext}` })
1195
- ]);
1196
- }
1197
- fileDir = dir;
1198
- }
1199
- if (!config.cache) (0, import_promises3.rm)(fileDir, { recursive: true });
1200
- }
1201
- } catch (error) {
1202
- if (error instanceof AlbumNotExistError) {
1203
- await session.send([
1204
- import_koishi.h.quote(messageId),
1205
- import_koishi.h.text(session.text(".notExistError"))
1206
- ]);
1207
- } else if (error instanceof MySqlError) {
1208
- await session.send([
1209
- import_koishi.h.quote(messageId),
1210
- import_koishi.h.text(session.text(".mysqlError"))
1211
- ]);
1212
- } else {
1213
- throw new Error(error);
1214
- }
1483
+ const { task, pendingAhead, queuePosition } = queue.add({
1484
+ type: "album",
1485
+ id,
1486
+ session,
1487
+ messageId
1488
+ });
1489
+ const params = {
1490
+ id,
1491
+ ahead: pendingAhead,
1492
+ pos: queuePosition,
1493
+ status: task.status
1494
+ };
1495
+ const msg = [import_koishi2.h.quote(messageId)];
1496
+ if (pendingAhead === 0 && queuePosition === 1) {
1497
+ msg.push(import_koishi2.h.text(session.text(".queueFirst", params)));
1498
+ } else if (pendingAhead > 0) {
1499
+ msg.push(import_koishi2.h.text(session.text(".queuePosition", params)));
1500
+ } else {
1501
+ msg.push(import_koishi2.h.text(session.text(".queueProcessing", params)));
1215
1502
  }
1503
+ await session.send(msg);
1504
+ }, "handleAlbumOrPhoto");
1505
+ ctx.command("jm.album <albumId:string>").alias("本子").action(async ({ session, options }, albumId) => {
1506
+ await handleAlbumOrPhoto(session, albumId);
1216
1507
  });
1217
1508
  ctx.command("jm.photo <photoId:string>").alias("本子章节").action(async ({ session }, photoId) => {
1218
- const messageId = session.messageId;
1219
- if (!/^\d+$/.test(photoId)) {
1220
- await session.send([
1221
- import_koishi.h.quote(messageId),
1222
- import_koishi.h.text("输入的ID不合法,请检查")
1223
- ]);
1224
- return;
1225
- }
1226
- try {
1227
- const jmClient = new JMAppClient(root);
1228
- const photo = await jmClient.getPhotoById(photoId);
1229
- await jmClient.downloadByPhoto(photo);
1230
- const photoName = photo.getName();
1231
- let filePath;
1232
- if (config.sendMethod === "zip") {
1233
- filePath = await jmClient.photoToZip(
1234
- photo,
1235
- photoName,
1236
- config.password,
1237
- config.level
1238
- );
1239
- } else {
1240
- filePath = await jmClient.photoToPdf(photo, photoName);
1241
- }
1242
- const { fileName, ext, dir } = getFileInfo(filePath);
1243
- const name2 = formatFileName(config.fileName, fileName, photoId);
1244
- if (debug) logger.info(`文件名:${name2}.${ext}`);
1245
- if (config.fileMethod === "buffer") {
1246
- const buffer = await (0, import_promises3.readFile)(filePath);
1247
- await session.send([
1248
- import_koishi.h.file(buffer, ext, { title: `${name2} (${photoId}).${ext}` })
1249
- ]);
1250
- } else {
1251
- await session.send([
1252
- import_koishi.h.file(`file:///${filePath}`, { title: `${name2}.${ext}` })
1253
- ]);
1254
- }
1255
- if (!config.cache) (0, import_promises3.rm)(dir, { recursive: true });
1256
- } catch (error) {
1257
- if (error instanceof PhotoNotExistError) {
1258
- await session.send([
1259
- import_koishi.h.quote(messageId),
1260
- import_koishi.h.text(session.text(".notExistError"))
1261
- ]);
1262
- } else if (error instanceof MySqlError) {
1263
- await session.send([
1264
- import_koishi.h.quote(messageId),
1265
- import_koishi.h.text(session.text(".mysqlError"))
1266
- ]);
1267
- } else {
1268
- throw new Error(error);
1269
- }
1270
- }
1509
+ await handleAlbumOrPhoto(session, photoId);
1271
1510
  });
1272
1511
  ctx.command("jm.album.info <albumId:string>").alias("本子信息").action(async ({ session, options }, albumId) => {
1273
1512
  const messageId = session.messageId;
1274
1513
  if (!/^\d+$/.test(albumId)) {
1275
1514
  await session.send([
1276
- import_koishi.h.quote(messageId),
1277
- import_koishi.h.text("输入的ID不合法,请检查")
1515
+ import_koishi2.h.quote(messageId),
1516
+ import_koishi2.h.text("输入的ID不合法,请检查")
1278
1517
  ]);
1279
1518
  return;
1280
1519
  }
1281
1520
  try {
1282
- const jmClient = new JMAppClient(root);
1521
+ const jmClient = new JMAppClient(root, ctx.http, config, logger);
1283
1522
  const album = await jmClient.getAlbumById(albumId);
1284
1523
  await session.send([
1285
- import_koishi.h.quote(messageId),
1286
- import_koishi.h.text(`ID:${album.getId()}
1524
+ import_koishi2.h.quote(messageId),
1525
+ import_koishi2.h.text(`ID:${album.getId()}
1287
1526
  `),
1288
- import_koishi.h.text(`名称:${album.getName()}
1527
+ import_koishi2.h.text(`名称:${album.getName()}
1289
1528
  `),
1290
- import_koishi.h.text(`章节数:${album.getPhotos().length}
1529
+ import_koishi2.h.text(`章节数:${album.getPhotos().length}
1291
1530
  `),
1292
- import_koishi.h.text(`作者:${album.getAuthors()?.join("、") ?? ""}
1531
+ import_koishi2.h.text(`作者:${album.getAuthors()?.join("、") ?? ""}
1293
1532
  `),
1294
- import_koishi.h.text(`登场人物:${album.getActors()?.join("、") ?? ""}
1533
+ import_koishi2.h.text(`登场人物:${album.getActors()?.join("、") ?? ""}
1295
1534
  `),
1296
- import_koishi.h.text(`点赞数:${album.getLikes()}
1535
+ import_koishi2.h.text(`点赞数:${album.getLikes()}
1297
1536
  `),
1298
- import_koishi.h.text(`观看数:${album.getTotalViews()}`)
1537
+ import_koishi2.h.text(`观看数:${album.getTotalViews()}`)
1299
1538
  ]);
1300
1539
  } catch (error) {
1301
1540
  if (error instanceof AlbumNotExistError) {
1302
1541
  await session.send([
1303
- import_koishi.h.quote(messageId),
1304
- import_koishi.h.text(session.text(".notExistError"))
1542
+ import_koishi2.h.quote(messageId),
1543
+ import_koishi2.h.text(session.text(".notExistError"))
1305
1544
  ]);
1306
1545
  } else if (error instanceof MySqlError) {
1307
1546
  await session.send([
1308
- import_koishi.h.quote(messageId),
1309
- import_koishi.h.text(session.text(".mysqlError"))
1547
+ import_koishi2.h.quote(messageId),
1548
+ import_koishi2.h.text(session.text(".mysqlError"))
1310
1549
  ]);
1311
1550
  } else {
1312
1551
  throw new Error(error);
1313
1552
  }
1314
1553
  }
1315
1554
  });
1555
+ ctx.command("jm.queue").alias("本子队列").action(async ({ session, options }) => {
1556
+ const messageId = session.messageId;
1557
+ const allTasks = queue.getAllTasks();
1558
+ const pendingOrProcessingTasks = allTasks.filter(
1559
+ (task) => task.status !== "completed"
1560
+ );
1561
+ if (pendingOrProcessingTasks.length === 0) {
1562
+ await session.send([
1563
+ import_koishi2.h.quote(messageId),
1564
+ import_koishi2.h.text(session.text(".emptyQueue"))
1565
+ ]);
1566
+ return;
1567
+ }
1568
+ const statusMap = {
1569
+ pending: session.text(".task.pending"),
1570
+ processing: session.text(".task.processing"),
1571
+ failed: session.text(".task.failed")
1572
+ };
1573
+ const typeMap = {
1574
+ album: session.text(".type.album"),
1575
+ photo: session.text(".type.photo")
1576
+ };
1577
+ const taskInfos = pendingOrProcessingTasks.map((task) => {
1578
+ return import_koishi2.h.text(
1579
+ session.text(".msgFormat", {
1580
+ id: task.payload.id,
1581
+ type: typeMap[task.payload.type],
1582
+ status: statusMap[task.status]
1583
+ })
1584
+ );
1585
+ });
1586
+ await session.send([import_koishi2.h.quote(messageId), ...taskInfos]);
1587
+ });
1316
1588
  }
1317
1589
  __name(apply, "apply");
1318
1590
  // Annotate the CommonJS export names for ESM import in node:
1319
1591
  0 && (module.exports = {
1320
1592
  Config,
1321
1593
  apply,
1322
- debug,
1323
- http,
1324
1594
  inject,
1325
- logger,
1326
- name,
1327
- retryCount
1595
+ name
1328
1596
  });
@@ -0,0 +1,20 @@
1
+ import { Config } from "..";
2
+ import { Logger, Session, HTTP } from "koishi";
3
+ export type JmTaskPayload = {
4
+ type: "album" | "photo";
5
+ id: string;
6
+ session: Session;
7
+ messageId: string;
8
+ };
9
+ interface ProcessorConfig {
10
+ root: string;
11
+ sendMethod: "zip" | "pdf";
12
+ password?: string;
13
+ level?: number;
14
+ fileName: string;
15
+ fileMethod: "buffer" | "file";
16
+ cache: boolean;
17
+ debug: boolean;
18
+ }
19
+ export declare const createJmProcessor: (processorConfig: ProcessorConfig, http: HTTP, config: Config, logger: Logger) => (payload: JmTaskPayload) => Promise<void>;
20
+ export {};
@@ -0,0 +1,75 @@
1
+ import { Config } from "..";
2
+ import { Logger } from "koishi";
3
+ import { JmTaskPayload } from "../processors/jmProcessor";
4
+ /**
5
+ * 任务可能的状态
6
+ * pending: 等待处理
7
+ * processing: 正在处理
8
+ * completed: 处理完成
9
+ * failed: 处理失败
10
+ */
11
+ type TaskStatus = "pending" | "processing" | "completed" | "failed";
12
+ interface Task {
13
+ id: string;
14
+ payload: JmTaskPayload;
15
+ status: TaskStatus;
16
+ createdAt: string;
17
+ processedAt?: string;
18
+ error?: string;
19
+ }
20
+ type TaskProcessor = (payload: JmTaskPayload) => Promise<void>;
21
+ interface QueueOptions {
22
+ concurrency?: number;
23
+ }
24
+ interface AddTaskResult {
25
+ task: Task;
26
+ pendingAhead: number;
27
+ queuePosition: number;
28
+ }
29
+ export declare class Queue {
30
+ private tasks;
31
+ private processor;
32
+ private readonly concurrency;
33
+ private activeTasks;
34
+ /**
35
+ * koishi 配置项
36
+ */
37
+ private config;
38
+ /**
39
+ * koishi 日志
40
+ */
41
+ private logger;
42
+ constructor(processor: TaskProcessor, options: QueueOptions, config: Config, logger: Logger);
43
+ /**
44
+ * 向队列添加一个新任务
45
+ */
46
+ add(payload: JmTaskPayload): AddTaskResult;
47
+ /**
48
+ * 获取任务状态
49
+ */
50
+ getTask(id: string): Task | undefined;
51
+ /**
52
+ * 获取所有任务的只读列表
53
+ */
54
+ getAllTasks(): Readonly<Task[]>;
55
+ /**
56
+ * 获取指定任务在队列中的位置信息
57
+ * @param taskId 任务ID
58
+ * @returns {pendingAhead: number, queuePosition: number}
59
+ * pendingAhead: 在此任务之前有多少个待处理任务
60
+ * queuePosition: 此任务在所有待处理任务中的位置(从1开始)
61
+ */
62
+ getTaskQueuePosition(taskId: string): {
63
+ pendingAhead: number;
64
+ queuePosition: number;
65
+ };
66
+ /**
67
+ * 检查并处理队列中的任务
68
+ */
69
+ private _processQueue;
70
+ /**
71
+ * 执行单个任务
72
+ */
73
+ private _runTask;
74
+ }
75
+ export {};
@@ -1,4 +1,5 @@
1
- import { HTTP } from "koishi";
1
+ import { Config } from "..";
2
+ import { HTTP, Logger } from "koishi";
2
3
  import { IJMResponse } from "../types/JMClient";
3
4
  /**
4
5
  * 文件是否存在
@@ -22,7 +23,7 @@ export declare function sanitizeFileName(fileName: string): string;
22
23
  * 从github获取最新的JM域名
23
24
  * @returns 域名列表
24
25
  */
25
- export declare function getDomainFromGithub(): Promise<any[]>;
26
+ export declare function getDomainFromGithub(http: HTTP): Promise<any[]>;
26
27
  /**
27
28
  * 限制Promise并发
28
29
  * @param promises Promise方法列表
@@ -38,7 +39,7 @@ export declare function limitPromiseAll<T>(promises: (() => Promise<T>)[], limit
38
39
  * @param retryIndex 尝试次数
39
40
  * @returns 请求结果
40
41
  */
41
- export declare function requestWithRetry<T = IJMResponse>(url: string, method: "GET" | "POST", config?: HTTP.RequestConfig, retryIndex?: number): Promise<T>;
42
+ export declare function requestWithRetry<T = IJMResponse>(url: string, method: "GET" | "POST", config: HTTP.RequestConfig, http: HTTP, pluginsConfig: Config, logger: Logger, retryIndex?: number): Promise<T>;
42
43
  /**
43
44
  * 依次使用定义的地址尝试进行请求,遇到特定错误尝试切换下一个
44
45
  * @param url 请求地址
@@ -47,7 +48,7 @@ export declare function requestWithRetry<T = IJMResponse>(url: string, method: "
47
48
  * @param urlIndex 地址下标,默认从0开始尝试
48
49
  * @returns 请求结果
49
50
  */
50
- export declare function requestWithUrlSwitch<T = IJMResponse>(url: string, method: "GET" | "POST", config?: HTTP.RequestConfig, type?: "IMAGE" | "CLIENT", urlIndex?: number): Promise<T>;
51
+ export declare function requestWithUrlSwitch<T = IJMResponse>(url: string, method: "GET" | "POST", config: HTTP.RequestConfig, http: HTTP, pluginsConfig: Config, logger: Logger, type?: "IMAGE" | "CLIENT", urlIndex?: number): Promise<T>;
51
52
  /**
52
53
  * 获取文件名和扩展名
53
54
  * @param filePath 文件路径
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wahaha216/koishi-plugin-jmcomic",
3
3
  "description": "下载JM本子,无需python。支持pdf、zip加密。",
4
- "version": "0.0.6",
4
+ "version": "0.1.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -10,12 +10,33 @@
10
10
  jm album xxxxxx
11
11
  jm album info xxxxxx
12
12
  jm photo xxxxxx
13
+ jm queue
13
14
  ```
14
15
 
15
16
  可在配置中配置是发送 PDF 还是 ZIP 压缩包,支持加密。
16
17
 
17
18
  ## 更新日志
18
19
 
20
+ <details>
21
+ <summary>0.1.1</summary>
22
+
23
+ 1. 添加队列时返回队列信息
24
+ 2. 提取代码
25
+
26
+ </details>
27
+
28
+ <details>
29
+ <summary>0.1.0</summary>
30
+
31
+ 1. 队列系统
32
+ 2. 下载并发与解密并发限制
33
+ 3. 修改配置页面顺序、分类
34
+ 4. 不再直接暴露变量,改为逐级传递
35
+ 5. 统一暴露Error类
36
+ 6. 添加域名切换条件
37
+
38
+ </details>
39
+
19
40
  <details>
20
41
  <summary>0.0.6</summary>
21
42
  添加了一些错误提示
@@ -28,9 +49,9 @@ jm photo xxxxxx
28
49
 
29
50
  <details>
30
51
  <summary>0.0.4</summary>
31
- 1.文件名移除前后空格
32
52
 
33
- 2.新增文件发送配置,用于配置文件是以 buffer 读取后发送还是以本地地址的形式发送。docker 中使用 file 形式需要在 bot 实现端同时映射/koishi 目录
53
+ 1. 文件名移除前后空格
54
+ 2. 新增文件发送配置,用于配置文件是以 buffer 读取后发送还是以本地地址的形式发送。docker 中使用 file 形式需要在 bot 实现端同时映射/koishi 目录
34
55
 
35
56
  </details>
36
57