mioku 1.0.6 → 1.0.8
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 -2
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +3 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/{exec-p6Vp158F.js → exec-3C9aJ9OX.js} +2 -2
- package/dist/{exec-p6Vp158F.js.map → exec-3C9aJ9OX.js.map} +1 -1
- package/dist/{exec-O8rCedzr.cjs → exec-BzdYBQLQ.cjs} +2 -2
- package/dist/{exec-O8rCedzr.cjs.map → exec-BzdYBQLQ.cjs.map} +1 -1
- package/dist/index.cjs +363 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +86 -13
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +86 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +364 -107
- 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-3C9aJ9OX.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,7 @@ var AdapterContextImpl = class {
|
|
|
870
870
|
#capabilities;
|
|
871
871
|
#logger;
|
|
872
872
|
#emit;
|
|
873
|
+
#correlator;
|
|
873
874
|
#pendingStarts = new Set();
|
|
874
875
|
constructor(options) {
|
|
875
876
|
this.#state = options.state;
|
|
@@ -879,6 +880,7 @@ var AdapterContextImpl = class {
|
|
|
879
880
|
this.#capabilities = options.capabilities;
|
|
880
881
|
this.#logger = options.logger;
|
|
881
882
|
this.#emit = options.emit;
|
|
883
|
+
this.#correlator = options.correlator;
|
|
882
884
|
}
|
|
883
885
|
registerBot(bot) {
|
|
884
886
|
return this.#bots.register(bot);
|
|
@@ -942,6 +944,7 @@ var AdapterContextImpl = class {
|
|
|
942
944
|
};
|
|
943
945
|
}
|
|
944
946
|
async dispatch(event) {
|
|
947
|
+
this.#correlator?.observe(event);
|
|
945
948
|
if (event.kind === "message") {
|
|
946
949
|
const senderId = event.user_id;
|
|
947
950
|
if (senderId != null) {
|
|
@@ -1030,6 +1033,9 @@ var BotRegistry = class {
|
|
|
1030
1033
|
for (const entry of this.#bots.values()) if (entry.bot_id === key) return entry.bot;
|
|
1031
1034
|
return void 0;
|
|
1032
1035
|
}
|
|
1036
|
+
find(adapter, bot_id) {
|
|
1037
|
+
return this.#bots.get(connectedBotKey(adapter, bot_id))?.bot;
|
|
1038
|
+
}
|
|
1033
1039
|
all() {
|
|
1034
1040
|
return Array.from(this.#bots.values()).map((entry) => entry.bot);
|
|
1035
1041
|
}
|
|
@@ -1903,104 +1909,6 @@ const Services = {
|
|
|
1903
1909
|
Help: defineService("help")
|
|
1904
1910
|
};
|
|
1905
1911
|
|
|
1906
|
-
//#endregion
|
|
1907
|
-
//#region src/runtime/cross-adapter-dedup.ts
|
|
1908
|
-
const PRUNE_INTERVAL = 128;
|
|
1909
|
-
const MESSAGE_TTL_MS = 15e3;
|
|
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("|");
|
|
1958
|
-
/**
|
|
1959
|
-
* 跨适配器消息去重(L2)
|
|
1960
|
-
*/
|
|
1961
|
-
var CrossAdapterEventDeduplicator = class {
|
|
1962
|
-
#entries = new Map();
|
|
1963
|
-
#inserts = 0;
|
|
1964
|
-
#dropped = 0;
|
|
1965
|
-
/** 本次运行以来被丢弃的跨适配器重复消息数 */
|
|
1966
|
-
get dropped() {
|
|
1967
|
-
return this.#dropped;
|
|
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
1912
|
//#endregion
|
|
2005
1913
|
//#region src/runtime/mioku-context.ts
|
|
2006
1914
|
const isObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -2098,6 +2006,59 @@ var MiokuContext = class {
|
|
|
2098
2006
|
return this.#options.bots.pick(bot_id);
|
|
2099
2007
|
}
|
|
2100
2008
|
/**
|
|
2009
|
+
* 取事件所属的跨适配器关联记录。
|
|
2010
|
+
* 同一条逻辑消息被多个适配器/bot 投递时,可从这里读到全部参与方。
|
|
2011
|
+
*/
|
|
2012
|
+
correlation(event) {
|
|
2013
|
+
return this.#options.correlator?.observationOf(event)?.record;
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* 观察到该事件的全部已连接 bot(首个即 primary)。
|
|
2017
|
+
* 跨适配器去重后,被标记为重复的投递不会进入普通 handler,
|
|
2018
|
+
* 但这里依然能看到全部参与方。
|
|
2019
|
+
*/
|
|
2020
|
+
botsForEvent(event) {
|
|
2021
|
+
const record = this.correlation(event);
|
|
2022
|
+
if (!record) {
|
|
2023
|
+
const bot = event.bot;
|
|
2024
|
+
return bot ? [bot] : [];
|
|
2025
|
+
}
|
|
2026
|
+
const resolved = [];
|
|
2027
|
+
for (const participant of record.participants) {
|
|
2028
|
+
const bot = participant.botId ? this.#options.bots.find(participant.adapter, participant.botId) ?? this.#options.bots.pick(participant.botId) : void 0;
|
|
2029
|
+
if (bot && !resolved.includes(bot)) resolved.push(bot);
|
|
2030
|
+
}
|
|
2031
|
+
return resolved;
|
|
2032
|
+
}
|
|
2033
|
+
/** 事件里被 @ 到的、且本运行时已连接的 bot */
|
|
2034
|
+
mentionedBots(event) {
|
|
2035
|
+
if (event.kind !== "message") return [];
|
|
2036
|
+
const targets = new Set();
|
|
2037
|
+
for (const segment$1 of event.message) {
|
|
2038
|
+
if (segment$1.type !== "at") continue;
|
|
2039
|
+
const data = segment$1.data ?? {};
|
|
2040
|
+
const target = data.qq ?? data.target;
|
|
2041
|
+
if (target != null && String(target) !== "") targets.add(String(target));
|
|
2042
|
+
}
|
|
2043
|
+
if (targets.size === 0) return [];
|
|
2044
|
+
return this.#options.bots.all().filter((bot) => targets.has(String(bot.bot_id)));
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* 这条消息应当由哪个 bot 回应:优先「被 @ 的 bot」,其次是关联组里首个到达的 bot。
|
|
2048
|
+
* 插件可用它替代隐式的 `event.bot`,从而不受适配器到达顺序影响。
|
|
2049
|
+
*/
|
|
2050
|
+
pickReplyBot(event) {
|
|
2051
|
+
const mentioned = this.mentionedBots(event);
|
|
2052
|
+
if (mentioned.length > 0) return mentioned[0];
|
|
2053
|
+
const participants = this.botsForEvent(event);
|
|
2054
|
+
if (participants.length > 0) return participants[0];
|
|
2055
|
+
return event.bot;
|
|
2056
|
+
}
|
|
2057
|
+
/** 事件关联/去重的运行统计 */
|
|
2058
|
+
correlationStats() {
|
|
2059
|
+
return this.#options.correlator?.stats();
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2101
2062
|
* 获取事件引用回复的消息内容。
|
|
2102
2063
|
* 返回 null 表示没有引用、拿不到对应 bot 或消息已失效。
|
|
2103
2064
|
*/
|
|
@@ -2130,10 +2091,10 @@ var MiokuContext = class {
|
|
|
2130
2091
|
const routes = inputRoutes.map((item) => item.startsWith("!") ? item.slice(1) : item);
|
|
2131
2092
|
const source = `plugin:${this.#options.pluginName}`;
|
|
2132
2093
|
const handledEvents = new WeakSet();
|
|
2133
|
-
const
|
|
2094
|
+
const correlator = bypassDedup ? void 0 : this.#options.correlator;
|
|
2134
2095
|
const wrappedHandler = async (event) => {
|
|
2135
2096
|
if (handledEvents.has(event)) return;
|
|
2136
|
-
if (
|
|
2097
|
+
if (correlator?.isDuplicate(event)) return;
|
|
2137
2098
|
handledEvents.add(event);
|
|
2138
2099
|
await handler(event);
|
|
2139
2100
|
};
|
|
@@ -2274,6 +2235,286 @@ var MiokuContext = class {
|
|
|
2274
2235
|
}
|
|
2275
2236
|
};
|
|
2276
2237
|
|
|
2238
|
+
//#endregion
|
|
2239
|
+
//#region src/runtime/event-correlator.ts
|
|
2240
|
+
const MESSAGE_TTL_MS = 15e3;
|
|
2241
|
+
const EVENT_TTL_MS = 6e4;
|
|
2242
|
+
const PRUNE_INTERVAL = 128;
|
|
2243
|
+
const MAX_SIZE = 4096;
|
|
2244
|
+
const MAX_SEGMENTS = 16;
|
|
2245
|
+
const MAX_TEXT = 256;
|
|
2246
|
+
const HEX32 = /[0-9a-f]{32}/i;
|
|
2247
|
+
const HEX40 = /[0-9a-f]{40}/i;
|
|
2248
|
+
const MEDIA_URL_QUERY_KEYS = [
|
|
2249
|
+
"fileid",
|
|
2250
|
+
"file_id",
|
|
2251
|
+
"md5",
|
|
2252
|
+
"fid"
|
|
2253
|
+
];
|
|
2254
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2255
|
+
const hashString = (input) => createHash("sha1").update(input).digest("hex").slice(0, 20);
|
|
2256
|
+
/** 稳定序列化:对象键排序、无多余空白,忽略跨适配器的排版差异 */
|
|
2257
|
+
const canonicalJson = (value) => {
|
|
2258
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
2259
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
2260
|
+
const obj = value;
|
|
2261
|
+
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(obj[key])}`).join(",")}}`;
|
|
2262
|
+
};
|
|
2263
|
+
/** 结构化载荷(json/xml/ark)的稳定摘要 */
|
|
2264
|
+
const structuredDigest = (raw) => {
|
|
2265
|
+
let value = raw;
|
|
2266
|
+
if (typeof raw === "string") {
|
|
2267
|
+
const trimmed = raw.trim();
|
|
2268
|
+
if (!trimmed) return "";
|
|
2269
|
+
try {
|
|
2270
|
+
value = JSON.parse(trimmed);
|
|
2271
|
+
} catch {
|
|
2272
|
+
return `s:${hashString(trimmed.slice(0, MAX_TEXT * 4))}`;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return `j:${hashString(canonicalJson(value))}`;
|
|
2276
|
+
};
|
|
2277
|
+
/** 去掉会过期/随会话变化的查询参数,保留跨适配器稳定的媒体定位 */
|
|
2278
|
+
const normalizeMediaUrl = (raw) => {
|
|
2279
|
+
try {
|
|
2280
|
+
const url = new URL(raw);
|
|
2281
|
+
const kept = new URLSearchParams();
|
|
2282
|
+
for (const key of MEDIA_URL_QUERY_KEYS) {
|
|
2283
|
+
const value = url.searchParams.get(key);
|
|
2284
|
+
if (value) kept.set(key, value);
|
|
2285
|
+
}
|
|
2286
|
+
const query = kept.toString();
|
|
2287
|
+
return `${url.host}${url.pathname}${query ? `?${query}` : ""}`;
|
|
2288
|
+
} catch {
|
|
2289
|
+
return raw;
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
const firstHex = (value, pattern) => {
|
|
2293
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
2294
|
+
const match$1 = value.match(pattern);
|
|
2295
|
+
return match$1 ? match$1[0].toLowerCase() : void 0;
|
|
2296
|
+
};
|
|
2297
|
+
/**
|
|
2298
|
+
* 媒体段的内容摘要:优先内容哈希(md5/sha1),其次是文件名里嵌的 md5,
|
|
2299
|
+
* 再次是规范化 URL。都拿不到时返回空串,由调用方退化为「只看段类型」。
|
|
2300
|
+
*/
|
|
2301
|
+
const mediaDigest = (data) => {
|
|
2302
|
+
const md5$1 = firstHex(data.md5, HEX32);
|
|
2303
|
+
if (md5$1) return `h:${md5$1}`;
|
|
2304
|
+
const sha1 = firstHex(data.sha1, HEX40);
|
|
2305
|
+
if (sha1) return `h:${sha1}`;
|
|
2306
|
+
for (const key of [
|
|
2307
|
+
"file_unique",
|
|
2308
|
+
"file_id",
|
|
2309
|
+
"fid",
|
|
2310
|
+
"id",
|
|
2311
|
+
"file",
|
|
2312
|
+
"name"
|
|
2313
|
+
]) {
|
|
2314
|
+
const hex = firstHex(data[key], HEX32);
|
|
2315
|
+
if (hex) return `h:${hex}`;
|
|
2316
|
+
}
|
|
2317
|
+
const url = data.url;
|
|
2318
|
+
if (typeof url === "string" && url) return `u:${normalizeMediaUrl(url)}`;
|
|
2319
|
+
for (const key of [
|
|
2320
|
+
"file",
|
|
2321
|
+
"file_id",
|
|
2322
|
+
"fid",
|
|
2323
|
+
"id",
|
|
2324
|
+
"name"
|
|
2325
|
+
]) {
|
|
2326
|
+
const value = data[key];
|
|
2327
|
+
if (typeof value === "string" && value) return `r:${value.toLowerCase()}`;
|
|
2328
|
+
}
|
|
2329
|
+
return "";
|
|
2330
|
+
};
|
|
2331
|
+
/** 只在载荷跨协议一致时才使用摘要的段类型 */
|
|
2332
|
+
const DIGEST_MEDIA_TYPES = new Set(["image", "flash"]);
|
|
2333
|
+
const segmentSignature = (segment$1) => {
|
|
2334
|
+
const data = isRecord(segment$1.data) ? segment$1.data : {};
|
|
2335
|
+
switch (segment$1.type) {
|
|
2336
|
+
case "text": {
|
|
2337
|
+
const text$1 = typeof data.text === "string" ? data.text : "";
|
|
2338
|
+
return `t:${text$1.slice(0, MAX_TEXT)}`;
|
|
2339
|
+
}
|
|
2340
|
+
case "at": {
|
|
2341
|
+
const target = data.qq ?? data.target;
|
|
2342
|
+
return `a:${target == null ? "" : String(target)}`;
|
|
2343
|
+
}
|
|
2344
|
+
case "face": {
|
|
2345
|
+
const id = data.id ?? data.face;
|
|
2346
|
+
return `f:${id == null ? "" : String(id)}`;
|
|
2347
|
+
}
|
|
2348
|
+
case "json":
|
|
2349
|
+
case "xml":
|
|
2350
|
+
case "ark": {
|
|
2351
|
+
const digest = structuredDigest(data.data ?? data.content ?? data);
|
|
2352
|
+
return digest ? `${segment$1.type}:${digest}` : segment$1.type;
|
|
2353
|
+
}
|
|
2354
|
+
default: {
|
|
2355
|
+
if (DIGEST_MEDIA_TYPES.has(segment$1.type)) {
|
|
2356
|
+
const digest = mediaDigest(data);
|
|
2357
|
+
return digest ? `${segment$1.type}:${digest}` : segment$1.type;
|
|
2358
|
+
}
|
|
2359
|
+
return segment$1.type;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
const contentSignatureOf = (event) => {
|
|
2364
|
+
const parts = [];
|
|
2365
|
+
for (const segment$1 of event.message) {
|
|
2366
|
+
if (parts.length >= MAX_SEGMENTS) break;
|
|
2367
|
+
if (segment$1.type === "reply") continue;
|
|
2368
|
+
parts.push(segmentSignature(segment$1));
|
|
2369
|
+
}
|
|
2370
|
+
return parts.join("|");
|
|
2371
|
+
};
|
|
2372
|
+
const messageKeyOf = (event) => {
|
|
2373
|
+
const type = event.message_type ?? "";
|
|
2374
|
+
const scope = event.group_id ?? event.user_id ?? "";
|
|
2375
|
+
const sender = event.user_id ?? event.sender?.user_id ?? "";
|
|
2376
|
+
return [
|
|
2377
|
+
"m",
|
|
2378
|
+
type,
|
|
2379
|
+
scope,
|
|
2380
|
+
sender,
|
|
2381
|
+
contentSignatureOf(event)
|
|
2382
|
+
].join("|");
|
|
2383
|
+
};
|
|
2384
|
+
const noticeKeyOf = (event) => [
|
|
2385
|
+
"n",
|
|
2386
|
+
event.identity.event_type,
|
|
2387
|
+
event.notice_type ?? "",
|
|
2388
|
+
event.sub_type ?? "",
|
|
2389
|
+
event.group_id ?? "",
|
|
2390
|
+
event.user_id ?? "",
|
|
2391
|
+
event.operator_id ?? "",
|
|
2392
|
+
event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
|
|
2393
|
+
].join("|");
|
|
2394
|
+
const requestKeyOf = (event) => [
|
|
2395
|
+
"r",
|
|
2396
|
+
event.identity.event_type,
|
|
2397
|
+
event.request_type ?? "",
|
|
2398
|
+
event.sub_type ?? "",
|
|
2399
|
+
event.group_id ?? "",
|
|
2400
|
+
event.user_id ?? "",
|
|
2401
|
+
event.comment ?? "",
|
|
2402
|
+
event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
|
|
2403
|
+
].join("|");
|
|
2404
|
+
const participantOf = (event) => ({
|
|
2405
|
+
adapter: event.identity.adapter,
|
|
2406
|
+
botId: event.identity.bot_id,
|
|
2407
|
+
eventType: event.identity.event_type,
|
|
2408
|
+
messageId: event.identity.message_id ?? event.identity.native_event_id
|
|
2409
|
+
});
|
|
2410
|
+
const sameParticipant = (a, b) => a.adapter === b.adapter && (a.botId ?? "") === (b.botId ?? "") && (a.messageId ?? "") === (b.messageId ?? "");
|
|
2411
|
+
/**
|
|
2412
|
+
* 跨适配器事件关联器
|
|
2413
|
+
*/
|
|
2414
|
+
var EventCorrelator = class {
|
|
2415
|
+
#groups = new Map();
|
|
2416
|
+
#byEvent = new WeakMap();
|
|
2417
|
+
#logger;
|
|
2418
|
+
#maxSize;
|
|
2419
|
+
#inserts = 0;
|
|
2420
|
+
#observed = 0;
|
|
2421
|
+
#duplicates = 0;
|
|
2422
|
+
#evicted = 0;
|
|
2423
|
+
constructor(options = {}) {
|
|
2424
|
+
this.#logger = options.logger;
|
|
2425
|
+
this.#maxSize = options.maxSize ?? MAX_SIZE;
|
|
2426
|
+
}
|
|
2427
|
+
/** 观察一个事件,登记它所属的关联组并返回其定位(同一事件只登记一次) */
|
|
2428
|
+
observe(event) {
|
|
2429
|
+
const cached = this.#byEvent.get(event);
|
|
2430
|
+
if (cached) return cached;
|
|
2431
|
+
const located = this.#locate(event);
|
|
2432
|
+
if (!located) return null;
|
|
2433
|
+
const { key, ttl } = located;
|
|
2434
|
+
const now = Date.now();
|
|
2435
|
+
this.#observed++;
|
|
2436
|
+
let group = this.#groups.get(key);
|
|
2437
|
+
let duplicate = false;
|
|
2438
|
+
if (group && group.expiresAt >= now) {
|
|
2439
|
+
duplicate = true;
|
|
2440
|
+
this.#duplicates++;
|
|
2441
|
+
const participant = participantOf(event);
|
|
2442
|
+
if (!group.participants.some((item) => sameParticipant(item, participant))) group.participants.push(participant);
|
|
2443
|
+
this.#logger?.debug(`去重命中: key=${key} adapter=${participant.adapter} bot=${participant.botId ?? ""} primary=${group.primary.adapter}:${group.primary.botId ?? ""}`);
|
|
2444
|
+
} else {
|
|
2445
|
+
const participant = participantOf(event);
|
|
2446
|
+
group = {
|
|
2447
|
+
key,
|
|
2448
|
+
expiresAt: now + ttl,
|
|
2449
|
+
firstSeenAt: now,
|
|
2450
|
+
primary: participant,
|
|
2451
|
+
participants: [participant]
|
|
2452
|
+
};
|
|
2453
|
+
this.#groups.set(key, group);
|
|
2454
|
+
this.#inserts++;
|
|
2455
|
+
if (this.#inserts >= PRUNE_INTERVAL || this.#groups.size > this.#maxSize) {
|
|
2456
|
+
this.#inserts = 0;
|
|
2457
|
+
this.#prune(now);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
const observation = {
|
|
2461
|
+
record: group,
|
|
2462
|
+
primary: !duplicate,
|
|
2463
|
+
duplicate
|
|
2464
|
+
};
|
|
2465
|
+
this.#byEvent.set(event, observation);
|
|
2466
|
+
return observation;
|
|
2467
|
+
}
|
|
2468
|
+
/** 读取某事件此前登记的关联定位(未登记或已过期时返回 undefined) */
|
|
2469
|
+
observationOf(event) {
|
|
2470
|
+
const observation = this.#byEvent.get(event);
|
|
2471
|
+
if (!observation) return void 0;
|
|
2472
|
+
if (observation.record.expiresAt < Date.now()) return void 0;
|
|
2473
|
+
return observation;
|
|
2474
|
+
}
|
|
2475
|
+
/** 该事件是否属于关联组里后续到达的重复投递(未登记时会先登记) */
|
|
2476
|
+
isDuplicate(event) {
|
|
2477
|
+
return this.observe(event)?.duplicate === true;
|
|
2478
|
+
}
|
|
2479
|
+
stats() {
|
|
2480
|
+
return {
|
|
2481
|
+
groups: this.#groups.size,
|
|
2482
|
+
observed: this.#observed,
|
|
2483
|
+
duplicates: this.#duplicates,
|
|
2484
|
+
evicted: this.#evicted
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
clear() {
|
|
2488
|
+
this.#groups.clear();
|
|
2489
|
+
}
|
|
2490
|
+
#locate(event) {
|
|
2491
|
+
if (event.kind === "message") return {
|
|
2492
|
+
key: messageKeyOf(event),
|
|
2493
|
+
ttl: MESSAGE_TTL_MS
|
|
2494
|
+
};
|
|
2495
|
+
if (event.kind === "notice") return {
|
|
2496
|
+
key: noticeKeyOf(event),
|
|
2497
|
+
ttl: EVENT_TTL_MS
|
|
2498
|
+
};
|
|
2499
|
+
if (event.kind === "request") return {
|
|
2500
|
+
key: requestKeyOf(event),
|
|
2501
|
+
ttl: EVENT_TTL_MS
|
|
2502
|
+
};
|
|
2503
|
+
return null;
|
|
2504
|
+
}
|
|
2505
|
+
#prune(now) {
|
|
2506
|
+
for (const [key, group] of this.#groups) if (group.expiresAt < now) this.#groups.delete(key);
|
|
2507
|
+
if (this.#groups.size <= this.#maxSize) return;
|
|
2508
|
+
const drop = this.#groups.size - this.#maxSize;
|
|
2509
|
+
let dropped = 0;
|
|
2510
|
+
for (const key of this.#groups.keys()) {
|
|
2511
|
+
this.#groups.delete(key);
|
|
2512
|
+
this.#evicted++;
|
|
2513
|
+
if (++dropped >= drop) break;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
|
|
2277
2518
|
//#endregion
|
|
2278
2519
|
//#region src/runtime/plugin-metadata.ts
|
|
2279
2520
|
const store$1 = getOrCreate("plugin-metadata", () => new Map());
|
|
@@ -4858,7 +5099,7 @@ var MiokuRuntime = class {
|
|
|
4858
5099
|
#enabledPlugins = new Map();
|
|
4859
5100
|
#driverFactory;
|
|
4860
5101
|
#builtinPlugins;
|
|
4861
|
-
#
|
|
5102
|
+
#correlator;
|
|
4862
5103
|
#started = false;
|
|
4863
5104
|
#stopped = false;
|
|
4864
5105
|
constructor(options) {
|
|
@@ -4866,7 +5107,7 @@ var MiokuRuntime = class {
|
|
|
4866
5107
|
this.#logger = options.logger;
|
|
4867
5108
|
this.#driverFactory = options.driverFactory ?? (() => createDefaultDriver());
|
|
4868
5109
|
this.#builtinPlugins = options.builtinPlugins ?? BUILTIN_PLUGINS$1;
|
|
4869
|
-
this.#
|
|
5110
|
+
this.#correlator = options.dedup?.crossAdapter === false ? void 0 : new EventCorrelator({ logger: this.#logger.child({ scope: "dedup" }) });
|
|
4870
5111
|
this.#driver = this.#driverFactory();
|
|
4871
5112
|
this.#bus = new EventBus();
|
|
4872
5113
|
this.#bus.setLogger((level, message, detail) => {
|
|
@@ -4889,6 +5130,10 @@ var MiokuRuntime = class {
|
|
|
4889
5130
|
get bus() {
|
|
4890
5131
|
return this.#bus;
|
|
4891
5132
|
}
|
|
5133
|
+
/** 事件关联器(跨适配器去重);为 undefined 时表示已禁用 */
|
|
5134
|
+
get correlator() {
|
|
5135
|
+
return this.#correlator;
|
|
5136
|
+
}
|
|
4892
5137
|
get bots() {
|
|
4893
5138
|
return this.#bots.all();
|
|
4894
5139
|
}
|
|
@@ -4996,7 +5241,8 @@ var MiokuRuntime = class {
|
|
|
4996
5241
|
driver: this.#driver,
|
|
4997
5242
|
capabilities: this.#capabilities,
|
|
4998
5243
|
logger: this.#logger.child({ adapter: state.definition.name }),
|
|
4999
|
-
emit: (event) => this.#emitLifecycle(event)
|
|
5244
|
+
emit: (event) => this.#emitLifecycle(event),
|
|
5245
|
+
correlator: this.#correlator
|
|
5000
5246
|
});
|
|
5001
5247
|
}
|
|
5002
5248
|
async #loadPlugin(plugin, type) {
|
|
@@ -5010,7 +5256,7 @@ var MiokuRuntime = class {
|
|
|
5010
5256
|
config: botConfig,
|
|
5011
5257
|
logger: this.#logger.child({ plugin: plugin.name }),
|
|
5012
5258
|
priority: plugin.priority ?? 100,
|
|
5013
|
-
|
|
5259
|
+
correlator: this.#correlator,
|
|
5014
5260
|
getAdapter: (name) => this.getAdapter(name),
|
|
5015
5261
|
listAdapters: () => this.adapters,
|
|
5016
5262
|
onUpdateConfig: async (updater) => {
|
|
@@ -5299,6 +5545,7 @@ var MiokuRuntime = class {
|
|
|
5299
5545
|
this.#enabledPlugins.clear();
|
|
5300
5546
|
this.#capabilities.clear();
|
|
5301
5547
|
this.#bots.clear();
|
|
5548
|
+
this.#correlator?.clear();
|
|
5302
5549
|
resetPluginMetadata();
|
|
5303
5550
|
try {
|
|
5304
5551
|
await this.#driver.shutdown();
|
|
@@ -5461,6 +5708,7 @@ async function start(options = {}) {
|
|
|
5461
5708
|
};
|
|
5462
5709
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
5463
5710
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
5711
|
+
installProcessErrorGuards();
|
|
5464
5712
|
return { stop: (reason) => stopRuntime(reason) };
|
|
5465
5713
|
}
|
|
5466
5714
|
function readVersion() {
|
|
@@ -5473,7 +5721,16 @@ function readVersion() {
|
|
|
5473
5721
|
}
|
|
5474
5722
|
}
|
|
5475
5723
|
const version$1 = readVersion();
|
|
5724
|
+
function installProcessErrorGuards() {
|
|
5725
|
+
if (process.listenerCount("unhandledRejection") === 0) process.on("unhandledRejection", (reason) => {
|
|
5726
|
+
const detail = reason instanceof Error ? reason.stack ?? reason.message : reason;
|
|
5727
|
+
rootLogger.error(`[unhandledRejection] ${String(detail)}`);
|
|
5728
|
+
});
|
|
5729
|
+
if (process.listenerCount("uncaughtException") === 0) process.on("uncaughtException", (err) => {
|
|
5730
|
+
rootLogger.error(`[uncaughtException] ${err.stack ?? err.message ?? String(err)}`);
|
|
5731
|
+
});
|
|
5732
|
+
}
|
|
5476
5733
|
|
|
5477
5734
|
//#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 };
|
|
5735
|
+
export { AI_GEMINI_THINKING_LEVELS, AI_THINKING_LEVELS, AdapterContextImpl, AdapterRegistrationConflictError, ArchMap, BOT_CWD, BUILTIN_PLUGINS, BotRegistry, CORE_PLUGINS, CapabilityRegistry, ChromeUA, 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, 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 };
|
|
5479
5736
|
//# sourceMappingURL=index.js.map
|