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/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_exec = require('./exec-CeafLFmG.cjs');
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/cross-adapter-dedup.ts
1893
- const PRUNE_INTERVAL = 128;
1894
- const MESSAGE_TTL_MS = 15e3;
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
- * 跨适配器消息去重(L2)
1970
+ * master 或当前群群主。等价于 `ctx.command({ permission: "owner" })` 的判断。
1945
1971
  */
1946
- var CrossAdapterEventDeduplicator = class {
1947
- #entries = new Map();
1948
- #inserts = 0;
1949
- #dropped = 0;
1950
- /** 本次运行以来被丢弃的跨适配器重复消息数 */
1951
- get dropped() {
1952
- return this.#dropped;
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.pluginName}`;
2135
+ const source = `plugin:${this.#options.pluginId}`;
2117
2136
  const handledEvents = new WeakSet();
2118
- const dedup = bypassDedup || this.#options.dedup === false ? null : new CrossAdapterEventDeduplicator();
2137
+ const correlator = bypassDedup ? void 0 : this.#options.correlator;
2119
2138
  const wrappedHandler = async (event) => {
2120
2139
  if (handledEvents.has(event)) return;
2121
- if (dedup?.isDuplicate(event)) return;
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$1(event) && event.kind === "message" && event.message_type === "group";
2268
+ return isObject(event) && event.kind === "message" && event.message_type === "group";
2235
2269
  }
2236
2270
  isPrivateMsg(event) {
2237
- return isObject$1(event) && event.kind === "message" && event.message_type === "private";
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 isEventOwnerOrAdmin(event);
2287
+ return isEventAdmin(event);
2249
2288
  }
2289
+ /** 同 `isAdmin`,别名 */
2250
2290
  hasRight(event) {
2251
- return hasEventRight(event);
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.handle("message", async (event) => {
3532
- const text$1 = ctx.text(event)?.trim();
3533
- if (!text$1 || event?.user_id === event?.self_id) return;
3534
- const prefix = getCommandPrefix();
3535
- if (!text$1.startsWith(`${prefix}update`)) return;
3536
- if (!isEventOwner(event)) {
3537
- ctx.logger.warn("[core] update 指令仅主人可用");
3538
- return;
3539
- }
3540
- const arg = text$1.slice(prefix.length + 6).trim();
3541
- const selfId = String(event?.self_id || "");
3542
- const bot = event.bot;
3543
- if (!bot) return;
3544
- if (arg === "all") {
3545
- const managed = await updateAllManaged();
3546
- if (managed.names.length === 0) {
3547
- await replyText(event, "未找到可更新的 mioku 包");
3548
- return;
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
- const pkgPrefix = type === "plugin" ? "mioku-plugin-" : "mioku-service-";
3566
- const fullName = name.startsWith(pkgPrefix) ? name : `${pkgPrefix}${name}`;
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 (items.length === 0) {
3588
- await replyText(event, "所有插件与服务均已是最新版本");
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
- await replyText(event, renderUpdateList(items));
3592
- const timeoutMs = SELECT_TIMEOUT_MS;
3593
- const listenerDispose = ctx.handle("message", async (ev) => {
3594
- if (String(ev?.self_id || "") !== selfId) return;
3595
- if (conversationKey(ev) !== key) return;
3596
- if (!isEventOwner(ev)) return;
3597
- const evText = ctx.text(ev)?.trim() || "";
3598
- if (evText.startsWith(`${prefix}update`)) return;
3599
- const sel = pending.get(key);
3600
- if (!sel) return;
3601
- clearTimeout(sel.timer);
3602
- sel.disposer();
3603
- pending.delete(key);
3604
- const parsed = parseSelection(evText, sel.items.length);
3605
- const chosen = parsed.all ? sel.items : parsed.indices.map((i) => sel.items[i]).filter(Boolean);
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
- await performUpdateAndReport(ctx, event, chosen.map((i) => i.name));
3611
- });
3612
- const timer = setTimeout(async () => {
3613
- const sel = pending.get(key);
3614
- if (!sel) return;
3615
- sel.disposer();
3616
- pending.delete(key);
3617
- await replyText(event, "操作超时,已取消更新");
3618
- }, timeoutMs);
3619
- pending.set(key, {
3620
- disposer: listenerDispose,
3621
- timer,
3622
- items
3623
- });
3624
- return;
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(text$1, head) {
3642
- if (!text$1.startsWith(head)) return null;
3643
- const rest = text$1.slice(head.length).trim();
3644
- const matched = rest.match(/^(plugin|service)\s+([\s\S]+)$/);
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: matched[1],
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 dispose = ctx.handle("message", async (event) => {
3658
- const text$1 = ctx.text(event)?.trim();
3659
- if (!text$1 || event?.user_id === event?.self_id) return;
3660
- const prefix = getCommandPrefix();
3661
- const installHead = `${prefix}install`;
3662
- const uninstallHead = `${prefix}uninstall`;
3663
- if (!text$1.startsWith(installHead) && !text$1.startsWith(uninstallHead)) return;
3664
- if (!isEventOwner(event)) {
3665
- ctx.logger.warn("[core] install/uninstall 指令仅主人可用");
3666
- return;
3667
- }
3668
- if (text$1.startsWith(installHead)) {
3669
- const parsed$1 = parseTargetArgs(text$1, installHead);
3670
- if (!parsed$1) {
3671
- await replyText(event, `用法:${prefix}install plugin <名称> 或 ${prefix}install service <名称>`);
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, `正在从 npm 安装${typeLabel(parsed$1.type)} ${parsed$1.name}...`);
4375
+ await replyText(event, `正在卸载${typeLabel(parsed.type)} ${parsed.name}...`);
3675
4376
  try {
3676
- const result = await installPackage(parsed$1.type, parsed$1.name);
4377
+ const result = await uninstallPackage(parsed.type, parsed.name);
3677
4378
  if (!result.ok) {
3678
- ctx.logger.error(`[core] 安装失败: ${result.error || result.output}`);
3679
- await replyText(event, `安装失败:${result.error || result.output}`);
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
- `已安装 ${result.packageName}`,
3684
- result.enabled ? "已在 plugins 中启用" : "",
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] 安装失败: ${error}`);
3690
- await replyText(event, `安装失败:${String(error)}`);
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
- return dispose;
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 dispose = ctx.handle("message", async (event) => {
3733
- const text$1 = ctx.text(event)?.trim();
3734
- if (!text$1 || event?.user_id === event?.self_id) return;
3735
- const prefix = getCommandPrefix();
3736
- const kind = text$1 === `${prefix}plugin-market` || text$1 === `${prefix}插件市场` ? "plugin" : text$1 === `${prefix}service-market` || text$1 === `${prefix}服务市场` ? "service" : null;
3737
- if (!kind) return;
3738
- if (!isEventOwner(event)) {
3739
- ctx.logger.warn("[core] market 指令仅主人可用");
3740
- return;
3741
- }
3742
- let items;
3743
- try {
3744
- items = await getMarketItems(kind);
3745
- } catch (error) {
3746
- ctx.logger.error(`[core] 获取市场信息失败: ${error}`);
3747
- await replyText(event, `获取市场失败:${String(error)}`);
3748
- return;
3749
- }
3750
- await sendTextOrForward({
3751
- ctx,
3752
- event,
3753
- text: renderMarketText(kind, items),
3754
- source: `Mioku ${kind === "plugin" ? "插件" : "服务"}市场`,
3755
- summary: `共 ${items.length} 个`
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.handle("message", async (event) => {
3765
- const text$1 = ctx.text(event)?.trim();
3766
- if (!text$1 || event?.user_id === event?.self_id) return;
3767
- const prefix = getCommandPrefix();
3768
- if (text$1 !== `${prefix}restart` && text$1 !== `${prefix}重启`) return;
3769
- if (!isEventOwner(event)) {
3770
- ctx.logger.warn("[core] restart 指令仅主人可用");
3771
- return;
3772
- }
3773
- const uptimeMs = process.uptime() * 1e3;
3774
- const selfId = Number(event?.self_id || 0);
3775
- const groupId = event?.message_type === "group" && event?.group_id ? Number(event.group_id) : null;
3776
- const userId = Number(event?.user_id || 0);
3777
- const marker = {
3778
- initiatedAt: Date.now(),
3779
- selfId,
3780
- groupId,
3781
- userId
3782
- };
3783
- try {
3784
- await replyText(event, `Bot已运行${formatUptime(uptimeMs)},正在重启...`);
3785
- } catch (error) {
3786
- ctx.logger.warn(`[core] 发送重启提示失败: ${error}`);
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.handle("message", async (event) => {
3828
- const text$1 = ctx.text(event)?.trim();
3829
- if (!text$1 || event?.user_id === event?.self_id) return;
3830
- const prefix = getCommandPrefix();
3831
- if (text$1 !== `${prefix}log` && text$1 !== `${prefix}日志`) return;
3832
- if (!isEventOwner(event)) {
3833
- ctx.logger.warn("[core] 日志指令仅主人可用");
3834
- return;
3835
- }
3836
- const selfId = String(event?.self_id || "");
3837
- const bot = event.bot;
3838
- if (!bot) return;
3839
- const logFile = getActiveLogFile();
3840
- if (!logFile) {
3841
- await replyText(event, "未找到日志文件。");
3842
- return;
3843
- }
3844
- let lines;
3845
- try {
3846
- lines = readLastLines(logFile, LOG_LINE_COUNT);
3847
- } catch (error) {
3848
- ctx.logger.error(`[core] 读取日志失败: ${error}`);
3849
- await replyText(event, `读取日志失败:${String(error)}`);
3850
- return;
3851
- }
3852
- if (lines.length === 0) {
3853
- await replyText(event, "日志为空。");
3854
- return;
3855
- }
3856
- let infoCount = 0;
3857
- let warnCount = 0;
3858
- let errorCount = 0;
3859
- for (const line of lines) if (/\bERROR\b|\berror\b/i.test(line)) errorCount++;
3860
- else if (/\bWARN\b|\bwarn\b/i.test(line)) warnCount++;
3861
- else if (/\bINFO\b|\binfo\b/i.test(line)) infoCount++;
3862
- const summary = `info ${infoCount} warn ${warnCount} error ${errorCount}`;
3863
- const chunks = chunk(lines, LINES_PER_NODE);
3864
- const nodes = chunks.map((chunkLines, idx) => ({
3865
- user_id: selfId,
3866
- nickname: `第${idx + 1}条日志`,
3867
- content: [ctx.segment.text(chunkLines.join("\n"))]
3868
- }));
3869
- try {
3870
- const target = event?.message_type === "group" && event?.group_id ? {
3871
- type: "group",
3872
- group_id: event.group_id
3873
- } : event?.user_id ? {
3874
- type: "private",
3875
- user_id: event.user_id
3876
- } : void 0;
3877
- if (target) await bot.sendForward(target, nodes, {
3878
- source: "最近100条运行日志",
3879
- summary
3880
- });
3881
- } catch (error) {
3882
- ctx.logger.error(`[core] 发送日志转发失败: ${error}`);
3883
- await replyText(event, `发送日志失败:${String(error)}`);
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) accessRules = normalizeAccessConfig(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
- disposers.push(ctx.handle("message", async (event) => {
4928
+ const handleCoreMessage = async (event) => {
4201
4929
  const ev = event;
4202
4930
  const text$1 = ev.message.text();
4203
- if (!cmdPrefix.test(text$1)) return;
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(cmdPrefix, "").trim();
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 (!isEventOwner(ev)) return;
4944
+ if (!isEventMaster(ev)) return;
4215
4945
  const { cmd, params } = createCmd(text$1);
4216
4946
  if (!cmd) return;
4217
- const subCmd = cmd.replace(cmdPrefix, "").replace(/\s+/g, "");
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
- #dedupEnabled;
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.#dedupEnabled = options.dedup?.crossAdapter !== false;
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, entry]) => {
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: entry.plugin.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
- dedup: this.#dedupEnabled,
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 meta of plugins) {
5308
- if (meta.config?.help) if (helpService) {
5309
- helpService.registerHelp(meta.name, meta.config.help);
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] 帮助服务未加载,跳过插件 "${meta.name}" 的帮助注册`);
5312
- const required = meta.config?.services;
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(`插件 "${meta.name}" 声明依赖服务 [${missing.join(", ")}],但它们未加载`);
6108
+ if (missing.length > 0) rootLogger.warn(`插件 "${pluginName}" 声明依赖服务 [${missing.join(", ")}],但它们未加载`);
5316
6109
  }
5317
6110
  }
5318
- rootLogger.info(`[plugin-artifacts] 已处理 ${plugins.length} 个插件的元数据,注册帮助 ${helpCount} 个`);
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;