@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/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { c as log, l as basename, n as config, o as pkg, r as getConfig, t as bindHandlers } from "./config-DKa7596f.js";
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-BPbnNAix.js";
4
- import { t as getMessageStore } from "./message-O73AeL_P.js";
5
- import { t as needQrOnboard } from "./onboard-DPWgX39H.js";
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(url, {
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(url, appId, secret).catch(() => {});
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 `![karin #${width}px #${height}px](${url})`;
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
- * 默认走 markdown 通道:
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
- if (grouping.markdowns.length) grouping.markdowns.forEach((m) => lines.push(m.markdown));
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
- * 永远走 markdown 通道(msg_type=2),文本 / at / 图片均通过 markdown
2075
- * 渲染。Markdown / Keyboard 已全量开放,老的 type=text/image 通道不再使用。
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.forEach((m) => lines.push(m.markdown));
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 推导兜底的 WSS 地址
4427
+ * 从 proxy.prodApi / proxy.sandboxApi 推导兜底的 WS 地址
4253
4428
  * 官方实际长期返回的也是 wss://{host}/websocket/,不依赖 /gateway 也能直连
4254
4429
  */
4255
- const buildFallbackWssUrl = (cfg) => {
4256
- return `${(cfg.sandbox ? cfg.sandboxApi : cfg.prodApi).replace(/^https?:\/\//i, "wss://").replace(/\/+$/, "")}/websocket/`;
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 = buildFallbackWssUrl(cfg);
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,使用硬编码 WSS ${fallback}`);
4450
+ log("warn", `${cfg.appId}: 无 access_token,使用硬编码 WS ${fallback}`);
4267
4451
  return fallback;
4268
4452
  }
4269
4453
  try {
4270
- const { data } = await axios.get(`${apiUrl}/gateway`, {
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 返回为空,使用硬编码 WSS ${fallback}`);
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},使用硬编码 WSS ${fallback}`);
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}),使用硬编码 WSS ${fallback}`);
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
- tokenApi: bot.tokenApi,
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 (bot) => {
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 { c as log, s as pluginDirName } from "./config-DKa7596f.js";
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 { a as writeConfig, c as log, i as getDefaultConfig, n as config } from "./config-DKa7596f.js";
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";
@@ -0,0 +1,4 @@
1
+ import "./config-B_Gc9JKZ.js";
2
+ import "./common-by1GpV0b.js";
3
+
4
+ export { };
@@ -1,22 +1,24 @@
1
1
  //#region src/web.config.d.ts
2
- declare const _default: import("node-karin").DefineConfig<{
3
- qqbot: Array<{
4
- name: string;
5
- appId: string;
6
- secret: string;
7
- prodApi: string;
8
- sandboxApi: string;
9
- tokenApi: string;
10
- sandbox: boolean;
11
- qqEnable: boolean;
12
- guildEnable: boolean;
13
- guildMode: boolean;
14
- regex: string[];
15
- "keyboard:enable": boolean;
16
- "messageCache:enable": boolean;
17
- "messageCache:self": boolean;
18
- "event:type": string;
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 };