koishi-plugin-dew-bot 1.0.262 → 1.0.263
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +271 -99
- package/lib/plugin/serverHandel/imageMessageFollowup.d.ts +9 -0
- package/lib/plugin/serverHandel/qqMessageButtons.d.ts +27 -0
- package/lib/plugin/serverHandel/qqMessagePreview.d.ts +3 -0
- package/lib/plugin/serverHandel/qqPageMessage.d.ts +2 -1
- package/lib/shared/bot/analysisMsg.d.ts +5 -0
- package/lib/shared/face/index.d.ts +13 -0
- package/package.json +1 -1
- package/src/plugin/cardLayout.ts +14 -0
- package/src/plugin/serverHandel/image.ts +5 -5
- package/src/plugin/serverHandel/imageMessageFollowup.ts +23 -0
- package/src/plugin/serverHandel/qqCommandMessage.ts +4 -1
- package/src/plugin/serverHandel/qqMessageButtons.ts +38 -0
- package/src/plugin/serverHandel/qqMessagePreview.ts +19 -8
- package/src/plugin/serverHandel/qqPageMessage.ts +7 -2
- package/src/plugin/serverHandel/qqRewardMessage.ts +2 -0
- package/src/plugin/serverHandel/qqStoryMessage.ts +4 -1
- package/src/plugin/serverHandel/qqTransactionMessage.ts +4 -1
- package/src/plugin/serverHandel/text.ts +1 -1
- package/src/shared/bot/analysisMsg.ts +34 -0
- package/src/shared/bot/serviceProto.ts +72 -1
- package/src/shared/face/index.ts +15 -0
package/lib/index.js
CHANGED
|
@@ -67,7 +67,7 @@ var serviceProto;
|
|
|
67
67
|
var init_serviceProto = __esm({
|
|
68
68
|
"src/shared/bot/serviceProto.ts"() {
|
|
69
69
|
serviceProto = {
|
|
70
|
-
"version":
|
|
70
|
+
"version": 39,
|
|
71
71
|
"services": [
|
|
72
72
|
{
|
|
73
73
|
"id": 0,
|
|
@@ -295,6 +295,18 @@ var init_serviceProto = __esm({
|
|
|
295
295
|
"target": "messageFace/TitleCardData"
|
|
296
296
|
},
|
|
297
297
|
"optional": true
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"id": 17,
|
|
301
|
+
"name": "buttons",
|
|
302
|
+
"type": {
|
|
303
|
+
"type": "Array",
|
|
304
|
+
"elementType": {
|
|
305
|
+
"type": "Reference",
|
|
306
|
+
"target": "messageFace/MessageButtonRow"
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
"optional": true
|
|
298
310
|
}
|
|
299
311
|
]
|
|
300
312
|
},
|
|
@@ -1899,6 +1911,65 @@ var init_serviceProto = __esm({
|
|
|
1899
1911
|
}
|
|
1900
1912
|
]
|
|
1901
1913
|
},
|
|
1914
|
+
"messageFace/MessageButtonRow": {
|
|
1915
|
+
"type": "Interface",
|
|
1916
|
+
"properties": [
|
|
1917
|
+
{
|
|
1918
|
+
"id": 0,
|
|
1919
|
+
"name": "buttons",
|
|
1920
|
+
"type": {
|
|
1921
|
+
"type": "Array",
|
|
1922
|
+
"elementType": {
|
|
1923
|
+
"type": "Reference",
|
|
1924
|
+
"target": "messageFace/MessageButton"
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
]
|
|
1929
|
+
},
|
|
1930
|
+
"messageFace/MessageButton": {
|
|
1931
|
+
"type": "Interface",
|
|
1932
|
+
"properties": [
|
|
1933
|
+
{
|
|
1934
|
+
"id": 0,
|
|
1935
|
+
"name": "label",
|
|
1936
|
+
"type": {
|
|
1937
|
+
"type": "String"
|
|
1938
|
+
}
|
|
1939
|
+
},
|
|
1940
|
+
{
|
|
1941
|
+
"id": 1,
|
|
1942
|
+
"name": "command",
|
|
1943
|
+
"type": {
|
|
1944
|
+
"type": "String"
|
|
1945
|
+
}
|
|
1946
|
+
},
|
|
1947
|
+
{
|
|
1948
|
+
"id": 2,
|
|
1949
|
+
"name": "style",
|
|
1950
|
+
"type": {
|
|
1951
|
+
"type": "Union",
|
|
1952
|
+
"members": [
|
|
1953
|
+
{
|
|
1954
|
+
"id": 0,
|
|
1955
|
+
"type": {
|
|
1956
|
+
"type": "Literal",
|
|
1957
|
+
"literal": "primary"
|
|
1958
|
+
}
|
|
1959
|
+
},
|
|
1960
|
+
{
|
|
1961
|
+
"id": 1,
|
|
1962
|
+
"type": {
|
|
1963
|
+
"type": "Literal",
|
|
1964
|
+
"literal": "secondary"
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
]
|
|
1968
|
+
},
|
|
1969
|
+
"optional": true
|
|
1970
|
+
}
|
|
1971
|
+
]
|
|
1972
|
+
},
|
|
1902
1973
|
"PtlMessage/ReqMessage": {
|
|
1903
1974
|
"type": "Interface",
|
|
1904
1975
|
"extends": [
|
|
@@ -3151,6 +3222,18 @@ var import_node_crypto = require("node:crypto");
|
|
|
3151
3222
|
// src/plugin/cardLayout.ts
|
|
3152
3223
|
async function layoutCard(document2 = globalThis.document) {
|
|
3153
3224
|
await document2.fonts.ready;
|
|
3225
|
+
for (const name3 of document2.querySelectorAll("#character-name,#companion-name")) {
|
|
3226
|
+
const style = document2.defaultView.getComputedStyle(name3);
|
|
3227
|
+
const maximum = parseFloat(style.fontSize);
|
|
3228
|
+
const spacing = parseFloat(style.letterSpacing) || 0;
|
|
3229
|
+
name3.style.whiteSpace = "nowrap";
|
|
3230
|
+
for (let size = maximum; size >= 18; size--) {
|
|
3231
|
+
name3.style.fontSize = size + "px";
|
|
3232
|
+
name3.style.letterSpacing = spacing * size / maximum + "px";
|
|
3233
|
+
if (name3.scrollWidth <= name3.clientWidth) break;
|
|
3234
|
+
}
|
|
3235
|
+
if (name3.scrollWidth > name3.clientWidth) name3.style.whiteSpace = "normal";
|
|
3236
|
+
}
|
|
3154
3237
|
const titleBanner = document2.querySelector(".title-banner-artwork");
|
|
3155
3238
|
if (titleBanner) {
|
|
3156
3239
|
try {
|
|
@@ -3944,6 +4027,143 @@ var handel_base = class {
|
|
|
3944
4027
|
// src/plugin/serverHandel/text.ts
|
|
3945
4028
|
init_sessions();
|
|
3946
4029
|
|
|
4030
|
+
// src/shared/bot/inlineCommands.ts
|
|
4031
|
+
function inlineCommandSpans(content) {
|
|
4032
|
+
const spans = [];
|
|
4033
|
+
let depth = 0, start = 0, labelStart = 0;
|
|
4034
|
+
for (let index = 0; index < content.length; index++) {
|
|
4035
|
+
if (content[index] === "[") {
|
|
4036
|
+
if (depth === 0) {
|
|
4037
|
+
start = index > 0 && content[index - 1] === "\\" ? index - 1 : index;
|
|
4038
|
+
labelStart = index + 1;
|
|
4039
|
+
}
|
|
4040
|
+
depth++;
|
|
4041
|
+
} else if (content[index] === "]" && depth > 0 && --depth === 0) {
|
|
4042
|
+
const label = content.slice(labelStart, index).replace(/\\$/, "");
|
|
4043
|
+
if (!label.trim() || /[\[\]\r\n]/.test(label) || /^\d+$/.test(label.trim())) continue;
|
|
4044
|
+
const alias = /^\(command:([^()[\]\r\n]+)\)/.exec(content.slice(index + 1));
|
|
4045
|
+
const command = alias ? alias[1].trim() : label.split(/[++]/, 1)[0].trim().replace(/^🔄\s*/u, "");
|
|
4046
|
+
if (!command) continue;
|
|
4047
|
+
const end = index + 1 + (alias?.[0].length ?? 0);
|
|
4048
|
+
spans.push({ start, end, label, command, ...alias ? { aliased: true } : {} });
|
|
4049
|
+
if (alias) index = end - 1;
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
return spans;
|
|
4053
|
+
}
|
|
4054
|
+
__name(inlineCommandSpans, "inlineCommandSpans");
|
|
4055
|
+
function renderPlainInlineCommandAliases(content) {
|
|
4056
|
+
let result = "", offset = 0;
|
|
4057
|
+
for (const span of inlineCommandSpans(content)) {
|
|
4058
|
+
if (!span.aliased) continue;
|
|
4059
|
+
result += content.slice(offset, span.start) + (span.label.trim() === span.command ? `[${span.command}]` : `${span.label}[${span.command}]`);
|
|
4060
|
+
offset = span.end;
|
|
4061
|
+
}
|
|
4062
|
+
return result + content.slice(offset);
|
|
4063
|
+
}
|
|
4064
|
+
__name(renderPlainInlineCommandAliases, "renderPlainInlineCommandAliases");
|
|
4065
|
+
|
|
4066
|
+
// src/shared/bot/analysisMsg.ts
|
|
4067
|
+
function messageButtonFallbackLines(data) {
|
|
4068
|
+
return [...new Set((data.buttons ?? []).flatMap((row) => row.buttons.map((button) => "[" + button.command + "]" + button.label)))];
|
|
4069
|
+
}
|
|
4070
|
+
__name(messageButtonFallbackLines, "messageButtonFallbackLines");
|
|
4071
|
+
function matchesButtonFallback(content, fallback) {
|
|
4072
|
+
const bracket = fallback.indexOf("]");
|
|
4073
|
+
const prefix = fallback.slice(0, bracket + 1);
|
|
4074
|
+
const line = content.trim();
|
|
4075
|
+
return line.startsWith(prefix) && line.slice(prefix.length).trim() === fallback.slice(prefix.length).trim();
|
|
4076
|
+
}
|
|
4077
|
+
__name(matchesButtonFallback, "matchesButtonFallback");
|
|
4078
|
+
function appendMessageButtonFallbacks(content, data) {
|
|
4079
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
4080
|
+
const missing = messageButtonFallbackLines(data).filter((fallback) => !lines.some((line) => matchesButtonFallback(line, fallback)));
|
|
4081
|
+
if (!missing.length) return content;
|
|
4082
|
+
return content + (content && !content.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n";
|
|
4083
|
+
}
|
|
4084
|
+
__name(appendMessageButtonFallbacks, "appendMessageButtonFallbacks");
|
|
4085
|
+
function withoutMessageButtonFallbacks(data) {
|
|
4086
|
+
const fallbacks = messageButtonFallbackLines(data);
|
|
4087
|
+
if (!fallbacks.length) return data;
|
|
4088
|
+
return { ...data, line: data.line.map((item) => {
|
|
4089
|
+
if (item.type !== 1 /* line */ || item.codeBlock || item.reward || item.mailPart !== void 0) return item;
|
|
4090
|
+
const lines = item.content.replace(/\r\n?/g, "\n").split("\n");
|
|
4091
|
+
const kept = lines.filter((line) => !fallbacks.some((fallback) => matchesButtonFallback(line, fallback)));
|
|
4092
|
+
return kept.length === lines.length ? item : { ...item, content: kept.join("\n") };
|
|
4093
|
+
}) };
|
|
4094
|
+
}
|
|
4095
|
+
__name(withoutMessageButtonFallbacks, "withoutMessageButtonFallbacks");
|
|
4096
|
+
var AnalysisMsg = class {
|
|
4097
|
+
static {
|
|
4098
|
+
__name(this, "AnalysisMsg");
|
|
4099
|
+
}
|
|
4100
|
+
parse(data) {
|
|
4101
|
+
let msg = "";
|
|
4102
|
+
let haveTitle = false;
|
|
4103
|
+
const commands = new Map(data.pageNav?.entries?.map((entry) => [entry.lineIndex, entry.viewCommand]) ?? []);
|
|
4104
|
+
const labeledEntries = /* @__PURE__ */ new Map();
|
|
4105
|
+
for (const entry of data.pageNav?.entries ?? []) {
|
|
4106
|
+
if (entry.label === void 0) continue;
|
|
4107
|
+
const row = labeledEntries.get(entry.lineIndex) ?? [];
|
|
4108
|
+
row.push(entry);
|
|
4109
|
+
labeledEntries.set(entry.lineIndex, row);
|
|
4110
|
+
}
|
|
4111
|
+
const withCommandHint = /* @__PURE__ */ __name((content, command) => {
|
|
4112
|
+
const hint = command && !content.includes(`[${command}]`) && content.trim() !== command ? ` [${command}]` : "";
|
|
4113
|
+
return content + hint;
|
|
4114
|
+
}, "withCommandHint");
|
|
4115
|
+
for (const [lineIndex, item] of data.line.entries()) {
|
|
4116
|
+
switch (item.type) {
|
|
4117
|
+
case 0 /* title */:
|
|
4118
|
+
haveTitle = true;
|
|
4119
|
+
msg += `┄━═${item.content}═━┄
|
|
4120
|
+
`;
|
|
4121
|
+
break;
|
|
4122
|
+
case 1 /* line */:
|
|
4123
|
+
const labeledRow = labeledEntries.get(lineIndex);
|
|
4124
|
+
const content = labeledRow ? labeledRow.map((entry) => entry.label + (entry.description ? " · " + entry.description : "") + ` [${entry.viewCommand}]`).join("") : withCommandHint(item.content, commands.get(lineIndex));
|
|
4125
|
+
const plainContent = item.codeBlock || item.mailPart === "body" || labeledRow ? content : renderPlainInlineCommandAliases(content);
|
|
4126
|
+
msg += `${plainContent}
|
|
4127
|
+
`;
|
|
4128
|
+
break;
|
|
4129
|
+
case 2 /* error */:
|
|
4130
|
+
msg += `❌出错了!错误原因:${item.content},请反馈至联系开发者
|
|
4131
|
+
`;
|
|
4132
|
+
break;
|
|
4133
|
+
default:
|
|
4134
|
+
break;
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
msg = appendMessageButtonFallbacks(msg, data);
|
|
4138
|
+
if (haveTitle) {
|
|
4139
|
+
msg += "┄━════════━┄";
|
|
4140
|
+
}
|
|
4141
|
+
return msg;
|
|
4142
|
+
}
|
|
4143
|
+
};
|
|
4144
|
+
|
|
4145
|
+
// src/plugin/serverHandel/qqMessageButtons.ts
|
|
4146
|
+
function renderQQKeyboard(data) {
|
|
4147
|
+
const rows = data.buttons;
|
|
4148
|
+
if (!rows?.length) return void 0;
|
|
4149
|
+
if (rows.length > 5 || rows.some((row) => !row.buttons.length || row.buttons.length > 5)) {
|
|
4150
|
+
throw new Error("QQ 按钮最多 5 行,每行必须有 1–5 个按钮");
|
|
4151
|
+
}
|
|
4152
|
+
return { content: { rows: rows.map((row, rowIndex) => ({
|
|
4153
|
+
buttons: row.buttons.map((button, buttonIndex) => {
|
|
4154
|
+
if (!button.label.trim() || !button.command.trim() || /[\r\n\0]/.test(button.label + button.command) || button.command.length > 4e3 || button.style !== void 0 && button.style !== "primary" && button.style !== "secondary") {
|
|
4155
|
+
throw new Error("QQ 按钮文案和指令必须是非空单行文本,样式必须为 primary 或 secondary");
|
|
4156
|
+
}
|
|
4157
|
+
return {
|
|
4158
|
+
id: "action-" + rowIndex + "-" + buttonIndex,
|
|
4159
|
+
render_data: { label: button.label, visited_label: button.label, style: button.style === "primary" ? 1 : 0 },
|
|
4160
|
+
action: { type: 2, permission: { type: 2 }, data: button.command, enter: true, reply: false, unsupport_tips: "请发送:" + button.command }
|
|
4161
|
+
};
|
|
4162
|
+
})
|
|
4163
|
+
})) } };
|
|
4164
|
+
}
|
|
4165
|
+
__name(renderQQKeyboard, "renderQQKeyboard");
|
|
4166
|
+
|
|
3947
4167
|
// src/plugin/serverHandel/qqMarkdownHelpers.ts
|
|
3948
4168
|
function escapeMarkdown(value) {
|
|
3949
4169
|
return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, "\\$&");
|
|
@@ -4066,6 +4286,7 @@ function renderQQMailMarkdown(data) {
|
|
|
4066
4286
|
}
|
|
4067
4287
|
__name(renderQQMailMarkdown, "renderQQMailMarkdown");
|
|
4068
4288
|
function renderQQRewardMarkdown(data, showGreeting) {
|
|
4289
|
+
data = withoutMessageButtonFallbacks(data);
|
|
4069
4290
|
if (data.mail) return appendMissingBattleLogLinks(renderQQMailMarkdown(data), data);
|
|
4070
4291
|
const rewards = data.line.filter((item) => item.type === 1 /* line */ && item.reward).map((item) => renderRewardLine(item.content)).filter(Boolean);
|
|
4071
4292
|
if (rewards.length && !data.mail) return appendMissingBattleLogLinks("🎁获得奖励\n\n" + rewards.join(" \n"), data);
|
|
@@ -4095,42 +4316,6 @@ function renderQQRewardMarkdown(data, showGreeting) {
|
|
|
4095
4316
|
}
|
|
4096
4317
|
__name(renderQQRewardMarkdown, "renderQQRewardMarkdown");
|
|
4097
4318
|
|
|
4098
|
-
// src/shared/bot/inlineCommands.ts
|
|
4099
|
-
function inlineCommandSpans(content) {
|
|
4100
|
-
const spans = [];
|
|
4101
|
-
let depth = 0, start = 0, labelStart = 0;
|
|
4102
|
-
for (let index = 0; index < content.length; index++) {
|
|
4103
|
-
if (content[index] === "[") {
|
|
4104
|
-
if (depth === 0) {
|
|
4105
|
-
start = index > 0 && content[index - 1] === "\\" ? index - 1 : index;
|
|
4106
|
-
labelStart = index + 1;
|
|
4107
|
-
}
|
|
4108
|
-
depth++;
|
|
4109
|
-
} else if (content[index] === "]" && depth > 0 && --depth === 0) {
|
|
4110
|
-
const label = content.slice(labelStart, index).replace(/\\$/, "");
|
|
4111
|
-
if (!label.trim() || /[\[\]\r\n]/.test(label) || /^\d+$/.test(label.trim())) continue;
|
|
4112
|
-
const alias = /^\(command:([^()[\]\r\n]+)\)/.exec(content.slice(index + 1));
|
|
4113
|
-
const command = alias ? alias[1].trim() : label.split(/[++]/, 1)[0].trim().replace(/^🔄\s*/u, "");
|
|
4114
|
-
if (!command) continue;
|
|
4115
|
-
const end = index + 1 + (alias?.[0].length ?? 0);
|
|
4116
|
-
spans.push({ start, end, label, command, ...alias ? { aliased: true } : {} });
|
|
4117
|
-
if (alias) index = end - 1;
|
|
4118
|
-
}
|
|
4119
|
-
}
|
|
4120
|
-
return spans;
|
|
4121
|
-
}
|
|
4122
|
-
__name(inlineCommandSpans, "inlineCommandSpans");
|
|
4123
|
-
function renderPlainInlineCommandAliases(content) {
|
|
4124
|
-
let result = "", offset = 0;
|
|
4125
|
-
for (const span of inlineCommandSpans(content)) {
|
|
4126
|
-
if (!span.aliased) continue;
|
|
4127
|
-
result += content.slice(offset, span.start) + (span.label.trim() === span.command ? `[${span.command}]` : `${span.label}[${span.command}]`);
|
|
4128
|
-
offset = span.end;
|
|
4129
|
-
}
|
|
4130
|
-
return result + content.slice(offset);
|
|
4131
|
-
}
|
|
4132
|
-
__name(renderPlainInlineCommandAliases, "renderPlainInlineCommandAliases");
|
|
4133
|
-
|
|
4134
4319
|
// src/plugin/serverHandel/qqPageMessage.ts
|
|
4135
4320
|
function compactListLabel(value) {
|
|
4136
4321
|
const label = compactDisplayText(value);
|
|
@@ -4291,6 +4476,7 @@ function renderQQInventoryMarkdown(data, showGreeting) {
|
|
|
4291
4476
|
}
|
|
4292
4477
|
__name(renderQQInventoryMarkdown, "renderQQInventoryMarkdown");
|
|
4293
4478
|
function renderQQPageMarkdown(data, showGreeting) {
|
|
4479
|
+
data = withoutMessageButtonFallbacks(data);
|
|
4294
4480
|
const hasEntryLabels = data.pageNav?.entries?.some((entry) => entry.label !== void 0) ?? false;
|
|
4295
4481
|
if (data.task && !hasEntryLabels) return renderQQTaskMarkdown(data);
|
|
4296
4482
|
if (!hasEntryLabels && data.pageNav && data.line.some((item) => item.type === 0 /* title */ && /^(?:💼)?我的背包$/u.test(compactDisplayText(item.content))))
|
|
@@ -4416,7 +4602,7 @@ function canSendQQPage(session, data) {
|
|
|
4416
4602
|
return !!internal?.sendMessage && !!internal.sendPrivateMessage;
|
|
4417
4603
|
}
|
|
4418
4604
|
__name(canSendQQPage, "canSendQQPage");
|
|
4419
|
-
async function sendQQMarkdown(session, markdown) {
|
|
4605
|
+
async function sendQQMarkdown(session, markdown, keyboard) {
|
|
4420
4606
|
const internal = session.bot.internal;
|
|
4421
4607
|
const request = {
|
|
4422
4608
|
// With msg_type=2, QQ requires the ordinary content field to be empty.
|
|
@@ -4424,6 +4610,7 @@ async function sendQQMarkdown(session, markdown) {
|
|
|
4424
4610
|
msg_type: 2,
|
|
4425
4611
|
markdown: { content: markdown }
|
|
4426
4612
|
};
|
|
4613
|
+
if (keyboard) request.keyboard = keyboard;
|
|
4427
4614
|
const age = Date.now() - session.timestamp;
|
|
4428
4615
|
if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1e3 - 2e3) {
|
|
4429
4616
|
const sequenced = session;
|
|
@@ -4437,57 +4624,9 @@ async function sendQQMarkdown(session, markdown) {
|
|
|
4437
4624
|
}
|
|
4438
4625
|
__name(sendQQMarkdown, "sendQQMarkdown");
|
|
4439
4626
|
|
|
4440
|
-
// src/shared/bot/analysisMsg.ts
|
|
4441
|
-
var AnalysisMsg = class {
|
|
4442
|
-
static {
|
|
4443
|
-
__name(this, "AnalysisMsg");
|
|
4444
|
-
}
|
|
4445
|
-
parse(data) {
|
|
4446
|
-
let msg = "";
|
|
4447
|
-
let haveTitle = false;
|
|
4448
|
-
const commands = new Map(data.pageNav?.entries?.map((entry) => [entry.lineIndex, entry.viewCommand]) ?? []);
|
|
4449
|
-
const labeledEntries = /* @__PURE__ */ new Map();
|
|
4450
|
-
for (const entry of data.pageNav?.entries ?? []) {
|
|
4451
|
-
if (entry.label === void 0) continue;
|
|
4452
|
-
const row = labeledEntries.get(entry.lineIndex) ?? [];
|
|
4453
|
-
row.push(entry);
|
|
4454
|
-
labeledEntries.set(entry.lineIndex, row);
|
|
4455
|
-
}
|
|
4456
|
-
const withCommandHint = /* @__PURE__ */ __name((content, command) => {
|
|
4457
|
-
const hint = command && !content.includes(`[${command}]`) && content.trim() !== command ? ` [${command}]` : "";
|
|
4458
|
-
return content + hint;
|
|
4459
|
-
}, "withCommandHint");
|
|
4460
|
-
for (const [lineIndex, item] of data.line.entries()) {
|
|
4461
|
-
switch (item.type) {
|
|
4462
|
-
case 0 /* title */:
|
|
4463
|
-
haveTitle = true;
|
|
4464
|
-
msg += `┄━═${item.content}═━┄
|
|
4465
|
-
`;
|
|
4466
|
-
break;
|
|
4467
|
-
case 1 /* line */:
|
|
4468
|
-
const labeledRow = labeledEntries.get(lineIndex);
|
|
4469
|
-
const content = labeledRow ? labeledRow.map((entry) => entry.label + (entry.description ? " · " + entry.description : "") + ` [${entry.viewCommand}]`).join("") : withCommandHint(item.content, commands.get(lineIndex));
|
|
4470
|
-
const plainContent = item.codeBlock || item.mailPart === "body" || labeledRow ? content : renderPlainInlineCommandAliases(content);
|
|
4471
|
-
msg += `${plainContent}
|
|
4472
|
-
`;
|
|
4473
|
-
break;
|
|
4474
|
-
case 2 /* error */:
|
|
4475
|
-
msg += `❌出错了!错误原因:${item.content},请反馈至联系开发者
|
|
4476
|
-
`;
|
|
4477
|
-
break;
|
|
4478
|
-
default:
|
|
4479
|
-
break;
|
|
4480
|
-
}
|
|
4481
|
-
}
|
|
4482
|
-
if (haveTitle) {
|
|
4483
|
-
msg += "┄━════════━┄";
|
|
4484
|
-
}
|
|
4485
|
-
return msg;
|
|
4486
|
-
}
|
|
4487
|
-
};
|
|
4488
|
-
|
|
4489
4627
|
// src/plugin/serverHandel/qqCommandMessage.ts
|
|
4490
4628
|
function renderQQCommandMarkdown(data, showGreeting) {
|
|
4629
|
+
data = withoutMessageButtonFallbacks(data);
|
|
4491
4630
|
if (data.notification) {
|
|
4492
4631
|
const onlyLine = data.line.length === 1 ? data.line[0] : void 0;
|
|
4493
4632
|
if (onlyLine?.type === 1 /* line */ && /^🔔系统提示:[^\r\n]+$/.test(onlyLine.content)) {
|
|
@@ -4551,6 +4690,7 @@ __name(renderQQCommandMarkdown, "renderQQCommandMarkdown");
|
|
|
4551
4690
|
|
|
4552
4691
|
// src/plugin/serverHandel/qqTransactionMessage.ts
|
|
4553
4692
|
function renderQQTransactionMarkdown(data, showGreeting) {
|
|
4693
|
+
data = withoutMessageButtonFallbacks(data);
|
|
4554
4694
|
const rewardMessage = isRewardMessage(data);
|
|
4555
4695
|
const shortageMessage = data.transaction === true && data.line.some((item) => item.type === 0 /* title */ && item.content === "❌材料不足");
|
|
4556
4696
|
const sections = [];
|
|
@@ -4644,6 +4784,7 @@ function fencedStory(content) {
|
|
|
4644
4784
|
}
|
|
4645
4785
|
__name(fencedStory, "fencedStory");
|
|
4646
4786
|
function renderQQStoryMarkdown(data, showGreeting) {
|
|
4787
|
+
data = withoutMessageButtonFallbacks(data);
|
|
4647
4788
|
if (data.reward === true && data.line.some((item) => item.type === 1 /* line */ && item.reward === true && item.content.trim())) {
|
|
4648
4789
|
const stories = data.line.filter((item) => item.type === 1 /* line */ && item.codeBlock === true).map((item) => fencedStory(item.content));
|
|
4649
4790
|
return [...stories, renderQQRewardMarkdown(data, false)].join("\n\n");
|
|
@@ -4702,15 +4843,29 @@ __name(renderQQStoryMarkdown, "renderQQStoryMarkdown");
|
|
|
4702
4843
|
function renderQQMessagePreview(data, showGreeting = false) {
|
|
4703
4844
|
const plain = new AnalysisMsg().parse(data);
|
|
4704
4845
|
const text = data.notification ? plain.trimEnd() : plain;
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4846
|
+
let keyboard;
|
|
4847
|
+
try {
|
|
4848
|
+
keyboard = renderQQKeyboard(data);
|
|
4849
|
+
} catch {
|
|
4850
|
+
return { kind: "text", text };
|
|
4851
|
+
}
|
|
4852
|
+
data = withoutMessageButtonFallbacks(data);
|
|
4853
|
+
const native = /* @__PURE__ */ __name((kind, markdown) => ({
|
|
4854
|
+
kind,
|
|
4855
|
+
markdown,
|
|
4856
|
+
text,
|
|
4857
|
+
...keyboard ? { keyboard } : {}
|
|
4858
|
+
}), "native");
|
|
4859
|
+
if (data.notification) return native("command", renderQQCommandMarkdown(data, false));
|
|
4860
|
+
if (hasStoryCodeBlock(data)) return native("story", renderQQStoryMarkdown(data, showGreeting));
|
|
4861
|
+
if (data.commandHelp) return native("command", renderQQCommandMarkdown(data, showGreeting));
|
|
4862
|
+
if (data.transaction) return native("transaction", renderQQTransactionMarkdown(data, showGreeting));
|
|
4863
|
+
if (data.pageNav) return native("page", renderQQPageMarkdown(data, showGreeting));
|
|
4864
|
+
if (isRewardMessage(data)) return native("reward", renderQQRewardMarkdown(data, showGreeting));
|
|
4711
4865
|
if (battleLogMessageLinks(data).length || data.line.some((item) => (item.type === 1 /* line */ || item.type === 2 /* error */) && hasInlineCommands(item.content))) {
|
|
4712
|
-
return
|
|
4866
|
+
return native("command", renderQQCommandMarkdown(data, showGreeting));
|
|
4713
4867
|
}
|
|
4868
|
+
if (keyboard) return native("command", renderQQCommandMarkdown(data, showGreeting));
|
|
4714
4869
|
return { kind: "text", text };
|
|
4715
4870
|
}
|
|
4716
4871
|
__name(renderQQMessagePreview, "renderQQMessagePreview");
|
|
@@ -4727,7 +4882,7 @@ var handelr_text = class extends handel_base {
|
|
|
4727
4882
|
if (preview.markdown !== void 0 && canSendQQPage(session, data)) {
|
|
4728
4883
|
const sendStartedAt = this.timing ? performance.now() : 0;
|
|
4729
4884
|
try {
|
|
4730
|
-
await sendQQMarkdown(session, preview.markdown);
|
|
4885
|
+
await sendQQMarkdown(session, preview.markdown, preview.keyboard);
|
|
4731
4886
|
if (this.timing) {
|
|
4732
4887
|
this.timing.delivery = "qq-markdown";
|
|
4733
4888
|
this.timing.sendMs = performance.now() - sendStartedAt;
|
|
@@ -4772,6 +4927,23 @@ function titleCardFollowup(data) {
|
|
|
4772
4927
|
}
|
|
4773
4928
|
__name(titleCardFollowup, "titleCardFollowup");
|
|
4774
4929
|
|
|
4930
|
+
// src/plugin/serverHandel/imageMessageFollowup.ts
|
|
4931
|
+
function renderQQImageFollowup(data) {
|
|
4932
|
+
const title = titleCardFollowup(data);
|
|
4933
|
+
const actionLines = messageButtonFallbackLines(data);
|
|
4934
|
+
if (!title && !actionLines.length) return void 0;
|
|
4935
|
+
const text = appendMessageButtonFallbacks(title?.text ?? "", data).trimEnd();
|
|
4936
|
+
let keyboard;
|
|
4937
|
+
try {
|
|
4938
|
+
keyboard = renderQQKeyboard(data);
|
|
4939
|
+
} catch {
|
|
4940
|
+
return { text };
|
|
4941
|
+
}
|
|
4942
|
+
const markdown = titleCardFollowup(withoutMessageButtonFallbacks(data))?.markdown ?? " ";
|
|
4943
|
+
return { text, markdown, ...keyboard ? { keyboard } : {} };
|
|
4944
|
+
}
|
|
4945
|
+
__name(renderQQImageFollowup, "renderQQImageFollowup");
|
|
4946
|
+
|
|
4775
4947
|
// src/plugin/serverHandel/image.ts
|
|
4776
4948
|
var ImageHandler = class extends handel_base {
|
|
4777
4949
|
static {
|
|
@@ -4802,16 +4974,16 @@ var ImageHandler = class extends handel_base {
|
|
|
4802
4974
|
}
|
|
4803
4975
|
const sendStartedAt = this.timing ? performance.now() : 0;
|
|
4804
4976
|
await this.send(data, content, "image");
|
|
4805
|
-
const followup =
|
|
4977
|
+
const followup = renderQQImageFollowup(data);
|
|
4806
4978
|
if (!followup) return;
|
|
4807
4979
|
const session = sessions_default.get(sessionKey(data.botId, data.userId, data.channelId));
|
|
4808
4980
|
let sentMarkdown = false;
|
|
4809
|
-
if (canSendQQPage(session, data)) {
|
|
4981
|
+
if (followup.markdown !== void 0 && canSendQQPage(session, data)) {
|
|
4810
4982
|
try {
|
|
4811
|
-
await sendQQMarkdown(session, followup.markdown);
|
|
4983
|
+
await sendQQMarkdown(session, followup.markdown, followup.keyboard);
|
|
4812
4984
|
sentMarkdown = true;
|
|
4813
4985
|
} catch (error) {
|
|
4814
|
-
this.ctx.logger.warn("
|
|
4986
|
+
this.ctx.logger.warn("图片操作 Markdown 发送失败,改用普通文字", error instanceof Error ? error.message : error);
|
|
4815
4987
|
}
|
|
4816
4988
|
}
|
|
4817
4989
|
if (!sentMarkdown) await this.send({ ...data, notification: true }, followup.text);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IMessage } from '../../shared/face';
|
|
2
|
+
import { type QQKeyboard } from './qqMessageButtons';
|
|
3
|
+
export interface QQImageFollowup {
|
|
4
|
+
text: string;
|
|
5
|
+
markdown?: string;
|
|
6
|
+
keyboard?: QQKeyboard;
|
|
7
|
+
}
|
|
8
|
+
/** Images are sent first; actions stay outside the image and retain a plain fallback. */
|
|
9
|
+
export declare function renderQQImageFollowup(data: IMessage): QQImageFollowup | undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IMessage } from '../../shared/face';
|
|
2
|
+
export interface QQKeyboard {
|
|
3
|
+
content: {
|
|
4
|
+
rows: Array<{
|
|
5
|
+
buttons: Array<{
|
|
6
|
+
id: string;
|
|
7
|
+
render_data: {
|
|
8
|
+
label: string;
|
|
9
|
+
visited_label: string;
|
|
10
|
+
style: 0 | 1;
|
|
11
|
+
};
|
|
12
|
+
action: {
|
|
13
|
+
type: 2;
|
|
14
|
+
permission: {
|
|
15
|
+
type: 2;
|
|
16
|
+
};
|
|
17
|
+
data: string;
|
|
18
|
+
enter: true;
|
|
19
|
+
reply: false;
|
|
20
|
+
unsupport_tips: string;
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Explicit actions use QQ's native keyboard; ordinary command links stay inline. */
|
|
27
|
+
export declare function renderQQKeyboard(data: Pick<IMessage, 'buttons'>): QQKeyboard | undefined;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { type IMessage } from '../../shared/face';
|
|
2
|
+
import { type QQKeyboard } from './qqMessageButtons';
|
|
2
3
|
export type QQMessageKind = 'text' | 'story' | 'command' | 'transaction' | 'page' | 'reward';
|
|
3
4
|
export interface QQMessagePreview {
|
|
4
5
|
kind: QQMessageKind;
|
|
5
6
|
/** Exact native QQ Markdown payload; absent when the bot sends plain text. */
|
|
6
7
|
markdown?: string;
|
|
8
|
+
/** Native buttons accompany the same Markdown in bot delivery and editor preview. */
|
|
9
|
+
keyboard?: QQKeyboard;
|
|
7
10
|
/** Plain fallback before the transport adds its QQ group greeting. */
|
|
8
11
|
text: string;
|
|
9
12
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Session } from 'koishi';
|
|
2
2
|
import { type IMessage, type PageNavData } from '../../shared/face';
|
|
3
3
|
import type { MsgMessage } from '../../shared/bot/MsgMessage';
|
|
4
|
+
import { type QQKeyboard } from './qqMessageButtons';
|
|
4
5
|
export { escapeMarkdown, renderQQTitle } from './qqMarkdownHelpers';
|
|
5
6
|
export declare function inlineCommandUrl(command: string): string;
|
|
6
7
|
export declare function hasInlineCommands(content: string): boolean;
|
|
@@ -13,4 +14,4 @@ export declare function canSendQQPage(session: Session | undefined, data: MsgMes
|
|
|
13
14
|
/** Send a native QQ Markdown page with inline command links. */
|
|
14
15
|
export declare function sendQQPage(session: Session, data: MsgMessage): Promise<void>;
|
|
15
16
|
/** Reuse the adapter's native reply API for structured Markdown messages. */
|
|
16
|
-
export declare function sendQQMarkdown(session: Session, markdown: string): Promise<void>;
|
|
17
|
+
export declare function sendQQMarkdown(session: Session, markdown: string, keyboard?: QQKeyboard): Promise<void>;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { IMessage } from '../face';
|
|
2
|
+
/** Platform-neutral button hints also work when Markdown or keyboards are unavailable. */
|
|
3
|
+
export declare function messageButtonFallbackLines(data: Pick<IMessage, 'buttons'>): string[];
|
|
4
|
+
export declare function appendMessageButtonFallbacks(content: string, data: Pick<IMessage, 'buttons'>): string;
|
|
5
|
+
/** Remove only complete action hints; keep source line indexes used by page metadata. */
|
|
6
|
+
export declare function withoutMessageButtonFallbacks<T extends IMessage>(data: T): T;
|
|
2
7
|
/** 把服务器下发的结构化行拼成纯文本;第一阶段只处理 title / line / error **/
|
|
3
8
|
export declare class AnalysisMsg {
|
|
4
9
|
parse(data: IMessage): string;
|
|
@@ -267,6 +267,17 @@ export interface PageNavData {
|
|
|
267
267
|
command: string;
|
|
268
268
|
}>;
|
|
269
269
|
}
|
|
270
|
+
/** 平台无关的消息按钮;点击后发送普通指令,业务仍负责权限与状态校验。 */
|
|
271
|
+
export interface MessageButton {
|
|
272
|
+
label: string;
|
|
273
|
+
command: string;
|
|
274
|
+
/** 主操作蓝色线框,次操作灰色线框;缺省为次操作。 */
|
|
275
|
+
style?: 'primary' | 'secondary';
|
|
276
|
+
}
|
|
277
|
+
/** 同一行的按钮;QQ 原生键盘最多五行,每行最多五个。 */
|
|
278
|
+
export interface MessageButtonRow {
|
|
279
|
+
buttons: MessageButton[];
|
|
280
|
+
}
|
|
270
281
|
/** 下发给客户端的消息主体 **/
|
|
271
282
|
export interface IMessage {
|
|
272
283
|
handleType: MessageHandleType;
|
|
@@ -293,4 +304,6 @@ export interface IMessage {
|
|
|
293
304
|
line: IMessageItem[];
|
|
294
305
|
companionCard?: CompanionCardData;
|
|
295
306
|
titleCard?: TitleCardData;
|
|
307
|
+
/** 通用指令按钮;不支持原生按钮的平台保留文字指令。 */
|
|
308
|
+
buttons?: MessageButtonRow[];
|
|
296
309
|
}
|
package/package.json
CHANGED
package/src/plugin/cardLayout.ts
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
export async function layoutCard(document: Document = globalThis.document) {
|
|
3
3
|
|
|
4
4
|
await document.fonts.ready
|
|
5
|
+
// Measure names in one line before the flow-layout cards return below.
|
|
6
|
+
for (const name of document.querySelectorAll<HTMLElement>('#character-name,#companion-name')) {
|
|
7
|
+
const style = document.defaultView!.getComputedStyle(name)
|
|
8
|
+
const maximum = parseFloat(style.fontSize)
|
|
9
|
+
const spacing = parseFloat(style.letterSpacing) || 0
|
|
10
|
+
name.style.whiteSpace = 'nowrap'
|
|
11
|
+
for (let size = maximum; size >= 18; size--) {
|
|
12
|
+
name.style.fontSize = size + 'px'
|
|
13
|
+
name.style.letterSpacing = spacing * size / maximum + 'px'
|
|
14
|
+
if (name.scrollWidth <= name.clientWidth) break
|
|
15
|
+
}
|
|
16
|
+
// Preserve the complete name when even the readable minimum cannot fit.
|
|
17
|
+
if (name.scrollWidth > name.clientWidth) name.style.whiteSpace = 'normal'
|
|
18
|
+
}
|
|
5
19
|
const titleBanner = document.querySelector<HTMLImageElement>('.title-banner-artwork')
|
|
6
20
|
if (titleBanner) {
|
|
7
21
|
try { await titleBanner.decode() } catch {
|
|
@@ -5,7 +5,7 @@ import handel_base from './handel_base'
|
|
|
5
5
|
import TextHandler from './text'
|
|
6
6
|
import sessions, { sessionKey } from '../sessions'
|
|
7
7
|
import { canSendQQPage, sendQQMarkdown } from './qqPageMessage'
|
|
8
|
-
import {
|
|
8
|
+
import { renderQQImageFollowup } from './imageMessageFollowup'
|
|
9
9
|
export default class ImageHandler extends handel_base {
|
|
10
10
|
async start(data: MsgMessage) {
|
|
11
11
|
let content = ''
|
|
@@ -32,16 +32,16 @@ export default class ImageHandler extends handel_base {
|
|
|
32
32
|
}
|
|
33
33
|
const sendStartedAt = this.timing ? performance.now() : 0
|
|
34
34
|
await this.send(data, content, 'image')
|
|
35
|
-
const followup =
|
|
35
|
+
const followup = renderQQImageFollowup(data)
|
|
36
36
|
if (!followup) return
|
|
37
37
|
const session = sessions.get(sessionKey(data.botId, data.userId, data.channelId))
|
|
38
38
|
let sentMarkdown = false
|
|
39
|
-
if (canSendQQPage(session, data)) {
|
|
39
|
+
if (followup.markdown !== undefined && canSendQQPage(session, data)) {
|
|
40
40
|
try {
|
|
41
|
-
await sendQQMarkdown(session, followup.markdown)
|
|
41
|
+
await sendQQMarkdown(session, followup.markdown, followup.keyboard)
|
|
42
42
|
sentMarkdown = true
|
|
43
43
|
} catch (error) {
|
|
44
|
-
this.ctx.logger.warn('
|
|
44
|
+
this.ctx.logger.warn('图片操作 Markdown 发送失败,改用普通文字', error instanceof Error ? error.message : error)
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
if (!sentMarkdown) await this.send({ ...data, notification: true }, followup.text)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IMessage } from '../../shared/face'
|
|
2
|
+
import { appendMessageButtonFallbacks, messageButtonFallbackLines, withoutMessageButtonFallbacks } from '../../shared/bot/analysisMsg'
|
|
3
|
+
import { titleCardFollowup } from './titleCardMessage'
|
|
4
|
+
import { renderQQKeyboard, type QQKeyboard } from './qqMessageButtons'
|
|
5
|
+
|
|
6
|
+
export interface QQImageFollowup {
|
|
7
|
+
text: string
|
|
8
|
+
markdown?: string
|
|
9
|
+
keyboard?: QQKeyboard
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Images are sent first; actions stay outside the image and retain a plain fallback. */
|
|
13
|
+
export function renderQQImageFollowup(data: IMessage): QQImageFollowup | undefined {
|
|
14
|
+
const title = titleCardFollowup(data)
|
|
15
|
+
const actionLines = messageButtonFallbackLines(data)
|
|
16
|
+
if (!title && !actionLines.length) return undefined
|
|
17
|
+
const text = appendMessageButtonFallbacks(title?.text ?? '', data).trimEnd()
|
|
18
|
+
let keyboard: QQKeyboard | undefined
|
|
19
|
+
try { keyboard = renderQQKeyboard(data) }
|
|
20
|
+
catch { return { text } }
|
|
21
|
+
const markdown = titleCardFollowup(withoutMessageButtonFallbacks(data))?.markdown ?? ' '
|
|
22
|
+
return { text, markdown, ...(keyboard ? { keyboard } : {}) }
|
|
23
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withoutMessageButtonFallbacks } from '../../shared/bot/analysisMsg'
|
|
2
|
+
import { renderQQKeyboard } from './qqMessageButtons'
|
|
1
3
|
import type { Session } from 'koishi'
|
|
2
4
|
import { MessageType, type IMessage } from '../../shared/face'
|
|
3
5
|
import type { MsgMessage } from '../../shared/bot/MsgMessage'
|
|
@@ -6,6 +8,7 @@ import { isRewardMessage, renderRewardLine, renderRewardTitle } from './qqReward
|
|
|
6
8
|
|
|
7
9
|
/** Render command help and ordinary prose with clickable bracketed commands. */
|
|
8
10
|
export function renderQQCommandMarkdown(data: IMessage, showGreeting: boolean): string {
|
|
11
|
+
data = withoutMessageButtonFallbacks(data)
|
|
9
12
|
if (data.notification) {
|
|
10
13
|
const onlyLine = data.line.length === 1 ? data.line[0] : undefined
|
|
11
14
|
if (onlyLine?.type === MessageType.line && /^🔔系统提示:[^\r\n]+$/.test(onlyLine.content)) {
|
|
@@ -72,5 +75,5 @@ export function renderQQCommandMarkdown(data: IMessage, showGreeting: boolean):
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
export async function sendQQCommand(session: Session, data: MsgMessage): Promise<void> {
|
|
75
|
-
await sendQQMarkdown(session, renderQQCommandMarkdown(data, session.event?.channel?.type === 0))
|
|
78
|
+
await sendQQMarkdown(session, renderQQCommandMarkdown(data, session.event?.channel?.type === 0), renderQQKeyboard(data))
|
|
76
79
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { IMessage } from '../../shared/face'
|
|
2
|
+
|
|
3
|
+
export interface QQKeyboard {
|
|
4
|
+
content: { rows: Array<{ buttons: Array<{
|
|
5
|
+
id: string
|
|
6
|
+
render_data: { label: string; visited_label: string; style: 0 | 1 }
|
|
7
|
+
action: {
|
|
8
|
+
type: 2
|
|
9
|
+
permission: { type: 2 }
|
|
10
|
+
data: string
|
|
11
|
+
enter: true
|
|
12
|
+
reply: false
|
|
13
|
+
unsupport_tips: string
|
|
14
|
+
}
|
|
15
|
+
}> }> }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Explicit actions use QQ's native keyboard; ordinary command links stay inline. */
|
|
19
|
+
export function renderQQKeyboard(data: Pick<IMessage, 'buttons'>): QQKeyboard | undefined {
|
|
20
|
+
const rows = data.buttons
|
|
21
|
+
if (!rows?.length) return undefined
|
|
22
|
+
if (rows.length > 5 || rows.some(row => !row.buttons.length || row.buttons.length > 5)) {
|
|
23
|
+
throw new Error('QQ 按钮最多 5 行,每行必须有 1–5 个按钮')
|
|
24
|
+
}
|
|
25
|
+
return { content: { rows: rows.map((row, rowIndex) => ({
|
|
26
|
+
buttons: row.buttons.map((button, buttonIndex) => {
|
|
27
|
+
if (!button.label.trim() || !button.command.trim() || /[\r\n\0]/.test(button.label + button.command)
|
|
28
|
+
|| button.command.length > 4000 || (button.style !== undefined && button.style !== 'primary' && button.style !== 'secondary')) {
|
|
29
|
+
throw new Error('QQ 按钮文案和指令必须是非空单行文本,样式必须为 primary 或 secondary')
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
id: 'action-' + rowIndex + '-' + buttonIndex,
|
|
33
|
+
render_data: { label: button.label, visited_label: button.label, style: button.style === 'primary' ? 1 : 0 },
|
|
34
|
+
action: { type: 2, permission: { type: 2 }, data: button.command, enter: true, reply: false, unsupport_tips: '请发送:' + button.command },
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
})) } }
|
|
38
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type IMessage, MessageType } from '../../shared/face'
|
|
2
|
-
import { AnalysisMsg } from '../../shared/bot/analysisMsg'
|
|
2
|
+
import { AnalysisMsg, withoutMessageButtonFallbacks } from '../../shared/bot/analysisMsg'
|
|
3
|
+
import { renderQQKeyboard, type QQKeyboard } from './qqMessageButtons'
|
|
3
4
|
import { hasInlineCommands, renderQQPageMarkdown } from './qqPageMessage'
|
|
4
5
|
import { renderQQCommandMarkdown } from './qqCommandMessage'
|
|
5
6
|
import { renderQQTransactionMarkdown } from './qqTransactionMessage'
|
|
@@ -13,6 +14,8 @@ export interface QQMessagePreview {
|
|
|
13
14
|
kind: QQMessageKind
|
|
14
15
|
/** Exact native QQ Markdown payload; absent when the bot sends plain text. */
|
|
15
16
|
markdown?: string
|
|
17
|
+
/** Native buttons accompany the same Markdown in bot delivery and editor preview. */
|
|
18
|
+
keyboard?: QQKeyboard
|
|
16
19
|
/** Plain fallback before the transport adds its QQ group greeting. */
|
|
17
20
|
text: string
|
|
18
21
|
}
|
|
@@ -21,14 +24,22 @@ export interface QQMessagePreview {
|
|
|
21
24
|
export function renderQQMessagePreview(data: IMessage, showGreeting = false): QQMessagePreview {
|
|
22
25
|
const plain = new AnalysisMsg().parse(data)
|
|
23
26
|
const text = data.notification ? plain.trimEnd() : plain
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
let keyboard: QQKeyboard | undefined
|
|
28
|
+
try { keyboard = renderQQKeyboard(data) }
|
|
29
|
+
catch { return { kind: 'text', text } }
|
|
30
|
+
data = withoutMessageButtonFallbacks(data)
|
|
31
|
+
const native = (kind: QQMessageKind, markdown: string): QQMessagePreview => ({
|
|
32
|
+
kind, markdown, text, ...(keyboard ? { keyboard } : {}),
|
|
33
|
+
})
|
|
34
|
+
if (data.notification) return native('command', renderQQCommandMarkdown(data, false))
|
|
35
|
+
if (hasStoryCodeBlock(data)) return native('story', renderQQStoryMarkdown(data, showGreeting))
|
|
36
|
+
if (data.commandHelp) return native('command', renderQQCommandMarkdown(data, showGreeting))
|
|
37
|
+
if (data.transaction) return native('transaction', renderQQTransactionMarkdown(data, showGreeting))
|
|
38
|
+
if (data.pageNav) return native('page', renderQQPageMarkdown(data, showGreeting))
|
|
39
|
+
if (isRewardMessage(data)) return native('reward', renderQQRewardMarkdown(data, showGreeting))
|
|
30
40
|
if (battleLogMessageLinks(data).length || data.line.some(item => (item.type === MessageType.line || item.type === MessageType.error) && hasInlineCommands(item.content))) {
|
|
31
|
-
return
|
|
41
|
+
return native('command', renderQQCommandMarkdown(data, showGreeting))
|
|
32
42
|
}
|
|
43
|
+
if (keyboard) return native('command', renderQQCommandMarkdown(data, showGreeting))
|
|
33
44
|
return { kind: 'text', text }
|
|
34
45
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Session } from 'koishi'
|
|
2
2
|
import { MessageType, type IMessage, type PageNavData } from '../../shared/face'
|
|
3
3
|
import type { MsgMessage } from '../../shared/bot/MsgMessage'
|
|
4
|
+
import { withoutMessageButtonFallbacks } from '../../shared/bot/analysisMsg'
|
|
5
|
+
import { renderQQKeyboard, type QQKeyboard } from './qqMessageButtons'
|
|
4
6
|
import { compactDisplayText, escapeMarkdown, renderQQTitle } from './qqMarkdownHelpers'
|
|
5
7
|
import { isRewardMessage, renderRewardLine, renderRewardTitle } from './qqRewardMessage'
|
|
6
8
|
import { artworkIdentity } from '../skillArtwork'
|
|
@@ -13,6 +15,7 @@ type QQRequest = {
|
|
|
13
15
|
content: string
|
|
14
16
|
msg_type: 2
|
|
15
17
|
markdown: { content: string }
|
|
18
|
+
keyboard?: QQKeyboard
|
|
16
19
|
msg_id?: string
|
|
17
20
|
msg_seq?: number
|
|
18
21
|
}
|
|
@@ -216,6 +219,7 @@ function renderQQInventoryMarkdown(data: IMessage, showGreeting: boolean): strin
|
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
export function renderQQPageMarkdown(data: IMessage, showGreeting: boolean): string {
|
|
222
|
+
data = withoutMessageButtonFallbacks(data)
|
|
219
223
|
const hasEntryLabels = data.pageNav?.entries?.some(entry => entry.label !== undefined) ?? false
|
|
220
224
|
if (data.task && !hasEntryLabels) return renderQQTaskMarkdown(data)
|
|
221
225
|
if (!hasEntryLabels && data.pageNav && data.line.some(item => item.type === MessageType.title
|
|
@@ -358,11 +362,11 @@ export function canSendQQPage(session: Session | undefined, data: MsgMessage): s
|
|
|
358
362
|
/** Send a native QQ Markdown page with inline command links. */
|
|
359
363
|
export async function sendQQPage(session: Session, data: MsgMessage): Promise<void> {
|
|
360
364
|
const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0)
|
|
361
|
-
await sendQQMarkdown(session, markdown)
|
|
365
|
+
await sendQQMarkdown(session, markdown, renderQQKeyboard(data))
|
|
362
366
|
}
|
|
363
367
|
|
|
364
368
|
/** Reuse the adapter's native reply API for structured Markdown messages. */
|
|
365
|
-
export async function sendQQMarkdown(session: Session, markdown: string): Promise<void> {
|
|
369
|
+
export async function sendQQMarkdown(session: Session, markdown: string, keyboard?: QQKeyboard): Promise<void> {
|
|
366
370
|
const internal = (session.bot as typeof session.bot & { internal: QQInternal }).internal
|
|
367
371
|
const request: QQRequest = {
|
|
368
372
|
// With msg_type=2, QQ requires the ordinary content field to be empty.
|
|
@@ -370,6 +374,7 @@ export async function sendQQMarkdown(session: Session, markdown: string): Promis
|
|
|
370
374
|
msg_type: 2,
|
|
371
375
|
markdown: { content: markdown },
|
|
372
376
|
}
|
|
377
|
+
if (keyboard) request.keyboard = keyboard
|
|
373
378
|
// Match the QQ adapter's passive-reply sequence.
|
|
374
379
|
const age = Date.now() - session.timestamp
|
|
375
380
|
if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1000 - 2000) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withoutMessageButtonFallbacks } from '../../shared/bot/analysisMsg'
|
|
1
2
|
import { MessageType, type IMessage } from '../../shared/face'
|
|
2
3
|
import { compactDisplayText, escapeMarkdown } from './qqMarkdownHelpers'
|
|
3
4
|
import { appendMissingBattleLogLinks, renderBattleLogLinkText } from './qqBattleLogLinks'
|
|
@@ -68,6 +69,7 @@ function renderQQMailMarkdown(data: IMessage): string {
|
|
|
68
69
|
|
|
69
70
|
/** Reward cards stay compact; mail keeps its content before the attachment quote. */
|
|
70
71
|
export function renderQQRewardMarkdown(data: IMessage, showGreeting: boolean): string {
|
|
72
|
+
data = withoutMessageButtonFallbacks(data)
|
|
71
73
|
if (data.mail) return appendMissingBattleLogLinks(renderQQMailMarkdown(data), data)
|
|
72
74
|
const rewards = data.line.filter(item => item.type === MessageType.line && item.reward)
|
|
73
75
|
.map(item => renderRewardLine(item.content)).filter(Boolean)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withoutMessageButtonFallbacks } from '../../shared/bot/analysisMsg'
|
|
2
|
+
import { renderQQKeyboard } from './qqMessageButtons'
|
|
1
3
|
import type { Session } from 'koishi'
|
|
2
4
|
import { MessageType, type IMessage } from '../../shared/face'
|
|
3
5
|
import type { MsgMessage } from '../../shared/bot/MsgMessage'
|
|
@@ -20,6 +22,7 @@ function fencedStory(content: string): string {
|
|
|
20
22
|
|
|
21
23
|
/** Marked stories keep their complete text; settled rewards use the shared reward card. */
|
|
22
24
|
export function renderQQStoryMarkdown(data: IMessage, showGreeting: boolean): string {
|
|
25
|
+
data = withoutMessageButtonFallbacks(data)
|
|
23
26
|
if (data.reward === true && data.line.some(item => item.type === MessageType.line && item.reward === true && item.content.trim())) {
|
|
24
27
|
const stories = data.line.filter(item => item.type === MessageType.line && item.codeBlock === true)
|
|
25
28
|
.map(item => fencedStory(item.content))
|
|
@@ -75,5 +78,5 @@ export function renderQQStoryMarkdown(data: IMessage, showGreeting: boolean): st
|
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
export async function sendQQStory(session: Session, data: MsgMessage): Promise<void> {
|
|
78
|
-
await sendQQMarkdown(session, renderQQStoryMarkdown(data, session.event?.channel?.type === 0))
|
|
81
|
+
await sendQQMarkdown(session, renderQQStoryMarkdown(data, session.event?.channel?.type === 0), renderQQKeyboard(data))
|
|
79
82
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withoutMessageButtonFallbacks } from '../../shared/bot/analysisMsg'
|
|
2
|
+
import { renderQQKeyboard } from './qqMessageButtons'
|
|
1
3
|
import type { Session } from 'koishi'
|
|
2
4
|
import { MessageType, type IMessage } from '../../shared/face'
|
|
3
5
|
import type { MsgMessage } from '../../shared/bot/MsgMessage'
|
|
@@ -6,6 +8,7 @@ import { isRewardMessage, renderRewardLine, renderRewardTitle } from './qqReward
|
|
|
6
8
|
|
|
7
9
|
/** Render only explicitly marked transactions; line content never enables this channel. */
|
|
8
10
|
export function renderQQTransactionMarkdown(data: IMessage, showGreeting: boolean): string {
|
|
11
|
+
data = withoutMessageButtonFallbacks(data)
|
|
9
12
|
const rewardMessage = isRewardMessage(data)
|
|
10
13
|
const shortageMessage = data.transaction === true && data.line.some(item =>
|
|
11
14
|
item.type === MessageType.title && item.content === '❌材料不足')
|
|
@@ -90,5 +93,5 @@ export function renderQQTransactionMarkdown(data: IMessage, showGreeting: boolea
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
export async function sendQQTransaction(session: Session, data: MsgMessage): Promise<void> {
|
|
93
|
-
await sendQQMarkdown(session, renderQQTransactionMarkdown(data, session.event?.channel?.type === 0))
|
|
96
|
+
await sendQQMarkdown(session, renderQQTransactionMarkdown(data, session.event?.channel?.type === 0), renderQQKeyboard(data))
|
|
94
97
|
}
|
|
@@ -12,7 +12,7 @@ export default class handelr_text extends handel_base {
|
|
|
12
12
|
if (preview.markdown !== undefined && canSendQQPage(session, data)) {
|
|
13
13
|
const sendStartedAt = this.timing ? performance.now() : 0
|
|
14
14
|
try {
|
|
15
|
-
await sendQQMarkdown(session, preview.markdown)
|
|
15
|
+
await sendQQMarkdown(session, preview.markdown, preview.keyboard)
|
|
16
16
|
if (this.timing) {
|
|
17
17
|
this.timing.delivery = 'qq-markdown'
|
|
18
18
|
this.timing.sendMs = performance.now() - sendStartedAt
|
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import { IMessage, MessageType, type PageNavData } from '../face'
|
|
2
2
|
import { renderPlainInlineCommandAliases } from './inlineCommands'
|
|
3
3
|
|
|
4
|
+
/** Platform-neutral button hints also work when Markdown or keyboards are unavailable. */
|
|
5
|
+
export function messageButtonFallbackLines(data: Pick<IMessage, 'buttons'>): string[] {
|
|
6
|
+
return [...new Set((data.buttons ?? []).flatMap(row => row.buttons.map(button =>
|
|
7
|
+
'[' + button.command + ']' + button.label)))]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function matchesButtonFallback(content: string, fallback: string): boolean {
|
|
11
|
+
const bracket = fallback.indexOf(']')
|
|
12
|
+
const prefix = fallback.slice(0, bracket + 1)
|
|
13
|
+
const line = content.trim()
|
|
14
|
+
return line.startsWith(prefix) && line.slice(prefix.length).trim() === fallback.slice(prefix.length).trim()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function appendMessageButtonFallbacks(content: string, data: Pick<IMessage, 'buttons'>): string {
|
|
18
|
+
const lines = content.replace(/\r\n?/g, '\n').split('\n')
|
|
19
|
+
const missing = messageButtonFallbackLines(data).filter(fallback =>
|
|
20
|
+
!lines.some(line => matchesButtonFallback(line, fallback)))
|
|
21
|
+
if (!missing.length) return content
|
|
22
|
+
return content + (content && !content.endsWith('\n') ? '\n' : '') + missing.join('\n') + '\n'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Remove only complete action hints; keep source line indexes used by page metadata. */
|
|
26
|
+
export function withoutMessageButtonFallbacks<T extends IMessage>(data: T): T {
|
|
27
|
+
const fallbacks = messageButtonFallbackLines(data)
|
|
28
|
+
if (!fallbacks.length) return data
|
|
29
|
+
return { ...data, line: data.line.map(item => {
|
|
30
|
+
if (item.type !== MessageType.line || item.codeBlock || item.reward || item.mailPart !== undefined) return item
|
|
31
|
+
const lines = item.content.replace(/\r\n?/g, '\n').split('\n')
|
|
32
|
+
const kept = lines.filter(line => !fallbacks.some(fallback => matchesButtonFallback(line, fallback)))
|
|
33
|
+
return kept.length === lines.length ? item : { ...item, content: kept.join('\n') }
|
|
34
|
+
}) }
|
|
35
|
+
}
|
|
36
|
+
|
|
4
37
|
/** 把服务器下发的结构化行拼成纯文本;第一阶段只处理 title / line / error **/
|
|
5
38
|
export class AnalysisMsg {
|
|
6
39
|
parse(data: IMessage) {
|
|
@@ -43,6 +76,7 @@ export class AnalysisMsg {
|
|
|
43
76
|
break
|
|
44
77
|
}
|
|
45
78
|
}
|
|
79
|
+
msg = appendMessageButtonFallbacks(msg, data)
|
|
46
80
|
if (haveTitle) {
|
|
47
81
|
msg += '┄━════════━┄'
|
|
48
82
|
}
|
|
@@ -25,7 +25,7 @@ export interface ServiceType {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export const serviceProto: ServiceProto<ServiceType> = {
|
|
28
|
-
"version":
|
|
28
|
+
"version": 39,
|
|
29
29
|
"services": [
|
|
30
30
|
{
|
|
31
31
|
"id": 0,
|
|
@@ -253,6 +253,18 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
|
|
253
253
|
"target": "messageFace/TitleCardData"
|
|
254
254
|
},
|
|
255
255
|
"optional": true
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"id": 17,
|
|
259
|
+
"name": "buttons",
|
|
260
|
+
"type": {
|
|
261
|
+
"type": "Array",
|
|
262
|
+
"elementType": {
|
|
263
|
+
"type": "Reference",
|
|
264
|
+
"target": "messageFace/MessageButtonRow"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"optional": true
|
|
256
268
|
}
|
|
257
269
|
]
|
|
258
270
|
},
|
|
@@ -1857,6 +1869,65 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
|
|
1857
1869
|
}
|
|
1858
1870
|
]
|
|
1859
1871
|
},
|
|
1872
|
+
"messageFace/MessageButtonRow": {
|
|
1873
|
+
"type": "Interface",
|
|
1874
|
+
"properties": [
|
|
1875
|
+
{
|
|
1876
|
+
"id": 0,
|
|
1877
|
+
"name": "buttons",
|
|
1878
|
+
"type": {
|
|
1879
|
+
"type": "Array",
|
|
1880
|
+
"elementType": {
|
|
1881
|
+
"type": "Reference",
|
|
1882
|
+
"target": "messageFace/MessageButton"
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
]
|
|
1887
|
+
},
|
|
1888
|
+
"messageFace/MessageButton": {
|
|
1889
|
+
"type": "Interface",
|
|
1890
|
+
"properties": [
|
|
1891
|
+
{
|
|
1892
|
+
"id": 0,
|
|
1893
|
+
"name": "label",
|
|
1894
|
+
"type": {
|
|
1895
|
+
"type": "String"
|
|
1896
|
+
}
|
|
1897
|
+
},
|
|
1898
|
+
{
|
|
1899
|
+
"id": 1,
|
|
1900
|
+
"name": "command",
|
|
1901
|
+
"type": {
|
|
1902
|
+
"type": "String"
|
|
1903
|
+
}
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
"id": 2,
|
|
1907
|
+
"name": "style",
|
|
1908
|
+
"type": {
|
|
1909
|
+
"type": "Union",
|
|
1910
|
+
"members": [
|
|
1911
|
+
{
|
|
1912
|
+
"id": 0,
|
|
1913
|
+
"type": {
|
|
1914
|
+
"type": "Literal",
|
|
1915
|
+
"literal": "primary"
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
"id": 1,
|
|
1920
|
+
"type": {
|
|
1921
|
+
"type": "Literal",
|
|
1922
|
+
"literal": "secondary"
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
]
|
|
1926
|
+
},
|
|
1927
|
+
"optional": true
|
|
1928
|
+
}
|
|
1929
|
+
]
|
|
1930
|
+
},
|
|
1860
1931
|
"PtlMessage/ReqMessage": {
|
|
1861
1932
|
"type": "Interface",
|
|
1862
1933
|
"extends": [
|
package/src/shared/face/index.ts
CHANGED
|
@@ -198,6 +198,19 @@ export interface PageNavData {
|
|
|
198
198
|
filters?: Array<{ name: string; emoji?: string; command: string }>;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/** 平台无关的消息按钮;点击后发送普通指令,业务仍负责权限与状态校验。 */
|
|
202
|
+
export interface MessageButton {
|
|
203
|
+
label: string;
|
|
204
|
+
command: string;
|
|
205
|
+
/** 主操作蓝色线框,次操作灰色线框;缺省为次操作。 */
|
|
206
|
+
style?: 'primary' | 'secondary';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 同一行的按钮;QQ 原生键盘最多五行,每行最多五个。 */
|
|
210
|
+
export interface MessageButtonRow {
|
|
211
|
+
buttons: MessageButton[];
|
|
212
|
+
}
|
|
213
|
+
|
|
201
214
|
/** 下发给客户端的消息主体 **/
|
|
202
215
|
export interface IMessage {
|
|
203
216
|
handleType: MessageHandleType;
|
|
@@ -224,4 +237,6 @@ export interface IMessage {
|
|
|
224
237
|
line: IMessageItem[];
|
|
225
238
|
companionCard?: CompanionCardData;
|
|
226
239
|
titleCard?: TitleCardData;
|
|
240
|
+
/** 通用指令按钮;不支持原生按钮的平台保留文字指令。 */
|
|
241
|
+
buttons?: MessageButtonRow[];
|
|
227
242
|
}
|