koishi-plugin-dew-bot 1.0.246 → 1.0.249

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/README.md CHANGED
@@ -1,53 +1,16 @@
1
- # koishi-plugin-dew-bot
2
-
3
- Koishi 插件:把聊天消息交给 dew-v7 Bot 游戏服处理,再把服务器推送的文字发回会话。
4
-
5
- ## 安装
6
-
7
- 在 Koishi 模板项目中把本仓库放到 `external/dew-bot`,然后安装依赖并启用插件。
8
-
9
- ```yaml
10
- plugins:
11
- dew-bot:
12
- 服务器地址: ws://dew-v7-test:3021
13
- ```
14
-
15
- 游戏服需先启动 Bot 进程(默认 WebSocket `3021`)。
16
-
17
- ## 配置
18
-
19
- | 项 | 说明 | 默认 |
20
- | --- | --- | --- |
21
- | 服务器地址 | 游戏服 WebSocket 地址 | `ws://dew-v7-test:3021` |
22
-
23
- 插件会忽略机器人自己发出的消息,避免回显再次上传形成循环。
24
-
25
- ## 发布到 npm
26
-
27
- 在 `bot/external/dew-bot` 目录执行:
28
-
29
- ```bash
30
- npm run release
31
- ```
32
-
33
- 脚本会先从 `bot` 工作区执行 `npm run build -- dew-bot`,构建成功后运行 `npm version patch --no-git-tag-version --workspaces=false`,再以 `public` 权限和 `latest` 标签发布到 `https://registry.npmjs.org`。例如 `1.0.245` 会提升为 `1.0.246`,不会创建 Git 提交或标签。发布前需已安装工作区依赖,并登录有该包发布权限的 npm 账号:
34
-
35
- ```bash
36
- npm login --registry=https://registry.npmjs.org
37
- ```
38
-
39
- 仅构建并检查包内容,执行:
40
-
41
- ```bash
42
- npm run release:check
43
- ```
44
-
45
- 这相当于 `npm run release -- --dry-run`,会真实构建并执行 `npm pack --dry-run`,不会提升版本号、生成压缩包或发布到 npm。
46
-
47
- 构建失败时脚本会停止,版本不递增。若递增后发布失败,本地保留新版本,脚本会以非零退出码结束并打印当前版本及重试命令;在插件目录确认该版本的发布状态后,可重试同一版本:
48
-
49
- ```bash
50
- npm publish --workspaces=false --access=public --tag=latest --registry=https://registry.npmjs.org
51
- ```
52
-
53
- 重试发布时不要再次运行 `npm run release`,否则会再次递增版本。
1
+ # 🌀 DEW · 无限轮回 RPG
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-dew-bot?style=flat-square&color=7c3aed)](https://www.npmjs.com/package/koishi-plugin-dew-bot)
4
+ ![Koishi](https://img.shields.io/badge/Koishi-4.x-6366f1?style=flat-square)
5
+
6
+ > **下一场轮回,主角是你。**
7
+
8
+ **DEW Bot** 是一款基于《无限恐怖》小说改编的 **RPG 机器人**。
9
+
10
+ 踏入主神空间,成为轮回者。与伙伴迎接未知的试炼,在生死之间积蓄力量,书写属于你的无限冒险。
11
+
12
+ 无论你熟悉原作,还是初次踏入无限流世界,这场轮回,都有你的位置。
13
+
14
+ ---
15
+
16
+ 连接 DEW 游戏服后,在聊天中发送 **帮助**,开启第一场冒险。
package/lib/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export declare const inject: {
3
3
  optional: string[];
4
4
  };
5
5
  export declare const name = "dew-bot";
6
+ export declare const usage: string;
6
7
  export interface Config {
7
8
  服务器地址: string;
8
9
  }
package/lib/index.js CHANGED
@@ -33,7 +33,8 @@ __export(src_exports, {
33
33
  Config: () => Config,
34
34
  apply: () => apply2,
35
35
  inject: () => inject,
36
- name: () => name
36
+ name: () => name,
37
+ usage: () => usage
37
38
  });
38
39
  module.exports = __toCommonJS(src_exports);
39
40
  var import_koishi2 = require("koishi");
@@ -309,6 +310,7 @@ function equipmentCardHtml(card, artwork, unknownFallback = false) {
309
310
  const notes = card.notes?.filter(Boolean).length ? `<div class="notes">${card.notes.filter(Boolean).map((note) => `<p>${escape6(note)}</p>`).join("")}</div>` : "";
310
311
  const enhancement = Number(card.enhancement) > 0 ? `<span class="enhance"><small>强化</small>+${escape6(card.enhancement)}</span>` : "";
311
312
  const price = card.referencePrice ? `<div class="price"><span>参考价格</span><strong>${escape6(card.referencePrice)}</strong></div>` : "";
313
+ const author = `<span class="author"><small>打造者</small>${escape6(card.authorName?.trim() || "未知")}</span>`;
312
314
  return `<!doctype html><html><head><meta charset="utf-8"><style>
313
315
  *{box-sizing:border-box}
314
316
  html,body{margin:0;width:760px;background:#f1eee5;font-family:'Microsoft YaHei','Noto Sans CJK SC',sans-serif;color:#343b35}
@@ -317,14 +319,14 @@ html,body{margin:0;width:760px;background:#f1eee5;font-family:'Microsoft YaHei',
317
319
  #capture:after{content:'';position:absolute;inset:17px;border:1px solid #8c8b6f22;pointer-events:none}
318
320
  .heading{display:flex;align-items:center;gap:24px}.heading-copy{flex:1;min-width:0}.overline{font-size:14px;letter-spacing:6px;color:#868a76;margin-bottom:14px}.name{font:700 43px/1.35 'KaiTi','STKaiti','Noto Serif CJK SC','SimSun',serif;margin:0;color:#303a31;overflow-wrap:anywhere}
319
321
  .seal{flex-shrink:0;width:66px;min-height:74px;transform:rotate(3deg);padding:8px 3px;color:#a04b40;border:2px solid #a04b40;outline:1px solid #a04b40;outline-offset:-6px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;border-radius:3px 1px 4px 2px}.seal strong{font:700 24px Georgia,'KaiTi',serif;letter-spacing:-1px}.seal span{font:16px 'KaiTi','SimSun',serif;letter-spacing:4px;padding-left:4px}
320
- .meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 26px;margin:24px 0 0;padding:15px 0;border-top:1px solid #8a8a7144;border-bottom:1px solid #8a8a7128;font-size:19px;color:#535d50;line-height:1.5}.meta .slot{color:#a04b40;font:21px 'KaiTi','SimSun',serif;letter-spacing:2px}.meta small{font-size:16px;color:#92917f;margin-right:9px}
322
+ .meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 26px;margin:24px 0 0;padding:15px 0;border-top:1px solid #8a8a7144;border-bottom:1px solid #8a8a7128;font-size:19px;color:#535d50;line-height:1.5}.meta .slot{color:#a04b40;font:21px 'KaiTi','SimSun',serif;letter-spacing:2px}.meta .author{min-width:0;overflow-wrap:anywhere}.meta small{font-size:16px;color:#92917f;margin-right:9px}
321
323
  .art{position:relative;width:610px;height:610px;max-width:100%;margin:18px auto 11px;display:grid;place-items:center;isolation:isolate;overflow:hidden}.art:before{content:'';position:absolute;inset:4%;border-radius:48% 52% 56% 44%;background:radial-gradient(ellipse,#aab4a443,transparent 69%);transform:rotate(-12deg)}.equipment-artwork{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;filter:saturate(.72) contrast(.97);mix-blend-mode:multiply;mask-image:linear-gradient(to bottom,transparent 0,#000 8%,#000 84%,transparent 100%),linear-gradient(to right,transparent 0,#000 7%,#000 93%,transparent 100%);mask-composite:intersect}.art-fallback{position:relative;width:360px;height:360px;max-width:90%;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#7d8b7d;border:1px solid #7c887d3b;border-radius:50%;background:radial-gradient(circle,#b1b9a435,transparent 70%);font-family:'KaiTi','SimSun',serif}.art-fallback:before,.art-fallback:after{content:'';position:absolute;inset:21px;border:1px solid #7c887d22;border-radius:50%}.art-fallback:after{inset:48px;border-color:#9a9e8533}.art-fallback .glyph{font-size:126px;line-height:1.2;opacity:.46}.art-fallback .kind{font-size:20px;letter-spacing:4px;opacity:.55}.art:has(.equipment-artwork) .art-fallback{visibility:hidden}.placeholder-note{position:absolute;right:18px;bottom:16px;z-index:2;padding:5px 9px;background:#f4f1e8ce;border:1px solid #9d9b8755;color:#536055;font:18px 'KaiTi','SimSun',serif;letter-spacing:2px}
322
324
  .power{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:18px 0 22px;border-top:1px solid #8a8a7128;border-bottom:1px solid #8a8a7144;color:#31463e}.power span{font:24px 'KaiTi','SimSun',serif;letter-spacing:12px}.power strong{font:700 45px/1.1 Georgia,'Times New Roman',serif;letter-spacing:2px;font-variant-numeric:lining-nums}
323
325
  .section{margin-top:29px}.section h2{display:flex;align-items:center;gap:16px;margin:0 0 17px;color:#626e5e;font:21px 'KaiTi','SimSun',serif;letter-spacing:5px}.section h2:before{content:'';width:5px;height:17px;background:#a04b40;transform:rotate(8deg);opacity:.75}.section h2:after{content:'';height:1px;flex:1;background:linear-gradient(to right,#7f897c55,transparent)}
324
326
  .affixes{display:grid;grid-template-columns:1fr 1fr;column-gap:26px}.affix{display:flex;align-items:baseline;justify-content:space-between;gap:10px;min-width:0;padding:9px 0 12px;border-bottom:1px solid #8a8a7124;font-size:18px;color:#697064}.affix span{overflow-wrap:anywhere}.affix strong{flex-shrink:0;font:700 22px Georgia,'KaiTi',serif;color:#344b42}.empty-line{font-size:18px;color:#92917f;margin:4px 0 12px}
325
327
  .skill{display:flex;align-items:baseline;justify-content:space-between;gap:18px;padding:5px 0 10px;font-size:20px;color:#304b40}.skill-name{font-weight:700;overflow-wrap:anywhere}.skill-name i{display:inline-block;width:13px;height:13px;margin:0 17px 1px 4px;background:#a04b40;transform:rotate(45deg)}.rank{font:18px Georgia,'KaiTi',serif;color:#7f8d7b;white-space:nowrap}
326
328
  .description p{margin:0;font:22px/1.75 'KaiTi','STKaiti','Noto Serif CJK SC','SimSun',serif;white-space:pre-wrap;overflow-wrap:anywhere;color:#313f38}.price{display:flex;align-items:baseline;gap:16px;margin-top:24px;padding-top:17px;border-top:1px solid #8a8a7128;font-size:18px;color:#8b907d}.price strong{font-size:18px;color:#354c42}.notes{margin-top:17px;padding:10px 15px;border-left:3px solid #a04b4080;background:#e8e8db70;color:#3e5549}.notes p{margin:5px 0;font-size:17px;line-height:1.6;overflow-wrap:anywhere}.edition{display:flex;align-items:center;gap:12px;justify-content:center;margin-top:34px;color:#a6a38e;font:14px 'KaiTi','SimSun',serif;letter-spacing:5px}.edition:before,.edition:after{content:'';height:1px;width:32px;background:#9b9c833d}
327
- </style></head><body><div id="capture" class="equipment-sheet ink-equipment-sheet"><header><div class="heading"><div class="heading-copy"><div class="overline">装 备 图 录</div><h1 class="name">${escape6(card.name)}</h1></div><div class="seal"><strong>${escape6(card.quality)}</strong><span>品阶</span></div></div><div class="meta"><span class="slot">${escape6(card.slot)}</span>${enhancement}</div></header><div class="art"><div class="art-fallback"><span class="glyph">?</span><span class="kind">未知外观</span></div>${artwork ? `<img class="skill-artwork equipment-artwork" alt="" src="${escape6(artwork)}">` : ""}${unknownFallback || !artwork ? '<span class="placeholder-note">外观暂未显现</span>' : ""}</div><div class="power"><span>装 备 战 力</span><strong>${escape6(power)}</strong></div><section class="section attributes"><h2>附加属性</h2><div class="affixes">${affixes}</div></section>${skills}${description}${price}${notes}<div class="edition">露 · 装备录</div></div></body></html>`;
329
+ </style></head><body><div id="capture" class="equipment-sheet ink-equipment-sheet"><header><div class="heading"><div class="heading-copy"><div class="overline">装 备 图 录</div><h1 class="name">${escape6(card.name)}</h1></div><div class="seal"><strong>${escape6(card.quality)}</strong><span>品阶</span></div></div><div class="meta"><span class="slot">${escape6(card.slot)}</span>${enhancement}${author}</div></header><div class="art"><div class="art-fallback"><span class="glyph">?</span><span class="kind">未知外观</span></div>${artwork ? `<img class="skill-artwork equipment-artwork" alt="" src="${escape6(artwork)}">` : ""}${unknownFallback || !artwork ? '<span class="placeholder-note">外观暂未显现</span>' : ""}</div><div class="power"><span>装 备 战 力</span><strong>${escape6(power)}</strong></div><section class="section attributes"><h2>附加属性</h2><div class="affixes">${affixes}</div></section>${skills}${description}${price}${notes}<div class="edition">露 · 装备录</div></div></body></html>`;
328
330
  }
329
331
  __name(equipmentCardHtml, "equipmentCardHtml");
330
332
 
@@ -717,23 +719,28 @@ function escapeMarkdown(value) {
717
719
  return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, "\\$&");
718
720
  }
719
721
  __name(escapeMarkdown, "escapeMarkdown");
720
- function pageCommand(command) {
721
- return command.startsWith("/") ? command : `/${command}`;
722
+ function compactDisplayText(value) {
723
+ return value.trim().replace(/[ \t\u3000]*([·×+|])[ \t\u3000]*/g, "$1").replace(/([\p{Script=Han}\p{Extended_Pictographic}\p{Emoji_Modifier}\p{Regional_Indicator}\uFE0F])[ \t\u3000]+/gu, "$1").replace(/[ \t\u3000]+(?=[\p{Script=Han}\p{Extended_Pictographic}\p{Regional_Indicator}])/gu, "");
722
724
  }
723
- __name(pageCommand, "pageCommand");
725
+ __name(compactDisplayText, "compactDisplayText");
726
+ function compactListLabel(value) {
727
+ const label = compactDisplayText(value);
728
+ return /[📖📚]/u.test(label) ? label.replace(/技能书(?=$|×\d+(?:\.\d+)?(?:||$)||)/g, "") : label;
729
+ }
730
+ __name(compactListLabel, "compactListLabel");
724
731
  function inlineCommandUrl(command) {
725
- const encoded = encodeURIComponent(pageCommand(command)).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
732
+ const encoded = encodeURIComponent(command).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
726
733
  return `mqqapi://aio/inlinecmd?command=${encoded}&enter=true&reply=false`;
727
734
  }
728
735
  __name(inlineCommandUrl, "inlineCommandUrl");
729
736
  function pageCommandLines(nav) {
730
737
  return [
731
- ...nav.previousCommand ? [`上一页:${pageCommand(nav.previousCommand)}`] : [],
732
- ...nav.nextCommand ? [`下一页:${pageCommand(nav.nextCommand)}`] : []
738
+ ...nav.previousCommand ? [nav.previousCommand] : [],
739
+ ...nav.nextCommand ? [nav.nextCommand] : []
733
740
  ];
734
741
  }
735
742
  __name(pageCommandLines, "pageCommandLines");
736
- function renderQQPageMarkdown(data, showGreeting, showCommands) {
743
+ function renderQQPageMarkdown(data, showGreeting) {
737
744
  const nav = data.pageNav;
738
745
  const entryCommands = new Map(nav?.entries?.map((entry) => [entry.lineIndex, entry.viewCommand]) ?? []);
739
746
  const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/;
@@ -744,7 +751,7 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
744
751
  const listCount = data.line.filter((item) => item.type === 1 /* line */ && listPattern.test(item.content)).length;
745
752
  const count = summary?.[1] ?? (nav?.total === 1 ? String(listCount) : void 0);
746
753
  const countUnit = summary?.[2] ?? "项";
747
- const isLeaderboardDirectory = !!nav?.entries?.length && nav.entries.every((entry) => /^排行榜查看\s/.test(entry.viewCommand));
754
+ const isLeaderboardDirectory = !!nav?.entries?.length && /^(?:🏆)?排行榜$/u.test(compactDisplayText(title?.content ?? ""));
748
755
  const sections = [];
749
756
  const bodyLines = [];
750
757
  const listLines = [];
@@ -752,7 +759,7 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
752
759
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
753
760
  }, "flushBody");
754
761
  const flushList = /* @__PURE__ */ __name(() => {
755
- if (listLines.length) sections.push(listLines.splice(0).join(" \n"));
762
+ if (listLines.length) sections.push(listLines.splice(0).join("\n"));
756
763
  }, "flushList");
757
764
  const flush = /* @__PURE__ */ __name(() => {
758
765
  flushList();
@@ -767,23 +774,27 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
767
774
  case 0 /* title */:
768
775
  flush();
769
776
  if (isLeaderboardDirectory) {
770
- sections.push(`## ${item.content.includes("🏆") ? "" : "🏆 "}${escapeMarkdown(item.content)}`);
771
- sections.push(`> 共 ${nav.entries.length} 个榜单 · 点击榜名查看`);
777
+ sections.push(`## ${item.content.includes("🏆") ? "" : "🏆"}${escapeMarkdown(compactDisplayText(item.content))}`);
778
+ sections.push(`> 共${nav.entries.length}个榜单·点击榜名查看`);
772
779
  } else {
773
- sections.push(`**${escapeMarkdown(item.content)}${count !== void 0 ? ` · ${count} ${countUnit}` : ""}**`);
780
+ sections.push(`**${escapeMarkdown(compactDisplayText(item.content))}${count !== void 0 ? `·${count}${countUnit}` : ""}**`);
774
781
  }
775
782
  break;
776
783
  case 1 /* line */: {
777
784
  const listEntry = listPattern.exec(item.content);
778
785
  if (listEntry) {
779
786
  flushBody();
780
- const command = entryCommands.get(lineIndex);
781
- const label = escapeMarkdown(listEntry[2]);
782
- const name2 = command ? `[${label}](${inlineCommandUrl(command)})` : label;
783
- listLines.push(`${escapeMarkdown(`[${listEntry[1]}]`)} ${name2}`);
787
+ const command2 = entryCommands.get(lineIndex);
788
+ const label = escapeMarkdown(compactListLabel(listEntry[2]));
789
+ const name2 = command2 ? `[${label}](${inlineCommandUrl(command2)})` : label;
790
+ listLines.push(`${listEntry[1]}. ${name2}`);
784
791
  break;
785
792
  }
786
793
  flushList();
794
+ if (nav && item.content.trim() === "[上一页] [下一页]") {
795
+ flushBody();
796
+ break;
797
+ }
787
798
  if (/^\[指令\]相关指令$/.test(item.content)) {
788
799
  flushBody();
789
800
  hasHelp = true;
@@ -792,11 +803,12 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
792
803
  const page = pagePattern.exec(item.content);
793
804
  if (page && nav) {
794
805
  flushBody();
795
- pageFooter = `**第 ${nav.current} / ${nav.total} 页${!title && page[1] ? ` · 共 ${page[1]} ${page[2]}` : ""}**`;
806
+ pageFooter = `**第${nav.current}/${nav.total}页${!title && page[1] ? `·共${page[1]}${page[2]}` : ""}**`;
796
807
  hasPageSummary = true;
797
808
  break;
798
809
  }
799
- bodyLines.push(escapeMarkdown(item.content));
810
+ const command = entryCommands.get(lineIndex);
811
+ bodyLines.push(command ? `[${escapeMarkdown(compactDisplayText(item.content))}](${inlineCommandUrl(command)})` : escapeMarkdown(item.content));
800
812
  break;
801
813
  }
802
814
  case 2 /* error */:
@@ -807,42 +819,18 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
807
819
  }
808
820
  flush();
809
821
  if (nav && !hasPageSummary && (nav.total > 1 || nav.previousCommand || nav.nextCommand))
810
- pageFooter = `**第 ${nav.current} / ${nav.total} 页**`;
822
+ pageFooter = `**第${nav.current}/${nav.total}页**`;
823
+ const previousLink = nav?.previousCommand ? `[上一页](${inlineCommandUrl(nav.previousCommand)})` : void 0;
824
+ const nextLink = nav?.nextCommand ? `[下一页](${inlineCommandUrl(nav.nextCommand)})` : void 0;
811
825
  const helpLink = hasHelp ? `[指令](${inlineCommandUrl("指令")})` : void 0;
812
- const footer = pageFooter ? [pageFooter, helpLink].filter(Boolean).join(" · ") : helpLink ? `更多操作:${helpLink}` : void 0;
826
+ const footer = pageFooter ? [pageFooter, previousLink, nextLink, helpLink].filter(Boolean).join("·") : helpLink ? `更多操作:${helpLink}` : void 0;
813
827
  if (footer) {
814
828
  if (isLeaderboardDirectory) sections.push("***");
815
829
  sections.push(footer);
816
830
  }
817
- if (showCommands && nav) {
818
- const commands = pageCommandLines(nav);
819
- if (commands.length) sections.push(`**翻页指令**
820
- ${commands.map(escapeMarkdown).join(" \n")}`);
821
- }
822
831
  return sections.join("\n\n") || " ";
823
832
  }
824
833
  __name(renderQQPageMarkdown, "renderQQPageMarkdown");
825
- function makeButton(id, label, command, userId, autoEnter) {
826
- return {
827
- id,
828
- render_data: { label, visited_label: label, style: 1 },
829
- action: {
830
- type: 2,
831
- permission: { type: 0, specify_user_ids: [userId] },
832
- data: pageCommand(command),
833
- ...autoEnter ? { enter: true } : {},
834
- unsupport_tips: "当前客户端暂不支持该按钮,请发送上一页或下一页。"
835
- }
836
- };
837
- }
838
- __name(makeButton, "makeButton");
839
- function buttonsFor(nav, userId, autoEnter) {
840
- return [
841
- ...nav.previousCommand ? [makeButton("previous", "上一页", nav.previousCommand, userId, autoEnter)] : [],
842
- ...nav.nextCommand ? [makeButton("next", "下一页", nav.nextCommand, userId, autoEnter)] : []
843
- ];
844
- }
845
- __name(buttonsFor, "buttonsFor");
846
834
  function canSendQQPage(session, data) {
847
835
  if (!session || session.bot.platform !== "qq" || session.bot.userId !== data.botId) return false;
848
836
  if (session.userId !== data.userId || (session.channelId ?? session.userId) !== data.channelId) return false;
@@ -850,23 +838,14 @@ function canSendQQPage(session, data) {
850
838
  return !!internal?.sendMessage && !!internal.sendPrivateMessage;
851
839
  }
852
840
  __name(canSendQQPage, "canSendQQPage");
853
- async function sendQQPage(session, data, withKeyboard) {
841
+ async function sendQQPage(session, data) {
854
842
  const internal = session.bot.internal;
855
- const nav = data.pageNav;
856
- const buttons = withKeyboard ? buttonsFor(nav, data.userId, session.isDirect) : [];
857
- const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0, buttons.length === 0);
858
- const hasEntries = !!nav.entries?.length;
859
- const hasInlineHelp = data.line.some((item) => item.type === 1 /* line */ && item.content === "[指令]相关指令");
843
+ const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0);
860
844
  const request = {
861
845
  // With msg_type=2, QQ requires the ordinary content field to be empty.
862
846
  content: "",
863
847
  msg_type: 2,
864
- markdown: {
865
- content: !session.isDirect && (buttons.length || hasEntries || hasInlineHelp) ? `${markdown}
866
-
867
- 点击后如未自动发送,请点发送。` : markdown
868
- },
869
- ...buttons.length ? { keyboard: { content: { rows: [{ buttons }] } } } : {}
848
+ markdown: { content: markdown }
870
849
  };
871
850
  const age = Date.now() - session.timestamp;
872
851
  if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1e3 - 2e3) {
@@ -891,20 +870,8 @@ var handelr_text = class extends handel_base {
891
870
  const session = sessions_default.get(sessionKey(data.botId, data.userId, data.channelId));
892
871
  if (data.pageNav && canSendQQPage(session, data)) {
893
872
  const sendStartedAt = this.timing ? performance.now() : 0;
894
- if (data.pageNav.previousCommand || data.pageNav.nextCommand) {
895
- try {
896
- await sendQQPage(session, data, true);
897
- if (this.timing) {
898
- this.timing.delivery = "qq-markdown-keyboard";
899
- this.timing.sendMs = performance.now() - sendStartedAt;
900
- }
901
- return;
902
- } catch (error) {
903
- this.ctx.logger.warn("QQ 翻页按钮发送失败,改用 Markdown 文字指令", error instanceof Error ? error.message : error);
904
- }
905
- }
906
873
  try {
907
- await sendQQPage(session, data, false);
874
+ await sendQQPage(session, data);
908
875
  if (this.timing) {
909
876
  this.timing.delivery = "qq-markdown";
910
877
  this.timing.sendMs = performance.now() - sendStartedAt;
@@ -986,7 +953,7 @@ var import_tsrpc = require("tsrpc");
986
953
 
987
954
  // src/shared/bot/serviceProto.ts
988
955
  var serviceProto = {
989
- "version": 10,
956
+ "version": 11,
990
957
  "services": [
991
958
  {
992
959
  "id": 0,
@@ -1578,6 +1545,14 @@ var serviceProto = {
1578
1545
  }
1579
1546
  },
1580
1547
  "optional": true
1548
+ },
1549
+ {
1550
+ "id": 12,
1551
+ "name": "authorName",
1552
+ "type": {
1553
+ "type": "String"
1554
+ },
1555
+ "optional": true
1581
1556
  }
1582
1557
  ]
1583
1558
  },
@@ -2170,8 +2145,23 @@ __name(apply, "apply");
2170
2145
  // src/index.ts
2171
2146
  var inject = { optional: ["puppeteer"] };
2172
2147
  var name = "dew-bot";
2148
+ var usage = `
2149
+ ## 🌀 DEW · 无限轮回 RPG
2150
+
2151
+ > **下一场轮回,主角是你。**
2152
+
2153
+ **DEW Bot** 是一款基于《无限恐怖》小说改编的 **RPG 机器人**。
2154
+
2155
+ 踏入主神空间,成为轮回者。与伙伴迎接未知的试炼,在生死之间积蓄力量,书写属于你的无限冒险。
2156
+
2157
+ 无论你熟悉原作,还是初次踏入无限流世界,这场轮回,都有你的位置。
2158
+
2159
+ ---
2160
+
2161
+ 连接 DEW 游戏服后,在聊天中发送 **帮助**,开启第一场冒险。
2162
+ `;
2173
2163
  var Config = import_koishi2.Schema.object({
2174
- 服务器地址: import_koishi2.Schema.string().default("ws://dew-v7-test:3021").description("游戏服 WebSocket 地址")
2164
+ 服务器地址: import_koishi2.Schema.string().default("ws://dew-v7-test:3021").description("DEW 游戏服的 WebSocket 地址;同机部署可填写 ws://127.0.0.1:3021")
2175
2165
  });
2176
2166
  function apply2(ctx, config) {
2177
2167
  ctx.plugin(apply, config);
@@ -2182,5 +2172,6 @@ __name(apply2, "apply");
2182
2172
  Config,
2183
2173
  apply,
2184
2174
  inject,
2185
- name
2175
+ name,
2176
+ usage
2186
2177
  });
@@ -7,7 +7,7 @@ export interface MessageTiming {
7
7
  renderMs?: number;
8
8
  imageBytes?: number;
9
9
  sendMs?: number;
10
- delivery?: 'image' | 'text' | 'qq-markdown-keyboard' | 'qq-markdown' | 'missing-session';
10
+ delivery?: 'image' | 'text' | 'qq-markdown' | 'missing-session';
11
11
  }
12
12
  export default class handel_base {
13
13
  ctx: Context;
@@ -1,10 +1,9 @@
1
1
  import type { Session } from 'koishi';
2
2
  import { type PageNavData } from '../../shared/face';
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage';
4
- export declare function pageCommand(command: string): string;
5
4
  export declare function pageCommandLines(nav: PageNavData): string[];
6
- export declare function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, showCommands: boolean): string;
5
+ export declare function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): string;
7
6
  /** Returns false if this is not the matching QQ session or the adapter has no native API. */
8
7
  export declare function canSendQQPage(session: Session | undefined, data: MsgMessage): session is Session;
9
- /** Send a native QQ Markdown page, optionally with auto-sending command buttons. */
10
- export declare function sendQQPage(session: Session, data: MsgMessage, withKeyboard: boolean): Promise<void>;
8
+ /** Send a native QQ Markdown page with inline command links. */
9
+ export declare function sendQQPage(session: Session, data: MsgMessage): Promise<void>;
@@ -44,6 +44,7 @@ export interface EquipmentCardData {
44
44
  rank: string;
45
45
  }>;
46
46
  notes?: string[];
47
+ authorName?: string;
47
48
  }
48
49
  export interface LeaderboardImageData {
49
50
  backgroundUrl?: string;
@@ -108,7 +109,7 @@ export interface IErrorMessage {
108
109
  }
109
110
  /** 第一阶段支持的消息项 **/
110
111
  export type IMessageItem = ITitleMessage | ILineMessage | IErrorMessage;
111
- /** 平台无关的翻页动作;按钮发送普通指令,客户端决定如何呈现。 */
112
+ /** 平台无关的翻页动作;客户端决定如何呈现可点击文字,点击后发送普通指令。 */
112
113
  export interface PageNavData {
113
114
  current: number;
114
115
  total: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-dew-bot",
3
- "description": "连游戏服,把聊天消息交给服务器处理",
4
- "version": "1.0.246",
3
+ "description": "基于《无限恐怖》小说改编的 RPG 机器人。踏入主神空间,与伙伴迎接轮回试炼,书写属于你的无限冒险。",
4
+ "version": "1.0.249",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -14,10 +14,22 @@
14
14
  "release:check": "node scripts/release.cjs --dry-run"
15
15
  },
16
16
  "license": "MIT",
17
+ "homepage": "https://gitee.com/ChuXuanD30/koishi-plugin-dew-bot",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://gitee.com/ChuXuanD30/koishi-plugin-dew-bot.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://gitee.com/ChuXuanD30/koishi-plugin-dew-bot/issues"
24
+ },
17
25
  "keywords": [
18
26
  "chatbot",
19
27
  "koishi",
20
- "plugin"
28
+ "plugin",
29
+ "rpg",
30
+ "game",
31
+ "qq",
32
+ "markdown"
21
33
  ],
22
34
  "peerDependencies": {
23
35
  "koishi": "^4.18.0",
@@ -29,7 +41,7 @@
29
41
  },
30
42
  "koishi": {
31
43
  "description": {
32
- "zh": "连游戏服,把聊天消息交给服务器处理"
44
+ "zh": "基于《无限恐怖》小说改编的 RPG 机器人。踏入主神空间,与伙伴迎接轮回试炼,书写属于你的无限冒险。"
33
45
  }
34
46
  }
35
47
  }
package/src/index.ts CHANGED
@@ -5,12 +5,28 @@ export const inject = { optional: ['puppeteer'] }
5
5
 
6
6
  export const name = 'dew-bot'
7
7
 
8
+ export const usage: string = `
9
+ ## 🌀 DEW · 无限轮回 RPG
10
+
11
+ > **下一场轮回,主角是你。**
12
+
13
+ **DEW Bot** 是一款基于《无限恐怖》小说改编的 **RPG 机器人**。
14
+
15
+ 踏入主神空间,成为轮回者。与伙伴迎接未知的试炼,在生死之间积蓄力量,书写属于你的无限冒险。
16
+
17
+ 无论你熟悉原作,还是初次踏入无限流世界,这场轮回,都有你的位置。
18
+
19
+ ---
20
+
21
+ 连接 DEW 游戏服后,在聊天中发送 **帮助**,开启第一场冒险。
22
+ `
23
+
8
24
  export interface Config {
9
25
  服务器地址: string
10
26
  }
11
27
 
12
28
  export const Config: Schema<Config> = Schema.object({
13
- 服务器地址: Schema.string().default('ws://dew-v7-test:3021').description('游戏服 WebSocket 地址'),
29
+ 服务器地址: Schema.string().default('ws://dew-v7-test:3021').description('DEW 游戏服的 WebSocket 地址;同机部署可填写 ws://127.0.0.1:3021'),
14
30
  })
15
31
 
16
32
  export function apply(ctx: Context, config: Config) {
@@ -20,6 +20,7 @@ export function equipmentCardHtml(card: EquipmentCardData, artwork?: string, unk
20
20
  : ''
21
21
  const enhancement = Number(card.enhancement) > 0 ? `<span class="enhance"><small>强化</small>+${escape(card.enhancement)}</span>` : ''
22
22
  const price = card.referencePrice ? `<div class="price"><span>参考价格</span><strong>${escape(card.referencePrice)}</strong></div>` : ''
23
+ const author = `<span class="author"><small>打造者</small>${escape(card.authorName?.trim() || '未知')}</span>`
23
24
 
24
25
  return `<!doctype html><html><head><meta charset="utf-8"><style>
25
26
  *{box-sizing:border-box}
@@ -29,12 +30,12 @@ html,body{margin:0;width:760px;background:#f1eee5;font-family:'Microsoft YaHei',
29
30
  #capture:after{content:'';position:absolute;inset:17px;border:1px solid #8c8b6f22;pointer-events:none}
30
31
  .heading{display:flex;align-items:center;gap:24px}.heading-copy{flex:1;min-width:0}.overline{font-size:14px;letter-spacing:6px;color:#868a76;margin-bottom:14px}.name{font:700 43px/1.35 'KaiTi','STKaiti','Noto Serif CJK SC','SimSun',serif;margin:0;color:#303a31;overflow-wrap:anywhere}
31
32
  .seal{flex-shrink:0;width:66px;min-height:74px;transform:rotate(3deg);padding:8px 3px;color:#a04b40;border:2px solid #a04b40;outline:1px solid #a04b40;outline-offset:-6px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;border-radius:3px 1px 4px 2px}.seal strong{font:700 24px Georgia,'KaiTi',serif;letter-spacing:-1px}.seal span{font:16px 'KaiTi','SimSun',serif;letter-spacing:4px;padding-left:4px}
32
- .meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 26px;margin:24px 0 0;padding:15px 0;border-top:1px solid #8a8a7144;border-bottom:1px solid #8a8a7128;font-size:19px;color:#535d50;line-height:1.5}.meta .slot{color:#a04b40;font:21px 'KaiTi','SimSun',serif;letter-spacing:2px}.meta small{font-size:16px;color:#92917f;margin-right:9px}
33
+ .meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 26px;margin:24px 0 0;padding:15px 0;border-top:1px solid #8a8a7144;border-bottom:1px solid #8a8a7128;font-size:19px;color:#535d50;line-height:1.5}.meta .slot{color:#a04b40;font:21px 'KaiTi','SimSun',serif;letter-spacing:2px}.meta .author{min-width:0;overflow-wrap:anywhere}.meta small{font-size:16px;color:#92917f;margin-right:9px}
33
34
  .art{position:relative;width:610px;height:610px;max-width:100%;margin:18px auto 11px;display:grid;place-items:center;isolation:isolate;overflow:hidden}.art:before{content:'';position:absolute;inset:4%;border-radius:48% 52% 56% 44%;background:radial-gradient(ellipse,#aab4a443,transparent 69%);transform:rotate(-12deg)}.equipment-artwork{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;filter:saturate(.72) contrast(.97);mix-blend-mode:multiply;mask-image:linear-gradient(to bottom,transparent 0,#000 8%,#000 84%,transparent 100%),linear-gradient(to right,transparent 0,#000 7%,#000 93%,transparent 100%);mask-composite:intersect}.art-fallback{position:relative;width:360px;height:360px;max-width:90%;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#7d8b7d;border:1px solid #7c887d3b;border-radius:50%;background:radial-gradient(circle,#b1b9a435,transparent 70%);font-family:'KaiTi','SimSun',serif}.art-fallback:before,.art-fallback:after{content:'';position:absolute;inset:21px;border:1px solid #7c887d22;border-radius:50%}.art-fallback:after{inset:48px;border-color:#9a9e8533}.art-fallback .glyph{font-size:126px;line-height:1.2;opacity:.46}.art-fallback .kind{font-size:20px;letter-spacing:4px;opacity:.55}.art:has(.equipment-artwork) .art-fallback{visibility:hidden}.placeholder-note{position:absolute;right:18px;bottom:16px;z-index:2;padding:5px 9px;background:#f4f1e8ce;border:1px solid #9d9b8755;color:#536055;font:18px 'KaiTi','SimSun',serif;letter-spacing:2px}
34
35
  .power{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:18px 0 22px;border-top:1px solid #8a8a7128;border-bottom:1px solid #8a8a7144;color:#31463e}.power span{font:24px 'KaiTi','SimSun',serif;letter-spacing:12px}.power strong{font:700 45px/1.1 Georgia,'Times New Roman',serif;letter-spacing:2px;font-variant-numeric:lining-nums}
35
36
  .section{margin-top:29px}.section h2{display:flex;align-items:center;gap:16px;margin:0 0 17px;color:#626e5e;font:21px 'KaiTi','SimSun',serif;letter-spacing:5px}.section h2:before{content:'';width:5px;height:17px;background:#a04b40;transform:rotate(8deg);opacity:.75}.section h2:after{content:'';height:1px;flex:1;background:linear-gradient(to right,#7f897c55,transparent)}
36
37
  .affixes{display:grid;grid-template-columns:1fr 1fr;column-gap:26px}.affix{display:flex;align-items:baseline;justify-content:space-between;gap:10px;min-width:0;padding:9px 0 12px;border-bottom:1px solid #8a8a7124;font-size:18px;color:#697064}.affix span{overflow-wrap:anywhere}.affix strong{flex-shrink:0;font:700 22px Georgia,'KaiTi',serif;color:#344b42}.empty-line{font-size:18px;color:#92917f;margin:4px 0 12px}
37
38
  .skill{display:flex;align-items:baseline;justify-content:space-between;gap:18px;padding:5px 0 10px;font-size:20px;color:#304b40}.skill-name{font-weight:700;overflow-wrap:anywhere}.skill-name i{display:inline-block;width:13px;height:13px;margin:0 17px 1px 4px;background:#a04b40;transform:rotate(45deg)}.rank{font:18px Georgia,'KaiTi',serif;color:#7f8d7b;white-space:nowrap}
38
39
  .description p{margin:0;font:22px/1.75 'KaiTi','STKaiti','Noto Serif CJK SC','SimSun',serif;white-space:pre-wrap;overflow-wrap:anywhere;color:#313f38}.price{display:flex;align-items:baseline;gap:16px;margin-top:24px;padding-top:17px;border-top:1px solid #8a8a7128;font-size:18px;color:#8b907d}.price strong{font-size:18px;color:#354c42}.notes{margin-top:17px;padding:10px 15px;border-left:3px solid #a04b4080;background:#e8e8db70;color:#3e5549}.notes p{margin:5px 0;font-size:17px;line-height:1.6;overflow-wrap:anywhere}.edition{display:flex;align-items:center;gap:12px;justify-content:center;margin-top:34px;color:#a6a38e;font:14px 'KaiTi','SimSun',serif;letter-spacing:5px}.edition:before,.edition:after{content:'';height:1px;width:32px;background:#9b9c833d}
39
- </style></head><body><div id="capture" class="equipment-sheet ink-equipment-sheet"><header><div class="heading"><div class="heading-copy"><div class="overline">装 备 图 录</div><h1 class="name">${escape(card.name)}</h1></div><div class="seal"><strong>${escape(card.quality)}</strong><span>品阶</span></div></div><div class="meta"><span class="slot">${escape(card.slot)}</span>${enhancement}</div></header><div class="art"><div class="art-fallback"><span class="glyph">?</span><span class="kind">未知外观</span></div>${artwork ? `<img class="skill-artwork equipment-artwork" alt="" src="${escape(artwork)}">` : ''}${unknownFallback || !artwork ? '<span class="placeholder-note">外观暂未显现</span>' : ''}</div><div class="power"><span>装 备 战 力</span><strong>${escape(power)}</strong></div><section class="section attributes"><h2>附加属性</h2><div class="affixes">${affixes}</div></section>${skills}${description}${price}${notes}<div class="edition">露 · 装备录</div></div></body></html>`
40
+ </style></head><body><div id="capture" class="equipment-sheet ink-equipment-sheet"><header><div class="heading"><div class="heading-copy"><div class="overline">装 备 图 录</div><h1 class="name">${escape(card.name)}</h1></div><div class="seal"><strong>${escape(card.quality)}</strong><span>品阶</span></div></div><div class="meta"><span class="slot">${escape(card.slot)}</span>${enhancement}${author}</div></header><div class="art"><div class="art-fallback"><span class="glyph">?</span><span class="kind">未知外观</span></div>${artwork ? `<img class="skill-artwork equipment-artwork" alt="" src="${escape(artwork)}">` : ''}${unknownFallback || !artwork ? '<span class="placeholder-note">外观暂未显现</span>' : ''}</div><div class="power"><span>装 备 战 力</span><strong>${escape(power)}</strong></div><section class="section attributes"><h2>附加属性</h2><div class="affixes">${affixes}</div></section>${skills}${description}${price}${notes}<div class="edition">露 · 装备录</div></div></body></html>`
40
41
  }
@@ -9,7 +9,7 @@ export interface MessageTiming {
9
9
  renderMs?: number
10
10
  imageBytes?: number
11
11
  sendMs?: number
12
- delivery?: 'image' | 'text' | 'qq-markdown-keyboard' | 'qq-markdown' | 'missing-session'
12
+ delivery?: 'image' | 'text' | 'qq-markdown' | 'missing-session'
13
13
  }
14
14
 
15
15
  export default class handel_base {
@@ -2,23 +2,10 @@ import type { Session } from 'koishi'
2
2
  import { MessageType, type PageNavData } from '../../shared/face'
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage'
4
4
 
5
- type Button = {
6
- id: string
7
- render_data: { label: string; visited_label: string; style: number }
8
- action: {
9
- type: 2
10
- permission: { type: 0; specify_user_ids: string[] }
11
- data: string
12
- enter?: true
13
- unsupport_tips: string
14
- }
15
- }
16
-
17
5
  type QQRequest = {
18
6
  content: string
19
7
  msg_type: 2
20
8
  markdown: { content: string }
21
- keyboard?: { content: { rows: Array<{ buttons: Button[] }> } }
22
9
  msg_id?: string
23
10
  msg_seq?: number
24
11
  }
@@ -40,26 +27,39 @@ function escapeMarkdown(value: string): string {
40
27
  return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, '\\$&')
41
28
  }
42
29
 
43
- export function pageCommand(command: string): string {
44
- return command.startsWith('/') ? command : `/${command}`
30
+ /** Compact display padding while retaining spaces between English words. */
31
+ function compactDisplayText(value: string): string {
32
+ return value.trim()
33
+ .replace(/[ \t\u3000]*([·×+|])[ \t\u3000]*/g, '$1')
34
+ .replace(/([\p{Script=Han}\p{Extended_Pictographic}\p{Emoji_Modifier}\p{Regional_Indicator}\uFE0F])[ \t\u3000]+/gu, '$1')
35
+ .replace(/[ \t\u3000]+(?=[\p{Script=Han}\p{Extended_Pictographic}\p{Regional_Indicator}])/gu, '')
36
+ }
37
+
38
+ function compactListLabel(value: string): string {
39
+ const label = compactDisplayText(value)
40
+ // The book icon already identifies skill books. Only shorten the displayed
41
+ // name suffix, including before inventory quantities or market columns.
42
+ return /[📖📚]/u.test(label)
43
+ ? label.replace(/技能书(?=$|×\d+(?:\.\d+)?(?:||$)||)/g, '')
44
+ : label
45
45
  }
46
46
 
47
47
  function inlineCommandUrl(command: string): string {
48
48
  // encodeURIComponent leaves parentheses unescaped, which would close the
49
49
  // surrounding Markdown link if an item ID ever contained one.
50
- const encoded = encodeURIComponent(pageCommand(command)).replace(/[!'()*]/g, character =>
50
+ const encoded = encodeURIComponent(command).replace(/[!'()*]/g, character =>
51
51
  `%${character.charCodeAt(0).toString(16).toUpperCase()}`)
52
52
  return `mqqapi://aio/inlinecmd?command=${encoded}&enter=true&reply=false`
53
53
  }
54
54
 
55
55
  export function pageCommandLines(nav: PageNavData): string[] {
56
56
  return [
57
- ...(nav.previousCommand ? [`上一页:${pageCommand(nav.previousCommand)}`] : []),
58
- ...(nav.nextCommand ? [`下一页:${pageCommand(nav.nextCommand)}`] : []),
57
+ ...(nav.previousCommand ? [nav.previousCommand] : []),
58
+ ...(nav.nextCommand ? [nav.nextCommand] : []),
59
59
  ]
60
60
  }
61
61
 
62
- export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, showCommands: boolean): string {
62
+ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): string {
63
63
  const nav = data.pageNav
64
64
  const entryCommands = new Map(nav?.entries?.map(entry => [entry.lineIndex, entry.viewCommand]) ?? [])
65
65
  const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/
@@ -71,7 +71,7 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, sh
71
71
  const count = summary?.[1] ?? (nav?.total === 1 ? String(listCount) : undefined)
72
72
  const countUnit = summary?.[2] ?? '项'
73
73
  const isLeaderboardDirectory = !!nav?.entries?.length
74
- && nav.entries.every(entry => /^排行榜查看\s/.test(entry.viewCommand))
74
+ && /^(?:🏆)?排行榜$/u.test(compactDisplayText(title?.content ?? ''))
75
75
  const sections: string[] = []
76
76
  const bodyLines: string[] = []
77
77
  const listLines: string[] = []
@@ -79,7 +79,7 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, sh
79
79
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(' \n'))
80
80
  }
81
81
  const flushList = () => {
82
- if (listLines.length) sections.push(listLines.splice(0).join(' \n'))
82
+ if (listLines.length) sections.push(listLines.splice(0).join('\n'))
83
83
  }
84
84
  const flush = () => { flushList(); flushBody() }
85
85
  let hasPageSummary = false
@@ -91,10 +91,10 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, sh
91
91
  case MessageType.title:
92
92
  flush()
93
93
  if (isLeaderboardDirectory) {
94
- sections.push(`## ${item.content.includes('🏆') ? '' : '🏆 '}${escapeMarkdown(item.content)}`)
95
- sections.push(`> 共 ${nav!.entries!.length} 个榜单 · 点击榜名查看`)
94
+ sections.push(`## ${item.content.includes('🏆') ? '' : '🏆'}${escapeMarkdown(compactDisplayText(item.content))}`)
95
+ sections.push(`> 共${nav!.entries!.length}个榜单·点击榜名查看`)
96
96
  } else {
97
- sections.push(`**${escapeMarkdown(item.content)}${count !== undefined ? ` · ${count} ${countUnit}` : ''}**`)
97
+ sections.push(`**${escapeMarkdown(compactDisplayText(item.content))}${count !== undefined ? `·${count}${countUnit}` : ''}**`)
98
98
  }
99
99
  break
100
100
  case MessageType.line: {
@@ -102,14 +102,21 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, sh
102
102
  if (listEntry) {
103
103
  flushBody()
104
104
  const command = entryCommands.get(lineIndex)
105
- const label = escapeMarkdown(listEntry[2])
105
+ const label = escapeMarkdown(compactListLabel(listEntry[2]))
106
106
  const name = command
107
107
  ? `[${label}](${inlineCommandUrl(command)})`
108
108
  : label
109
- listLines.push(`${escapeMarkdown(`[${listEntry[1]}]`)} ${name}`)
109
+ // Keep the numeric marker as Markdown syntax; escape only the item text.
110
+ listLines.push(`${listEntry[1]}. ${name}`)
110
111
  break
111
112
  }
112
113
  flushList()
114
+ // Recruitment pages include this legacy plain-text navigation hint.
115
+ // QQ navigation is already rendered as clickable links in the footer.
116
+ if (nav && item.content.trim() === '[上一页] [下一页]') {
117
+ flushBody()
118
+ break
119
+ }
113
120
  if (/^\[指令\]相关指令$/.test(item.content)) {
114
121
  flushBody()
115
122
  hasHelp = true
@@ -118,11 +125,14 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, sh
118
125
  const page = pagePattern.exec(item.content)
119
126
  if (page && nav) {
120
127
  flushBody()
121
- pageFooter = `**第 ${nav.current} / ${nav.total} 页${!title && page[1] ? ` · 共 ${page[1]} ${page[2]}` : ''}**`
128
+ pageFooter = `**第${nav.current}/${nav.total}页${!title && page[1] ? `·共${page[1]}${page[2]}` : ''}**`
122
129
  hasPageSummary = true
123
130
  break
124
131
  }
125
- bodyLines.push(escapeMarkdown(item.content))
132
+ const command = entryCommands.get(lineIndex)
133
+ bodyLines.push(command
134
+ ? `[${escapeMarkdown(compactDisplayText(item.content))}](${inlineCommandUrl(command)})`
135
+ : escapeMarkdown(item.content))
126
136
  break
127
137
  }
128
138
  case MessageType.error:
@@ -133,43 +143,20 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, sh
133
143
  }
134
144
  flush()
135
145
  if (nav && !hasPageSummary && (nav.total > 1 || nav.previousCommand || nav.nextCommand))
136
- pageFooter = `**第 ${nav.current} / ${nav.total} 页**`
146
+ pageFooter = `**第${nav.current}/${nav.total}页**`
147
+ const previousLink = nav?.previousCommand ? `[上一页](${inlineCommandUrl(nav.previousCommand)})` : undefined
148
+ const nextLink = nav?.nextCommand ? `[下一页](${inlineCommandUrl(nav.nextCommand)})` : undefined
137
149
  const helpLink = hasHelp ? `[指令](${inlineCommandUrl('指令')})` : undefined
138
150
  const footer = pageFooter
139
- ? [pageFooter, helpLink].filter(Boolean).join(' · ')
151
+ ? [pageFooter, previousLink, nextLink, helpLink].filter(Boolean).join('·')
140
152
  : helpLink ? `更多操作:${helpLink}` : undefined
141
153
  if (footer) {
142
154
  if (isLeaderboardDirectory) sections.push('***')
143
155
  sections.push(footer)
144
156
  }
145
- if (showCommands && nav) {
146
- const commands = pageCommandLines(nav)
147
- if (commands.length) sections.push(`**翻页指令** \n${commands.map(escapeMarkdown).join(' \n')}`)
148
- }
149
157
  return sections.join('\n\n') || ' '
150
158
  }
151
159
 
152
- function makeButton(id: string, label: string, command: string, userId: string, autoEnter: boolean): Button {
153
- return {
154
- id,
155
- render_data: { label, visited_label: label, style: 1 },
156
- action: {
157
- type: 2,
158
- permission: { type: 0, specify_user_ids: [userId] },
159
- data: pageCommand(command),
160
- ...(autoEnter ? { enter: true as const } : {}),
161
- unsupport_tips: '当前客户端暂不支持该按钮,请发送上一页或下一页。',
162
- },
163
- }
164
- }
165
-
166
- function buttonsFor(nav: PageNavData, userId: string, autoEnter: boolean): Button[] {
167
- return [
168
- ...(nav.previousCommand ? [makeButton('previous', '上一页', nav.previousCommand, userId, autoEnter)] : []),
169
- ...(nav.nextCommand ? [makeButton('next', '下一页', nav.nextCommand, userId, autoEnter)] : []),
170
- ]
171
- }
172
-
173
160
  /** Returns false if this is not the matching QQ session or the adapter has no native API. */
174
161
  export function canSendQQPage(session: Session | undefined, data: MsgMessage): session is Session {
175
162
  if (!session || session.bot.platform !== 'qq' || session.bot.userId !== data.botId) return false
@@ -178,27 +165,17 @@ export function canSendQQPage(session: Session | undefined, data: MsgMessage): s
178
165
  return !!internal?.sendMessage && !!internal.sendPrivateMessage
179
166
  }
180
167
 
181
- /** Send a native QQ Markdown page, optionally with auto-sending command buttons. */
182
- export async function sendQQPage(session: Session, data: MsgMessage, withKeyboard: boolean): Promise<void> {
168
+ /** Send a native QQ Markdown page with inline command links. */
169
+ export async function sendQQPage(session: Session, data: MsgMessage): Promise<void> {
183
170
  const internal = (session.bot as typeof session.bot & { internal: QQInternal }).internal
184
- const nav = data.pageNav!
185
- const buttons = withKeyboard ? buttonsFor(nav, data.userId, session.isDirect) : []
186
- const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0, buttons.length === 0)
187
- const hasEntries = !!nav.entries?.length
188
- const hasInlineHelp = data.line.some(item => item.type === MessageType.line && item.content === '[指令]相关指令')
171
+ const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0)
189
172
  const request: QQRequest = {
190
173
  // With msg_type=2, QQ requires the ordinary content field to be empty.
191
174
  content: '',
192
175
  msg_type: 2,
193
- markdown: {
194
- content: !session.isDirect && (buttons.length || hasEntries || hasInlineHelp)
195
- ? `${markdown}\n\n点击后如未自动发送,请点发送。`
196
- : markdown,
197
- },
198
- ...(buttons.length ? { keyboard: { content: { rows: [{ buttons }] } } } : {}),
176
+ markdown: { content: markdown },
199
177
  }
200
- // Match the QQ adapter's passive-reply sequence, including when a prior
201
- // keyboard attempt failed and this call retries without the keyboard.
178
+ // Match the QQ adapter's passive-reply sequence.
202
179
  const age = Date.now() - session.timestamp
203
180
  if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1000 - 2000) {
204
181
  const sequenced = session as Session & { seq?: number }
@@ -10,20 +10,8 @@ export default class handelr_text extends handel_base {
10
10
  const session = sessions.get(sessionKey(data.botId, data.userId, data.channelId))
11
11
  if (data.pageNav && canSendQQPage(session, data)) {
12
12
  const sendStartedAt = this.timing ? performance.now() : 0
13
- if (data.pageNav.previousCommand || data.pageNav.nextCommand) {
14
- try {
15
- await sendQQPage(session, data, true)
16
- if (this.timing) {
17
- this.timing.delivery = 'qq-markdown-keyboard'
18
- this.timing.sendMs = performance.now() - sendStartedAt
19
- }
20
- return
21
- } catch (error) {
22
- this.ctx.logger.warn('QQ 翻页按钮发送失败,改用 Markdown 文字指令', error instanceof Error ? error.message : error)
23
- }
24
- }
25
13
  try {
26
- await sendQQPage(session, data, false)
14
+ await sendQQPage(session, data)
27
15
  if (this.timing) {
28
16
  this.timing.delivery = 'qq-markdown'
29
17
  this.timing.sendMs = performance.now() - sendStartedAt
@@ -20,7 +20,7 @@ export interface ServiceType {
20
20
  }
21
21
 
22
22
  export const serviceProto: ServiceProto<ServiceType> = {
23
- "version": 10,
23
+ "version": 11,
24
24
  "services": [
25
25
  {
26
26
  "id": 0,
@@ -612,6 +612,14 @@ export const serviceProto: ServiceProto<ServiceType> = {
612
612
  }
613
613
  },
614
614
  "optional": true
615
+ },
616
+ {
617
+ "id": 12,
618
+ "name": "authorName",
619
+ "type": {
620
+ "type": "String"
621
+ },
622
+ "optional": true
615
623
  }
616
624
  ]
617
625
  },
@@ -1042,4 +1050,4 @@ export const serviceProto: ServiceProto<ServiceType> = {
1042
1050
  ]
1043
1051
  }
1044
1052
  }
1045
- };
1053
+ };
@@ -23,6 +23,7 @@ export interface EquipmentCardData {
23
23
  affixes: Array<{ name: string; value: string }>;
24
24
  skills: Array<{ name: string; rank: string }>;
25
25
  notes?: string[];
26
+ authorName?: string;
26
27
  }
27
28
  export interface LeaderboardImageData {
28
29
  backgroundUrl?: string;
@@ -83,7 +84,7 @@ export interface IErrorMessage {
83
84
  /** 第一阶段支持的消息项 **/
84
85
  export type IMessageItem = ITitleMessage | ILineMessage | IErrorMessage;
85
86
 
86
- /** 平台无关的翻页动作;按钮发送普通指令,客户端决定如何呈现。 */
87
+ /** 平台无关的翻页动作;客户端决定如何呈现可点击文字,点击后发送普通指令。 */
87
88
  export interface PageNavData {
88
89
  current: number;
89
90
  total: number;