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.
@@ -1 +1 @@
1
- {"version":3,"file":"exec-O8rCedzr.cjs","names":["path","candidate: string","command: string","resolved: string | null","resolved: string","args: string[]","options: { cwd?: string; env?: NodeJS.ProcessEnv }","options: { cwd?: string }"],"sources":["../package.json","../src/internal/exec.ts"],"sourcesContent":["{\n \"name\": \"mioku\",\n \"type\": \"module\",\n \"version\": \"1.0.6\",\n \"packageManager\": \"bun@1.2.0\",\n \"description\": \"Mioku - A plugin-based QQ bot framework\",\n \"keywords\": [\n \"onebot\",\n \"framework\",\n \"bot\",\n \"mioku\"\n ],\n \"bin\": {\n \"mioku\": \"./dist/cli/index.js\"\n },\n \"engines\": {\n \"node\": \">= 22.18.0\"\n },\n \"homepage\": \"https://github.com/mioku-lab/mioku#readme\",\n \"files\": [\n \"dist\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/mioku-lab/mioku/issues\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/mioku-lab/mioku.git\",\n \"directory\": \"packages/mioku\"\n },\n \"scripts\": {\n \"dev\": \"tsdown -w\",\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"exports\": {\n \".\": {\n \"require\": \"./dist/index.cjs\",\n \"import\": \"./dist/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"author\": \"Jerryplusy <jerryplusy@outlook.com>\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"napcat-sdk\": \"^0.16.0\",\n \"consola\": \"^3.4.2\",\n \"dayjs\": \"^1.11.19\",\n \"dedent\": \"^1.7.1\",\n \"filesize\": \"^11.0.13\",\n \"inquirer\": \"^10.0.0\",\n \"jiti\": \"^2.6.1\",\n \"lowdb\": \"^7.0.1\",\n \"mri\": \"^1.2.0\",\n \"node-cron\": \"^4.2.1\",\n \"pretty-ms\": \"^9.3.0\",\n \"string2argv\": \"^1.0.2\",\n \"lodash\": \"^4.17.21\"\n },\n \"devDependencies\": {\n \"@types/lodash\": \"^4.17.0\",\n \"@types/node\": \"^22.0.0\",\n \"tsdown\": \"^0.11.0\",\n \"typescript\": \"^5.8.0\"\n }\n}\n","import { spawn, spawnSync } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\n\nconst isWindows = process.platform === \"win32\";\n\nconst resolveCache = new Map<string, string | null>();\n\nfunction windowsExtensions(): string[] {\n const raw = process.env.PATHEXT || \".COM;.EXE;.BAT;.CMD\";\n return raw\n .split(\";\")\n .map((ext) => ext.trim())\n .filter(Boolean);\n}\n\n// PATH is a snapshot taken at process start, so a package manager installed\n// mid-run won't be on it. We enumerate every plausible install prefix for\n// bun / npm / scoop / chocolatey so the CLI can find bun in one shot.\nfunction searchDirs(): string[] {\n const raw = process.env.PATH || process.env.Path || \"\";\n const dirs = raw\n .split(path.delimiter)\n .map((d) => d.trim())\n .filter(Boolean);\n const home = os.homedir();\n const appData = process.env.APPDATA || path.join(home, \"AppData\", \"Roaming\");\n const localAppData =\n process.env.LOCALAPPDATA || path.join(home, \"AppData\", \"Local\");\n const programFiles = process.env.ProgramFiles || \"C:\\\\Program Files\";\n const programFilesX86 =\n process.env[\"ProgramFiles(x86)\"] || \"C:\\\\Program Files (x86)\";\n\n const extra = isWindows\n ? [\n // 官方安装器\n path.join(home, \".bun\", \"bin\"),\n // npm 全局(带 .cmd shim)\n path.join(appData, \"npm\"),\n path.join(appData, \"npm\", \"node_modules\", \".bin\"),\n // 备用 bun 位置\n path.join(localAppData, \"bun\", \"bin\"),\n path.join(programFiles, \"bun\", \"bin\"),\n path.join(programFilesX86, \"bun\", \"bin\"),\n // scoop\n path.join(home, \"scoop\", \"shims\"),\n // chocolatey\n \"C:\\\\ProgramData\\\\chocolatey\\\\bin\",\n // node 自带(很多用户 npm i -g 装到 node 目录)\n path.join(programFiles, \"nodejs\"),\n path.join(programFilesX86, \"nodejs\"),\n ]\n : [\n path.join(home, \".bun\", \"bin\"),\n \"/usr/local/bin\",\n \"/opt/homebrew/bin\",\n path.join(home, \".npm-global\", \"bin\"),\n ];\n\n return [...dirs, ...extra.filter((dir) => dir && !dir.endsWith(path.sep))];\n}\n\nfunction isExecutableFile(candidate: string): boolean {\n try {\n return fs.statSync(candidate).isFile();\n } catch {\n return false;\n }\n}\n\nfunction searchCommandViaShell(command: string): string | null {\n if (command.includes(\"/\") || command.includes(path.sep)) return null;\n const shellCmd = isWindows ? \"where\" : \"which\";\n try {\n const result = spawnSync(shellCmd, [command], {\n encoding: \"utf-8\",\n windowsHide: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: 5000,\n });\n if (result.status === 0 && result.stdout) {\n const first = result.stdout\n .split(/\\r?\\n/)\n .map((s) => s.trim())\n .find(Boolean);\n if (first && isExecutableFile(first)) return first;\n }\n } catch {}\n return null;\n}\n\n/** 在 PATH 及常见安装目录中解析命令的完整路径,结果带缓存 */\nexport function resolveCommand(command: string): string | null {\n if (resolveCache.has(command)) return resolveCache.get(command) ?? null;\n\n let resolved: string | null = null;\n\n if (command.includes(\"/\") || command.includes(path.sep)) {\n resolved = isExecutableFile(command) ? path.resolve(command) : null;\n } else {\n const exts = isWindows ? windowsExtensions() : [\"\"];\n const hasKnownExt =\n isWindows &&\n exts.some((ext) => command.toLowerCase().endsWith(ext.toLowerCase()));\n\n outer: for (const dir of searchDirs()) {\n if (hasKnownExt) {\n const direct = path.join(dir, command);\n if (isExecutableFile(direct)) {\n resolved = direct;\n break;\n }\n continue;\n }\n for (const ext of exts) {\n const candidate = path.join(dir, command + ext);\n if (isExecutableFile(candidate)) {\n resolved = candidate;\n break outer;\n }\n }\n }\n\n if (!resolved) resolved = searchCommandViaShell(command);\n }\n\n resolveCache.set(command, resolved);\n return resolved;\n}\n\nexport function clearCommandCache(): void {\n resolveCache.clear();\n}\n\nfunction needsCmdShell(resolved: string): boolean {\n if (!isWindows) return false;\n const ext = path.extname(resolved).toLowerCase();\n return ext === \".cmd\" || ext === \".bat\";\n}\n\nexport interface SpawnPlan {\n file: string;\n args: string[];\n windowsVerbatimArguments?: boolean;\n shell?: boolean | string;\n}\n\n// Windows can't CreateProcess a .cmd/.bat shim directly (npm installs bun as\n// bun.cmd), so those get routed through cmd.exe via shell: true. Previously\n// we hand-rolled a cmd.exe invocation with windowsVerbatimArguments: true,\n// but that combination misbehaves with spawnSync + stdio:\"inherit\" on\n// Node.js >= 20 (manifests as the spawned process printing \"undefined\" or\n// a spurious EINVAL), so we let Node's shell wrapper do the quoting.\n/** 生成跨平台的 spawn 参数,Windows 下 .cmd/.bat 会改经 cmd.exe 执行 */\nexport function buildSpawnPlan(command: string, args: string[]): SpawnPlan {\n const resolved = resolveCommand(command);\n\n if (!resolved) {\n return { file: command, args };\n }\n\n if (!needsCmdShell(resolved)) {\n return { file: resolved, args };\n }\n\n return {\n file: resolved,\n args,\n shell: true,\n };\n}\n\nexport interface RunCommandResult {\n stdout: string;\n stderr: string;\n code: number;\n}\n\n/** 执行命令并收集 stdout/stderr 与退出码,失败不抛异常 */\nexport function runCommand(\n command: string,\n args: string[],\n options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},\n): Promise<RunCommandResult> {\n const plan = buildSpawnPlan(command, args);\n\n return new Promise((resolve) => {\n const child = spawn(plan.file, plan.args, {\n cwd: options.cwd,\n env: options.env,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n shell: plan.shell,\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout?.on(\"data\", (chunk) => {\n stdout += String(chunk);\n });\n child.stderr?.on(\"data\", (chunk) => {\n stderr += String(chunk);\n });\n\n child.on(\"close\", (code) => {\n resolve({ stdout, stderr, code: code ?? 1 });\n });\n child.on(\"error\", (error) => {\n resolve({\n stdout,\n stderr: `${stderr}\\n${error.message}`.trim(),\n code: 1,\n });\n });\n });\n}\n\n/** 同步执行命令并继承当前终端的输入输出,非零退出码时抛错 */\nexport function runCommandInherit(\n command: string,\n args: string[],\n options: { cwd?: string } = {},\n): void {\n const plan = buildSpawnPlan(command, args);\n const result = spawnSync(plan.file, plan.args, {\n cwd: options.cwd,\n stdio: \"inherit\",\n shell: plan.shell,\n });\n\n if (result.error) throw result.error;\n if (result.status !== 0) {\n throw new Error(`${command} 退出码 ${result.status}`);\n }\n}\n\n/** 判断命令是否可用 */\nexport function commandExists(command: string): boolean {\n return resolveCommand(command) !== null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"exec-BzdYBQLQ.cjs","names":["path","candidate: string","command: string","resolved: string | null","resolved: string","args: string[]","options: { cwd?: string; env?: NodeJS.ProcessEnv }","options: { cwd?: string }"],"sources":["../package.json","../src/internal/exec.ts"],"sourcesContent":["{\n \"name\": \"mioku\",\n \"type\": \"module\",\n \"version\": \"1.0.8\",\n \"packageManager\": \"bun@1.2.0\",\n \"description\": \"Mioku - A plugin-based QQ bot framework\",\n \"keywords\": [\n \"onebot\",\n \"framework\",\n \"bot\",\n \"mioku\"\n ],\n \"bin\": {\n \"mioku\": \"./dist/cli/index.js\"\n },\n \"engines\": {\n \"node\": \">= 22.18.0\"\n },\n \"homepage\": \"https://github.com/mioku-lab/mioku#readme\",\n \"files\": [\n \"dist\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/mioku-lab/mioku/issues\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/mioku-lab/mioku.git\",\n \"directory\": \"packages/mioku\"\n },\n \"scripts\": {\n \"dev\": \"tsdown -w\",\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"exports\": {\n \".\": {\n \"require\": \"./dist/index.cjs\",\n \"import\": \"./dist/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"author\": \"Jerryplusy <jerryplusy@outlook.com>\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"napcat-sdk\": \"^0.16.0\",\n \"consola\": \"^3.4.2\",\n \"dayjs\": \"^1.11.19\",\n \"dedent\": \"^1.7.1\",\n \"filesize\": \"^11.0.13\",\n \"inquirer\": \"^10.0.0\",\n \"jiti\": \"^2.6.1\",\n \"lowdb\": \"^7.0.1\",\n \"mri\": \"^1.2.0\",\n \"node-cron\": \"^4.2.1\",\n \"pretty-ms\": \"^9.3.0\",\n \"string2argv\": \"^1.0.2\",\n \"lodash\": \"^4.17.21\"\n },\n \"devDependencies\": {\n \"@types/lodash\": \"^4.17.0\",\n \"@types/node\": \"^22.0.0\",\n \"tsdown\": \"^0.11.0\",\n \"typescript\": \"^5.8.0\"\n }\n}\n","import { spawn, spawnSync } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\n\nconst isWindows = process.platform === \"win32\";\n\nconst resolveCache = new Map<string, string | null>();\n\nfunction windowsExtensions(): string[] {\n const raw = process.env.PATHEXT || \".COM;.EXE;.BAT;.CMD\";\n return raw\n .split(\";\")\n .map((ext) => ext.trim())\n .filter(Boolean);\n}\n\n// PATH is a snapshot taken at process start, so a package manager installed\n// mid-run won't be on it. We enumerate every plausible install prefix for\n// bun / npm / scoop / chocolatey so the CLI can find bun in one shot.\nfunction searchDirs(): string[] {\n const raw = process.env.PATH || process.env.Path || \"\";\n const dirs = raw\n .split(path.delimiter)\n .map((d) => d.trim())\n .filter(Boolean);\n const home = os.homedir();\n const appData = process.env.APPDATA || path.join(home, \"AppData\", \"Roaming\");\n const localAppData =\n process.env.LOCALAPPDATA || path.join(home, \"AppData\", \"Local\");\n const programFiles = process.env.ProgramFiles || \"C:\\\\Program Files\";\n const programFilesX86 =\n process.env[\"ProgramFiles(x86)\"] || \"C:\\\\Program Files (x86)\";\n\n const extra = isWindows\n ? [\n // 官方安装器\n path.join(home, \".bun\", \"bin\"),\n // npm 全局(带 .cmd shim)\n path.join(appData, \"npm\"),\n path.join(appData, \"npm\", \"node_modules\", \".bin\"),\n // 备用 bun 位置\n path.join(localAppData, \"bun\", \"bin\"),\n path.join(programFiles, \"bun\", \"bin\"),\n path.join(programFilesX86, \"bun\", \"bin\"),\n // scoop\n path.join(home, \"scoop\", \"shims\"),\n // chocolatey\n \"C:\\\\ProgramData\\\\chocolatey\\\\bin\",\n // node 自带(很多用户 npm i -g 装到 node 目录)\n path.join(programFiles, \"nodejs\"),\n path.join(programFilesX86, \"nodejs\"),\n ]\n : [\n path.join(home, \".bun\", \"bin\"),\n \"/usr/local/bin\",\n \"/opt/homebrew/bin\",\n path.join(home, \".npm-global\", \"bin\"),\n ];\n\n return [...dirs, ...extra.filter((dir) => dir && !dir.endsWith(path.sep))];\n}\n\nfunction isExecutableFile(candidate: string): boolean {\n try {\n return fs.statSync(candidate).isFile();\n } catch {\n return false;\n }\n}\n\nfunction searchCommandViaShell(command: string): string | null {\n if (command.includes(\"/\") || command.includes(path.sep)) return null;\n const shellCmd = isWindows ? \"where\" : \"which\";\n try {\n const result = spawnSync(shellCmd, [command], {\n encoding: \"utf-8\",\n windowsHide: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: 5000,\n });\n if (result.status === 0 && result.stdout) {\n const first = result.stdout\n .split(/\\r?\\n/)\n .map((s) => s.trim())\n .find(Boolean);\n if (first && isExecutableFile(first)) return first;\n }\n } catch {}\n return null;\n}\n\n/** 在 PATH 及常见安装目录中解析命令的完整路径,结果带缓存 */\nexport function resolveCommand(command: string): string | null {\n if (resolveCache.has(command)) return resolveCache.get(command) ?? null;\n\n let resolved: string | null = null;\n\n if (command.includes(\"/\") || command.includes(path.sep)) {\n resolved = isExecutableFile(command) ? path.resolve(command) : null;\n } else {\n const exts = isWindows ? windowsExtensions() : [\"\"];\n const hasKnownExt =\n isWindows &&\n exts.some((ext) => command.toLowerCase().endsWith(ext.toLowerCase()));\n\n outer: for (const dir of searchDirs()) {\n if (hasKnownExt) {\n const direct = path.join(dir, command);\n if (isExecutableFile(direct)) {\n resolved = direct;\n break;\n }\n continue;\n }\n for (const ext of exts) {\n const candidate = path.join(dir, command + ext);\n if (isExecutableFile(candidate)) {\n resolved = candidate;\n break outer;\n }\n }\n }\n\n if (!resolved) resolved = searchCommandViaShell(command);\n }\n\n resolveCache.set(command, resolved);\n return resolved;\n}\n\nexport function clearCommandCache(): void {\n resolveCache.clear();\n}\n\nfunction needsCmdShell(resolved: string): boolean {\n if (!isWindows) return false;\n const ext = path.extname(resolved).toLowerCase();\n return ext === \".cmd\" || ext === \".bat\";\n}\n\nexport interface SpawnPlan {\n file: string;\n args: string[];\n windowsVerbatimArguments?: boolean;\n shell?: boolean | string;\n}\n\n// Windows can't CreateProcess a .cmd/.bat shim directly (npm installs bun as\n// bun.cmd), so those get routed through cmd.exe via shell: true. Previously\n// we hand-rolled a cmd.exe invocation with windowsVerbatimArguments: true,\n// but that combination misbehaves with spawnSync + stdio:\"inherit\" on\n// Node.js >= 20 (manifests as the spawned process printing \"undefined\" or\n// a spurious EINVAL), so we let Node's shell wrapper do the quoting.\n/** 生成跨平台的 spawn 参数,Windows 下 .cmd/.bat 会改经 cmd.exe 执行 */\nexport function buildSpawnPlan(command: string, args: string[]): SpawnPlan {\n const resolved = resolveCommand(command);\n\n if (!resolved) {\n return { file: command, args };\n }\n\n if (!needsCmdShell(resolved)) {\n return { file: resolved, args };\n }\n\n return {\n file: resolved,\n args,\n shell: true,\n };\n}\n\nexport interface RunCommandResult {\n stdout: string;\n stderr: string;\n code: number;\n}\n\n/** 执行命令并收集 stdout/stderr 与退出码,失败不抛异常 */\nexport function runCommand(\n command: string,\n args: string[],\n options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},\n): Promise<RunCommandResult> {\n const plan = buildSpawnPlan(command, args);\n\n return new Promise((resolve) => {\n const child = spawn(plan.file, plan.args, {\n cwd: options.cwd,\n env: options.env,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n shell: plan.shell,\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout?.on(\"data\", (chunk) => {\n stdout += String(chunk);\n });\n child.stderr?.on(\"data\", (chunk) => {\n stderr += String(chunk);\n });\n\n child.on(\"close\", (code) => {\n resolve({ stdout, stderr, code: code ?? 1 });\n });\n child.on(\"error\", (error) => {\n resolve({\n stdout,\n stderr: `${stderr}\\n${error.message}`.trim(),\n code: 1,\n });\n });\n });\n}\n\n/** 同步执行命令并继承当前终端的输入输出,非零退出码时抛错 */\nexport function runCommandInherit(\n command: string,\n args: string[],\n options: { cwd?: string } = {},\n): void {\n const plan = buildSpawnPlan(command, args);\n const result = spawnSync(plan.file, plan.args, {\n cwd: options.cwd,\n stdio: \"inherit\",\n shell: plan.shell,\n });\n\n if (result.error) throw result.error;\n if (result.status !== 0) {\n throw new Error(`${command} 退出码 ${result.status}`);\n }\n}\n\n/** 判断命令是否可用 */\nexport function commandExists(command: string): boolean {\n return resolveCommand(command) !== null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAGa;;;;ACEb,MAAM,YAAY,QAAQ,aAAa;AAEvC,MAAM,eAAe,IAAI;AAEzB,SAAS,oBAA8B;CACrC,MAAM,MAAM,QAAQ,IAAI,WAAW;AACnC,QAAO,IACJ,MAAM,IAAI,CACV,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,CACxB,OAAO,QAAQ;AACnB;AAKD,SAAS,aAAuB;CAC9B,MAAM,MAAM,QAAQ,IAAI,QAAQ,QAAQ,IAAI,QAAQ;CACpD,MAAM,OAAO,IACV,MAAMA,UAAK,UAAU,CACrB,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CACpB,OAAO,QAAQ;CAClB,MAAM,OAAO,QAAG,SAAS;CACzB,MAAM,UAAU,QAAQ,IAAI,WAAW,UAAK,KAAK,MAAM,WAAW,UAAU;CAC5E,MAAM,eACJ,QAAQ,IAAI,gBAAgB,UAAK,KAAK,MAAM,WAAW,QAAQ;CACjE,MAAM,eAAe,QAAQ,IAAI,gBAAgB;CACjD,MAAM,kBACJ,QAAQ,IAAI,wBAAwB;CAEtC,MAAM,QAAQ,YACV;EAEE,UAAK,KAAK,MAAM,QAAQ,MAAM;EAE9B,UAAK,KAAK,SAAS,MAAM;EACzB,UAAK,KAAK,SAAS,OAAO,gBAAgB,OAAO;EAEjD,UAAK,KAAK,cAAc,OAAO,MAAM;EACrC,UAAK,KAAK,cAAc,OAAO,MAAM;EACrC,UAAK,KAAK,iBAAiB,OAAO,MAAM;EAExC,UAAK,KAAK,MAAM,SAAS,QAAQ;EAEjC;EAEA,UAAK,KAAK,cAAc,SAAS;EACjC,UAAK,KAAK,iBAAiB,SAAS;CACrC,IACD;EACE,UAAK,KAAK,MAAM,QAAQ,MAAM;EAC9B;EACA;EACA,UAAK,KAAK,MAAM,eAAe,MAAM;CACtC;AAEL,QAAO,CAAC,GAAG,MAAM,GAAG,MAAM,OAAO,CAAC,QAAQ,QAAQ,IAAI,SAASA,UAAK,IAAI,CAAC,AAAC;AAC3E;AAED,SAAS,iBAAiBC,WAA4B;AACpD,KAAI;AACF,SAAO,QAAG,SAAS,UAAU,CAAC,QAAQ;CACvC,QAAO;AACN,SAAO;CACR;AACF;AAED,SAAS,sBAAsBC,SAAgC;AAC7D,KAAI,QAAQ,SAAS,IAAI,IAAI,QAAQ,SAASF,UAAK,IAAI,CAAE,QAAO;CAChE,MAAM,WAAW,YAAY,UAAU;AACvC,KAAI;EACF,MAAM,SAAS,kCAAU,UAAU,CAAC,OAAQ,GAAE;GAC5C,UAAU;GACV,aAAa;GACb,OAAO;IAAC;IAAU;IAAQ;GAAO;GACjC,SAAS;EACV,EAAC;AACF,MAAI,OAAO,WAAW,KAAK,OAAO,QAAQ;GACxC,MAAM,QAAQ,OAAO,OAClB,MAAM,QAAQ,CACd,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CACpB,KAAK,QAAQ;AAChB,OAAI,SAAS,iBAAiB,MAAM,CAAE,QAAO;EAC9C;CACF,QAAO,CAAE;AACV,QAAO;AACR;;AAGD,SAAgB,eAAeE,SAAgC;AAC7D,KAAI,aAAa,IAAI,QAAQ,CAAE,QAAO,aAAa,IAAI,QAAQ,IAAI;CAEnE,IAAIC,WAA0B;AAE9B,KAAI,QAAQ,SAAS,IAAI,IAAI,QAAQ,SAASH,UAAK,IAAI,CACrD,YAAW,iBAAiB,QAAQ,GAAG,UAAK,QAAQ,QAAQ,GAAG;MAC1D;EACL,MAAM,OAAO,YAAY,mBAAmB,GAAG,CAAC,EAAG;EACnD,MAAM,cACJ,aACA,KAAK,KAAK,CAAC,QAAQ,QAAQ,aAAa,CAAC,SAAS,IAAI,aAAa,CAAC,CAAC;AAEvE,QAAO,MAAK,MAAM,OAAO,YAAY,EAAE;AACrC,OAAI,aAAa;IACf,MAAM,SAAS,UAAK,KAAK,KAAK,QAAQ;AACtC,QAAI,iBAAiB,OAAO,EAAE;AAC5B,gBAAW;AACX;IACD;AACD;GACD;AACD,QAAK,MAAM,OAAO,MAAM;IACtB,MAAM,YAAY,UAAK,KAAK,KAAK,UAAU,IAAI;AAC/C,QAAI,iBAAiB,UAAU,EAAE;AAC/B,gBAAW;AACX,WAAM;IACP;GACF;EACF;AAED,OAAK,SAAU,YAAW,sBAAsB,QAAQ;CACzD;AAED,cAAa,IAAI,SAAS,SAAS;AACnC,QAAO;AACR;AAED,SAAgB,oBAA0B;AACxC,cAAa,OAAO;AACrB;AAED,SAAS,cAAcI,UAA2B;AAChD,MAAK,UAAW,QAAO;CACvB,MAAM,MAAM,UAAK,QAAQ,SAAS,CAAC,aAAa;AAChD,QAAO,QAAQ,UAAU,QAAQ;AAClC;;AAgBD,SAAgB,eAAeF,SAAiBG,MAA2B;CACzE,MAAM,WAAW,eAAe,QAAQ;AAExC,MAAK,SACH,QAAO;EAAE,MAAM;EAAS;CAAM;AAGhC,MAAK,cAAc,SAAS,CAC1B,QAAO;EAAE,MAAM;EAAU;CAAM;AAGjC,QAAO;EACL,MAAM;EACN;EACA,OAAO;CACR;AACF;;AASD,SAAgB,WACdH,SACAG,MACAC,UAAqD,CAAE,GAC5B;CAC3B,MAAM,OAAO,eAAe,SAAS,KAAK;AAE1C,QAAO,IAAI,QAAQ,CAAC,YAAY;EAC9B,MAAM,QAAQ,8BAAM,KAAK,MAAM,KAAK,MAAM;GACxC,KAAK,QAAQ;GACb,KAAK,QAAQ;GACb,OAAO;IAAC;IAAU;IAAQ;GAAO;GACjC,OAAO,KAAK;EACb,EAAC;EAEF,IAAI,SAAS;EACb,IAAI,SAAS;AAEb,QAAM,QAAQ,GAAG,QAAQ,CAAC,UAAU;AAClC,aAAU,OAAO,MAAM;EACxB,EAAC;AACF,QAAM,QAAQ,GAAG,QAAQ,CAAC,UAAU;AAClC,aAAU,OAAO,MAAM;EACxB,EAAC;AAEF,QAAM,GAAG,SAAS,CAAC,SAAS;AAC1B,WAAQ;IAAE;IAAQ;IAAQ,MAAM,QAAQ;GAAG,EAAC;EAC7C,EAAC;AACF,QAAM,GAAG,SAAS,CAAC,UAAU;AAC3B,WAAQ;IACN;IACA,QAAQ,CAAC,EAAE,OAAO,IAAI,MAAM,QAAQ,EAAE,MAAM;IAC5C,MAAM;GACP,EAAC;EACH,EAAC;CACH;AACF;;AAGD,SAAgB,kBACdJ,SACAG,MACAE,UAA4B,CAAE,GACxB;CACN,MAAM,OAAO,eAAe,SAAS,KAAK;CAC1C,MAAM,SAAS,kCAAU,KAAK,MAAM,KAAK,MAAM;EAC7C,KAAK,QAAQ;EACb,OAAO;EACP,OAAO,KAAK;CACb,EAAC;AAEF,KAAI,OAAO,MAAO,OAAM,OAAO;AAC/B,KAAI,OAAO,WAAW,EACpB,OAAM,IAAI,OAAO,EAAE,QAAQ,OAAO,OAAO,OAAO;AAEnD;;AAGD,SAAgB,cAAcL,SAA0B;AACtD,QAAO,eAAe,QAAQ,KAAK;AACpC"}
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_exec = require('./exec-O8rCedzr.cjs');
1
+ const require_exec = require('./exec-BzdYBQLQ.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,7 @@ var AdapterContextImpl = class {
855
855
  #capabilities;
856
856
  #logger;
857
857
  #emit;
858
+ #correlator;
858
859
  #pendingStarts = new Set();
859
860
  constructor(options) {
860
861
  this.#state = options.state;
@@ -864,6 +865,7 @@ var AdapterContextImpl = class {
864
865
  this.#capabilities = options.capabilities;
865
866
  this.#logger = options.logger;
866
867
  this.#emit = options.emit;
868
+ this.#correlator = options.correlator;
867
869
  }
868
870
  registerBot(bot) {
869
871
  return this.#bots.register(bot);
@@ -927,6 +929,7 @@ var AdapterContextImpl = class {
927
929
  };
928
930
  }
929
931
  async dispatch(event) {
932
+ this.#correlator?.observe(event);
930
933
  if (event.kind === "message") {
931
934
  const senderId = event.user_id;
932
935
  if (senderId != null) {
@@ -1015,6 +1018,9 @@ var BotRegistry = class {
1015
1018
  for (const entry of this.#bots.values()) if (entry.bot_id === key) return entry.bot;
1016
1019
  return void 0;
1017
1020
  }
1021
+ find(adapter, bot_id) {
1022
+ return this.#bots.get(connectedBotKey(adapter, bot_id))?.bot;
1023
+ }
1018
1024
  all() {
1019
1025
  return Array.from(this.#bots.values()).map((entry) => entry.bot);
1020
1026
  }
@@ -1888,104 +1894,6 @@ const Services = {
1888
1894
  Help: defineService("help")
1889
1895
  };
1890
1896
 
1891
- //#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("|");
1943
- /**
1944
- * 跨适配器消息去重(L2)
1945
- */
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
1897
  //#endregion
1990
1898
  //#region src/runtime/mioku-context.ts
1991
1899
  const isObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -2083,6 +1991,59 @@ var MiokuContext = class {
2083
1991
  return this.#options.bots.pick(bot_id);
2084
1992
  }
2085
1993
  /**
1994
+ * 取事件所属的跨适配器关联记录。
1995
+ * 同一条逻辑消息被多个适配器/bot 投递时,可从这里读到全部参与方。
1996
+ */
1997
+ correlation(event) {
1998
+ return this.#options.correlator?.observationOf(event)?.record;
1999
+ }
2000
+ /**
2001
+ * 观察到该事件的全部已连接 bot(首个即 primary)。
2002
+ * 跨适配器去重后,被标记为重复的投递不会进入普通 handler,
2003
+ * 但这里依然能看到全部参与方。
2004
+ */
2005
+ botsForEvent(event) {
2006
+ const record = this.correlation(event);
2007
+ if (!record) {
2008
+ const bot = event.bot;
2009
+ return bot ? [bot] : [];
2010
+ }
2011
+ const resolved = [];
2012
+ for (const participant of record.participants) {
2013
+ const bot = participant.botId ? this.#options.bots.find(participant.adapter, participant.botId) ?? this.#options.bots.pick(participant.botId) : void 0;
2014
+ if (bot && !resolved.includes(bot)) resolved.push(bot);
2015
+ }
2016
+ return resolved;
2017
+ }
2018
+ /** 事件里被 @ 到的、且本运行时已连接的 bot */
2019
+ mentionedBots(event) {
2020
+ if (event.kind !== "message") return [];
2021
+ const targets = new Set();
2022
+ for (const segment$1 of event.message) {
2023
+ if (segment$1.type !== "at") continue;
2024
+ const data = segment$1.data ?? {};
2025
+ const target = data.qq ?? data.target;
2026
+ if (target != null && String(target) !== "") targets.add(String(target));
2027
+ }
2028
+ if (targets.size === 0) return [];
2029
+ return this.#options.bots.all().filter((bot) => targets.has(String(bot.bot_id)));
2030
+ }
2031
+ /**
2032
+ * 这条消息应当由哪个 bot 回应:优先「被 @ 的 bot」,其次是关联组里首个到达的 bot。
2033
+ * 插件可用它替代隐式的 `event.bot`,从而不受适配器到达顺序影响。
2034
+ */
2035
+ pickReplyBot(event) {
2036
+ const mentioned = this.mentionedBots(event);
2037
+ if (mentioned.length > 0) return mentioned[0];
2038
+ const participants = this.botsForEvent(event);
2039
+ if (participants.length > 0) return participants[0];
2040
+ return event.bot;
2041
+ }
2042
+ /** 事件关联/去重的运行统计 */
2043
+ correlationStats() {
2044
+ return this.#options.correlator?.stats();
2045
+ }
2046
+ /**
2086
2047
  * 获取事件引用回复的消息内容。
2087
2048
  * 返回 null 表示没有引用、拿不到对应 bot 或消息已失效。
2088
2049
  */
@@ -2115,10 +2076,10 @@ var MiokuContext = class {
2115
2076
  const routes = inputRoutes.map((item) => item.startsWith("!") ? item.slice(1) : item);
2116
2077
  const source = `plugin:${this.#options.pluginName}`;
2117
2078
  const handledEvents = new WeakSet();
2118
- const dedup = bypassDedup || this.#options.dedup === false ? null : new CrossAdapterEventDeduplicator();
2079
+ const correlator = bypassDedup ? void 0 : this.#options.correlator;
2119
2080
  const wrappedHandler = async (event) => {
2120
2081
  if (handledEvents.has(event)) return;
2121
- if (dedup?.isDuplicate(event)) return;
2082
+ if (correlator?.isDuplicate(event)) return;
2122
2083
  handledEvents.add(event);
2123
2084
  await handler(event);
2124
2085
  };
@@ -2259,6 +2220,286 @@ var MiokuContext = class {
2259
2220
  }
2260
2221
  };
2261
2222
 
2223
+ //#endregion
2224
+ //#region src/runtime/event-correlator.ts
2225
+ const MESSAGE_TTL_MS = 15e3;
2226
+ const EVENT_TTL_MS = 6e4;
2227
+ const PRUNE_INTERVAL = 128;
2228
+ const MAX_SIZE = 4096;
2229
+ const MAX_SEGMENTS = 16;
2230
+ const MAX_TEXT = 256;
2231
+ const HEX32 = /[0-9a-f]{32}/i;
2232
+ const HEX40 = /[0-9a-f]{40}/i;
2233
+ const MEDIA_URL_QUERY_KEYS = [
2234
+ "fileid",
2235
+ "file_id",
2236
+ "md5",
2237
+ "fid"
2238
+ ];
2239
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2240
+ const hashString = (input) => (0, node_crypto.createHash)("sha1").update(input).digest("hex").slice(0, 20);
2241
+ /** 稳定序列化:对象键排序、无多余空白,忽略跨适配器的排版差异 */
2242
+ const canonicalJson = (value) => {
2243
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
2244
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
2245
+ const obj = value;
2246
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(obj[key])}`).join(",")}}`;
2247
+ };
2248
+ /** 结构化载荷(json/xml/ark)的稳定摘要 */
2249
+ const structuredDigest = (raw) => {
2250
+ let value = raw;
2251
+ if (typeof raw === "string") {
2252
+ const trimmed = raw.trim();
2253
+ if (!trimmed) return "";
2254
+ try {
2255
+ value = JSON.parse(trimmed);
2256
+ } catch {
2257
+ return `s:${hashString(trimmed.slice(0, MAX_TEXT * 4))}`;
2258
+ }
2259
+ }
2260
+ return `j:${hashString(canonicalJson(value))}`;
2261
+ };
2262
+ /** 去掉会过期/随会话变化的查询参数,保留跨适配器稳定的媒体定位 */
2263
+ const normalizeMediaUrl = (raw) => {
2264
+ try {
2265
+ const url = new URL(raw);
2266
+ const kept = new URLSearchParams();
2267
+ for (const key of MEDIA_URL_QUERY_KEYS) {
2268
+ const value = url.searchParams.get(key);
2269
+ if (value) kept.set(key, value);
2270
+ }
2271
+ const query = kept.toString();
2272
+ return `${url.host}${url.pathname}${query ? `?${query}` : ""}`;
2273
+ } catch {
2274
+ return raw;
2275
+ }
2276
+ };
2277
+ const firstHex = (value, pattern) => {
2278
+ if (typeof value !== "string" || !value) return void 0;
2279
+ const match$1 = value.match(pattern);
2280
+ return match$1 ? match$1[0].toLowerCase() : void 0;
2281
+ };
2282
+ /**
2283
+ * 媒体段的内容摘要:优先内容哈希(md5/sha1),其次是文件名里嵌的 md5,
2284
+ * 再次是规范化 URL。都拿不到时返回空串,由调用方退化为「只看段类型」。
2285
+ */
2286
+ const mediaDigest = (data) => {
2287
+ const md5$1 = firstHex(data.md5, HEX32);
2288
+ if (md5$1) return `h:${md5$1}`;
2289
+ const sha1 = firstHex(data.sha1, HEX40);
2290
+ if (sha1) return `h:${sha1}`;
2291
+ for (const key of [
2292
+ "file_unique",
2293
+ "file_id",
2294
+ "fid",
2295
+ "id",
2296
+ "file",
2297
+ "name"
2298
+ ]) {
2299
+ const hex = firstHex(data[key], HEX32);
2300
+ if (hex) return `h:${hex}`;
2301
+ }
2302
+ const url = data.url;
2303
+ if (typeof url === "string" && url) return `u:${normalizeMediaUrl(url)}`;
2304
+ for (const key of [
2305
+ "file",
2306
+ "file_id",
2307
+ "fid",
2308
+ "id",
2309
+ "name"
2310
+ ]) {
2311
+ const value = data[key];
2312
+ if (typeof value === "string" && value) return `r:${value.toLowerCase()}`;
2313
+ }
2314
+ return "";
2315
+ };
2316
+ /** 只在载荷跨协议一致时才使用摘要的段类型 */
2317
+ const DIGEST_MEDIA_TYPES = new Set(["image", "flash"]);
2318
+ const segmentSignature = (segment$1) => {
2319
+ const data = isRecord(segment$1.data) ? segment$1.data : {};
2320
+ switch (segment$1.type) {
2321
+ case "text": {
2322
+ const text$1 = typeof data.text === "string" ? data.text : "";
2323
+ return `t:${text$1.slice(0, MAX_TEXT)}`;
2324
+ }
2325
+ case "at": {
2326
+ const target = data.qq ?? data.target;
2327
+ return `a:${target == null ? "" : String(target)}`;
2328
+ }
2329
+ case "face": {
2330
+ const id = data.id ?? data.face;
2331
+ return `f:${id == null ? "" : String(id)}`;
2332
+ }
2333
+ case "json":
2334
+ case "xml":
2335
+ case "ark": {
2336
+ const digest = structuredDigest(data.data ?? data.content ?? data);
2337
+ return digest ? `${segment$1.type}:${digest}` : segment$1.type;
2338
+ }
2339
+ default: {
2340
+ if (DIGEST_MEDIA_TYPES.has(segment$1.type)) {
2341
+ const digest = mediaDigest(data);
2342
+ return digest ? `${segment$1.type}:${digest}` : segment$1.type;
2343
+ }
2344
+ return segment$1.type;
2345
+ }
2346
+ }
2347
+ };
2348
+ const contentSignatureOf = (event) => {
2349
+ const parts = [];
2350
+ for (const segment$1 of event.message) {
2351
+ if (parts.length >= MAX_SEGMENTS) break;
2352
+ if (segment$1.type === "reply") continue;
2353
+ parts.push(segmentSignature(segment$1));
2354
+ }
2355
+ return parts.join("|");
2356
+ };
2357
+ const messageKeyOf = (event) => {
2358
+ const type = event.message_type ?? "";
2359
+ const scope = event.group_id ?? event.user_id ?? "";
2360
+ const sender = event.user_id ?? event.sender?.user_id ?? "";
2361
+ return [
2362
+ "m",
2363
+ type,
2364
+ scope,
2365
+ sender,
2366
+ contentSignatureOf(event)
2367
+ ].join("|");
2368
+ };
2369
+ const noticeKeyOf = (event) => [
2370
+ "n",
2371
+ event.identity.event_type,
2372
+ event.notice_type ?? "",
2373
+ event.sub_type ?? "",
2374
+ event.group_id ?? "",
2375
+ event.user_id ?? "",
2376
+ event.operator_id ?? "",
2377
+ event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
2378
+ ].join("|");
2379
+ const requestKeyOf = (event) => [
2380
+ "r",
2381
+ event.identity.event_type,
2382
+ event.request_type ?? "",
2383
+ event.sub_type ?? "",
2384
+ event.group_id ?? "",
2385
+ event.user_id ?? "",
2386
+ event.comment ?? "",
2387
+ event.identity.timestamp == null ? "" : Math.floor(event.identity.timestamp / 1e3)
2388
+ ].join("|");
2389
+ const participantOf = (event) => ({
2390
+ adapter: event.identity.adapter,
2391
+ botId: event.identity.bot_id,
2392
+ eventType: event.identity.event_type,
2393
+ messageId: event.identity.message_id ?? event.identity.native_event_id
2394
+ });
2395
+ const sameParticipant = (a, b) => a.adapter === b.adapter && (a.botId ?? "") === (b.botId ?? "") && (a.messageId ?? "") === (b.messageId ?? "");
2396
+ /**
2397
+ * 跨适配器事件关联器
2398
+ */
2399
+ var EventCorrelator = class {
2400
+ #groups = new Map();
2401
+ #byEvent = new WeakMap();
2402
+ #logger;
2403
+ #maxSize;
2404
+ #inserts = 0;
2405
+ #observed = 0;
2406
+ #duplicates = 0;
2407
+ #evicted = 0;
2408
+ constructor(options = {}) {
2409
+ this.#logger = options.logger;
2410
+ this.#maxSize = options.maxSize ?? MAX_SIZE;
2411
+ }
2412
+ /** 观察一个事件,登记它所属的关联组并返回其定位(同一事件只登记一次) */
2413
+ observe(event) {
2414
+ const cached = this.#byEvent.get(event);
2415
+ if (cached) return cached;
2416
+ const located = this.#locate(event);
2417
+ if (!located) return null;
2418
+ const { key, ttl } = located;
2419
+ const now = Date.now();
2420
+ this.#observed++;
2421
+ let group = this.#groups.get(key);
2422
+ let duplicate = false;
2423
+ if (group && group.expiresAt >= now) {
2424
+ duplicate = true;
2425
+ this.#duplicates++;
2426
+ const participant = participantOf(event);
2427
+ if (!group.participants.some((item) => sameParticipant(item, participant))) group.participants.push(participant);
2428
+ this.#logger?.debug(`去重命中: key=${key} adapter=${participant.adapter} bot=${participant.botId ?? ""} primary=${group.primary.adapter}:${group.primary.botId ?? ""}`);
2429
+ } else {
2430
+ const participant = participantOf(event);
2431
+ group = {
2432
+ key,
2433
+ expiresAt: now + ttl,
2434
+ firstSeenAt: now,
2435
+ primary: participant,
2436
+ participants: [participant]
2437
+ };
2438
+ this.#groups.set(key, group);
2439
+ this.#inserts++;
2440
+ if (this.#inserts >= PRUNE_INTERVAL || this.#groups.size > this.#maxSize) {
2441
+ this.#inserts = 0;
2442
+ this.#prune(now);
2443
+ }
2444
+ }
2445
+ const observation = {
2446
+ record: group,
2447
+ primary: !duplicate,
2448
+ duplicate
2449
+ };
2450
+ this.#byEvent.set(event, observation);
2451
+ return observation;
2452
+ }
2453
+ /** 读取某事件此前登记的关联定位(未登记或已过期时返回 undefined) */
2454
+ observationOf(event) {
2455
+ const observation = this.#byEvent.get(event);
2456
+ if (!observation) return void 0;
2457
+ if (observation.record.expiresAt < Date.now()) return void 0;
2458
+ return observation;
2459
+ }
2460
+ /** 该事件是否属于关联组里后续到达的重复投递(未登记时会先登记) */
2461
+ isDuplicate(event) {
2462
+ return this.observe(event)?.duplicate === true;
2463
+ }
2464
+ stats() {
2465
+ return {
2466
+ groups: this.#groups.size,
2467
+ observed: this.#observed,
2468
+ duplicates: this.#duplicates,
2469
+ evicted: this.#evicted
2470
+ };
2471
+ }
2472
+ clear() {
2473
+ this.#groups.clear();
2474
+ }
2475
+ #locate(event) {
2476
+ if (event.kind === "message") return {
2477
+ key: messageKeyOf(event),
2478
+ ttl: MESSAGE_TTL_MS
2479
+ };
2480
+ if (event.kind === "notice") return {
2481
+ key: noticeKeyOf(event),
2482
+ ttl: EVENT_TTL_MS
2483
+ };
2484
+ if (event.kind === "request") return {
2485
+ key: requestKeyOf(event),
2486
+ ttl: EVENT_TTL_MS
2487
+ };
2488
+ return null;
2489
+ }
2490
+ #prune(now) {
2491
+ for (const [key, group] of this.#groups) if (group.expiresAt < now) this.#groups.delete(key);
2492
+ if (this.#groups.size <= this.#maxSize) return;
2493
+ const drop = this.#groups.size - this.#maxSize;
2494
+ let dropped = 0;
2495
+ for (const key of this.#groups.keys()) {
2496
+ this.#groups.delete(key);
2497
+ this.#evicted++;
2498
+ if (++dropped >= drop) break;
2499
+ }
2500
+ }
2501
+ };
2502
+
2262
2503
  //#endregion
2263
2504
  //#region src/runtime/plugin-metadata.ts
2264
2505
  const store$1 = getOrCreate("plugin-metadata", () => new Map());
@@ -4843,7 +5084,7 @@ var MiokuRuntime = class {
4843
5084
  #enabledPlugins = new Map();
4844
5085
  #driverFactory;
4845
5086
  #builtinPlugins;
4846
- #dedupEnabled;
5087
+ #correlator;
4847
5088
  #started = false;
4848
5089
  #stopped = false;
4849
5090
  constructor(options) {
@@ -4851,7 +5092,7 @@ var MiokuRuntime = class {
4851
5092
  this.#logger = options.logger;
4852
5093
  this.#driverFactory = options.driverFactory ?? (() => createDefaultDriver());
4853
5094
  this.#builtinPlugins = options.builtinPlugins ?? BUILTIN_PLUGINS$1;
4854
- this.#dedupEnabled = options.dedup?.crossAdapter !== false;
5095
+ this.#correlator = options.dedup?.crossAdapter === false ? void 0 : new EventCorrelator({ logger: this.#logger.child({ scope: "dedup" }) });
4855
5096
  this.#driver = this.#driverFactory();
4856
5097
  this.#bus = new EventBus();
4857
5098
  this.#bus.setLogger((level, message, detail) => {
@@ -4874,6 +5115,10 @@ var MiokuRuntime = class {
4874
5115
  get bus() {
4875
5116
  return this.#bus;
4876
5117
  }
5118
+ /** 事件关联器(跨适配器去重);为 undefined 时表示已禁用 */
5119
+ get correlator() {
5120
+ return this.#correlator;
5121
+ }
4877
5122
  get bots() {
4878
5123
  return this.#bots.all();
4879
5124
  }
@@ -4981,7 +5226,8 @@ var MiokuRuntime = class {
4981
5226
  driver: this.#driver,
4982
5227
  capabilities: this.#capabilities,
4983
5228
  logger: this.#logger.child({ adapter: state.definition.name }),
4984
- emit: (event) => this.#emitLifecycle(event)
5229
+ emit: (event) => this.#emitLifecycle(event),
5230
+ correlator: this.#correlator
4985
5231
  });
4986
5232
  }
4987
5233
  async #loadPlugin(plugin, type) {
@@ -4995,7 +5241,7 @@ var MiokuRuntime = class {
4995
5241
  config: botConfig,
4996
5242
  logger: this.#logger.child({ plugin: plugin.name }),
4997
5243
  priority: plugin.priority ?? 100,
4998
- dedup: this.#dedupEnabled,
5244
+ correlator: this.#correlator,
4999
5245
  getAdapter: (name) => this.getAdapter(name),
5000
5246
  listAdapters: () => this.adapters,
5001
5247
  onUpdateConfig: async (updater) => {
@@ -5284,6 +5530,7 @@ var MiokuRuntime = class {
5284
5530
  this.#enabledPlugins.clear();
5285
5531
  this.#capabilities.clear();
5286
5532
  this.#bots.clear();
5533
+ this.#correlator?.clear();
5287
5534
  resetPluginMetadata();
5288
5535
  try {
5289
5536
  await this.#driver.shutdown();
@@ -5446,6 +5693,7 @@ async function start(options = {}) {
5446
5693
  };
5447
5694
  process.on("SIGINT", () => void shutdown("SIGINT"));
5448
5695
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
5696
+ installProcessErrorGuards();
5449
5697
  return { stop: (reason) => stopRuntime(reason) };
5450
5698
  }
5451
5699
  function readVersion() {
@@ -5458,6 +5706,15 @@ function readVersion() {
5458
5706
  }
5459
5707
  }
5460
5708
  const version$1 = readVersion();
5709
+ function installProcessErrorGuards() {
5710
+ if (process.listenerCount("unhandledRejection") === 0) process.on("unhandledRejection", (reason) => {
5711
+ const detail = reason instanceof Error ? reason.stack ?? reason.message : reason;
5712
+ rootLogger.error(`[unhandledRejection] ${String(detail)}`);
5713
+ });
5714
+ if (process.listenerCount("uncaughtException") === 0) process.on("uncaughtException", (err) => {
5715
+ rootLogger.error(`[uncaughtException] ${err.stack ?? err.message ?? String(err)}`);
5716
+ });
5717
+ }
5461
5718
 
5462
5719
  //#endregion
5463
5720
  exports.AI_GEMINI_THINKING_LEVELS = AI_GEMINI_THINKING_LEVELS;
@@ -5473,6 +5730,7 @@ exports.CapabilityRegistry = CapabilityRegistry;
5473
5730
  exports.ChromeUA = ChromeUA;
5474
5731
  exports.DriverShutdownError = DriverShutdownError;
5475
5732
  exports.EventBus = EventBus;
5733
+ exports.EventCorrelator = EventCorrelator;
5476
5734
  exports.HttpRequestError = HttpRequestError;
5477
5735
  exports.MessageSegmentImpl = MessageSegmentImpl;
5478
5736
  exports.MiokuContext = MiokuContext;