koishi-plugin-dew-bot 1.0.245 → 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 +16 -23
- package/lib/index.d.ts +1 -0
- package/lib/index.js +81 -70
- package/lib/plugin/serverHandel/handel_base.d.ts +1 -1
- package/lib/plugin/serverHandel/qqPageMessage.d.ts +3 -4
- package/lib/shared/face/index.d.ts +1 -1
- package/package.json +20 -4
- package/src/index.ts +17 -1
- package/src/plugin/serverHandel/handel_base.ts +1 -1
- package/src/plugin/serverHandel/qqPageMessage.ts +73 -66
- package/src/plugin/serverHandel/text.ts +1 -13
- package/src/shared/face/index.ts +1 -1
package/README.md
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
## 配置
|
|
18
|
-
|
|
19
|
-
| 项 | 说明 | 默认 |
|
|
20
|
-
| --- | --- | --- |
|
|
21
|
-
| 服务器地址 | 游戏服 WebSocket 地址 | `ws://dew-v7-test:3021` |
|
|
22
|
-
|
|
23
|
-
插件会忽略机器人自己发出的消息,避免回显再次上传形成循环。
|
|
1
|
+
# 🌀 DEW · 无限轮回 RPG
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/koishi-plugin-dew-bot)
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
> **下一场轮回,主角是你。**
|
|
7
|
+
|
|
8
|
+
**DEW Bot** 是一款基于《无限恐怖》小说改编的 **RPG 机器人**。
|
|
9
|
+
|
|
10
|
+
踏入主神空间,成为轮回者。与伙伴迎接未知的试炼,在生死之间积蓄力量,书写属于你的无限冒险。
|
|
11
|
+
|
|
12
|
+
无论你熟悉原作,还是初次踏入无限流世界,这场轮回,都有你的位置。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
连接 DEW 游戏服后,在聊天中发送 **帮助**,开启第一场冒险。
|
package/lib/index.d.ts
CHANGED
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,66 +718,91 @@ function escapeMarkdown(value) {
|
|
|
717
718
|
return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, "\\$&");
|
|
718
719
|
}
|
|
719
720
|
__name(escapeMarkdown, "escapeMarkdown");
|
|
720
|
-
function
|
|
721
|
-
return
|
|
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(
|
|
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(
|
|
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 ? [
|
|
732
|
-
...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
|
|
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]) ?? []);
|
|
745
|
+
const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/;
|
|
746
|
+
const pagePattern = /^第\s*\d+\s*\/\s*\d+\s*页(?:\s*[||]\s*共\s*(\d+)\s*([项条]))?$/;
|
|
747
|
+
const title = data.line.find((item) => item.type === 0 /* title */);
|
|
748
|
+
const summaryLine = data.line.find((item) => item.type === 1 /* line */ && pagePattern.test(item.content));
|
|
749
|
+
const summary = summaryLine && pagePattern.exec(summaryLine.content);
|
|
750
|
+
const listCount = data.line.filter((item) => item.type === 1 /* line */ && listPattern.test(item.content)).length;
|
|
751
|
+
const count = summary?.[1] ?? (nav?.total === 1 ? String(listCount) : void 0);
|
|
752
|
+
const countUnit = summary?.[2] ?? "项";
|
|
753
|
+
const isLeaderboardDirectory = !!nav?.entries?.length && /^(?:🏆)?排行榜$/u.test(compactDisplayText(title?.content ?? ""));
|
|
739
754
|
const sections = [];
|
|
740
755
|
const bodyLines = [];
|
|
741
|
-
const
|
|
756
|
+
const listLines = [];
|
|
742
757
|
const flushBody = /* @__PURE__ */ __name(() => {
|
|
743
758
|
if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
|
|
744
759
|
}, "flushBody");
|
|
745
|
-
const
|
|
746
|
-
if (
|
|
747
|
-
}, "
|
|
760
|
+
const flushList = /* @__PURE__ */ __name(() => {
|
|
761
|
+
if (listLines.length) sections.push(listLines.splice(0).join("\n"));
|
|
762
|
+
}, "flushList");
|
|
748
763
|
const flush = /* @__PURE__ */ __name(() => {
|
|
749
|
-
|
|
764
|
+
flushList();
|
|
750
765
|
flushBody();
|
|
751
766
|
}, "flush");
|
|
752
767
|
let hasPageSummary = false;
|
|
768
|
+
let hasHelp = false;
|
|
769
|
+
let pageFooter;
|
|
753
770
|
if (showGreeting) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
|
|
754
771
|
for (const [lineIndex, item] of data.line.entries()) {
|
|
755
772
|
switch (item.type) {
|
|
756
773
|
case 0 /* title */:
|
|
757
774
|
flush();
|
|
758
|
-
|
|
775
|
+
if (isLeaderboardDirectory) {
|
|
776
|
+
sections.push(`## ${item.content.includes("🏆") ? "" : "🏆"}${escapeMarkdown(compactDisplayText(item.content))}`);
|
|
777
|
+
sections.push(`> 共${nav.entries.length}个榜单·点击榜名查看`);
|
|
778
|
+
} else {
|
|
779
|
+
sections.push(`**${escapeMarkdown(compactDisplayText(item.content))}${count !== void 0 ? `·${count}${countUnit}` : ""}**`);
|
|
780
|
+
}
|
|
759
781
|
break;
|
|
760
782
|
case 1 /* line */: {
|
|
761
|
-
const
|
|
762
|
-
if (
|
|
783
|
+
const listEntry = listPattern.exec(item.content);
|
|
784
|
+
if (listEntry) {
|
|
763
785
|
flushBody();
|
|
764
786
|
const command = entryCommands.get(lineIndex);
|
|
765
|
-
const label = escapeMarkdown(
|
|
787
|
+
const label = escapeMarkdown(compactListLabel(listEntry[2]));
|
|
766
788
|
const name2 = command ? `[${label}](${inlineCommandUrl(command)})` : label;
|
|
767
|
-
|
|
789
|
+
listLines.push(`${listEntry[1]}. ${name2}`);
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
flushList();
|
|
793
|
+
if (nav && item.content.trim() === "[上一页] [下一页]") {
|
|
794
|
+
flushBody();
|
|
768
795
|
break;
|
|
769
796
|
}
|
|
770
|
-
flushNumbered();
|
|
771
797
|
if (/^\[指令\]相关指令$/.test(item.content)) {
|
|
772
798
|
flushBody();
|
|
773
|
-
|
|
799
|
+
hasHelp = true;
|
|
774
800
|
break;
|
|
775
801
|
}
|
|
776
|
-
const page =
|
|
802
|
+
const page = pagePattern.exec(item.content);
|
|
777
803
|
if (page && nav) {
|
|
778
804
|
flushBody();
|
|
779
|
-
|
|
805
|
+
pageFooter = `**第${nav.current}/${nav.total}页${!title && page[1] ? `·共${page[1]}${page[2]}` : ""}**`;
|
|
780
806
|
hasPageSummary = true;
|
|
781
807
|
break;
|
|
782
808
|
}
|
|
@@ -790,36 +816,19 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
|
|
|
790
816
|
}
|
|
791
817
|
}
|
|
792
818
|
flush();
|
|
793
|
-
if (nav && !hasPageSummary
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
${
|
|
819
|
+
if (nav && !hasPageSummary && (nav.total > 1 || nav.previousCommand || nav.nextCommand))
|
|
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;
|
|
823
|
+
const helpLink = hasHelp ? `[指令](${inlineCommandUrl("指令")})` : void 0;
|
|
824
|
+
const footer = pageFooter ? [pageFooter, previousLink, nextLink, helpLink].filter(Boolean).join("·") : helpLink ? `更多操作:${helpLink}` : void 0;
|
|
825
|
+
if (footer) {
|
|
826
|
+
if (isLeaderboardDirectory) sections.push("***");
|
|
827
|
+
sections.push(footer);
|
|
798
828
|
}
|
|
799
829
|
return sections.join("\n\n") || " ";
|
|
800
830
|
}
|
|
801
831
|
__name(renderQQPageMarkdown, "renderQQPageMarkdown");
|
|
802
|
-
function makeButton(id, label, command, userId, autoEnter) {
|
|
803
|
-
return {
|
|
804
|
-
id,
|
|
805
|
-
render_data: { label, visited_label: label, style: 1 },
|
|
806
|
-
action: {
|
|
807
|
-
type: 2,
|
|
808
|
-
permission: { type: 0, specify_user_ids: [userId] },
|
|
809
|
-
data: pageCommand(command),
|
|
810
|
-
...autoEnter ? { enter: true } : {},
|
|
811
|
-
unsupport_tips: "当前客户端暂不支持该按钮,请发送上一页或下一页。"
|
|
812
|
-
}
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
__name(makeButton, "makeButton");
|
|
816
|
-
function buttonsFor(nav, userId, autoEnter) {
|
|
817
|
-
return [
|
|
818
|
-
...nav.previousCommand ? [makeButton("previous", "上一页", nav.previousCommand, userId, autoEnter)] : [],
|
|
819
|
-
...nav.nextCommand ? [makeButton("next", "下一页", nav.nextCommand, userId, autoEnter)] : []
|
|
820
|
-
];
|
|
821
|
-
}
|
|
822
|
-
__name(buttonsFor, "buttonsFor");
|
|
823
832
|
function canSendQQPage(session, data) {
|
|
824
833
|
if (!session || session.bot.platform !== "qq" || session.bot.userId !== data.botId) return false;
|
|
825
834
|
if (session.userId !== data.userId || (session.channelId ?? session.userId) !== data.channelId) return false;
|
|
@@ -827,23 +836,21 @@ function canSendQQPage(session, data) {
|
|
|
827
836
|
return !!internal?.sendMessage && !!internal.sendPrivateMessage;
|
|
828
837
|
}
|
|
829
838
|
__name(canSendQQPage, "canSendQQPage");
|
|
830
|
-
async function sendQQPage(session, data
|
|
839
|
+
async function sendQQPage(session, data) {
|
|
831
840
|
const internal = session.bot.internal;
|
|
832
841
|
const nav = data.pageNav;
|
|
833
|
-
const
|
|
834
|
-
const
|
|
835
|
-
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);
|
|
836
844
|
const hasInlineHelp = data.line.some((item) => item.type === 1 /* line */ && item.content === "[指令]相关指令");
|
|
837
845
|
const request = {
|
|
838
846
|
// With msg_type=2, QQ requires the ordinary content field to be empty.
|
|
839
847
|
content: "",
|
|
840
848
|
msg_type: 2,
|
|
841
849
|
markdown: {
|
|
842
|
-
content: !session.isDirect && (
|
|
850
|
+
content: !session.isDirect && (hasInlineNavigation || hasInlineHelp) ? `${markdown}
|
|
843
851
|
|
|
844
852
|
点击后如未自动发送,请点发送。` : markdown
|
|
845
|
-
}
|
|
846
|
-
...buttons.length ? { keyboard: { content: { rows: [{ buttons }] } } } : {}
|
|
853
|
+
}
|
|
847
854
|
};
|
|
848
855
|
const age = Date.now() - session.timestamp;
|
|
849
856
|
if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1e3 - 2e3) {
|
|
@@ -868,20 +875,8 @@ var handelr_text = class extends handel_base {
|
|
|
868
875
|
const session = sessions_default.get(sessionKey(data.botId, data.userId, data.channelId));
|
|
869
876
|
if (data.pageNav && canSendQQPage(session, data)) {
|
|
870
877
|
const sendStartedAt = this.timing ? performance.now() : 0;
|
|
871
|
-
if (data.pageNav.previousCommand || data.pageNav.nextCommand) {
|
|
872
|
-
try {
|
|
873
|
-
await sendQQPage(session, data, true);
|
|
874
|
-
if (this.timing) {
|
|
875
|
-
this.timing.delivery = "qq-markdown-keyboard";
|
|
876
|
-
this.timing.sendMs = performance.now() - sendStartedAt;
|
|
877
|
-
}
|
|
878
|
-
return;
|
|
879
|
-
} catch (error) {
|
|
880
|
-
this.ctx.logger.warn("QQ 翻页按钮发送失败,改用 Markdown 文字指令", error instanceof Error ? error.message : error);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
878
|
try {
|
|
884
|
-
await sendQQPage(session, data
|
|
879
|
+
await sendQQPage(session, data);
|
|
885
880
|
if (this.timing) {
|
|
886
881
|
this.timing.delivery = "qq-markdown";
|
|
887
882
|
this.timing.sendMs = performance.now() - sendStartedAt;
|
|
@@ -2147,8 +2142,23 @@ __name(apply, "apply");
|
|
|
2147
2142
|
// src/index.ts
|
|
2148
2143
|
var inject = { optional: ["puppeteer"] };
|
|
2149
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
|
+
`;
|
|
2150
2160
|
var Config = import_koishi2.Schema.object({
|
|
2151
|
-
服务器地址: import_koishi2.Schema.string().default("ws://dew-v7-test:3021").description("
|
|
2161
|
+
服务器地址: import_koishi2.Schema.string().default("ws://dew-v7-test:3021").description("DEW 游戏服的 WebSocket 地址;同机部署可填写 ws://127.0.0.1:3021")
|
|
2152
2162
|
});
|
|
2153
2163
|
function apply2(ctx, config) {
|
|
2154
2164
|
ctx.plugin(apply, config);
|
|
@@ -2159,5 +2169,6 @@ __name(apply2, "apply");
|
|
|
2159
2169
|
Config,
|
|
2160
2170
|
apply,
|
|
2161
2171
|
inject,
|
|
2162
|
-
name
|
|
2172
|
+
name,
|
|
2173
|
+
usage
|
|
2163
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
|
|
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
|
|
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
|
|
10
|
-
export declare function sendQQPage(session: Session, data: MsgMessage
|
|
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.
|
|
3
|
+
"description": "基于《无限恐怖》小说改编的 RPG 机器人。踏入主神空间,与伙伴迎接轮回试炼,书写属于你的无限冒险。",
|
|
4
|
+
"version": "1.0.248",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -9,11 +9,27 @@
|
|
|
9
9
|
"src",
|
|
10
10
|
"assets"
|
|
11
11
|
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"release": "node scripts/release.cjs",
|
|
14
|
+
"release:check": "node scripts/release.cjs --dry-run"
|
|
15
|
+
},
|
|
12
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
|
+
},
|
|
13
25
|
"keywords": [
|
|
14
26
|
"chatbot",
|
|
15
27
|
"koishi",
|
|
16
|
-
"plugin"
|
|
28
|
+
"plugin",
|
|
29
|
+
"rpg",
|
|
30
|
+
"game",
|
|
31
|
+
"qq",
|
|
32
|
+
"markdown"
|
|
17
33
|
],
|
|
18
34
|
"peerDependencies": {
|
|
19
35
|
"koishi": "^4.18.0",
|
|
@@ -25,7 +41,7 @@
|
|
|
25
41
|
},
|
|
26
42
|
"koishi": {
|
|
27
43
|
"description": {
|
|
28
|
-
"zh": "
|
|
44
|
+
"zh": "基于《无限恐怖》小说改编的 RPG 机器人。踏入主神空间,与伙伴迎接轮回试炼,书写属于你的无限冒险。"
|
|
29
45
|
}
|
|
30
46
|
}
|
|
31
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('
|
|
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
|
|
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,68 +27,105 @@ function escapeMarkdown(value: string): string {
|
|
|
40
27
|
return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, '\\$&')
|
|
41
28
|
}
|
|
42
29
|
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
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 ? [
|
|
58
|
-
...(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
|
|
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
|
+
const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/
|
|
66
|
+
const pagePattern = /^第\s*\d+\s*\/\s*\d+\s*页(?:\s*[||]\s*共\s*(\d+)\s*([项条]))?$/
|
|
67
|
+
const title = data.line.find(item => item.type === MessageType.title)
|
|
68
|
+
const summaryLine = data.line.find(item => item.type === MessageType.line && pagePattern.test(item.content))
|
|
69
|
+
const summary = summaryLine && pagePattern.exec(summaryLine.content)
|
|
70
|
+
const listCount = data.line.filter(item => item.type === MessageType.line && listPattern.test(item.content)).length
|
|
71
|
+
const count = summary?.[1] ?? (nav?.total === 1 ? String(listCount) : undefined)
|
|
72
|
+
const countUnit = summary?.[2] ?? '项'
|
|
73
|
+
const isLeaderboardDirectory = !!nav?.entries?.length
|
|
74
|
+
&& /^(?:🏆)?排行榜$/u.test(compactDisplayText(title?.content ?? ''))
|
|
65
75
|
const sections: string[] = []
|
|
66
76
|
const bodyLines: string[] = []
|
|
67
|
-
const
|
|
77
|
+
const listLines: string[] = []
|
|
68
78
|
const flushBody = () => {
|
|
69
79
|
if (bodyLines.length) sections.push(bodyLines.splice(0).join(' \n'))
|
|
70
80
|
}
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
81
|
+
const flushList = () => {
|
|
82
|
+
if (listLines.length) sections.push(listLines.splice(0).join('\n'))
|
|
73
83
|
}
|
|
74
|
-
const flush = () => {
|
|
84
|
+
const flush = () => { flushList(); flushBody() }
|
|
75
85
|
let hasPageSummary = false
|
|
86
|
+
let hasHelp = false
|
|
87
|
+
let pageFooter: string | undefined
|
|
76
88
|
if (showGreeting) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
|
|
77
89
|
for (const [lineIndex, item] of data.line.entries()) {
|
|
78
90
|
switch (item.type) {
|
|
79
91
|
case MessageType.title:
|
|
80
92
|
flush()
|
|
81
|
-
|
|
93
|
+
if (isLeaderboardDirectory) {
|
|
94
|
+
sections.push(`## ${item.content.includes('🏆') ? '' : '🏆'}${escapeMarkdown(compactDisplayText(item.content))}`)
|
|
95
|
+
sections.push(`> 共${nav!.entries!.length}个榜单·点击榜名查看`)
|
|
96
|
+
} else {
|
|
97
|
+
sections.push(`**${escapeMarkdown(compactDisplayText(item.content))}${count !== undefined ? `·${count}${countUnit}` : ''}**`)
|
|
98
|
+
}
|
|
82
99
|
break
|
|
83
100
|
case MessageType.line: {
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
101
|
+
const listEntry = listPattern.exec(item.content)
|
|
102
|
+
if (listEntry) {
|
|
86
103
|
flushBody()
|
|
87
104
|
const command = entryCommands.get(lineIndex)
|
|
88
|
-
const label = escapeMarkdown(
|
|
105
|
+
const label = escapeMarkdown(compactListLabel(listEntry[2]))
|
|
89
106
|
const name = command
|
|
90
107
|
? `[${label}](${inlineCommandUrl(command)})`
|
|
91
108
|
: label
|
|
92
|
-
|
|
109
|
+
// Keep the numeric marker as Markdown syntax; escape only the item text.
|
|
110
|
+
listLines.push(`${listEntry[1]}. ${name}`)
|
|
111
|
+
break
|
|
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()
|
|
93
118
|
break
|
|
94
119
|
}
|
|
95
|
-
flushNumbered()
|
|
96
120
|
if (/^\[指令\]相关指令$/.test(item.content)) {
|
|
97
121
|
flushBody()
|
|
98
|
-
|
|
122
|
+
hasHelp = true
|
|
99
123
|
break
|
|
100
124
|
}
|
|
101
|
-
const page =
|
|
125
|
+
const page = pagePattern.exec(item.content)
|
|
102
126
|
if (page && nav) {
|
|
103
127
|
flushBody()
|
|
104
|
-
|
|
128
|
+
pageFooter = `**第${nav.current}/${nav.total}页${!title && page[1] ? `·共${page[1]}${page[2]}` : ''}**`
|
|
105
129
|
hasPageSummary = true
|
|
106
130
|
break
|
|
107
131
|
}
|
|
@@ -115,35 +139,21 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, sh
|
|
|
115
139
|
}
|
|
116
140
|
}
|
|
117
141
|
flush()
|
|
118
|
-
if (nav && !hasPageSummary
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
if (nav && !hasPageSummary && (nav.total > 1 || nav.previousCommand || nav.nextCommand))
|
|
143
|
+
pageFooter = `**第${nav.current}/${nav.total}页**`
|
|
144
|
+
const previousLink = nav?.previousCommand ? `[上一页](${inlineCommandUrl(nav.previousCommand)})` : undefined
|
|
145
|
+
const nextLink = nav?.nextCommand ? `[下一页](${inlineCommandUrl(nav.nextCommand)})` : undefined
|
|
146
|
+
const helpLink = hasHelp ? `[指令](${inlineCommandUrl('指令')})` : undefined
|
|
147
|
+
const footer = pageFooter
|
|
148
|
+
? [pageFooter, previousLink, nextLink, helpLink].filter(Boolean).join('·')
|
|
149
|
+
: helpLink ? `更多操作:${helpLink}` : undefined
|
|
150
|
+
if (footer) {
|
|
151
|
+
if (isLeaderboardDirectory) sections.push('***')
|
|
152
|
+
sections.push(footer)
|
|
122
153
|
}
|
|
123
154
|
return sections.join('\n\n') || ' '
|
|
124
155
|
}
|
|
125
156
|
|
|
126
|
-
function makeButton(id: string, label: string, command: string, userId: string, autoEnter: boolean): Button {
|
|
127
|
-
return {
|
|
128
|
-
id,
|
|
129
|
-
render_data: { label, visited_label: label, style: 1 },
|
|
130
|
-
action: {
|
|
131
|
-
type: 2,
|
|
132
|
-
permission: { type: 0, specify_user_ids: [userId] },
|
|
133
|
-
data: pageCommand(command),
|
|
134
|
-
...(autoEnter ? { enter: true as const } : {}),
|
|
135
|
-
unsupport_tips: '当前客户端暂不支持该按钮,请发送上一页或下一页。',
|
|
136
|
-
},
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function buttonsFor(nav: PageNavData, userId: string, autoEnter: boolean): Button[] {
|
|
141
|
-
return [
|
|
142
|
-
...(nav.previousCommand ? [makeButton('previous', '上一页', nav.previousCommand, userId, autoEnter)] : []),
|
|
143
|
-
...(nav.nextCommand ? [makeButton('next', '下一页', nav.nextCommand, userId, autoEnter)] : []),
|
|
144
|
-
]
|
|
145
|
-
}
|
|
146
|
-
|
|
147
157
|
/** Returns false if this is not the matching QQ session or the adapter has no native API. */
|
|
148
158
|
export function canSendQQPage(session: Session | undefined, data: MsgMessage): session is Session {
|
|
149
159
|
if (!session || session.bot.platform !== 'qq' || session.bot.userId !== data.botId) return false
|
|
@@ -152,27 +162,24 @@ export function canSendQQPage(session: Session | undefined, data: MsgMessage): s
|
|
|
152
162
|
return !!internal?.sendMessage && !!internal.sendPrivateMessage
|
|
153
163
|
}
|
|
154
164
|
|
|
155
|
-
/** Send a native QQ Markdown page
|
|
156
|
-
export async function sendQQPage(session: Session, data: MsgMessage
|
|
165
|
+
/** Send a native QQ Markdown page with inline command links. */
|
|
166
|
+
export async function sendQQPage(session: Session, data: MsgMessage): Promise<void> {
|
|
157
167
|
const internal = (session.bot as typeof session.bot & { internal: QQInternal }).internal
|
|
158
168
|
const nav = data.pageNav!
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
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)
|
|
162
171
|
const hasInlineHelp = data.line.some(item => item.type === MessageType.line && item.content === '[指令]相关指令')
|
|
163
172
|
const request: QQRequest = {
|
|
164
173
|
// With msg_type=2, QQ requires the ordinary content field to be empty.
|
|
165
174
|
content: '',
|
|
166
175
|
msg_type: 2,
|
|
167
176
|
markdown: {
|
|
168
|
-
content: !session.isDirect && (
|
|
177
|
+
content: !session.isDirect && (hasInlineNavigation || hasInlineHelp)
|
|
169
178
|
? `${markdown}\n\n点击后如未自动发送,请点发送。`
|
|
170
179
|
: markdown,
|
|
171
180
|
},
|
|
172
|
-
...(buttons.length ? { keyboard: { content: { rows: [{ buttons }] } } } : {}),
|
|
173
181
|
}
|
|
174
|
-
// Match the QQ adapter's passive-reply sequence
|
|
175
|
-
// keyboard attempt failed and this call retries without the keyboard.
|
|
182
|
+
// Match the QQ adapter's passive-reply sequence.
|
|
176
183
|
const age = Date.now() - session.timestamp
|
|
177
184
|
if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1000 - 2000) {
|
|
178
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
|
|
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
|
package/src/shared/face/index.ts
CHANGED
|
@@ -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;
|