@napuketto/adapter 0.0.20 → 0.0.21

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.
@@ -1,7 +1,9 @@
1
1
  import { i as BaseAction, o as ActionRegistry, r as BaseProtocolAdapter, t as ensureSilk } from "../core-of7sLiQX.mjs";
2
- import { i as createWsServerSchema, n as collectGrayTipUids, r as createHttpServerSchema, t as forEachRawMessage } from "../raw-message-DfHdOVHi.mjs";
3
- import { ChatType, GrayTipSubType, GroupApi, GroupNotifyMsgType, NTGroupMemberRole, NTGroupRequestOperateTypes, TicketApi, TipGroupElementType, WebHonorType, kernelError, toCanonicalElements } from "@napuketto/kernel";
2
+ import { i as createWsServerSchema, n as collectGrayTipUids, r as createHttpServerSchema, t as forEachRawMessage } from "../raw-message-Bx18pIQS.mjs";
3
+ import { ChatType, GrayTipSubType, GroupApi, GroupNotifyMsgStatus, GroupNotifyMsgType, NTGroupMemberRole, NTGroupRequestOperateTypes, TicketApi, TipGroupElementType, WebHonorType, kernelError, toCanonicalElements } from "@napuketto/kernel";
4
+ import { downloadUrl, inferExtension } from "@napuketto/media";
4
5
  import { z } from "zod";
6
+ import { randomUUID } from "node:crypto";
5
7
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
6
8
  import { basename, join } from "node:path";
7
9
  import { HttpClient, HttpServer, WsClient, WsServer } from "@napuketto/network";
