@karinjs/adapter-qqbot 2.1.0 → 2.2.1
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 +10 -0
- package/dist/apps/cleanup.js +2 -2
- package/dist/apps/login.js +2 -2
- package/dist/config-B_Gc9JKZ.js +253 -0
- package/dist/index.d.ts +21 -6
- package/dist/index.js +223 -36
- package/dist/{message-O73AeL_P.js → message-Dt4QkDYn.js} +1 -1
- package/dist/{onboard-DPWgX39H.js → onboard-BXQ1cG8h.js} +1 -1
- package/dist/utils-DIq0lGMt.js +4 -0
- package/dist/web.config.d.ts +21 -19
- package/dist/web.config.js +224 -96
- package/package.json +3 -3
- package/dist/config-DKa7596f.js +0 -170
- package/dist/utils-BPbnNAix.js +0 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { c as
|
|
1
|
+
import { c as joinHttpPath, d as normalizeWsGatewayUrl, f as pkg, h as basename, l as normalizeHttpUrl, m as log, n as config, r as getConfig, s as buildFallbackWsUrlFromApi, t as bindHandlers, u as normalizeProxyConfig } from "./config-B_Gc9JKZ.js";
|
|
2
2
|
import { i as random, n as getImageSize, r as handleUrl, t as fakeEvent } from "./common-by1GpV0b.js";
|
|
3
|
-
import "./utils-
|
|
4
|
-
import { t as getMessageStore } from "./message-
|
|
5
|
-
import { t as needQrOnboard } from "./onboard-
|
|
6
|
-
import { AdapterBase, app, buttonHandle, createDirectMessage, createFriendDecreaseNotice, createFriendIncreaseNotice, createFriendMessage, createGroupMemberAddNotice, createGroupMemberDelNotice, createGroupMessage, createGuildMessage, fileToUrl, fileToUrlHandlerKey, handler, karin as karin$1, karinToQQBot, logger, registerBot, segment, unregisterBot } from "node-karin";
|
|
3
|
+
import "./utils-DIq0lGMt.js";
|
|
4
|
+
import { t as getMessageStore } from "./message-Dt4QkDYn.js";
|
|
5
|
+
import { t as needQrOnboard } from "./onboard-BXQ1cG8h.js";
|
|
6
|
+
import { AdapterBase, app, buttonHandle, common, createDirectMessage, createFriendDecreaseNotice, createFriendIncreaseNotice, createFriendMessage, createGroupMemberAddNotice, createGroupMemberDelNotice, createGroupMessage, createGuildMessage, fileToUrl, fileToUrlHandlerKey, handler, karin as karin$1, karinToQQBot, logger, registerBot, segment, unregisterBot } from "node-karin";
|
|
7
7
|
import { URL } from "url";
|
|
8
8
|
import lodash from "node-karin/lodash";
|
|
9
9
|
import { createReadStream } from "node:fs";
|
|
@@ -1280,9 +1280,10 @@ const tokens = /* @__PURE__ */ new Map();
|
|
|
1280
1280
|
* - 同一 appId 旧定时器会被清理,避免并发刷新链
|
|
1281
1281
|
*/
|
|
1282
1282
|
const getAccessToken = async (url, appId, secret) => {
|
|
1283
|
+
const tokenUrl = normalizeHttpUrl(url);
|
|
1283
1284
|
let res;
|
|
1284
1285
|
try {
|
|
1285
|
-
res = await axios.post(
|
|
1286
|
+
res = await axios.post(tokenUrl, {
|
|
1286
1287
|
appId: String(appId),
|
|
1287
1288
|
clientSecret: secret
|
|
1288
1289
|
}, { headers: { "User-Agent": getUserAgent() } });
|
|
@@ -1304,7 +1305,7 @@ const getAccessToken = async (url, appId, secret) => {
|
|
|
1304
1305
|
tokens.set(appId, state);
|
|
1305
1306
|
const delay = Math.max(5e3, expiresIn * 1e3 - 5e4);
|
|
1306
1307
|
state.refreshTimer = setTimeout(() => {
|
|
1307
|
-
getAccessToken(
|
|
1308
|
+
getAccessToken(tokenUrl, appId, secret).catch(() => {});
|
|
1308
1309
|
}, delay);
|
|
1309
1310
|
return {
|
|
1310
1311
|
accessToken,
|
|
@@ -1331,7 +1332,7 @@ const stopTokenRefresh = (appId) => {
|
|
|
1331
1332
|
*/
|
|
1332
1333
|
const createAxiosInstance = (baseURL, appId) => {
|
|
1333
1334
|
const instance = axios.create({
|
|
1334
|
-
baseURL,
|
|
1335
|
+
baseURL: normalizeHttpUrl(baseURL),
|
|
1335
1336
|
headers: {
|
|
1336
1337
|
"Content-Type": "application/json",
|
|
1337
1338
|
"User-Agent": getUserAgent()
|
|
@@ -1729,6 +1730,7 @@ const resolveApiMessageId = (ctx, contact, messageId) => {
|
|
|
1729
1730
|
//#endregion
|
|
1730
1731
|
//#region src/core/adapter/text-to-md.ts
|
|
1731
1732
|
const MAX_BUTTONS = 5;
|
|
1733
|
+
const MARKDOWN_IMAGE_RE = /!\[[^\]]*]\(\s*(<[^>]+>|[^\s)]+)(?:\s+["'][^"']*["'])?\s*\)/g;
|
|
1732
1734
|
/**
|
|
1733
1735
|
* 提取文本中的 URL,将其替换为 `[按钮 N]` 占位文本并返回 button 列表
|
|
1734
1736
|
*
|
|
@@ -1769,6 +1771,40 @@ const imagesToMarkdown = async (urls) => {
|
|
|
1769
1771
|
return ``;
|
|
1770
1772
|
}));
|
|
1771
1773
|
};
|
|
1774
|
+
/**
|
|
1775
|
+
* 将 markdown 文本拆为普通文本片段与图片来源。
|
|
1776
|
+
*/
|
|
1777
|
+
const splitMarkdownImages = (markdown) => {
|
|
1778
|
+
const parts = [];
|
|
1779
|
+
let lastIndex = 0;
|
|
1780
|
+
for (const match of markdown.matchAll(MARKDOWN_IMAGE_RE)) {
|
|
1781
|
+
const index = match.index ?? 0;
|
|
1782
|
+
if (index > lastIndex) {
|
|
1783
|
+
const text = markdown.slice(lastIndex, index);
|
|
1784
|
+
if (text) parts.push({
|
|
1785
|
+
type: "text",
|
|
1786
|
+
value: text
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
const source = (match[1] || "").trim().replace(/^<|>$/g, "");
|
|
1790
|
+
if (source) parts.push({
|
|
1791
|
+
type: "image",
|
|
1792
|
+
source
|
|
1793
|
+
});
|
|
1794
|
+
lastIndex = index + match[0].length;
|
|
1795
|
+
}
|
|
1796
|
+
if (lastIndex < markdown.length) {
|
|
1797
|
+
const text = markdown.slice(lastIndex);
|
|
1798
|
+
if (text) parts.push({
|
|
1799
|
+
type: "text",
|
|
1800
|
+
value: text
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
return parts.length ? parts : [{
|
|
1804
|
+
type: "text",
|
|
1805
|
+
value: markdown
|
|
1806
|
+
}];
|
|
1807
|
+
};
|
|
1772
1808
|
|
|
1773
1809
|
//#endregion
|
|
1774
1810
|
//#region src/core/adapter/pipeline-qq.ts
|
|
@@ -1797,19 +1833,21 @@ const FRIEND_EVENT_WHITELIST = /* @__PURE__ */ new Set([
|
|
|
1797
1833
|
const sendQQ = async (ctx, contact, elements) => {
|
|
1798
1834
|
const target = contact.scene === "friend" ? "user" : "group";
|
|
1799
1835
|
const grouping = groupElements(contact.scene, elements);
|
|
1800
|
-
if (ctx.cfg.keyboard.enable && grouping.text.length) {
|
|
1836
|
+
if (ctx.cfg.markdown.enable && ctx.cfg.keyboard.enable && grouping.text.length) {
|
|
1801
1837
|
const { text, buttons } = extractUrlButtons(grouping.text.join(""), contact.scene === "friend");
|
|
1802
1838
|
grouping.text = [text];
|
|
1803
1839
|
grouping.buttons.push(...buttons);
|
|
1804
1840
|
}
|
|
1805
1841
|
/**
|
|
1806
|
-
*
|
|
1842
|
+
* 自动 Markdown 通道:
|
|
1807
1843
|
* text / at / 可转换为公网 URL 的图片通过 markdown content 渲染;无法转换的图片
|
|
1808
1844
|
* 与视频 / 语音 / 文件由 sendQQMarkdown 内部以 msg_type=7 单独补发。所有富媒体
|
|
1809
1845
|
* 都会优先通过 fileToUrl 取得公网地址,QQ 上传只做兜底。只有显式引用的
|
|
1810
1846
|
* 纯文本消息会降级为 msg_type=0,以保证 QQ 客户端能稳定显示引用气泡。
|
|
1847
|
+
* 显式 segment.markdown 不受该自动通道开关影响。
|
|
1811
1848
|
*/
|
|
1812
|
-
return sendQQMarkdown(ctx, contact, grouping, target);
|
|
1849
|
+
if (ctx.cfg.markdown.enable) return sendQQMarkdown(ctx, contact, grouping, target);
|
|
1850
|
+
return sendQQClassic(ctx, contact, grouping, target);
|
|
1813
1851
|
};
|
|
1814
1852
|
/**
|
|
1815
1853
|
* Markdown 通道:合成一条 msg_type=2 + 可选 keyboard
|
|
@@ -1819,7 +1857,8 @@ const sendQQMarkdown = async (ctx, contact, grouping, target) => {
|
|
|
1819
1857
|
const lines = [];
|
|
1820
1858
|
if (grouping.text.length) lines.push(grouping.text.join(""));
|
|
1821
1859
|
const fallbackImages = await appendMarkdownImages(ctx, lines, grouping.qqImages);
|
|
1822
|
-
|
|
1860
|
+
const markdownFallbackImages = await appendExplicitMarkdown(ctx, lines, grouping);
|
|
1861
|
+
fallbackImages.push(...markdownFallbackImages);
|
|
1823
1862
|
if (contact.scene === "group") {
|
|
1824
1863
|
warnUnsupportedCommandEnterButtons$1(ctx, collectCommandEnterButtons(grouping.buttons, grouping.keyboards));
|
|
1825
1864
|
warnUnsupportedCommandEnterMarkdowns$1(ctx, grouping.markdowns.map((m) => m.markdown));
|
|
@@ -1843,6 +1882,47 @@ const sendQQMarkdown = async (ctx, contact, grouping, target) => {
|
|
|
1843
1882
|
return flushQQ(ctx, contact, grouping, items);
|
|
1844
1883
|
};
|
|
1845
1884
|
/**
|
|
1885
|
+
* 经典通道:文本走 msg_type=0,图片 / 富媒体走 msg_type=7。
|
|
1886
|
+
*/
|
|
1887
|
+
const sendQQClassic = async (ctx, contact, grouping, target) => {
|
|
1888
|
+
const items = [];
|
|
1889
|
+
const lines = [];
|
|
1890
|
+
const images = [...grouping.qqImages];
|
|
1891
|
+
if (grouping.text.length) lines.push(grouping.text.join(""));
|
|
1892
|
+
if (!grouping.markdowns.length && (grouping.buttons.length || grouping.keyboards.length)) ctx.logger("warn", "Markdown 通道已关闭,button / keyboard 无法随普通文本发送,已跳过");
|
|
1893
|
+
const content = lines.join("\n").trim();
|
|
1894
|
+
if (content) items.push(ctx.super.qq.text(content));
|
|
1895
|
+
for (const image of images) items.push(await buildQQMediaItem(ctx, target, contact.peer, "image", image));
|
|
1896
|
+
for (const m of grouping.media) items.push(await buildQQMediaItem(ctx, target, contact.peer, m.kind, m.source, m.name));
|
|
1897
|
+
await appendExplicitMarkdownItems(ctx, contact, grouping, target, items);
|
|
1898
|
+
if (!items.length) items.push(ctx.super.qq.text("不支持发送的消息类型"));
|
|
1899
|
+
return flushQQ(ctx, contact, grouping, items);
|
|
1900
|
+
};
|
|
1901
|
+
/**
|
|
1902
|
+
* 显式 `segment.markdown` 是调用方指定的发送类型,不受 Markdown 自动通道开关影响。
|
|
1903
|
+
*/
|
|
1904
|
+
const appendExplicitMarkdownItems = async (ctx, contact, grouping, target, items) => {
|
|
1905
|
+
if (!grouping.markdowns.length) return;
|
|
1906
|
+
const lines = [];
|
|
1907
|
+
const fallbackImages = await appendExplicitMarkdown(ctx, lines, grouping);
|
|
1908
|
+
if (contact.scene === "group") {
|
|
1909
|
+
warnUnsupportedCommandEnterButtons$1(ctx, collectCommandEnterButtons(grouping.buttons, grouping.keyboards));
|
|
1910
|
+
warnUnsupportedCommandEnterMarkdowns$1(ctx, grouping.markdowns.map((m) => m.markdown));
|
|
1911
|
+
}
|
|
1912
|
+
const keyboard = buildKeyboard$1(grouping);
|
|
1913
|
+
const content = lines.length ? lines.join("\n") : BUTTON_ONLY_MARKDOWN$1;
|
|
1914
|
+
items.push(ctx.super.qq.markdown({ content }, keyboard));
|
|
1915
|
+
for (const file of fallbackImages) {
|
|
1916
|
+
const res = await ctx.super.media.uploadFallback(target, contact.peer, "image", file, false);
|
|
1917
|
+
items.push(ctx.super.qq.media(res.file_info));
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
const buildQQMediaItem = async (ctx, target, peer, type, source, name) => {
|
|
1921
|
+
const resolved = await resolvePreferredMediaSource(ctx, type, source, name);
|
|
1922
|
+
const res = resolved.via === "fallback" ? await ctx.super.media.uploadFallback(target, peer, type, resolved.source, false, name) : await ctx.super.media.upload(target, peer, type, resolved.source, false, name);
|
|
1923
|
+
return ctx.super.qq.media(res.file_info);
|
|
1924
|
+
};
|
|
1925
|
+
/**
|
|
1846
1926
|
* 将能转成公网 URL 的图片追加到 markdown 行,无法转换的图片返回给富媒体 fallback。
|
|
1847
1927
|
*
|
|
1848
1928
|
* @param ctx 适配器实例,用于输出降级日志。
|
|
@@ -1863,6 +1943,26 @@ const appendMarkdownImages = async (ctx, lines, files) => {
|
|
|
1863
1943
|
return fallback;
|
|
1864
1944
|
};
|
|
1865
1945
|
/**
|
|
1946
|
+
* 处理显式 segment.markdown 中的图片:可转公网 URL 的继续嵌入,无法转的改走富媒体。
|
|
1947
|
+
*/
|
|
1948
|
+
const appendExplicitMarkdown = async (ctx, lines, grouping) => {
|
|
1949
|
+
const fallback = [];
|
|
1950
|
+
for (const markdown of grouping.markdowns) for (const part of splitMarkdownImages(markdown.markdown)) {
|
|
1951
|
+
if (part.type === "text") {
|
|
1952
|
+
const text = part.value.trim();
|
|
1953
|
+
if (text) lines.push(text);
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
const resolved = await resolvePreferredMediaSource(ctx, "image", part.source);
|
|
1957
|
+
if (resolved.via === "fallback") {
|
|
1958
|
+
fallback.push(part.source);
|
|
1959
|
+
continue;
|
|
1960
|
+
}
|
|
1961
|
+
await appendImageUrlLine(lines, resolved.source);
|
|
1962
|
+
}
|
|
1963
|
+
return fallback;
|
|
1964
|
+
};
|
|
1965
|
+
/**
|
|
1866
1966
|
* 生成单张 markdown 图片行。
|
|
1867
1967
|
* 尺寸获取失败时使用保守默认尺寸,保证 fileToUrl 成功的图片仍走 markdown。
|
|
1868
1968
|
*
|
|
@@ -2071,17 +2171,18 @@ const BUTTON_ONLY_MARKDOWN = "";
|
|
|
2071
2171
|
/**
|
|
2072
2172
|
* 处理频道场景(子频道 + 私信)的发送
|
|
2073
2173
|
*
|
|
2074
|
-
*
|
|
2075
|
-
*
|
|
2174
|
+
* 默认把普通文本 / at / 图片通过 markdown 渲染。关闭自动 Markdown 通道后,
|
|
2175
|
+
* 普通文本和图片改走经典发送;显式 segment.markdown 仍按调用方指定走 Markdown。
|
|
2076
2176
|
*/
|
|
2077
2177
|
const sendGuild = async (ctx, contact, elements) => {
|
|
2078
2178
|
const grouping = groupElements(contact.scene, elements);
|
|
2079
|
-
if (ctx.cfg.keyboard.enable && grouping.text.length) {
|
|
2179
|
+
if (ctx.cfg.markdown.enable && ctx.cfg.keyboard.enable && grouping.text.length) {
|
|
2080
2180
|
const { text, buttons } = extractUrlButtons(grouping.text.join(""), false);
|
|
2081
2181
|
grouping.text = [text];
|
|
2082
2182
|
grouping.buttons.push(...buttons);
|
|
2083
2183
|
}
|
|
2084
|
-
return sendGuildMarkdown(ctx, contact, grouping);
|
|
2184
|
+
if (ctx.cfg.markdown.enable) return sendGuildMarkdown(ctx, contact, grouping);
|
|
2185
|
+
return sendGuildClassic(ctx, contact, grouping);
|
|
2085
2186
|
};
|
|
2086
2187
|
/**
|
|
2087
2188
|
* Markdown 通道
|
|
@@ -2099,7 +2200,13 @@ const sendGuildMarkdown = async (ctx, contact, grouping) => {
|
|
|
2099
2200
|
const mdImages = await imagesToMarkdown(allImages);
|
|
2100
2201
|
lines.push(...mdImages);
|
|
2101
2202
|
}
|
|
2102
|
-
grouping.markdowns
|
|
2203
|
+
for (const markdown of grouping.markdowns) for (const part of splitMarkdownImages(markdown.markdown)) if (part.type === "text") {
|
|
2204
|
+
const text = part.value.trim();
|
|
2205
|
+
if (text) lines.push(text);
|
|
2206
|
+
} else {
|
|
2207
|
+
const [mdImage] = await imagesToMarkdown([part.source]);
|
|
2208
|
+
lines.push(mdImage);
|
|
2209
|
+
}
|
|
2103
2210
|
warnUnsupportedCommandEnterButtons(ctx, contact.scene, collectCommandEnterButtons(grouping.buttons, grouping.keyboards));
|
|
2104
2211
|
warnUnsupportedCommandEnterMarkdowns(ctx, contact.scene, grouping.markdowns.map((m) => m.markdown));
|
|
2105
2212
|
if (lines.length || grouping.buttons.length || grouping.keyboards.length) {
|
|
@@ -2110,6 +2217,55 @@ const sendGuildMarkdown = async (ctx, contact, grouping) => {
|
|
|
2110
2217
|
if (!items.length) items.push(ctx.super.guild.text("不支持发送的消息类型"));
|
|
2111
2218
|
return flushGuild(ctx, contact, grouping, items);
|
|
2112
2219
|
};
|
|
2220
|
+
/**
|
|
2221
|
+
* 经典通道:文本走 content,图片按文档走 image / file_image 单独发送。
|
|
2222
|
+
*/
|
|
2223
|
+
const sendGuildClassic = async (ctx, contact, grouping) => {
|
|
2224
|
+
const items = [];
|
|
2225
|
+
const lines = [];
|
|
2226
|
+
const images = [...grouping.guildImageUrls, ...grouping.guildImageFiles];
|
|
2227
|
+
if (grouping.text.length) lines.push(grouping.text.join(""));
|
|
2228
|
+
if (!grouping.markdowns.length && (grouping.buttons.length || grouping.keyboards.length)) ctx.logger("warn", "Markdown 通道已关闭,button / keyboard 无法随普通文本发送,已跳过");
|
|
2229
|
+
const content = lines.join("\n").trim();
|
|
2230
|
+
if (content) items.push(ctx.super.guild.text(content));
|
|
2231
|
+
for (const image of images) items.push(await buildGuildImageItem(image));
|
|
2232
|
+
await appendExplicitGuildMarkdownItems(ctx, contact, grouping, items);
|
|
2233
|
+
if (!items.length) items.push(ctx.super.guild.text("不支持发送的消息类型"));
|
|
2234
|
+
return flushGuild(ctx, contact, grouping, items);
|
|
2235
|
+
};
|
|
2236
|
+
/**
|
|
2237
|
+
* 显式 `segment.markdown` 是调用方指定的发送类型,不受 Markdown 自动通道开关影响。
|
|
2238
|
+
*/
|
|
2239
|
+
const appendExplicitGuildMarkdownItems = async (ctx, contact, grouping, items) => {
|
|
2240
|
+
if (!grouping.markdowns.length) return;
|
|
2241
|
+
const lines = [];
|
|
2242
|
+
for (const markdown of grouping.markdowns) for (const part of splitMarkdownImages(markdown.markdown)) if (part.type === "text") {
|
|
2243
|
+
const text = part.value.trim();
|
|
2244
|
+
if (text) lines.push(text);
|
|
2245
|
+
} else {
|
|
2246
|
+
const [mdImage] = await imagesToMarkdown([part.source]);
|
|
2247
|
+
lines.push(mdImage);
|
|
2248
|
+
}
|
|
2249
|
+
warnUnsupportedCommandEnterButtons(ctx, contact.scene, collectCommandEnterButtons(grouping.buttons, grouping.keyboards));
|
|
2250
|
+
warnUnsupportedCommandEnterMarkdowns(ctx, contact.scene, grouping.markdowns.map((m) => m.markdown));
|
|
2251
|
+
const keyboard = buildKeyboard(grouping);
|
|
2252
|
+
const content = lines.length ? lines.join("\n") : BUTTON_ONLY_MARKDOWN;
|
|
2253
|
+
items.push(ctx.super.guild.markdown({ content }, keyboard));
|
|
2254
|
+
};
|
|
2255
|
+
const buildGuildImageItem = async (source) => {
|
|
2256
|
+
if (/^https?:\/\//i.test(source)) return {
|
|
2257
|
+
type: "image",
|
|
2258
|
+
image: source
|
|
2259
|
+
};
|
|
2260
|
+
const form = new FormData();
|
|
2261
|
+
form.append("file_image", await common.buffer(source, { http: false }), { filename: resolveImageFilename(source) });
|
|
2262
|
+
return form;
|
|
2263
|
+
};
|
|
2264
|
+
const resolveImageFilename = (source) => {
|
|
2265
|
+
const file = source.split(/[?#]/)[0] || "";
|
|
2266
|
+
const name = path.basename(file);
|
|
2267
|
+
return name.includes(".") ? name : "image.jpg";
|
|
2268
|
+
};
|
|
2113
2269
|
const buildKeyboard = (grouping) => {
|
|
2114
2270
|
const rows = [];
|
|
2115
2271
|
grouping.buttons.forEach((b) => rows.push(...karinToQQBot(b)));
|
|
@@ -3255,6 +3411,25 @@ const cacheReference = (client, message) => {
|
|
|
3255
3411
|
}).catch((err) => log("warn", `[getMsg] 写入引用上下文失败: ${message.messageId}`, err));
|
|
3256
3412
|
};
|
|
3257
3413
|
/**
|
|
3414
|
+
* 将 Karin 快速回复自动生成的当前消息引用 ID 转成 QQ 客户端可见引用 ID。
|
|
3415
|
+
*
|
|
3416
|
+
* Karin 核心只能根据 `e.message_id` 生成 `segment.reply(e.message_id)`;而 QQ 群/单聊
|
|
3417
|
+
* 的可见引用需要 `message_scene.ext` 里的 `msg_idx=REFIDX_xxx`。这里仅转换
|
|
3418
|
+
* `reply.messageId === ev.d.id` 的那一个自动引用段,保留 `ev.d.id` 作为 Karin 标准
|
|
3419
|
+
* messageId,避免影响 getMsg / recallMsg / 被动回复 msg_id 等主链路。
|
|
3420
|
+
*/
|
|
3421
|
+
const normalizeQuickReplyReference = (elements, sourceMessageId, referenceMessageId) => {
|
|
3422
|
+
if (!sourceMessageId || !referenceMessageId) return elements;
|
|
3423
|
+
let normalized = false;
|
|
3424
|
+
return elements.map((element) => {
|
|
3425
|
+
if (!normalized && element.type === "reply" && element.messageId === sourceMessageId) {
|
|
3426
|
+
normalized = true;
|
|
3427
|
+
return segment.reply(referenceMessageId);
|
|
3428
|
+
}
|
|
3429
|
+
return element;
|
|
3430
|
+
});
|
|
3431
|
+
};
|
|
3432
|
+
/**
|
|
3258
3433
|
* 群消息(@机器人 / 普通消息合并入口)
|
|
3259
3434
|
*/
|
|
3260
3435
|
const onGroupMsg = (client, ev, opts) => {
|
|
@@ -3299,7 +3474,7 @@ const onGroupMsg = (client, ev, opts) => {
|
|
|
3299
3474
|
time,
|
|
3300
3475
|
contact,
|
|
3301
3476
|
sender,
|
|
3302
|
-
srcReply: (elements) => client.srcReply(e, [...elements, segment.pasmsg(ev.d.id)])
|
|
3477
|
+
srcReply: (elements) => client.srcReply(e, [...normalizeQuickReplyReference(elements, karinMessageId, messageIndex), segment.pasmsg(ev.d.id)])
|
|
3303
3478
|
});
|
|
3304
3479
|
return e;
|
|
3305
3480
|
};
|
|
@@ -3345,7 +3520,7 @@ const onFriendMsg = (client, ev) => {
|
|
|
3345
3520
|
time,
|
|
3346
3521
|
contact,
|
|
3347
3522
|
sender,
|
|
3348
|
-
srcReply: (elements) => client.srcReply(e, [...elements, segment.pasmsg(ev.d.id)])
|
|
3523
|
+
srcReply: (elements) => client.srcReply(e, [...normalizeQuickReplyReference(elements, karinMessageId, messageIndex), segment.pasmsg(ev.d.id)])
|
|
3349
3524
|
});
|
|
3350
3525
|
return e;
|
|
3351
3526
|
};
|
|
@@ -4249,25 +4424,34 @@ const QUICK_DISCONNECT_THRESHOLD_MS = 5e3;
|
|
|
4249
4424
|
const QUICK_DISCONNECT_LIMIT = 3;
|
|
4250
4425
|
const QUICK_DISCONNECT_DELAY_MS = 6e4;
|
|
4251
4426
|
/**
|
|
4252
|
-
* 从 prodApi / sandboxApi 推导兜底的
|
|
4427
|
+
* 从 proxy.prodApi / proxy.sandboxApi 推导兜底的 WS 地址
|
|
4253
4428
|
* 官方实际长期返回的也是 wss://{host}/websocket/,不依赖 /gateway 也能直连
|
|
4254
4429
|
*/
|
|
4255
|
-
const
|
|
4256
|
-
return
|
|
4430
|
+
const buildFallbackWsUrl = (cfg) => {
|
|
4431
|
+
return buildFallbackWsUrlFromApi(cfg.sandbox ? cfg.proxy.sandboxApi : cfg.proxy.prodApi);
|
|
4257
4432
|
};
|
|
4258
4433
|
/**
|
|
4259
4434
|
* 获取网关地址:优先调 /gateway;失败 / 超时 / 限频 → 直接 fallback 到硬编码
|
|
4260
4435
|
*/
|
|
4261
4436
|
const fetchGateway = async (cfg) => {
|
|
4262
|
-
const apiUrl = cfg.sandbox ? cfg.sandboxApi : cfg.prodApi;
|
|
4263
|
-
const fallback =
|
|
4437
|
+
const apiUrl = normalizeHttpUrl(cfg.sandbox ? cfg.proxy.sandboxApi : cfg.proxy.prodApi);
|
|
4438
|
+
const fallback = buildFallbackWsUrl(cfg);
|
|
4439
|
+
const proxyWsValue = cfg.sandbox ? cfg.proxy.sandboxWs : cfg.proxy.prodWs;
|
|
4440
|
+
if (proxyWsValue.trim()) try {
|
|
4441
|
+
const proxyWsUrl = normalizeWsGatewayUrl(proxyWsValue);
|
|
4442
|
+
log("debug", `${cfg.appId}: 使用自定义 WS ${proxyWsUrl}`);
|
|
4443
|
+
return proxyWsUrl;
|
|
4444
|
+
} catch (err) {
|
|
4445
|
+
const reason = err?.message || "unknown";
|
|
4446
|
+
log("warn", `${cfg.appId}: 自定义 WS 地址无效 (${reason}),尝试获取 /gateway`);
|
|
4447
|
+
}
|
|
4264
4448
|
const accessToken = getBotAccessToken(cfg.appId);
|
|
4265
4449
|
if (!accessToken) {
|
|
4266
|
-
log("warn", `${cfg.appId}: 无 access_token,使用硬编码
|
|
4450
|
+
log("warn", `${cfg.appId}: 无 access_token,使用硬编码 WS ${fallback}`);
|
|
4267
4451
|
return fallback;
|
|
4268
4452
|
}
|
|
4269
4453
|
try {
|
|
4270
|
-
const { data } = await axios.get(
|
|
4454
|
+
const { data } = await axios.get(joinHttpPath(apiUrl, "gateway"), {
|
|
4271
4455
|
headers: {
|
|
4272
4456
|
Authorization: `QQBot ${accessToken}`,
|
|
4273
4457
|
"User-Agent": getUserAgent()
|
|
@@ -4275,7 +4459,7 @@ const fetchGateway = async (cfg) => {
|
|
|
4275
4459
|
timeout: GATEWAY_FETCH_TIMEOUT_MS
|
|
4276
4460
|
});
|
|
4277
4461
|
if (data?.url) return data.url;
|
|
4278
|
-
log("warn", `${cfg.appId}: /gateway 返回为空,使用硬编码
|
|
4462
|
+
log("warn", `${cfg.appId}: /gateway 返回为空,使用硬编码 WS ${fallback}`);
|
|
4279
4463
|
return fallback;
|
|
4280
4464
|
} catch (err) {
|
|
4281
4465
|
if (axios.isAxiosError(err)) {
|
|
@@ -4283,10 +4467,10 @@ const fetchGateway = async (cfg) => {
|
|
|
4283
4467
|
const status = response?.status ?? 0;
|
|
4284
4468
|
const data = response?.data;
|
|
4285
4469
|
const detail = formatOpenAPIError(status, typeof data?.code === "number" ? data.code : void 0, typeof data?.message === "string" ? data.message : void 0);
|
|
4286
|
-
log("warn", `${cfg.appId}: /gateway 调用失败 | ${detail},使用硬编码
|
|
4470
|
+
log("warn", `${cfg.appId}: /gateway 调用失败 | ${detail},使用硬编码 WS ${fallback}`);
|
|
4287
4471
|
} else {
|
|
4288
4472
|
const reason = err?.message || "unknown";
|
|
4289
|
-
log("warn", `${cfg.appId}: /gateway 调用失败 (${reason}),使用硬编码
|
|
4473
|
+
log("warn", `${cfg.appId}: /gateway 调用失败 (${reason}),使用硬编码 WS ${fallback}`);
|
|
4290
4474
|
}
|
|
4291
4475
|
return fallback;
|
|
4292
4476
|
}
|
|
@@ -4418,7 +4602,7 @@ const onClose = (cfg, intents, attempt, event, client) => {
|
|
|
4418
4602
|
if (code === 4004) {
|
|
4419
4603
|
log("warn", `${cfg.appId}: access_token 已失效,刷新 token 后重连...`);
|
|
4420
4604
|
schedule(cfg.appId, 1e3, () => {
|
|
4421
|
-
getAccessToken(cfg.tokenApi, cfg.appId, cfg.secret).then(() => connect(cfg, intents, 0)).catch(() => {
|
|
4605
|
+
getAccessToken(cfg.proxy.tokenApi, cfg.appId, cfg.secret).then(() => connect(cfg, intents, 0)).catch(() => {
|
|
4422
4606
|
log("error", `${cfg.appId}: access_token 刷新失败,${RATE_LIMIT_DELAY_MS}ms 后重试连接...`);
|
|
4423
4607
|
schedule(cfg.appId, RATE_LIMIT_DELAY_MS, () => connect(cfg, intents, attempt + 1));
|
|
4424
4608
|
});
|
|
@@ -4499,18 +4683,21 @@ const initState = /* @__PURE__ */ new Map();
|
|
|
4499
4683
|
const hashConfig = (bot) => JSON.stringify({
|
|
4500
4684
|
appId: bot.appId,
|
|
4501
4685
|
secret: bot.secret,
|
|
4502
|
-
|
|
4503
|
-
prodApi: bot.prodApi,
|
|
4504
|
-
sandboxApi: bot.sandboxApi,
|
|
4686
|
+
proxy: bot.proxy,
|
|
4505
4687
|
sandbox: bot.sandbox,
|
|
4506
4688
|
event: bot.event,
|
|
4507
4689
|
keyboard: bot.keyboard,
|
|
4690
|
+
markdown: bot.markdown,
|
|
4508
4691
|
messageCache: bot.messageCache
|
|
4509
4692
|
});
|
|
4510
4693
|
/**
|
|
4511
4694
|
* 创建单个 Bot 实例
|
|
4512
4695
|
*/
|
|
4513
|
-
const createBot = async (
|
|
4696
|
+
const createBot = async (input) => {
|
|
4697
|
+
const bot = {
|
|
4698
|
+
...input,
|
|
4699
|
+
proxy: normalizeProxyConfig(input.proxy)
|
|
4700
|
+
};
|
|
4514
4701
|
const appId = String(bot.appId);
|
|
4515
4702
|
if (!appId) {
|
|
4516
4703
|
logger.warn("[QQ Official Bot] 配置缺少 appId,跳过");
|
|
@@ -4532,8 +4719,8 @@ const createBot = async (bot) => {
|
|
|
4532
4719
|
offAll(appId);
|
|
4533
4720
|
stopTokenRefresh(appId);
|
|
4534
4721
|
logger.info(`[QQ Official Bot][${appId}] 获取 access_token...`);
|
|
4535
|
-
await getAccessToken(bot.tokenApi, appId, bot.secret);
|
|
4536
|
-
const baseUrl = bot.sandbox ? bot.sandboxApi : bot.prodApi;
|
|
4722
|
+
await getAccessToken(bot.proxy.tokenApi, appId, bot.secret);
|
|
4723
|
+
const baseUrl = bot.sandbox ? bot.proxy.sandboxApi : bot.proxy.prodApi;
|
|
4537
4724
|
const api = new QQBotApi(createAxiosInstance(baseUrl, appId));
|
|
4538
4725
|
const me = await api.meta.getMe();
|
|
4539
4726
|
const client = new AdapterQQBot(bot, api);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { m as log, p as pluginDirName } from "./config-B_Gc9JKZ.js";
|
|
2
2
|
import { karinPathBase, karinPathTemp } from "node-karin";
|
|
3
3
|
import { createWriteStream } from "node:fs";
|
|
4
4
|
import axios from "node-karin/axios";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as getDefaultConfig, m as log, n as config, o as writeConfig } from "./config-B_Gc9JKZ.js";
|
|
2
2
|
import { a as sleep } from "./common-by1GpV0b.js";
|
|
3
3
|
import qrcode from "qrcode";
|
|
4
4
|
import axios from "node-karin/axios";
|
package/dist/web.config.d.ts
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
//#region src/web.config.d.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
type WebQQBotInput = {
|
|
3
|
+
name: unknown;
|
|
4
|
+
appId: unknown;
|
|
5
|
+
secret: unknown;
|
|
6
|
+
'proxy:api': unknown;
|
|
7
|
+
'proxy:ws': unknown;
|
|
8
|
+
sandbox: unknown;
|
|
9
|
+
qqEnable: unknown;
|
|
10
|
+
guildEnable: unknown;
|
|
11
|
+
guildMode: unknown;
|
|
12
|
+
regex: unknown;
|
|
13
|
+
'keyboard:enable': unknown;
|
|
14
|
+
'markdown:enable': unknown;
|
|
15
|
+
'messageCache:enable': unknown;
|
|
16
|
+
'messageCache:self': unknown;
|
|
17
|
+
'event:type': unknown;
|
|
18
|
+
};
|
|
19
|
+
type WebConfigInput = {
|
|
20
|
+
qqbot: WebQQBotInput[];
|
|
21
|
+
};
|
|
22
|
+
declare const _default: import("node-karin").DefineComponentConfig<WebConfigInput>;
|
|
21
23
|
//#endregion
|
|
22
24
|
export { _default as default };
|