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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildSpawnPlan, commandExists, resolveCommand, runCommand, runCommandInherit, version } from "./exec-
|
|
1
|
+
import { buildSpawnPlan, commandExists, resolveCommand, runCommand, runCommandInherit, version } from "./exec-C3QTbHLt.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as fs$5 from "node:fs";
|
|
4
4
|
import * as fs$4 from "node:fs";
|
|
@@ -13,7 +13,7 @@ import path, { default as path$1 } from "node:path";
|
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
14
|
import { hrtime } from "node:process";
|
|
15
15
|
import { box, colorize, colors, colors as colors$1, stripAnsi } from "consola/utils";
|
|
16
|
-
import crypto, { randomUUID } from "node:crypto";
|
|
16
|
+
import crypto, { createHash, randomUUID } from "node:crypto";
|
|
17
17
|
import nodeCron from "node-cron";
|
|
18
18
|
import mriLib from "mri";
|
|
19
19
|
import { Low } from "lowdb";
|
|
@@ -870,6 +870,8 @@ var AdapterContextImpl = class {
|
|
|
870
870
|
#capabilities;
|
|
871
871
|
#logger;
|
|
872
872
|
#emit;
|
|
873
|
+
#correlator;
|
|
874
|
+
#commands;
|
|
873
875
|
#pendingStarts = new Set();
|
|
874
876
|
constructor(options) {
|
|
875
877
|
this.#state = options.state;
|
|
@@ -879,6 +881,8 @@ var AdapterContextImpl = class {
|
|
|
879
881
|
this.#capabilities = options.capabilities;
|
|
880
882
|
this.#logger = options.logger;
|
|
881
883
|
this.#emit = options.emit;
|
|
884
|
+
this.#correlator = options.correlator;
|
|
885
|
+
this.#commands = options.commands;
|
|
882
886
|
}
|
|
883
887
|
registerBot(bot) {
|
|
884
888
|
return this.#bots.register(bot);
|
|
@@ -942,6 +946,7 @@ var AdapterContextImpl = class {
|
|
|
942
946
|
};
|
|
943
947
|
}
|
|
944
948
|
async dispatch(event) {
|
|
949
|
+
this.#correlator?.observe(event);
|
|
945
950
|
if (event.kind === "message") {
|
|
946
951
|
const senderId = event.user_id;
|
|
947
952
|
if (senderId != null) {
|
|
@@ -952,6 +957,12 @@ var AdapterContextImpl = class {
|
|
|
952
957
|
}
|
|
953
958
|
}
|
|
954
959
|
}
|
|
960
|
+
if (event.kind === "message") {
|
|
961
|
+
const duplicate = this.#correlator?.observationOf(event)?.duplicate ?? false;
|
|
962
|
+
if (!duplicate) {
|
|
963
|
+
if (await this.#commands.dispatch(event)) return;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
955
966
|
await this.#bus.dispatch(event);
|
|
956
967
|
}
|
|
957
968
|
listen(route, handler) {
|
|
@@ -1030,6 +1041,9 @@ var BotRegistry = class {
|
|
|
1030
1041
|
for (const entry of this.#bots.values()) if (entry.bot_id === key) return entry.bot;
|
|
1031
1042
|
return void 0;
|
|
1032
1043
|
}
|
|
1044
|
+
find(adapter, bot_id) {
|
|
1045
|
+
return this.#bots.get(connectedBotKey(adapter, bot_id))?.bot;
|
|
1046
|
+
}
|
|
1033
1047
|
all() {
|
|
1034
1048
|
return Array.from(this.#bots.values()).map((entry) => entry.bot);
|
|
1035
1049
|
}
|
|
@@ -1064,9 +1078,13 @@ var EventBus = class {
|
|
|
1064
1078
|
#nextOrder = 0;
|
|
1065
1079
|
#dispatched = 0;
|
|
1066
1080
|
#logger = null;
|
|
1081
|
+
#filter = null;
|
|
1067
1082
|
setLogger(logger) {
|
|
1068
1083
|
this.#logger = logger;
|
|
1069
1084
|
}
|
|
1085
|
+
setFilter(filter$1) {
|
|
1086
|
+
this.#filter = filter$1;
|
|
1087
|
+
}
|
|
1070
1088
|
/** 注册事件监听,返回取消注册函数 */
|
|
1071
1089
|
register(route, handler, options = {}) {
|
|
1072
1090
|
if (!route) throw new Error("EventBus.register: route must be a non-empty string");
|
|
@@ -1100,6 +1118,11 @@ var EventBus = class {
|
|
|
1100
1118
|
const matched = [];
|
|
1101
1119
|
for (const reg of this.#registrations) {
|
|
1102
1120
|
if (reg.disposed) continue;
|
|
1121
|
+
if (this.#filter) try {
|
|
1122
|
+
if (!this.#filter(reg, event)) continue;
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
this.#logger?.("error", `Event registration filter failed for "${reg.source}"`, err);
|
|
1125
|
+
}
|
|
1103
1126
|
if (isWildcardMatch(reg.route, event.type)) {
|
|
1104
1127
|
matched.push(reg);
|
|
1105
1128
|
continue;
|
|
@@ -1138,6 +1161,57 @@ var EventBus = class {
|
|
|
1138
1161
|
}
|
|
1139
1162
|
};
|
|
1140
1163
|
|
|
1164
|
+
//#endregion
|
|
1165
|
+
//#region src/runtime/permissions.ts
|
|
1166
|
+
const isObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1167
|
+
/** 从事件对象中提取发送者 id(支持裸 id / user_id / sender.user_id) */
|
|
1168
|
+
const toUserId = (value) => {
|
|
1169
|
+
if (typeof value === "number" || typeof value === "string" || typeof value === "bigint") return String(value);
|
|
1170
|
+
if (isObject$1(value)) {
|
|
1171
|
+
if ("user_id" in value) return toUserId(value.user_id);
|
|
1172
|
+
if ("sender" in value) {
|
|
1173
|
+
const sender = value.sender;
|
|
1174
|
+
if (isObject$1(sender) && "user_id" in sender) return toUserId(sender.user_id);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return void 0;
|
|
1178
|
+
};
|
|
1179
|
+
/** 是否群消息 */
|
|
1180
|
+
const isGroupMessage = (event) => isObject$1(event) && event.kind === "message" && event.message_type === "group";
|
|
1181
|
+
/** 当前群群主(仅限所在群) */
|
|
1182
|
+
const isEventGroupOwner = (event) => {
|
|
1183
|
+
if (!isGroupMessage(event)) return false;
|
|
1184
|
+
return event.sender?.role === "owner";
|
|
1185
|
+
};
|
|
1186
|
+
/** 当前群群主或群管理员(仅限所在群) */
|
|
1187
|
+
const isEventGroupAdmin = (event) => {
|
|
1188
|
+
if (!isGroupMessage(event)) return false;
|
|
1189
|
+
const role = event.sender?.role;
|
|
1190
|
+
return role === "owner" || role === "admin";
|
|
1191
|
+
};
|
|
1192
|
+
/** 仅看 owners 名单:bot 主人(无群身份加成) */
|
|
1193
|
+
const isEventMaster$1 = (event) => {
|
|
1194
|
+
const id = toUserId(event);
|
|
1195
|
+
if (!id) return false;
|
|
1196
|
+
return isOwner(id);
|
|
1197
|
+
};
|
|
1198
|
+
/** 仅看 admins 名单:配置管理员 */
|
|
1199
|
+
const isEventAdminConfigOnly = (event) => {
|
|
1200
|
+
const id = toUserId(event);
|
|
1201
|
+
if (!id) return false;
|
|
1202
|
+
return isAdmin(id);
|
|
1203
|
+
};
|
|
1204
|
+
/**
|
|
1205
|
+
* master 或当前群群主
|
|
1206
|
+
*/
|
|
1207
|
+
const isEventOwner$1 = (event) => isEventMaster$1(event) || isEventGroupOwner(event);
|
|
1208
|
+
/**
|
|
1209
|
+
* master、配置管理员、当前群群主或群管理员
|
|
1210
|
+
*/
|
|
1211
|
+
const isEventAdmin$1 = (event) => isEventOwner$1(event) || isEventAdminConfigOnly(event) || isEventGroupAdmin(event);
|
|
1212
|
+
const isEventOwnerOrAdmin$1 = (event) => isEventAdmin$1(event);
|
|
1213
|
+
const hasEventRight$1 = (event) => isEventOwnerOrAdmin$1(event);
|
|
1214
|
+
|
|
1141
1215
|
//#endregion
|
|
1142
1216
|
//#region src/utils.ts
|
|
1143
1217
|
const ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0";
|
|
@@ -1904,130 +1978,19 @@ const Services = {
|
|
|
1904
1978
|
};
|
|
1905
1979
|
|
|
1906
1980
|
//#endregion
|
|
1907
|
-
//#region src/runtime/
|
|
1908
|
-
|
|
1909
|
-
const
|
|
1910
|
-
const EVENT_TTL_MS = 6e4;
|
|
1911
|
-
const MAX_SIZE = 4096;
|
|
1912
|
-
const MAX_SEGMENTS = 16;
|
|
1913
|
-
const MAX_TEXT = 256;
|
|
1914
|
-
const contentFingerprintOf = (event) => {
|
|
1915
|
-
const parts = [];
|
|
1916
|
-
for (const seg of event.message) {
|
|
1917
|
-
if (parts.length >= MAX_SEGMENTS) break;
|
|
1918
|
-
if (seg.type === "reply") continue;
|
|
1919
|
-
const data = seg.data ?? {};
|
|
1920
|
-
if (seg.type === "text") {
|
|
1921
|
-
const text$1 = typeof data.text === "string" ? data.text : "";
|
|
1922
|
-
parts.push(`t:${text$1.slice(0, MAX_TEXT)}`);
|
|
1923
|
-
} else if (seg.type === "at") {
|
|
1924
|
-
const target = data.qq ?? data.target;
|
|
1925
|
-
parts.push(`a:${target == null ? "" : String(target)}`);
|
|
1926
|
-
} else if (seg.type === "face") parts.push("f");
|
|
1927
|
-
else parts.push(seg.type);
|
|
1928
|
-
}
|
|
1929
|
-
return parts.join("|");
|
|
1930
|
-
};
|
|
1931
|
-
const scopeKeyOf = (event, content) => [
|
|
1932
|
-
event.message_type ?? "",
|
|
1933
|
-
event.sender?.nickname?.trim() || event.user_id || "",
|
|
1934
|
-
content
|
|
1935
|
-
].join("|");
|
|
1936
|
-
const noticeKeyOf = (event) => [
|
|
1937
|
-
event.kind,
|
|
1938
|
-
event.identity.event_type,
|
|
1939
|
-
event.notice_type ?? "",
|
|
1940
|
-
event.sub_type ?? "",
|
|
1941
|
-
event.group_id ?? "",
|
|
1942
|
-
event.user_id ?? "",
|
|
1943
|
-
event.operator_id ?? "",
|
|
1944
|
-
event.identity.fingerprint ?? "",
|
|
1945
|
-
event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
|
|
1946
|
-
].join("|");
|
|
1947
|
-
const requestKeyOf = (event) => [
|
|
1948
|
-
event.kind,
|
|
1949
|
-
event.identity.event_type,
|
|
1950
|
-
event.request_type ?? "",
|
|
1951
|
-
event.sub_type ?? "",
|
|
1952
|
-
event.group_id ?? "",
|
|
1953
|
-
event.user_id ?? "",
|
|
1954
|
-
event.comment ?? "",
|
|
1955
|
-
event.identity.fingerprint ?? "",
|
|
1956
|
-
event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
|
|
1957
|
-
].join("|");
|
|
1981
|
+
//#region src/runtime/mioku-context.ts
|
|
1982
|
+
/** 仅看 owners 名单:bot 主人(无群身份加成) */
|
|
1983
|
+
const isEventMaster = (event) => isEventMaster$1(event);
|
|
1958
1984
|
/**
|
|
1959
|
-
*
|
|
1985
|
+
* master 或当前群群主。等价于 `ctx.command({ permission: "owner" })` 的判断。
|
|
1960
1986
|
*/
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
}
|
|
1969
|
-
isDuplicate(event) {
|
|
1970
|
-
let key;
|
|
1971
|
-
let ttl;
|
|
1972
|
-
if (event.kind === "message") {
|
|
1973
|
-
key = scopeKeyOf(event, contentFingerprintOf(event));
|
|
1974
|
-
ttl = MESSAGE_TTL_MS;
|
|
1975
|
-
} else if (event.kind === "notice") {
|
|
1976
|
-
key = noticeKeyOf(event);
|
|
1977
|
-
ttl = EVENT_TTL_MS;
|
|
1978
|
-
} else if (event.kind === "request") {
|
|
1979
|
-
key = requestKeyOf(event);
|
|
1980
|
-
ttl = EVENT_TTL_MS;
|
|
1981
|
-
} else return false;
|
|
1982
|
-
const now = Date.now();
|
|
1983
|
-
const existing = this.#entries.get(key);
|
|
1984
|
-
if (existing?.expiresAt != null && existing.expiresAt >= now) {
|
|
1985
|
-
this.#dropped++;
|
|
1986
|
-
return true;
|
|
1987
|
-
}
|
|
1988
|
-
this.#entries.set(key, { expiresAt: now + ttl });
|
|
1989
|
-
this.#inserts++;
|
|
1990
|
-
if (this.#inserts >= PRUNE_INTERVAL || this.#entries.size > MAX_SIZE) {
|
|
1991
|
-
this.#inserts = 0;
|
|
1992
|
-
this.#prune(now);
|
|
1993
|
-
}
|
|
1994
|
-
return false;
|
|
1995
|
-
}
|
|
1996
|
-
#prune(now) {
|
|
1997
|
-
for (const [key, entry] of this.#entries) if (entry.expiresAt < now) this.#entries.delete(key);
|
|
1998
|
-
}
|
|
1999
|
-
clear() {
|
|
2000
|
-
this.#entries.clear();
|
|
2001
|
-
}
|
|
2002
|
-
};
|
|
2003
|
-
|
|
2004
|
-
//#endregion
|
|
2005
|
-
//#region src/runtime/mioku-context.ts
|
|
2006
|
-
const isObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2007
|
-
/** 从事件对象中提取发送者 id(支持裸 id / user_id / sender.user_id) */
|
|
2008
|
-
const toUserId = (value) => {
|
|
2009
|
-
if (typeof value === "number" || typeof value === "string" || typeof value === "bigint") return String(value);
|
|
2010
|
-
if (isObject$1(value)) {
|
|
2011
|
-
if ("user_id" in value) return toUserId(value.user_id);
|
|
2012
|
-
if ("sender" in value) {
|
|
2013
|
-
const sender = value.sender;
|
|
2014
|
-
if (isObject$1(sender) && "user_id" in sender) return toUserId(sender.user_id);
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
return void 0;
|
|
2018
|
-
};
|
|
2019
|
-
const isEventOwner = (event) => {
|
|
2020
|
-
const id = toUserId(event);
|
|
2021
|
-
if (!id) return false;
|
|
2022
|
-
return isOwner(id);
|
|
2023
|
-
};
|
|
2024
|
-
const isEventAdmin = (event) => {
|
|
2025
|
-
const id = toUserId(event);
|
|
2026
|
-
if (!id) return false;
|
|
2027
|
-
return isAdmin(id);
|
|
2028
|
-
};
|
|
2029
|
-
const isEventOwnerOrAdmin = (event) => isEventOwner(event) || isEventAdmin(event);
|
|
2030
|
-
const hasEventRight = (event) => isEventOwnerOrAdmin(event);
|
|
1987
|
+
const isEventOwner = (event) => isEventOwner$1(event);
|
|
1988
|
+
/**
|
|
1989
|
+
* master、配置管理员、当前群群主或群管理员。等价于 `ctx.command({ permission: "admin" })` 的判断。
|
|
1990
|
+
*/
|
|
1991
|
+
const isEventAdmin = (event) => isEventAdmin$1(event);
|
|
1992
|
+
const isEventOwnerOrAdmin = (event) => isEventOwnerOrAdmin$1(event);
|
|
1993
|
+
const hasEventRight = (event) => hasEventRight$1(event);
|
|
2031
1994
|
const CTX_UTILS = {
|
|
2032
1995
|
localeDate,
|
|
2033
1996
|
localeTime,
|
|
@@ -2082,6 +2045,9 @@ var MiokuContext = class {
|
|
|
2082
2045
|
get pluginName() {
|
|
2083
2046
|
return this.#options.pluginName;
|
|
2084
2047
|
}
|
|
2048
|
+
get pluginId() {
|
|
2049
|
+
return this.#options.pluginId;
|
|
2050
|
+
}
|
|
2085
2051
|
/** 第一个已连接的 bot(多数单 bot 场景直接用这个) */
|
|
2086
2052
|
get bot() {
|
|
2087
2053
|
return this.#options.bots.all()[0];
|
|
@@ -2098,6 +2064,59 @@ var MiokuContext = class {
|
|
|
2098
2064
|
return this.#options.bots.pick(bot_id);
|
|
2099
2065
|
}
|
|
2100
2066
|
/**
|
|
2067
|
+
* 取事件所属的跨适配器关联记录。
|
|
2068
|
+
* 同一条逻辑消息被多个适配器/bot 投递时,可从这里读到全部参与方。
|
|
2069
|
+
*/
|
|
2070
|
+
correlation(event) {
|
|
2071
|
+
return this.#options.correlator?.observationOf(event)?.record;
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* 观察到该事件的全部已连接 bot(首个即 primary)。
|
|
2075
|
+
* 跨适配器去重后,被标记为重复的投递不会进入普通 handler,
|
|
2076
|
+
* 但这里依然能看到全部参与方。
|
|
2077
|
+
*/
|
|
2078
|
+
botsForEvent(event) {
|
|
2079
|
+
const record = this.correlation(event);
|
|
2080
|
+
if (!record) {
|
|
2081
|
+
const bot = event.bot;
|
|
2082
|
+
return bot ? [bot] : [];
|
|
2083
|
+
}
|
|
2084
|
+
const resolved = [];
|
|
2085
|
+
for (const participant of record.participants) {
|
|
2086
|
+
const bot = participant.botId ? this.#options.bots.find(participant.adapter, participant.botId) ?? this.#options.bots.pick(participant.botId) : void 0;
|
|
2087
|
+
if (bot && !resolved.includes(bot)) resolved.push(bot);
|
|
2088
|
+
}
|
|
2089
|
+
return resolved;
|
|
2090
|
+
}
|
|
2091
|
+
/** 事件里被 @ 到的、且本运行时已连接的 bot */
|
|
2092
|
+
mentionedBots(event) {
|
|
2093
|
+
if (event.kind !== "message") return [];
|
|
2094
|
+
const targets = new Set();
|
|
2095
|
+
for (const segment$1 of event.message) {
|
|
2096
|
+
if (segment$1.type !== "at") continue;
|
|
2097
|
+
const data = segment$1.data ?? {};
|
|
2098
|
+
const target = data.qq ?? data.target;
|
|
2099
|
+
if (target != null && String(target) !== "") targets.add(String(target));
|
|
2100
|
+
}
|
|
2101
|
+
if (targets.size === 0) return [];
|
|
2102
|
+
return this.#options.bots.all().filter((bot) => targets.has(String(bot.bot_id)));
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* 这条消息应当由哪个 bot 回应:优先「被 @ 的 bot」,其次是关联组里首个到达的 bot。
|
|
2106
|
+
* 插件可用它替代隐式的 `event.bot`,从而不受适配器到达顺序影响。
|
|
2107
|
+
*/
|
|
2108
|
+
pickReplyBot(event) {
|
|
2109
|
+
const mentioned = this.mentionedBots(event);
|
|
2110
|
+
if (mentioned.length > 0) return mentioned[0];
|
|
2111
|
+
const participants = this.botsForEvent(event);
|
|
2112
|
+
if (participants.length > 0) return participants[0];
|
|
2113
|
+
return event.bot;
|
|
2114
|
+
}
|
|
2115
|
+
/** 事件关联/去重的运行统计 */
|
|
2116
|
+
correlationStats() {
|
|
2117
|
+
return this.#options.correlator?.stats();
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2101
2120
|
* 获取事件引用回复的消息内容。
|
|
2102
2121
|
* 返回 null 表示没有引用、拿不到对应 bot 或消息已失效。
|
|
2103
2122
|
*/
|
|
@@ -2128,12 +2147,12 @@ var MiokuContext = class {
|
|
|
2128
2147
|
const inputRoutes = Array.isArray(route) ? route : [route];
|
|
2129
2148
|
const bypassDedup = inputRoutes.some((item) => item.startsWith("!"));
|
|
2130
2149
|
const routes = inputRoutes.map((item) => item.startsWith("!") ? item.slice(1) : item);
|
|
2131
|
-
const source = `plugin:${this.#options.
|
|
2150
|
+
const source = `plugin:${this.#options.pluginId}`;
|
|
2132
2151
|
const handledEvents = new WeakSet();
|
|
2133
|
-
const
|
|
2152
|
+
const correlator = bypassDedup ? void 0 : this.#options.correlator;
|
|
2134
2153
|
const wrappedHandler = async (event) => {
|
|
2135
2154
|
if (handledEvents.has(event)) return;
|
|
2136
|
-
if (
|
|
2155
|
+
if (correlator?.isDuplicate(event)) return;
|
|
2137
2156
|
handledEvents.add(event);
|
|
2138
2157
|
await handler(event);
|
|
2139
2158
|
};
|
|
@@ -2150,6 +2169,17 @@ var MiokuContext = class {
|
|
|
2150
2169
|
this.#addCleanup(dispose);
|
|
2151
2170
|
return dispose;
|
|
2152
2171
|
}
|
|
2172
|
+
command(definitionOrMatcher, handler, options = {}) {
|
|
2173
|
+
const dispose = typeof definitionOrMatcher === "object" && definitionOrMatcher !== null && "name" in definitionOrMatcher ? this.#options.commands.register(this.#options.pluginId, {
|
|
2174
|
+
...definitionOrMatcher,
|
|
2175
|
+
priority: definitionOrMatcher.priority ?? this.#options.priority
|
|
2176
|
+
}, this) : this.#options.commands.register(this.#options.pluginId, definitionOrMatcher, handler, {
|
|
2177
|
+
...options,
|
|
2178
|
+
priority: options.priority ?? this.#options.priority
|
|
2179
|
+
}, this);
|
|
2180
|
+
this.#addCleanup(dispose);
|
|
2181
|
+
return dispose;
|
|
2182
|
+
}
|
|
2153
2183
|
/** 注册 cron 定时任务,插件卸载时自动停止 */
|
|
2154
2184
|
cron(expression, handler) {
|
|
2155
2185
|
const task = nodeCron.schedule(expression, async (taskContext) => {
|
|
@@ -2206,6 +2236,10 @@ var MiokuContext = class {
|
|
|
2206
2236
|
get buses() {
|
|
2207
2237
|
return this.#options.bus;
|
|
2208
2238
|
}
|
|
2239
|
+
/** 当前运行时的命令管理器 */
|
|
2240
|
+
get commands() {
|
|
2241
|
+
return this.#options.commands;
|
|
2242
|
+
}
|
|
2209
2243
|
get segment() {
|
|
2210
2244
|
return segment;
|
|
2211
2245
|
}
|
|
@@ -2246,24 +2280,30 @@ var MiokuContext = class {
|
|
|
2246
2280
|
return noticeAdmins(this.#options.bots.all(), this.#options.config.admins, message, options);
|
|
2247
2281
|
}
|
|
2248
2282
|
isGroupMsg(event) {
|
|
2249
|
-
return isObject
|
|
2283
|
+
return isObject(event) && event.kind === "message" && event.message_type === "group";
|
|
2250
2284
|
}
|
|
2251
2285
|
isPrivateMsg(event) {
|
|
2252
|
-
return isObject
|
|
2286
|
+
return isObject(event) && event.kind === "message" && event.message_type === "private";
|
|
2287
|
+
}
|
|
2288
|
+
/** 事件发送者是否为 bot 主人(仅看 owners 名单,不识别群身份) */
|
|
2289
|
+
isMaster(event) {
|
|
2290
|
+
return isEventMaster(event);
|
|
2253
2291
|
}
|
|
2254
|
-
/**
|
|
2292
|
+
/** 事件发送者是否为 bot 主人或当前群群主;等价于 `permission: "owner"` */
|
|
2255
2293
|
isOwner(event) {
|
|
2256
2294
|
return isEventOwner(event);
|
|
2257
2295
|
}
|
|
2258
|
-
/**
|
|
2296
|
+
/** 事件发送者是否为 bot 主人、配置管理员、当前群群主或群管理员;等价于 `permission: "admin"` */
|
|
2259
2297
|
isAdmin(event) {
|
|
2260
2298
|
return isEventAdmin(event);
|
|
2261
2299
|
}
|
|
2300
|
+
/** 同 `isAdmin`,保留旧名称 */
|
|
2262
2301
|
isOwnerOrAdmin(event) {
|
|
2263
|
-
return
|
|
2302
|
+
return isEventAdmin(event);
|
|
2264
2303
|
}
|
|
2304
|
+
/** 同 `isAdmin`,别名 */
|
|
2265
2305
|
hasRight(event) {
|
|
2266
|
-
return
|
|
2306
|
+
return isEventAdmin(event);
|
|
2267
2307
|
}
|
|
2268
2308
|
async dispose() {
|
|
2269
2309
|
const all = Array.from(this.#cleanup);
|
|
@@ -2274,19 +2314,667 @@ var MiokuContext = class {
|
|
|
2274
2314
|
}
|
|
2275
2315
|
};
|
|
2276
2316
|
|
|
2317
|
+
//#endregion
|
|
2318
|
+
//#region src/runtime/event-correlator.ts
|
|
2319
|
+
const MESSAGE_TTL_MS = 15e3;
|
|
2320
|
+
const EVENT_TTL_MS = 6e4;
|
|
2321
|
+
const PRUNE_INTERVAL = 128;
|
|
2322
|
+
const MAX_SIZE = 4096;
|
|
2323
|
+
const MAX_SEGMENTS = 16;
|
|
2324
|
+
const MAX_TEXT = 256;
|
|
2325
|
+
const HEX32 = /[0-9a-f]{32}/i;
|
|
2326
|
+
const HEX40 = /[0-9a-f]{40}/i;
|
|
2327
|
+
const MEDIA_URL_QUERY_KEYS = [
|
|
2328
|
+
"fileid",
|
|
2329
|
+
"file_id",
|
|
2330
|
+
"md5",
|
|
2331
|
+
"fid"
|
|
2332
|
+
];
|
|
2333
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2334
|
+
const hashString = (input) => createHash("sha1").update(input).digest("hex").slice(0, 20);
|
|
2335
|
+
/** 稳定序列化:对象键排序、无多余空白,忽略跨适配器的排版差异 */
|
|
2336
|
+
const canonicalJson = (value) => {
|
|
2337
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
2338
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
2339
|
+
const obj = value;
|
|
2340
|
+
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(obj[key])}`).join(",")}}`;
|
|
2341
|
+
};
|
|
2342
|
+
/** 结构化载荷(json/xml/ark)的稳定摘要 */
|
|
2343
|
+
const structuredDigest = (raw) => {
|
|
2344
|
+
let value = raw;
|
|
2345
|
+
if (typeof raw === "string") {
|
|
2346
|
+
const trimmed = raw.trim();
|
|
2347
|
+
if (!trimmed) return "";
|
|
2348
|
+
try {
|
|
2349
|
+
value = JSON.parse(trimmed);
|
|
2350
|
+
} catch {
|
|
2351
|
+
return `s:${hashString(trimmed.slice(0, MAX_TEXT * 4))}`;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return `j:${hashString(canonicalJson(value))}`;
|
|
2355
|
+
};
|
|
2356
|
+
/** 去掉会过期/随会话变化的查询参数,保留跨适配器稳定的媒体定位 */
|
|
2357
|
+
const normalizeMediaUrl = (raw) => {
|
|
2358
|
+
try {
|
|
2359
|
+
const url = new URL(raw);
|
|
2360
|
+
const kept = new URLSearchParams();
|
|
2361
|
+
for (const key of MEDIA_URL_QUERY_KEYS) {
|
|
2362
|
+
const value = url.searchParams.get(key);
|
|
2363
|
+
if (value) kept.set(key, value);
|
|
2364
|
+
}
|
|
2365
|
+
const query = kept.toString();
|
|
2366
|
+
return `${url.host}${url.pathname}${query ? `?${query}` : ""}`;
|
|
2367
|
+
} catch {
|
|
2368
|
+
return raw;
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
const firstHex = (value, pattern) => {
|
|
2372
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
2373
|
+
const match$1 = value.match(pattern);
|
|
2374
|
+
return match$1 ? match$1[0].toLowerCase() : void 0;
|
|
2375
|
+
};
|
|
2376
|
+
/**
|
|
2377
|
+
* 媒体段的内容摘要:优先内容哈希(md5/sha1),其次是文件名里嵌的 md5,
|
|
2378
|
+
* 再次是规范化 URL。都拿不到时返回空串,由调用方退化为「只看段类型」。
|
|
2379
|
+
*/
|
|
2380
|
+
const mediaDigest = (data) => {
|
|
2381
|
+
const md5$1 = firstHex(data.md5, HEX32);
|
|
2382
|
+
if (md5$1) return `h:${md5$1}`;
|
|
2383
|
+
const sha1 = firstHex(data.sha1, HEX40);
|
|
2384
|
+
if (sha1) return `h:${sha1}`;
|
|
2385
|
+
for (const key of [
|
|
2386
|
+
"file_unique",
|
|
2387
|
+
"file_id",
|
|
2388
|
+
"fid",
|
|
2389
|
+
"id",
|
|
2390
|
+
"file",
|
|
2391
|
+
"name"
|
|
2392
|
+
]) {
|
|
2393
|
+
const hex = firstHex(data[key], HEX32);
|
|
2394
|
+
if (hex) return `h:${hex}`;
|
|
2395
|
+
}
|
|
2396
|
+
const url = data.url;
|
|
2397
|
+
if (typeof url === "string" && url) return `u:${normalizeMediaUrl(url)}`;
|
|
2398
|
+
for (const key of [
|
|
2399
|
+
"file",
|
|
2400
|
+
"file_id",
|
|
2401
|
+
"fid",
|
|
2402
|
+
"id",
|
|
2403
|
+
"name"
|
|
2404
|
+
]) {
|
|
2405
|
+
const value = data[key];
|
|
2406
|
+
if (typeof value === "string" && value) return `r:${value.toLowerCase()}`;
|
|
2407
|
+
}
|
|
2408
|
+
return "";
|
|
2409
|
+
};
|
|
2410
|
+
/** 只在载荷跨协议一致时才使用摘要的段类型 */
|
|
2411
|
+
const DIGEST_MEDIA_TYPES = new Set(["image", "flash"]);
|
|
2412
|
+
const segmentSignature = (segment$1) => {
|
|
2413
|
+
const data = isRecord(segment$1.data) ? segment$1.data : {};
|
|
2414
|
+
switch (segment$1.type) {
|
|
2415
|
+
case "text": {
|
|
2416
|
+
const text$1 = typeof data.text === "string" ? data.text : "";
|
|
2417
|
+
return `t:${text$1.slice(0, MAX_TEXT)}`;
|
|
2418
|
+
}
|
|
2419
|
+
case "at": {
|
|
2420
|
+
const target = data.qq ?? data.target;
|
|
2421
|
+
return `a:${target == null ? "" : String(target)}`;
|
|
2422
|
+
}
|
|
2423
|
+
case "face": {
|
|
2424
|
+
const id = data.id ?? data.face;
|
|
2425
|
+
return `f:${id == null ? "" : String(id)}`;
|
|
2426
|
+
}
|
|
2427
|
+
case "json":
|
|
2428
|
+
case "xml":
|
|
2429
|
+
case "ark": {
|
|
2430
|
+
const digest = structuredDigest(data.data ?? data.content ?? data);
|
|
2431
|
+
return digest ? `${segment$1.type}:${digest}` : segment$1.type;
|
|
2432
|
+
}
|
|
2433
|
+
default: {
|
|
2434
|
+
if (DIGEST_MEDIA_TYPES.has(segment$1.type)) {
|
|
2435
|
+
const digest = mediaDigest(data);
|
|
2436
|
+
return digest ? `${segment$1.type}:${digest}` : segment$1.type;
|
|
2437
|
+
}
|
|
2438
|
+
return segment$1.type;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
const contentSignatureOf = (event) => {
|
|
2443
|
+
const parts = [];
|
|
2444
|
+
for (const segment$1 of event.message) {
|
|
2445
|
+
if (parts.length >= MAX_SEGMENTS) break;
|
|
2446
|
+
if (segment$1.type === "reply") continue;
|
|
2447
|
+
parts.push(segmentSignature(segment$1));
|
|
2448
|
+
}
|
|
2449
|
+
return parts.join("|");
|
|
2450
|
+
};
|
|
2451
|
+
const messageKeyOf = (event) => {
|
|
2452
|
+
const type = event.message_type ?? "";
|
|
2453
|
+
const scope = event.group_id ?? event.user_id ?? "";
|
|
2454
|
+
const sender = event.user_id ?? event.sender?.user_id ?? "";
|
|
2455
|
+
return [
|
|
2456
|
+
"m",
|
|
2457
|
+
type,
|
|
2458
|
+
scope,
|
|
2459
|
+
sender,
|
|
2460
|
+
contentSignatureOf(event)
|
|
2461
|
+
].join("|");
|
|
2462
|
+
};
|
|
2463
|
+
const noticeKeyOf = (event) => [
|
|
2464
|
+
"n",
|
|
2465
|
+
event.identity.event_type,
|
|
2466
|
+
event.notice_type ?? "",
|
|
2467
|
+
event.sub_type ?? "",
|
|
2468
|
+
event.group_id ?? "",
|
|
2469
|
+
event.user_id ?? "",
|
|
2470
|
+
event.operator_id ?? "",
|
|
2471
|
+
event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
|
|
2472
|
+
].join("|");
|
|
2473
|
+
const requestKeyOf = (event) => [
|
|
2474
|
+
"r",
|
|
2475
|
+
event.identity.event_type,
|
|
2476
|
+
event.request_type ?? "",
|
|
2477
|
+
event.sub_type ?? "",
|
|
2478
|
+
event.group_id ?? "",
|
|
2479
|
+
event.user_id ?? "",
|
|
2480
|
+
event.comment ?? "",
|
|
2481
|
+
event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
|
|
2482
|
+
].join("|");
|
|
2483
|
+
const participantOf = (event) => ({
|
|
2484
|
+
adapter: event.identity.adapter,
|
|
2485
|
+
botId: event.identity.bot_id,
|
|
2486
|
+
eventType: event.identity.event_type,
|
|
2487
|
+
messageId: event.identity.message_id ?? event.identity.native_event_id
|
|
2488
|
+
});
|
|
2489
|
+
const sameParticipant = (a, b) => a.adapter === b.adapter && (a.botId ?? "") === (b.botId ?? "") && (a.messageId ?? "") === (b.messageId ?? "");
|
|
2490
|
+
/**
|
|
2491
|
+
* 跨适配器事件关联器
|
|
2492
|
+
*/
|
|
2493
|
+
var EventCorrelator = class {
|
|
2494
|
+
#groups = new Map();
|
|
2495
|
+
#byEvent = new WeakMap();
|
|
2496
|
+
#logger;
|
|
2497
|
+
#maxSize;
|
|
2498
|
+
#inserts = 0;
|
|
2499
|
+
#observed = 0;
|
|
2500
|
+
#duplicates = 0;
|
|
2501
|
+
#evicted = 0;
|
|
2502
|
+
constructor(options = {}) {
|
|
2503
|
+
this.#logger = options.logger;
|
|
2504
|
+
this.#maxSize = options.maxSize ?? MAX_SIZE;
|
|
2505
|
+
}
|
|
2506
|
+
/** 观察一个事件,登记它所属的关联组并返回其定位(同一事件只登记一次) */
|
|
2507
|
+
observe(event) {
|
|
2508
|
+
const cached = this.#byEvent.get(event);
|
|
2509
|
+
if (cached) return cached;
|
|
2510
|
+
const located = this.#locate(event);
|
|
2511
|
+
if (!located) return null;
|
|
2512
|
+
const { key, ttl } = located;
|
|
2513
|
+
const now = Date.now();
|
|
2514
|
+
this.#observed++;
|
|
2515
|
+
let group = this.#groups.get(key);
|
|
2516
|
+
let duplicate = false;
|
|
2517
|
+
if (group && group.expiresAt >= now) {
|
|
2518
|
+
duplicate = true;
|
|
2519
|
+
this.#duplicates++;
|
|
2520
|
+
const participant = participantOf(event);
|
|
2521
|
+
if (!group.participants.some((item) => sameParticipant(item, participant))) group.participants.push(participant);
|
|
2522
|
+
this.#logger?.debug(`去重命中: key=${key} adapter=${participant.adapter} bot=${participant.botId ?? ""} primary=${group.primary.adapter}:${group.primary.botId ?? ""}`);
|
|
2523
|
+
} else {
|
|
2524
|
+
const participant = participantOf(event);
|
|
2525
|
+
group = {
|
|
2526
|
+
key,
|
|
2527
|
+
expiresAt: now + ttl,
|
|
2528
|
+
firstSeenAt: now,
|
|
2529
|
+
primary: participant,
|
|
2530
|
+
participants: [participant]
|
|
2531
|
+
};
|
|
2532
|
+
this.#groups.set(key, group);
|
|
2533
|
+
this.#inserts++;
|
|
2534
|
+
if (this.#inserts >= PRUNE_INTERVAL || this.#groups.size > this.#maxSize) {
|
|
2535
|
+
this.#inserts = 0;
|
|
2536
|
+
this.#prune(now);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
const observation = {
|
|
2540
|
+
record: group,
|
|
2541
|
+
primary: !duplicate,
|
|
2542
|
+
duplicate
|
|
2543
|
+
};
|
|
2544
|
+
this.#byEvent.set(event, observation);
|
|
2545
|
+
return observation;
|
|
2546
|
+
}
|
|
2547
|
+
/** 读取某事件此前登记的关联定位(未登记或已过期时返回 undefined) */
|
|
2548
|
+
observationOf(event) {
|
|
2549
|
+
const observation = this.#byEvent.get(event);
|
|
2550
|
+
if (!observation) return void 0;
|
|
2551
|
+
if (observation.record.expiresAt < Date.now()) return void 0;
|
|
2552
|
+
return observation;
|
|
2553
|
+
}
|
|
2554
|
+
/** 该事件是否属于关联组里后续到达的重复投递(未登记时会先登记) */
|
|
2555
|
+
isDuplicate(event) {
|
|
2556
|
+
return this.observe(event)?.duplicate === true;
|
|
2557
|
+
}
|
|
2558
|
+
stats() {
|
|
2559
|
+
return {
|
|
2560
|
+
groups: this.#groups.size,
|
|
2561
|
+
observed: this.#observed,
|
|
2562
|
+
duplicates: this.#duplicates,
|
|
2563
|
+
evicted: this.#evicted
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
clear() {
|
|
2567
|
+
this.#groups.clear();
|
|
2568
|
+
}
|
|
2569
|
+
#locate(event) {
|
|
2570
|
+
if (event.kind === "message") return {
|
|
2571
|
+
key: messageKeyOf(event),
|
|
2572
|
+
ttl: MESSAGE_TTL_MS
|
|
2573
|
+
};
|
|
2574
|
+
if (event.kind === "notice") return {
|
|
2575
|
+
key: noticeKeyOf(event),
|
|
2576
|
+
ttl: EVENT_TTL_MS
|
|
2577
|
+
};
|
|
2578
|
+
if (event.kind === "request") return {
|
|
2579
|
+
key: requestKeyOf(event),
|
|
2580
|
+
ttl: EVENT_TTL_MS
|
|
2581
|
+
};
|
|
2582
|
+
return null;
|
|
2583
|
+
}
|
|
2584
|
+
#prune(now) {
|
|
2585
|
+
for (const [key, group] of this.#groups) if (group.expiresAt < now) this.#groups.delete(key);
|
|
2586
|
+
if (this.#groups.size <= this.#maxSize) return;
|
|
2587
|
+
const drop = this.#groups.size - this.#maxSize;
|
|
2588
|
+
let dropped = 0;
|
|
2589
|
+
for (const key of this.#groups.keys()) {
|
|
2590
|
+
this.#groups.delete(key);
|
|
2591
|
+
this.#evicted++;
|
|
2592
|
+
if (++dropped >= drop) break;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
};
|
|
2596
|
+
|
|
2597
|
+
//#endregion
|
|
2598
|
+
//#region src/runtime/commands.ts
|
|
2599
|
+
const normalizeRole = (role) => {
|
|
2600
|
+
const value = String(role ?? "member").trim().toLowerCase();
|
|
2601
|
+
if (value === "master" || value === "owner" || value === "admin") return value;
|
|
2602
|
+
return "member";
|
|
2603
|
+
};
|
|
2604
|
+
const userIdOf = (event) => toUserId(event);
|
|
2605
|
+
const textHookMatches = (text$1, raw) => {
|
|
2606
|
+
const source = String(raw ?? "").trim();
|
|
2607
|
+
if (!source) return false;
|
|
2608
|
+
if (source.startsWith("/") && source.length > 1) {
|
|
2609
|
+
const end = source.lastIndexOf("/");
|
|
2610
|
+
if (end > 0) try {
|
|
2611
|
+
const flags = source.slice(end + 1);
|
|
2612
|
+
const regex = new RegExp(source.slice(1, end), flags);
|
|
2613
|
+
return regex.test(text$1);
|
|
2614
|
+
} catch {
|
|
2615
|
+
return false;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
return text$1 === source || text$1.startsWith(source);
|
|
2619
|
+
};
|
|
2620
|
+
const routeMatches = (event, route) => {
|
|
2621
|
+
if (!route) return false;
|
|
2622
|
+
return event.type === route || event.routes.some((item) => item === route);
|
|
2623
|
+
};
|
|
2624
|
+
const parsePrefixes = (prefixes, defaultPrefix) => {
|
|
2625
|
+
if (prefixes === false) return ["", defaultPrefix];
|
|
2626
|
+
const values = Array.isArray(prefixes) ? prefixes : [prefixes];
|
|
2627
|
+
const normalized = values.map((value) => String(value)).filter((value, index, all) => all.indexOf(value) === index);
|
|
2628
|
+
return normalized.length > 0 ? normalized : [defaultPrefix];
|
|
2629
|
+
};
|
|
2630
|
+
const commandBody = (text$1, prefixes) => {
|
|
2631
|
+
const prefix = [...prefixes].sort((a, b) => b.length - a.length).find((item) => item === "" || text$1.startsWith(item));
|
|
2632
|
+
if (prefix === void 0) return void 0;
|
|
2633
|
+
return text$1.slice(prefix.length).trimStart();
|
|
2634
|
+
};
|
|
2635
|
+
const stringMatch = (body, matcher) => {
|
|
2636
|
+
const value = matcher.trim();
|
|
2637
|
+
if (!value) return void 0;
|
|
2638
|
+
if (body !== value && !(body.startsWith(value) && /\s/.test(body[value.length] ?? ""))) return void 0;
|
|
2639
|
+
const rest = body.slice(value.length).trim();
|
|
2640
|
+
return { args: rest ? rest.split(/\s+/) : [] };
|
|
2641
|
+
};
|
|
2642
|
+
const commandKey$1 = (value) => String(value).trim().replace(/^[#/.]+\s*/, "").split(/\s+/)[0].toLowerCase();
|
|
2643
|
+
const displayCommand = (command, defaultPrefix) => {
|
|
2644
|
+
if (command.prefixes === false) return command.name;
|
|
2645
|
+
const prefixes = command.prefixes === void 0 ? [defaultPrefix] : Array.isArray(command.prefixes) ? command.prefixes : [command.prefixes];
|
|
2646
|
+
return `${prefixes.find((prefix) => prefix.length > 0) ?? ""}${command.name}`;
|
|
2647
|
+
};
|
|
2648
|
+
var CommandManager = class {
|
|
2649
|
+
#getDefaultPrefix;
|
|
2650
|
+
#getOwners;
|
|
2651
|
+
#getAdmins;
|
|
2652
|
+
#logger;
|
|
2653
|
+
#commands = [];
|
|
2654
|
+
#legacy = new Map();
|
|
2655
|
+
#access;
|
|
2656
|
+
#nextOrder = 0;
|
|
2657
|
+
constructor(options) {
|
|
2658
|
+
this.#getDefaultPrefix = options.getDefaultPrefix;
|
|
2659
|
+
this.#getOwners = options.getOwners;
|
|
2660
|
+
this.#getAdmins = options.getAdmins;
|
|
2661
|
+
this.#logger = options.logger;
|
|
2662
|
+
}
|
|
2663
|
+
register(plugin, definitionOrMatcher, handlerOrContext, optionsOrContext = {}, context) {
|
|
2664
|
+
const isDefinition = typeof definitionOrMatcher === "object" && definitionOrMatcher !== null && "name" in definitionOrMatcher;
|
|
2665
|
+
const shortcutOptions = isDefinition ? {} : optionsOrContext;
|
|
2666
|
+
const resolvedContext = isDefinition ? handlerOrContext : context;
|
|
2667
|
+
const handler = isDefinition ? void 0 : handlerOrContext;
|
|
2668
|
+
const definition = isDefinition ? definitionOrMatcher : {
|
|
2669
|
+
...shortcutOptions,
|
|
2670
|
+
name: shortcutOptions.name ?? String(definitionOrMatcher),
|
|
2671
|
+
match: definitionOrMatcher,
|
|
2672
|
+
handler
|
|
2673
|
+
};
|
|
2674
|
+
if (typeof definition.handler !== "function") throw new Error("CommandManager.register: handler must be a function");
|
|
2675
|
+
if (!resolvedContext) throw new Error("CommandManager.register: context is required");
|
|
2676
|
+
const name = String(definition.name ?? "").trim();
|
|
2677
|
+
if (!name) throw new Error("CommandManager.register: name is required");
|
|
2678
|
+
const matcher = definition.match ?? name;
|
|
2679
|
+
const command = {
|
|
2680
|
+
plugin,
|
|
2681
|
+
id: String(definition.id ?? name).trim() || name,
|
|
2682
|
+
name,
|
|
2683
|
+
aliases: (definition.aliases ?? []).map((item) => String(item).trim()).filter(Boolean),
|
|
2684
|
+
matcher,
|
|
2685
|
+
description: String(definition.description ?? "").trim(),
|
|
2686
|
+
usage: definition.usage,
|
|
2687
|
+
permission: normalizeRole(definition.permission),
|
|
2688
|
+
priority: typeof definition.priority === "number" && !Number.isNaN(definition.priority) ? definition.priority : 0,
|
|
2689
|
+
prefixes: definition.prefixes,
|
|
2690
|
+
handler: definition.handler,
|
|
2691
|
+
order: this.#nextOrder++,
|
|
2692
|
+
context: resolvedContext
|
|
2693
|
+
};
|
|
2694
|
+
this.#commands.push(command);
|
|
2695
|
+
return () => {
|
|
2696
|
+
const index = this.#commands.indexOf(command);
|
|
2697
|
+
if (index >= 0) this.#commands.splice(index, 1);
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
setAccessControl(config) {
|
|
2701
|
+
this.#access = config;
|
|
2702
|
+
}
|
|
2703
|
+
syncMetadata(metadata) {
|
|
2704
|
+
this.#legacy.set(metadata.name, { metadata });
|
|
2705
|
+
}
|
|
2706
|
+
removePlugin(plugin) {
|
|
2707
|
+
this.#legacy.delete(plugin);
|
|
2708
|
+
for (let i = this.#commands.length - 1; i >= 0; i--) if (this.#commands[i].plugin === plugin) this.#commands.splice(i, 1);
|
|
2709
|
+
}
|
|
2710
|
+
clear() {
|
|
2711
|
+
this.#commands.length = 0;
|
|
2712
|
+
this.#legacy.clear();
|
|
2713
|
+
this.#access = void 0;
|
|
2714
|
+
}
|
|
2715
|
+
list() {
|
|
2716
|
+
return this.#orderedCommands();
|
|
2717
|
+
}
|
|
2718
|
+
#orderedCommands() {
|
|
2719
|
+
return [...this.#commands].sort((a, b) => a.priority - b.priority || a.order - b.order);
|
|
2720
|
+
}
|
|
2721
|
+
getPluginHelp(plugin) {
|
|
2722
|
+
const metadata = this.#legacy.get(plugin)?.metadata;
|
|
2723
|
+
const runtime = this.#commands.filter((item) => item.plugin === plugin);
|
|
2724
|
+
if (!metadata?.config.help && runtime.length === 0) return void 0;
|
|
2725
|
+
const commands = [];
|
|
2726
|
+
const seen = new Set();
|
|
2727
|
+
const runtimeByKey = new Map(runtime.map((command) => [commandKey$1(command.name), command]));
|
|
2728
|
+
for (const command of metadata?.config.help?.commands ?? []) {
|
|
2729
|
+
const key = commandKey$1(command.cmd);
|
|
2730
|
+
const runtimeCommand = runtimeByKey.get(key);
|
|
2731
|
+
if (runtimeCommand) {
|
|
2732
|
+
const cmd = displayCommand(runtimeCommand, this.#getDefaultPrefix());
|
|
2733
|
+
seen.add(key);
|
|
2734
|
+
commands.push({
|
|
2735
|
+
cmd,
|
|
2736
|
+
desc: runtimeCommand.description || command.desc || cmd,
|
|
2737
|
+
usage: runtimeCommand.usage ?? command.usage,
|
|
2738
|
+
role: runtimeCommand.permission
|
|
2739
|
+
});
|
|
2740
|
+
} else if (!seen.has(key)) {
|
|
2741
|
+
seen.add(key);
|
|
2742
|
+
commands.push(command);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
for (const command of runtime) {
|
|
2746
|
+
const cmd = displayCommand(command, this.#getDefaultPrefix());
|
|
2747
|
+
if (seen.has(commandKey$1(cmd))) continue;
|
|
2748
|
+
seen.add(commandKey$1(cmd));
|
|
2749
|
+
commands.push({
|
|
2750
|
+
cmd,
|
|
2751
|
+
desc: command.description || cmd,
|
|
2752
|
+
usage: command.usage,
|
|
2753
|
+
role: command.permission
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
const help = metadata?.config.help;
|
|
2757
|
+
return {
|
|
2758
|
+
title: help?.title || plugin,
|
|
2759
|
+
description: help?.description || metadata?.description || "",
|
|
2760
|
+
commands
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
catalog() {
|
|
2764
|
+
const items = [];
|
|
2765
|
+
const plugins = new Set([...this.#legacy.keys(), ...this.#commands.map((item) => item.plugin)]);
|
|
2766
|
+
for (const plugin of plugins) {
|
|
2767
|
+
const metadata = this.#legacy.get(plugin)?.metadata;
|
|
2768
|
+
const help = metadata?.config.help;
|
|
2769
|
+
items.push({
|
|
2770
|
+
kind: "plugin",
|
|
2771
|
+
plugin,
|
|
2772
|
+
id: plugin,
|
|
2773
|
+
label: help?.title || plugin,
|
|
2774
|
+
desc: help?.description || metadata?.description,
|
|
2775
|
+
source: metadata ? "manifest" : "runtime"
|
|
2776
|
+
});
|
|
2777
|
+
const seen = new Set();
|
|
2778
|
+
for (const command of this.#commands.filter((item) => item.plugin === plugin)) {
|
|
2779
|
+
seen.add(commandKey$1(command.id));
|
|
2780
|
+
items.push({
|
|
2781
|
+
kind: "command",
|
|
2782
|
+
plugin,
|
|
2783
|
+
id: command.id,
|
|
2784
|
+
label: command.name,
|
|
2785
|
+
desc: command.description,
|
|
2786
|
+
usage: command.usage,
|
|
2787
|
+
permission: command.permission,
|
|
2788
|
+
priority: command.priority,
|
|
2789
|
+
match: command.matcher instanceof RegExp ? command.matcher.toString() : command.matcher,
|
|
2790
|
+
source: "runtime"
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
for (const hook of metadata?.config.accessHooks ?? []) {
|
|
2794
|
+
if (seen.has(commandKey$1(hook.id))) continue;
|
|
2795
|
+
seen.add(commandKey$1(hook.id));
|
|
2796
|
+
items.push({
|
|
2797
|
+
kind: "command",
|
|
2798
|
+
plugin,
|
|
2799
|
+
id: hook.id,
|
|
2800
|
+
label: hook.id,
|
|
2801
|
+
desc: hook.description,
|
|
2802
|
+
match: hook.match,
|
|
2803
|
+
event: hook.event,
|
|
2804
|
+
source: "manifest"
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
for (const command of help?.commands ?? []) {
|
|
2808
|
+
if (seen.has(commandKey$1(command.cmd))) continue;
|
|
2809
|
+
seen.add(commandKey$1(command.cmd));
|
|
2810
|
+
items.push({
|
|
2811
|
+
kind: "command",
|
|
2812
|
+
plugin,
|
|
2813
|
+
id: command.cmd,
|
|
2814
|
+
label: command.cmd,
|
|
2815
|
+
desc: command.desc,
|
|
2816
|
+
usage: command.usage,
|
|
2817
|
+
permission: command.role,
|
|
2818
|
+
source: "manifest"
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
return items.sort((a, b) => a.kind === b.kind ? a.plugin.localeCompare(b.plugin) || a.id.localeCompare(b.id) : a.kind === "plugin" ? -1 : 1);
|
|
2823
|
+
}
|
|
2824
|
+
async dispatch(event) {
|
|
2825
|
+
const text$1 = String(event.message?.text?.() ?? "").trim();
|
|
2826
|
+
if (!text$1) return false;
|
|
2827
|
+
const match$1 = this.#find(text$1);
|
|
2828
|
+
if (!match$1) return false;
|
|
2829
|
+
if (!this.#canUse(match$1.command, event)) return true;
|
|
2830
|
+
try {
|
|
2831
|
+
await match$1.command.handler({
|
|
2832
|
+
ctx: match$1.command.context,
|
|
2833
|
+
event,
|
|
2834
|
+
command: match$1.command,
|
|
2835
|
+
text: text$1,
|
|
2836
|
+
body: match$1.body,
|
|
2837
|
+
args: match$1.args,
|
|
2838
|
+
match: match$1.match
|
|
2839
|
+
});
|
|
2840
|
+
} catch (error) {
|
|
2841
|
+
this.#logger?.error(`Command "${match$1.command.plugin}:${match$1.command.id}" failed`, error);
|
|
2842
|
+
}
|
|
2843
|
+
return true;
|
|
2844
|
+
}
|
|
2845
|
+
shouldDispatch(source, event) {
|
|
2846
|
+
if (!source.startsWith("plugin:")) return true;
|
|
2847
|
+
const plugin = source.slice(7);
|
|
2848
|
+
const metadata = this.#legacy.get(plugin)?.metadata;
|
|
2849
|
+
if (!metadata) return this.#pluginAllowed(plugin, event);
|
|
2850
|
+
if (!this.#pluginAllowed(plugin, event)) return false;
|
|
2851
|
+
const text$1 = event.kind === "message" ? String(event.message.text()).trim() : "";
|
|
2852
|
+
const matches = (metadata.config.accessHooks ?? []).filter((hook) => event.kind === "message" && textHookMatches(text$1, hook.match) || routeMatches(event, hook.event));
|
|
2853
|
+
return matches.every((hook) => this.#commandAllowed(plugin, hook.id, event));
|
|
2854
|
+
}
|
|
2855
|
+
#find(text$1) {
|
|
2856
|
+
for (const command of this.#orderedCommands()) {
|
|
2857
|
+
const prefixes = parsePrefixes(command.prefixes ?? this.#getDefaultPrefix(), this.#getDefaultPrefix());
|
|
2858
|
+
const body = commandBody(text$1, prefixes);
|
|
2859
|
+
if (body === void 0) continue;
|
|
2860
|
+
if (typeof command.matcher === "string") {
|
|
2861
|
+
const candidates = [
|
|
2862
|
+
command.matcher,
|
|
2863
|
+
command.name,
|
|
2864
|
+
...command.aliases
|
|
2865
|
+
];
|
|
2866
|
+
for (const candidate of candidates) {
|
|
2867
|
+
const result = stringMatch(body, candidate);
|
|
2868
|
+
if (result) return {
|
|
2869
|
+
command,
|
|
2870
|
+
body,
|
|
2871
|
+
args: result.args
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
continue;
|
|
2875
|
+
}
|
|
2876
|
+
command.matcher.lastIndex = 0;
|
|
2877
|
+
const matched = body.match(command.matcher);
|
|
2878
|
+
if (!matched) continue;
|
|
2879
|
+
const args = matched.length > 1 ? matched.slice(1).filter((item) => item != null) : body.slice(matched[0].length).trim().split(/\s+/).filter(Boolean);
|
|
2880
|
+
return {
|
|
2881
|
+
command,
|
|
2882
|
+
body,
|
|
2883
|
+
args,
|
|
2884
|
+
match: matched
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
return void 0;
|
|
2888
|
+
}
|
|
2889
|
+
#canUse(command, event) {
|
|
2890
|
+
const userId = userIdOf(event);
|
|
2891
|
+
const master = Boolean(userId && this.#isMaster(userId));
|
|
2892
|
+
const owner = master || this.#isGroupOwner(event);
|
|
2893
|
+
const admin = owner || Boolean(userId && this.#isAdmin(userId)) || this.#isGroupAdmin(event);
|
|
2894
|
+
if (command.permission === "master") {
|
|
2895
|
+
if (!master) return false;
|
|
2896
|
+
} else if (command.permission === "owner") {
|
|
2897
|
+
if (!owner) return false;
|
|
2898
|
+
} else if (command.permission === "admin" && !admin) return false;
|
|
2899
|
+
return this.#commandAllowed(command.plugin, command.id, event);
|
|
2900
|
+
}
|
|
2901
|
+
#pluginAllowed(plugin, event) {
|
|
2902
|
+
const userId = event.kind === "message" ? userIdOf(event) : "user_id" in event ? event.user_id : void 0;
|
|
2903
|
+
if (userId && (this.#isMaster(userId) || this.#isAdmin(userId)) || this.#isGroupOwner(event) || this.#isGroupAdmin(event)) return true;
|
|
2904
|
+
return this.#resolveAction(plugin, void 0, event) !== "block";
|
|
2905
|
+
}
|
|
2906
|
+
#commandAllowed(plugin, command, event) {
|
|
2907
|
+
const userId = event.kind === "message" ? userIdOf(event) : "user_id" in event ? event.user_id : void 0;
|
|
2908
|
+
if (userId && (this.#isMaster(userId) || this.#isAdmin(userId)) || this.#isGroupOwner(event) || this.#isGroupAdmin(event)) return true;
|
|
2909
|
+
return this.#resolveAction(plugin, command, event) !== "block";
|
|
2910
|
+
}
|
|
2911
|
+
#resolveAction(plugin, command, event) {
|
|
2912
|
+
const access = this.#access;
|
|
2913
|
+
if (!access) return void 0;
|
|
2914
|
+
const scopes = [
|
|
2915
|
+
(() => {
|
|
2916
|
+
const userId = event.kind === "message" ? userIdOf(event) : "user_id" in event && event.user_id ? String(event.user_id) : void 0;
|
|
2917
|
+
return userId ? access.users[userId] : void 0;
|
|
2918
|
+
})(),
|
|
2919
|
+
(() => {
|
|
2920
|
+
const groupId = "group_id" in event && event.group_id ? String(event.group_id) : void 0;
|
|
2921
|
+
return groupId ? access.groups[groupId] : void 0;
|
|
2922
|
+
})(),
|
|
2923
|
+
access.global
|
|
2924
|
+
];
|
|
2925
|
+
for (const scope of scopes) {
|
|
2926
|
+
if (!scope) continue;
|
|
2927
|
+
const commandRule = command ? scope.commands?.[plugin]?.[command]?.action : void 0;
|
|
2928
|
+
if (commandRule) return commandRule;
|
|
2929
|
+
const pluginRule = scope.plugins?.[plugin]?.action;
|
|
2930
|
+
if (pluginRule) return pluginRule;
|
|
2931
|
+
}
|
|
2932
|
+
return void 0;
|
|
2933
|
+
}
|
|
2934
|
+
#isMaster(userId) {
|
|
2935
|
+
return this.#owners().includes(userId);
|
|
2936
|
+
}
|
|
2937
|
+
#isAdmin(userId) {
|
|
2938
|
+
return this.#isMaster(userId) || this.#admins().includes(userId);
|
|
2939
|
+
}
|
|
2940
|
+
#isGroupOwner(event) {
|
|
2941
|
+
return isEventGroupOwner(event);
|
|
2942
|
+
}
|
|
2943
|
+
#isGroupAdmin(event) {
|
|
2944
|
+
return isEventGroupAdmin(event);
|
|
2945
|
+
}
|
|
2946
|
+
#owners() {
|
|
2947
|
+
return this.#configIds("owners");
|
|
2948
|
+
}
|
|
2949
|
+
#admins() {
|
|
2950
|
+
return this.#configIds("admins");
|
|
2951
|
+
}
|
|
2952
|
+
#configIds(key) {
|
|
2953
|
+
return [...key === "owners" ? this.#getOwners() : this.#getAdmins()].map(String);
|
|
2954
|
+
}
|
|
2955
|
+
};
|
|
2956
|
+
let activeCommandManager;
|
|
2957
|
+
const setActiveCommandManager = (manager) => {
|
|
2958
|
+
activeCommandManager = manager;
|
|
2959
|
+
};
|
|
2960
|
+
const getActiveCommandManager = () => activeCommandManager;
|
|
2961
|
+
|
|
2277
2962
|
//#endregion
|
|
2278
2963
|
//#region src/runtime/plugin-metadata.ts
|
|
2279
2964
|
const store$1 = getOrCreate("plugin-metadata", () => new Map());
|
|
2280
2965
|
const setPluginMetadata = (meta) => {
|
|
2281
2966
|
store$1.set(meta.name, meta);
|
|
2967
|
+
getActiveCommandManager()?.syncMetadata(meta);
|
|
2282
2968
|
};
|
|
2283
2969
|
const removePluginMetadata = (name) => {
|
|
2284
2970
|
store$1.delete(name);
|
|
2971
|
+
getActiveCommandManager()?.removePlugin(name);
|
|
2285
2972
|
};
|
|
2286
2973
|
const getPluginMetadata = (name) => store$1.get(name);
|
|
2287
2974
|
const getPluginMetadataList = () => [...store$1.values()];
|
|
2288
2975
|
const resetPluginMetadata = () => {
|
|
2289
2976
|
store$1.clear();
|
|
2977
|
+
getActiveCommandManager()?.clear();
|
|
2290
2978
|
};
|
|
2291
2979
|
|
|
2292
2980
|
//#endregion
|
|
@@ -2946,12 +3634,6 @@ async function sendTextOrForward(options) {
|
|
|
2946
3634
|
}
|
|
2947
3635
|
}
|
|
2948
3636
|
|
|
2949
|
-
//#endregion
|
|
2950
|
-
//#region src/builtin/core/commands/prefix.ts
|
|
2951
|
-
function getCommandPrefix() {
|
|
2952
|
-
return botConfig.prefix ?? ".";
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
3637
|
//#endregion
|
|
2956
3638
|
//#region src/builtin/core/system/package-manager.ts
|
|
2957
3639
|
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
@@ -3543,102 +4225,102 @@ async function performUpdateAndReport(ctx, event, names) {
|
|
|
3543
4225
|
triggerRestart(marker);
|
|
3544
4226
|
}
|
|
3545
4227
|
function registerUpdateCommands(ctx) {
|
|
3546
|
-
const dispose = ctx.
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
await performUpdateAndReport(ctx, event, managed.names);
|
|
3566
|
-
return;
|
|
3567
|
-
}
|
|
3568
|
-
if (arg === "mioku" || arg === "self") {
|
|
3569
|
-
await performUpdateAndReport(ctx, event, ["mioku"]);
|
|
3570
|
-
return;
|
|
3571
|
-
}
|
|
3572
|
-
if (arg.startsWith("plugin ") || arg.startsWith("service ")) {
|
|
3573
|
-
const [typeRaw, ...rest] = arg.split(/\s+/);
|
|
3574
|
-
const name = rest.join(" ").trim();
|
|
3575
|
-
const type = typeRaw === "plugin" ? "plugin" : "service";
|
|
3576
|
-
if (!name) {
|
|
3577
|
-
await replyText(event, `用法:${prefix}update ${type} <名称>`);
|
|
4228
|
+
const dispose = ctx.command({
|
|
4229
|
+
name: "update",
|
|
4230
|
+
aliases: ["更新"],
|
|
4231
|
+
permission: "master",
|
|
4232
|
+
priority: -1e3,
|
|
4233
|
+
description: "检查并选择插件/服务更新",
|
|
4234
|
+
handler: async ({ event, args }) => {
|
|
4235
|
+
const prefix = String(ctx.config.prefix ?? ".");
|
|
4236
|
+
const arg = args.join(" ").trim();
|
|
4237
|
+
const selfId = String(event?.self_id || "");
|
|
4238
|
+
const bot = event.bot;
|
|
4239
|
+
if (!bot) return;
|
|
4240
|
+
if (arg === "all") {
|
|
4241
|
+
const managed = await updateAllManaged();
|
|
4242
|
+
if (managed.names.length === 0) {
|
|
4243
|
+
await replyText(event, "未找到可更新的 mioku 包");
|
|
4244
|
+
return;
|
|
4245
|
+
}
|
|
4246
|
+
await performUpdateAndReport(ctx, event, managed.names);
|
|
3578
4247
|
return;
|
|
3579
4248
|
}
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
await performUpdateAndReport(ctx, event, [fullName]);
|
|
3583
|
-
return;
|
|
3584
|
-
}
|
|
3585
|
-
if (arg === "check" || arg === "检查") {
|
|
3586
|
-
const pending = getPendingMap();
|
|
3587
|
-
const key = conversationKey(event);
|
|
3588
|
-
const existing = pending.get(key);
|
|
3589
|
-
if (existing) {
|
|
3590
|
-
existing.disposer();
|
|
3591
|
-
clearTimeout(existing.timer);
|
|
3592
|
-
pending.delete(key);
|
|
3593
|
-
}
|
|
3594
|
-
let items;
|
|
3595
|
-
try {
|
|
3596
|
-
items = await checkUpdates();
|
|
3597
|
-
} catch (error) {
|
|
3598
|
-
ctx.logger.error(`[core] 检查更新失败: ${error}`);
|
|
3599
|
-
await replyText(event, `检查更新失败:${String(error)}`);
|
|
4249
|
+
if (arg === "mioku" || arg === "self") {
|
|
4250
|
+
await performUpdateAndReport(ctx, event, ["mioku"]);
|
|
3600
4251
|
return;
|
|
3601
4252
|
}
|
|
3602
|
-
if (
|
|
3603
|
-
|
|
4253
|
+
if (arg.startsWith("plugin ") || arg.startsWith("service ")) {
|
|
4254
|
+
const [typeRaw, ...rest] = arg.split(/\s+/);
|
|
4255
|
+
const name = rest.join(" ").trim();
|
|
4256
|
+
const type = typeRaw === "plugin" ? "plugin" : "service";
|
|
4257
|
+
if (!name) {
|
|
4258
|
+
await replyText(event, `用法:${prefix}update ${type} <名称>`);
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
const pkgPrefix = type === "plugin" ? "mioku-plugin-" : "mioku-service-";
|
|
4262
|
+
const fullName = name.startsWith(pkgPrefix) ? name : `${pkgPrefix}${name}`;
|
|
4263
|
+
await performUpdateAndReport(ctx, event, [fullName]);
|
|
3604
4264
|
return;
|
|
3605
4265
|
}
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
if (
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
if (chosen.length === 0) {
|
|
3622
|
-
await replyText(event, "未选择任何有效项,已取消");
|
|
4266
|
+
if (arg === "check" || arg === "检查") {
|
|
4267
|
+
const pending = getPendingMap();
|
|
4268
|
+
const key = conversationKey(event);
|
|
4269
|
+
const existing = pending.get(key);
|
|
4270
|
+
if (existing) {
|
|
4271
|
+
existing.disposer();
|
|
4272
|
+
clearTimeout(existing.timer);
|
|
4273
|
+
pending.delete(key);
|
|
4274
|
+
}
|
|
4275
|
+
let items;
|
|
4276
|
+
try {
|
|
4277
|
+
items = await checkUpdates();
|
|
4278
|
+
} catch (error) {
|
|
4279
|
+
ctx.logger.error(`[core] 检查更新失败: ${error}`);
|
|
4280
|
+
await replyText(event, `检查更新失败:${String(error)}`);
|
|
3623
4281
|
return;
|
|
3624
4282
|
}
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
4283
|
+
if (items.length === 0) {
|
|
4284
|
+
await replyText(event, "所有插件与服务均已是最新版本");
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
await replyText(event, renderUpdateList(items));
|
|
4288
|
+
const timeoutMs = SELECT_TIMEOUT_MS;
|
|
4289
|
+
const listenerDispose = ctx.handle("message", async (ev) => {
|
|
4290
|
+
if (String(ev?.self_id || "") !== selfId) return;
|
|
4291
|
+
if (conversationKey(ev) !== key) return;
|
|
4292
|
+
if (!isEventMaster(ev)) return;
|
|
4293
|
+
const evText = ctx.text(ev)?.trim() || "";
|
|
4294
|
+
if (evText.startsWith(`${prefix}update`)) return;
|
|
4295
|
+
const sel = pending.get(key);
|
|
4296
|
+
if (!sel) return;
|
|
4297
|
+
clearTimeout(sel.timer);
|
|
4298
|
+
sel.disposer();
|
|
4299
|
+
pending.delete(key);
|
|
4300
|
+
const parsed = parseSelection(evText, sel.items.length);
|
|
4301
|
+
const chosen = parsed.all ? sel.items : parsed.indices.map((i) => sel.items[i]).filter(Boolean);
|
|
4302
|
+
if (chosen.length === 0) {
|
|
4303
|
+
await replyText(event, "未选择任何有效项,已取消");
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
await performUpdateAndReport(ctx, event, chosen.map((i) => i.name));
|
|
4307
|
+
});
|
|
4308
|
+
const timer = setTimeout(async () => {
|
|
4309
|
+
const sel = pending.get(key);
|
|
4310
|
+
if (!sel) return;
|
|
4311
|
+
sel.disposer();
|
|
4312
|
+
pending.delete(key);
|
|
4313
|
+
await replyText(event, "操作超时,已取消更新");
|
|
4314
|
+
}, timeoutMs);
|
|
4315
|
+
pending.set(key, {
|
|
4316
|
+
disposer: listenerDispose,
|
|
4317
|
+
timer,
|
|
4318
|
+
items
|
|
4319
|
+
});
|
|
4320
|
+
return;
|
|
4321
|
+
}
|
|
4322
|
+
await replyText(event, `用法:\n${prefix}update check 检查并选择更新\n${prefix}update all 更新全部\n${prefix}update mioku 更新框架\n${prefix}update plugin <名称>\n${prefix}update service <名称>`);
|
|
3640
4323
|
}
|
|
3641
|
-
await replyText(event, `用法:\n${prefix}update check 检查并选择更新\n${prefix}update all 更新全部\n${prefix}update mioku 更新框架\n${prefix}update plugin <名称>\n${prefix}update service <名称>`);
|
|
3642
4324
|
});
|
|
3643
4325
|
return () => {
|
|
3644
4326
|
dispose();
|
|
@@ -3653,15 +4335,13 @@ function registerUpdateCommands(ctx) {
|
|
|
3653
4335
|
|
|
3654
4336
|
//#endregion
|
|
3655
4337
|
//#region src/builtin/core/commands/install.ts
|
|
3656
|
-
function parseTargetArgs(
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
const
|
|
3660
|
-
if (!matched) return null;
|
|
3661
|
-
const name = matched[2].trim();
|
|
4338
|
+
function parseTargetArgs(args) {
|
|
4339
|
+
const type = args[0];
|
|
4340
|
+
if (type !== "plugin" && type !== "service") return null;
|
|
4341
|
+
const name = args.slice(1).join(" ").trim();
|
|
3662
4342
|
if (!name) return null;
|
|
3663
4343
|
return {
|
|
3664
|
-
type
|
|
4344
|
+
type,
|
|
3665
4345
|
name
|
|
3666
4346
|
};
|
|
3667
4347
|
}
|
|
@@ -3669,68 +4349,70 @@ function typeLabel(type) {
|
|
|
3669
4349
|
return type === "plugin" ? "插件" : "服务";
|
|
3670
4350
|
}
|
|
3671
4351
|
function registerInstallCommands(ctx) {
|
|
3672
|
-
const
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
4352
|
+
const register = (name) => ctx.command({
|
|
4353
|
+
name,
|
|
4354
|
+
aliases: name === "install" ? ["安装"] : ["卸载"],
|
|
4355
|
+
permission: "master",
|
|
4356
|
+
priority: -1e3,
|
|
4357
|
+
description: name === "install" ? "从 npm 安装插件/服务" : "卸载插件/服务",
|
|
4358
|
+
handler: async ({ event, args }) => {
|
|
4359
|
+
if (name === "install") {
|
|
4360
|
+
const parsed$1 = parseTargetArgs(args);
|
|
4361
|
+
if (!parsed$1) {
|
|
4362
|
+
await replyText(event, `用法:${ctx.config.prefix ?? "."}install plugin <名称> 或 ${ctx.config.prefix ?? "."}install service <名称>`);
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
await replyText(event, `正在从 npm 安装${typeLabel(parsed$1.type)} ${parsed$1.name}...`);
|
|
4366
|
+
try {
|
|
4367
|
+
const result = await installPackage(parsed$1.type, parsed$1.name);
|
|
4368
|
+
if (!result.ok) {
|
|
4369
|
+
ctx.logger.error(`[core] 安装失败: ${result.error || result.output}`);
|
|
4370
|
+
await replyText(event, `安装失败:${result.error || result.output}`);
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
const lines = [
|
|
4374
|
+
`已安装 ${result.packageName}`,
|
|
4375
|
+
result.enabled ? "已在 plugins 中启用" : "",
|
|
4376
|
+
"重启后生效"
|
|
4377
|
+
].filter(Boolean);
|
|
4378
|
+
await replyText(event, lines.join("\n"));
|
|
4379
|
+
} catch (error) {
|
|
4380
|
+
ctx.logger.error(`[core] 安装失败: ${error}`);
|
|
4381
|
+
await replyText(event, `安装失败:${String(error)}`);
|
|
4382
|
+
}
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
const parsed = parseTargetArgs(args);
|
|
4386
|
+
if (!parsed) {
|
|
4387
|
+
await replyText(event, `用法:${ctx.config.prefix ?? "."}uninstall plugin <名称> 或 ${ctx.config.prefix ?? "."}uninstall service <名称>`);
|
|
3687
4388
|
return;
|
|
3688
4389
|
}
|
|
3689
|
-
await replyText(event,
|
|
4390
|
+
await replyText(event, `正在卸载${typeLabel(parsed.type)} ${parsed.name}...`);
|
|
3690
4391
|
try {
|
|
3691
|
-
const result = await
|
|
4392
|
+
const result = await uninstallPackage(parsed.type, parsed.name);
|
|
3692
4393
|
if (!result.ok) {
|
|
3693
|
-
ctx.logger.error(`[core]
|
|
3694
|
-
await replyText(event,
|
|
4394
|
+
ctx.logger.error(`[core] 卸载失败: ${result.error || result.output}`);
|
|
4395
|
+
await replyText(event, `卸载失败:${result.error || result.output}`);
|
|
3695
4396
|
return;
|
|
3696
4397
|
}
|
|
3697
4398
|
const lines = [
|
|
3698
|
-
|
|
3699
|
-
result.
|
|
4399
|
+
`已卸载 ${result.packageName}`,
|
|
4400
|
+
result.removedFromConfig ? "已从 plugins 中移除" : "",
|
|
3700
4401
|
"重启后生效"
|
|
3701
4402
|
].filter(Boolean);
|
|
3702
4403
|
await replyText(event, lines.join("\n"));
|
|
3703
4404
|
} catch (error) {
|
|
3704
|
-
ctx.logger.error(`[core]
|
|
3705
|
-
await replyText(event,
|
|
4405
|
+
ctx.logger.error(`[core] 卸载失败: ${error}`);
|
|
4406
|
+
await replyText(event, `卸载失败:${String(error)}`);
|
|
3706
4407
|
}
|
|
3707
|
-
return;
|
|
3708
|
-
}
|
|
3709
|
-
const parsed = parseTargetArgs(text$1, uninstallHead);
|
|
3710
|
-
if (!parsed) {
|
|
3711
|
-
await replyText(event, `用法:${prefix}uninstall plugin <名称> 或 ${prefix}uninstall service <名称>`);
|
|
3712
|
-
return;
|
|
3713
|
-
}
|
|
3714
|
-
await replyText(event, `正在卸载${typeLabel(parsed.type)} ${parsed.name}...`);
|
|
3715
|
-
try {
|
|
3716
|
-
const result = await uninstallPackage(parsed.type, parsed.name);
|
|
3717
|
-
if (!result.ok) {
|
|
3718
|
-
ctx.logger.error(`[core] 卸载失败: ${result.error || result.output}`);
|
|
3719
|
-
await replyText(event, `卸载失败:${result.error || result.output}`);
|
|
3720
|
-
return;
|
|
3721
|
-
}
|
|
3722
|
-
const lines = [
|
|
3723
|
-
`已卸载 ${result.packageName}`,
|
|
3724
|
-
result.removedFromConfig ? "已从 plugins 中移除" : "",
|
|
3725
|
-
"重启后生效"
|
|
3726
|
-
].filter(Boolean);
|
|
3727
|
-
await replyText(event, lines.join("\n"));
|
|
3728
|
-
} catch (error) {
|
|
3729
|
-
ctx.logger.error(`[core] 卸载失败: ${error}`);
|
|
3730
|
-
await replyText(event, `卸载失败:${String(error)}`);
|
|
3731
4408
|
}
|
|
3732
4409
|
});
|
|
3733
|
-
|
|
4410
|
+
const install = register("install");
|
|
4411
|
+
const uninstall = register("uninstall");
|
|
4412
|
+
return () => {
|
|
4413
|
+
install();
|
|
4414
|
+
uninstall();
|
|
4415
|
+
};
|
|
3734
4416
|
}
|
|
3735
4417
|
|
|
3736
4418
|
//#endregion
|
|
@@ -3744,64 +4426,76 @@ function renderMarketText(kind, items) {
|
|
|
3744
4426
|
return [`Mioku ${label}市场(共 ${items.length} 个)`, ...lines].join("\n");
|
|
3745
4427
|
}
|
|
3746
4428
|
function registerMarketCommands(ctx) {
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
4429
|
+
const pluginMarket = ctx.command({
|
|
4430
|
+
id: "market",
|
|
4431
|
+
name: "plugin-market",
|
|
4432
|
+
aliases: ["插件市场"],
|
|
4433
|
+
permission: "master",
|
|
4434
|
+
priority: -1e3,
|
|
4435
|
+
description: "查看插件市场",
|
|
4436
|
+
handler: (input) => handleMarket(ctx, input.event, "plugin")
|
|
4437
|
+
});
|
|
4438
|
+
const serviceMarket = ctx.command({
|
|
4439
|
+
id: "market",
|
|
4440
|
+
name: "service-market",
|
|
4441
|
+
aliases: ["服务市场"],
|
|
4442
|
+
permission: "master",
|
|
4443
|
+
priority: -1e3,
|
|
4444
|
+
description: "查看服务市场",
|
|
4445
|
+
handler: (input) => handleMarket(ctx, input.event, "service")
|
|
4446
|
+
});
|
|
4447
|
+
return () => {
|
|
4448
|
+
pluginMarket();
|
|
4449
|
+
serviceMarket();
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
async function handleMarket(ctx, event, kind) {
|
|
4453
|
+
let items;
|
|
4454
|
+
try {
|
|
4455
|
+
items = await getMarketItems(kind);
|
|
4456
|
+
} catch (error) {
|
|
4457
|
+
ctx.logger.error(`[core] 获取市场信息失败: ${error}`);
|
|
4458
|
+
await replyText(event, `获取市场失败:${String(error)}`);
|
|
4459
|
+
return;
|
|
4460
|
+
}
|
|
4461
|
+
await sendTextOrForward({
|
|
4462
|
+
ctx,
|
|
4463
|
+
event,
|
|
4464
|
+
text: renderMarketText(kind, items),
|
|
4465
|
+
source: `Mioku ${kind === "plugin" ? "插件" : "服务"}市场`,
|
|
4466
|
+
summary: `共 ${items.length} 个`
|
|
3772
4467
|
});
|
|
3773
|
-
return dispose;
|
|
3774
4468
|
}
|
|
3775
4469
|
|
|
3776
4470
|
//#endregion
|
|
3777
4471
|
//#region src/builtin/core/commands/restart.ts
|
|
3778
4472
|
function registerRestartCommand(ctx) {
|
|
3779
|
-
const dispose = ctx.
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
return;
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
4473
|
+
const dispose = ctx.command({
|
|
4474
|
+
name: "restart",
|
|
4475
|
+
aliases: ["重启"],
|
|
4476
|
+
permission: "master",
|
|
4477
|
+
priority: -1e3,
|
|
4478
|
+
description: "重启机器人进程",
|
|
4479
|
+
handler: async ({ event }) => {
|
|
4480
|
+
if (event?.user_id === event?.self_id) return;
|
|
4481
|
+
const uptimeMs = process.uptime() * 1e3;
|
|
4482
|
+
const selfId = Number(event?.self_id || 0);
|
|
4483
|
+
const groupId = event?.message_type === "group" && event?.group_id ? Number(event.group_id) : null;
|
|
4484
|
+
const userId = Number(event?.user_id || 0);
|
|
4485
|
+
const marker = {
|
|
4486
|
+
initiatedAt: Date.now(),
|
|
4487
|
+
selfId,
|
|
4488
|
+
groupId,
|
|
4489
|
+
userId
|
|
4490
|
+
};
|
|
4491
|
+
try {
|
|
4492
|
+
await replyText(event, `Bot已运行${formatUptime(uptimeMs)},正在重启...`);
|
|
4493
|
+
} catch (error) {
|
|
4494
|
+
ctx.logger.warn(`[core] 发送重启提示失败: ${error}`);
|
|
4495
|
+
}
|
|
4496
|
+
ctx.logger.info(`[core] 正在执行重启命令 ${formatUptime(uptimeMs)}`);
|
|
4497
|
+
triggerRestart(marker);
|
|
3802
4498
|
}
|
|
3803
|
-
ctx.logger.info(`[core] 正在执行重启命令 ${formatUptime(uptimeMs)}`);
|
|
3804
|
-
triggerRestart(marker);
|
|
3805
4499
|
});
|
|
3806
4500
|
return dispose;
|
|
3807
4501
|
}
|
|
@@ -3839,63 +4533,63 @@ function chunk(arr, size) {
|
|
|
3839
4533
|
return result;
|
|
3840
4534
|
}
|
|
3841
4535
|
function registerLogCommand(ctx) {
|
|
3842
|
-
const dispose = ctx.
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
return;
|
|
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
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
})
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
4536
|
+
const dispose = ctx.command({
|
|
4537
|
+
name: "log",
|
|
4538
|
+
aliases: ["日志"],
|
|
4539
|
+
permission: "master",
|
|
4540
|
+
priority: -1e3,
|
|
4541
|
+
description: "查看最近100条日志",
|
|
4542
|
+
handler: async ({ event }) => {
|
|
4543
|
+
if (event?.user_id === event?.self_id) return;
|
|
4544
|
+
const selfId = String(event?.self_id || "");
|
|
4545
|
+
const bot = event.bot;
|
|
4546
|
+
if (!bot) return;
|
|
4547
|
+
const logFile = getActiveLogFile();
|
|
4548
|
+
if (!logFile) {
|
|
4549
|
+
await replyText(event, "未找到日志文件。");
|
|
4550
|
+
return;
|
|
4551
|
+
}
|
|
4552
|
+
let lines;
|
|
4553
|
+
try {
|
|
4554
|
+
lines = readLastLines(logFile, LOG_LINE_COUNT);
|
|
4555
|
+
} catch (error) {
|
|
4556
|
+
ctx.logger.error(`[core] 读取日志失败: ${error}`);
|
|
4557
|
+
await replyText(event, `读取日志失败:${String(error)}`);
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
if (lines.length === 0) {
|
|
4561
|
+
await replyText(event, "日志为空。");
|
|
4562
|
+
return;
|
|
4563
|
+
}
|
|
4564
|
+
let infoCount = 0;
|
|
4565
|
+
let warnCount = 0;
|
|
4566
|
+
let errorCount = 0;
|
|
4567
|
+
for (const line of lines) if (/\bERROR\b|\berror\b/i.test(line)) errorCount++;
|
|
4568
|
+
else if (/\bWARN\b|\bwarn\b/i.test(line)) warnCount++;
|
|
4569
|
+
else if (/\bINFO\b|\binfo\b/i.test(line)) infoCount++;
|
|
4570
|
+
const summary = `info ${infoCount} warn ${warnCount} error ${errorCount}`;
|
|
4571
|
+
const chunks = chunk(lines, LINES_PER_NODE);
|
|
4572
|
+
const nodes = chunks.map((chunkLines, idx) => ({
|
|
4573
|
+
user_id: selfId,
|
|
4574
|
+
nickname: `第${idx + 1}条日志`,
|
|
4575
|
+
content: [ctx.segment.text(chunkLines.join("\n"))]
|
|
4576
|
+
}));
|
|
4577
|
+
try {
|
|
4578
|
+
const target = event?.message_type === "group" && event?.group_id ? {
|
|
4579
|
+
type: "group",
|
|
4580
|
+
group_id: event.group_id
|
|
4581
|
+
} : event?.user_id ? {
|
|
4582
|
+
type: "private",
|
|
4583
|
+
user_id: event.user_id
|
|
4584
|
+
} : void 0;
|
|
4585
|
+
if (target) await bot.sendForward(target, nodes, {
|
|
4586
|
+
source: "最近100条运行日志",
|
|
4587
|
+
summary
|
|
4588
|
+
});
|
|
4589
|
+
} catch (error) {
|
|
4590
|
+
ctx.logger.error(`[core] 发送日志转发失败: ${error}`);
|
|
4591
|
+
await replyText(event, `发送日志失败:${String(error)}`);
|
|
4592
|
+
}
|
|
3899
4593
|
}
|
|
3900
4594
|
});
|
|
3901
4595
|
return dispose;
|
|
@@ -4122,7 +4816,6 @@ const core = definePlugin({
|
|
|
4122
4816
|
description: "mioku 内置核心插件",
|
|
4123
4817
|
async setup(ctx) {
|
|
4124
4818
|
const rawPrefix = ctx.config.prefix ?? ".";
|
|
4125
|
-
const cmdPrefix = new RegExp(`^${escapeRegExp(rawPrefix)}`);
|
|
4126
4819
|
const displayPrefix = rawPrefix;
|
|
4127
4820
|
const statusAdminOnly = ctx.config.status_permission === "admin-only";
|
|
4128
4821
|
rootLogger.info("========================================");
|
|
@@ -4140,12 +4833,17 @@ const core = definePlugin({
|
|
|
4140
4833
|
}));
|
|
4141
4834
|
} else ctx.logger.warn("config-service 未加载,core 插件将使用默认配置");
|
|
4142
4835
|
let accessRules = ensureAccessControlConfig();
|
|
4836
|
+
ctx.commands.setAccessControl(accessRules);
|
|
4143
4837
|
if (configService) {
|
|
4144
4838
|
await configService.registerConfig("core", "access-control", accessRules);
|
|
4145
4839
|
const persisted = await configService.getConfig("core", "access-control");
|
|
4146
|
-
if (persisted)
|
|
4840
|
+
if (persisted) {
|
|
4841
|
+
accessRules = normalizeAccessConfig(persisted);
|
|
4842
|
+
ctx.commands.setAccessControl(accessRules);
|
|
4843
|
+
}
|
|
4147
4844
|
disposers.push(configService.onConfigChange("core", "access-control", (next) => {
|
|
4148
4845
|
accessRules = normalizeAccessConfig(next);
|
|
4846
|
+
ctx.commands.setAccessControl(accessRules);
|
|
4149
4847
|
}));
|
|
4150
4848
|
}
|
|
4151
4849
|
rootLogger.info(`访问控制已挂载: ${ctx.bots.length} 个 bot`);
|
|
@@ -4207,17 +4905,49 @@ const core = definePlugin({
|
|
|
4207
4905
|
bots: collectBots(),
|
|
4208
4906
|
adapters: collectAdapters()
|
|
4209
4907
|
}));
|
|
4908
|
+
ctx.command({
|
|
4909
|
+
name: "help",
|
|
4910
|
+
aliases: ["帮助"],
|
|
4911
|
+
permission: "master",
|
|
4912
|
+
priority: -1e3,
|
|
4913
|
+
description: "显示帮助信息",
|
|
4914
|
+
handler: async ({ event }) => {
|
|
4915
|
+
await event.reply(buildCoreHelpText(displayPrefix));
|
|
4916
|
+
}
|
|
4917
|
+
});
|
|
4918
|
+
ctx.command({
|
|
4919
|
+
name: "status",
|
|
4920
|
+
aliases: ["状态"],
|
|
4921
|
+
priority: -1e3,
|
|
4922
|
+
description: "显示框架状态",
|
|
4923
|
+
handler: async ({ event }) => {
|
|
4924
|
+
if (statusAdminOnly && !isEventOwnerOrAdmin(event)) return;
|
|
4925
|
+
await event.reply(await formatMiokuStatus(await getStatus()));
|
|
4926
|
+
}
|
|
4927
|
+
});
|
|
4928
|
+
ctx.command({
|
|
4929
|
+
name: "adapter",
|
|
4930
|
+
aliases: ["适配器"],
|
|
4931
|
+
priority: -1e3,
|
|
4932
|
+
description: "显示适配器与连接实例",
|
|
4933
|
+
handler: async ({ event }) => {
|
|
4934
|
+
if (statusAdminOnly && !isEventOwnerOrAdmin(event)) return;
|
|
4935
|
+
await event.reply(await getAdapterText());
|
|
4936
|
+
}
|
|
4937
|
+
});
|
|
4210
4938
|
const atTarget = (event) => {
|
|
4211
4939
|
const at = event.message.find((seg) => seg.type === "at");
|
|
4212
4940
|
const qq = at?.data?.qq ?? at?.data?.target;
|
|
4213
4941
|
return qq != null ? String(qq) : void 0;
|
|
4214
4942
|
};
|
|
4215
|
-
|
|
4943
|
+
const handleCoreMessage = async (event) => {
|
|
4216
4944
|
const ev = event;
|
|
4217
4945
|
const text$1 = ev.message.text();
|
|
4218
|
-
|
|
4946
|
+
const currentPrefix = String(ctx.config.prefix ?? ".");
|
|
4947
|
+
const currentCmdPrefix = new RegExp(`^${escapeRegExp(currentPrefix)}`);
|
|
4948
|
+
if (!currentCmdPrefix.test(text$1)) return;
|
|
4219
4949
|
if (statusAdminOnly && !isEventOwnerOrAdmin(ev)) return;
|
|
4220
|
-
const bare = text$1.replace(
|
|
4950
|
+
const bare = text$1.replace(currentCmdPrefix, "").trim();
|
|
4221
4951
|
if (bare === "状态" || bare === "status") {
|
|
4222
4952
|
await ev.reply(await formatMiokuStatus(await getStatus()));
|
|
4223
4953
|
return;
|
|
@@ -4226,10 +4956,10 @@ const core = definePlugin({
|
|
|
4226
4956
|
await ev.reply(await getAdapterText());
|
|
4227
4957
|
return;
|
|
4228
4958
|
}
|
|
4229
|
-
if (!
|
|
4959
|
+
if (!isEventMaster(ev)) return;
|
|
4230
4960
|
const { cmd, params } = createCmd(text$1);
|
|
4231
4961
|
if (!cmd) return;
|
|
4232
|
-
const subCmd = cmd.replace(
|
|
4962
|
+
const subCmd = cmd.replace(currentCmdPrefix, "").replace(/\s+/g, "");
|
|
4233
4963
|
const target = params[0];
|
|
4234
4964
|
switch (subCmd) {
|
|
4235
4965
|
case "help":
|
|
@@ -4451,7 +5181,29 @@ const core = definePlugin({
|
|
|
4451
5181
|
process.exit(0);
|
|
4452
5182
|
}
|
|
4453
5183
|
}
|
|
4454
|
-
}
|
|
5184
|
+
};
|
|
5185
|
+
for (const command of [
|
|
5186
|
+
{
|
|
5187
|
+
name: "plugin",
|
|
5188
|
+
aliases: ["插件"],
|
|
5189
|
+
description: "插件管理"
|
|
5190
|
+
},
|
|
5191
|
+
{
|
|
5192
|
+
name: "settings",
|
|
5193
|
+
aliases: ["设置"],
|
|
5194
|
+
description: "框架设置管理"
|
|
5195
|
+
},
|
|
5196
|
+
{
|
|
5197
|
+
name: "exit",
|
|
5198
|
+
aliases: ["退出"],
|
|
5199
|
+
description: "退出框架进程"
|
|
5200
|
+
}
|
|
5201
|
+
]) ctx.command({
|
|
5202
|
+
...command,
|
|
5203
|
+
permission: "master",
|
|
5204
|
+
priority: -1e3,
|
|
5205
|
+
handler: ({ event }) => handleCoreMessage(event)
|
|
5206
|
+
});
|
|
4455
5207
|
rootLogger.info("========================================");
|
|
4456
5208
|
rootLogger.info(" Mioku 服务初始化完成");
|
|
4457
5209
|
rootLogger.info("========================================");
|
|
@@ -4858,7 +5610,8 @@ var MiokuRuntime = class {
|
|
|
4858
5610
|
#enabledPlugins = new Map();
|
|
4859
5611
|
#driverFactory;
|
|
4860
5612
|
#builtinPlugins;
|
|
4861
|
-
#
|
|
5613
|
+
#correlator;
|
|
5614
|
+
#commands;
|
|
4862
5615
|
#started = false;
|
|
4863
5616
|
#stopped = false;
|
|
4864
5617
|
constructor(options) {
|
|
@@ -4866,7 +5619,7 @@ var MiokuRuntime = class {
|
|
|
4866
5619
|
this.#logger = options.logger;
|
|
4867
5620
|
this.#driverFactory = options.driverFactory ?? (() => createDefaultDriver());
|
|
4868
5621
|
this.#builtinPlugins = options.builtinPlugins ?? BUILTIN_PLUGINS$1;
|
|
4869
|
-
this.#
|
|
5622
|
+
this.#correlator = options.dedup?.crossAdapter === false ? void 0 : new EventCorrelator({ logger: this.#logger.child({ scope: "dedup" }) });
|
|
4870
5623
|
this.#driver = this.#driverFactory();
|
|
4871
5624
|
this.#bus = new EventBus();
|
|
4872
5625
|
this.#bus.setLogger((level, message, detail) => {
|
|
@@ -4876,6 +5629,14 @@ var MiokuRuntime = class {
|
|
|
4876
5629
|
});
|
|
4877
5630
|
this.#bots = new BotRegistry();
|
|
4878
5631
|
this.#capabilities = new CapabilityRegistry();
|
|
5632
|
+
this.#commands = new CommandManager({
|
|
5633
|
+
getDefaultPrefix: () => String(botConfig.prefix ?? "."),
|
|
5634
|
+
getOwners: () => botConfig.owners,
|
|
5635
|
+
getAdmins: () => botConfig.admins,
|
|
5636
|
+
logger: this.#logger
|
|
5637
|
+
});
|
|
5638
|
+
this.#bus.setFilter((registration, event) => this.#commands.shouldDispatch(registration.source, event));
|
|
5639
|
+
setActiveCommandManager(this.#commands);
|
|
4879
5640
|
}
|
|
4880
5641
|
get cwd() {
|
|
4881
5642
|
return this.#cwd;
|
|
@@ -4889,6 +5650,13 @@ var MiokuRuntime = class {
|
|
|
4889
5650
|
get bus() {
|
|
4890
5651
|
return this.#bus;
|
|
4891
5652
|
}
|
|
5653
|
+
get commands() {
|
|
5654
|
+
return this.#commands;
|
|
5655
|
+
}
|
|
5656
|
+
/** 事件关联器(跨适配器去重);为 undefined 时表示已禁用 */
|
|
5657
|
+
get correlator() {
|
|
5658
|
+
return this.#correlator;
|
|
5659
|
+
}
|
|
4892
5660
|
get bots() {
|
|
4893
5661
|
return this.#bots.all();
|
|
4894
5662
|
}
|
|
@@ -4903,14 +5671,16 @@ var MiokuRuntime = class {
|
|
|
4903
5671
|
return this.#bots.pick(bot_id);
|
|
4904
5672
|
}
|
|
4905
5673
|
listPlugins() {
|
|
4906
|
-
return Array.from(this.#enabledPlugins.entries()).map(([key
|
|
5674
|
+
return Array.from(this.#enabledPlugins.entries()).map(([key]) => {
|
|
4907
5675
|
const colon = key.indexOf(":");
|
|
4908
5676
|
const type = key.slice(0, colon);
|
|
4909
5677
|
const name = key.slice(colon + 1);
|
|
5678
|
+
const isExternal = type !== "builtin";
|
|
5679
|
+
const version$2 = isExternal ? getPluginMetadata(name)?.version : void 0;
|
|
4910
5680
|
return {
|
|
4911
5681
|
name,
|
|
4912
5682
|
type: type === "builtin" ? "builtin" : "external",
|
|
4913
|
-
version:
|
|
5683
|
+
version: version$2
|
|
4914
5684
|
};
|
|
4915
5685
|
});
|
|
4916
5686
|
}
|
|
@@ -4996,13 +5766,17 @@ var MiokuRuntime = class {
|
|
|
4996
5766
|
driver: this.#driver,
|
|
4997
5767
|
capabilities: this.#capabilities,
|
|
4998
5768
|
logger: this.#logger.child({ adapter: state.definition.name }),
|
|
4999
|
-
emit: (event) => this.#emitLifecycle(event)
|
|
5769
|
+
emit: (event) => this.#emitLifecycle(event),
|
|
5770
|
+
correlator: this.#correlator,
|
|
5771
|
+
commands: this.#commands
|
|
5000
5772
|
});
|
|
5001
5773
|
}
|
|
5002
5774
|
async #loadPlugin(plugin, type) {
|
|
5003
5775
|
const cleanupTasks = [];
|
|
5004
5776
|
const ctx = new MiokuContext({
|
|
5005
5777
|
pluginName: `${type}:${plugin.name}`,
|
|
5778
|
+
pluginId: plugin.name === "mioku-core" ? "core" : plugin.name,
|
|
5779
|
+
commands: this.#commands,
|
|
5006
5780
|
bus: this.#bus,
|
|
5007
5781
|
bots: this.#bots,
|
|
5008
5782
|
driver: this.#driver,
|
|
@@ -5010,7 +5784,7 @@ var MiokuRuntime = class {
|
|
|
5010
5784
|
config: botConfig,
|
|
5011
5785
|
logger: this.#logger.child({ plugin: plugin.name }),
|
|
5012
5786
|
priority: plugin.priority ?? 100,
|
|
5013
|
-
|
|
5787
|
+
correlator: this.#correlator,
|
|
5014
5788
|
getAdapter: (name) => this.getAdapter(name),
|
|
5015
5789
|
listAdapters: () => this.adapters,
|
|
5016
5790
|
onUpdateConfig: async (updater) => {
|
|
@@ -5299,6 +6073,9 @@ var MiokuRuntime = class {
|
|
|
5299
6073
|
this.#enabledPlugins.clear();
|
|
5300
6074
|
this.#capabilities.clear();
|
|
5301
6075
|
this.#bots.clear();
|
|
6076
|
+
this.#correlator?.clear();
|
|
6077
|
+
this.#commands.clear();
|
|
6078
|
+
setActiveCommandManager(void 0);
|
|
5302
6079
|
resetPluginMetadata();
|
|
5303
6080
|
try {
|
|
5304
6081
|
await this.#driver.shutdown();
|
|
@@ -5315,22 +6092,38 @@ const createRuntime = (options) => {
|
|
|
5315
6092
|
|
|
5316
6093
|
//#endregion
|
|
5317
6094
|
//#region src/runtime/plugin-artifacts.ts
|
|
6095
|
+
const commandKey = (value) => String(value).trim().replace(/^[#/.]+\s*/, "").split(/\s+/)[0].toLowerCase();
|
|
5318
6096
|
async function registerPluginArtifacts(ctx) {
|
|
5319
6097
|
const helpService = ctx ? getService(ctx, Services.Help) : servicesRegistry["help"];
|
|
5320
6098
|
const plugins = getPluginMetadataList();
|
|
6099
|
+
const commandManager = getActiveCommandManager();
|
|
6100
|
+
const metadataByName = new Map(plugins.map((meta) => [meta.name, meta]));
|
|
6101
|
+
const pluginNames = new Set([...metadataByName.keys(), ...(commandManager?.catalog() ?? []).filter((item) => item.kind === "plugin").map((item) => item.plugin)]);
|
|
5321
6102
|
let helpCount = 0;
|
|
5322
|
-
for (const
|
|
5323
|
-
|
|
5324
|
-
|
|
6103
|
+
for (const pluginName of pluginNames) {
|
|
6104
|
+
const meta = metadataByName.get(pluginName);
|
|
6105
|
+
const generated = commandManager?.getPluginHelp(pluginName) ?? meta?.config?.help;
|
|
6106
|
+
const existing = helpService?.getHelp(pluginName);
|
|
6107
|
+
const help = generated ? {
|
|
6108
|
+
...generated,
|
|
6109
|
+
title: existing?.title || generated.title,
|
|
6110
|
+
description: existing?.description || generated.description,
|
|
6111
|
+
commands: [...existing?.commands ?? [], ...generated.commands.filter((command) => {
|
|
6112
|
+
const key = commandKey(command.cmd);
|
|
6113
|
+
return !(existing?.commands ?? []).some((item) => commandKey(item.cmd) === key);
|
|
6114
|
+
})]
|
|
6115
|
+
} : existing;
|
|
6116
|
+
if (help) if (helpService) {
|
|
6117
|
+
helpService.registerHelp(pluginName, help);
|
|
5325
6118
|
helpCount++;
|
|
5326
|
-
} else rootLogger.warn(`[plugin-artifacts] 帮助服务未加载,跳过插件 "${
|
|
5327
|
-
const required = meta
|
|
6119
|
+
} else rootLogger.warn(`[plugin-artifacts] 帮助服务未加载,跳过插件 "${pluginName}" 的帮助注册`);
|
|
6120
|
+
const required = meta?.config?.services;
|
|
5328
6121
|
if (required && required.length > 0) {
|
|
5329
6122
|
const missing = required.filter((name) => !servicesRegistry[name]);
|
|
5330
|
-
if (missing.length > 0) rootLogger.warn(`插件 "${
|
|
6123
|
+
if (missing.length > 0) rootLogger.warn(`插件 "${pluginName}" 声明依赖服务 [${missing.join(", ")}],但它们未加载`);
|
|
5331
6124
|
}
|
|
5332
6125
|
}
|
|
5333
|
-
rootLogger.info(`[plugin-artifacts] 已处理 ${
|
|
6126
|
+
rootLogger.info(`[plugin-artifacts] 已处理 ${pluginNames.size} 个插件,注册帮助 ${helpCount} 个`);
|
|
5334
6127
|
}
|
|
5335
6128
|
|
|
5336
6129
|
//#endregion
|
|
@@ -5461,6 +6254,7 @@ async function start(options = {}) {
|
|
|
5461
6254
|
};
|
|
5462
6255
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
5463
6256
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
6257
|
+
installProcessErrorGuards();
|
|
5464
6258
|
return { stop: (reason) => stopRuntime(reason) };
|
|
5465
6259
|
}
|
|
5466
6260
|
function readVersion() {
|
|
@@ -5473,7 +6267,16 @@ function readVersion() {
|
|
|
5473
6267
|
}
|
|
5474
6268
|
}
|
|
5475
6269
|
const version$1 = readVersion();
|
|
6270
|
+
function installProcessErrorGuards() {
|
|
6271
|
+
if (process.listenerCount("unhandledRejection") === 0) process.on("unhandledRejection", (reason) => {
|
|
6272
|
+
const detail = reason instanceof Error ? reason.stack ?? reason.message : reason;
|
|
6273
|
+
rootLogger.error(`[unhandledRejection] ${String(detail)}`);
|
|
6274
|
+
});
|
|
6275
|
+
if (process.listenerCount("uncaughtException") === 0) process.on("uncaughtException", (err) => {
|
|
6276
|
+
rootLogger.error(`[uncaughtException] ${err.stack ?? err.message ?? String(err)}`);
|
|
6277
|
+
});
|
|
6278
|
+
}
|
|
5476
6279
|
|
|
5477
6280
|
//#endregion
|
|
5478
|
-
export { AI_GEMINI_THINKING_LEVELS, AI_THINKING_LEVELS, AdapterContextImpl, AdapterRegistrationConflictError, ArchMap, BOT_CWD, BUILTIN_PLUGINS, BotRegistry, CORE_PLUGINS, CapabilityRegistry, ChromeUA, DriverShutdownError, EventBus, HttpRequestError, MessageSegmentImpl, MiokuContext, MiokuRuntime, ServiceManager, Services, SystemMap, TOOL_RESULT_FOLLOWUP_KEY, UnsupportedCapabilityError, WebSocketConnectTimeoutError, addService, applyLogLevel, asMessage, atOf, avatarGet, avatarSet, base64Decode, base64Encode, bindCapabilities, botConfig, botStatus, box, buildAdapterReport, buildMiokuStatus, buildRoutes, buildSpawnPlan, clamp, collectBotStats, colorize, colors, commandExists, connectedBotKey, connectedBots, conversationGetHistory, createCmd, createDB, createDefaultDriver, createFriendRef, createGroupRef, createImportContext, createMessage, createMiokuLogger, createRuntime, createStore, dayjs, dedent, defineAdapter, defineCapability, definePlugin, defineService, defineState, deleteServiceConfig, discoverAdapterCandidates, discoverPluginCandidates, displayAdapterId, ensureDataDir, filesize, filter, find, findLocalPlugins, formatAdapterReport, formatDuration, formatMiokuStatus, formatMiokuStatusFn, forwardSend, friendDelete, friendGetInfo, friendGetList, fs, getAllPluginRuntimeStates, getBuiltinPlugins, getConfigDir, getDataDir, getGTk, getPluginConfigDir, getPluginDataDir, getPluginMetadata, getPluginMetadataList, getPluginRuntimeState, getService, getServiceConfig, getServiceConfigDir, getServiceConfigs, getServiceDataDir, getTerminalInput, groupGetInfo, groupGetList, groupGetMembers, groupLeave, groupSetName, groupSetPortrait, groupSetWholeBan, hasEventRight, hasPluginState, hasRight, hasService, isAdapterEvent, isAdmin, isBoolean, isBotEvent, isDefined, isEventAdmin, isEventOwner, isEventOwnerOrAdmin, isFunction, isMessage, isMessageEvent, isMetaEvent, isMiokuPlugin, isNoticeEvent, isNumber, isObject, isOwner, isOwnerOrAdmin, isRequestEvent, isSegment, isString, loadAdapterDefinition, loadLocalPlugin, loadNpmPlugin, localNum, localeDate, localeTime, rootLogger as logger, match, md5, memberBan, memberGetInfo, memberKick, memberPoke, memberSetAdmin, memberSetCard, memberSetTitle, messageGet, messageGetForward, messageRecall, messageSend, mriLib as mri, noNullish, normalizeAIThinkingLevel, normalizeAdmins, normalizeOwners, normalizePlugins, normalizeSkillPermissionRole, noticeAdmins, noticeFriends, noticeGroups, noticeOwners, path, prettyMs, profileSet, qs, randomId, randomInt, randomItem, randomItems, readMiokuConfig, readPackageJson, readPackageJsonSafe, registerPluginArtifacts, registerServiceConfig, registerStatusProvider, reloadMiokuConfig, removePluginMetadata, requireService, resetPluginMetadata, resetPluginRuntimeState, resolveCommand, resolveEntry, resolveLocalPluginEntry, resolveLocalPluginPath, rootLogger, runCommand, runCommandInherit, segment, sendMessage, manager_default as serviceManager, servicesRegistry, setBotCwd, setBuiltinPlugins, setPluginMetadata, setPluginRuntimeState, setService, setWritableConfig, start, startRuntime, string2argv, stringifyError, stripAnsi, text, toArray, toId, toUserId, unique, updateMiokuConfig, updateServiceConfig, uuid, version$1 as version, wait, writePackageJson };
|
|
6281
|
+
export { AI_GEMINI_THINKING_LEVELS, AI_THINKING_LEVELS, AdapterContextImpl, AdapterRegistrationConflictError, ArchMap, BOT_CWD, BUILTIN_PLUGINS, BotRegistry, CORE_PLUGINS, CapabilityRegistry, ChromeUA, CommandManager, DriverShutdownError, EventBus, EventCorrelator, HttpRequestError, MessageSegmentImpl, MiokuContext, MiokuRuntime, ServiceManager, Services, SystemMap, TOOL_RESULT_FOLLOWUP_KEY, UnsupportedCapabilityError, WebSocketConnectTimeoutError, addService, applyLogLevel, asMessage, atOf, avatarGet, avatarSet, base64Decode, base64Encode, bindCapabilities, botConfig, botStatus, box, buildAdapterReport, buildMiokuStatus, buildRoutes, buildSpawnPlan, clamp, collectBotStats, colorize, colors, commandExists, connectedBotKey, connectedBots, conversationGetHistory, createCmd, createDB, createDefaultDriver, createFriendRef, createGroupRef, createImportContext, createMessage, createMiokuLogger, createRuntime, createStore, dayjs, dedent, defineAdapter, defineCapability, definePlugin, defineService, defineState, deleteServiceConfig, discoverAdapterCandidates, discoverPluginCandidates, displayAdapterId, ensureDataDir, filesize, filter, find, findLocalPlugins, formatAdapterReport, formatDuration, formatMiokuStatus, formatMiokuStatusFn, forwardSend, friendDelete, friendGetInfo, friendGetList, fs, getActiveCommandManager, getAllPluginRuntimeStates, getBuiltinPlugins, getConfigDir, getDataDir, getGTk, getPluginConfigDir, getPluginDataDir, getPluginMetadata, getPluginMetadataList, getPluginRuntimeState, getService, getServiceConfig, getServiceConfigDir, getServiceConfigs, getServiceDataDir, getTerminalInput, groupGetInfo, groupGetList, groupGetMembers, groupLeave, groupSetName, groupSetPortrait, groupSetWholeBan, hasEventRight, hasPluginState, hasRight, hasService, isAdapterEvent, isAdmin, isBoolean, isBotEvent, isDefined, isEventAdmin, isEventAdminConfigOnly, isEventGroupAdmin, isEventGroupOwner, isEventMaster, isEventOwner, isEventOwnerOrAdmin, isFunction, isMessage, isMessageEvent, isMetaEvent, isMiokuPlugin, isNoticeEvent, isNumber, isObject, isOwner, isOwnerOrAdmin, isRequestEvent, isSegment, isString, loadAdapterDefinition, loadLocalPlugin, loadNpmPlugin, localNum, localeDate, localeTime, rootLogger as logger, match, md5, memberBan, memberGetInfo, memberKick, memberPoke, memberSetAdmin, memberSetCard, memberSetTitle, messageGet, messageGetForward, messageRecall, messageSend, mriLib as mri, noNullish, normalizeAIThinkingLevel, normalizeAdmins, normalizeOwners, normalizePlugins, normalizeSkillPermissionRole, noticeAdmins, noticeFriends, noticeGroups, noticeOwners, path, prettyMs, profileSet, qs, randomId, randomInt, randomItem, randomItems, readMiokuConfig, readPackageJson, readPackageJsonSafe, registerPluginArtifacts, registerServiceConfig, registerStatusProvider, reloadMiokuConfig, removePluginMetadata, requireService, resetPluginMetadata, resetPluginRuntimeState, resolveCommand, resolveEntry, resolveLocalPluginEntry, resolveLocalPluginPath, rootLogger, runCommand, runCommandInherit, segment, sendMessage, manager_default as serviceManager, servicesRegistry, setActiveCommandManager, setBotCwd, setBuiltinPlugins, setPluginMetadata, setPluginRuntimeState, setService, setWritableConfig, start, startRuntime, string2argv, stringifyError, stripAnsi, text, toArray, toId, toUserId, unique, updateMiokuConfig, updateServiceConfig, uuid, version$1 as version, wait, writePackageJson };
|
|
5479
6282
|
//# sourceMappingURL=index.js.map
|