@@ -1853,7 +1855,7 @@ function cqMessageToCanonical(text) {
1853
1855
  //#endregion
1854
1856
  //#region src/onebot11/helper/message-info.ts
1855
1857
  /** 毫秒 → 秒(Unix 时间戳)。 */
1856
- const MS_TO_SEC$3 = 1e3;
1858
+ const MS_TO_SEC$4 = 1e3;
1857
1859
  /** RawMessage → OB11 消息信息(message_id 经 MessageUnique 映射)。
1858
1860
  * ctx(可选):接收方向 ID 转换上下文(at uid→uin、reply NT msgId→OB11 id,P2-19)。 */
1859
1861
  function toOb11MessageInfo(msg, unique, ctx = {}) {
@@ -1870,7 +1872,7 @@ function toOb11MessageInfo(msg, unique, ctx = {}) {
1870
1872
  user_id: Number(msg.senderUin),
1871
1873
  nickname: msg.sendNickName
1872
1874
  },
1873
- time: Math.floor(Number(msg.msgTime) / MS_TO_SEC$3),
1875
+ time: Math.floor(Number(msg.msgTime) / MS_TO_SEC$4),
1874
1876
  message: canonicalToSegments(elements),
1875
1877
  raw_message: canonicalToCqMessage(elements)
1876
1878
  };
@@ -1885,57 +1887,93 @@ function toOb11MessageInfo(msg, unique, ctx = {}) {
1885
1887
  //#endregion
1886
1888
  //#region src/onebot11/helper/notice.ts
1887
1889
  /**
1888
- * OB11 notice 事件翻译(P2-7,2026-08-05)
1890
+ * OB11 notice 事件翻译(P2-7,2026-08-05;2026-09-08 T4 补全)
1889
1891
  *
1890
1892
  * NT QQ 中群成员变动/撤回等系统事件通过**消息的灰色提示元素(grayTip)**广播。
1891
1893
  * 本模块把 RawMessage 里的 grayTip 翻译为 OB11 notice 事件(纯函数,ADR-008)。
1892
1894
  *
1893
1895
  * 支持:
1894
- * - group_recall(撤回,grayTip.subElementType=REVOKE)
1896
+ * - group_recall(群撤回,grayTip.subElementType=REVOKE + chatType=GROUP)
1897
+ * - friend_recall(好友撤回,REVOKE + chatType=C2C,2026-09-08)
1895
1898
  * - group_increase / group_decrease(群成员变动,subElementType=GROUP + TipGroupElement.type)
1896
1899
  * - group_admin(管理员变更,TipGroupElement.type=BLOCK/UNBLOCK 的 role 语义)
1897
1900
  * - group_ban(禁言,TipGroupElement.type=SHUT_UP)
1901
+ * - notify.poke(戳一戳,aioOpGrayTipElement;⚠️ 待真实 poke 事件验证,2026-09-08)
1902
+ *
1903
+ * 未知/未翻译的 grayTip 子类型(JSON/BUDDY/ESSENCE/GROUP_NOTIFY 等)打 raw JSON
1904
+ * 日志(ctx.logger 可选)——为后续真实事件校准积累数据。
1898
1905
  *
1899
1906
  * user_id/operator_id 都是 uin:接收 uidToUin Map(调用方批量转换后传入,保持纯函数)。
1900
1907
  */
1901
1908
  /** 毫秒 → 秒(Unix 时间戳)。 */
1902
- const MS_TO_SEC$2 = 1e3;
1909
+ const MS_TO_SEC$3 = 1e3;
1903
1910
  /** 检查消息是否含可翻译的 grayTip 元素。 */
1904
1911
  function hasGrayTip(msg) {
1905
1912
  return msg.elements.some((el) => el.grayTipElement !== void 0);
1906
1913
  }
1907
1914
  /** uid → uin(上下文映射缺省退化为 uid)。 */
1908
- function toUin(uid, ctx) {
1915
+ function toUin$1(uid, ctx) {
1909
1916
  const uin = ctx.uidToUin.get(uid);
1910
1917
  return Number(uin ?? uid);
1911
1918
  }
1912
1919
  /** 基础字段(time/self_id/post_type)。 */
1913
1920
  function base(msg, ctx) {
1914
1921
  return {
1915
- time: Math.floor(Number(msg.msgTime) / MS_TO_SEC$2),
1922
+ time: Math.floor(Number(msg.msgTime) / MS_TO_SEC$3),
1916
1923
  self_id: Number(ctx.selfUin),
1917
1924
  post_type: "notice",
1918
1925
  group_id: Number(msg.peerUid)
1919
1926
  };
1920
1927
  }
1921
- /** 撤回事件(group_recall)。 */
1928
+ /** 撤回事件(chatType 分流:GROUP→group_recall / C2C→friend_recall,2026-09-08)。 */
1922
1929
  function toRecall(msg, g, ctx) {
1923
1930
  if (g?.revokeElement === void 0) return null;
1924
1931
  const revoke = g.revokeElement;
1932
+ if (msg.chatType === ChatType.C2C) return {
1933
+ time: Math.floor(Number(msg.msgTime) / MS_TO_SEC$3),
1934
+ self_id: Number(ctx.selfUin),
1935
+ post_type: "notice",
1936
+ notice_type: "friend_recall",
1937
+ user_id: toUin$1(revoke.operatorUid, ctx),
1938
+ message_id: Number(msg.msgSeq)
1939
+ };
1925
1940
  return {
1926
1941
  ...base(msg, ctx),
1927
1942
  notice_type: "group_recall",
1928
- user_id: toUin(revoke.operatorUid, ctx),
1929
- operator_id: toUin(revoke.operatorUid, ctx),
1943
+ user_id: toUin$1(revoke.operatorUid, ctx),
1944
+ operator_id: toUin$1(revoke.operatorUid, ctx),
1930
1945
  message_id: Number(msg.msgSeq)
1931
1946
  };
1932
1947
  }
1948
+ /**
1949
+ * 戳一戳(notify.poke,aioOpGrayTipElement,2026-09-08)。
1950
+ *
1951
+ * ⚠️ 待真实 poke 事件验证:aioOp 载荷的完整字段形状未经实测(实体类型仅有
1952
+ * operateType/peerUid 两个已证字段)。当前口径:user_id = 消息发送者,
1953
+ * target_id = aioOp.peerUid(C2C 即对端;群内为目标 uid 的假设待校准),
1954
+ * group_id = 群号(C2C 为 0)。poke 路径始终打 raw 日志积累校准数据。
1955
+ */
1956
+ function toPoke(msg, g, ctx) {
1957
+ const aioOp = g?.aioOpGrayTipElement;
1958
+ ctx.logger?.warn({ raw: JSON.stringify(g)?.slice(0, 2e3) }, "ob11: poke grayTip raw(待真实事件校准)");
1959
+ const isGroup = msg.chatType === ChatType.GROUP;
1960
+ return {
1961
+ time: Math.floor(Number(msg.msgTime) / MS_TO_SEC$3),
1962
+ self_id: Number(ctx.selfUin),
1963
+ post_type: "notice",
1964
+ notice_type: "notify",
1965
+ sub_type: "poke",
1966
+ group_id: isGroup ? Number(msg.peerUid) : 0,
1967
+ user_id: toUin$1(msg.senderUid, ctx),
1968
+ target_id: toUin$1(aioOp?.peerUid ?? "", ctx)
1969
+ };
1970
+ }
1933
1971
  /** 群成员变动(group_increase / group_decrease / group_admin / group_ban)。 */
1934
1972
  function toGroupChange(msg, g, ctx) {
1935
1973
  const grp = g?.groupElement;
1936
1974
  if (grp === void 0 || grp.type === void 0) return null;
1937
1975
  const common = {
1938
- time: Math.floor(Number(msg.msgTime) / MS_TO_SEC$2),
1976
+ time: Math.floor(Number(msg.msgTime) / MS_TO_SEC$3),
1939
1977
  selfId: Number(ctx.selfUin),
1940
1978
  groupId: Number(msg.peerUid),
1941
1979
  memberUid: grp.memberUid ?? "",
@@ -1960,8 +1998,8 @@ function toMemberAdd(common, ctx) {
1960
1998
  notice_type: "group_increase",
1961
1999
  sub_type: adminUid === memberUid ? "approve" : "invite",
1962
2000
  group_id: groupId,
1963
- operator_id: toUin(adminUid, ctx),
1964
- user_id: toUin(memberUid, ctx)
2001
+ operator_id: toUin$1(adminUid, ctx),
2002
+ user_id: toUin$1(memberUid, ctx)
1965
2003
  };
1966
2004
  }
1967
2005
  /** 群成员退出(leave)。 */
@@ -1974,8 +2012,8 @@ function toQuit(common, ctx) {
1974
2012
  notice_type: "group_decrease",
1975
2013
  sub_type: "leave",
1976
2014
  group_id: groupId,
1977
- operator_id: toUin(adminUid, ctx),
1978
- user_id: toUin(memberUid, ctx)
2015
+ operator_id: toUin$1(adminUid, ctx),
2016
+ user_id: toUin$1(memberUid, ctx)
1979
2017
  };
1980
2018
  }
1981
2019
  /** 管理员变更(BLOCK=set / UNBLOCK=unset)。 */
@@ -1988,7 +2026,7 @@ function toAdminChange(common, grp, ctx) {
1988
2026
  notice_type: "group_admin",
1989
2027
  sub_type: grp.type === TipGroupElementType.BLOCK ? "set" : "unset",
1990
2028
  group_id: groupId,
1991
- user_id: toUin(memberUid === "" ? adminUid : memberUid, ctx)
2029
+ user_id: toUin$1(memberUid === "" ? adminUid : memberUid, ctx)
1992
2030
  };
1993
2031
  }
1994
2032
  /** 禁言(duration>0=ban / 否则 lift_ban)。 */
@@ -2002,29 +2040,128 @@ function toBan(common, grp, ctx) {
2002
2040
  notice_type: "group_ban",
2003
2041
  sub_type: Number(shutUp?.duration ?? 0) > 0 ? "ban" : "lift_ban",
2004
2042
  group_id: groupId,
2005
- operator_id: toUin(shutUp?.admin?.uid ?? adminUid, ctx),
2006
- user_id: toUin(shutUp?.member?.uid ?? memberUid, ctx),
2043
+ operator_id: toUin$1(shutUp?.admin?.uid ?? adminUid, ctx),
2044
+ user_id: toUin$1(shutUp?.member?.uid ?? memberUid, ctx),
2007
2045
  duration: Number(shutUp?.duration ?? 0)
2008
2046
  };
2009
2047
  }
2010
- /** RawMessage → OB11 notice 事件(无 grayTip 返回 null)。 */
2048
+ /** 单个 grayTip 元素 → notice 事件(null = 不可翻译;未知子类型打 raw 日志)。 */
2049
+ function translateGrayTip(msg, g, ctx) {
2050
+ const subType = g.subElementType;
2051
+ if (subType === GrayTipSubType.REVOKE) return toRecall(msg, g, ctx);
2052
+ if (subType === GrayTipSubType.GROUP) return toGroupChange(msg, g, ctx);
2053
+ if (g.aioOpGrayTipElement !== void 0) return toPoke(msg, g, ctx);
2054
+ ctx.logger?.warn({
2055
+ subType,
2056
+ raw: JSON.stringify(g)?.slice(0, 2e3)
2057
+ }, "ob11: 未翻译的 grayTip 子类型(raw 校准数据)");
2058
+ return null;
2059
+ }
2060
+ /** RawMessage → OB11 notice 事件(无 grayTip 返回 null)。
2061
+ * 未知/未翻译子类型打 raw JSON 日志(校准数据积累,2026-09-08)。 */
2011
2062
  function toOb11NoticeEvent(msg, ctx) {
2012
2063
  for (const el of msg.elements) {
2013
2064
  const g = el.grayTipElement;
2014
2065
  if (g === void 0) continue;
2015
- const subType = g.subElementType;
2016
- if (subType === GrayTipSubType.REVOKE) {
2017
- const recall = toRecall(msg, g, ctx);
2018
- if (recall !== null) return recall;
2019
- }
2020
- if (subType === GrayTipSubType.GROUP) {
2021
- const change = toGroupChange(msg, g, ctx);
2022
- if (change !== null) return change;
2023
- }
2066
+ const event = translateGrayTip(msg, g, ctx);
2067
+ if (event !== null) return event;
2024
2068
  }
2025
2069
  return null;
2026
2070
  }
2027
2071
  //#endregion
2072
+ //#region src/onebot11/helper/request.ts
2073
+ /**
2074
+ * OB11 request 事件翻译(2026-09-08)
2075
+ *
2076
+ * 事件源(kernel 事件通道,本次接线):
2077
+ * - `Group/onGroupNotifiesUpdated`(GroupBridge,群系统通知推送)→ group_add / group_invite
2078
+ * - `Buddy/onBuddyReqChange`(FriendBridge,好友申请推送)→ friend
2079
+ *
2080
+ * flag 语义(与应答动作匹配路径一致):
2081
+ * - 群请求 flag = GroupNotify.seq(set_group_add_request 按 `item.seq === flag` 匹配)
2082
+ * - 好友请求 flag = BuddyReq.reqTime(set_friend_add_request 按 `reqTime === flag` 匹配)
2083
+ *
2084
+ * 纯函数(ADR-008):uid→uin 由调用方批量转换后传入。
2085
+ * ⚠️ onBuddyReqChange 回调参数形状未经真实事件校准(wrapper 字符串证据仅有方法名),
2086
+ * narrowBuddyReqs 防御性收窄,未知形状返回 null 由调用方打 raw 日志积累校准数据。
2087
+ */
2088
+ /** 毫秒 → 秒(Unix 时间戳)。 */
2089
+ const MS_TO_SEC$2 = 1e3;
2090
+ /** uid → uin(上下文映射缺省时:数字 uid 直取数值,非数字 uid 返回 0 表示未知)。 */
2091
+ function toUin(uid, ctx) {
2092
+ if (uid === void 0 || uid === "") return 0;
2093
+ const uin = ctx.uidToUin.get(uid);
2094
+ if (uin !== void 0) return Number(uin);
2095
+ const n = Number(uid);
2096
+ return Number.isFinite(n) ? n : 0;
2097
+ }
2098
+ /**
2099
+ * GroupNotify → OB11 群请求事件(group_add / group_invite)。
2100
+ *
2101
+ * type 语义与 get_group_system_msg 动作同一解释:1/5=邀请(invite)、7=申请(add);
2102
+ * 仅未处理状态(KUNHANDLE)推送——已处理通知重复推送不再发事件。
2103
+ * 非请求类通知(踢人/转让等处理结果回执)返回 null。
2104
+ */
2105
+ function toOb11GroupRequestEvent(notify, ctx) {
2106
+ if (notify.status !== GroupNotifyMsgStatus.KUNHANDLE) return null;
2107
+ let subType;
2108
+ switch (notify.type) {
2109
+ case GroupNotifyMsgType.INVITED_BY_MEMBER:
2110
+ case GroupNotifyMsgType.INVITED_NEED_ADMINI_STRATOR_PASS:
2111
+ subType = "invite";
2112
+ break;
2113
+ case GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS:
2114
+ subType = "add";
2115
+ break;
2116
+ default: return null;
2117
+ }
2118
+ return {
2119
+ time: Math.floor(Date.now() / MS_TO_SEC$2),
2120
+ self_id: Number(ctx.selfUin),
2121
+ post_type: "request",
2122
+ request_type: "group",
2123
+ sub_type: subType,
2124
+ group_id: Number(notify.group?.groupCode ?? 0),
2125
+ user_id: toUin(notify.user1?.uid, ctx),
2126
+ comment: notify.postscript ?? "",
2127
+ flag: String(notify.seq)
2128
+ };
2129
+ }
2130
+ /** BuddyReq 防御性收窄:仅接受字段形状可用的对象(onBuddyReqChange 参数待真实事件校准)。 */
2131
+ function isBuddyReqLike(value) {
2132
+ if (typeof value !== "object" || value === null) return false;
2133
+ const v = value;
2134
+ return typeof v["reqTime"] === "string" && typeof v["friendUid"] === "string";
2135
+ }
2136
+ /**
2137
+ * onBuddyReqChange 回调参数 → BuddyReq 列表(防御性收窄)。
2138
+ * 已知候选形状:BuddyReq[] / { buddyReqs: BuddyReq[] };其余返回 null(调用方打 raw 日志)。
2139
+ */
2140
+ function narrowBuddyReqs(arg) {
2141
+ if (Array.isArray(arg)) return arg.every(isBuddyReqLike) ? arg : null;
2142
+ if (typeof arg === "object" && arg !== null) {
2143
+ const reqs = arg["buddyReqs"];
2144
+ if (Array.isArray(reqs) && reqs.every(isBuddyReqLike)) return reqs;
2145
+ }
2146
+ return null;
2147
+ }
2148
+ /**
2149
+ * BuddyReq → OB11 好友请求事件(friend)。
2150
+ * comment 取 words 字段(验证留言,形状待校准,缺省空串);flag = reqTime。
2151
+ */
2152
+ function toOb11FriendRequestEvent(req, ctx) {
2153
+ const words = req["words"];
2154
+ return {
2155
+ time: Math.floor(Date.now() / MS_TO_SEC$2),
2156
+ self_id: Number(ctx.selfUin),
2157
+ post_type: "request",
2158
+ request_type: "friend",
2159
+ user_id: toUin(req.friendUid, ctx),
2160
+ comment: typeof words === "string" ? words : "",
2161
+ flag: req.reqTime
2162
+ };
2163
+ }
2164
+ //#endregion
2028
2165
  //#region src/onebot11/action/message/get-forward-msg.ts
2029
2166
  /**
2030
2167
  * get_forward_msg 动作:获取合并转发内容(P2-12 接 kernel MsgApi.fetchForwardMessage)
@@ -2156,10 +2293,16 @@ var GetGroupMsgHistoryAction = class extends BaseAction {
2156
2293
  //#endregion
2157
2294
  //#region src/onebot11/action/message/get-media.ts
2158
2295
  /**
2159
- * get_image / get_record 动作:获取图片/语音文件信息(P2-14,简化版)
2296
+ * get_image / get_record 动作:获取图片/语音文件(P2-14;2026-09-08 T5 接主动下载)
2160
2297
  *
2161
- * message_id 反查 → fetchMsgsByMsgId → 找 PIC/PTT 元素 → 返回元素已有路径与 URL。
2162
- * 不做主动下载/转码(NapCat 走 downloadRichMedia 事件驱动下载,待后续接入)。
2298
+ * message_id 反查 → fetchMsgsByMsgId → 找 PIC/PTT 元素:
2299
+ * - 本地文件解析:NT 相对路径(sourcePath/filePath)按 mediaBaseDir(QQ NT
2300
+ * global 目录)解析为绝对路径,命中磁盘即返回 file
2301
+ * - 图片主动下载:本地未命中且有 picUrl → @napuketto/media downloadUrl 落
2302
+ * cacheDir/media/,返回 file(绝对路径)+ url + file_size/file_name
2303
+ * - 语音下载缺口:原生 downloadRichMedia 签名未探测(wrapper 字符串证据仅
2304
+ * 方法名),本地未命中时返回原始 filePath(NT 相对)+ 元数据,待 T10 diag
2305
+ * 实测后接入
2163
2306
  */
2164
2307
  const getMediaSchema = z.object({
2165
2308
  /** CQ 码 file 字段或 message_id(简化:message_id 反查)。 */
@@ -2167,6 +2310,35 @@ const getMediaSchema = z.object({
2167
2310
  message_id: z.union([z.number(), z.string()]).optional()
2168
2311
  });
2169
2312
  /**
2313
+ * NT 相对路径 → 本地绝对路径(存在才返回)。
2314
+ * 候选:原样绝对路径 / mediaBaseDir 下相对路径(反斜杠归一化)。
2315
+ */
2316
+ function resolveLocalMediaFile(relPath, deps) {
2317
+ if (relPath === "") return null;
2318
+ if (existsSync(relPath)) return relPath;
2319
+ const base = deps.mediaBaseDir;
2320
+ if (base !== void 0 && base !== "") {
2321
+ const joined = join(base, relPath.replaceAll("\\", "/"));
2322
+ if (existsSync(joined)) return joined;
2323
+ }
2324
+ return null;
2325
+ }
2326
+ /** 图片主动下载(picUrl → cacheDir/media/<uuid>.<ext>;无 cacheDir/url 返回 null)。 */
2327
+ async function downloadImageToCache(url, deps) {
2328
+ const cacheDir = deps.cacheDir;
2329
+ if (cacheDir === void 0 || cacheDir === "") return null;
2330
+ const mediaDir = join(cacheDir, "media");
2331
+ const { mkdir } = await import("node:fs/promises");
2332
+ await mkdir(mediaDir, { recursive: true });
2333
+ const dest = join(mediaDir, `${randomUUID()}${inferExtension(null, url)}`);
2334
+ try {
2335
+ await downloadUrl(url, dest);
2336
+ return dest;
2337
+ } catch {
2338
+ return null;
2339
+ }
2340
+ }
2341
+ /**
2170
2342
  * 反查消息并提取媒体元素路径。
2171
2343
  * 返回 null = 消息不存在;extract 返回 undefined = 无匹配元素。
2172
2344
  */
@@ -2180,26 +2352,13 @@ async function resolveMedia(id, label, deps, extract) {
2180
2352
  }
2181
2353
  throw kernelError(`消息 ${id} 不包含${label}`, "NOT_FOUND");
2182
2354
  }
2183
- /** 从消息元素提取媒体路径与 URL(PIC/PTT,纯函数)。 */
2184
- function extractMediaPath(element) {
2185
- const pic = element.picElement;
2186
- if (pic !== void 0) {
2187
- const out = {};
2188
- const path = pic.sourcePath ?? pic.picPath;
2189
- if (path !== void 0) out.file = path;
2190
- if (pic.picUrl !== void 0) out.url = pic.picUrl;
2191
- if (Object.keys(out).length === 0) return;
2192
- return out;
2193
- }
2194
- const ptt = element.pttElement;
2195
- if (ptt !== void 0 && ptt.filePath !== void 0) return { file: ptt.filePath };
2196
- }
2197
- /** 提取语音元素(get_record 用)。 */
2198
- function extractPttPath(element) {
2199
- const ptt = element.pttElement;
2200
- if (ptt !== void 0 && ptt.filePath !== void 0) return { file: ptt.filePath };
2355
+ /** 取 id 参数(message_id 优先,file 兜底)。 */
2356
+ function takeId(payload, actionName) {
2357
+ const id = payload.message_id ?? payload.file;
2358
+ if (id === void 0) throw kernelError(`${actionName} 需要 message_id 或 file`, "INVALID_PARAM");
2359
+ return id;
2201
2360
  }
2202
- /** 获取图片信息(P2-14)。 */
2361
+ /** 获取图片信息(本地优先,picUrl 主动下载兜底)。 */
2203
2362
  var GetImageAction = class extends BaseAction {
2204
2363
  name = "get_image";
2205
2364
  schema = getMediaSchema;
@@ -2209,16 +2368,29 @@ var GetImageAction = class extends BaseAction {
2209
2368
  super();
2210
2369
  this.deps = deps;
2211
2370
  }
2212
- _handle(payload) {
2213
- return this.resolve(payload, "图片", "get_image");
2214
- }
2215
- resolve(payload, label, actionName) {
2216
- const id = payload.message_id ?? payload.file;
2217
- if (id === void 0) throw kernelError(`${actionName} 需要 message_id 或 file`, "INVALID_PARAM");
2218
- return resolveMedia(id, label, this.deps, extractMediaPath);
2371
+ async _handle(payload) {
2372
+ const base = await resolveMedia(takeId(payload, "get_image"), "图片", this.deps, (el) => {
2373
+ const pic = el.picElement;
2374
+ if (pic === void 0) return;
2375
+ const out = {};
2376
+ if (pic.fileSize !== void 0) out.file_size = pic.fileSize;
2377
+ if (pic.fileName !== void 0) out.file_name = pic.fileName;
2378
+ if (pic.picUrl !== void 0) out.url = pic.picUrl;
2379
+ const rel = pic.sourcePath ?? pic.picPath;
2380
+ if (rel !== void 0) {
2381
+ const local = resolveLocalMediaFile(rel, this.deps);
2382
+ if (local !== null) out.file = local;
2383
+ }
2384
+ return out;
2385
+ });
2386
+ if (base.file === void 0 && base.url !== void 0) {
2387
+ const downloaded = await downloadImageToCache(base.url, this.deps);
2388
+ if (downloaded !== null) base.file = downloaded;
2389
+ }
2390
+ return base;
2219
2391
  }
2220
2392
  };
2221
- /** 获取语音信息(P2-14)。 */
2393
+ /** 获取语音信息(本地文件解析;主动下载缺口见文件头注释)。 */
2222
2394
  var GetRecordAction = class extends BaseAction {
2223
2395
  name = "get_record";
2224
2396
  schema = getMediaSchema;
@@ -2229,9 +2401,15 @@ var GetRecordAction = class extends BaseAction {
2229
2401
  this.deps = deps;
2230
2402
  }
2231
2403
  async _handle(payload) {
2232
- const id = payload.message_id ?? payload.file;
2233
- if (id === void 0) throw kernelError("get_record 需要 message_id 或 file", "INVALID_PARAM");
2234
- return await resolveMedia(id, "语音", this.deps, extractPttPath);
2404
+ return await resolveMedia(takeId(payload, "get_record"), "语音", this.deps, (el) => {
2405
+ const ptt = el.pttElement;
2406
+ if (ptt === void 0 || ptt.filePath === void 0) return;
2407
+ const out = {};
2408
+ if (ptt.fileSize !== void 0) out.file_size = ptt.fileSize;
2409
+ if (ptt.fileName !== void 0) out.file_name = ptt.fileName;
2410
+ out.file = resolveLocalMediaFile(ptt.filePath, this.deps) ?? ptt.filePath;
2411
+ return out;
2412
+ });
2235
2413
  }
2236
2414
  };
2237
2415
  //#endregion
@@ -3287,6 +3465,8 @@ var OneBotApi = class {
3287
3465
  cleanCache;
3288
3466
  /** 缓存目录(download_file 用)。 */
3289
3467
  cacheDir;
3468
+ /** QQ NT global 目录(媒体 NT 相对路径解析用)。 */
3469
+ mediaBaseDir;
3290
3470
  /** 进程退出回调(bot_exit 用)。 */
3291
3471
  exit;
3292
3472
  /** 进程重启回调(set_restart 用)。 */
@@ -3308,6 +3488,7 @@ var OneBotApi = class {
3308
3488
  this.appVersion = opts.system.appVersion;
3309
3489
  this.cleanCache = opts.system.cleanCache;
3310
3490
  this.cacheDir = opts.system.cacheDir;
3491
+ this.mediaBaseDir = opts.system.mediaBaseDir;
3311
3492
  this.exit = opts.system.exit;
3312
3493
  this.restart = opts.system.restart;
3313
3494
  }
@@ -3391,10 +3572,13 @@ function toOb11MessageEvent(msg, selfUin, unique, messageFormat = "array", ctx =
3391
3572
  */
3392
3573
  /** Authorization 头前缀。 */
3393
3574
  const BEARER_PREFIX = "Bearer ";
3394
- /** 校验 URL 查询参数里的 access_token(WS client 连接 URL 可能带)。 */
3575
+ /** 校验 URL 查询参数里的 access_token(WS client 连接 URL 可能带)。
3576
+ * ⚠️ req.url 是纯路径(无 origin),new URL(相对路径) 必抛——2026-09-08
3577
+ * T10 E2E 实测抓到的 bug(查询参数鉴权从未生效,WS 全被 4401 关闭),
3578
+ * 用 dummy base 解析。 */
3395
3579
  function hasAccessTokenQuery(url, token) {
3396
3580
  try {
3397
- return new URL(url).searchParams.get("access_token") === token;
3581
+ return new URL(url, "http://localhost").searchParams.get("access_token") === token;
3398
3582
  } catch {
3399
3583
  return false;
3400
3584
  }
@@ -3525,15 +3709,20 @@ const MS_TO_SEC = 1e3;
3525
3709
  var NapukettoOneBot11Adapter = class extends BaseProtocolAdapter {
3526
3710
  protocol = "onebot11";
3527
3711
  msgChannel;
3712
+ groupChannel;
3713
+ friendChannel;
3714
+ calibLogger;
3528
3715
  selfUin;
3529
3716
  oneBotApi;
3530
3717
  /** 动作注册表(公共只读:IPC 桥等装配方枚举动作名直接挂载)。 */
3531
3718
  registry;
3532
- unsubscribe = null;
3719
+ unsubscribes = [];
3533
3720
  transports = null;
3534
3721
  heartbeatTimer = null;
3535
3722
  reportSelfMessage = false;
3536
3723
  messageFormat = "array";
3724
+ /** IPC 桥模式标记(subscribeOnly 启动;reload 时跳过传输重建)。 */
3725
+ subscribedOnly = false;
3537
3726
  constructor(opts) {
3538
3727
  super({
3539
3728
  config: opts.config,
@@ -3541,18 +3730,33 @@ var NapukettoOneBot11Adapter = class extends BaseProtocolAdapter {
3541
3730
  hooks: {
3542
3731
  onStart: (config) => this.startTransports(config),
3543
3732
  onStop: () => this.stopAll(),
3544
- onReload: () => {
3545
- return Promise.resolve();
3546
- }
3733
+ onReload: (config) => this.reloadTransports(config)
3547
3734
  }
3548
3735
  });
3549
3736
  this.msgChannel = opts.msgChannel;
3737
+ this.groupChannel = opts.groupChannel;
3738
+ this.friendChannel = opts.friendChannel;
3739
+ this.calibLogger = opts.logger;
3550
3740
  this.selfUin = opts.self.uin;
3551
3741
  this.oneBotApi = new OneBotApi(opts);
3552
3742
  this.registry = createOb11ActionRegistry({ api: this.oneBotApi });
3553
3743
  }
3744
+ /**
3745
+ * 配置热更新(P2-6,2026-09-08 实现):stop 旧传输/心跳/退订 → 按新配置重建。
3746
+ * IPC 桥模式(subscribeOnly,无传输)只刷新上报开关字段,不重建。
3747
+ */
3748
+ async reloadTransports(config) {
3749
+ if (this.subscribedOnly) {
3750
+ this.reportSelfMessage = config.reportSelfMessage;
3751
+ this.messageFormat = config.messagePostFormat;
3752
+ return;
3753
+ }
3754
+ await this.stopAll();
3755
+ await this.startTransports(config);
3756
+ }
3554
3757
  /** 启动传输:装配(HTTP/WS)+ 打开 server/client + 广播 lifecycle enable + 起心跳。 */
3555
3758
  async startTransports(config) {
3759
+ this.subscribedOnly = false;
3556
3760
  this.reportSelfMessage = config.reportSelfMessage;
3557
3761
  this.messageFormat = config.messagePostFormat;
3558
3762
  const broadcaster = this.getBroadcaster();
@@ -3616,10 +3820,10 @@ var NapukettoOneBot11Adapter = class extends BaseProtocolAdapter {
3616
3820
  this.transports = null;
3617
3821
  }
3618
3822
  }
3619
- /** 订阅 kernel 消息事件(幂等)。 */
3823
+ /** 订阅 kernel 事件(消息 + 群通知 + 好友请求,幂等)。 */
3620
3824
  subscribe() {
3621
- if (this.unsubscribe !== null) return;
3622
- this.unsubscribe = this.msgChannel.on("Msg/onRecvMsg", (msgs) => {
3825
+ if (this.unsubscribes.length > 0) return;
3826
+ this.unsubscribes.push(this.msgChannel.on("Msg/onRecvMsg", (msgs) => {
3623
3827
  forEachRawMessage(msgs, (msg) => {
3624
3828
  if (hasGrayTip(msg)) {
3625
3829
  this.broadcastNotice(msg).catch(() => {});
@@ -3628,7 +3832,24 @@ var NapukettoOneBot11Adapter = class extends BaseProtocolAdapter {
3628
3832
  if (!this.reportSelfMessage && String(msg.senderUin) === this.selfUin) return;
3629
3833
  this.broadcastMessageEvent(msg);
3630
3834
  });
3631
- });
3835
+ }));
3836
+ if (this.groupChannel !== void 0) this.unsubscribes.push(this.groupChannel.on("Group/onGroupNotifiesUpdated", (doubt, notifies) => {
3837
+ if (doubt || !Array.isArray(notifies)) return;
3838
+ this.broadcastGroupRequests(notifies);
3839
+ }));
3840
+ if (this.friendChannel !== void 0) {
3841
+ this.unsubscribes.push(this.friendChannel.on("Buddy/onBuddyReqChange", (arg) => {
3842
+ const reqs = narrowBuddyReqs(arg);
3843
+ if (reqs === null) {
3844
+ this.calibLogger?.warn({ raw: JSON.stringify(arg)?.slice(0, 2e3) }, "ob11: onBuddyReqChange 未知参数形状");
3845
+ return;
3846
+ }
3847
+ this.broadcastFriendRequests(reqs);
3848
+ }));
3849
+ for (const evt of ["Buddy/onBuddyListChange", "Buddy/onBuddyListChangedV2"]) this.unsubscribes.push(this.friendChannel.on(evt, (arg) => {
3850
+ this.calibLogger?.info({ raw: JSON.stringify(arg)?.slice(0, 2e3) }, `ob11: ${evt} raw(friend_add 校准数据)`);
3851
+ }));
3852
+ }
3632
3853
  }
3633
3854
  /**
3634
3855
  * 仅启动接收链路(IPC 桥模式,2026-08-27):订阅消息通道维护 messageUnique +
@@ -3639,10 +3860,12 @@ var NapukettoOneBot11Adapter = class extends BaseProtocolAdapter {
3639
3860
  const config = await this.config.load();
3640
3861
  this.reportSelfMessage = config.reportSelfMessage;
3641
3862
  this.messageFormat = config.messagePostFormat;
3863
+ this.subscribedOnly = true;
3642
3864
  this.subscribe();
3643
3865
  }
3644
3866
  /** 仅退订(与 subscribeOnly 配对的进程级清理;传输关闭仍走 stop())。 */
3645
3867
  unsubscribeOnly() {
3868
+ this.subscribedOnly = false;
3646
3869
  this.unsubscribeAll();
3647
3870
  }
3648
3871
  /**
@@ -3669,21 +3892,57 @@ var NapukettoOneBot11Adapter = class extends BaseProtocolAdapter {
3669
3892
  if (uids.length > 0) uidToUin = await this.oneBotApi.uidToUin(uids);
3670
3893
  const notice = toOb11NoticeEvent(msg, {
3671
3894
  selfUin: this.selfUin,
3672
- uidToUin
3895
+ uidToUin,
3896
+ ...this.calibLogger !== void 0 ? { logger: this.calibLogger } : {}
3673
3897
  });
3674
3898
  if (notice !== null) this.broadcastEvent(notice);
3675
3899
  }
3900
+ /** 广播群系统通知 → OB11 group request 事件(批量 uidToUin 后翻译,纯函数)。 */
3901
+ async broadcastGroupRequests(notifies) {
3902
+ const uids = [];
3903
+ for (const n of notifies) {
3904
+ const notify = n;
3905
+ for (const u of [notify.user1, notify.user2]) if (typeof u?.uid === "string" && u.uid !== "") uids.push(u.uid);
3906
+ }
3907
+ let uidToUin = /* @__PURE__ */ new Map();
3908
+ if (uids.length > 0) try {
3909
+ uidToUin = await this.oneBotApi.uidToUin(uids);
3910
+ } catch {}
3911
+ const ctx = {
3912
+ selfUin: this.selfUin,
3913
+ uidToUin
3914
+ };
3915
+ for (const n of notifies) {
3916
+ const event = toOb11GroupRequestEvent(n, ctx);
3917
+ if (event !== null) this.broadcastEvent(event);
3918
+ }
3919
+ }
3920
+ /** 广播好友申请 → OB11 friend request 事件(批量 uidToUin 后翻译,纯函数)。 */
3921
+ async broadcastFriendRequests(reqs) {
3922
+ const uids = reqs.map((r) => r.friendUid).filter((uid) => uid !== "");
3923
+ let uidToUin = /* @__PURE__ */ new Map();
3924
+ if (uids.length > 0) try {
3925
+ uidToUin = await this.oneBotApi.uidToUin(uids);
3926
+ } catch {}
3927
+ const ctx = {
3928
+ selfUin: this.selfUin,
3929
+ uidToUin
3930
+ };
3931
+ for (const req of reqs) this.broadcastEvent(toOb11FriendRequestEvent(req, ctx));
3932
+ }
3676
3933
  /** 退订(幂等)。 */
3677
3934
  unsubscribeAll() {
3678
- this.unsubscribe?.();
3679
- this.unsubscribe = null;
3935
+ for (const off of this.unsubscribes) off();
3936
+ this.unsubscribes = [];
3680
3937
  }
3681
3938
  /**
3682
3939
  * 请求分发(挂到 network transport 的 onRequest):
3683
3940
  * OB11 标准请求 { action, params, echo } → 动作注册表 → handle(HTTP)/websocketHandle(WS)。
3684
3941
  */
3685
3942
  async handleRequest(req, respond) {
3686
- const { action: rawAction, params, echo } = req ?? {};
3943
+ const parsed = req ?? {};
3944
+ const { action: rawAction, echo } = parsed;
3945
+ const params = parsed.params ?? {};
3687
3946
  let action = "";
3688
3947
  if (typeof rawAction === "string") action = rawAction;
3689
3948
  if (action === "") {