koishi-plugin-maibot 1.7.35 → 1.7.37

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
@@ -29,6 +29,7 @@ export interface Config {
29
29
  lockRefreshConcurrency?: number;
30
30
  confirmTimeout?: number;
31
31
  rebindTimeout?: number;
32
+ sgidCacheMinutes?: number;
32
33
  protectionCheckInterval?: number;
33
34
  authLevelForProxy?: number;
34
35
  protectionLockMessage?: string;
@@ -50,6 +51,7 @@ export interface Config {
50
51
  enabled: boolean;
51
52
  refIdLabel: string;
52
53
  };
54
+ errorHelpUrl?: string;
53
55
  }
54
56
  export declare const Config: Schema<Config>;
55
57
  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,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,CAmEhC,CAAA;AA2iCF,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;CACtB;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CAqEhC,CAAA;AAymCF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QA0pJjD"}
package/lib/index.js CHANGED
@@ -42,6 +42,7 @@ exports.Config = koishi_1.Schema.object({
42
42
  lockRefreshConcurrency: koishi_1.Schema.number().default(3).description('锁定账号刷新时的并发数,默认3个账号同时刷新'),
43
43
  confirmTimeout: koishi_1.Schema.number().default(10000).description('确认提示超时时间(毫秒),默认10秒(10000毫秒)'),
44
44
  rebindTimeout: koishi_1.Schema.number().default(60000).description('重新绑定超时时间(毫秒),默认60秒(60000毫秒)'),
45
+ sgidCacheMinutes: koishi_1.Schema.number().default(10).description('SGID缓存有效期(分钟),默认10分钟(0表示禁用缓存)'),
45
46
  protectionCheckInterval: koishi_1.Schema.number().default(60000).description('保护模式检查间隔(毫秒),默认60秒(60000毫秒)'),
46
47
  authLevelForProxy: koishi_1.Schema.number().default(3).description('代操作功能需要的auth等级,默认3'),
47
48
  protectionLockMessage: koishi_1.Schema.string().default('🛡️ 保护模式:{playerid}{at} 你的账号已自动锁定成功').description('保护模式锁定成功消息(支持占位符:{playerid} 玩家名,{at} @用户)'),
@@ -74,6 +75,7 @@ exports.Config = koishi_1.Schema.object({
74
75
  enabled: true,
75
76
  refIdLabel: 'Ref_ID',
76
77
  }),
78
+ errorHelpUrl: koishi_1.Schema.string().default('https://awmc.cc/forums/8/').description('任务出错时引导用户提问的URL(留空则不显示引导信息)'),
77
79
  });
78
80
  // 我认识了很多朋友 以下是我认识的好朋友们!
79
81
  // Fracture_Hikaritsu
