@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,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:配置仓库(YAML 落盘、schema 校验、细粒度变更通知、外部改动热加载)
|
|
3
|
+
* 依赖方向:依赖 config/schema、config/yaml、util/{fs,deep}、类型包
|
|
4
|
+
* 生命周期:应用级单例;`dispose()` 关闭文件监听
|
|
5
|
+
* 注意事项:四条行为约定 ——
|
|
6
|
+
*
|
|
7
|
+
* **变更通知按叶子路径。** `diffPaths` 算出实际变化的路径,只通知关注它的订阅者,
|
|
8
|
+
* 而不是一有变动就全量重算。
|
|
9
|
+
*
|
|
10
|
+
* **写入用 `atomicWrite`**(临时文件 + rename):直接写会在断电或强杀进程时
|
|
11
|
+
* 留下一份截断的 YAML。
|
|
12
|
+
*
|
|
13
|
+
* **配置写坏了不崩。** 记录错误、退回上一份可用配置(首次加载则用缺省值),
|
|
14
|
+
* 并**绝不覆盖**使用者那份坏文件 —— 他要照着报错自己改。
|
|
15
|
+
*
|
|
16
|
+
* **新增配置项会补进既有文件。** 校验通过后按当前 schema 重写,新选项连同
|
|
17
|
+
* 中文注释一并补入,幂等。否则升级之后新选项在使用者的配置里根本看不见。
|
|
18
|
+
*/
|
|
19
|
+
import { watch, type FSWatcher } from "node:fs"
|
|
20
|
+
import { basename, join } from "node:path"
|
|
21
|
+
import type { ConfigChange, ConfigHandle, DeepPartial, DeepReadonly, Disposer, Logger, SchemaDescriptor } from "@yunzai-ng/types"
|
|
22
|
+
import { atomicWrite, ensureDir, readText } from "../util/fs.js"
|
|
23
|
+
import { deepClone, deepMerge, diffPaths, pathAffects, type PlainObject } from "../util/deep.js"
|
|
24
|
+
import { SchemaError, type Schema, type SchemaIssue } from "./schema.js"
|
|
25
|
+
import { parseYaml, serializeYaml } from "./yaml.js"
|
|
26
|
+
|
|
27
|
+
/** 配置名的合法形式:小写字母开头,可含数字、点、横线、下划线 */
|
|
28
|
+
const NAME_RE = /^[a-z][a-z0-9._-]*$/i
|
|
29
|
+
|
|
30
|
+
/** 外部改动的合并窗口(毫秒):编辑器保存常触发多次 fs 事件 */
|
|
31
|
+
const RELOAD_DEBOUNCE = 200
|
|
32
|
+
|
|
33
|
+
/** 配置仓库参数 */
|
|
34
|
+
export interface ConfigStoreOptions {
|
|
35
|
+
/** 配置目录(绝对路径) */
|
|
36
|
+
dir: string
|
|
37
|
+
/** 日志器 */
|
|
38
|
+
logger: Logger
|
|
39
|
+
/** 是否监听文件外部改动,缺省 true */
|
|
40
|
+
watch?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 声明配置时的附加信息 */
|
|
44
|
+
export interface DefineConfigOptions {
|
|
45
|
+
/** 文件头注释里显示的名字,缺省用配置名 */
|
|
46
|
+
title?: string
|
|
47
|
+
/** 额外的文件头说明行 */
|
|
48
|
+
notes?: string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 配置文件的对外快照信息(WebUI 列表用) */
|
|
52
|
+
export interface ConfigSummary {
|
|
53
|
+
/** 配置名 */
|
|
54
|
+
name: string
|
|
55
|
+
/** 文件绝对路径 */
|
|
56
|
+
file: string
|
|
57
|
+
/** 显示标题 */
|
|
58
|
+
title: string
|
|
59
|
+
/** 表单描述 */
|
|
60
|
+
schema: SchemaDescriptor
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 单个配置文件的句柄
|
|
65
|
+
*
|
|
66
|
+
* 由 `ConfigStore.define()` 创建,插件通过 `ctx.config` 拿到它。
|
|
67
|
+
*/
|
|
68
|
+
export class ConfigFile<T> implements ConfigHandle<T> {
|
|
69
|
+
/** 配置名 */
|
|
70
|
+
readonly name: string
|
|
71
|
+
/** 文件绝对路径 */
|
|
72
|
+
readonly file: string
|
|
73
|
+
/** 表单描述 */
|
|
74
|
+
readonly schema: SchemaDescriptor
|
|
75
|
+
/** 显示标题 */
|
|
76
|
+
readonly title: string
|
|
77
|
+
|
|
78
|
+
/** 校验用的 schema */
|
|
79
|
+
readonly #validator: Schema<T>
|
|
80
|
+
/** 日志器 */
|
|
81
|
+
readonly #logger: Logger
|
|
82
|
+
/** 文件头注释 */
|
|
83
|
+
readonly #header: string[]
|
|
84
|
+
/** 当前值 */
|
|
85
|
+
#value: T
|
|
86
|
+
/** 变更订阅者 */
|
|
87
|
+
readonly #subscribers = new Set<(change: ConfigChange<T>) => void>()
|
|
88
|
+
/** 按路径订阅者 */
|
|
89
|
+
readonly #watchers = new Set<{ path: string; cb: (change: ConfigChange<T>) => void }>()
|
|
90
|
+
/** 最近一次由自己写出的文本,用于识别"这次改动是我自己造成的" */
|
|
91
|
+
#lastWritten: string | undefined
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param params 构造参数
|
|
95
|
+
*/
|
|
96
|
+
constructor(params: {
|
|
97
|
+
/** 配置名 */
|
|
98
|
+
name: string
|
|
99
|
+
/** 文件路径 */
|
|
100
|
+
file: string
|
|
101
|
+
/** 校验 schema */
|
|
102
|
+
validator: Schema<T>
|
|
103
|
+
/** 初始值 */
|
|
104
|
+
value: T
|
|
105
|
+
/** 日志器 */
|
|
106
|
+
logger: Logger
|
|
107
|
+
/** 显示标题 */
|
|
108
|
+
title: string
|
|
109
|
+
/** 文件头注释 */
|
|
110
|
+
header: string[]
|
|
111
|
+
}) {
|
|
112
|
+
this.name = params.name
|
|
113
|
+
this.file = params.file
|
|
114
|
+
this.title = params.title
|
|
115
|
+
this.#validator = params.validator
|
|
116
|
+
this.schema = params.validator.describe()
|
|
117
|
+
this.#value = params.value
|
|
118
|
+
this.#logger = params.logger
|
|
119
|
+
this.#header = params.header
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 取当前配置快照
|
|
124
|
+
* @returns 只读快照;未发生变更时多次调用返回同一对象
|
|
125
|
+
*/
|
|
126
|
+
get(): DeepReadonly<T> {
|
|
127
|
+
return this.#value as unknown as DeepReadonly<T>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 局部更新并落盘
|
|
132
|
+
* @param patch 深合并的补丁
|
|
133
|
+
* @param source 变更来源,WebUI 保存时传 `"webui"`,缺省 `"api"`
|
|
134
|
+
* @returns 更新后的快照
|
|
135
|
+
* @throws SchemaError 校验失败,此时原配置不变、文件不变
|
|
136
|
+
*/
|
|
137
|
+
async patch(patch: DeepPartial<T>, source: ConfigChange<T>["source"] = "api"): Promise<DeepReadonly<T>> {
|
|
138
|
+
const merged = deepMerge(this.#value as unknown as PlainObject, patch as unknown as PlainObject)
|
|
139
|
+
return this.#commit(merged, source)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 整体替换并落盘
|
|
144
|
+
* @param value 完整配置
|
|
145
|
+
* @param source 变更来源,缺省 `"api"`
|
|
146
|
+
* @returns 更新后的快照
|
|
147
|
+
* @throws SchemaError 校验失败
|
|
148
|
+
*/
|
|
149
|
+
async replace(value: T, source: ConfigChange<T>["source"] = "api"): Promise<DeepReadonly<T>> {
|
|
150
|
+
return this.#commit(value, source)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 恢复默认值并落盘
|
|
155
|
+
* @param source 变更来源,缺省 `"api"`
|
|
156
|
+
* @returns 更新后的快照
|
|
157
|
+
*/
|
|
158
|
+
async reset(source: ConfigChange<T>["source"] = "api"): Promise<DeepReadonly<T>> {
|
|
159
|
+
return this.#commit(this.#validator.defaults(), source)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 订阅变更
|
|
164
|
+
* @param cb 回调;抛错只记日志,不影响其他订阅者
|
|
165
|
+
* @returns 取消订阅
|
|
166
|
+
*/
|
|
167
|
+
onChange(cb: (change: ConfigChange<T>) => void): Disposer {
|
|
168
|
+
this.#subscribers.add(cb)
|
|
169
|
+
return () => void this.#subscribers.delete(cb)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 只订阅某个路径(及其子路径)的变更
|
|
174
|
+
*
|
|
175
|
+
* 这是"细粒度失效"的入口:渲染器仅关注 `render.*`,即不会被 `bot.*` 的
|
|
176
|
+
* 变动通知。父子路径双向匹配 —— 修改 `bot` 会通知订阅 `bot.masterQQ` 的一方,
|
|
177
|
+
* 反之亦然。
|
|
178
|
+
* @param path 点分路径,空串等价于 `onChange`
|
|
179
|
+
* @param cb 回调
|
|
180
|
+
* @returns 取消订阅
|
|
181
|
+
*/
|
|
182
|
+
watch(path: string, cb: (change: ConfigChange<T>) => void): Disposer {
|
|
183
|
+
const entry = { path, cb }
|
|
184
|
+
this.#watchers.add(entry)
|
|
185
|
+
return () => void this.#watchers.delete(entry)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 从磁盘加载
|
|
190
|
+
*
|
|
191
|
+
* 出错时保留当前值并返回问题列表,**不会**抛错也**不会**覆盖磁盘文件。
|
|
192
|
+
* @param source 变更来源,用于变更事件
|
|
193
|
+
* @param createIfMissing 文件不存在时是否写出默认配置
|
|
194
|
+
* @returns 校验问题列表(可能只含 warn)
|
|
195
|
+
*/
|
|
196
|
+
async load(source: ConfigChange<T>["source"], createIfMissing: boolean): Promise<SchemaIssue[]> {
|
|
197
|
+
const text = await readText(this.file)
|
|
198
|
+
|
|
199
|
+
if (text === undefined) {
|
|
200
|
+
const value = this.#validator.defaults()
|
|
201
|
+
this.#value = value
|
|
202
|
+
if (createIfMissing) await this.#write(value)
|
|
203
|
+
return []
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 自己刚写出去的内容,跳过重复解析
|
|
207
|
+
if (text === this.#lastWritten) return []
|
|
208
|
+
|
|
209
|
+
let raw: unknown
|
|
210
|
+
try {
|
|
211
|
+
raw = parseYaml(text)
|
|
212
|
+
} catch (err) {
|
|
213
|
+
this.#logger.error(`配置 ${this.name} 的 YAML 语法有误,已沿用上一份可用配置`, err)
|
|
214
|
+
return [{ path: "", message: err instanceof Error ? err.message : String(err), severity: "error" }]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const result = this.#validator.safeParse(raw)
|
|
218
|
+
if (!result.ok) {
|
|
219
|
+
this.#logger.error(`配置 ${this.name} 校验失败,已沿用上一份可用配置:\n${formatIssues(result.issues)}`)
|
|
220
|
+
return result.issues
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const issue of result.issues) {
|
|
224
|
+
this.#logger.warn(`配置 ${this.name} 的 ${issue.path}:${issue.message}`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const prev = this.#value
|
|
228
|
+
this.#value = result.value
|
|
229
|
+
|
|
230
|
+
// 按当前 schema 回写:补上新增选项与注释。序列化是确定性的,故此操作幂等。
|
|
231
|
+
const canonical = this.#render(result.value)
|
|
232
|
+
if (canonical !== text) await this.#write(result.value)
|
|
233
|
+
|
|
234
|
+
const paths = diffPaths(prev, result.value)
|
|
235
|
+
if (paths.length > 0) this.#emit(prev, result.value, paths, source)
|
|
236
|
+
|
|
237
|
+
return result.issues
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 校验、落盘、通知
|
|
242
|
+
* @param candidate 候选值
|
|
243
|
+
* @param source 变更来源
|
|
244
|
+
* @returns 新快照
|
|
245
|
+
* @throws SchemaError 校验失败
|
|
246
|
+
*/
|
|
247
|
+
async #commit(candidate: unknown, source: ConfigChange<T>["source"]): Promise<DeepReadonly<T>> {
|
|
248
|
+
const result = this.#validator.safeParse(candidate)
|
|
249
|
+
if (!result.ok) throw new SchemaError(result.issues)
|
|
250
|
+
|
|
251
|
+
const prev = this.#value
|
|
252
|
+
const next = result.value
|
|
253
|
+
const paths = diffPaths(prev, next)
|
|
254
|
+
|
|
255
|
+
// 无实质变化就不写盘,避免 WebUI 里点一下保存就产生一次磁盘写入与一轮通知
|
|
256
|
+
if (paths.length === 0) return this.get()
|
|
257
|
+
|
|
258
|
+
this.#value = next
|
|
259
|
+
await this.#write(next)
|
|
260
|
+
this.#emit(prev, next, paths, source)
|
|
261
|
+
return this.get()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 序列化为 YAML 文本
|
|
266
|
+
* @param value 配置值
|
|
267
|
+
* @returns YAML 文本
|
|
268
|
+
*/
|
|
269
|
+
#render(value: T): string {
|
|
270
|
+
return serializeYaml(value, { header: this.#header, descriptor: this.schema })
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 原子写盘
|
|
275
|
+
* @param value 配置值
|
|
276
|
+
*/
|
|
277
|
+
async #write(value: T): Promise<void> {
|
|
278
|
+
const text = this.#render(value)
|
|
279
|
+
this.#lastWritten = text
|
|
280
|
+
await atomicWrite(this.file, text)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 派发变更事件
|
|
285
|
+
* @param prev 旧值
|
|
286
|
+
* @param next 新值
|
|
287
|
+
* @param paths 变化路径
|
|
288
|
+
* @param source 变更来源
|
|
289
|
+
*/
|
|
290
|
+
#emit(prev: T, next: T, paths: string[], source: ConfigChange<T>["source"]): void {
|
|
291
|
+
const change: ConfigChange<T> = {
|
|
292
|
+
prev: prev as unknown as DeepReadonly<T>,
|
|
293
|
+
next: next as unknown as DeepReadonly<T>,
|
|
294
|
+
paths,
|
|
295
|
+
source
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const cb of this.#subscribers) this.#invoke(cb, change)
|
|
299
|
+
for (const entry of this.#watchers) {
|
|
300
|
+
if (paths.some(p => pathAffects(p, entry.path))) this.#invoke(entry.cb, change)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 调用订阅回调并隔离错误
|
|
306
|
+
* @param cb 回调
|
|
307
|
+
* @param change 变更事件
|
|
308
|
+
*/
|
|
309
|
+
#invoke(cb: (change: ConfigChange<T>) => void, change: ConfigChange<T>): void {
|
|
310
|
+
try {
|
|
311
|
+
cb(change)
|
|
312
|
+
} catch (err) {
|
|
313
|
+
this.#logger.error(`配置 ${this.name} 的变更回调抛错`, err)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 把校验问题列成多行文本
|
|
320
|
+
* @param issues 问题列表
|
|
321
|
+
* @returns 多行文本
|
|
322
|
+
*/
|
|
323
|
+
function formatIssues(issues: readonly SchemaIssue[]): string {
|
|
324
|
+
return issues.map(i => ` · ${i.path === "" ? "(根)" : i.path}:${i.message}`).join("\n")
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 配置仓库
|
|
329
|
+
*
|
|
330
|
+
* 一个进程一个实例,管住 `config/` 目录下所有 YAML 与唯一一个目录监听器。
|
|
331
|
+
*/
|
|
332
|
+
export class ConfigStore {
|
|
333
|
+
/** 配置目录 */
|
|
334
|
+
readonly #dir: string
|
|
335
|
+
/** 日志器 */
|
|
336
|
+
readonly #logger: Logger
|
|
337
|
+
/** 是否监听外部改动 */
|
|
338
|
+
readonly #watchEnabled: boolean
|
|
339
|
+
/** 名字 → 配置文件 */
|
|
340
|
+
|
|
341
|
+
readonly #files = new Map<string, ConfigFile<any>>()
|
|
342
|
+
/** 目录监听器 */
|
|
343
|
+
#watcher: FSWatcher | undefined
|
|
344
|
+
/** 重载去抖定时器 */
|
|
345
|
+
readonly #timers = new Map<string, NodeJS.Timeout>()
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @param opts 仓库参数
|
|
349
|
+
*/
|
|
350
|
+
constructor(opts: ConfigStoreOptions) {
|
|
351
|
+
this.#dir = opts.dir
|
|
352
|
+
this.#logger = opts.logger.child({ scope: "config" })
|
|
353
|
+
this.#watchEnabled = opts.watch !== false
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** 配置目录 */
|
|
357
|
+
get dir(): string {
|
|
358
|
+
return this.#dir
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 声明一份配置
|
|
363
|
+
*
|
|
364
|
+
* 同名重复声明会抛错 —— 两个插件抢同一个文件是必须暴露的 bug,
|
|
365
|
+
* 而不是让后者悄悄覆盖前者。
|
|
366
|
+
* @param name 配置名,同时是文件名(`<name>.yaml`)
|
|
367
|
+
* @param validator schema
|
|
368
|
+
* @param opts 附加信息
|
|
369
|
+
* @returns 配置句柄
|
|
370
|
+
* @throws 名字非法或重复声明时
|
|
371
|
+
*/
|
|
372
|
+
async define<T>(name: string, validator: Schema<T>, opts: DefineConfigOptions = {}): Promise<ConfigFile<T>> {
|
|
373
|
+
if (!NAME_RE.test(name)) throw new Error(`配置名 ${name} 不合法:需以字母开头,只含字母数字与 . - _`)
|
|
374
|
+
if (this.#files.has(name)) throw new Error(`配置 ${name} 已被声明,请换个名字`)
|
|
375
|
+
|
|
376
|
+
await ensureDir(this.#dir)
|
|
377
|
+
|
|
378
|
+
const title = opts.title ?? name
|
|
379
|
+
const header = [
|
|
380
|
+
`Yunzai NG 配置:${title}`,
|
|
381
|
+
"本文件由 schema 自动生成:保存时会重建注释,但不会丢弃任何配置值。",
|
|
382
|
+
"可以直接手改,保存后框架会自动重新加载;改错了会在日志里指出具体字段。",
|
|
383
|
+
...(opts.notes ?? [])
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
const file = new ConfigFile<T>({
|
|
387
|
+
name,
|
|
388
|
+
file: join(this.#dir, `${name}.yaml`),
|
|
389
|
+
validator,
|
|
390
|
+
value: validator.defaults(),
|
|
391
|
+
logger: this.#logger,
|
|
392
|
+
title,
|
|
393
|
+
header
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
this.#files.set(name, file)
|
|
397
|
+
await file.load("default", true)
|
|
398
|
+
return file
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 取已声明的配置
|
|
403
|
+
* @param name 配置名
|
|
404
|
+
* @returns 配置句柄;未声明时 undefined
|
|
405
|
+
*/
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
*
|
|
409
|
+
*/
|
|
410
|
+
get(name: string): ConfigFile<any> | undefined {
|
|
411
|
+
return this.#files.get(name)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* 移除一份配置声明(插件卸载时调用)
|
|
416
|
+
*
|
|
417
|
+
* 只解除内存中的登记,不删磁盘文件 —— 插件重装后配置还在。
|
|
418
|
+
* @param name 配置名
|
|
419
|
+
*/
|
|
420
|
+
remove(name: string): void {
|
|
421
|
+
this.#files.delete(name)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* 列出全部配置
|
|
426
|
+
* @returns 配置摘要数组,按名字排序
|
|
427
|
+
*/
|
|
428
|
+
list(): ConfigSummary[] {
|
|
429
|
+
return [...this.#files.values()]
|
|
430
|
+
.map(file => ({ name: file.name, file: file.file, title: file.title, schema: file.schema }))
|
|
431
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 开始监听配置目录
|
|
436
|
+
*
|
|
437
|
+
* 监听**目录**而不是每个文件:原子写用的是 rename,被替换的文件 inode 会变,
|
|
438
|
+
* 盯着文件的 watcher 在 Windows 上会失效。顺带只需一个监听器。
|
|
439
|
+
* @returns 取消监听
|
|
440
|
+
*/
|
|
441
|
+
startWatching(): Disposer {
|
|
442
|
+
if (!this.#watchEnabled || this.#watcher) return () => undefined
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
// persistent: false —— 配置监听不应该拖着进程不让退出
|
|
446
|
+
this.#watcher = watch(this.#dir, { persistent: false }, (_event, filename) => {
|
|
447
|
+
if (!filename) return
|
|
448
|
+
const base = basename(String(filename))
|
|
449
|
+
const match = /^(.+)\.ya?ml$/i.exec(base)
|
|
450
|
+
if (!match) return // 原子写留下的 .tmp-xxxx 之类,忽略
|
|
451
|
+
this.#scheduleReload(match[1]!)
|
|
452
|
+
})
|
|
453
|
+
this.#watcher.on("error", err => this.#logger.warn("配置目录监听出错,热加载已停止", err))
|
|
454
|
+
} catch (err) {
|
|
455
|
+
this.#logger.warn("无法监听配置目录,配置将只在启动时加载", err)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return () => this.dispose()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** 停止监听并清理定时器 */
|
|
462
|
+
dispose(): void {
|
|
463
|
+
for (const timer of this.#timers.values()) clearTimeout(timer)
|
|
464
|
+
this.#timers.clear()
|
|
465
|
+
this.#watcher?.close()
|
|
466
|
+
this.#watcher = undefined
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* 去抖后重载某个配置
|
|
471
|
+
* @param name 配置名
|
|
472
|
+
*/
|
|
473
|
+
#scheduleReload(name: string): void {
|
|
474
|
+
const file = this.#files.get(name)
|
|
475
|
+
if (!file) return
|
|
476
|
+
|
|
477
|
+
const existing = this.#timers.get(name)
|
|
478
|
+
if (existing) clearTimeout(existing)
|
|
479
|
+
|
|
480
|
+
const timer = setTimeout(() => {
|
|
481
|
+
this.#timers.delete(name)
|
|
482
|
+
void file
|
|
483
|
+
.load("file", false)
|
|
484
|
+
.then(issues => {
|
|
485
|
+
if (issues.some(i => i.severity === "error")) return
|
|
486
|
+
this.#logger.info(`配置 ${name} 已重新加载`)
|
|
487
|
+
})
|
|
488
|
+
.catch((err: unknown) => this.#logger.error(`重新加载配置 ${name} 失败`, err))
|
|
489
|
+
}, RELOAD_DEBOUNCE)
|
|
490
|
+
|
|
491
|
+
if (typeof timer.unref === "function") timer.unref()
|
|
492
|
+
this.#timers.set(name, timer)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 创建配置仓库
|
|
498
|
+
* @param opts 仓库参数
|
|
499
|
+
* @returns 配置仓库
|
|
500
|
+
*/
|
|
501
|
+
export function createConfigStore(opts: ConfigStoreOptions): ConfigStore {
|
|
502
|
+
return new ConfigStore(opts)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** 深拷贝一份配置快照,供需要可变副本的场景使用 */
|
|
506
|
+
export const cloneConfig = deepClone
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:把配置对象序列化成**带中文注释**的 YAML,以及从 YAML 反序列化
|
|
3
|
+
* 依赖方向:依赖 yaml 包与类型包
|
|
4
|
+
* 生命周期:纯函数
|
|
5
|
+
* 注意事项:注释由 schema 的 `title`/`description`/枚举候选自动生成,不另维护一份
|
|
6
|
+
* YAML 模板 —— 两份东西的默认值会各自漂移,而使用者拿到的注释会常年过期。
|
|
7
|
+
*
|
|
8
|
+
* 代价是**保存时会重写整个文件,用户手写的注释不被保留**。这是有意的
|
|
9
|
+
* 取舍:配置的主编辑面是 WebUI,文件是产物;换来的是注释永远与当前版本
|
|
10
|
+
* 一致。文件里的**值**当然完整保留。
|
|
11
|
+
*/
|
|
12
|
+
import { Document, isMap, isSeq, parse, type Node } from "yaml"
|
|
13
|
+
import type { SchemaDescriptor } from "@yunzai-ng/types"
|
|
14
|
+
|
|
15
|
+
/** 序列化选项 */
|
|
16
|
+
export interface SerializeOptions {
|
|
17
|
+
/** 文件头注释(每行前会自动加 `# `) */
|
|
18
|
+
header?: string[]
|
|
19
|
+
/** 表单描述,用于生成字段注释 */
|
|
20
|
+
descriptor?: SchemaDescriptor
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 解析 YAML 文本
|
|
25
|
+
* @param text YAML 文本
|
|
26
|
+
* @returns 解析结果;空文本返回 undefined
|
|
27
|
+
* @throws 语法错误时抛出,错误信息里带行号
|
|
28
|
+
*/
|
|
29
|
+
export function parseYaml(text: string): unknown {
|
|
30
|
+
if (text.trim() === "") return undefined
|
|
31
|
+
return parse(text, { merge: true })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 把配置对象序列化成带注释的 YAML
|
|
36
|
+
* @param value 配置值
|
|
37
|
+
* @param opts 序列化选项
|
|
38
|
+
* @returns YAML 文本(以换行结尾)
|
|
39
|
+
*/
|
|
40
|
+
export function serializeYaml(value: unknown, opts: SerializeOptions = {}): string {
|
|
41
|
+
const doc = new Document(value)
|
|
42
|
+
|
|
43
|
+
if (opts.header && opts.header.length > 0) {
|
|
44
|
+
doc.commentBefore = opts.header.map(line => ` ${line}`).join("\n")
|
|
45
|
+
}
|
|
46
|
+
if (opts.descriptor && doc.contents) {
|
|
47
|
+
annotate(doc.contents, opts.descriptor)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// lineWidth: 0 关闭自动折行 —— 折行后的长 URL、cookie 在文本编辑器里很难改
|
|
51
|
+
return doc.toString({ lineWidth: 0, indent: 2, nullStr: "~" })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 给 YAML 节点递归挂注释
|
|
56
|
+
* @param node YAML 节点
|
|
57
|
+
* @param descriptor 对应的表单描述
|
|
58
|
+
*/
|
|
59
|
+
function annotate(node: Node, descriptor: SchemaDescriptor): void {
|
|
60
|
+
if (isMap(node) && descriptor.properties) {
|
|
61
|
+
for (const item of node.items) {
|
|
62
|
+
const key = item.key
|
|
63
|
+
if (typeof key !== "object" || key === null || !("value" in key)) continue
|
|
64
|
+
const name = String((key as { value: unknown }).value)
|
|
65
|
+
const child = descriptor.properties[name]
|
|
66
|
+
if (!child) continue
|
|
67
|
+
|
|
68
|
+
const comment = buildComment(child)
|
|
69
|
+
if (comment) (key as { commentBefore?: string }).commentBefore = comment
|
|
70
|
+
|
|
71
|
+
const valueNode = item.value
|
|
72
|
+
if (valueNode && typeof valueNode === "object") annotate(valueNode as Node, child)
|
|
73
|
+
}
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isSeq(node) && descriptor.items) {
|
|
78
|
+
// 只给第一个元素挂注释:每一项都重复一遍会把列表淹没
|
|
79
|
+
const first = node.items[0]
|
|
80
|
+
if (first && typeof first === "object") annotate(first as Node, descriptor.items)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 由字段描述生成注释文本
|
|
86
|
+
* @param descriptor 字段描述
|
|
87
|
+
* @returns 注释文本;无可写内容时返回 undefined
|
|
88
|
+
*/
|
|
89
|
+
function buildComment(descriptor: SchemaDescriptor): string | undefined {
|
|
90
|
+
const lines: string[] = []
|
|
91
|
+
|
|
92
|
+
if (descriptor.title) lines.push(descriptor.title)
|
|
93
|
+
if (descriptor.description) lines.push(...descriptor.description.split("\n"))
|
|
94
|
+
|
|
95
|
+
if (descriptor.enum && descriptor.enum.length > 0) {
|
|
96
|
+
const items = descriptor.enum.map(item => {
|
|
97
|
+
const label = item.label ?? item.description
|
|
98
|
+
return label ? `${String(item.value)}(${label})` : String(item.value)
|
|
99
|
+
})
|
|
100
|
+
lines.push(`可选值:${items.join(" / ")}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (descriptor.widget === "duration") lines.push("格式:毫秒数,或 30s / 5m / 2h / 7d")
|
|
104
|
+
if (descriptor.widget === "cron") lines.push("格式:cron 表达式,如 0 0 8 * * *")
|
|
105
|
+
if (descriptor.secret) lines.push("敏感信息,请勿分享本文件")
|
|
106
|
+
|
|
107
|
+
if (descriptor.min !== undefined || descriptor.max !== undefined) {
|
|
108
|
+
const range =
|
|
109
|
+
descriptor.min !== undefined && descriptor.max !== undefined
|
|
110
|
+
? `${descriptor.min} ~ ${descriptor.max}`
|
|
111
|
+
: descriptor.min !== undefined
|
|
112
|
+
? `≥ ${descriptor.min}`
|
|
113
|
+
: `≤ ${descriptor.max}`
|
|
114
|
+
lines.push(`取值范围:${range}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (lines.length === 0) return undefined
|
|
118
|
+
return lines.map(line => ` ${line}`).join("\n")
|
|
119
|
+
}
|