@yunzai-ng/core 0.1.0
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/LICENSE +661 -0
- package/README.md +51 -0
- package/dist/adapter/accounts.d.ts +174 -0
- package/dist/adapter/accounts.d.ts.map +1 -0
- package/dist/adapter/accounts.js +653 -0
- package/dist/adapter/accounts.js.map +1 -0
- package/dist/adapter/bots.d.ts +142 -0
- package/dist/adapter/bots.d.ts.map +1 -0
- package/dist/adapter/bots.js +454 -0
- package/dist/adapter/bots.js.map +1 -0
- package/dist/adapter/host.d.ts +114 -0
- package/dist/adapter/host.d.ts.map +1 -0
- package/dist/adapter/host.js +90 -0
- package/dist/adapter/host.js.map +1 -0
- package/dist/adapter/login.d.ts +174 -0
- package/dist/adapter/login.d.ts.map +1 -0
- package/dist/adapter/login.js +345 -0
- package/dist/adapter/login.js.map +1 -0
- package/dist/adapter/registry.d.ts +102 -0
- package/dist/adapter/registry.d.ts.map +1 -0
- package/dist/adapter/registry.js +150 -0
- package/dist/adapter/registry.js.map +1 -0
- package/dist/config/core-config.d.ts +151 -0
- package/dist/config/core-config.d.ts.map +1 -0
- package/dist/config/core-config.js +338 -0
- package/dist/config/core-config.js.map +1 -0
- package/dist/config/schema.d.ts +449 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +871 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/store.d.ts +184 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +428 -0
- package/dist/config/store.js.map +1 -0
- package/dist/config/yaml.d.ts +23 -0
- package/dist/config/yaml.d.ts.map +1 -0
- package/dist/config/yaml.js +108 -0
- package/dist/config/yaml.js.map +1 -0
- package/dist/http/client.d.ts +82 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +658 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/app.d.ts +244 -0
- package/dist/kernel/app.d.ts.map +1 -0
- package/dist/kernel/app.js +654 -0
- package/dist/kernel/app.js.map +1 -0
- package/dist/kernel/kv-sink.d.ts +34 -0
- package/dist/kernel/kv-sink.d.ts.map +1 -0
- package/dist/kernel/kv-sink.js +25 -0
- package/dist/kernel/kv-sink.js.map +1 -0
- package/dist/kernel/policy.d.ts +77 -0
- package/dist/kernel/policy.d.ts.map +1 -0
- package/dist/kernel/policy.js +99 -0
- package/dist/kernel/policy.js.map +1 -0
- package/dist/kernel/runtime.d.ts +117 -0
- package/dist/kernel/runtime.d.ts.map +1 -0
- package/dist/kernel/runtime.js +197 -0
- package/dist/kernel/runtime.js.map +1 -0
- package/dist/kernel/sql-sink.d.ts +36 -0
- package/dist/kernel/sql-sink.d.ts.map +1 -0
- package/dist/kernel/sql-sink.js +114 -0
- package/dist/kernel/sql-sink.js.map +1 -0
- package/dist/logger/format.d.ts +67 -0
- package/dist/logger/format.d.ts.map +1 -0
- package/dist/logger/format.js +218 -0
- package/dist/logger/format.js.map +1 -0
- package/dist/logger/index.d.ts +97 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +363 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/rotate.d.ts +47 -0
- package/dist/logger/rotate.d.ts.map +1 -0
- package/dist/logger/rotate.js +242 -0
- package/dist/logger/rotate.js.map +1 -0
- package/dist/message/segment.d.ts +255 -0
- package/dist/message/segment.d.ts.map +1 -0
- package/dist/message/segment.js +510 -0
- package/dist/message/segment.js.map +1 -0
- package/dist/message/split.d.ts +34 -0
- package/dist/message/split.d.ts.map +1 -0
- package/dist/message/split.js +79 -0
- package/dist/message/split.js.map +1 -0
- package/dist/message/target.d.ts +29 -0
- package/dist/message/target.d.ts.map +1 -0
- package/dist/message/target.js +34 -0
- package/dist/message/target.js.map +1 -0
- package/dist/pipeline/cooldown.d.ts +84 -0
- package/dist/pipeline/cooldown.d.ts.map +1 -0
- package/dist/pipeline/cooldown.js +94 -0
- package/dist/pipeline/cooldown.js.map +1 -0
- package/dist/pipeline/dispatch.d.ts +98 -0
- package/dist/pipeline/dispatch.d.ts.map +1 -0
- package/dist/pipeline/dispatch.js +323 -0
- package/dist/pipeline/dispatch.js.map +1 -0
- package/dist/pipeline/event.d.ts +130 -0
- package/dist/pipeline/event.d.ts.map +1 -0
- package/dist/pipeline/event.js +538 -0
- package/dist/pipeline/event.js.map +1 -0
- package/dist/pipeline/middleware.d.ts +63 -0
- package/dist/pipeline/middleware.d.ts.map +1 -0
- package/dist/pipeline/middleware.js +174 -0
- package/dist/pipeline/middleware.js.map +1 -0
- package/dist/pipeline/prompt.d.ts +80 -0
- package/dist/pipeline/prompt.d.ts.map +1 -0
- package/dist/pipeline/prompt.js +155 -0
- package/dist/pipeline/prompt.js.map +1 -0
- package/dist/pipeline/router.d.ts +98 -0
- package/dist/pipeline/router.d.ts.map +1 -0
- package/dist/pipeline/router.js +489 -0
- package/dist/pipeline/router.js.map +1 -0
- package/dist/platform/detect.d.ts +24 -0
- package/dist/platform/detect.d.ts.map +1 -0
- package/dist/platform/detect.js +178 -0
- package/dist/platform/detect.js.map +1 -0
- package/dist/platform/paths.d.ts +38 -0
- package/dist/platform/paths.d.ts.map +1 -0
- package/dist/platform/paths.js +129 -0
- package/dist/platform/paths.js.map +1 -0
- package/dist/platform/system.d.ts +71 -0
- package/dist/platform/system.d.ts.map +1 -0
- package/dist/platform/system.js +242 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/plugin/context.d.ts +92 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +573 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +92 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +82 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/discover.d.ts +106 -0
- package/dist/plugin/discover.d.ts.map +1 -0
- package/dist/plugin/discover.js +254 -0
- package/dist/plugin/discover.js.map +1 -0
- package/dist/plugin/events.d.ts +109 -0
- package/dist/plugin/events.d.ts.map +1 -0
- package/dist/plugin/events.js +209 -0
- package/dist/plugin/events.js.map +1 -0
- package/dist/plugin/hooks.d.ts +266 -0
- package/dist/plugin/hooks.d.ts.map +1 -0
- package/dist/plugin/hooks.js +84 -0
- package/dist/plugin/hooks.js.map +1 -0
- package/dist/plugin/host.d.ts +160 -0
- package/dist/plugin/host.d.ts.map +1 -0
- package/dist/plugin/host.js +546 -0
- package/dist/plugin/host.js.map +1 -0
- package/dist/plugin/market.d.ts +264 -0
- package/dist/plugin/market.d.ts.map +1 -0
- package/dist/plugin/market.js +700 -0
- package/dist/plugin/market.js.map +1 -0
- package/dist/plugin/services.d.ts +116 -0
- package/dist/plugin/services.d.ts.map +1 -0
- package/dist/plugin/services.js +213 -0
- package/dist/plugin/services.js.map +1 -0
- package/dist/plugin/tar.d.ts +65 -0
- package/dist/plugin/tar.d.ts.map +1 -0
- package/dist/plugin/tar.js +277 -0
- package/dist/plugin/tar.js.map +1 -0
- package/dist/render/registry.d.ts +127 -0
- package/dist/render/registry.d.ts.map +1 -0
- package/dist/render/registry.js +306 -0
- package/dist/render/registry.js.map +1 -0
- package/dist/scheduler/index.d.ts +67 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +279 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/server/api.d.ts +179 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +602 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/auth.d.ts +83 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +178 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/browse.d.ts +60 -0
- package/dist/server/browse.d.ts.map +1 -0
- package/dist/server/browse.js +187 -0
- package/dist/server/browse.js.map +1 -0
- package/dist/server/files.d.ts +27 -0
- package/dist/server/files.d.ts.map +1 -0
- package/dist/server/files.js +76 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/index.d.ts +189 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1123 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/table.d.ts +105 -0
- package/dist/server/table.d.ts.map +1 -0
- package/dist/server/table.js +289 -0
- package/dist/server/table.js.map +1 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +120 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/json.d.ts +73 -0
- package/dist/store/json.d.ts.map +1 -0
- package/dist/store/json.js +174 -0
- package/dist/store/json.js.map +1 -0
- package/dist/store/kv.d.ts +126 -0
- package/dist/store/kv.d.ts.map +1 -0
- package/dist/store/kv.js +232 -0
- package/dist/store/kv.js.map +1 -0
- package/dist/store/level.d.ts +103 -0
- package/dist/store/level.d.ts.map +1 -0
- package/dist/store/level.js +119 -0
- package/dist/store/level.js.map +1 -0
- package/dist/store/memory.d.ts +61 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +94 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sql.d.ts +142 -0
- package/dist/store/sql.d.ts.map +1 -0
- package/dist/store/sql.js +246 -0
- package/dist/store/sql.js.map +1 -0
- package/dist/testing/fake.d.ts +109 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +196 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-adapter.d.ts +205 -0
- package/dist/testing/mock-adapter.d.ts.map +1 -0
- package/dist/testing/mock-adapter.js +309 -0
- package/dist/testing/mock-adapter.js.map +1 -0
- package/dist/util/deep.d.ts +108 -0
- package/dist/util/deep.d.ts.map +1 -0
- package/dist/util/deep.js +275 -0
- package/dist/util/deep.js.map +1 -0
- package/dist/util/defer.d.ts +107 -0
- package/dist/util/defer.d.ts.map +1 -0
- package/dist/util/defer.js +163 -0
- package/dist/util/defer.js.map +1 -0
- package/dist/util/dispose.d.ts +88 -0
- package/dist/util/dispose.d.ts.map +1 -0
- package/dist/util/dispose.js +143 -0
- package/dist/util/dispose.js.map +1 -0
- package/dist/util/duration.d.ts +28 -0
- package/dist/util/duration.d.ts.map +1 -0
- package/dist/util/duration.js +77 -0
- package/dist/util/duration.js.map +1 -0
- package/dist/util/fs.d.ts +110 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +269 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/id.d.ts +49 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +87 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/lru.d.ts +74 -0
- package/dist/util/lru.d.ts.map +1 -0
- package/dist/util/lru.js +156 -0
- package/dist/util/lru.js.map +1 -0
- package/dist/util/queue.d.ts +64 -0
- package/dist/util/queue.d.ts.map +1 -0
- package/dist/util/queue.js +147 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/text.d.ts +86 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +182 -0
- package/dist/util/text.js.map +1 -0
- package/package.json +44 -0
- package/src/adapter/accounts.ts +758 -0
- package/src/adapter/bots.ts +582 -0
- package/src/adapter/host.ts +224 -0
- package/src/adapter/login.ts +481 -0
- package/src/adapter/registry.ts +202 -0
- package/src/config/core-config.test.ts +60 -0
- package/src/config/core-config.ts +401 -0
- package/src/config/schema.test.ts +179 -0
- package/src/config/schema.ts +1036 -0
- package/src/config/store.test.ts +245 -0
- package/src/config/store.ts +506 -0
- package/src/config/yaml.ts +119 -0
- package/src/http/client.test.ts +568 -0
- package/src/http/client.ts +777 -0
- package/src/index.ts +106 -0
- package/src/kernel/app.test.ts +267 -0
- package/src/kernel/app.ts +843 -0
- package/src/kernel/kv-sink.ts +58 -0
- package/src/kernel/policy.ts +120 -0
- package/src/kernel/runtime.test.ts +542 -0
- package/src/kernel/runtime.ts +307 -0
- package/src/kernel/sql-sink.ts +154 -0
- package/src/logger/format.ts +259 -0
- package/src/logger/index.ts +427 -0
- package/src/logger/rotate.ts +265 -0
- package/src/message/segment.test.ts +335 -0
- package/src/message/segment.ts +569 -0
- package/src/message/split.ts +97 -0
- package/src/message/target.ts +48 -0
- package/src/pipeline/cooldown.ts +136 -0
- package/src/pipeline/dispatch.ts +419 -0
- package/src/pipeline/event.ts +735 -0
- package/src/pipeline/middleware.test.ts +426 -0
- package/src/pipeline/middleware.ts +199 -0
- package/src/pipeline/prompt.ts +212 -0
- package/src/pipeline/router.test.ts +432 -0
- package/src/pipeline/router.ts +575 -0
- package/src/platform/detect.ts +178 -0
- package/src/platform/paths.ts +147 -0
- package/src/platform/system.test.ts +102 -0
- package/src/platform/system.ts +289 -0
- package/src/plugin/context.test.ts +459 -0
- package/src/plugin/context.ts +757 -0
- package/src/plugin/define.test.ts +120 -0
- package/src/plugin/define.ts +142 -0
- package/src/plugin/discover.test.ts +253 -0
- package/src/plugin/discover.ts +353 -0
- package/src/plugin/events.test.ts +162 -0
- package/src/plugin/events.ts +266 -0
- package/src/plugin/hooks.ts +373 -0
- package/src/plugin/host.test.ts +696 -0
- package/src/plugin/host.ts +738 -0
- package/src/plugin/market.test.ts +781 -0
- package/src/plugin/market.ts +870 -0
- package/src/plugin/services.test.ts +135 -0
- package/src/plugin/services.ts +261 -0
- package/src/plugin/tar.test.ts +158 -0
- package/src/plugin/tar.ts +309 -0
- package/src/render/registry.test.ts +127 -0
- package/src/render/registry.ts +439 -0
- package/src/scheduler/index.ts +357 -0
- package/src/server/api.test.ts +802 -0
- package/src/server/api.ts +869 -0
- package/src/server/auth.ts +201 -0
- package/src/server/browse.test.ts +193 -0
- package/src/server/browse.ts +240 -0
- package/src/server/files.ts +97 -0
- package/src/server/index.test.ts +566 -0
- package/src/server/index.ts +1386 -0
- package/src/server/table.ts +351 -0
- package/src/store/index.ts +156 -0
- package/src/store/json.ts +198 -0
- package/src/store/kv.test.ts +307 -0
- package/src/store/kv.ts +256 -0
- package/src/store/level.ts +167 -0
- package/src/store/memory.ts +109 -0
- package/src/store/sql.test.ts +204 -0
- package/src/store/sql.ts +324 -0
- package/src/testing/fake.ts +297 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +588 -0
- package/src/util/deep.ts +272 -0
- package/src/util/defer.ts +211 -0
- package/src/util/dispose.ts +169 -0
- package/src/util/duration.ts +82 -0
- package/src/util/fs.ts +279 -0
- package/src/util/id.ts +94 -0
- package/src/util/lru.ts +185 -0
- package/src/util/queue.ts +167 -0
- package/src/util/text.ts +189 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:发现磁盘上的插件、解析入口、规划加载顺序
|
|
3
|
+
* 依赖方向:依赖 util/fs、类型包;不 import 任何插件
|
|
4
|
+
* 生命周期:每次全量加载/热重载时调用
|
|
5
|
+
* 注意事项:**顺序规划与文件发现分离**是这里的关键:先把候选文件找出来 → 全部 import
|
|
6
|
+
* 拿到声明式元信息 → 用依赖图算出确定顺序 → 再按序 setup。加载顺序由声明的
|
|
7
|
+
* 依赖决定,而非文件名字典序。
|
|
8
|
+
*
|
|
9
|
+
* 依赖缺失**不连坐内核**:只把依赖它的插件标记为跳过,其余照常工作。
|
|
10
|
+
*/
|
|
11
|
+
import { basename, isAbsolute, join, resolve } from "node:path"
|
|
12
|
+
import type { Logger } from "@yunzai-ng/types"
|
|
13
|
+
import { isDirectory, isFile, listDirs, listFiles, readJson } from "../util/fs.js"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 本进程是否运行在 TypeScript 源码模式
|
|
17
|
+
*
|
|
18
|
+
* 通过"本模块自身是否 .ts"判断:vitest / tsx 下为真,编译产物下为假。
|
|
19
|
+
* 为真时才把 `src/index.ts` 当作合法入口,否则一个没编译的插件会以
|
|
20
|
+
* "Unknown file extension .ts" 这种毫无指向性的错误失败。
|
|
21
|
+
*/
|
|
22
|
+
const TS_RUNTIME = import.meta.url.endsWith(".ts")
|
|
23
|
+
|
|
24
|
+
/** 依次尝试的入口文件(相对插件目录) */
|
|
25
|
+
const ENTRY_CANDIDATES = ["dist/index.js", "index.js", "index.mjs", "lib/index.js"]
|
|
26
|
+
|
|
27
|
+
/** 单文件插件允许的扩展名 */
|
|
28
|
+
const SINGLE_FILE_EXT = [".js", ".mjs"]
|
|
29
|
+
|
|
30
|
+
/** package.json 里与本框架相关的字段 */
|
|
31
|
+
interface PackageManifest {
|
|
32
|
+
/** npm 包名 */
|
|
33
|
+
name?: string
|
|
34
|
+
/** 版本 */
|
|
35
|
+
version?: string
|
|
36
|
+
/** 说明 */
|
|
37
|
+
description?: string
|
|
38
|
+
/** 作者,可能是字符串或对象 */
|
|
39
|
+
author?: string | { name?: string }
|
|
40
|
+
/** 主页 */
|
|
41
|
+
homepage?: string
|
|
42
|
+
/** 入口 */
|
|
43
|
+
main?: string
|
|
44
|
+
/** 模块入口 */
|
|
45
|
+
module?: string
|
|
46
|
+
/** exports 映射(只认 `"."`) */
|
|
47
|
+
exports?: unknown
|
|
48
|
+
/** 框架专属字段,用于不执行代码就能读到的信息 */
|
|
49
|
+
yunzai?: {
|
|
50
|
+
/** 覆盖插件名 */
|
|
51
|
+
name?: string
|
|
52
|
+
/** 是否禁用 */
|
|
53
|
+
disabled?: boolean
|
|
54
|
+
/** 入口文件 */
|
|
55
|
+
entry?: string
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 一个待加载的插件候选 */
|
|
60
|
+
export interface PluginCandidate {
|
|
61
|
+
/**
|
|
62
|
+
* 名字线索(目录名或包名)
|
|
63
|
+
*
|
|
64
|
+
* 真正的插件名以 `definePlugin({ name })` 为准;这个只用于导入失败时的日志,
|
|
65
|
+
* 因为那时候还读不到定义。
|
|
66
|
+
*/
|
|
67
|
+
id: string
|
|
68
|
+
/** 插件根目录绝对路径 */
|
|
69
|
+
dir: string
|
|
70
|
+
/** 入口文件绝对路径 */
|
|
71
|
+
entry: string
|
|
72
|
+
/** 是否为随发行版预置的插件 */
|
|
73
|
+
builtin: boolean
|
|
74
|
+
/** package.json 内容(若有) */
|
|
75
|
+
manifest?: PackageManifest
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 被跳过的插件及原因 */
|
|
79
|
+
export interface SkippedPlugin {
|
|
80
|
+
/** 插件名 */
|
|
81
|
+
name: string
|
|
82
|
+
/** 跳过原因(直接展示给用户,必须能照着解决) */
|
|
83
|
+
reason: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 发现插件的参数 */
|
|
87
|
+
export interface DiscoverOptions {
|
|
88
|
+
/** 要扫描的目录(绝对路径),按顺序扫描,先出现者优先 */
|
|
89
|
+
dirs: string[]
|
|
90
|
+
/** 其中属于"内置"的目录,用于给候选打 builtin 标记 */
|
|
91
|
+
builtinDirs?: string[]
|
|
92
|
+
/** 用户在配置里禁用的插件名 */
|
|
93
|
+
disabled?: readonly string[]
|
|
94
|
+
/** 日志器 */
|
|
95
|
+
logger: Logger
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 解析 package.json 的入口字段
|
|
100
|
+
*
|
|
101
|
+
* 只认 `exports["."]` 的字符串形式与 `import`/`default` 条件;再复杂的条件导出
|
|
102
|
+
* 交给 Node 自己解析(此时返回 undefined,由调用方回落到目录导入)。
|
|
103
|
+
* @param manifest package.json 内容
|
|
104
|
+
* @returns 相对路径;无法确定时 undefined
|
|
105
|
+
*/
|
|
106
|
+
function entryFromManifest(manifest: PackageManifest): string | undefined {
|
|
107
|
+
if (manifest.yunzai?.entry) return manifest.yunzai.entry
|
|
108
|
+
|
|
109
|
+
const exp = manifest.exports
|
|
110
|
+
if (typeof exp === "string") return exp
|
|
111
|
+
if (exp && typeof exp === "object") {
|
|
112
|
+
const dot = (exp as Record<string, unknown>)["."]
|
|
113
|
+
if (typeof dot === "string") return dot
|
|
114
|
+
if (dot && typeof dot === "object") {
|
|
115
|
+
const cond = dot as Record<string, unknown>
|
|
116
|
+
for (const key of ["import", "module", "default"]) {
|
|
117
|
+
const value = cond[key]
|
|
118
|
+
if (typeof value === "string") return value
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return manifest.module ?? manifest.main
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 在插件目录里找入口文件
|
|
128
|
+
* @param dir 插件目录
|
|
129
|
+
* @param manifest package.json 内容
|
|
130
|
+
* @returns 入口绝对路径;找不到时 undefined
|
|
131
|
+
*/
|
|
132
|
+
async function resolveEntry(dir: string, manifest?: PackageManifest): Promise<string | undefined> {
|
|
133
|
+
const declared = manifest ? entryFromManifest(manifest) : undefined
|
|
134
|
+
if (declared) {
|
|
135
|
+
const full = resolve(dir, declared)
|
|
136
|
+
if (await isFile(full)) return full
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const candidates = TS_RUNTIME ? [...ENTRY_CANDIDATES, "src/index.ts", "index.ts"] : ENTRY_CANDIDATES
|
|
140
|
+
for (const rel of candidates) {
|
|
141
|
+
const full = join(dir, rel)
|
|
142
|
+
if (await isFile(full)) return full
|
|
143
|
+
}
|
|
144
|
+
return undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 取插件名线索
|
|
149
|
+
*
|
|
150
|
+
* 优先 `yunzai.name`,其次去掉 scope 的包名,最后目录名。
|
|
151
|
+
* @param dir 插件目录
|
|
152
|
+
* @param manifest package.json 内容
|
|
153
|
+
* @returns 名字线索
|
|
154
|
+
*/
|
|
155
|
+
function idOf(dir: string, manifest?: PackageManifest): string {
|
|
156
|
+
const explicit = manifest?.yunzai?.name
|
|
157
|
+
if (explicit) return explicit
|
|
158
|
+
const pkg = manifest?.name
|
|
159
|
+
if (pkg) return pkg.startsWith("@") ? (pkg.split("/")[1] ?? pkg) : pkg
|
|
160
|
+
return basename(dir)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 扫描一个目录下的全部插件
|
|
165
|
+
* @param root 要扫描的目录
|
|
166
|
+
* @param builtin 是否标记为内置
|
|
167
|
+
* @param logger 日志器
|
|
168
|
+
* @returns 候选数组
|
|
169
|
+
*/
|
|
170
|
+
async function scanDir(root: string, builtin: boolean, logger: Logger): Promise<PluginCandidate[]> {
|
|
171
|
+
if (!(await isDirectory(root))) return []
|
|
172
|
+
const out: PluginCandidate[] = []
|
|
173
|
+
|
|
174
|
+
for (const name of await listDirs(root)) {
|
|
175
|
+
// `.` 开头是隐藏目录,`_` 开头约定为"作者临时禁用",node_modules 不用说
|
|
176
|
+
if (name.startsWith(".") || name.startsWith("_") || name === "node_modules") continue
|
|
177
|
+
|
|
178
|
+
const dir = join(root, name)
|
|
179
|
+
const manifest = await readJson<PackageManifest>(join(dir, "package.json"))
|
|
180
|
+
if (manifest?.yunzai?.disabled) {
|
|
181
|
+
logger.debug(`插件目录 ${name} 在 package.json 里自我标记为 disabled,跳过`)
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const entry = await resolveEntry(dir, manifest)
|
|
186
|
+
if (!entry) {
|
|
187
|
+
logger.warn(
|
|
188
|
+
`插件目录 ${name} 里找不到入口文件:请提供 ${ENTRY_CANDIDATES.join(" / ")} 之一,` +
|
|
189
|
+
`或在 package.json 的 main / yunzai.entry 里指明。若是 TypeScript 插件,请先编译`
|
|
190
|
+
)
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
out.push({ id: idOf(dir, manifest), dir, entry, builtin, manifest })
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 顶层单文件插件:plugins/我的小功能.js。给"就想加个几十行小功能"的用户留的口子,
|
|
198
|
+
// 不必为此建目录和 package.json
|
|
199
|
+
for (const ext of SINGLE_FILE_EXT) {
|
|
200
|
+
for (const file of await listFiles(root, ext)) {
|
|
201
|
+
const name = basename(file, ext)
|
|
202
|
+
if (name.startsWith(".") || name.startsWith("_")) continue
|
|
203
|
+
out.push({ id: name, dir: root, entry: join(root, file), builtin })
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return out
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 发现全部插件
|
|
212
|
+
*
|
|
213
|
+
* 同名插件只保留先扫到的那份(目录顺序即优先级),并记一条警告 ——
|
|
214
|
+
* 用户插件目录排在预置目录之前,因此复制一份预置插件加以修改即可覆盖原插件。
|
|
215
|
+
* @param opts 参数
|
|
216
|
+
* @returns 候选数组
|
|
217
|
+
*/
|
|
218
|
+
export async function discoverPlugins(opts: DiscoverOptions): Promise<PluginCandidate[]> {
|
|
219
|
+
const builtin = new Set((opts.builtinDirs ?? []).map(dir => resolve(dir)))
|
|
220
|
+
const disabled = new Set(opts.disabled ?? [])
|
|
221
|
+
const seen = new Map<string, PluginCandidate>()
|
|
222
|
+
|
|
223
|
+
for (const raw of opts.dirs) {
|
|
224
|
+
if (!isAbsolute(raw)) {
|
|
225
|
+
opts.logger.warn(`插件目录 ${raw} 不是绝对路径,已忽略(相对路径会随启动方式变化)`)
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
const dir = resolve(raw)
|
|
229
|
+
for (const candidate of await scanDir(dir, builtin.has(dir), opts.logger)) {
|
|
230
|
+
if (disabled.has(candidate.id)) {
|
|
231
|
+
opts.logger.info(`插件 ${candidate.id} 已被用户禁用,跳过`)
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
const exists = seen.get(candidate.id)
|
|
235
|
+
if (exists) {
|
|
236
|
+
opts.logger.warn(`发现同名插件 ${candidate.id}:使用 ${exists.dir},忽略 ${candidate.dir}`)
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
seen.set(candidate.id, candidate)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return [...seen.values()]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 参与排序所需的最小信息 */
|
|
247
|
+
export interface OrderableUnit {
|
|
248
|
+
/** 插件名 */
|
|
249
|
+
name: string
|
|
250
|
+
/** 依赖的插件名 */
|
|
251
|
+
dependencies?: string[]
|
|
252
|
+
/** 优先级,小者先加载 */
|
|
253
|
+
priority?: number
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** 排序结果 */
|
|
257
|
+
export interface LoadOrder<T extends OrderableUnit> {
|
|
258
|
+
/** 可加载的插件,已按依赖与优先级排好 */
|
|
259
|
+
order: T[]
|
|
260
|
+
/** 被跳过的插件 */
|
|
261
|
+
skipped: SkippedPlugin[]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 默认加载优先级 */
|
|
265
|
+
const DEFAULT_PRIORITY = 100
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 规划加载顺序
|
|
269
|
+
*
|
|
270
|
+
* 三步:去重 → 剔除依赖不可满足者(级联)→ 拓扑排序。
|
|
271
|
+
*
|
|
272
|
+
* 排序在同层节点之间以 `(priority, name)` 作**确定性**排序:
|
|
273
|
+
* 同一份插件集合在任何机器上都得到相同顺序,否则"甲机器正常、乙机器异常"
|
|
274
|
+
* 这类问题会耗费大量排障时间。
|
|
275
|
+
* @param units 待排序的插件定义
|
|
276
|
+
* @returns 顺序与跳过列表
|
|
277
|
+
*/
|
|
278
|
+
export function resolveLoadOrder<T extends OrderableUnit>(units: readonly T[]): LoadOrder<T> {
|
|
279
|
+
const skipped: SkippedPlugin[] = []
|
|
280
|
+
const byName = new Map<string, T>()
|
|
281
|
+
|
|
282
|
+
for (const unit of units) {
|
|
283
|
+
if (byName.has(unit.name)) {
|
|
284
|
+
skipped.push({ name: unit.name, reason: `插件名 ${unit.name} 重复,只加载第一个` })
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
byName.set(unit.name, unit)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 级联剔除:A 依赖 B,B 因缺依赖被剔除,A 也留不住
|
|
291
|
+
let changed = true
|
|
292
|
+
while (changed) {
|
|
293
|
+
changed = false
|
|
294
|
+
for (const [name, unit] of byName) {
|
|
295
|
+
const missing = (unit.dependencies ?? []).filter(dep => !byName.has(dep))
|
|
296
|
+
if (missing.length === 0) continue
|
|
297
|
+
byName.delete(name)
|
|
298
|
+
skipped.push({
|
|
299
|
+
name,
|
|
300
|
+
reason: `依赖的插件未安装或已被跳过:${missing.join("、")}。装上它们或删掉本插件的依赖声明`
|
|
301
|
+
})
|
|
302
|
+
changed = true
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** 同层排序:优先级小者先,其次名字字典序 */
|
|
307
|
+
const compare = (a: T, b: T): number => {
|
|
308
|
+
const pa = a.priority ?? DEFAULT_PRIORITY
|
|
309
|
+
const pb = b.priority ?? DEFAULT_PRIORITY
|
|
310
|
+
return pa !== pb ? pa - pb : a.name.localeCompare(b.name)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Kahn:indegree 为"还有几个依赖没就绪"
|
|
314
|
+
const indegree = new Map<string, number>()
|
|
315
|
+
const dependents = new Map<string, string[]>()
|
|
316
|
+
for (const [name, unit] of byName) {
|
|
317
|
+
const deps = (unit.dependencies ?? []).filter(dep => byName.has(dep))
|
|
318
|
+
indegree.set(name, deps.length)
|
|
319
|
+
for (const dep of deps) {
|
|
320
|
+
const list = dependents.get(dep)
|
|
321
|
+
if (list) list.push(name)
|
|
322
|
+
else dependents.set(dep, [name])
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const ready = [...byName.values()].filter(unit => indegree.get(unit.name) === 0).sort(compare)
|
|
327
|
+
const order: T[] = []
|
|
328
|
+
|
|
329
|
+
while (ready.length > 0) {
|
|
330
|
+
const unit = ready.shift()!
|
|
331
|
+
order.push(unit)
|
|
332
|
+
for (const name of dependents.get(unit.name) ?? []) {
|
|
333
|
+
const left = (indegree.get(name) ?? 0) - 1
|
|
334
|
+
indegree.set(name, left)
|
|
335
|
+
if (left !== 0) continue
|
|
336
|
+
const next = byName.get(name)
|
|
337
|
+
if (!next) continue
|
|
338
|
+
// 插进有序位置,保持"每次取出的都是当前可加载里最该先加载的那个"
|
|
339
|
+
const at = ready.findIndex(item => compare(item, next) > 0)
|
|
340
|
+
if (at < 0) ready.push(next)
|
|
341
|
+
else ready.splice(at, 0, next)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (order.length !== byName.size) {
|
|
346
|
+
const loop = [...byName.keys()].filter(name => (indegree.get(name) ?? 0) > 0).sort()
|
|
347
|
+
for (const name of loop) {
|
|
348
|
+
skipped.push({ name, reason: `插件依赖成环,无法确定加载顺序:${loop.join(" → ")}` })
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return { order, skipped }
|
|
353
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:`CoreEventBus` 行为测试
|
|
3
|
+
* 依赖方向:测试文件,依赖 testing/fake
|
|
4
|
+
* 生命周期:每个用例一条总线
|
|
5
|
+
* 注意事项:这里断言的是"不用 EventEmitter"换来的三件事 —— 错误隔离、能等异步、
|
|
6
|
+
* 能按插件批量摘除。其中**一个监听器抛错不能影响其他监听器**为重点。
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it, vi } from "vitest"
|
|
9
|
+
import { createEventBus } from "./events.js"
|
|
10
|
+
import { fakeLogger } from "../testing/fake.js"
|
|
11
|
+
|
|
12
|
+
describe("CoreEventBus", () => {
|
|
13
|
+
it("按注册顺序通知全部监听器", async () => {
|
|
14
|
+
const bus = createEventBus({ logger: fakeLogger() })
|
|
15
|
+
const seen: string[] = []
|
|
16
|
+
bus.on("app/ready", () => void seen.push("a"), "p1")
|
|
17
|
+
bus.on("app/ready", () => void seen.push("b"), "p2")
|
|
18
|
+
|
|
19
|
+
await bus.emit("app/ready")
|
|
20
|
+
expect(seen).toEqual(["a", "b"])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("一个监听器抛错不影响其余监听器,且 emit 自身不拒绝", async () => {
|
|
24
|
+
const logger = fakeLogger()
|
|
25
|
+
const bus = createEventBus({ logger })
|
|
26
|
+
const seen: string[] = []
|
|
27
|
+
|
|
28
|
+
bus.on("app/ready", () => {
|
|
29
|
+
throw new Error("插件写错了")
|
|
30
|
+
}, "bad-plugin")
|
|
31
|
+
bus.on("app/ready", () => void seen.push("still-ran"), "good-plugin")
|
|
32
|
+
|
|
33
|
+
await expect(bus.emit("app/ready")).resolves.toBeUndefined()
|
|
34
|
+
expect(seen).toEqual(["still-ran"])
|
|
35
|
+
expect(logger.lines.some(l => l.includes("bad-plugin") && l.includes("出错"))).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("异步监听器抛错同样被隔离", async () => {
|
|
39
|
+
const logger = fakeLogger()
|
|
40
|
+
const bus = createEventBus({ logger })
|
|
41
|
+
bus.on("app/stopping", async () => {
|
|
42
|
+
await Promise.resolve()
|
|
43
|
+
throw new Error("保存失败")
|
|
44
|
+
}, "p")
|
|
45
|
+
|
|
46
|
+
await expect(bus.emit("app/stopping")).resolves.toBeUndefined()
|
|
47
|
+
expect(logger.lines.some(l => l.includes("保存失败"))).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("emit 会等待异步监听器完成(停机时要靠这个)", async () => {
|
|
51
|
+
const bus = createEventBus({ logger: fakeLogger() })
|
|
52
|
+
let saved = false
|
|
53
|
+
bus.on("app/stopping", async () => {
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, 20))
|
|
55
|
+
saved = true
|
|
56
|
+
}, "p")
|
|
57
|
+
|
|
58
|
+
await bus.emit("app/stopping")
|
|
59
|
+
expect(saved).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("卡住的监听器会被放弃等待并记警告,而不是吊死停机流程", async () => {
|
|
63
|
+
const logger = fakeLogger()
|
|
64
|
+
const bus = createEventBus({ logger, handlerTimeout: 20 })
|
|
65
|
+
bus.on("app/stopping", () => new Promise<void>(() => undefined), "stuck-plugin")
|
|
66
|
+
|
|
67
|
+
await bus.emit("app/stopping")
|
|
68
|
+
expect(logger.lines.some(l => l.includes("stuck-plugin") && l.includes("放弃等待"))).toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("once 只触发一次,且抛错时同样被摘除", async () => {
|
|
72
|
+
const logger = fakeLogger()
|
|
73
|
+
const bus = createEventBus({ logger })
|
|
74
|
+
const fn = vi.fn(() => {
|
|
75
|
+
throw new Error("boom")
|
|
76
|
+
})
|
|
77
|
+
bus.once("app/ready", fn, "p")
|
|
78
|
+
|
|
79
|
+
await bus.emit("app/ready")
|
|
80
|
+
await bus.emit("app/ready")
|
|
81
|
+
expect(fn).toHaveBeenCalledTimes(1)
|
|
82
|
+
expect(bus.count("app/ready")).toBe(0)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("监听器在处理中订阅新监听器不会被本次 emit 触发", async () => {
|
|
86
|
+
const bus = createEventBus({ logger: fakeLogger() })
|
|
87
|
+
const seen: string[] = []
|
|
88
|
+
let added = false
|
|
89
|
+
bus.on("app/ready", () => {
|
|
90
|
+
seen.push("first")
|
|
91
|
+
if (added) return
|
|
92
|
+
added = true
|
|
93
|
+
bus.on("app/ready", () => void seen.push("added-during-emit"), "p")
|
|
94
|
+
}, "p")
|
|
95
|
+
|
|
96
|
+
// 本次 emit 用的是快照,新加的监听器要等下一次
|
|
97
|
+
await bus.emit("app/ready")
|
|
98
|
+
expect(seen).toEqual(["first"])
|
|
99
|
+
|
|
100
|
+
await bus.emit("app/ready")
|
|
101
|
+
expect(seen).toEqual(["first", "first", "added-during-emit"])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("disposer 摘除监听器;重复调用无副作用", async () => {
|
|
105
|
+
const bus = createEventBus({ logger: fakeLogger() })
|
|
106
|
+
const fn = vi.fn()
|
|
107
|
+
const undo = bus.on("message", fn, "p")
|
|
108
|
+
undo()
|
|
109
|
+
undo()
|
|
110
|
+
|
|
111
|
+
expect(bus.count("message")).toBe(0)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("removeByOwner 摘除某插件的全部监听(热重载兜底)", async () => {
|
|
115
|
+
const bus = createEventBus({ logger: fakeLogger() })
|
|
116
|
+
bus.on("app/ready", () => undefined, "p1")
|
|
117
|
+
bus.on("app/stopping", () => undefined, "p1")
|
|
118
|
+
bus.on("app/ready", () => undefined, "p2")
|
|
119
|
+
|
|
120
|
+
expect(bus.removeByOwner("p1")).toBe(2)
|
|
121
|
+
expect(bus.count("app/ready")).toBe(1)
|
|
122
|
+
expect(bus.count("app/stopping")).toBe(0)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it("emitDetached 不等待监听器,但错误照样记录", async () => {
|
|
126
|
+
const logger = fakeLogger()
|
|
127
|
+
const bus = createEventBus({ logger })
|
|
128
|
+
let finished = false
|
|
129
|
+
bus.on("app/ready", async () => {
|
|
130
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
131
|
+
finished = true
|
|
132
|
+
throw new Error("迟到的错误")
|
|
133
|
+
}, "p")
|
|
134
|
+
|
|
135
|
+
bus.emitDetached("app/ready")
|
|
136
|
+
expect(finished).toBe(false)
|
|
137
|
+
|
|
138
|
+
await new Promise(resolve => setTimeout(resolve, 30))
|
|
139
|
+
expect(finished).toBe(true)
|
|
140
|
+
expect(logger.lines.some(l => l.includes("迟到的错误"))).toBe(true)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("list 汇总监听器与触发次数", async () => {
|
|
144
|
+
const bus = createEventBus({ logger: fakeLogger() })
|
|
145
|
+
bus.on("app/ready", () => undefined, "p1")
|
|
146
|
+
await bus.emit("app/ready")
|
|
147
|
+
|
|
148
|
+
const list = bus.list()
|
|
149
|
+
expect(list).toHaveLength(1)
|
|
150
|
+
expect(list[0]).toMatchObject({ event: "app/ready", owner: "p1", calls: 1 })
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("close 之后不再派发事件", async () => {
|
|
154
|
+
const bus = createEventBus({ logger: fakeLogger() })
|
|
155
|
+
const fn = vi.fn()
|
|
156
|
+
bus.on("app/ready", fn, "p")
|
|
157
|
+
bus.close()
|
|
158
|
+
|
|
159
|
+
await bus.emit("app/ready")
|
|
160
|
+
expect(fn).not.toHaveBeenCalled()
|
|
161
|
+
})
|
|
162
|
+
})
|