@@ -555,11 +557,13 @@ class RequestQueue {
555
557
  function processSGID(input) {
556
558
  const trimmed = input.trim();
557
559
  // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
558
- const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
560
+ const isReqLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
561
+ // 检查是否为二维码图片链接格式(https://wq.wahlap.net/qrcode/img/)
562
+ const isImgLink = trimmed.includes('https://wq.wahlap.net/qrcode/img/');
559
563
  const isSGID = trimmed.startsWith('SGWCMAID');
560
564
  let qrText = trimmed;
561
565
  // 如果是网页地址,提取MAID并转换为SGWCMAID格式
562
- if (isLink) {
566
+ if (isReqLink) {
563
567
  try {
564
568
  // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
565
569
  // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
@@ -577,6 +581,24 @@ function processSGID(input) {
577
581
  return null;
578
582
  }
579
583
  }
584
+ else if (isImgLink) {
585
+ try {
586
+ // 从图片URL中提取MAID部分:https://wq.wahlap.net/qrcode/img/MAID260128205107...png?v
587
+ // 匹配 /qrcode/img/ 后面的 MAID 开头的内容(到 .png 或 ? 之前)
588
+ const match = trimmed.match(/qrcode\/img\/(MAID[^?\.]+)/i);
589
+ if (match && match[1]) {
590
+ const maid = match[1];
591
+ // 在前面加上 SGWC 变成 SGWCMAID...
592
+ qrText = 'SGWC' + maid;
593
+ }
594
+ else {
595
+ return null;
596
+ }
597
+ }
598
+ catch (error) {
599
+ return null;
600
+ }
601
+ }
580
602
  else if (!isSGID) {
581
603
  return null;
582
604
  }
@@ -681,23 +703,24 @@ async function waitForUserReply(session, ctx, timeout) {
681
703
  }
682
704
  /**
683
705
  * 交互式获取二维码文本(qr_text)
684
- * 支持10分钟内使用上次输入的SGID缓存
706
+ * 支持配置的时间内使用上次输入的SGID缓存
685
707
  * 如果缓存存在且有效,直接使用;否则提示用户输入
686
708
  */
687
709
  async function getQrText(session, ctx, api, binding, config, timeout = 60000, promptMessage, useCache = true // 是否使用缓存(默认启用)
688
710
  ) {
689
711
  const logger = ctx.logger('maibot');
690
- // 如果启用缓存且binding存在,检查是否有10分钟内的SGID缓存
691
- if (useCache && binding && binding.lastQrCode && binding.lastQrCodeTime) {
712
+ // 如果启用缓存且binding存在,检查是否有缓存
713
+ const cacheMinutes = config.sgidCacheMinutes ?? 10;
714
+ if (useCache && cacheMinutes > 0 && binding && binding.lastQrCode && binding.lastQrCodeTime) {
692
715
  const cacheAge = Date.now() - new Date(binding.lastQrCodeTime).getTime();
693
- const cacheValidDuration = 10 * 60 * 1000; // 10分钟
716
+ const cacheValidDuration = cacheMinutes * 60 * 1000;
694
717
  if (cacheAge < cacheValidDuration && binding.lastQrCode.startsWith('SGWCMAID')) {
695
718
  logger.info(`使用缓存的SGID(${Math.floor(cacheAge / 1000)}秒前输入)`);
696
719
  // 直接返回缓存的SGID,不验证(让调用方验证,如果失败再提示输入)
697
720
  return { qrText: binding.lastQrCode, fromCache: true };
698
721
  }
699
722
  else {
700
- logger.debug(`缓存已过期(${Math.floor(cacheAge / 1000)}秒前输入,超过10分钟)`);
723
+ logger.debug(`缓存已过期(${Math.floor(cacheAge / 1000)}秒前输入,超过${cacheMinutes}分钟)`);
701
724
  }
702
725
  }
703
726
  // 没有有效缓存,提示用户输入
@@ -720,10 +743,13 @@ async function getQrText(session, ctx, api, binding, config, timeout = 60000, pr
720
743
  logger.debug(`收到用户输入: ${trimmed.substring(0, 50)}`);
721
744
  let qrText = trimmed;
722
745
  // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
723
- const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
746
+ const isReqLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
747
+ // 检查是否为二维码图片链接格式(https://wq.wahlap.net/qrcode/img/)
748
+ const isImgLink = trimmed.includes('https://wq.wahlap.net/qrcode/img/');
749
+ const isLink = isReqLink || isImgLink;
724
750
  const isSGID = trimmed.startsWith('SGWCMAID');
725
751
  // 如果是网页地址,提取MAID并转换为SGWCMAID格式
726
- if (isLink) {
752
+ if (isReqLink) {
727
753
  try {
728
754
  // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
729
755
  // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
@@ -735,19 +761,41 @@ async function getQrText(session, ctx, api, binding, config, timeout = 60000, pr
735
761
  logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrText.substring(0, 24)}...`);
736
762
  }
737
763
  else {
738
- await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
764
+ await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
739
765
  return { qrText: '', error: '无法从网页地址中提取MAID' };
740
766
  }
741
767
  }
742
768
  catch (error) {
743
769
  logger.warn('解析网页地址失败:', error);
744
- await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
770
+ await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
745
771
  return { qrText: '', error: '网页地址格式错误' };
746
772
  }
747
773
  }
774
+ else if (isImgLink) {
775
+ try {
776
+ // 从图片URL中提取MAID部分:https://wq.wahlap.net/qrcode/img/MAID260128205107...png?v
777
+ // 匹配 /qrcode/img/ 后面的 MAID 开头的内容(到 .png 或 ? 之前)
778
+ const match = trimmed.match(/qrcode\/img\/(MAID[^?\.]+)/i);
779
+ if (match && match[1]) {
780
+ const maid = match[1];
781
+ // 在前面加上 SGWC 变成 SGWCMAID...
782
+ qrText = 'SGWC' + maid;
783
+ logger.info(`从图片地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrText.substring(0, 24)}...`);
784
+ }
785
+ else {
786
+ await session.send('⚠️ 无法从图片地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
787
+ return { qrText: '', error: '无法从图片地址中提取MAID' };
788
+ }
789
+ }
790
+ catch (error) {
791
+ logger.warn('解析图片地址失败:', error);
792
+ await session.send('⚠️ 图片地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
793
+ return { qrText: '', error: '图片地址格式错误' };
794
+ }
795
+ }
748
796
  else if (!isSGID) {
749
- await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)');
750
- return { qrText: '', error: '无效的二维码格式,必须是SGID文本或网页地址' };
797
+ await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
798
+ return { qrText: '', error: '无效的二维码格式,必须是SGID文本或网页/图片地址' };
751
799
  }
752
800
  // 验证SGID格式和长度
753
801
  if (!qrText.startsWith('SGWCMAID')) {
@@ -758,7 +806,7 @@ async function getQrText(session, ctx, api, binding, config, timeout = 60000, pr
758
806
  await session.send('❌ SGID长度错误,应在48-128字符之间');
759
807
  return { qrText: '', error: '二维码长度错误,应在48-128字符之间' };
760
808
  }
761
- logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrText.substring(0, 50)}...`);
809
+ logger.info(`✅ 接收到${isLink ? '链接地址(已转换)' : 'SGID'}: ${qrText.substring(0, 50)}...`);
762
810
  // 尝试撤回用户发送的消息(如果启用了自动撤回)
763
811
  await tryRecallMessage(session, ctx, config);
764
812
  await session.send('⏳ 正在处理,请稍候...');
@@ -853,10 +901,13 @@ async function promptForRebind(session, ctx, api, binding, config, timeout = 600
853
901
  logger.debug(`收到用户输入: ${trimmed.substring(0, 50)}`);
854
902
  let qrCode = trimmed;
855
903
  // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
856
- const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
904
+ const isReqLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
905
+ // 检查是否为二维码图片链接格式(https://wq.wahlap.net/qrcode/img/)
906
+ const isImgLink = trimmed.includes('https://wq.wahlap.net/qrcode/img/');
907
+ const isLink = isReqLink || isImgLink;
857
908
  const isSGID = trimmed.startsWith('SGWCMAID');
858
909
  // 如果是网页地址,提取MAID并转换为SGWCMAID格式
859
- if (isLink) {
910
+ if (isReqLink) {
860
911
  try {
861
912
  // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
862
913
  // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
@@ -868,19 +919,41 @@ async function promptForRebind(session, ctx, api, binding, config, timeout = 600
868
919
  logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
869
920
  }
870
921
  else {
871
- await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
922
+ await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
872
923
  return { success: false, error: '无法从网页地址中提取MAID', messageId: promptMessageId };
873
924
  }
874
925
  }
875
926
  catch (error) {
876
927
  logger.warn('解析网页地址失败:', error);
877
- await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
928
+ await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
878
929
  return { success: false, error: '网页地址格式错误', messageId: promptMessageId };
879
930
  }
880
931
  }
932
+ else if (isImgLink) {
933
+ try {
934
+ // 从图片URL中提取MAID部分:https://wq.wahlap.net/qrcode/img/MAID260128205107...png?v
935
+ // 匹配 /qrcode/img/ 后面的 MAID 开头的内容(到 .png 或 ? 之前)
936
+ const match = trimmed.match(/qrcode\/img\/(MAID[^?\.]+)/i);
937
+ if (match && match[1]) {
938
+ const maid = match[1];
939
+ // 在前面加上 SGWC 变成 SGWCMAID...
940
+ qrCode = 'SGWC' + maid;
941
+ logger.info(`从图片地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
942
+ }
943
+ else {
944
+ await session.send('⚠️ 无法从图片地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
945
+ return { success: false, error: '无法从图片地址中提取MAID', messageId: promptMessageId };
946
+ }
947
+ }
948
+ catch (error) {
949
+ logger.warn('解析图片地址失败:', error);
950
+ await session.send('⚠️ 图片地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
951
+ return { success: false, error: '图片地址格式错误', messageId: promptMessageId };
952
+ }
953
+ }
881
954
  else if (!isSGID) {
882
- await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)');
883
- return { success: false, error: '无效的二维码格式,必须是SGID文本或网页地址', messageId: promptMessageId };
955
+ await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
956
+ return { success: false, error: '无效的二维码格式,必须是SGID文本或网页/图片地址', messageId: promptMessageId };
884
957
  }
885
958
  // 验证SGID格式和长度
886
959
  if (!qrCode.startsWith('SGWCMAID')) {
@@ -891,7 +964,7 @@ async function promptForRebind(session, ctx, api, binding, config, timeout = 600
891
964
  await session.send('❌ 识别失败:SGID长度错误,应在48-128字符之间');
892
965
  return { success: false, error: '二维码长度错误,应在48-128字符之间', messageId: promptMessageId };
893
966
  }
894
- logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
967
+ logger.info(`✅ 接收到${isLink ? '链接地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
895
968
  // 发送识别中反馈
896
969
  await session.send('⏳ 正在处理,请稍候...');
897
970
  // 使用新API获取用户信息
@@ -968,6 +1041,104 @@ function apply(ctx, config) {
968
1041
  const requestQueue = queueConfig.enabled ? new RequestQueue(queueConfig.interval) : null;
969
1042
  // 操作记录配置
970
1043
  const operationLogConfig = config.operationLog || { enabled: true, refIdLabel: 'Ref_ID' };
1044
+ // 错误帮助URL配置
1045
+ const errorHelpUrl = config.errorHelpUrl || '';
1046
+ /**
1047
+ * 获取上传任务的统计信息(平均处理时长和今日成功率)
1048
+ * @param commandPrefix 命令前缀,用于筛选日志(如 'mai上传B50' 或 'mai上传落雪b50')
1049
+ * @returns 统计信息字符串
1050
+ */
1051
+ async function getUploadStats(commandPrefix) {
1052
+ try {
1053
+ const today = new Date();
1054
+ today.setHours(0, 0, 0, 0);
1055
+ const todayStart = today.getTime();
1056
+ // 获取今日所有相关操作记录
1057
+ const allLogs = await ctx.database.get('maibot_operation_logs', {});
1058
+ const todayLogs = allLogs.filter(log => {
1059
+ const logTime = new Date(log.createdAt).getTime();
1060
+ return logTime >= todayStart && log.command.startsWith(commandPrefix);
1061
+ });
1062
+ if (todayLogs.length === 0) {
1063
+ return '';
1064
+ }
1065
+ // 统计成功率
1066
+ const taskCompleteLogs = todayLogs.filter(log => log.command.includes('-任务完成'));
1067
+ const successCount = taskCompleteLogs.filter(log => log.status === 'success').length;
1068
+ const failureCount = taskCompleteLogs.filter(log => log.status === 'failure').length;
1069
+ const totalCompleted = successCount + failureCount;
1070
+ // 计算平均处理时长(从任务提交到任务完成)
1071
+ let avgDuration = 0;
1072
+ let durationCount = 0;
1073
+ // 获取所有任务提交记录和对应的完成记录
1074
+ const submitLogs = todayLogs.filter(log => log.command === commandPrefix && log.status === 'success');
1075
+ for (const submitLog of submitLogs) {
1076
+ // 尝试从 apiResponse 中获取 task_id
1077
+ if (!submitLog.apiResponse)
1078
+ continue;
1079
+ try {
1080
+ const response = JSON.parse(submitLog.apiResponse);
1081
+ const taskId = response.task_id;
1082
+ if (!taskId)
1083
+ continue;
1084
+ // 查找对应的完成记录
1085
+ const completeLog = taskCompleteLogs.find(log => {
1086
+ if (!log.apiResponse)
1087
+ return false;
1088
+ try {
1089
+ const completeResponse = JSON.parse(log.apiResponse);
1090
+ return completeResponse.alive_task_id === taskId || String(completeResponse.alive_task_id) === String(taskId);
1091
+ }
1092
+ catch {
1093
+ return false;
1094
+ }
1095
+ });
1096
+ if (completeLog) {
1097
+ const submitTime = new Date(submitLog.createdAt).getTime();
1098
+ const completeTime = new Date(completeLog.createdAt).getTime();
1099
+ const duration = (completeTime - submitTime) / 1000; // 转换为秒
1100
+ if (duration > 0 && duration < 600) { // 排除异常数据(超过10分钟的)
1101
+ avgDuration += duration;
1102
+ durationCount++;
1103
+ }
1104
+ }
1105
+ }
1106
+ catch {
1107
+ continue;
1108
+ }
1109
+ }
1110
+ // 计算平均时长
1111
+ if (durationCount > 0) {
1112
+ avgDuration = avgDuration / durationCount;
1113
+ }
1114
+ // 计算成功率
1115
+ const successRate = totalCompleted > 0 ? Math.round((successCount / totalCompleted) * 100) : 0;
1116
+ // 构建统计信息字符串
1117
+ let statsStr = '';
1118
+ if (avgDuration > 0) {
1119
+ statsStr += `平均处理用时 ${avgDuration.toFixed(2)} s`;
1120
+ }
1121
+ if (totalCompleted > 0) {
1122
+ if (statsStr)
1123
+ statsStr += ',';
1124
+ statsStr += `今日成功率 ${successRate}%`;
1125
+ }
1126
+ return statsStr;
1127
+ }
1128
+ catch (error) {
1129
+ logger.warn('获取上传统计信息失败:', error);
1130
+ return '';
1131
+ }
1132
+ }
1133
+ /**
1134
+ * 获取错误帮助信息(如果配置了帮助URL)
1135
+ */
1136
+ function getErrorHelpInfo() {
1137
+ if (!errorHelpUrl) {
1138
+ return '';
1139
+ }
1140
+ return `\n\n如有问题,请前往 ${errorHelpUrl} 提问`;
1141
+ }
971
1142
  /**
972
1143
  * 生成唯一的 ref_id
973
1144
  */
@@ -1269,7 +1440,7 @@ function apply(ctx, config) {
1269
1440
  if (isDone || hasError) {
1270
1441
  // 任务完成或出错,发送通知并停止
1271
1442
  const statusText = hasError
1272
- ? `❌ 任务失败:${detail.error}`
1443
+ ? `❌ 任务失败:${detail.error}${getErrorHelpInfo()}`
1273
1444
  : '✅ 任务已完成';
1274
1445
  const finishTime = detail.alive_task_end_time
1275
1446
  ? `\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')}`
@@ -1299,7 +1470,7 @@ function apply(ctx, config) {
1299
1470
  status: 'failure',
1300
1471
  errorMessage: '任务轮询超时(10分钟)',
1301
1472
  });
1302
- let msg = `${mention} 水鱼B50任务 ${taskId} 上传失败,请稍后再试一次。`;
1473
+ let msg = `${mention} 水鱼B50任务 ${taskId} 上传失败,请稍后再试一次。${getErrorHelpInfo()}`;
1303
1474
  const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
1304
1475
  if (maintenanceMsg) {
1305
1476
  msg += `\n${maintenanceMsg}`;
@@ -1319,7 +1490,7 @@ function apply(ctx, config) {
1319
1490
  status: 'error',
1320
1491
  errorMessage: error instanceof Error ? error.message : '未知错误',
1321
1492
  });
1322
- let msg = `${mention} 水鱼B50任务 ${taskId} 上传失败,请稍后再试一次。`;
1493
+ let msg = `${mention} 水鱼B50任务 ${taskId} 上传失败,请稍后再试一次。${getErrorHelpInfo()}`;
1323
1494
  const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
1324
1495
  if (maintenanceMsg) {
1325
1496
  msg += `\n${maintenanceMsg}`;
@@ -1353,7 +1524,7 @@ function apply(ctx, config) {
1353
1524
  if (isDone || hasError) {
1354
1525
  // 任务完成或出错,发送通知并停止
1355
1526
  const statusText = hasError
1356
- ? `❌ 任务失败:${detail.error}`
1527
+ ? `❌ 任务失败:${detail.error}${getErrorHelpInfo()}`
1357
1528
  : '✅ 任务已完成';
1358
1529
  const finishTime = detail.alive_task_end_time
1359
1530
  ? `\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')}`
@@ -1383,7 +1554,7 @@ function apply(ctx, config) {
1383
1554
  status: 'failure',
1384
1555
  errorMessage: '任务轮询超时(10分钟)',
1385
1556
  });
1386
- let msg = `${mention} 落雪B50任务 ${taskId} 上传失败,请稍后再试一次。`;
1557
+ let msg = `${mention} 落雪B50任务 ${taskId} 上传失败,请稍后再试一次。${getErrorHelpInfo()}`;
1387
1558
  const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
1388
1559
  if (maintenanceMsg) {
1389
1560
  msg += `\n${maintenanceMsg}`;
@@ -1403,7 +1574,7 @@ function apply(ctx, config) {
1403
1574
  status: 'error',
1404
1575
  errorMessage: error instanceof Error ? error.message : '未知错误',
1405
1576
  });
1406
- let msg = `${mention} 落雪B50任务 ${taskId} 上传失败,请稍后再试一次。`;
1577
+ let msg = `${mention} 落雪B50任务 ${taskId} 上传失败,请稍后再试一次。${getErrorHelpInfo()}`;
1407
1578
  const maintenanceMsg = getMaintenanceMessage(maintenanceNotice);
1408
1579
  if (maintenanceMsg) {
1409
1580
  msg += `\n${maintenanceMsg}`;
@@ -1689,10 +1860,13 @@ function apply(ctx, config) {
1689
1860
  logger.debug(`收到用户输入: ${trimmed.substring(0, 50)}`);
1690
1861
  qrCode = trimmed;
1691
1862
  // 检查是否为公众号网页地址格式(https://wq.wahlap.net/qrcode/req/)
1692
- const isLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
1863
+ const isReqLink = trimmed.includes('https://wq.wahlap.net/qrcode/req/');
1864
+ // 检查是否为二维码图片链接格式(https://wq.wahlap.net/qrcode/img/)
1865
+ const isImgLink = trimmed.includes('https://wq.wahlap.net/qrcode/img/');
1866
+ const isLink = isReqLink || isImgLink;
1693
1867
  const isSGID = trimmed.startsWith('SGWCMAID');
1694
1868
  // 如果是网页地址,提取MAID并转换为SGWCMAID格式
1695
- if (isLink) {
1869
+ if (isReqLink) {
1696
1870
  try {
1697
1871
  // 从URL中提取MAID部分:https://wq.wahlap.net/qrcode/req/MAID2601...55.html?...
1698
1872
  // 匹配 /qrcode/req/ 后面的 MAID 开头的内容(到 .html 或 ? 之前)
@@ -1704,30 +1878,52 @@ function apply(ctx, config) {
1704
1878
  logger.info(`从网页地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
1705
1879
  }
1706
1880
  else {
1707
- await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1881
+ await session.send('⚠️ 无法从网页地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1708
1882
  throw new Error('无法从网页地址中提取MAID');
1709
1883
  }
1710
1884
  }
1711
1885
  catch (error) {
1712
1886
  logger.warn('解析网页地址失败:', error);
1713
- await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1887
+ await session.send('⚠️ 网页地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1714
1888
  throw new Error('网页地址格式错误');
1715
1889
  }
1716
1890
  }
1891
+ else if (isImgLink) {
1892
+ try {
1893
+ // 从图片URL中提取MAID部分:https://wq.wahlap.net/qrcode/img/MAID260128205107...png?v
1894
+ // 匹配 /qrcode/img/ 后面的 MAID 开头的内容(到 .png 或 ? 之前)
1895
+ const match = trimmed.match(/qrcode\/img\/(MAID[^?\.]+)/i);
1896
+ if (match && match[1]) {
1897
+ const maid = match[1];
1898
+ // 在前面加上 SGWC 变成 SGWCMAID...
1899
+ qrCode = 'SGWC' + maid;
1900
+ logger.info(`从图片地址提取MAID并转换: ${maid.substring(0, 20)}... -> ${qrCode.substring(0, 24)}...`);
1901
+ }
1902
+ else {
1903
+ await session.send('⚠️ 无法从图片地址中提取MAID,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1904
+ throw new Error('无法从图片地址中提取MAID');
1905
+ }
1906
+ }
1907
+ catch (error) {
1908
+ logger.warn('解析图片地址失败:', error);
1909
+ await session.send('⚠️ 图片地址格式错误,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1910
+ throw new Error('图片地址格式错误');
1911
+ }
1912
+ }
1717
1913
  else if (!isSGID) {
1718
- await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址(https://wq.wahlap.net/qrcode/req/...)');
1719
- throw new Error('无效的二维码格式,必须是SGID文本或网页地址');
1914
+ await session.send('⚠️ 未识别到有效的SGID格式或网页地址,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1915
+ throw new Error('无效的二维码格式,必须是SGID文本或网页/图片地址');
1720
1916
  }
1721
1917
  // 验证SGID格式和长度
1722
1918
  if (!qrCode.startsWith('SGWCMAID')) {
1723
- await session.send('⚠️ 未识别到有效的SGID格式,请发送SGID文本(SGWCMAID开头)或公众号提供的网页地址');
1919
+ await session.send('⚠️ 未识别到有效的SGID格式,请发送SGID文本(SGWCMAID开头)或公众号提供的网页/图片地址');
1724
1920
  throw new Error('无效的二维码格式,必须以 SGWCMAID 开头');
1725
1921
  }
1726
1922
  if (qrCode.length < 48 || qrCode.length > 128) {
1727
1923
  await session.send('❌ SGID长度错误,应在48-128字符之间');
1728
1924
  throw new Error('二维码长度错误,应在48-128字符之间');
1729
1925
  }
1730
- logger.info(`✅ 接收到${isLink ? '网页地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
1926
+ logger.info(`✅ 接收到${isLink ? '链接地址(已转换)' : 'SGID'}: ${qrCode.substring(0, 50)}...`);
1731
1927
  // 发送识别中反馈
1732
1928
  await session.send('⏳ 正在处理,请稍候...');
1733
1929
  }
@@ -2854,9 +3050,11 @@ function apply(ctx, config) {
2854
3050
  return '⚠️ 当前账号已有未完成的水鱼B50任务,请稍后再试,无需重复上传。';
2855
3051
  }
2856
3052
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2857
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
3053
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
2858
3054
  }
2859
- const successMessage = `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
3055
+ const statsInfo = await getUploadStats('mai上传B50');
3056
+ const statsStr = statsInfo ? `\n${statsInfo}` : '';
3057
+ const successMessage = `✅ B50上传任务已提交!${statsStr}\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2860
3058
  const refId = await logOperation({
2861
3059
  command: 'mai上传B50',
2862
3060
  session,
@@ -2939,13 +3137,15 @@ function apply(ctx, config) {
2939
3137
  return `✅ 重新绑定成功!请重新执行上传操作。`;
2940
3138
  }
2941
3139
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2942
- return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
3140
+ return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
2943
3141
  }
2944
3142
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
2945
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
3143
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
2946
3144
  }
2947
3145
  }
2948
- const successMessage = `✅ B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
3146
+ const statsInfo = await getUploadStats('mai上传B50');
3147
+ const statsStr = statsInfo ? `\n${statsInfo}` : '';
3148
+ const successMessage = `✅ B50上传任务已提交!${statsStr}\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
2949
3149
  const refId = await logOperation({
2950
3150
  command: 'mai上传B50',
2951
3151
  session,
@@ -2969,7 +3169,7 @@ function apply(ctx, config) {
2969
3169
  if (maintenanceMsg) {
2970
3170
  msg += `\n${maintenanceMsg}`;
2971
3171
  }
2972
- msg += `\n\n${maintenanceMessage}`;
3172
+ msg += `\n\n${maintenanceMessage}${getErrorHelpInfo()}`;
2973
3173
  return msg;
2974
3174
  }
2975
3175
  if (error?.response) {
@@ -3909,9 +4109,11 @@ function apply(ctx, config) {
3909
4109
  return '⚠️ 当前账号已有未完成的落雪B50任务,请稍后再试,无需重复上传。';
3910
4110
  }
3911
4111
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
3912
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
4112
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
3913
4113
  }
3914
- const successMessage = `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
4114
+ const statsInfo = await getUploadStats('mai上传落雪b50');
4115
+ const statsStr = statsInfo ? `\n${statsInfo}` : '';
4116
+ const successMessage = `✅ 落雪B50上传任务已提交!${statsStr}\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
3915
4117
  const refId = await logOperation({
3916
4118
  command: 'mai上传落雪b50',
3917
4119
  session,
@@ -3923,7 +4125,7 @@ function apply(ctx, config) {
3923
4125
  scheduleLxB50Notification(session, result.task_id, refId);
3924
4126
  return appendRefId(successMessage, refId);
3925
4127
  }
3926
- return `❌ 获取二维码失败:${qrTextResult.error}`;
4128
+ return `❌ 获取二维码失败:${qrTextResult.error}${getErrorHelpInfo()}`;
3927
4129
  }
3928
4130
  // 在调用API前加入队列
3929
4131
  await waitForQueue(session);
@@ -3994,13 +4196,15 @@ function apply(ctx, config) {
3994
4196
  return `✅ 重新绑定成功!请重新执行上传操作。`;
3995
4197
  }
3996
4198
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
3997
- return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}`;
4199
+ return `❌ 上传失败:${result.msg || '未知错误'}\n重新绑定失败:${rebindResult.error || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
3998
4200
  }
3999
4201
  const taskIdInfo = result.task_id ? `\n任务ID: ${result.task_id}` : '';
4000
- return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}`;
4202
+ return `❌ 上传失败:${result.msg || '未知错误'}${taskIdInfo}${getErrorHelpInfo()}`;
4001
4203
  }
4002
4204
  }
4003
- const successMessage = `✅ 落雪B50上传任务已提交!\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
4205
+ const statsInfo = await getUploadStats('mai上传落雪b50');
4206
+ const statsStr = statsInfo ? `\n${statsInfo}` : '';
4207
+ const successMessage = `✅ 落雪B50上传任务已提交!${statsStr}\n任务ID: ${result.task_id}\n\n请耐心等待任务完成,预计1-10分钟`;
4004
4208
  const refId = await logOperation({
4005
4209
  command: 'mai上传落雪b50',
4006
4210
  session,
@@ -4023,12 +4227,12 @@ function apply(ctx, config) {
4023
4227
  if (maintenanceMsg) {
4024
4228
  msg += `\n${maintenanceMsg}`;
4025
4229
  }
4026
- msg += `\n\n${maintenanceMessage}`;
4230
+ msg += `\n\n${maintenanceMessage}${getErrorHelpInfo()}`;
4027
4231
  return msg;
4028
4232
  })()
4029
4233
  : (error?.response
4030
- ? `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}`
4031
- : `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}`));
4234
+ ? `❌ API请求失败: ${error.response.status} ${error.response.statusText}\n\n${maintenanceMessage}${getErrorHelpInfo()}`
4235
+ : `❌ 上传失败: ${error?.message || '未知错误'}\n\n${maintenanceMessage}${getErrorHelpInfo()}`));
4032
4236
  const refId = await logOperation({
4033
4237
  command: 'mai上传落雪b50',
4034
4238
  session,