koishi-plugin-dew-bot 1.0.246 → 1.0.248

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");
@@ -717,23 +718,28 @@ function escapeMarkdown(value) {
717
718
  return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, "\\$&");
718
719
  }
719
720
  __name(escapeMarkdown, "escapeMarkdown");
720
- function pageCommand(command) {
721
- return command.startsWith("/") ? command : `/${command}`;
721
+ function compactDisplayText(value) {
722
+ 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
723
  }
723
- __name(pageCommand, "pageCommand");
724
+ __name(compactDisplayText, "compactDisplayText");
725
+ function compactListLabel(value) {
726
+ const label = compactDisplayText(value);
727
+ return /[📖📚]/u.test(label) ? label.replace(/技能书(?=$|×\d+(?:\.\d+)?(?:||$)||)/g, "") : label;
728
+ }
729
+ __name(compactListLabel, "compactListLabel");
724
730
  function inlineCommandUrl(command) {
725
- const encoded = encodeURIComponent(pageCommand(command)).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
731
+ const encoded = encodeURIComponent(command).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
726
732
  return `mqqapi://aio/inlinecmd?command=${encoded}&enter=true&reply=false`;
727
733
  }
728
734
  __name(inlineCommandUrl, "inlineCommandUrl");
729
735
  function pageCommandLines(nav) {
730
736
  return [
731
- ...nav.previousCommand ? [`上一页:${pageCommand(nav.previousCommand)}`] : [],
732
- ...nav.nextCommand ? [`下一页:${pageCommand(nav.nextCommand)}`] : []
737
+ ...nav.previousCommand ? [nav.previousCommand] : [],
738
+ ...nav.nextCommand ? [nav.nextCommand] : []
733
739
  ];
734
740
  }
735
741
  __name(pageCommandLines, "pageCommandLines");
736
- function renderQQPageMarkdown(data, showGreeting, showCommands) {
742
+ function renderQQPageMarkdown(data, showGreeting) {
737
743
  const nav = data.pageNav;
738
744
  const entryCommands = new Map(nav?.entries?.map((entry) => [entry.lineIndex, entry.viewCommand]) ?? []);
739
745
  const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/;
@@ -744,7 +750,7 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
744
750
  const listCount = data.line.filter((item) => item.type === 1 /* line */ && listPattern.test(item.content)).length;
745
751
  const count = summary?.[1] ?? (nav?.total === 1 ? String(listCount) : void 0);
746
752
  const countUnit = summary?.[2] ?? "项";
747
- const isLeaderboardDirectory = !!nav?.entries?.length && nav.entries.every((entry) => /^排行榜查看\s/.test(entry.viewCommand));
753
+ const isLeaderboardDirectory = !!nav?.entries?.length && /^(?:🏆)?排行榜$/u.test(compactDisplayText(title?.content ?? ""));
748
754
  const sections = [];
749
755
  const bodyLines = [];
750
756
  const listLines = [];
@@ -752,7 +758,7 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
752
758
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
753
759
  }, "flushBody");
754
760
  const flushList = /* @__PURE__ */ __name(() => {
755
- if (listLines.length) sections.push(listLines.splice(0).join(" \n"));
761
+ if (listLines.length) sections.push(listLines.splice(0).join("\n"));
756
762
  }, "flushList");
757
763
  const flush = /* @__PURE__ */ __name(() => {
758
764
  flushList();
@@ -767,10 +773,10 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
767
773
  case 0 /* title */:
768
774
  flush();
769
775
  if (isLeaderboardDirectory) {
770
- sections.push(`## ${item.content.includes("🏆") ? "" : "🏆 "}${escapeMarkdown(item.content)}`);
771
- sections.push(`> 共 ${nav.entries.length} 个榜单 · 点击榜名查看`);
776
+ sections.push(`## ${item.content.includes("🏆") ? "" : "🏆"}${escapeMarkdown(compactDisplayText(item.content))}`);
777
+ sections.push(`> 共${nav.entries.length}个榜单·点击榜名查看`);
772
778
  } else {
773
- sections.push(`**${escapeMarkdown(item.content)}${count !== void 0 ? ` · ${count} ${countUnit}` : ""}**`);
779
+ sections.push(`**${escapeMarkdown(compactDisplayText(item.content))}${count !== void 0 ? `·${count}${countUnit}` : ""}**`);
774
780
  }
775
781
  break;
776
782
  case 1 /* line */: {
@@ -778,12 +784,16 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
778
784
  if (listEntry) {
779
785
  flushBody();
780
786
  const command = entryCommands.get(lineIndex);
781
- const label = escapeMarkdown(listEntry[2]);
787
+ const label = escapeMarkdown(compactListLabel(listEntry[2]));
782
788
  const name2 = command ? `[${label}](${inlineCommandUrl(command)})` : label;
783
- listLines.push(`${escapeMarkdown(`[${listEntry[1]}]`)} ${name2}`);
789
+ listLines.push(`${listEntry[1]}. ${name2}`);
784
790
  break;
785
791
  }
786
792
  flushList();
793
+ if (nav && item.content.trim() === "[上一页] [下一页]") {
794
+ flushBody();
795
+ break;
796
+ }
787
797
  if (/^\[指令\]相关指令$/.test(item.content)) {
788
798
  flushBody();
789
799
  hasHelp = true;
@@ -792,7 +802,7 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
792
802
  const page = pagePattern.exec(item.content);
793
803
  if (page && nav) {
794
804
  flushBody();
795
- pageFooter = `**第 ${nav.current} / ${nav.total} 页${!title && page[1] ? ` · 共 ${page[1]} ${page[2]}` : ""}**`;
805
+ pageFooter = `**第${nav.current}/${nav.total}页${!title && page[1] ? `·共${page[1]}${page[2]}` : ""}**`;
796
806
  hasPageSummary = true;
797
807
  break;
798
808
  }
@@ -807,42 +817,18 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
807
817
  }
808
818
  flush();
809
819
  if (nav && !hasPageSummary && (nav.total > 1 || nav.previousCommand || nav.nextCommand))
810
- pageFooter = `**第 ${nav.current} / ${nav.total} 页**`;
820
+ pageFooter = `**第${nav.current}/${nav.total}页**`;
821
+ const previousLink = nav?.previousCommand ? `[上一页](${inlineCommandUrl(nav.previousCommand)})` : void 0;
822
+ const nextLink = nav?.nextCommand ? `[下一页](${inlineCommandUrl(nav.nextCommand)})` : void 0;
811
823
  const helpLink = hasHelp ? `[指令](${inlineCommandUrl("指令")})` : void 0;
812
- const footer = pageFooter ? [pageFooter, helpLink].filter(Boolean).join(" · ") : helpLink ? `更多操作:${helpLink}` : void 0;
824
+ const footer = pageFooter ? [pageFooter, previousLink, nextLink, helpLink].filter(Boolean).join("·") : helpLink ? `更多操作:${helpLink}` : void 0;
813
825
  if (footer) {
814
826
  if (isLeaderboardDirectory) sections.push("***");
815
827
  sections.push(footer);
816
828
  }
817
- if (showCommands && nav) {
818
- const commands = pageCommandLines(nav);
819
- if (commands.length) sections.push(`**翻页指令**
820
- ${commands.map(escapeMarkdown).join(" \n")}`);
821
- }
822
829
  return sections.join("\n\n") || " ";
823
830
  }
824
831
  __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
832
  function canSendQQPage(session, data) {
847
833
  if (!session || session.bot.platform !== "qq" || session.bot.userId !== data.botId) return false;
848
834
  if (session.userId !== data.userId || (session.channelId ?? session.userId) !== data.channelId) return false;
@@ -850,23 +836,21 @@ function canSendQQPage(session, data) {
850
836
  return !!internal?.sendMessage && !!internal.sendPrivateMessage;
851
837
  }
852
838
  __name(canSendQQPage, "canSendQQPage");
853
- async function sendQQPage(session, data, withKeyboard) {
839
+ async function sendQQPage(session, data) {
854
840
  const internal = session.bot.internal;
855
841
  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;
842
+ const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0);
843
+ const hasInlineNavigation = !!(nav.previousCommand || nav.nextCommand || nav.entries?.length);
859
844
  const hasInlineHelp = data.line.some((item) => item.type === 1 /* line */ && item.content === "[指令]相关指令");
860
845
  const request = {
861
846
  // With msg_type=2, QQ requires the ordinary content field to be empty.
862
847
  content: "",
863
848
  msg_type: 2,
864
849
  markdown: {
865
- content: !session.isDirect && (buttons.length || hasEntries || hasInlineHelp) ? `${markdown}
850
+ content: !session.isDirect && (hasInlineNavigation || hasInlineHelp) ? `${markdown}
866
851
 
867
852
  点击后如未自动发送,请点发送。` : markdown
868
- },
869
- ...buttons.length ? { keyboard: { content: { rows: [{ buttons }] } } } : {}
853
+ }
870
854
  };
871
855
  const age = Date.now() - session.timestamp;
872
856
  if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1e3 - 2e3) {
@@ -891,20 +875,8 @@ var handelr_text = class extends handel_base {
891
875
  const session = sessions_default.get(sessionKey(data.botId, data.userId, data.channelId));
892
876
  if (data.pageNav && canSendQQPage(session, data)) {
893
877
  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
878
  try {
907
- await sendQQPage(session, data, false);
879
+ await sendQQPage(session, data);
908
880
  if (this.timing) {
909
881
  this.timing.delivery = "qq-markdown";
910
882
  this.timing.sendMs = performance.now() - sendStartedAt;
@@ -2170,8 +2142,23 @@ __name(apply, "apply");
2170
2142
  // src/index.ts
2171
2143
  var inject = { optional: ["puppeteer"] };
2172
2144
  var name = "dew-bot";
2145
+ var usage = `
2146
+ ## 🌀 DEW · 无限轮回 RPG
2147
+
2148
+ > **下一场轮回,主角是你。**
2149
+
2150
+ **DEW Bot** 是一款基于《无限恐怖》小说改编的 **RPG 机器人**。
2151
+
2152
+ 踏入主神空间,成为轮回者。与伙伴迎接未知的试炼,在生死之间积蓄力量,书写属于你的无限冒险。
2153
+
2154
+ 无论你熟悉原作,还是初次踏入无限流世界,这场轮回,都有你的位置。
2155
+
2156
+ ---
2157
+
2158
+ 连接 DEW 游戏服后,在聊天中发送 **帮助**,开启第一场冒险。
2159
+ `;
2173
2160
  var Config = import_koishi2.Schema.object({
2174
- 服务器地址: import_koishi2.Schema.string().default("ws://dew-v7-test:3021").description("游戏服 WebSocket 地址")
2161
+ 服务器地址: import_koishi2.Schema.string().default("ws://dew-v7-test:3021").description("DEW 游戏服的 WebSocket 地址;同机部署可填写 ws://127.0.0.1:3021")
2175
2162
  });
2176
2163
  function apply2(ctx, config) {
2177
2164
  ctx.plugin(apply, config);
@@ -2182,5 +2169,6 @@ __name(apply2, "apply");
2182
2169
  Config,
2183
2170
  apply,
2184
2171
  inject,
2185
- name
2172
+ name,
2173
+ usage
2186
2174
  });
@@ -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>;
@@ -108,7 +108,7 @@ export interface IErrorMessage {
108
108
  }
109
109
  /** 第一阶段支持的消息项 **/
110
110
  export type IMessageItem = ITitleMessage | ILineMessage | IErrorMessage;
111
- /** 平台无关的翻页动作;按钮发送普通指令,客户端决定如何呈现。 */
111
+ /** 平台无关的翻页动作;客户端决定如何呈现可点击文字,点击后发送普通指令。 */
112
112
  export interface PageNavData {
113
113
  current: number;
114
114
  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.248",
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) {
@@ -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,7 +125,7 @@ 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
  }
@@ -133,43 +140,20 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, sh
133
140
  }
134
141
  flush()
135
142
  if (nav && !hasPageSummary && (nav.total > 1 || nav.previousCommand || nav.nextCommand))
136
- pageFooter = `**第 ${nav.current} / ${nav.total} 页**`
143
+ pageFooter = `**第${nav.current}/${nav.total}页**`
144
+ const previousLink = nav?.previousCommand ? `[上一页](${inlineCommandUrl(nav.previousCommand)})` : undefined
145
+ const nextLink = nav?.nextCommand ? `[下一页](${inlineCommandUrl(nav.nextCommand)})` : undefined
137
146
  const helpLink = hasHelp ? `[指令](${inlineCommandUrl('指令')})` : undefined
138
147
  const footer = pageFooter
139
- ? [pageFooter, helpLink].filter(Boolean).join(' · ')
148
+ ? [pageFooter, previousLink, nextLink, helpLink].filter(Boolean).join('·')
140
149
  : helpLink ? `更多操作:${helpLink}` : undefined
141
150
  if (footer) {
142
151
  if (isLeaderboardDirectory) sections.push('***')
143
152
  sections.push(footer)
144
153
  }
145
- if (showCommands && nav) {
146
- const commands = pageCommandLines(nav)
147
- if (commands.length) sections.push(`**翻页指令** \n${commands.map(escapeMarkdown).join(' \n')}`)
148
- }
149
154
  return sections.join('\n\n') || ' '
150
155
  }
151
156
 
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
157
  /** Returns false if this is not the matching QQ session or the adapter has no native API. */
174
158
  export function canSendQQPage(session: Session | undefined, data: MsgMessage): session is Session {
175
159
  if (!session || session.bot.platform !== 'qq' || session.bot.userId !== data.botId) return false
@@ -178,27 +162,24 @@ export function canSendQQPage(session: Session | undefined, data: MsgMessage): s
178
162
  return !!internal?.sendMessage && !!internal.sendPrivateMessage
179
163
  }
180
164
 
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> {
165
+ /** Send a native QQ Markdown page with inline command links. */
166
+ export async function sendQQPage(session: Session, data: MsgMessage): Promise<void> {
183
167
  const internal = (session.bot as typeof session.bot & { internal: QQInternal }).internal
184
168
  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
169
+ const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0)
170
+ const hasInlineNavigation = !!(nav.previousCommand || nav.nextCommand || nav.entries?.length)
188
171
  const hasInlineHelp = data.line.some(item => item.type === MessageType.line && item.content === '[指令]相关指令')
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
176
  markdown: {
194
- content: !session.isDirect && (buttons.length || hasEntries || hasInlineHelp)
177
+ content: !session.isDirect && (hasInlineNavigation || hasInlineHelp)
195
178
  ? `${markdown}\n\n点击后如未自动发送,请点发送。`
196
179
  : markdown,
197
180
  },
198
- ...(buttons.length ? { keyboard: { content: { rows: [{ buttons }] } } } : {}),
199
181
  }
200
- // Match the QQ adapter's passive-reply sequence, including when a prior
201
- // keyboard attempt failed and this call retries without the keyboard.
182
+ // Match the QQ adapter's passive-reply sequence.
202
183
  const age = Date.now() - session.timestamp
203
184
  if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1000 - 2000) {
204
185
  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
@@ -83,7 +83,7 @@ export interface IErrorMessage {
83
83
  /** 第一阶段支持的消息项 **/
84
84
  export type IMessageItem = ITitleMessage | ILineMessage | IErrorMessage;
85
85
 
86
- /** 平台无关的翻页动作;按钮发送普通指令,客户端决定如何呈现。 */
86
+ /** 平台无关的翻页动作;客户端决定如何呈现可点击文字,点击后发送普通指令。 */
87
87
  export interface PageNavData {
88
88
  current: number;
89
89
  total: number;