koishi-plugin-maibot 1.7.36 → 1.7.38

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/lib/index.d.ts CHANGED
@@ -51,6 +51,8 @@ export interface Config {
51
51
  enabled: boolean;
52
52
  refIdLabel: string;
53
53
  };
54
+ errorHelpUrl?: string;
55
+ b50PollInterval?: number;
54
56
  }
55
57
  export declare const Config: Schema<Config>;
56
58
  export declare function apply(ctx: Context, config: Config): void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAW,MAAM,QAAQ,CAAA;AAIjD,eAAO,MAAM,IAAI,WAAW,CAAA;AAC5B,eAAO,MAAM,MAAM,UAAe,CAAA;AAElC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,MAAM;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,WAAW,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,CAAC,EAAE;QAClB,OAAO,EAAE,OAAO,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,aAAa,CAAC,EAAE;QACd,YAAY,EAAE,MAAM,CAAA;QACpB,aAAa,EAAE,MAAM,CAAA;KACtB,CAAA;IACD,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,SAAS,CAAC,EAAE;QACV,OAAO,EAAE,OAAO,CAAA;QAChB,QAAQ,EAAE,MAAM,EAAE,CAAA;QAClB,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,CAAC,EAAE;QACN,OAAO,EAAE,OAAO,CAAA;QAChB,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,YAAY,CAAC,EAAE;QACb,OAAO,EAAE,OAAO,CAAA;QAChB,UAAU,EAAE,MAAM,CAAA;KACnB,CAAA;CACF;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CAoEhC,CAAA;AA4iCF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAkhJjD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAW,MAAM,QAAQ,CAAA;AAIjD,eAAO,MAAM,IAAI,WAAW,CAAA;AAC5B,eAAO,MAAM,MAAM,UAAe,CAAA;AAElC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,MAAM;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,WAAW,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,CAAC,EAAE;QAClB,OAAO,EAAE,OAAO,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,aAAa,CAAC,EAAE;QACd,YAAY,EAAE,MAAM,CAAA;QACpB,aAAa,EAAE,MAAM,CAAA;KACtB,CAAA;IACD,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,SAAS,CAAC,EAAE;QACV,OAAO,EAAE,OAAO,CAAA;QAChB,QAAQ,EAAE,MAAM,EAAE,CAAA;QAClB,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,CAAC,EAAE;QACN,OAAO,EAAE,OAAO,CAAA;QAChB,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,YAAY,CAAC,EAAE;QACb,OAAO,EAAE,OAAO,CAAA;QAChB,UAAU,EAAE,MAAM,CAAA;KACnB,CAAA;IACD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CAsEhC,CAAA;AAymCF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QA+qJjD"}
package/lib/index.js CHANGED
@@ -75,6 +75,8 @@ exports.Config = koishi_1.Schema.object({
75
75
  enabled: true,
76
76
  refIdLabel: 'Ref_ID',
77
77
  }),
78
+ errorHelpUrl: koishi_1.Schema.string().default('https://awmc.cc/forums/8/').description('任务出错时引导用户提问的URL(留空则不显示引导信息)'),
79
+ b50PollInterval: koishi_1.Schema.number().default(2000).description('B50任务轮询间隔(毫秒),默认2000毫秒'),
78
80
  });
79
81
  // 我认识了很多朋友 以下是我认识的好朋友们!
80
82
  // Fracture_Hikaritsu
