@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.
- package/dist/onebot11/index.d.mts +33 -4
- package/dist/onebot11/index.d.mts.map +1 -1
- package/dist/onebot11/index.mjs +336 -77
- package/dist/onebot11/index.mjs.map +1 -1
- package/dist/{raw-message-DfHdOVHi.mjs → raw-message-Bx18pIQS.mjs} +2 -2
- package/dist/raw-message-Bx18pIQS.mjs.map +1 -0
- package/dist/satori/index.d.mts +5 -0
- package/dist/satori/index.d.mts.map +1 -1
- package/dist/satori/index.mjs +30 -6
- package/dist/satori/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/raw-message-DfHdOVHi.mjs.map +0 -1
package/dist/onebot11/index.mjs
CHANGED
|
@@ -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-
|
|
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$
|
|
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$
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
/**
|
|
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
|
|
2016
|
-
if (
|
|
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
|
|
2296
|
+
* get_image / get_record 动作:获取图片/语音文件(P2-14;2026-09-08 T5 接主动下载)
|
|
2160
2297
|
*
|
|
2161
|
-
* message_id 反查 → fetchMsgsByMsgId → 找 PIC/PTT
|
|
2162
|
-
*
|
|
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
|
-
/**
|
|
2184
|
-
function
|
|
2185
|
-
const
|
|
2186
|
-
if (
|
|
2187
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
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
|
-
|
|
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.
|
|
3622
|
-
this.
|
|
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.
|
|
3679
|
-
this.
|
|
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
|
|
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 === "") {
|