mioku 1.0.7 → 1.0.9
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/cli/index.cjs +3 -4
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +3 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/{exec-CeafLFmG.cjs → exec-BUUBQy_V.cjs} +2 -2
- package/dist/{exec-CeafLFmG.cjs.map → exec-BUUBQy_V.cjs.map} +1 -1
- package/dist/{exec-BxcCq8G9.js → exec-C3QTbHLt.js} +2 -2
- package/dist/{exec-BxcCq8G9.js.map → exec-C3QTbHLt.js.map} +1 -1
- package/dist/index.cjs +1220 -409
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +216 -25
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +216 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1214 -411
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_exec = require('./exec-
|
|
1
|
+
const require_exec = require('./exec-BUUBQy_V.cjs');
|
|
2
2
|
const node_fs = require_exec.__toESM(require("node:fs"));
|
|
3
3
|
const node_path = require_exec.__toESM(require("node:path"));
|
|
4
4
|
const node_url = require_exec.__toESM(require("node:url"));
|
|
@@ -855,6 +855,8 @@ var AdapterContextImpl = class {
|
|
|
855
855
|
#capabilities;
|
|
856
856
|
#logger;
|
|
857
857
|
#emit;
|
|
858
|
+
#correlator;
|
|
859
|
+
#commands;
|
|
858
860
|
#pendingStarts = new Set();
|
|
859
861
|
constructor(options) {
|
|
860
862
|
this.#state = options.state;
|
|
@@ -864,6 +866,8 @@ var AdapterContextImpl = class {
|
|
|
864
866
|
this.#capabilities = options.capabilities;
|
|
865
867
|
this.#logger = options.logger;
|
|
866
868
|
this.#emit = options.emit;
|
|
869
|
+
this.#correlator = options.correlator;
|
|
870
|
+
this.#commands = options.commands;
|
|
867
871
|
}
|
|
868
872
|
registerBot(bot) {
|
|
869
873
|
return this.#bots.register(bot);
|
|
@@ -927,6 +931,7 @@ var AdapterContextImpl = class {
|
|
|
927
931
|
};
|
|
928
932
|
}
|
|
929
933
|
async dispatch(event) {
|
|
934
|
+
this.#correlator?.observe(event);
|
|
930
935
|
if (event.kind === "message") {
|
|
931
936
|
const senderId = event.user_id;
|
|
932
937
|
if (senderId != null) {
|
|
@@ -937,6 +942,12 @@ var AdapterContextImpl = class {
|
|
|
937
942
|
}
|
|
938
943
|
}
|
|
939
944
|
}
|
|
945
|
+
if (event.kind === "message") {
|
|
946
|
+
const duplicate = this.#correlator?.observationOf(event)?.duplicate ?? false;
|
|
947
|
+
if (!duplicate) {
|
|
948
|
+
if (await this.#commands.dispatch(event)) return;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
940
951
|
await this.#bus.dispatch(event);
|
|
941
952
|
}
|
|
942
953
|
listen(route, handler) {
|
|
@@ -1015,6 +1026,9 @@ var BotRegistry = class {
|
|
|
1015
1026
|
for (const entry of this.#bots.values()) if (entry.bot_id === key) return entry.bot;
|
|
1016
1027
|
return void 0;
|
|
1017
1028
|
}
|
|
1029
|
+
find(adapter, bot_id) {
|
|
1030
|
+
return this.#bots.get(connectedBotKey(adapter, bot_id))?.bot;
|
|
1031
|
+
}
|
|
1018
1032
|
all() {
|
|
1019
1033
|
return Array.from(this.#bots.values()).map((entry) => entry.bot);
|
|
1020
1034
|
}
|
|
@@ -1049,9 +1063,13 @@ var EventBus = class {
|
|
|
1049
1063
|
#nextOrder = 0;
|
|
1050
1064
|
#dispatched = 0;
|
|
1051
1065
|
#logger = null;
|
|
1066
|
+
#filter = null;
|
|
1052
1067
|
setLogger(logger) {
|
|
1053
1068
|
this.#logger = logger;
|
|
1054
1069
|
}
|
|
1070
|
+
setFilter(filter$1) {
|
|
1071
|
+
this.#filter = filter$1;
|
|
1072
|
+
}
|
|
1055
1073
|
/** 注册事件监听,返回取消注册函数 */
|
|
1056
1074
|
register(route, handler, options = {}) {
|
|
1057
1075
|
if (!route) throw new Error("EventBus.register: route must be a non-empty string");
|
|
@@ -1085,6 +1103,11 @@ var EventBus = class {
|
|
|
1085
1103
|
const matched = [];
|
|
1086
1104
|
for (const reg of this.#registrations) {
|
|
1087
1105
|
if (reg.disposed) continue;
|
|
1106
|
+
if (this.#filter) try {
|
|
1107
|
+
if (!this.#filter(reg, event)) continue;
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
this.#logger?.("error", `Event registration filter failed for "${reg.source}"`, err);
|
|
1110
|
+
}
|
|
1088
1111
|
if (isWildcardMatch(reg.route, event.type)) {
|
|
1089
1112
|
matched.push(reg);
|
|
1090
1113
|
continue;
|
|
@@ -1123,6 +1146,57 @@ var EventBus = class {
|
|
|
1123
1146
|
}
|
|
1124
1147
|
};
|
|
1125
1148
|
|
|
1149
|
+
//#endregion
|
|
1150
|
+
//#region src/runtime/permissions.ts
|
|
1151
|
+
const isObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1152
|
+
/** 从事件对象中提取发送者 id(支持裸 id / user_id / sender.user_id) */
|
|
1153
|
+
const toUserId = (value) => {
|
|
1154
|
+
if (typeof value === "number" || typeof value === "string" || typeof value === "bigint") return String(value);
|
|
1155
|
+
if (isObject$1(value)) {
|
|
1156
|
+
if ("user_id" in value) return toUserId(value.user_id);
|
|
1157
|
+
if ("sender" in value) {
|
|
1158
|
+
const sender = value.sender;
|
|
1159
|
+
if (isObject$1(sender) && "user_id" in sender) return toUserId(sender.user_id);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return void 0;
|
|
1163
|
+
};
|
|
1164
|
+
/** 是否群消息 */
|
|
1165
|
+
const isGroupMessage = (event) => isObject$1(event) && event.kind === "message" && event.message_type === "group";
|
|
1166
|
+
/** 当前群群主(仅限所在群) */
|
|
1167
|
+
const isEventGroupOwner = (event) => {
|
|
1168
|
+
if (!isGroupMessage(event)) return false;
|
|
1169
|
+
return event.sender?.role === "owner";
|
|
1170
|
+
};
|
|
1171
|
+
/** 当前群群主或群管理员(仅限所在群) */
|
|
1172
|
+
const isEventGroupAdmin = (event) => {
|
|
1173
|
+
if (!isGroupMessage(event)) return false;
|
|
1174
|
+
const role = event.sender?.role;
|
|
1175
|
+
return role === "owner" || role === "admin";
|
|
1176
|
+
};
|
|
1177
|
+
/** 仅看 owners 名单:bot 主人(无群身份加成) */
|
|
1178
|
+
const isEventMaster$1 = (event) => {
|
|
1179
|
+
const id = toUserId(event);
|
|
1180
|
+
if (!id) return false;
|
|
1181
|
+
return isOwner(id);
|
|
1182
|
+
};
|
|
1183
|
+
/** 仅看 admins 名单:配置管理员 */
|
|
1184
|
+
const isEventAdminConfigOnly = (event) => {
|
|
1185
|
+
const id = toUserId(event);
|
|
1186
|
+
if (!id) return false;
|
|
1187
|
+
return isAdmin(id);
|
|
1188
|
+
};
|
|
1189
|
+
/**
|
|
1190
|
+
* master 或当前群群主
|
|
1191
|
+
*/
|
|
1192
|
+
const isEventOwner$1 = (event) => isEventMaster$1(event) || isEventGroupOwner(event);
|
|
1193
|
+
/**
|
|
1194
|
+
* master、配置管理员、当前群群主或群管理员
|
|
1195
|
+
*/
|
|
1196
|
+
const isEventAdmin$1 = (event) => isEventOwner$1(event) || isEventAdminConfigOnly(event) || isEventGroupAdmin(event);
|
|
1197
|
+
const isEventOwnerOrAdmin$1 = (event) => isEventAdmin$1(event);
|
|
1198
|
+
const hasEventRight$1 = (event) => isEventOwnerOrAdmin$1(event);
|
|
1199
|
+
|
|
1126
1200
|
//#endregion
|
|
1127
1201
|
//#region src/utils.ts
|
|
1128
1202
|
const ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0";
|
|
@@ -1889,130 +1963,19 @@ const Services = {
|
|
|
1889
1963
|
};
|
|
1890
1964
|
|
|
1891
1965
|
//#endregion
|
|
1892
|
-
//#region src/runtime/
|
|
1893
|
-
|
|
1894
|
-
const
|
|
1895
|
-
const EVENT_TTL_MS = 6e4;
|
|
1896
|
-
const MAX_SIZE = 4096;
|
|
1897
|
-
const MAX_SEGMENTS = 16;
|
|
1898
|
-
const MAX_TEXT = 256;
|
|
1899
|
-
const contentFingerprintOf = (event) => {
|
|
1900
|
-
const parts = [];
|
|
1901
|
-
for (const seg of event.message) {
|
|
1902
|
-
if (parts.length >= MAX_SEGMENTS) break;
|
|
1903
|
-
if (seg.type === "reply") continue;
|
|
1904
|
-
const data = seg.data ?? {};
|
|
1905
|
-
if (seg.type === "text") {
|
|
1906
|
-
const text$1 = typeof data.text === "string" ? data.text : "";
|
|
1907
|
-
parts.push(`t:${text$1.slice(0, MAX_TEXT)}`);
|
|
1908
|
-
} else if (seg.type === "at") {
|
|
1909
|
-
const target = data.qq ?? data.target;
|
|
1910
|
-
parts.push(`a:${target == null ? "" : String(target)}`);
|
|
1911
|
-
} else if (seg.type === "face") parts.push("f");
|
|
1912
|
-
else parts.push(seg.type);
|
|
1913
|
-
}
|
|
1914
|
-
return parts.join("|");
|
|
1915
|
-
};
|
|
1916
|
-
const scopeKeyOf = (event, content) => [
|
|
1917
|
-
event.message_type ?? "",
|
|
1918
|
-
event.sender?.nickname?.trim() || event.user_id || "",
|
|
1919
|
-
content
|
|
1920
|
-
].join("|");
|
|
1921
|
-
const noticeKeyOf = (event) => [
|
|
1922
|
-
event.kind,
|
|
1923
|
-
event.identity.event_type,
|
|
1924
|
-
event.notice_type ?? "",
|
|
1925
|
-
event.sub_type ?? "",
|
|
1926
|
-
event.group_id ?? "",
|
|
1927
|
-
event.user_id ?? "",
|
|
1928
|
-
event.operator_id ?? "",
|
|
1929
|
-
event.identity.fingerprint ?? "",
|
|
1930
|
-
event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
|
|
1931
|
-
].join("|");
|
|
1932
|
-
const requestKeyOf = (event) => [
|
|
1933
|
-
event.kind,
|
|
1934
|
-
event.identity.event_type,
|
|
1935
|
-
event.request_type ?? "",
|
|
1936
|
-
event.sub_type ?? "",
|
|
1937
|
-
event.group_id ?? "",
|
|
1938
|
-
event.user_id ?? "",
|
|
1939
|
-
event.comment ?? "",
|
|
1940
|
-
event.identity.fingerprint ?? "",
|
|
1941
|
-
event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
|
|
1942
|
-
].join("|");
|
|
1966
|
+
//#region src/runtime/mioku-context.ts
|
|
1967
|
+
/** 仅看 owners 名单:bot 主人(无群身份加成) */
|
|
1968
|
+
const isEventMaster = (event) => isEventMaster$1(event);
|
|
1943
1969
|
/**
|
|
1944
|
-
*
|
|
1970
|
+
* master 或当前群群主。等价于 `ctx.command({ permission: "owner" })` 的判断。
|
|
1945
1971
|
*/
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
}
|
|
1954
|
-
isDuplicate(event) {
|
|
1955
|
-
let key;
|
|
1956
|
-
let ttl;
|
|
1957
|
-
if (event.kind === "message") {
|
|
1958
|
-
key = scopeKeyOf(event, contentFingerprintOf(event));
|
|
1959
|
-
ttl = MESSAGE_TTL_MS;
|
|
1960
|
-
} else if (event.kind === "notice") {
|
|
1961
|
-
key = noticeKeyOf(event);
|
|
1962
|
-
ttl = EVENT_TTL_MS;
|
|
1963
|
-
} else if (event.kind === "request") {
|
|
1964
|
-
key = requestKeyOf(event);
|
|
1965
|
-
ttl = EVENT_TTL_MS;
|
|
1966
|
-
} else return false;
|
|
1967
|
-
const now = Date.now();
|
|
1968
|
-
const existing = this.#entries.get(key);
|
|
1969
|
-
if (existing?.expiresAt != null && existing.expiresAt >= now) {
|
|
1970
|
-
this.#dropped++;
|
|
1971
|
-
return true;
|
|
1972
|
-
}
|
|
1973
|
-
this.#entries.set(key, { expiresAt: now + ttl });
|
|
1974
|
-
this.#inserts++;
|
|
1975
|
-
if (this.#inserts >= PRUNE_INTERVAL || this.#entries.size > MAX_SIZE) {
|
|
1976
|
-
this.#inserts = 0;
|
|
1977
|
-
this.#prune(now);
|
|
1978
|
-
}
|
|
1979
|
-
return false;
|
|
1980
|
-
}
|
|
1981
|
-
#prune(now) {
|
|
1982
|
-
for (const [key, entry] of this.#entries) if (entry.expiresAt < now) this.#entries.delete(key);
|
|
1983
|
-
}
|
|
1984
|
-
clear() {
|
|
1985
|
-
this.#entries.clear();
|
|
1986
|
-
}
|
|
1987
|
-
};
|
|
1988
|
-
|
|
1989
|
-
//#endregion
|
|
1990
|
-
//#region src/runtime/mioku-context.ts
|
|
1991
|
-
const isObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1992
|
-
/** 从事件对象中提取发送者 id(支持裸 id / user_id / sender.user_id) */
|
|
1993
|
-
const toUserId = (value) => {
|
|
1994
|
-
if (typeof value === "number" || typeof value === "string" || typeof value === "bigint") return String(value);
|
|
1995
|
-
if (isObject$1(value)) {
|
|
1996
|
-
if ("user_id" in value) return toUserId(value.user_id);
|
|
1997
|
-
if ("sender" in value) {
|
|
1998
|
-
const sender = value.sender;
|
|
1999
|
-
if (isObject$1(sender) && "user_id" in sender) return toUserId(sender.user_id);
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
return void 0;
|
|
2003
|
-
};
|
|
2004
|
-
const isEventOwner = (event) => {
|
|
2005
|
-
const id = toUserId(event);
|
|
2006
|
-
if (!id) return false;
|
|
2007
|
-
return isOwner(id);
|
|
2008
|
-
};
|
|
2009
|
-
const isEventAdmin = (event) => {
|
|
2010
|
-
const id = toUserId(event);
|
|
2011
|
-
if (!id) return false;
|
|
2012
|
-
return isAdmin(id);
|
|
2013
|
-
};
|
|
2014
|
-
const isEventOwnerOrAdmin = (event) => isEventOwner(event) || isEventAdmin(event);
|
|
2015
|
-
const hasEventRight = (event) => isEventOwnerOrAdmin(event);
|
|
1972
|
+
const isEventOwner = (event) => isEventOwner$1(event);
|
|
1973
|
+
/**
|
|
1974
|
+
* master、配置管理员、当前群群主或群管理员。等价于 `ctx.command({ permission: "admin" })` 的判断。
|
|
1975
|
+
*/
|
|
1976
|
+
const isEventAdmin = (event) => isEventAdmin$1(event);
|
|
1977
|
+
const isEventOwnerOrAdmin = (event) => isEventOwnerOrAdmin$1(event);
|
|
1978
|
+
const hasEventRight = (event) => hasEventRight$1(event);
|
|
2016
1979
|
const CTX_UTILS = {
|
|
2017
1980
|
localeDate,
|
|
2018
1981
|
localeTime,
|
|
@@ -2067,6 +2030,9 @@ var MiokuContext = class {
|
|
|
2067
2030
|
get pluginName() {
|
|
2068
2031
|
return this.#options.pluginName;
|
|
2069
2032
|
}
|
|
2033
|
+
get pluginId() {
|
|
2034
|
+
return this.#options.pluginId;
|
|
2035
|
+
}
|
|
2070
2036
|
/** 第一个已连接的 bot(多数单 bot 场景直接用这个) */
|
|
2071
2037
|
get bot() {
|
|
2072
2038
|
return this.#options.bots.all()[0];
|
|
@@ -2083,6 +2049,59 @@ var MiokuContext = class {
|
|
|
2083
2049
|
return this.#options.bots.pick(bot_id);
|
|
2084
2050
|
}
|
|
2085
2051
|
/**
|
|
2052
|
+
* 取事件所属的跨适配器关联记录。
|
|
2053
|
+
* 同一条逻辑消息被多个适配器/bot 投递时,可从这里读到全部参与方。
|
|
2054
|
+
*/
|
|
2055
|
+
correlation(event) {
|
|
2056
|
+
return this.#options.correlator?.observationOf(event)?.record;
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* 观察到该事件的全部已连接 bot(首个即 primary)。
|
|
2060
|
+
* 跨适配器去重后,被标记为重复的投递不会进入普通 handler,
|
|
2061
|
+
* 但这里依然能看到全部参与方。
|
|
2062
|
+
*/
|
|
2063
|
+
botsForEvent(event) {
|
|
2064
|
+
const record = this.correlation(event);
|
|
2065
|
+
if (!record) {
|
|
2066
|
+
const bot = event.bot;
|
|
2067
|
+
return bot ? [bot] : [];
|
|
2068
|
+
}
|
|
2069
|
+
const resolved = [];
|
|
2070
|
+
for (const participant of record.participants) {
|
|
2071
|
+
const bot = participant.botId ? this.#options.bots.find(participant.adapter, participant.botId) ?? this.#options.bots.pick(participant.botId) : void 0;
|
|
2072
|
+
if (bot && !resolved.includes(bot)) resolved.push(bot);
|
|
2073
|
+
}
|
|
2074
|
+
return resolved;
|
|
2075
|
+
}
|
|
2076
|
+
/** 事件里被 @ 到的、且本运行时已连接的 bot */
|
|
2077
|
+
mentionedBots(event) {
|
|
2078
|
+
if (event.kind !== "message") return [];
|
|
2079
|
+
const targets = new Set();
|
|
2080
|
+
for (const segment$1 of event.message) {
|
|
2081
|
+
if (segment$1.type !== "at") continue;
|
|
2082
|
+
const data = segment$1.data ?? {};
|
|
2083
|
+
const target = data.qq ?? data.target;
|
|
2084
|
+
if (target != null && String(target) !== "") targets.add(String(target));
|
|
2085
|
+
}
|
|
2086
|
+
if (targets.size === 0) return [];
|
|
2087
|
+
return this.#options.bots.all().filter((bot) => targets.has(String(bot.bot_id)));
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* 这条消息应当由哪个 bot 回应:优先「被 @ 的 bot」,其次是关联组里首个到达的 bot。
|
|
2091
|
+
* 插件可用它替代隐式的 `event.bot`,从而不受适配器到达顺序影响。
|
|
2092
|
+
*/
|
|
2093
|
+
pickReplyBot(event) {
|
|
2094
|
+
const mentioned = this.mentionedBots(event);
|
|
2095
|
+
if (mentioned.length > 0) return mentioned[0];
|
|
2096
|
+
const participants = this.botsForEvent(event);
|
|
2097
|
+
if (participants.length > 0) return participants[0];
|
|
2098
|
+
return event.bot;
|
|
2099
|
+
}
|
|
2100
|
+
/** 事件关联/去重的运行统计 */
|
|
2101
|
+
correlationStats() {
|
|
2102
|
+
return this.#options.correlator?.stats();
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2086
2105
|
* 获取事件引用回复的消息内容。
|
|
2087
2106
|
* 返回 null 表示没有引用、拿不到对应 bot 或消息已失效。
|
|
2088
2107
|
*/
|
|
@@ -2113,12 +2132,12 @@ var MiokuContext = class {
|
|
|
2113
2132
|
const inputRoutes = Array.isArray(route) ? route : [route];
|
|
2114
2133
|
const bypassDedup = inputRoutes.some((item) => item.startsWith("!"));
|
|
2115
2134
|
const routes = inputRoutes.map((item) => item.startsWith("!") ? item.slice(1) : item);
|
|
2116
|
-
const source = `plugin:${this.#options.
|
|
2135
|
+
const source = `plugin:${this.#options.pluginId}`;
|
|
2117
2136
|
const handledEvents = new WeakSet();
|
|
2118
|
-
const
|
|
2137
|
+
const correlator = bypassDedup ? void 0 : this.#options.correlator;
|
|
2119
2138
|
const wrappedHandler = async (event) => {
|
|
2120
2139
|
if (handledEvents.has(event)) return;
|
|
2121
|
-
if (
|
|
2140
|
+
if (correlator?.isDuplicate(event)) return;
|
|
2122
2141
|
handledEvents.add(event);
|
|
2123
2142
|
await handler(event);
|
|
2124
2143
|
};
|
|
@@ -2135,6 +2154,17 @@ var MiokuContext = class {
|
|
|
2135
2154
|
this.#addCleanup(dispose);
|
|
2136
2155
|
return dispose;
|
|
2137
2156
|
}
|
|
2157
|
+
command(definitionOrMatcher, handler, options = {}) {
|
|
2158
|
+
const dispose = typeof definitionOrMatcher === "object" && definitionOrMatcher !== null && "name" in definitionOrMatcher ? this.#options.commands.register(this.#options.pluginId, {
|
|
2159
|
+
...definitionOrMatcher,
|
|
2160
|
+
priority: definitionOrMatcher.priority ?? this.#options.priority
|
|
2161
|
+
}, this) : this.#options.commands.register(this.#options.pluginId, definitionOrMatcher, handler, {
|
|
2162
|
+
...options,
|
|
2163
|
+
priority: options.priority ?? this.#options.priority
|
|
2164
|
+
}, this);
|
|
2165
|
+
this.#addCleanup(dispose);
|
|
2166
|
+
return dispose;
|
|
2167
|
+
}
|
|
2138
2168
|
/** 注册 cron 定时任务,插件卸载时自动停止 */
|
|
2139
2169
|
cron(expression, handler) {
|
|
2140
2170
|
const task = node_cron.default.schedule(expression, async (taskContext) => {
|
|
@@ -2191,6 +2221,10 @@ var MiokuContext = class {
|
|
|
2191
2221
|
get buses() {
|
|
2192
2222
|
return this.#options.bus;
|
|
2193
2223
|
}
|
|
2224
|
+
/** 当前运行时的命令管理器 */
|
|
2225
|
+
get commands() {
|
|
2226
|
+
return this.#options.commands;
|
|
2227
|
+
}
|
|
2194
2228
|
get segment() {
|
|
2195
2229
|
return segment;
|
|
2196
2230
|
}
|
|
@@ -2231,24 +2265,30 @@ var MiokuContext = class {
|
|
|
2231
2265
|
return noticeAdmins(this.#options.bots.all(), this.#options.config.admins, message, options);
|
|
2232
2266
|
}
|
|
2233
2267
|
isGroupMsg(event) {
|
|
2234
|
-
return isObject
|
|
2268
|
+
return isObject(event) && event.kind === "message" && event.message_type === "group";
|
|
2235
2269
|
}
|
|
2236
2270
|
isPrivateMsg(event) {
|
|
2237
|
-
return isObject
|
|
2271
|
+
return isObject(event) && event.kind === "message" && event.message_type === "private";
|
|
2272
|
+
}
|
|
2273
|
+
/** 事件发送者是否为 bot 主人(仅看 owners 名单,不识别群身份) */
|
|
2274
|
+
isMaster(event) {
|
|
2275
|
+
return isEventMaster(event);
|
|
2238
2276
|
}
|
|
2239
|
-
/**
|
|
2277
|
+
/** 事件发送者是否为 bot 主人或当前群群主;等价于 `permission: "owner"` */
|
|
2240
2278
|
isOwner(event) {
|
|
2241
2279
|
return isEventOwner(event);
|
|
2242
2280
|
}
|
|
2243
|
-
/**
|
|
2281
|
+
/** 事件发送者是否为 bot 主人、配置管理员、当前群群主或群管理员;等价于 `permission: "admin"` */
|
|
2244
2282
|
isAdmin(event) {
|
|
2245
2283
|
return isEventAdmin(event);
|
|
2246
2284
|
}
|
|
2285
|
+
/** 同 `isAdmin`,保留旧名称 */
|
|
2247
2286
|
isOwnerOrAdmin(event) {
|
|
2248
|
-
return
|
|
2287
|
+
return isEventAdmin(event);
|
|
2249
2288
|
}
|
|
2289
|
+
/** 同 `isAdmin`,别名 */
|
|
2250
2290
|
hasRight(event) {
|
|
2251
|
-
return
|
|
2291
|
+
return isEventAdmin(event);
|
|
2252
2292
|
}
|
|
2253
2293
|
async dispose() {
|
|
2254
2294
|
const all = Array.from(this.#cleanup);
|
|
@@ -2259,19 +2299,667 @@ var MiokuContext = class {
|
|
|
2259
2299
|
}
|
|
2260
2300
|
};
|
|
2261
2301
|
|
|
2302
|
+
//#endregion
|
|
2303
|
+
//#region src/runtime/event-correlator.ts
|
|
2304
|
+
const MESSAGE_TTL_MS = 15e3;
|
|
2305
|
+
const EVENT_TTL_MS = 6e4;
|
|
2306
|
+
const PRUNE_INTERVAL = 128;
|
|
2307
|
+
const MAX_SIZE = 4096;
|
|
2308
|
+
const MAX_SEGMENTS = 16;
|
|
2309
|
+
const MAX_TEXT = 256;
|
|
2310
|
+
const HEX32 = /[0-9a-f]{32}/i;
|
|
2311
|
+
const HEX40 = /[0-9a-f]{40}/i;
|
|
2312
|
+
const MEDIA_URL_QUERY_KEYS = [
|
|
2313
|
+
"fileid",
|
|
2314
|
+
"file_id",
|
|
2315
|
+
"md5",
|
|
2316
|
+
"fid"
|
|
2317
|
+
];
|
|
2318
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2319
|
+
const hashString = (input) => (0, node_crypto.createHash)("sha1").update(input).digest("hex").slice(0, 20);
|
|
2320
|
+
/** 稳定序列化:对象键排序、无多余空白,忽略跨适配器的排版差异 */
|
|
2321
|
+
const canonicalJson = (value) => {
|
|
2322
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
2323
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
2324
|
+
const obj = value;
|
|
2325
|
+
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(obj[key])}`).join(",")}}`;
|
|
2326
|
+
};
|
|
2327
|
+
/** 结构化载荷(json/xml/ark)的稳定摘要 */
|
|
2328
|
+
const structuredDigest = (raw) => {
|
|
2329
|
+
let value = raw;
|
|
2330
|
+
if (typeof raw === "string") {
|
|
2331
|
+
const trimmed = raw.trim();
|
|
2332
|
+
if (!trimmed) return "";
|
|
2333
|
+
try {
|
|
2334
|
+
value = JSON.parse(trimmed);
|
|
2335
|
+
} catch {
|
|
2336
|
+
return `s:${hashString(trimmed.slice(0, MAX_TEXT * 4))}`;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return `j:${hashString(canonicalJson(value))}`;
|
|
2340
|
+
};
|
|
2341
|
+
/** 去掉会过期/随会话变化的查询参数,保留跨适配器稳定的媒体定位 */
|
|
2342
|
+
const normalizeMediaUrl = (raw) => {
|
|
2343
|
+
try {
|
|
2344
|
+
const url = new URL(raw);
|
|
2345
|
+
const kept = new URLSearchParams();
|
|
2346
|
+
for (const key of MEDIA_URL_QUERY_KEYS) {
|
|
2347
|
+
const value = url.searchParams.get(key);
|
|
2348
|
+
if (value) kept.set(key, value);
|
|
2349
|
+
}
|
|
2350
|
+
const query = kept.toString();
|
|
2351
|
+
return `${url.host}${url.pathname}${query ? `?${query}` : ""}`;
|
|
2352
|
+
} catch {
|
|
2353
|
+
return raw;
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
const firstHex = (value, pattern) => {
|
|
2357
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
2358
|
+
const match$1 = value.match(pattern);
|
|
2359
|
+
return match$1 ? match$1[0].toLowerCase() : void 0;
|
|
2360
|
+
};
|
|
2361
|
+
/**
|
|
2362
|
+
* 媒体段的内容摘要:优先内容哈希(md5/sha1),其次是文件名里嵌的 md5,
|
|
2363
|
+
* 再次是规范化 URL。都拿不到时返回空串,由调用方退化为「只看段类型」。
|
|
2364
|
+
*/
|
|
2365
|
+
const mediaDigest = (data) => {
|
|
2366
|
+
const md5$1 = firstHex(data.md5, HEX32);
|
|
2367
|
+
if (md5$1) return `h:${md5$1}`;
|
|
2368
|
+
const sha1 = firstHex(data.sha1, HEX40);
|
|
2369
|
+
if (sha1) return `h:${sha1}`;
|
|
2370
|
+
for (const key of [
|
|
2371
|
+
"file_unique",
|
|
2372
|
+
"file_id",
|
|
2373
|
+
"fid",
|
|
2374
|
+
"id",
|
|
2375
|
+
"file",
|
|
2376
|
+
"name"
|
|
2377
|
+
]) {
|
|
2378
|
+
const hex = firstHex(data[key], HEX32);
|
|
2379
|
+
if (hex) return `h:${hex}`;
|
|
2380
|
+
}
|
|
2381
|
+
const url = data.url;
|
|
2382
|
+
if (typeof url === "string" && url) return `u:${normalizeMediaUrl(url)}`;
|
|
2383
|
+
for (const key of [
|
|
2384
|
+
"file",
|
|
2385
|
+
"file_id",
|
|
2386
|
+
"fid",
|
|
2387
|
+
"id",
|
|
2388
|
+
"name"
|
|
2389
|
+
]) {
|
|
2390
|
+
const value = data[key];
|
|
2391
|
+
if (typeof value === "string" && value) return `r:${value.toLowerCase()}`;
|
|
2392
|
+
}
|
|
2393
|
+
return "";
|
|
2394
|
+
};
|
|
2395
|
+
/** 只在载荷跨协议一致时才使用摘要的段类型 */
|
|
2396
|
+
const DIGEST_MEDIA_TYPES = new Set(["image", "flash"]);
|
|
2397
|
+
const segmentSignature = (segment$1) => {
|
|
2398
|
+
const data = isRecord(segment$1.data) ? segment$1.data : {};
|
|
2399
|
+
switch (segment$1.type) {
|
|
2400
|
+
case "text": {
|
|
2401
|
+
const text$1 = typeof data.text === "string" ? data.text : "";
|
|
2402
|
+
return `t:${text$1.slice(0, MAX_TEXT)}`;
|
|
2403
|
+
}
|
|
2404
|
+
case "at": {
|
|
2405
|
+
const target = data.qq ?? data.target;
|
|
2406
|
+
return `a:${target == null ? "" : String(target)}`;
|
|
2407
|
+
}
|
|
2408
|
+
case "face": {
|
|
2409
|
+
const id = data.id ?? data.face;
|
|
2410
|
+
return `f:${id == null ? "" : String(id)}`;
|
|
2411
|
+
}
|
|
2412
|
+
case "json":
|
|
2413
|
+
case "xml":
|
|
2414
|
+
case "ark": {
|
|
2415
|
+
const digest = structuredDigest(data.data ?? data.content ?? data);
|
|
2416
|
+
return digest ? `${segment$1.type}:${digest}` : segment$1.type;
|
|
2417
|
+
}
|
|
2418
|
+
default: {
|
|
2419
|
+
if (DIGEST_MEDIA_TYPES.has(segment$1.type)) {
|
|
2420
|
+
const digest = mediaDigest(data);
|
|
2421
|
+
return digest ? `${segment$1.type}:${digest}` : segment$1.type;
|
|
2422
|
+
}
|
|
2423
|
+
return segment$1.type;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
const contentSignatureOf = (event) => {
|
|
2428
|
+
const parts = [];
|
|
2429
|
+
for (const segment$1 of event.message) {
|
|
2430
|
+
if (parts.length >= MAX_SEGMENTS) break;
|
|
2431
|
+
if (segment$1.type === "reply") continue;
|
|
2432
|
+
parts.push(segmentSignature(segment$1));
|
|
2433
|
+
}
|
|
2434
|
+
return parts.join("|");
|
|
2435
|
+
};
|
|
2436
|
+
const messageKeyOf = (event) => {
|
|
2437
|
+
const type = event.message_type ?? "";
|
|
2438
|
+
const scope = event.group_id ?? event.user_id ?? "";
|
|
2439
|
+
const sender = event.user_id ?? event.sender?.user_id ?? "";
|
|
2440
|
+
return [
|
|
2441
|
+
"m",
|
|
2442
|
+
type,
|
|
2443
|
+
scope,
|
|
2444
|
+
sender,
|
|
2445
|
+
contentSignatureOf(event)
|
|
2446
|
+
].join("|");
|
|
2447
|
+
};
|
|
2448
|
+
const noticeKeyOf = (event) => [
|
|
2449
|
+
"n",
|
|
2450
|
+
event.identity.event_type,
|
|
2451
|
+
event.notice_type ?? "",
|
|
2452
|
+
event.sub_type ?? "",
|
|
2453
|
+
event.group_id ?? "",
|
|
2454
|
+
event.user_id ?? "",
|
|
2455
|
+
event.operator_id ?? "",
|
|
2456
|
+
event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
|
|
2457
|
+
].join("|");
|
|
2458
|
+
const requestKeyOf = (event) => [
|
|
2459
|
+
"r",
|
|
2460
|
+
event.identity.event_type,
|
|
2461
|
+
event.request_type ?? "",
|
|
2462
|
+
event.sub_type ?? "",
|
|
2463
|
+
event.group_id ?? "",
|
|
2464
|
+
event.user_id ?? "",
|
|
2465
|
+
event.comment ?? "",
|
|
2466
|
+
event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
|
|
2467
|
+
].join("|");
|
|
2468
|
+
const participantOf = (event) => ({
|
|
2469
|
+
adapter: event.identity.adapter,
|
|
2470
|
+
botId: event.identity.bot_id,
|
|
2471
|
+
eventType: event.identity.event_type,
|
|
2472
|
+
messageId: event.identity.message_id ?? event.identity.native_event_id
|
|
2473
|
+
});
|
|
2474
|
+
const sameParticipant = (a, b) => a.adapter === b.adapter && (a.botId ?? "") === (b.botId ?? "") && (a.messageId ?? "") === (b.messageId ?? "");
|
|
2475
|
+
/**
|
|
2476
|
+
* 跨适配器事件关联器
|
|
2477
|
+
*/
|
|
2478
|
+
var EventCorrelator = class {
|
|
2479
|
+
#groups = new Map();
|
|
2480
|
+
#byEvent = new WeakMap();
|
|
2481
|
+
#logger;
|
|
2482
|
+
#maxSize;
|
|
2483
|
+
#inserts = 0;
|
|
2484
|
+
#observed = 0;
|
|
2485
|
+
#duplicates = 0;
|
|
2486
|
+
#evicted = 0;
|
|
2487
|
+
constructor(options = {}) {
|
|
2488
|
+
this.#logger = options.logger;
|
|
2489
|
+
this.#maxSize = options.maxSize ?? MAX_SIZE;
|
|
2490
|
+
}
|
|
2491
|
+
/** 观察一个事件,登记它所属的关联组并返回其定位(同一事件只登记一次) */
|
|
2492
|
+
observe(event) {
|
|
2493
|
+
const cached = this.#byEvent.get(event);
|
|
2494
|
+
if (cached) return cached;
|
|
2495
|
+
const located = this.#locate(event);
|
|
2496
|
+
if (!located) return null;
|
|
2497
|
+
const { key, ttl } = located;
|
|
2498
|
+
const now = Date.now();
|
|
2499
|
+
this.#observed++;
|
|
2500
|
+
let group = this.#groups.get(key);
|
|
2501
|
+
let duplicate = false;
|
|
2502
|
+
if (group && group.expiresAt >= now) {
|
|
2503
|
+
duplicate = true;
|
|
2504
|
+
this.#duplicates++;
|
|
2505
|
+
const participant = participantOf(event);
|
|
2506
|
+
if (!group.participants.some((item) => sameParticipant(item, participant))) group.participants.push(participant);
|
|
2507
|
+
this.#logger?.debug(`去重命中: key=${key} adapter=${participant.adapter} bot=${participant.botId ?? ""} primary=${group.primary.adapter}:${group.primary.botId ?? ""}`);
|
|
2508
|
+
} else {
|
|
2509
|
+
const participant = participantOf(event);
|
|
2510
|
+
group = {
|
|
2511
|
+
key,
|
|
2512
|
+
expiresAt: now + ttl,
|
|
2513
|
+
firstSeenAt: now,
|
|
2514
|
+
primary: participant,
|
|
2515
|
+
participants: [participant]
|
|
2516
|
+
};
|
|
2517
|
+
this.#groups.set(key, group);
|
|
2518
|
+
this.#inserts++;
|
|
2519
|
+
if (this.#inserts >= PRUNE_INTERVAL || this.#groups.size > this.#maxSize) {
|
|
2520
|
+
this.#inserts = 0;
|
|
2521
|
+
this.#prune(now);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
const observation = {
|
|
2525
|
+
record: group,
|
|
2526
|
+
primary: !duplicate,
|
|
2527
|
+
duplicate
|
|
2528
|
+
};
|
|
2529
|
+
this.#byEvent.set(event, observation);
|
|
2530
|
+
return observation;
|
|
2531
|
+
}
|
|
2532
|
+
/** 读取某事件此前登记的关联定位(未登记或已过期时返回 undefined) */
|
|
2533
|
+
observationOf(event) {
|
|
2534
|
+
const observation = this.#byEvent.get(event);
|
|
2535
|
+
if (!observation) return void 0;
|
|
2536
|
+
if (observation.record.expiresAt < Date.now()) return void 0;
|
|
2537
|
+
return observation;
|
|
2538
|
+
}
|
|
2539
|
+
/** 该事件是否属于关联组里后续到达的重复投递(未登记时会先登记) */
|
|
2540
|
+
isDuplicate(event) {
|
|
2541
|
+
return this.observe(event)?.duplicate === true;
|
|
2542
|
+
}
|
|
2543
|
+
stats() {
|
|
2544
|
+
return {
|
|
2545
|
+
groups: this.#groups.size,
|
|
2546
|
+
observed: this.#observed,
|
|
2547
|
+
duplicates: this.#duplicates,
|
|
2548
|
+
evicted: this.#evicted
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
clear() {
|
|
2552
|
+
this.#groups.clear();
|
|
2553
|
+
}
|
|
2554
|
+
#locate(event) {
|
|
2555
|
+
if (event.kind === "message") return {
|
|
2556
|
+
key: messageKeyOf(event),
|
|
2557
|
+
ttl: MESSAGE_TTL_MS
|
|
2558
|
+
};
|
|
2559
|
+
if (event.kind === "notice") return {
|
|
2560
|
+
key: noticeKeyOf(event),
|
|
2561
|
+
ttl: EVENT_TTL_MS
|
|
2562
|
+
};
|
|
2563
|
+
if (event.kind === "request") return {
|
|
2564
|
+
key: requestKeyOf(event),
|
|
2565
|
+
ttl: EVENT_TTL_MS
|
|
2566
|
+
};
|
|
2567
|
+
return null;
|
|
2568
|
+
}
|
|
2569
|
+
#prune(now) {
|
|
2570
|
+
for (const [key, group] of this.#groups) if (group.expiresAt < now) this.#groups.delete(key);
|
|
2571
|
+
if (this.#groups.size <= this.#maxSize) return;
|
|
2572
|
+
const drop = this.#groups.size - this.#maxSize;
|
|
2573
|
+
let dropped = 0;
|
|
2574
|
+
for (const key of this.#groups.keys()) {
|
|
2575
|
+
this.#groups.delete(key);
|
|
2576
|
+
this.#evicted++;
|
|
2577
|
+
if (++dropped >= drop) break;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
};
|
|
2581
|
+
|
|
2582
|
+
//#endregion
|
|
2583
|
+
//#region src/runtime/commands.ts
|
|
2584
|
+
const normalizeRole = (role) => {
|
|
2585
|
+
const value = String(role ?? "member").trim().toLowerCase();
|
|
2586
|
+
if (value === "master" || value === "owner" || value === "admin") return value;
|
|
2587
|
+
return "member";
|
|
2588
|
+
};
|
|
2589
|
+
const userIdOf = (event) => toUserId(event);
|
|
2590
|
+
const textHookMatches = (text$1, raw) => {
|
|
2591
|
+
const source = String(raw ?? "").trim();
|
|
2592
|
+
if (!source) return false;
|
|
2593
|
+
if (source.startsWith("/") && source.length > 1) {
|
|
2594
|
+
const end = source.lastIndexOf("/");
|
|
2595
|
+
if (end > 0) try {
|
|
2596
|
+
const flags = source.slice(end + 1);
|
|
2597
|
+
const regex = new RegExp(source.slice(1, end), flags);
|
|
2598
|
+
return regex.test(text$1);
|
|
2599
|
+
} catch {
|
|
2600
|
+
return false;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
return text$1 === source || text$1.startsWith(source);
|
|
2604
|
+
};
|
|
2605
|
+
const routeMatches = (event, route) => {
|
|
2606
|
+
if (!route) return false;
|
|
2607
|
+
return event.type === route || event.routes.some((item) => item === route);
|
|
2608
|
+
};
|
|
2609
|
+
const parsePrefixes = (prefixes, defaultPrefix) => {
|
|
2610
|
+
if (prefixes === false) return ["", defaultPrefix];
|
|
2611
|
+
const values = Array.isArray(prefixes) ? prefixes : [prefixes];
|
|
2612
|
+
const normalized = values.map((value) => String(value)).filter((value, index, all) => all.indexOf(value) === index);
|
|
2613
|
+
return normalized.length > 0 ? normalized : [defaultPrefix];
|
|
2614
|
+
};
|
|
2615
|
+
const commandBody = (text$1, prefixes) => {
|
|
2616
|
+
const prefix = [...prefixes].sort((a, b) => b.length - a.length).find((item) => item === "" || text$1.startsWith(item));
|
|
2617
|
+
if (prefix === void 0) return void 0;
|
|
2618
|
+
return text$1.slice(prefix.length).trimStart();
|
|
2619
|
+
};
|
|
2620
|
+
const stringMatch = (body, matcher) => {
|
|
2621
|
+
const value = matcher.trim();
|
|
2622
|
+
if (!value) return void 0;
|
|
2623
|
+
if (body !== value && !(body.startsWith(value) && /\s/.test(body[value.length] ?? ""))) return void 0;
|
|
2624
|
+
const rest = body.slice(value.length).trim();
|
|
2625
|
+
return { args: rest ? rest.split(/\s+/) : [] };
|
|
2626
|
+
};
|
|
2627
|
+
const commandKey$1 = (value) => String(value).trim().replace(/^[#/.]+\s*/, "").split(/\s+/)[0].toLowerCase();
|
|
2628
|
+
const displayCommand = (command, defaultPrefix) => {
|
|
2629
|
+
if (command.prefixes === false) return command.name;
|
|
2630
|
+
const prefixes = command.prefixes === void 0 ? [defaultPrefix] : Array.isArray(command.prefixes) ? command.prefixes : [command.prefixes];
|
|
2631
|
+
return `${prefixes.find((prefix) => prefix.length > 0) ?? ""}${command.name}`;
|
|
2632
|
+
};
|
|
2633
|
+
var CommandManager = class {
|
|
2634
|
+
#getDefaultPrefix;
|
|
2635
|
+
#getOwners;
|
|
2636
|
+
#getAdmins;
|
|
2637
|
+
#logger;
|
|
2638
|
+
#commands = [];
|
|
2639
|
+
#legacy = new Map();
|
|
2640
|
+
#access;
|
|
2641
|
+
#nextOrder = 0;
|
|
2642
|
+
constructor(options) {
|
|
2643
|
+
this.#getDefaultPrefix = options.getDefaultPrefix;
|
|
2644
|
+
this.#getOwners = options.getOwners;
|
|
2645
|
+
this.#getAdmins = options.getAdmins;
|
|
2646
|
+
this.#logger = options.logger;
|
|
2647
|
+
}
|
|
2648
|
+
register(plugin, definitionOrMatcher, handlerOrContext, optionsOrContext = {}, context) {
|
|
2649
|
+
const isDefinition = typeof definitionOrMatcher === "object" && definitionOrMatcher !== null && "name" in definitionOrMatcher;
|
|
2650
|
+
const shortcutOptions = isDefinition ? {} : optionsOrContext;
|
|
2651
|
+
const resolvedContext = isDefinition ? handlerOrContext : context;
|
|
2652
|
+
const handler = isDefinition ? void 0 : handlerOrContext;
|
|
2653
|
+
const definition = isDefinition ? definitionOrMatcher : {
|
|
2654
|
+
...shortcutOptions,
|
|
2655
|
+
name: shortcutOptions.name ?? String(definitionOrMatcher),
|
|
2656
|
+
match: definitionOrMatcher,
|
|
2657
|
+
handler
|
|
2658
|
+
};
|
|
2659
|
+
if (typeof definition.handler !== "function") throw new Error("CommandManager.register: handler must be a function");
|
|
2660
|
+
if (!resolvedContext) throw new Error("CommandManager.register: context is required");
|
|
2661
|
+
const name = String(definition.name ?? "").trim();
|
|
2662
|
+
if (!name) throw new Error("CommandManager.register: name is required");
|
|
2663
|
+
const matcher = definition.match ?? name;
|
|
2664
|
+
const command = {
|
|
2665
|
+
plugin,
|
|
2666
|
+
id: String(definition.id ?? name).trim() || name,
|
|
2667
|
+
name,
|
|
2668
|
+
aliases: (definition.aliases ?? []).map((item) => String(item).trim()).filter(Boolean),
|
|
2669
|
+
matcher,
|
|
2670
|
+
description: String(definition.description ?? "").trim(),
|
|
2671
|
+
usage: definition.usage,
|
|
2672
|
+
permission: normalizeRole(definition.permission),
|
|
2673
|
+
priority: typeof definition.priority === "number" && !Number.isNaN(definition.priority) ? definition.priority : 0,
|
|
2674
|
+
prefixes: definition.prefixes,
|
|
2675
|
+
handler: definition.handler,
|
|
2676
|
+
order: this.#nextOrder++,
|
|
2677
|
+
context: resolvedContext
|
|
2678
|
+
};
|
|
2679
|
+
this.#commands.push(command);
|
|
2680
|
+
return () => {
|
|
2681
|
+
const index = this.#commands.indexOf(command);
|
|
2682
|
+
if (index >= 0) this.#commands.splice(index, 1);
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
setAccessControl(config) {
|
|
2686
|
+
this.#access = config;
|
|
2687
|
+
}
|
|
2688
|
+
syncMetadata(metadata) {
|
|
2689
|
+
this.#legacy.set(metadata.name, { metadata });
|
|
2690
|
+
}
|
|
2691
|
+
removePlugin(plugin) {
|
|
2692
|
+
this.#legacy.delete(plugin);
|
|
2693
|
+
for (let i = this.#commands.length - 1; i >= 0; i--) if (this.#commands[i].plugin === plugin) this.#commands.splice(i, 1);
|
|
2694
|
+
}
|
|
2695
|
+
clear() {
|
|
2696
|
+
this.#commands.length = 0;
|
|
2697
|
+
this.#legacy.clear();
|
|
2698
|
+
this.#access = void 0;
|
|
2699
|
+
}
|
|
2700
|
+
list() {
|
|
2701
|
+
return this.#orderedCommands();
|
|
2702
|
+
}
|
|
2703
|
+
#orderedCommands() {
|
|
2704
|
+
return [...this.#commands].sort((a, b) => a.priority - b.priority || a.order - b.order);
|
|
2705
|
+
}
|
|
2706
|
+
getPluginHelp(plugin) {
|
|
2707
|
+
const metadata = this.#legacy.get(plugin)?.metadata;
|
|
2708
|
+
const runtime = this.#commands.filter((item) => item.plugin === plugin);
|
|
2709
|
+
if (!metadata?.config.help && runtime.length === 0) return void 0;
|
|
2710
|
+
const commands = [];
|
|
2711
|
+
const seen = new Set();
|
|
2712
|
+
const runtimeByKey = new Map(runtime.map((command) => [commandKey$1(command.name), command]));
|
|
2713
|
+
for (const command of metadata?.config.help?.commands ?? []) {
|
|
2714
|
+
const key = commandKey$1(command.cmd);
|
|
2715
|
+
const runtimeCommand = runtimeByKey.get(key);
|
|
2716
|
+
if (runtimeCommand) {
|
|
2717
|
+
const cmd = displayCommand(runtimeCommand, this.#getDefaultPrefix());
|
|
2718
|
+
seen.add(key);
|
|
2719
|
+
commands.push({
|
|
2720
|
+
cmd,
|
|
2721
|
+
desc: runtimeCommand.description || command.desc || cmd,
|
|
2722
|
+
usage: runtimeCommand.usage ?? command.usage,
|
|
2723
|
+
role: runtimeCommand.permission
|
|
2724
|
+
});
|
|
2725
|
+
} else if (!seen.has(key)) {
|
|
2726
|
+
seen.add(key);
|
|
2727
|
+
commands.push(command);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
for (const command of runtime) {
|
|
2731
|
+
const cmd = displayCommand(command, this.#getDefaultPrefix());
|
|
2732
|
+
if (seen.has(commandKey$1(cmd))) continue;
|
|
2733
|
+
seen.add(commandKey$1(cmd));
|
|
2734
|
+
commands.push({
|
|
2735
|
+
cmd,
|
|
2736
|
+
desc: command.description || cmd,
|
|
2737
|
+
usage: command.usage,
|
|
2738
|
+
role: command.permission
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
const help = metadata?.config.help;
|
|
2742
|
+
return {
|
|
2743
|
+
title: help?.title || plugin,
|
|
2744
|
+
description: help?.description || metadata?.description || "",
|
|
2745
|
+
commands
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
catalog() {
|
|
2749
|
+
const items = [];
|
|
2750
|
+
const plugins = new Set([...this.#legacy.keys(), ...this.#commands.map((item) => item.plugin)]);
|
|
2751
|
+
for (const plugin of plugins) {
|
|
2752
|
+
const metadata = this.#legacy.get(plugin)?.metadata;
|
|
2753
|
+
const help = metadata?.config.help;
|
|
2754
|
+
items.push({
|
|
2755
|
+
kind: "plugin",
|
|
2756
|
+
plugin,
|
|
2757
|
+
id: plugin,
|
|
2758
|
+
label: help?.title || plugin,
|
|
2759
|
+
desc: help?.description || metadata?.description,
|
|
2760
|
+
source: metadata ? "manifest" : "runtime"
|
|
2761
|
+
});
|
|
2762
|
+
const seen = new Set();
|
|
2763
|
+
for (const command of this.#commands.filter((item) => item.plugin === plugin)) {
|
|
2764
|
+
seen.add(commandKey$1(command.id));
|
|
2765
|
+
items.push({
|
|
2766
|
+
kind: "command",
|
|
2767
|
+
plugin,
|
|
2768
|
+
id: command.id,
|
|
2769
|
+
label: command.name,
|
|
2770
|
+
desc: command.description,
|
|
2771
|
+
usage: command.usage,
|
|
2772
|
+
permission: command.permission,
|
|
2773
|
+
priority: command.priority,
|
|
2774
|
+
match: command.matcher instanceof RegExp ? command.matcher.toString() : command.matcher,
|
|
2775
|
+
source: "runtime"
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
for (const hook of metadata?.config.accessHooks ?? []) {
|
|
2779
|
+
if (seen.has(commandKey$1(hook.id))) continue;
|
|
2780
|
+
seen.add(commandKey$1(hook.id));
|
|
2781
|
+
items.push({
|
|
2782
|
+
kind: "command",
|
|
2783
|
+
plugin,
|
|
2784
|
+
id: hook.id,
|
|
2785
|
+
label: hook.id,
|
|
2786
|
+
desc: hook.description,
|
|
2787
|
+
match: hook.match,
|
|
2788
|
+
event: hook.event,
|
|
2789
|
+
source: "manifest"
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
for (const command of help?.commands ?? []) {
|
|
2793
|
+
if (seen.has(commandKey$1(command.cmd))) continue;
|
|
2794
|
+
seen.add(commandKey$1(command.cmd));
|
|
2795
|
+
items.push({
|
|
2796
|
+
kind: "command",
|
|
2797
|
+
plugin,
|
|
2798
|
+
id: command.cmd,
|
|
2799
|
+
label: command.cmd,
|
|
2800
|
+
desc: command.desc,
|
|
2801
|
+
usage: command.usage,
|
|
2802
|
+
permission: command.role,
|
|
2803
|
+
source: "manifest"
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
return items.sort((a, b) => a.kind === b.kind ? a.plugin.localeCompare(b.plugin) || a.id.localeCompare(b.id) : a.kind === "plugin" ? -1 : 1);
|
|
2808
|
+
}
|
|
2809
|
+
async dispatch(event) {
|
|
2810
|
+
const text$1 = String(event.message?.text?.() ?? "").trim();
|
|
2811
|
+
if (!text$1) return false;
|
|
2812
|
+
const match$1 = this.#find(text$1);
|
|
2813
|
+
if (!match$1) return false;
|
|
2814
|
+
if (!this.#canUse(match$1.command, event)) return true;
|
|
2815
|
+
try {
|
|
2816
|
+
await match$1.command.handler({
|
|
2817
|
+
ctx: match$1.command.context,
|
|
2818
|
+
event,
|
|
2819
|
+
command: match$1.command,
|
|
2820
|
+
text: text$1,
|
|
2821
|
+
body: match$1.body,
|
|
2822
|
+
args: match$1.args,
|
|
2823
|
+
match: match$1.match
|
|
2824
|
+
});
|
|
2825
|
+
} catch (error) {
|
|
2826
|
+
this.#logger?.error(`Command "${match$1.command.plugin}:${match$1.command.id}" failed`, error);
|
|
2827
|
+
}
|
|
2828
|
+
return true;
|
|
2829
|
+
}
|
|
2830
|
+
shouldDispatch(source, event) {
|
|
2831
|
+
if (!source.startsWith("plugin:")) return true;
|
|
2832
|
+
const plugin = source.slice(7);
|
|
2833
|
+
const metadata = this.#legacy.get(plugin)?.metadata;
|
|
2834
|
+
if (!metadata) return this.#pluginAllowed(plugin, event);
|
|
2835
|
+
if (!this.#pluginAllowed(plugin, event)) return false;
|
|
2836
|
+
const text$1 = event.kind === "message" ? String(event.message.text()).trim() : "";
|
|
2837
|
+
const matches = (metadata.config.accessHooks ?? []).filter((hook) => event.kind === "message" && textHookMatches(text$1, hook.match) || routeMatches(event, hook.event));
|
|
2838
|
+
return matches.every((hook) => this.#commandAllowed(plugin, hook.id, event));
|
|
2839
|
+
}
|
|
2840
|
+
#find(text$1) {
|
|
2841
|
+
for (const command of this.#orderedCommands()) {
|
|
2842
|
+
const prefixes = parsePrefixes(command.prefixes ?? this.#getDefaultPrefix(), this.#getDefaultPrefix());
|
|
2843
|
+
const body = commandBody(text$1, prefixes);
|
|
2844
|
+
if (body === void 0) continue;
|
|
2845
|
+
if (typeof command.matcher === "string") {
|
|
2846
|
+
const candidates = [
|
|
2847
|
+
command.matcher,
|
|
2848
|
+
command.name,
|
|
2849
|
+
...command.aliases
|
|
2850
|
+
];
|
|
2851
|
+
for (const candidate of candidates) {
|
|
2852
|
+
const result = stringMatch(body, candidate);
|
|
2853
|
+
if (result) return {
|
|
2854
|
+
command,
|
|
2855
|
+
body,
|
|
2856
|
+
args: result.args
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
continue;
|
|
2860
|
+
}
|
|
2861
|
+
command.matcher.lastIndex = 0;
|
|
2862
|
+
const matched = body.match(command.matcher);
|
|
2863
|
+
if (!matched) continue;
|
|
2864
|
+
const args = matched.length > 1 ? matched.slice(1).filter((item) => item != null) : body.slice(matched[0].length).trim().split(/\s+/).filter(Boolean);
|
|
2865
|
+
return {
|
|
2866
|
+
command,
|
|
2867
|
+
body,
|
|
2868
|
+
args,
|
|
2869
|
+
match: matched
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
return void 0;
|
|
2873
|
+
}
|
|
2874
|
+
#canUse(command, event) {
|
|
2875
|
+
const userId = userIdOf(event);
|
|
2876
|
+
const master = Boolean(userId && this.#isMaster(userId));
|
|
2877
|
+
const owner = master || this.#isGroupOwner(event);
|
|
2878
|
+
const admin = owner || Boolean(userId && this.#isAdmin(userId)) || this.#isGroupAdmin(event);
|
|
2879
|
+
if (command.permission === "master") {
|
|
2880
|
+
if (!master) return false;
|
|
2881
|
+
} else if (command.permission === "owner") {
|
|
2882
|
+
if (!owner) return false;
|
|
2883
|
+
} else if (command.permission === "admin" && !admin) return false;
|
|
2884
|
+
return this.#commandAllowed(command.plugin, command.id, event);
|
|
2885
|
+
}
|
|
2886
|
+
#pluginAllowed(plugin, event) {
|
|
2887
|
+
const userId = event.kind === "message" ? userIdOf(event) : "user_id" in event ? event.user_id : void 0;
|
|
2888
|
+
if (userId && (this.#isMaster(userId) || this.#isAdmin(userId)) || this.#isGroupOwner(event) || this.#isGroupAdmin(event)) return true;
|
|
2889
|
+
return this.#resolveAction(plugin, void 0, event) !== "block";
|
|
2890
|
+
}
|
|
2891
|
+
#commandAllowed(plugin, command, event) {
|
|
2892
|
+
const userId = event.kind === "message" ? userIdOf(event) : "user_id" in event ? event.user_id : void 0;
|
|
2893
|
+
if (userId && (this.#isMaster(userId) || this.#isAdmin(userId)) || this.#isGroupOwner(event) || this.#isGroupAdmin(event)) return true;
|
|
2894
|
+
return this.#resolveAction(plugin, command, event) !== "block";
|
|
2895
|
+
}
|
|
2896
|
+
#resolveAction(plugin, command, event) {
|
|
2897
|
+
const access = this.#access;
|
|
2898
|
+
if (!access) return void 0;
|
|
2899
|
+
const scopes = [
|
|
2900
|
+
(() => {
|
|
2901
|
+
const userId = event.kind === "message" ? userIdOf(event) : "user_id" in event && event.user_id ? String(event.user_id) : void 0;
|
|
2902
|
+
return userId ? access.users[userId] : void 0;
|
|
2903
|
+
})(),
|
|
2904
|
+
(() => {
|
|
2905
|
+
const groupId = "group_id" in event && event.group_id ? String(event.group_id) : void 0;
|
|
2906
|
+
return groupId ? access.groups[groupId] : void 0;
|
|
2907
|
+
})(),
|
|
2908
|
+
access.global
|
|
2909
|
+
];
|
|
2910
|
+
for (const scope of scopes) {
|
|
2911
|
+
if (!scope) continue;
|
|
2912
|
+
const commandRule = command ? scope.commands?.[plugin]?.[command]?.action : void 0;
|
|
2913
|
+
if (commandRule) return commandRule;
|
|
2914
|
+
const pluginRule = scope.plugins?.[plugin]?.action;
|
|
2915
|
+
if (pluginRule) return pluginRule;
|
|
2916
|
+
}
|
|
2917
|
+
return void 0;
|
|
2918
|
+
}
|
|
2919
|
+
#isMaster(userId) {
|
|
2920
|
+
return this.#owners().includes(userId);
|
|
2921
|
+
}
|
|
2922
|
+
#isAdmin(userId) {
|
|
2923
|
+
return this.#isMaster(userId) || this.#admins().includes(userId);
|
|
2924
|
+
}
|
|
2925
|
+
#isGroupOwner(event) {
|
|
2926
|
+
return isEventGroupOwner(event);
|
|
2927
|
+
}
|
|
2928
|
+
#isGroupAdmin(event) {
|
|
2929
|
+
return isEventGroupAdmin(event);
|
|
2930
|
+
}
|
|
2931
|
+
#owners() {
|
|
2932
|
+
return this.#configIds("owners");
|
|
2933
|
+
}
|
|
2934
|
+
#admins() {
|
|
2935
|
+
return this.#configIds("admins");
|
|
2936
|
+
}
|
|
2937
|
+
#configIds(key) {
|
|
2938
|
+
return [...key === "owners" ? this.#getOwners() : this.#getAdmins()].map(String);
|
|
2939
|
+
}
|
|
2940
|
+
};
|
|
2941
|
+
let activeCommandManager;
|
|
2942
|
+
const setActiveCommandManager = (manager) => {
|
|
2943
|
+
activeCommandManager = manager;
|
|
2944
|
+
};
|
|
2945
|
+
const getActiveCommandManager = () => activeCommandManager;
|
|
2946
|
+
|
|
2262
2947
|
//#endregion
|
|
2263
2948
|
//#region src/runtime/plugin-metadata.ts
|
|
2264
2949
|
const store$1 = getOrCreate("plugin-metadata", () => new Map());
|
|
2265
2950
|
const setPluginMetadata = (meta) => {
|
|
2266
2951
|
store$1.set(meta.name, meta);
|
|
2952
|
+
getActiveCommandManager()?.syncMetadata(meta);
|
|
2267
2953
|
};
|
|
2268
2954
|
const removePluginMetadata = (name) => {
|
|
2269
2955
|
store$1.delete(name);
|
|
2956
|
+
getActiveCommandManager()?.removePlugin(name);
|
|
2270
2957
|
};
|
|
2271
2958
|
const getPluginMetadata = (name) => store$1.get(name);
|
|
2272
2959
|
const getPluginMetadataList = () => [...store$1.values()];
|
|
2273
2960
|
const resetPluginMetadata = () => {
|
|
2274
2961
|
store$1.clear();
|
|
2962
|
+
getActiveCommandManager()?.clear();
|
|
2275
2963
|
};
|
|
2276
2964
|
|
|
2277
2965
|
//#endregion
|
|
@@ -2931,12 +3619,6 @@ async function sendTextOrForward(options) {
|
|
|
2931
3619
|
}
|
|
2932
3620
|
}
|
|
2933
3621
|
|
|
2934
|
-
//#endregion
|
|
2935
|
-
//#region src/builtin/core/commands/prefix.ts
|
|
2936
|
-
function getCommandPrefix() {
|
|
2937
|
-
return botConfig.prefix ?? ".";
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
3622
|
//#endregion
|
|
2941
3623
|
//#region src/builtin/core/system/package-manager.ts
|
|
2942
3624
|
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
@@ -3528,102 +4210,102 @@ async function performUpdateAndReport(ctx, event, names) {
|
|
|
3528
4210
|
triggerRestart(marker);
|
|
3529
4211
|
}
|
|
3530
4212
|
function registerUpdateCommands(ctx) {
|
|
3531
|
-
const dispose = ctx.
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
await performUpdateAndReport(ctx, event, managed.names);
|
|
3551
|
-
return;
|
|
3552
|
-
}
|
|
3553
|
-
if (arg === "mioku" || arg === "self") {
|
|
3554
|
-
await performUpdateAndReport(ctx, event, ["mioku"]);
|
|
3555
|
-
return;
|
|
3556
|
-
}
|
|
3557
|
-
if (arg.startsWith("plugin ") || arg.startsWith("service ")) {
|
|
3558
|
-
const [typeRaw, ...rest] = arg.split(/\s+/);
|
|
3559
|
-
const name = rest.join(" ").trim();
|
|
3560
|
-
const type = typeRaw === "plugin" ? "plugin" : "service";
|
|
3561
|
-
if (!name) {
|
|
3562
|
-
await replyText(event, `用法:${prefix}update ${type} <名称>`);
|
|
4213
|
+
const dispose = ctx.command({
|
|
4214
|
+
name: "update",
|
|
4215
|
+
aliases: ["更新"],
|
|
4216
|
+
permission: "master",
|
|
4217
|
+
priority: -1e3,
|
|
4218
|
+
description: "检查并选择插件/服务更新",
|
|
4219
|
+
handler: async ({ event, args }) => {
|
|
4220
|
+
const prefix = String(ctx.config.prefix ?? ".");
|
|
4221
|
+
const arg = args.join(" ").trim();
|
|
4222
|
+
const selfId = String(event?.self_id || "");
|
|
4223
|
+
const bot = event.bot;
|
|
4224
|
+
if (!bot) return;
|
|
4225
|
+
if (arg === "all") {
|
|
4226
|
+
const managed = await updateAllManaged();
|
|
4227
|
+
if (managed.names.length === 0) {
|
|
4228
|
+
await replyText(event, "未找到可更新的 mioku 包");
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
await performUpdateAndReport(ctx, event, managed.names);
|
|
3563
4232
|
return;
|
|
3564
4233
|
}
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
await performUpdateAndReport(ctx, event, [fullName]);
|
|
3568
|
-
return;
|
|
3569
|
-
}
|
|
3570
|
-
if (arg === "check" || arg === "检查") {
|
|
3571
|
-
const pending = getPendingMap();
|
|
3572
|
-
const key = conversationKey(event);
|
|
3573
|
-
const existing = pending.get(key);
|
|
3574
|
-
if (existing) {
|
|
3575
|
-
existing.disposer();
|
|
3576
|
-
clearTimeout(existing.timer);
|
|
3577
|
-
pending.delete(key);
|
|
3578
|
-
}
|
|
3579
|
-
let items;
|
|
3580
|
-
try {
|
|
3581
|
-
items = await checkUpdates();
|
|
3582
|
-
} catch (error) {
|
|
3583
|
-
ctx.logger.error(`[core] 检查更新失败: ${error}`);
|
|
3584
|
-
await replyText(event, `检查更新失败:${String(error)}`);
|
|
4234
|
+
if (arg === "mioku" || arg === "self") {
|
|
4235
|
+
await performUpdateAndReport(ctx, event, ["mioku"]);
|
|
3585
4236
|
return;
|
|
3586
4237
|
}
|
|
3587
|
-
if (
|
|
3588
|
-
|
|
4238
|
+
if (arg.startsWith("plugin ") || arg.startsWith("service ")) {
|
|
4239
|
+
const [typeRaw, ...rest] = arg.split(/\s+/);
|
|
4240
|
+
const name = rest.join(" ").trim();
|
|
4241
|
+
const type = typeRaw === "plugin" ? "plugin" : "service";
|
|
4242
|
+
if (!name) {
|
|
4243
|
+
await replyText(event, `用法:${prefix}update ${type} <名称>`);
|
|
4244
|
+
return;
|
|
4245
|
+
}
|
|
4246
|
+
const pkgPrefix = type === "plugin" ? "mioku-plugin-" : "mioku-service-";
|
|
4247
|
+
const fullName = name.startsWith(pkgPrefix) ? name : `${pkgPrefix}${name}`;
|
|
4248
|
+
await performUpdateAndReport(ctx, event, [fullName]);
|
|
3589
4249
|
return;
|
|
3590
4250
|
}
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
if (
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
if (chosen.length === 0) {
|
|
3607
|
-
await replyText(event, "未选择任何有效项,已取消");
|
|
4251
|
+
if (arg === "check" || arg === "检查") {
|
|
4252
|
+
const pending = getPendingMap();
|
|
4253
|
+
const key = conversationKey(event);
|
|
4254
|
+
const existing = pending.get(key);
|
|
4255
|
+
if (existing) {
|
|
4256
|
+
existing.disposer();
|
|
4257
|
+
clearTimeout(existing.timer);
|
|
4258
|
+
pending.delete(key);
|
|
4259
|
+
}
|
|
4260
|
+
let items;
|
|
4261
|
+
try {
|
|
4262
|
+
items = await checkUpdates();
|
|
4263
|
+
} catch (error) {
|
|
4264
|
+
ctx.logger.error(`[core] 检查更新失败: ${error}`);
|
|
4265
|
+
await replyText(event, `检查更新失败:${String(error)}`);
|
|
3608
4266
|
return;
|
|
3609
4267
|
}
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
4268
|
+
if (items.length === 0) {
|
|
4269
|
+
await replyText(event, "所有插件与服务均已是最新版本");
|
|
4270
|
+
return;
|
|
4271
|
+
}
|
|
4272
|
+
await replyText(event, renderUpdateList(items));
|
|
4273
|
+
const timeoutMs = SELECT_TIMEOUT_MS;
|
|
4274
|
+
const listenerDispose = ctx.handle("message", async (ev) => {
|
|
4275
|
+
if (String(ev?.self_id || "") !== selfId) return;
|
|
4276
|
+
if (conversationKey(ev) !== key) return;
|
|
4277
|
+
if (!isEventMaster(ev)) return;
|
|
4278
|
+
const evText = ctx.text(ev)?.trim() || "";
|
|
4279
|
+
if (evText.startsWith(`${prefix}update`)) return;
|
|
4280
|
+
const sel = pending.get(key);
|
|
4281
|
+
if (!sel) return;
|
|
4282
|
+
clearTimeout(sel.timer);
|
|
4283
|
+
sel.disposer();
|
|
4284
|
+
pending.delete(key);
|
|
4285
|
+
const parsed = parseSelection(evText, sel.items.length);
|
|
4286
|
+
const chosen = parsed.all ? sel.items : parsed.indices.map((i) => sel.items[i]).filter(Boolean);
|
|
4287
|
+
if (chosen.length === 0) {
|
|
4288
|
+
await replyText(event, "未选择任何有效项,已取消");
|
|
4289
|
+
return;
|
|
4290
|
+
}
|
|
4291
|
+
await performUpdateAndReport(ctx, event, chosen.map((i) => i.name));
|
|
4292
|
+
});
|
|
4293
|
+
const timer = setTimeout(async () => {
|
|
4294
|
+
const sel = pending.get(key);
|
|
4295
|
+
if (!sel) return;
|
|
4296
|
+
sel.disposer();
|
|
4297
|
+
pending.delete(key);
|
|
4298
|
+
await replyText(event, "操作超时,已取消更新");
|
|
4299
|
+
}, timeoutMs);
|
|
4300
|
+
pending.set(key, {
|
|
4301
|
+
disposer: listenerDispose,
|
|
4302
|
+
timer,
|
|
4303
|
+
items
|
|
4304
|
+
});
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
await replyText(event, `用法:\n${prefix}update check 检查并选择更新\n${prefix}update all 更新全部\n${prefix}update mioku 更新框架\n${prefix}update plugin <名称>\n${prefix}update service <名称>`);
|
|
3625
4308
|
}
|
|
3626
|
-
await replyText(event, `用法:\n${prefix}update check 检查并选择更新\n${prefix}update all 更新全部\n${prefix}update mioku 更新框架\n${prefix}update plugin <名称>\n${prefix}update service <名称>`);
|
|
3627
4309
|
});
|
|
3628
4310
|
return () => {
|
|
3629
4311
|
dispose();
|
|
@@ -3638,15 +4320,13 @@ function registerUpdateCommands(ctx) {
|
|
|
3638
4320
|
|
|
3639
4321
|
//#endregion
|
|
3640
4322
|
//#region src/builtin/core/commands/install.ts
|
|
3641
|
-
function parseTargetArgs(
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
const
|
|
3645
|
-
if (!matched) return null;
|
|
3646
|
-
const name = matched[2].trim();
|
|
4323
|
+
function parseTargetArgs(args) {
|
|
4324
|
+
const type = args[0];
|
|
4325
|
+
if (type !== "plugin" && type !== "service") return null;
|
|
4326
|
+
const name = args.slice(1).join(" ").trim();
|
|
3647
4327
|
if (!name) return null;
|
|
3648
4328
|
return {
|
|
3649
|
-
type
|
|
4329
|
+
type,
|
|
3650
4330
|
name
|
|
3651
4331
|
};
|
|
3652
4332
|
}
|
|
@@ -3654,68 +4334,70 @@ function typeLabel(type) {
|
|
|
3654
4334
|
return type === "plugin" ? "插件" : "服务";
|
|
3655
4335
|
}
|
|
3656
4336
|
function registerInstallCommands(ctx) {
|
|
3657
|
-
const
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
4337
|
+
const register = (name) => ctx.command({
|
|
4338
|
+
name,
|
|
4339
|
+
aliases: name === "install" ? ["安装"] : ["卸载"],
|
|
4340
|
+
permission: "master",
|
|
4341
|
+
priority: -1e3,
|
|
4342
|
+
description: name === "install" ? "从 npm 安装插件/服务" : "卸载插件/服务",
|
|
4343
|
+
handler: async ({ event, args }) => {
|
|
4344
|
+
if (name === "install") {
|
|
4345
|
+
const parsed$1 = parseTargetArgs(args);
|
|
4346
|
+
if (!parsed$1) {
|
|
4347
|
+
await replyText(event, `用法:${ctx.config.prefix ?? "."}install plugin <名称> 或 ${ctx.config.prefix ?? "."}install service <名称>`);
|
|
4348
|
+
return;
|
|
4349
|
+
}
|
|
4350
|
+
await replyText(event, `正在从 npm 安装${typeLabel(parsed$1.type)} ${parsed$1.name}...`);
|
|
4351
|
+
try {
|
|
4352
|
+
const result = await installPackage(parsed$1.type, parsed$1.name);
|
|
4353
|
+
if (!result.ok) {
|
|
4354
|
+
ctx.logger.error(`[core] 安装失败: ${result.error || result.output}`);
|
|
4355
|
+
await replyText(event, `安装失败:${result.error || result.output}`);
|
|
4356
|
+
return;
|
|
4357
|
+
}
|
|
4358
|
+
const lines = [
|
|
4359
|
+
`已安装 ${result.packageName}`,
|
|
4360
|
+
result.enabled ? "已在 plugins 中启用" : "",
|
|
4361
|
+
"重启后生效"
|
|
4362
|
+
].filter(Boolean);
|
|
4363
|
+
await replyText(event, lines.join("\n"));
|
|
4364
|
+
} catch (error) {
|
|
4365
|
+
ctx.logger.error(`[core] 安装失败: ${error}`);
|
|
4366
|
+
await replyText(event, `安装失败:${String(error)}`);
|
|
4367
|
+
}
|
|
4368
|
+
return;
|
|
4369
|
+
}
|
|
4370
|
+
const parsed = parseTargetArgs(args);
|
|
4371
|
+
if (!parsed) {
|
|
4372
|
+
await replyText(event, `用法:${ctx.config.prefix ?? "."}uninstall plugin <名称> 或 ${ctx.config.prefix ?? "."}uninstall service <名称>`);
|
|
3672
4373
|
return;
|
|
3673
4374
|
}
|
|
3674
|
-
await replyText(event,
|
|
4375
|
+
await replyText(event, `正在卸载${typeLabel(parsed.type)} ${parsed.name}...`);
|
|
3675
4376
|
try {
|
|
3676
|
-
const result = await
|
|
4377
|
+
const result = await uninstallPackage(parsed.type, parsed.name);
|
|
3677
4378
|
if (!result.ok) {
|
|
3678
|
-
ctx.logger.error(`[core]
|
|
3679
|
-
await replyText(event,
|
|
4379
|
+
ctx.logger.error(`[core] 卸载失败: ${result.error || result.output}`);
|
|
4380
|
+
await replyText(event, `卸载失败:${result.error || result.output}`);
|
|
3680
4381
|
return;
|
|
3681
4382
|
}
|
|
3682
4383
|
const lines = [
|
|
3683
|
-
|
|
3684
|
-
result.
|
|
4384
|
+
`已卸载 ${result.packageName}`,
|
|
4385
|
+
result.removedFromConfig ? "已从 plugins 中移除" : "",
|
|
3685
4386
|
"重启后生效"
|
|
3686
4387
|
].filter(Boolean);
|
|
3687
4388
|
await replyText(event, lines.join("\n"));
|
|
3688
4389
|
} catch (error) {
|
|
3689
|
-
ctx.logger.error(`[core]
|
|
3690
|
-
await replyText(event,
|
|
4390
|
+
ctx.logger.error(`[core] 卸载失败: ${error}`);
|
|
4391
|
+
await replyText(event, `卸载失败:${String(error)}`);
|
|
3691
4392
|
}
|
|
3692
|
-
return;
|
|
3693
|
-
}
|
|
3694
|
-
const parsed = parseTargetArgs(text$1, uninstallHead);
|
|
3695
|
-
if (!parsed) {
|
|
3696
|
-
await replyText(event, `用法:${prefix}uninstall plugin <名称> 或 ${prefix}uninstall service <名称>`);
|
|
3697
|
-
return;
|
|
3698
|
-
}
|
|
3699
|
-
await replyText(event, `正在卸载${typeLabel(parsed.type)} ${parsed.name}...`);
|
|
3700
|
-
try {
|
|
3701
|
-
const result = await uninstallPackage(parsed.type, parsed.name);
|
|
3702
|
-
if (!result.ok) {
|
|
3703
|
-
ctx.logger.error(`[core] 卸载失败: ${result.error || result.output}`);
|
|
3704
|
-
await replyText(event, `卸载失败:${result.error || result.output}`);
|
|
3705
|
-
return;
|
|
3706
|
-
}
|
|
3707
|
-
const lines = [
|
|
3708
|
-
`已卸载 ${result.packageName}`,
|
|
3709
|
-
result.removedFromConfig ? "已从 plugins 中移除" : "",
|
|
3710
|
-
"重启后生效"
|
|
3711
|
-
].filter(Boolean);
|
|
3712
|
-
await replyText(event, lines.join("\n"));
|
|
3713
|
-
} catch (error) {
|
|
3714
|
-
ctx.logger.error(`[core] 卸载失败: ${error}`);
|
|
3715
|
-
await replyText(event, `卸载失败:${String(error)}`);
|
|
3716
4393
|
}
|
|
3717
4394
|
});
|
|
3718
|
-
|
|
4395
|
+
const install = register("install");
|
|
4396
|
+
const uninstall = register("uninstall");
|
|
4397
|
+
return () => {
|
|
4398
|
+
install();
|
|
4399
|
+
uninstall();
|
|
4400
|
+
};
|
|
3719
4401
|
}
|
|
3720
4402
|
|
|
3721
4403
|
//#endregion
|
|
@@ -3729,64 +4411,76 @@ function renderMarketText(kind, items) {
|
|
|
3729
4411
|
return [`Mioku ${label}市场(共 ${items.length} 个)`, ...lines].join("\n");
|
|
3730
4412
|
}
|
|
3731
4413
|
function registerMarketCommands(ctx) {
|
|
3732
|
-
const
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
4414
|
+
const pluginMarket = ctx.command({
|
|
4415
|
+
id: "market",
|
|
4416
|
+
name: "plugin-market",
|
|
4417
|
+
aliases: ["插件市场"],
|
|
4418
|
+
permission: "master",
|
|
4419
|
+
priority: -1e3,
|
|
4420
|
+
description: "查看插件市场",
|
|
4421
|
+
handler: (input) => handleMarket(ctx, input.event, "plugin")
|
|
4422
|
+
});
|
|
4423
|
+
const serviceMarket = ctx.command({
|
|
4424
|
+
id: "market",
|
|
4425
|
+
name: "service-market",
|
|
4426
|
+
aliases: ["服务市场"],
|
|
4427
|
+
permission: "master",
|
|
4428
|
+
priority: -1e3,
|
|
4429
|
+
description: "查看服务市场",
|
|
4430
|
+
handler: (input) => handleMarket(ctx, input.event, "service")
|
|
4431
|
+
});
|
|
4432
|
+
return () => {
|
|
4433
|
+
pluginMarket();
|
|
4434
|
+
serviceMarket();
|
|
4435
|
+
};
|
|
4436
|
+
}
|
|
4437
|
+
async function handleMarket(ctx, event, kind) {
|
|
4438
|
+
let items;
|
|
4439
|
+
try {
|
|
4440
|
+
items = await getMarketItems(kind);
|
|
4441
|
+
} catch (error) {
|
|
4442
|
+
ctx.logger.error(`[core] 获取市场信息失败: ${error}`);
|
|
4443
|
+
await replyText(event, `获取市场失败:${String(error)}`);
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4446
|
+
await sendTextOrForward({
|
|
4447
|
+
ctx,
|
|
4448
|
+
event,
|
|
4449
|
+
text: renderMarketText(kind, items),
|
|
4450
|
+
source: `Mioku ${kind === "plugin" ? "插件" : "服务"}市场`,
|
|
4451
|
+
summary: `共 ${items.length} 个`
|
|
3757
4452
|
});
|
|
3758
|
-
return dispose;
|
|
3759
4453
|
}
|
|
3760
4454
|
|
|
3761
4455
|
//#endregion
|
|
3762
4456
|
//#region src/builtin/core/commands/restart.ts
|
|
3763
4457
|
function registerRestartCommand(ctx) {
|
|
3764
|
-
const dispose = ctx.
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
return;
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
4458
|
+
const dispose = ctx.command({
|
|
4459
|
+
name: "restart",
|
|
4460
|
+
aliases: ["重启"],
|
|
4461
|
+
permission: "master",
|
|
4462
|
+
priority: -1e3,
|
|
4463
|
+
description: "重启机器人进程",
|
|
4464
|
+
handler: async ({ event }) => {
|
|
4465
|
+
if (event?.user_id === event?.self_id) return;
|
|
4466
|
+
const uptimeMs = process.uptime() * 1e3;
|
|
4467
|
+
const selfId = Number(event?.self_id || 0);
|
|
4468
|
+
const groupId = event?.message_type === "group" && event?.group_id ? Number(event.group_id) : null;
|
|
4469
|
+
const userId = Number(event?.user_id || 0);
|
|
4470
|
+
const marker = {
|
|
4471
|
+
initiatedAt: Date.now(),
|
|
4472
|
+
selfId,
|
|
4473
|
+
groupId,
|
|
4474
|
+
userId
|
|
4475
|
+
};
|
|
4476
|
+
try {
|
|
4477
|
+
await replyText(event, `Bot已运行${formatUptime(uptimeMs)},正在重启...`);
|
|
4478
|
+
} catch (error) {
|
|
4479
|
+
ctx.logger.warn(`[core] 发送重启提示失败: ${error}`);
|
|
4480
|
+
}
|
|
4481
|
+
ctx.logger.info(`[core] 正在执行重启命令 ${formatUptime(uptimeMs)}`);
|
|
4482
|
+
triggerRestart(marker);
|
|
3787
4483
|
}
|
|
3788
|
-
ctx.logger.info(`[core] 正在执行重启命令 ${formatUptime(uptimeMs)}`);
|
|
3789
|
-
triggerRestart(marker);
|
|
3790
4484
|
});
|
|
3791
4485
|
return dispose;
|
|
3792
4486
|
}
|
|
@@ -3824,63 +4518,63 @@ function chunk(arr, size) {
|
|
|
3824
4518
|
return result;
|
|
3825
4519
|
}
|
|
3826
4520
|
function registerLogCommand(ctx) {
|
|
3827
|
-
const dispose = ctx.
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
return;
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
})
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
4521
|
+
const dispose = ctx.command({
|
|
4522
|
+
name: "log",
|
|
4523
|
+
aliases: ["日志"],
|
|
4524
|
+
permission: "master",
|
|
4525
|
+
priority: -1e3,
|
|
4526
|
+
description: "查看最近100条日志",
|
|
4527
|
+
handler: async ({ event }) => {
|
|
4528
|
+
if (event?.user_id === event?.self_id) return;
|
|
4529
|
+
const selfId = String(event?.self_id || "");
|
|
4530
|
+
const bot = event.bot;
|
|
4531
|
+
if (!bot) return;
|
|
4532
|
+
const logFile = getActiveLogFile();
|
|
4533
|
+
if (!logFile) {
|
|
4534
|
+
await replyText(event, "未找到日志文件。");
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
let lines;
|
|
4538
|
+
try {
|
|
4539
|
+
lines = readLastLines(logFile, LOG_LINE_COUNT);
|
|
4540
|
+
} catch (error) {
|
|
4541
|
+
ctx.logger.error(`[core] 读取日志失败: ${error}`);
|
|
4542
|
+
await replyText(event, `读取日志失败:${String(error)}`);
|
|
4543
|
+
return;
|
|
4544
|
+
}
|
|
4545
|
+
if (lines.length === 0) {
|
|
4546
|
+
await replyText(event, "日志为空。");
|
|
4547
|
+
return;
|
|
4548
|
+
}
|
|
4549
|
+
let infoCount = 0;
|
|
4550
|
+
let warnCount = 0;
|
|
4551
|
+
let errorCount = 0;
|
|
4552
|
+
for (const line of lines) if (/\bERROR\b|\berror\b/i.test(line)) errorCount++;
|
|
4553
|
+
else if (/\bWARN\b|\bwarn\b/i.test(line)) warnCount++;
|
|
4554
|
+
else if (/\bINFO\b|\binfo\b/i.test(line)) infoCount++;
|
|
4555
|
+
const summary = `info ${infoCount} warn ${warnCount} error ${errorCount}`;
|
|
4556
|
+
const chunks = chunk(lines, LINES_PER_NODE);
|
|
4557
|
+
const nodes = chunks.map((chunkLines, idx) => ({
|
|
4558
|
+
user_id: selfId,
|
|
4559
|
+
nickname: `第${idx + 1}条日志`,
|
|
4560
|
+
content: [ctx.segment.text(chunkLines.join("\n"))]
|
|
4561
|
+
}));
|
|
4562
|
+
try {
|
|
4563
|
+
const target = event?.message_type === "group" && event?.group_id ? {
|
|
4564
|
+
type: "group",
|
|
4565
|
+
group_id: event.group_id
|
|
4566
|
+
} : event?.user_id ? {
|
|
4567
|
+
type: "private",
|
|
4568
|
+
user_id: event.user_id
|
|
4569
|
+
} : void 0;
|
|
4570
|
+
if (target) await bot.sendForward(target, nodes, {
|
|
4571
|
+
source: "最近100条运行日志",
|
|
4572
|
+
summary
|
|
4573
|
+
});
|
|
4574
|
+
} catch (error) {
|
|
4575
|
+
ctx.logger.error(`[core] 发送日志转发失败: ${error}`);
|
|
4576
|
+
await replyText(event, `发送日志失败:${String(error)}`);
|
|
4577
|
+
}
|
|
3884
4578
|
}
|
|
3885
4579
|
});
|
|
3886
4580
|
return dispose;
|
|
@@ -4107,7 +4801,6 @@ const core = definePlugin({
|
|
|
4107
4801
|
description: "mioku 内置核心插件",
|
|
4108
4802
|
async setup(ctx) {
|
|
4109
4803
|
const rawPrefix = ctx.config.prefix ?? ".";
|
|
4110
|
-
const cmdPrefix = new RegExp(`^${escapeRegExp(rawPrefix)}`);
|
|
4111
4804
|
const displayPrefix = rawPrefix;
|
|
4112
4805
|
const statusAdminOnly = ctx.config.status_permission === "admin-only";
|
|
4113
4806
|
rootLogger.info("========================================");
|
|
@@ -4125,12 +4818,17 @@ const core = definePlugin({
|
|
|
4125
4818
|
}));
|
|
4126
4819
|
} else ctx.logger.warn("config-service 未加载,core 插件将使用默认配置");
|
|
4127
4820
|
let accessRules = ensureAccessControlConfig();
|
|
4821
|
+
ctx.commands.setAccessControl(accessRules);
|
|
4128
4822
|
if (configService) {
|
|
4129
4823
|
await configService.registerConfig("core", "access-control", accessRules);
|
|
4130
4824
|
const persisted = await configService.getConfig("core", "access-control");
|
|
4131
|
-
if (persisted)
|
|
4825
|
+
if (persisted) {
|
|
4826
|
+
accessRules = normalizeAccessConfig(persisted);
|
|
4827
|
+
ctx.commands.setAccessControl(accessRules);
|
|
4828
|
+
}
|
|
4132
4829
|
disposers.push(configService.onConfigChange("core", "access-control", (next) => {
|
|
4133
4830
|
accessRules = normalizeAccessConfig(next);
|
|
4831
|
+
ctx.commands.setAccessControl(accessRules);
|
|
4134
4832
|
}));
|
|
4135
4833
|
}
|
|
4136
4834
|
rootLogger.info(`访问控制已挂载: ${ctx.bots.length} 个 bot`);
|
|
@@ -4192,17 +4890,49 @@ const core = definePlugin({
|
|
|
4192
4890
|
bots: collectBots(),
|
|
4193
4891
|
adapters: collectAdapters()
|
|
4194
4892
|
}));
|
|
4893
|
+
ctx.command({
|
|
4894
|
+
name: "help",
|
|
4895
|
+
aliases: ["帮助"],
|
|
4896
|
+
permission: "master",
|
|
4897
|
+
priority: -1e3,
|
|
4898
|
+
description: "显示帮助信息",
|
|
4899
|
+
handler: async ({ event }) => {
|
|
4900
|
+
await event.reply(buildCoreHelpText(displayPrefix));
|
|
4901
|
+
}
|
|
4902
|
+
});
|
|
4903
|
+
ctx.command({
|
|
4904
|
+
name: "status",
|
|
4905
|
+
aliases: ["状态"],
|
|
4906
|
+
priority: -1e3,
|
|
4907
|
+
description: "显示框架状态",
|
|
4908
|
+
handler: async ({ event }) => {
|
|
4909
|
+
if (statusAdminOnly && !isEventOwnerOrAdmin(event)) return;
|
|
4910
|
+
await event.reply(await formatMiokuStatus(await getStatus()));
|
|
4911
|
+
}
|
|
4912
|
+
});
|
|
4913
|
+
ctx.command({
|
|
4914
|
+
name: "adapter",
|
|
4915
|
+
aliases: ["适配器"],
|
|
4916
|
+
priority: -1e3,
|
|
4917
|
+
description: "显示适配器与连接实例",
|
|
4918
|
+
handler: async ({ event }) => {
|
|
4919
|
+
if (statusAdminOnly && !isEventOwnerOrAdmin(event)) return;
|
|
4920
|
+
await event.reply(await getAdapterText());
|
|
4921
|
+
}
|
|
4922
|
+
});
|
|
4195
4923
|
const atTarget = (event) => {
|
|
4196
4924
|
const at = event.message.find((seg) => seg.type === "at");
|
|
4197
4925
|
const qq = at?.data?.qq ?? at?.data?.target;
|
|
4198
4926
|
return qq != null ? String(qq) : void 0;
|
|
4199
4927
|
};
|
|
4200
|
-
|
|
4928
|
+
const handleCoreMessage = async (event) => {
|
|
4201
4929
|
const ev = event;
|
|
4202
4930
|
const text$1 = ev.message.text();
|
|
4203
|
-
|
|
4931
|
+
const currentPrefix = String(ctx.config.prefix ?? ".");
|
|
4932
|
+
const currentCmdPrefix = new RegExp(`^${escapeRegExp(currentPrefix)}`);
|
|
4933
|
+
if (!currentCmdPrefix.test(text$1)) return;
|
|
4204
4934
|
if (statusAdminOnly && !isEventOwnerOrAdmin(ev)) return;
|
|
4205
|
-
const bare = text$1.replace(
|
|
4935
|
+
const bare = text$1.replace(currentCmdPrefix, "").trim();
|
|
4206
4936
|
if (bare === "状态" || bare === "status") {
|
|
4207
4937
|
await ev.reply(await formatMiokuStatus(await getStatus()));
|
|
4208
4938
|
return;
|
|
@@ -4211,10 +4941,10 @@ const core = definePlugin({
|
|
|
4211
4941
|
await ev.reply(await getAdapterText());
|
|
4212
4942
|
return;
|
|
4213
4943
|
}
|
|
4214
|
-
if (!
|
|
4944
|
+
if (!isEventMaster(ev)) return;
|
|
4215
4945
|
const { cmd, params } = createCmd(text$1);
|
|
4216
4946
|
if (!cmd) return;
|
|
4217
|
-
const subCmd = cmd.replace(
|
|
4947
|
+
const subCmd = cmd.replace(currentCmdPrefix, "").replace(/\s+/g, "");
|
|
4218
4948
|
const target = params[0];
|
|
4219
4949
|
switch (subCmd) {
|
|
4220
4950
|
case "help":
|
|
@@ -4436,7 +5166,29 @@ const core = definePlugin({
|
|
|
4436
5166
|
process.exit(0);
|
|
4437
5167
|
}
|
|
4438
5168
|
}
|
|
4439
|
-
}
|
|
5169
|
+
};
|
|
5170
|
+
for (const command of [
|
|
5171
|
+
{
|
|
5172
|
+
name: "plugin",
|
|
5173
|
+
aliases: ["插件"],
|
|
5174
|
+
description: "插件管理"
|
|
5175
|
+
},
|
|
5176
|
+
{
|
|
5177
|
+
name: "settings",
|
|
5178
|
+
aliases: ["设置"],
|
|
5179
|
+
description: "框架设置管理"
|
|
5180
|
+
},
|
|
5181
|
+
{
|
|
5182
|
+
name: "exit",
|
|
5183
|
+
aliases: ["退出"],
|
|
5184
|
+
description: "退出框架进程"
|
|
5185
|
+
}
|
|
5186
|
+
]) ctx.command({
|
|
5187
|
+
...command,
|
|
5188
|
+
permission: "master",
|
|
5189
|
+
priority: -1e3,
|
|
5190
|
+
handler: ({ event }) => handleCoreMessage(event)
|
|
5191
|
+
});
|
|
4440
5192
|
rootLogger.info("========================================");
|
|
4441
5193
|
rootLogger.info(" Mioku 服务初始化完成");
|
|
4442
5194
|
rootLogger.info("========================================");
|
|
@@ -4843,7 +5595,8 @@ var MiokuRuntime = class {
|
|
|
4843
5595
|
#enabledPlugins = new Map();
|
|
4844
5596
|
#driverFactory;
|
|
4845
5597
|
#builtinPlugins;
|
|
4846
|
-
#
|
|
5598
|
+
#correlator;
|
|
5599
|
+
#commands;
|
|
4847
5600
|
#started = false;
|
|
4848
5601
|
#stopped = false;
|
|
4849
5602
|
constructor(options) {
|
|
@@ -4851,7 +5604,7 @@ var MiokuRuntime = class {
|
|
|
4851
5604
|
this.#logger = options.logger;
|
|
4852
5605
|
this.#driverFactory = options.driverFactory ?? (() => createDefaultDriver());
|
|
4853
5606
|
this.#builtinPlugins = options.builtinPlugins ?? BUILTIN_PLUGINS$1;
|
|
4854
|
-
this.#
|
|
5607
|
+
this.#correlator = options.dedup?.crossAdapter === false ? void 0 : new EventCorrelator({ logger: this.#logger.child({ scope: "dedup" }) });
|
|
4855
5608
|
this.#driver = this.#driverFactory();
|
|
4856
5609
|
this.#bus = new EventBus();
|
|
4857
5610
|
this.#bus.setLogger((level, message, detail) => {
|
|
@@ -4861,6 +5614,14 @@ var MiokuRuntime = class {
|
|
|
4861
5614
|
});
|
|
4862
5615
|
this.#bots = new BotRegistry();
|
|
4863
5616
|
this.#capabilities = new CapabilityRegistry();
|
|
5617
|
+
this.#commands = new CommandManager({
|
|
5618
|
+
getDefaultPrefix: () => String(botConfig.prefix ?? "."),
|
|
5619
|
+
getOwners: () => botConfig.owners,
|
|
5620
|
+
getAdmins: () => botConfig.admins,
|
|
5621
|
+
logger: this.#logger
|
|
5622
|
+
});
|
|
5623
|
+
this.#bus.setFilter((registration, event) => this.#commands.shouldDispatch(registration.source, event));
|
|
5624
|
+
setActiveCommandManager(this.#commands);
|
|
4864
5625
|
}
|
|
4865
5626
|
get cwd() {
|
|
4866
5627
|
return this.#cwd;
|
|
@@ -4874,6 +5635,13 @@ var MiokuRuntime = class {
|
|
|
4874
5635
|
get bus() {
|
|
4875
5636
|
return this.#bus;
|
|
4876
5637
|
}
|
|
5638
|
+
get commands() {
|
|
5639
|
+
return this.#commands;
|
|
5640
|
+
}
|
|
5641
|
+
/** 事件关联器(跨适配器去重);为 undefined 时表示已禁用 */
|
|
5642
|
+
get correlator() {
|
|
5643
|
+
return this.#correlator;
|
|
5644
|
+
}
|
|
4877
5645
|
get bots() {
|
|
4878
5646
|
return this.#bots.all();
|
|
4879
5647
|
}
|
|
@@ -4888,14 +5656,16 @@ var MiokuRuntime = class {
|
|
|
4888
5656
|
return this.#bots.pick(bot_id);
|
|
4889
5657
|
}
|
|
4890
5658
|
listPlugins() {
|
|
4891
|
-
return Array.from(this.#enabledPlugins.entries()).map(([key
|
|
5659
|
+
return Array.from(this.#enabledPlugins.entries()).map(([key]) => {
|
|
4892
5660
|
const colon = key.indexOf(":");
|
|
4893
5661
|
const type = key.slice(0, colon);
|
|
4894
5662
|
const name = key.slice(colon + 1);
|
|
5663
|
+
const isExternal = type !== "builtin";
|
|
5664
|
+
const version$2 = isExternal ? getPluginMetadata(name)?.version : void 0;
|
|
4895
5665
|
return {
|
|
4896
5666
|
name,
|
|
4897
5667
|
type: type === "builtin" ? "builtin" : "external",
|
|
4898
|
-
version:
|
|
5668
|
+
version: version$2
|
|
4899
5669
|
};
|
|
4900
5670
|
});
|
|
4901
5671
|
}
|
|
@@ -4981,13 +5751,17 @@ var MiokuRuntime = class {
|
|
|
4981
5751
|
driver: this.#driver,
|
|
4982
5752
|
capabilities: this.#capabilities,
|
|
4983
5753
|
logger: this.#logger.child({ adapter: state.definition.name }),
|
|
4984
|
-
emit: (event) => this.#emitLifecycle(event)
|
|
5754
|
+
emit: (event) => this.#emitLifecycle(event),
|
|
5755
|
+
correlator: this.#correlator,
|
|
5756
|
+
commands: this.#commands
|
|
4985
5757
|
});
|
|
4986
5758
|
}
|
|
4987
5759
|
async #loadPlugin(plugin, type) {
|
|
4988
5760
|
const cleanupTasks = [];
|
|
4989
5761
|
const ctx = new MiokuContext({
|
|
4990
5762
|
pluginName: `${type}:${plugin.name}`,
|
|
5763
|
+
pluginId: plugin.name === "mioku-core" ? "core" : plugin.name,
|
|
5764
|
+
commands: this.#commands,
|
|
4991
5765
|
bus: this.#bus,
|
|
4992
5766
|
bots: this.#bots,
|
|
4993
5767
|
driver: this.#driver,
|
|
@@ -4995,7 +5769,7 @@ var MiokuRuntime = class {
|
|
|
4995
5769
|
config: botConfig,
|
|
4996
5770
|
logger: this.#logger.child({ plugin: plugin.name }),
|
|
4997
5771
|
priority: plugin.priority ?? 100,
|
|
4998
|
-
|
|
5772
|
+
correlator: this.#correlator,
|
|
4999
5773
|
getAdapter: (name) => this.getAdapter(name),
|
|
5000
5774
|
listAdapters: () => this.adapters,
|
|
5001
5775
|
onUpdateConfig: async (updater) => {
|
|
@@ -5284,6 +6058,9 @@ var MiokuRuntime = class {
|
|
|
5284
6058
|
this.#enabledPlugins.clear();
|
|
5285
6059
|
this.#capabilities.clear();
|
|
5286
6060
|
this.#bots.clear();
|
|
6061
|
+
this.#correlator?.clear();
|
|
6062
|
+
this.#commands.clear();
|
|
6063
|
+
setActiveCommandManager(void 0);
|
|
5287
6064
|
resetPluginMetadata();
|
|
5288
6065
|
try {
|
|
5289
6066
|
await this.#driver.shutdown();
|
|
@@ -5300,22 +6077,38 @@ const createRuntime = (options) => {
|
|
|
5300
6077
|
|
|
5301
6078
|
//#endregion
|
|
5302
6079
|
//#region src/runtime/plugin-artifacts.ts
|
|
6080
|
+
const commandKey = (value) => String(value).trim().replace(/^[#/.]+\s*/, "").split(/\s+/)[0].toLowerCase();
|
|
5303
6081
|
async function registerPluginArtifacts(ctx) {
|
|
5304
6082
|
const helpService = ctx ? getService(ctx, Services.Help) : servicesRegistry["help"];
|
|
5305
6083
|
const plugins = getPluginMetadataList();
|
|
6084
|
+
const commandManager = getActiveCommandManager();
|
|
6085
|
+
const metadataByName = new Map(plugins.map((meta) => [meta.name, meta]));
|
|
6086
|
+
const pluginNames = new Set([...metadataByName.keys(), ...(commandManager?.catalog() ?? []).filter((item) => item.kind === "plugin").map((item) => item.plugin)]);
|
|
5306
6087
|
let helpCount = 0;
|
|
5307
|
-
for (const
|
|
5308
|
-
|
|
5309
|
-
|
|
6088
|
+
for (const pluginName of pluginNames) {
|
|
6089
|
+
const meta = metadataByName.get(pluginName);
|
|
6090
|
+
const generated = commandManager?.getPluginHelp(pluginName) ?? meta?.config?.help;
|
|
6091
|
+
const existing = helpService?.getHelp(pluginName);
|
|
6092
|
+
const help = generated ? {
|
|
6093
|
+
...generated,
|
|
6094
|
+
title: existing?.title || generated.title,
|
|
6095
|
+
description: existing?.description || generated.description,
|
|
6096
|
+
commands: [...existing?.commands ?? [], ...generated.commands.filter((command) => {
|
|
6097
|
+
const key = commandKey(command.cmd);
|
|
6098
|
+
return !(existing?.commands ?? []).some((item) => commandKey(item.cmd) === key);
|
|
6099
|
+
})]
|
|
6100
|
+
} : existing;
|
|
6101
|
+
if (help) if (helpService) {
|
|
6102
|
+
helpService.registerHelp(pluginName, help);
|
|
5310
6103
|
helpCount++;
|
|
5311
|
-
} else rootLogger.warn(`[plugin-artifacts] 帮助服务未加载,跳过插件 "${
|
|
5312
|
-
const required = meta
|
|
6104
|
+
} else rootLogger.warn(`[plugin-artifacts] 帮助服务未加载,跳过插件 "${pluginName}" 的帮助注册`);
|
|
6105
|
+
const required = meta?.config?.services;
|
|
5313
6106
|
if (required && required.length > 0) {
|
|
5314
6107
|
const missing = required.filter((name) => !servicesRegistry[name]);
|
|
5315
|
-
if (missing.length > 0) rootLogger.warn(`插件 "${
|
|
6108
|
+
if (missing.length > 0) rootLogger.warn(`插件 "${pluginName}" 声明依赖服务 [${missing.join(", ")}],但它们未加载`);
|
|
5316
6109
|
}
|
|
5317
6110
|
}
|
|
5318
|
-
rootLogger.info(`[plugin-artifacts] 已处理 ${
|
|
6111
|
+
rootLogger.info(`[plugin-artifacts] 已处理 ${pluginNames.size} 个插件,注册帮助 ${helpCount} 个`);
|
|
5319
6112
|
}
|
|
5320
6113
|
|
|
5321
6114
|
//#endregion
|
|
@@ -5446,6 +6239,7 @@ async function start(options = {}) {
|
|
|
5446
6239
|
};
|
|
5447
6240
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
5448
6241
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
6242
|
+
installProcessErrorGuards();
|
|
5449
6243
|
return { stop: (reason) => stopRuntime(reason) };
|
|
5450
6244
|
}
|
|
5451
6245
|
function readVersion() {
|
|
@@ -5458,6 +6252,15 @@ function readVersion() {
|
|
|
5458
6252
|
}
|
|
5459
6253
|
}
|
|
5460
6254
|
const version$1 = readVersion();
|
|
6255
|
+
function installProcessErrorGuards() {
|
|
6256
|
+
if (process.listenerCount("unhandledRejection") === 0) process.on("unhandledRejection", (reason) => {
|
|
6257
|
+
const detail = reason instanceof Error ? reason.stack ?? reason.message : reason;
|
|
6258
|
+
rootLogger.error(`[unhandledRejection] ${String(detail)}`);
|
|
6259
|
+
});
|
|
6260
|
+
if (process.listenerCount("uncaughtException") === 0) process.on("uncaughtException", (err) => {
|
|
6261
|
+
rootLogger.error(`[uncaughtException] ${err.stack ?? err.message ?? String(err)}`);
|
|
6262
|
+
});
|
|
6263
|
+
}
|
|
5461
6264
|
|
|
5462
6265
|
//#endregion
|
|
5463
6266
|
exports.AI_GEMINI_THINKING_LEVELS = AI_GEMINI_THINKING_LEVELS;
|
|
@@ -5471,8 +6274,10 @@ exports.BotRegistry = BotRegistry;
|
|
|
5471
6274
|
exports.CORE_PLUGINS = CORE_PLUGINS;
|
|
5472
6275
|
exports.CapabilityRegistry = CapabilityRegistry;
|
|
5473
6276
|
exports.ChromeUA = ChromeUA;
|
|
6277
|
+
exports.CommandManager = CommandManager;
|
|
5474
6278
|
exports.DriverShutdownError = DriverShutdownError;
|
|
5475
6279
|
exports.EventBus = EventBus;
|
|
6280
|
+
exports.EventCorrelator = EventCorrelator;
|
|
5476
6281
|
exports.HttpRequestError = HttpRequestError;
|
|
5477
6282
|
exports.MessageSegmentImpl = MessageSegmentImpl;
|
|
5478
6283
|
exports.MiokuContext = MiokuContext;
|
|
@@ -5582,6 +6387,7 @@ Object.defineProperty(exports, 'fs', {
|
|
|
5582
6387
|
return node_fs.default;
|
|
5583
6388
|
}
|
|
5584
6389
|
});
|
|
6390
|
+
exports.getActiveCommandManager = getActiveCommandManager;
|
|
5585
6391
|
exports.getAllPluginRuntimeStates = getAllPluginRuntimeStates;
|
|
5586
6392
|
exports.getBuiltinPlugins = getBuiltinPlugins;
|
|
5587
6393
|
exports.getConfigDir = getConfigDir;
|
|
@@ -5615,6 +6421,10 @@ exports.isBoolean = isBoolean;
|
|
|
5615
6421
|
exports.isBotEvent = isBotEvent;
|
|
5616
6422
|
exports.isDefined = isDefined;
|
|
5617
6423
|
exports.isEventAdmin = isEventAdmin;
|
|
6424
|
+
exports.isEventAdminConfigOnly = isEventAdminConfigOnly;
|
|
6425
|
+
exports.isEventGroupAdmin = isEventGroupAdmin;
|
|
6426
|
+
exports.isEventGroupOwner = isEventGroupOwner;
|
|
6427
|
+
exports.isEventMaster = isEventMaster;
|
|
5618
6428
|
exports.isEventOwner = isEventOwner;
|
|
5619
6429
|
exports.isEventOwnerOrAdmin = isEventOwnerOrAdmin;
|
|
5620
6430
|
exports.isFunction = isFunction;
|
|
@@ -5701,6 +6511,7 @@ exports.segment = segment;
|
|
|
5701
6511
|
exports.sendMessage = sendMessage;
|
|
5702
6512
|
exports.serviceManager = manager_default;
|
|
5703
6513
|
exports.servicesRegistry = servicesRegistry;
|
|
6514
|
+
exports.setActiveCommandManager = setActiveCommandManager;
|
|
5704
6515
|
exports.setBotCwd = setBotCwd;
|
|
5705
6516
|
exports.setBuiltinPlugins = setBuiltinPlugins;
|
|
5706
6517
|
exports.setPluginMetadata = setPluginMetadata;
|