@@ -556,11 +558,13 @@ class RequestQueue {
556
558
  function processSGID(input) {
557
559
  const trimmed = input.trim();
558
560
  // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
559
- const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
561
+ const isReqLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
562
+ // 检查是否为二维码图片链接格式(https://wq.wahlap.net/qrcode/img/)
563
+ const isImgLink = trimmed.includes('https://wq.wahlap.net/qrcode/img/');
560
564
  const isSGID = trimmed.startsWith('SGWCMAID');
561
565
  let qrText = trimmed;
562
566
  // 如果是网页地址,提取MAID并转换为SGWCMAID格式
563
- if (isLink) {
567
+ if (isReqLink) {
564
568
  try {
565
569
  // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
566
570
  // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
@@ -578,6 +582,24 @@ function processSGID(input) {
578
582
  return null;
579
583
  }
580
584
  }
585
+ else if (isImgLink) {
586
+ try {
587
+ // 从图片URL中提取MAID部分:https://wq.wahlap.net/qrcode/img/MAID260128205107...png?v
588
+ // 匹配 /qrcode/img/ 后面的 MAID 开头的内容(到 .png 或 ? 之前)
589
+ const match = trimmed.match(/qrcode\/img\/(MAID[^?\.]+)/i);
590
+ if (match && match[1]) {
591
+ const maid = match[1];
592
+ // 在前面加上 SGWC 变成 SGWCMAID...
593
+ qrText = 'SGWC' + maid;
594
+ }
595
+ else {
596
+ return null;
597
+ }
598
+ }
599
+ catch (error) {
600
+ return null;
601
+ }
602
+ }
581
603
  else if (!isSGID) {
582
604
  return null;
583
605
  }
@@ -722,10 +744,13 @@ async function getQrText(session, ctx, api, binding, config, timeout = 60000, pr
722
744
  logger.debug(`收到用户输入: ${trimmed.substring(0, 50)}`);
723
745
  let qrText = trimmed;
724
746
  // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
725
- const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
747
+ const isReqLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
748
+ // 检查是否为二维码图片链接格式(https://wq.wahlap.net/qrcode/img/)
749
+ const isImgLink = trimmed.includes('https://wq.wahlap.net/qrcode/img/');
750
+ const isLink = isReqLink || isImgLink;
726
751
  const isSGID = trimmed.startsWith('SGWCMAID');
727
752
  // 如果是网页地址,提取MAID并转换为SGWCMAID格式
728
- if (isLink) {
753
+ if (isReqLink) {
729
754
  try {
730
755
  // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
731
756
  // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
@@ -737,19 +762,41 @@ async function getQrText(session, ctx, api, binding, config, timeout = 60000, pr
737
762
  logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrText.substring(0, 24)}...`);
738
763
  }
739
764
  else {
740
- await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
765
+ await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
741
766
  return { qrText: '', error: '无法从网页地址中提取MAID' };
742
767
  }
743
768
  }
744
769
  catch (error) {
745
770
  logger.warn('解析网页地址失败:', error);
746
- await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
771
+ await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
747
772
  return { qrText: '', error: '网页地址格式错误' };
748
773
  }
749
774
  }
775
+ else if (isImgLink) {
776
+ try {
777
+ // 从图片URL中提取MAID部分:https://wq.wahlap.net/qrcode/img/MAID260128205107...png?v
778
+ // 匹配 /qrcode/img/ 后面的 MAID 开头的内容(到 .png 或 ? 之前)
779
+ const match = trimmed.match(/qrcode\/img\/(MAID[^?\.]+)/i);
780
+ if (match && match[1]) {
781
+ const maid = match[1];
782
+ // 在前面加上 SGWC 变成 SGWCMAID...
783
+ qrText = 'SGWC' + maid;
784
+ logger.info(`从图片地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrText.substring(0, 24)}...`);
785
+ }
786
+ else {
787
+ await session.send('⚠️ 无法从图片地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
788
+ return { qrText: '', error: '无法从图片地址中提取MAID' };
789
+ }
790
+ }
791
+ catch (error) {
792
+ logger.warn('解析图片地址失败:', error);
793
+ await session.send('⚠️ 图片地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
794
+ return { qrText: '', error: '图片地址格式错误' };
795
+ }
796
+ }
750
797
  else if (!isSGID) {
751
- await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)');
752
- return { qrText: '', error: '无效的二维码格式,必须是SGID文本或网页地址' };
798
+ await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
799
+ return { qrText: '', error: '无效的二维码格式,必须是SGID文本或网页/图片地址' };
753
800
  }
754
801
  // 验证SGID格式和长度
755
802
  if (!qrText.startsWith('SGWCMAID')) {
@@ -760,7 +807,7 @@ async function getQrText(session, ctx, api, binding, config, timeout = 60000, pr
760
807
  await session.send('❌ SGID长度错误,应在48-128字符之间');
761
808
  return { qrText: '', error: '二维码长度错误,应在48-128字符之间' };
762
809
  }
763
- logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrText.substring(0, 50)}...`);
810
+ logger.info(`✅ 接收到${isLink ? '链接地址(已转换)' : 'SGID'}: ${qrText.substring(0, 50)}...`);
764
811
  // 尝试撤回用户发送的消息(如果启用了自动撤回)
765
812
  await tryRecallMessage(session, ctx, config);
766
813
  await session.send('⏳ 正在处理,请稍候...');
@@ -855,10 +902,13 @@ async function promptForRebind(session, ctx, api, binding, config, timeout = 600
855
902
  logger.debug(`收到用户输入: ${trimmed.substring(0, 50)}`);
856
903
  let qrCode = trimmed;
857
904
  // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
858
- const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
905
+ const isReqLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
906
+ // 检查是否为二维码图片链接格式(https://wq.wahlap.net/qrcode/img/)
907
+ const isImgLink = trimmed.includes('https://wq.wahlap.net/qrcode/img/');
908
+ const isLink = isReqLink || isImgLink;
859
909
  const isSGID = trimmed.startsWith('SGWCMAID');
860
910
  // 如果是网页地址,提取MAID并转换为SGWCMAID格式
861
- if (isLink) {
911
+ if (isReqLink) {
862
912
  try {
863
913
  // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
864
914
  // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
@@ -870,19 +920,41 @@ async function promptForRebind(session, ctx, api, binding, config, timeout = 600
870
920
  logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
871
921
  }
872
922
  else {
873
- await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
923
+ await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
874
924
  return { success: false, error: '无法从网页地址中提取MAID', messageId: promptMessageId };
875
925
  }
876
926
  }
877
927
  catch (error) {
878
928
  logger.warn('解析网页地址失败:', error);
879
- await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
929
+ await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
880
930
  return { success: false, error: '网页地址格式错误', messageId: promptMessageId };
881
931
  }
882
932
  }
933
+ else if (isImgLink) {
934
+ try {
935
+ // 从图片URL中提取MAID部分:https://wq.wahlap.net/qrcode/img/MAID260128205107...png?v
936
+ // 匹配 /qrcode/img/ 后面的 MAID 开头的内容(到 .png 或 ? 之前)
937
+ const match = trimmed.match(/qrcode\/img\/(MAID[^?\.]+)/i);
938
+ if (match && match[1]) {
939
+ const maid = match[1];
940
+ // 在前面加上 SGWC 变成 SGWCMAID...
941
+ qrCode = 'SGWC' + maid;
942
+ logger.info(`从图片地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
943
+ }
944
+ else {
945
+ await session.send('⚠️ 无法从图片地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
946
+ return { success: false, error: '无法从图片地址中提取MAID', messageId: promptMessageId };
947
+ }
948
+ }
949
+ catch (error) {
950
+ logger.warn('解析图片地址失败:', error);
951
+ await session.send('⚠️ 图片地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
952
+ return { success: false, error: '图片地址格式错误', messageId: promptMessageId };
953
+ }
954
+ }
883
955
  else if (!isSGID) {
884
- await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)');
885
- return { success: false, error: '无效的二维码格式,必须是SGID文本或网页地址', messageId: promptMessageId };
956
+ await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
957
+ return { success: false, error: '无效的二维码格式,必须是SGID文本或网页/图片地址', messageId: promptMessageId };
886
958
  }
887
959
  // 验证SGID格式和长度
888
960
  if (!qrCode.startsWith('SGWCMAID')) {
@@ -893,7 +965,7 @@ async function promptForRebind(session, ctx, api, binding, config, timeout = 600
893
965
  await session.send('❌ 识别失败:SGID长度错误,应在48-128字符之间');
894
966
  return { success: false, error: '二维码长度错误,应在48-128字符之间', messageId: promptMessageId };
895
967
  }
896
- logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
968
+ logger.info(`✅ 接收到${isLink ? '链接地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
897
969
  // 发送识别中反馈
898
970
  await session.send('⏳ 正在处理,请稍候...');
899
971
  // 使用新API获取用户信息
@@ -970,6 +1042,104 @@ function apply(ctx, config) {
970
1042
  const requestQueue = queueConfig.enabled ? new RequestQueue(queueConfig.interval) : null;
971
1043
  // 操作记录配置
972
1044
  const operationLogConfig = config.operationLog || { enabled: true, refIdLabel: 'Ref_ID' };
1045
+ // 错误帮助URL配置
1046
+ const errorHelpUrl = config.errorHelpUrl || '';
1047
+ /**
1048
+ * 获取上传任务的统计信息(平均处理时长和今日成功率)
1049
+ * @param commandPrefix 命令前缀,用于筛选日志(如 'mai上传B50' 或 'mai上传落雪b50')
1050
+ * @returns 统计信息字符串
1051
+ */
1052
+ async function getUploadStats(commandPrefix) {
1053
+ try {
1054
+ const today = new Date();
1055
+ today.setHours(0, 0, 0, 0);
1056
+ const todayStart = today.getTime();
1057
+ // 获取今日所有相关操作记录
1058
+ const allLogs = await ctx.database.get('maibot_operation_logs', {});
1059
+ const todayLogs = allLogs.filter(log => {
1060
+ const logTime = new Date(log.createdAt).getTime();
1061
+ return logTime >= todayStart && log.command.startsWith(commandPrefix);
1062
+ });
1063
+ if (todayLogs.length === 0) {
1064
+ return '';
1065
+ }
1066
+ // 统计成功率
1067
+ const taskCompleteLogs = todayLogs.filter(log => log.command.includes('-任务完成'));
1068
+ const successCount = taskCompleteLogs.filter(log => log.status === 'success').length;
1069
+ const failureCount = taskCompleteLogs.filter(log => log.status === 'failure').length;
1070
+ const totalCompleted = successCount + failureCount;
1071
+ // 计算平均处理时长(从任务提交到任务完成)
1072
+ let avgDuration = 0;
1073
+ let durationCount = 0;
1074
+ // 获取所有任务提交记录和对应的完成记录
1075
+ const submitLogs = todayLogs.filter(log => log.command === commandPrefix && log.status === 'success');
1076
+ for (const submitLog of submitLogs) {
1077
+ // 尝试从 apiResponse 中获取 task_id
1078
+ if (!submitLog.apiResponse)
1079
+ continue;
1080
+ try {
1081
+ const response = JSON.parse(submitLog.apiResponse);
1082
+ const taskId = response.task_id;
1083
+ if (!taskId)
1084
+ continue;
1085
+ // 查找对应的完成记录
1086
+ const completeLog = taskCompleteLogs.find(log => {
1087
+ if (!log.apiResponse)
1088
+ return false;
1089
+ try {
1090
+ const completeResponse = JSON.parse(log.apiResponse);
1091
+ return completeResponse.alive_task_id === taskId || String(completeResponse.alive_task_id) === String(taskId);
1092
+ }
1093
+ catch {
1094
+ return false;
1095
+ }
1096
+ });
1097
+ if (completeLog) {
1098
+ const submitTime = new Date(submitLog.createdAt).getTime();
1099
+ const completeTime = new Date(completeLog.createdAt).getTime();
1100
+ const duration = (completeTime - submitTime) / 1000; // 转换为秒
1101
+ if (duration > 0 && duration < 600) { // 排除异常数据(超过10分钟的)
1102
+ avgDuration += duration;
1103
+ durationCount++;
1104
+ }
1105
+ }
1106
+ }
1107
+ catch {
1108
+ continue;
1109
+ }
1110
+ }
1111
+ // 计算平均时长
1112
+ if (durationCount > 0) {
1113
+ avgDuration = avgDuration / durationCount;
1114
+ }
1115
+ // 计算成功率
1116
+ const successRate = totalCompleted > 0 ? Math.round((successCount / totalCompleted) * 100) : 0;
1117
+ // 构建统计信息字符串
1118
+ let statsStr = '';
1119
+ if (avgDuration > 0) {
1120
+ statsStr += `平均处理用时 ${avgDuration.toFixed(1)} s`;
1121
+ }
1122
+ if (totalCompleted > 0) {
1123
+ if (statsStr)
1124
+ statsStr += ',';
1125
+ statsStr += `今日成功率 ${successRate}%`;
1126
+ }
1127
+ return statsStr;
1128
+ }
1129
+ catch (error) {
1130
+ logger.warn('获取上传统计信息失败:', error);
1131
+ return '';
1132
+ }
1133
+ }
1134
+ /**
1135
+ * 获取错误帮助信息(如果配置了帮助URL)
1136
+ */
1137
+ function getErrorHelpInfo() {
1138
+ if (!errorHelpUrl) {
1139
+ return '';
1140
+ }
1141
+ return `\n\n如有问题,请前往 ${errorHelpUrl} 提问`;
1142
+ }
973
1143
  /**
974
1144
  * 生成唯一的 ref_id
975
1145
  */
@@ -1257,9 +1427,10 @@ function apply(ctx, config) {
1257
1427
  }
1258
1428
  const mention = buildMention(session);
1259
1429
  const guildId = session.guildId;
1260
- const maxAttempts = 300; // 10分钟超时:300次 * 2秒 = 600秒 = 10分钟
1261
- const interval = 2000; // 每2秒轮询一次
1262
- const initialDelay = 2000; // 首次延迟2秒后开始检查
1430
+ const pollInterval = config.b50PollInterval || 2000;
1431
+ const maxAttempts = Math.ceil(600000 / pollInterval); // 10分钟超时
1432
+ const interval = pollInterval;
1433
+ const initialDelay = pollInterval; // 首次延迟与轮询间隔相同
1263
1434
  let attempts = 0;
1264
1435
  const poll = async () => {
1265
1436
  attempts += 1;
@@ -1271,7 +1442,7 @@ function apply(ctx, config) {
1271
1442
  if (isDone || hasError) {
1272
1443
  // 任务完成或出错,发送通知并停止
1273
1444
  const statusText = hasError
1274
- ? `❌ 任务失败:${detail.error}`
1445
+ ? `❌ 任务失败:${detail.error}${getErrorHelpInfo()}`
1275
1446
  : '✅ 任务已完成';
1276
1447
  const finishTime = detail.alive_task_end_time
1277
1448
  ? `\n完成时间: ${new Date((typeof detail.alive_task_end_time === 'number' ? detail.alive_task_end_time : parseInt(String(detail.alive_task_end_time))) * 1000).toLocaleString('zh-CN')}`
@@ -1301,7 +1472,7 @@ function apply(ctx, config) {
1301
1472
  status: 'failure',
1302
1473
  errorMessage: '任务轮询超时(10分钟)',
1303
1474
  });
1304
- let msg = `${mention} 水鱼B50任务 ${taskId} 上传失败,请稍后再试一次。`;
1475
+ let msg = `${mention} 水鱼B50任务 ${taskId} 上传失败,请稍后再试一次。${getErrorHelpInfo()}`;
1305
1476
  const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
1306
1477
  if (maintenanceMsg) {
1307
1478
  msg += `\n${maintenanceMsg}`;
@@ -1321,7 +1492,7 @@ function apply(ctx, config) {
1321
1492
  status: 'error',
1322
1493
  errorMessage: error instanceof Error ? error.message : '未知错误',
1323
1494
  });
1324
- let msg = `${mention} 水鱼B50任务 ${taskId} 上传失败,请稍后再试一次。`;
1495
+ let msg = `${mention} 水鱼B50任务 ${taskId} 上传失败,请稍后再试一次。${getErrorHelpInfo()}`;
1325
1496
  const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
1326
1497
  if (maintenanceMsg) {
1327
1498
  msg += `\n${maintenanceMsg}`;
@@ -1341,9 +1512,10 @@ function apply(ctx, config) {
1341
1512
  }
1342
1513
  const mention = buildMention(session);
1343
1514
  const guildId = session.guildId;
1344
- const maxAttempts = 300; // 10分钟超时:300次 * 2秒 = 600秒 = 10分钟
1345
- const interval = 2000; // 每2秒轮询一次
1346
- const initialDelay = 2000; // 首次延迟2秒后开始检查
1515
+ const pollInterval = config.b50PollInterval || 2000;
1516
+ const maxAttempts = Math.ceil(600000 / pollInterval); // 10分钟超时
1517
+ const interval = pollInterval;
1518
+ const initialDelay = pollInterval; // 首次延迟与轮询间隔相同
1347
1519
  let attempts = 0;
1348
1520
  const poll = async () => {
1349
1521
  attempts += 1;
@@ -1355,7 +1527,7 @@ function apply(ctx, config) {
1355
1527
  if (isDone || hasError) {
1356
1528
  // 任务完成或出错,发送通知并停止
1357
1529
  const statusText = hasError
1358
- ? `❌ 任务失败:${detail.error}`
1530
+ ? `❌ 任务失败:${detail.error}${getErrorHelpInfo()}`
1359
1531
  : '✅ 任务已完成';
1360
1532
  const finishTime = detail.alive_task_end_time
1361
1533
  ? `\n完成时间: ${new Date((typeof detail.alive_task_end_time === 'number' ? detail.alive_task_end_time : parseInt(String(detail.alive_task_end_time))) * 1000).toLocaleString('zh-CN')}`
@@ -1385,7 +1557,7 @@ function apply(ctx, config) {
1385
1557
  status: 'failure',
1386
1558
  errorMessage: '任务轮询超时(10分钟)',
1387
1559
  });
1388
- let msg = `${mention} 落雪B50任务 ${taskId} 上传失败,请稍后再试一次。`;
1560
+ let msg = `${mention} 落雪B50任务 ${taskId} 上传失败,请稍后再试一次。${getErrorHelpInfo()}`;
1389
1561
  const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
1390
1562
  if (maintenanceMsg) {
1391
1563
  msg += `\n${maintenanceMsg}`;
@@ -1405,7 +1577,7 @@ function apply(ctx, config) {
1405
1577
  status: 'error',
1406
1578
  errorMessage: error instanceof Error ? error.message : '未知错误',
1407
1579
  });
1408
- let msg = `${mention} 落雪B50任务 ${taskId} 上传失败,请稍后再试一次。`;
1580
+ let msg = `${mention} 落雪B50任务 ${taskId} 上传失败,请稍后再试一次。${getErrorHelpInfo()}`;
1409
1581
  const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
1410
1582
  if (maintenanceMsg) {
1411
1583
  msg += `\n${maintenanceMsg}`;
@@ -1691,10 +1863,13 @@ function apply(ctx, config) {
1691
1863
  logger.debug(`收到用户输入: ${trimmed.substring(0, 50)}`);
1692
1864
  qrCode = trimmed;
1693
1865
  // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
1694
- const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
1866
+ const isReqLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
1867
+ // 检查是否为二维码图片链接格式(https://wq.wahlap.net/qrcode/img/)
1868
+ const isImgLink = trimmed.includes('https://wq.wahlap.net/qrcode/img/');
1869
+ const isLink = isReqLink || isImgLink;
1695
1870
  const isSGID = trimmed.startsWith('SGWCMAID');
1696
1871
  // 如果是网页地址,提取MAID并转换为SGWCMAID格式
1697
- if (isLink) {
1872
+ if (isReqLink) {
1698
1873
  try {
1699
1874
  // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
1700
1875
  // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
@@ -1706,30 +1881,52 @@ function apply(ctx, config) {
1706
1881
  logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
1707
1882
  }
1708
1883
  else {
1709
- await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1884
+ await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1710
1885
  throw new Error('无法从网页地址中提取MAID');
1711
1886
  }
1712
1887
  }
1713
1888
  catch (error) {
1714
1889
  logger.warn('解析网页地址失败:', error);
1715
- await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1890
+ await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1716
1891
  throw new Error('网页地址格式错误');
1717
1892
  }
1718
1893
  }
1894
+ else if (isImgLink) {
1895
+ try {
1896
+ // 从图片URL中提取MAID部分:https://wq.wahlap.net/qrcode/img/MAID260128205107...png?v
1897
+ // 匹配 /qrcode/img/ 后面的 MAID 开头的内容(到 .png 或 ? 之前)
1898
+ const match = trimmed.match(/qrcode\/img\/(MAID[^?\.]+)/i);
1899
+ if (match && match[1]) {
1900
+ const maid = match[1];
1901
+ // 在前面加上 SGWC 变成 SGWCMAID...
1902
+ qrCode = 'SGWC' + maid;
1903
+ logger.info(`从图片地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
1904
+ }
1905
+ else {
1906
+ await session.send('⚠️ 无法从图片地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1907
+ throw new Error('无法从图片地址中提取MAID');
1908
+ }
1909
+ }
1910
+ catch (error) {
1911
+ logger.warn('解析图片地址失败:', error);
1912
+ await session.send('⚠️ 图片地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1913
+ throw new Error('图片地址格式错误');
1914
+ }
1915
+ }
1719
1916
  else if (!isSGID) {
1720
- await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)');
1721
- throw new Error('无效的二维码格式,必须是SGID文本或网页地址');
1917
+ await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1918
+ throw new Error('无效的二维码格式,必须是SGID文本或网页/图片地址');
1722
1919
  }
1723
1920
  // 验证SGID格式和长度
1724
1921
  if (!qrCode.startsWith('SGWCMAID')) {
1725
- await session.send('⚠️ 未识别到有效的SGID格式,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1922
+ await session.send('⚠️ 未识别到有效的SGID格式,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1726
1923
  throw new Error('无效的二维码格式,必须以 SGWCMAID 开头');
1727
1924
  }
1728
1925
  if (qrCode.length < 48 || qrCode.length > 128) {
1729
1926
  await session.send('❌ SGID长度错误,应在48-128字符之间');
1730
1927
  throw new Error('二维码长度错误,应在48-128字符之间');
1731
1928
  }
1732
- logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
1929
+ logger.info(`✅ 接收到${isLink ? '链接地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
1733
1930
  // 发送识别中反馈
1734
1931
  await session.send('⏳ 正在处理,请稍候...');
1735
1932
  }
@@ -2856,9 +3053,11 @@ function apply(ctx, config) {
2856
3053
  return '⚠️ 当前账号已有未完成的水鱼B50任务,请稍后再试,无需重复上传。';
2857
3054
  }
2858
3055
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2859
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
3056
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
2860
3057
  }
2861
- const successMessage = `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
3058
+ const statsInfo = await getUploadStats('mai上传B50');
3059
+ const statsStr = statsInfo ? `\n${statsInfo}` : '';
3060
+ const successMessage = `✅ B50上传任务已提交!${statsStr}\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2862
3061
  const refId = await logOperation({
2863
3062
  command: 'mai上传B50',
2864
3063
  session,
@@ -2941,13 +3140,15 @@ function apply(ctx, config) {
2941
3140
  return `✅ 重新绑定成功!请重新执行上传操作。`;
2942
3141
  }
2943
3142
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2944
- return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
3143
+ return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
2945
3144
  }
2946
3145
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2947
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
3146
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
2948
3147
  }
2949
3148
  }
2950
- const successMessage = `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
3149
+ const statsInfo = await getUploadStats('mai上传B50');
3150
+ const statsStr = statsInfo ? `\n${statsInfo}` : '';
3151
+ const successMessage = `✅ B50上传任务已提交!${statsStr}\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2951
3152
  const refId = await logOperation({
2952
3153
  command: 'mai上传B50',
2953
3154
  session,
@@ -2971,7 +3172,7 @@ function apply(ctx, config) {
2971
3172
  if (maintenanceMsg) {
2972
3173
  msg += `\n${maintenanceMsg}`;
2973
3174
  }
2974
- msg += `\n\n${maintenanceMessage}`;
3175
+ msg += `\n\n${maintenanceMessage}${getErrorHelpInfo()}`;
2975
3176
  return msg;
2976
3177
  }
2977
3178
  if (error?.response) {
@@ -3911,9 +4112,11 @@ function apply(ctx, config) {
3911
4112
  return '⚠️ 当前账号已有未完成的落雪B50任务,请稍后再试,无需重复上传。';
3912
4113
  }
3913
4114
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
3914
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
4115
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
3915
4116
  }
3916
- const successMessage = `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
4117
+ const statsInfo = await getUploadStats('mai上传落雪b50');
4118
+ const statsStr = statsInfo ? `\n${statsInfo}` : '';
4119
+ const successMessage = `✅ 落雪B50上传任务已提交!${statsStr}\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
3917
4120
  const refId = await logOperation({
3918
4121
  command: 'mai上传落雪b50',
3919
4122
  session,
@@ -3925,7 +4128,7 @@ function apply(ctx, config) {
3925
4128
  scheduleLxB50Notification(session, result.task_id, refId);
3926
4129
  return appendRefId(successMessage, refId);
3927
4130
  }
3928
- return `❌ 获取二维码失败:${qrTextResult.error}`;
4131
+ return `❌ 获取二维码失败:${qrTextResult.error}${getErrorHelpInfo()}`;
3929
4132
  }
3930
4133
  // 在调用API前加入队列
3931
4134
  await waitForQueue(session);
@@ -3996,13 +4199,15 @@ function apply(ctx, config) {
3996
4199
  return `✅ 重新绑定成功!请重新执行上传操作。`;
3997
4200
  }
3998
4201
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
3999
- return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
4202
+ return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
4000
4203
  }
4001
4204
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
4002
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
4205
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
4003
4206
  }
4004
4207
  }
4005
- const successMessage = `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
4208
+ const statsInfo = await getUploadStats('mai上传落雪b50');
4209
+ const statsStr = statsInfo ? `\n${statsInfo}` : '';
4210
+ const successMessage = `✅ 落雪B50上传任务已提交!${statsStr}\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
4006
4211
  const refId = await logOperation({
4007
4212
  command: 'mai上传落雪b50',
4008
4213
  session,
@@ -4025,12 +4230,12 @@ function apply(ctx, config) {
4025
4230
  if (maintenanceMsg) {
4026
4231
  msg += `\n${maintenanceMsg}`;
4027
4232
  }
4028
- msg += `\n\n${maintenanceMessage}`;
4233
+ msg += `\n\n${maintenanceMessage}${getErrorHelpInfo()}`;
4029
4234
  return msg;
4030
4235
  })()
4031
4236
  : (error?.response
4032
- ? `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`
4033
- : `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`));
4237
+ ? `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}${getErrorHelpInfo()}`
4238
+ : `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}${getErrorHelpInfo()}`));
4034
4239
  const refId = await logOperation({
4035
4240
  command: 'mai上传落雪b50',
4036
4241
  session,
@@ -4998,14 +5203,33 @@ function apply(ctx, config) {
4998
5203
  }
4999
5204
  // 按执行次数排序
5000
5205
  const sortedCommands = Object.entries(commandStats).sort((a, b) => b[1].total - a[1].total);
5206
+ // 获取B50平均处理时长统计
5207
+ const pollInterval = config.b50PollInterval || 2000;
5208
+ const fishStats = await getUploadStats('mai上传B50');
5209
+ const lxStats = await getUploadStats('mai上传落雪b50');
5001
5210
  let result = `📊 今日命令执行统计\n\n`;
5002
5211
  result += `统计时间: ${new Date().toLocaleString('zh-CN')}\n`;
5003
- result += `总操作数: ${todayLogs.length}\n\n`;
5212
+ result += `总操作数: ${todayLogs.length}\n`;
5213
+ result += `轮询间隔: ${pollInterval} ms\n\n`;
5214
+ // B50处理时长统计
5215
+ result += `📈 B50处理统计:\n`;
5216
+ if (fishStats) {
5217
+ result += ` 🐟 水鱼: ${fishStats}\n`;
5218
+ }
5219
+ else {
5220
+ result += ` 🐟 水鱼: 暂无今日数据\n`;
5221
+ }
5222
+ if (lxStats) {
5223
+ result += ` ❄️ 落雪: ${lxStats}\n`;
5224
+ }
5225
+ else {
5226
+ result += ` ❄️ 落雪: 暂无今日数据\n`;
5227
+ }
5004
5228
  if (sortedCommands.length === 0) {
5005
- result += `ℹ️ 今日暂无操作记录`;
5229
+ result += `\nℹ️ 今日暂无操作记录`;
5006
5230
  }
5007
5231
  else {
5008
- result += `各命令执行情况:\n`;
5232
+ result += `\n各命令执行情况:\n`;
5009
5233
  for (const [command, stats] of sortedCommands) {
5010
5234
  result += `\n${command}:\n`;
5011
5235
  result += ` 总次数: ${stats.total}\n`;