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 +16 -53
- package/lib/index.d.ts +1 -0
- package/lib/index.js +54 -66
- 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 +16 -4
- package/src/index.ts +17 -1
- package/src/plugin/serverHandel/handel_base.ts +1 -1
- package/src/plugin/serverHandel/qqPageMessage.ts +44 -63
- package/src/plugin/serverHandel/text.ts +1 -13
- package/src/shared/face/index.ts +1 -1
package/README.md
CHANGED
|
@@ -1,53 +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
|
-
插件会忽略机器人自己发出的消息,避免回显再次上传形成循环。
|
|
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
|
+
[](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,23 +718,28 @@ 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]) ?? []);
|
|
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 &&
|
|
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("
|
|
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("🏆") ? "" : "🏆
|
|
771
|
-
sections.push(`>
|
|
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 ?
|
|
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(`${
|
|
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 =
|
|
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 =
|
|
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("
|
|
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
|
|
839
|
+
async function sendQQPage(session, data) {
|
|
854
840
|
const internal = session.bot.internal;
|
|
855
841
|
const nav = data.pageNav;
|
|
856
|
-
const
|
|
857
|
-
const
|
|
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 && (
|
|
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
|
|
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("
|
|
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
|
|
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": [
|
|
@@ -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('
|
|
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,26 +27,39 @@ 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
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
|
-
&&
|
|
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('
|
|
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('🏆') ? '' : '🏆
|
|
95
|
-
sections.push(`>
|
|
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 ?
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
182
|
-
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> {
|
|
183
167
|
const internal = (session.bot as typeof session.bot & { internal: QQInternal }).internal
|
|
184
168
|
const nav = data.pageNav!
|
|
185
|
-
const
|
|
186
|
-
const
|
|
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 && (
|
|
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
|
|
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
|
|
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;
|