@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,1036 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:配置 schema 构造器(校验 + WebUI 表单描述,一份声明两处产出)
|
|
3
|
+
* 依赖方向:依赖类型包与 util/deep、util/duration
|
|
4
|
+
* 生命周期:schema 对象不可变,可自由复用与共享
|
|
5
|
+
* 注意事项:**为什么自己写而不用 zod**:面板要把 schema 降级成表单描述,而 zod 只能读 `_def`
|
|
6
|
+
* 反推、版本一升就碎;内核要跑在 Termux 上,少一个运行时依赖就少一次装包失败;
|
|
7
|
+
* 配置是人手写的 YAML,需要「字符串 '8080' 当端口」这类宽容转换,而 zod 的 coerce
|
|
8
|
+
* 是全局开关、粒度不够。
|
|
9
|
+
*
|
|
10
|
+
* 三条刻意的语义:**数组整体替换**不逐元素合并(见 util/deep);**缺失的对象节点按
|
|
11
|
+
* 子字段默认值物化**,故用户只写关心的几个键即可;**未识别的键保留但报 warn**,
|
|
12
|
+
* 不静默丢弃 —— 静默丢弃会让拼错的配置项改半天没反应。
|
|
13
|
+
*/
|
|
14
|
+
import type { IssueSeverity, SchemaDescriptor, SchemaEnumItem, SchemaIssue, SchemaWidget } from "@yunzai-ng/types"
|
|
15
|
+
import { deepClone, isPlainObject } from "../util/deep.js"
|
|
16
|
+
import { parseDuration } from "../util/duration.js"
|
|
17
|
+
|
|
18
|
+
// `SchemaIssue` 定义在类型包:面板前端也要消费它(400 响应体里的 issues 数组),
|
|
19
|
+
// 而前端不许依赖内核。这里转发出去,使用者仍可从 @yunzai-ng/core 拿到
|
|
20
|
+
export type { IssueSeverity, SchemaIssue } from "@yunzai-ng/types"
|
|
21
|
+
|
|
22
|
+
/** 校验结果 */
|
|
23
|
+
export type ParseResult<T> =
|
|
24
|
+
| {
|
|
25
|
+
/** 校验通过 */
|
|
26
|
+
ok: true
|
|
27
|
+
/** 已填充默认值、已强制转换的结果 */
|
|
28
|
+
value: T
|
|
29
|
+
/** 非致命问题(如未识别的键) */
|
|
30
|
+
issues: SchemaIssue[]
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
/** 校验失败 */
|
|
34
|
+
ok: false
|
|
35
|
+
/** 全部问题 */
|
|
36
|
+
issues: SchemaIssue[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 校验失败抛出的错误 */
|
|
40
|
+
export class SchemaError extends Error {
|
|
41
|
+
/** 错误名 */
|
|
42
|
+
override readonly name = "SchemaError"
|
|
43
|
+
/** 全部问题 */
|
|
44
|
+
readonly issues: readonly SchemaIssue[]
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param issues 校验问题列表
|
|
48
|
+
*/
|
|
49
|
+
constructor(issues: readonly SchemaIssue[]) {
|
|
50
|
+
const lines = issues
|
|
51
|
+
.filter(i => i.severity === "error")
|
|
52
|
+
.map(i => ` · ${i.path === "" ? "(根)" : i.path}:${i.message}`)
|
|
53
|
+
super(`配置校验失败:\n${lines.join("\n")}`)
|
|
54
|
+
this.issues = issues
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 校验失败的哨兵值 */
|
|
59
|
+
const INVALID = Symbol("invalid")
|
|
60
|
+
|
|
61
|
+
/** 表单元信息 */
|
|
62
|
+
interface SchemaMeta {
|
|
63
|
+
/** 表单标签 */
|
|
64
|
+
title?: string
|
|
65
|
+
/** 说明文案 */
|
|
66
|
+
description?: string
|
|
67
|
+
/** 是否敏感 */
|
|
68
|
+
secret?: boolean
|
|
69
|
+
/** 是否只读 */
|
|
70
|
+
readonly?: boolean
|
|
71
|
+
/** 控件类型 */
|
|
72
|
+
widget?: SchemaWidget
|
|
73
|
+
/** 分组 */
|
|
74
|
+
group?: string
|
|
75
|
+
/** 排序权重 */
|
|
76
|
+
order?: number
|
|
77
|
+
/** 占位符 */
|
|
78
|
+
placeholder?: string
|
|
79
|
+
/** 条件显隐 */
|
|
80
|
+
showWhen?: Record<string, unknown>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 内部定义结构 */
|
|
84
|
+
interface SchemaDef {
|
|
85
|
+
/** 节点类型 */
|
|
86
|
+
kind: "string" | "number" | "boolean" | "enum" | "literal" | "array" | "object" | "record" | "unknown"
|
|
87
|
+
/** 表单元信息 */
|
|
88
|
+
meta: SchemaMeta
|
|
89
|
+
/** 是否可缺省 */
|
|
90
|
+
optional?: boolean
|
|
91
|
+
/** 是否有默认值 */
|
|
92
|
+
hasDefault?: boolean
|
|
93
|
+
/** 默认值 */
|
|
94
|
+
defaultValue?: unknown
|
|
95
|
+
/** 下界(数值大小 / 字符串长度 / 数组长度) */
|
|
96
|
+
min?: number
|
|
97
|
+
/** 上界 */
|
|
98
|
+
max?: number
|
|
99
|
+
/** 字符串正则 */
|
|
100
|
+
pattern?: RegExp
|
|
101
|
+
/** 字符串是否自动去首尾空白,缺省 true */
|
|
102
|
+
trim?: boolean
|
|
103
|
+
/**
|
|
104
|
+
* 字符串节点是否允许保留数值形态
|
|
105
|
+
*
|
|
106
|
+
* 仅 `duration()` 使用:时长既可写 `"30s"` 也可写 `30000`,两种写法都要
|
|
107
|
+
* 原样保留 —— 若统一转成字符串,配置文件里的 `cooldown: 0` 会被改写成
|
|
108
|
+
* `cooldown: '0'`,用户会以为框架在乱改自己的文件。
|
|
109
|
+
*/
|
|
110
|
+
allowNumber?: boolean
|
|
111
|
+
/** 数值是否必须为整数 */
|
|
112
|
+
int?: boolean
|
|
113
|
+
/** 数值步长(仅表单提示) */
|
|
114
|
+
step?: number
|
|
115
|
+
/** 枚举候选 */
|
|
116
|
+
enumItems?: SchemaEnumItem[]
|
|
117
|
+
/** 字面量值 */
|
|
118
|
+
literalValue?: unknown
|
|
119
|
+
/** 数组元素 schema */
|
|
120
|
+
|
|
121
|
+
element?: Schema<any>
|
|
122
|
+
/** 单值是否自动包成数组 */
|
|
123
|
+
wrapSingle?: boolean
|
|
124
|
+
/** 对象字段表 */
|
|
125
|
+
|
|
126
|
+
shape?: Record<string, Schema<any>>
|
|
127
|
+
/** 是否拒绝未识别的键 */
|
|
128
|
+
strict?: boolean
|
|
129
|
+
/** record 的值 schema */
|
|
130
|
+
|
|
131
|
+
valueSchema?: Schema<any>
|
|
132
|
+
/** 自定义校验 */
|
|
133
|
+
checks?: { message: string; test: (value: never) => boolean }[]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 配置 schema 节点
|
|
138
|
+
*
|
|
139
|
+
* 不可变:全部 fluent 方法均返回新实例,因此可安全地抽取公共片段以供复用。
|
|
140
|
+
*/
|
|
141
|
+
export class Schema<T> {
|
|
142
|
+
/** 内部定义 */
|
|
143
|
+
readonly #def: SchemaDef
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param def 内部定义;请通过 `s.*` 工厂函数创建,不要直接 new
|
|
147
|
+
*/
|
|
148
|
+
constructor(def: SchemaDef) {
|
|
149
|
+
this.#def = def
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ───────────────────────────── 元信息 ───────────────────────────── */
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 设置表单标签
|
|
156
|
+
* @param text 标签文案
|
|
157
|
+
* @returns 新 schema
|
|
158
|
+
*/
|
|
159
|
+
title(text: string): Schema<T> {
|
|
160
|
+
return this.#patch({ meta: { title: text } })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 设置说明文案
|
|
165
|
+
* @param text 说明
|
|
166
|
+
* @returns 新 schema
|
|
167
|
+
*/
|
|
168
|
+
desc(text: string): Schema<T> {
|
|
169
|
+
return this.#patch({ meta: { description: text } })
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 设置分组(WebUI 折叠区块)
|
|
174
|
+
* @param name 分组名
|
|
175
|
+
* @returns 新 schema
|
|
176
|
+
*/
|
|
177
|
+
group(name: string): Schema<T> {
|
|
178
|
+
return this.#patch({ meta: { group: name } })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 设置排序权重,小的在前
|
|
183
|
+
* @param weight 权重
|
|
184
|
+
* @returns 新 schema
|
|
185
|
+
*/
|
|
186
|
+
order(weight: number): Schema<T> {
|
|
187
|
+
return this.#patch({ meta: { order: weight } })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 指定控件类型
|
|
192
|
+
* @param widget 控件
|
|
193
|
+
* @returns 新 schema
|
|
194
|
+
*/
|
|
195
|
+
widget(widget: SchemaWidget): Schema<T> {
|
|
196
|
+
return this.#patch({ meta: { widget } })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 标记为敏感字段
|
|
201
|
+
*
|
|
202
|
+
* WebUI 读取时脱敏、日志中不打印。CK、token、密码必须标。
|
|
203
|
+
* @returns 新 schema
|
|
204
|
+
*/
|
|
205
|
+
secret(): Schema<T> {
|
|
206
|
+
return this.#patch({ meta: { secret: true, widget: this.#def.meta.widget ?? "password" } })
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 标记为只读
|
|
211
|
+
* @returns 新 schema
|
|
212
|
+
*/
|
|
213
|
+
readonly(): Schema<T> {
|
|
214
|
+
return this.#patch({ meta: { readonly: true } })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 设置输入框占位符
|
|
219
|
+
* @param text 占位文案
|
|
220
|
+
* @returns 新 schema
|
|
221
|
+
*/
|
|
222
|
+
placeholder(text: string): Schema<T> {
|
|
223
|
+
return this.#patch({ meta: { placeholder: text } })
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 设置条件显隐
|
|
228
|
+
* @param cond 同级字段取值条件,如 `{ mode: "ws-reverse" }`;值写成数组表示"取其中之一"
|
|
229
|
+
* @returns 新 schema
|
|
230
|
+
*/
|
|
231
|
+
showWhen(cond: Record<string, unknown>): Schema<T> {
|
|
232
|
+
return this.#patch({ meta: { showWhen: cond } })
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* ───────────────────────────── 可选与默认 ───────────────────────────── */
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 标记为可缺省
|
|
239
|
+
* @returns 新 schema,输出类型带上 undefined
|
|
240
|
+
*/
|
|
241
|
+
optional(): Schema<T | undefined> {
|
|
242
|
+
return this.#patch<T | undefined>({ optional: true })
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 设置默认值
|
|
247
|
+
*
|
|
248
|
+
* 有默认值的字段在输出类型里仍是必填 —— 因为解析后一定有值。
|
|
249
|
+
* @param value 默认值
|
|
250
|
+
* @returns 新 schema
|
|
251
|
+
*/
|
|
252
|
+
default(value: T): Schema<T> {
|
|
253
|
+
return this.#patch({ hasDefault: true, defaultValue: value })
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 追加自定义校验
|
|
258
|
+
* @param message 不通过时的中文提示
|
|
259
|
+
* @param test 返回 true 表示通过
|
|
260
|
+
* @returns 新 schema
|
|
261
|
+
*/
|
|
262
|
+
check(message: string, test: (value: T) => boolean): Schema<T> {
|
|
263
|
+
const checks = [...(this.#def.checks ?? []), { message, test: test as (value: never) => boolean }]
|
|
264
|
+
return this.#patch({ checks })
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* ───────────────────────────── 约束 ───────────────────────────── */
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 下界:数值最小值 / 字符串最短长度 / 数组最少元素
|
|
271
|
+
* @param value 下界
|
|
272
|
+
* @returns 新 schema
|
|
273
|
+
*/
|
|
274
|
+
min(value: number): Schema<T> {
|
|
275
|
+
return this.#patch({ min: value })
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 上界:数值最大值 / 字符串最长长度 / 数组最多元素
|
|
280
|
+
* @param value 上界
|
|
281
|
+
* @returns 新 schema
|
|
282
|
+
*/
|
|
283
|
+
max(value: number): Schema<T> {
|
|
284
|
+
return this.#patch({ max: value })
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 字符串正则约束
|
|
289
|
+
* @param regex 正则
|
|
290
|
+
* @param message 自定义提示
|
|
291
|
+
* @returns 新 schema
|
|
292
|
+
*/
|
|
293
|
+
pattern(regex: RegExp, message?: string): Schema<T> {
|
|
294
|
+
const next = this.#patch<T>({ pattern: regex })
|
|
295
|
+
return message ? next.check(message, v => typeof v !== "string" || regex.test(v)) : next
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 要求整数
|
|
300
|
+
* @returns 新 schema
|
|
301
|
+
*/
|
|
302
|
+
int(): Schema<T> {
|
|
303
|
+
return this.#patch({ int: true, step: this.#def.step ?? 1 })
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 数值步长(表单提示用)
|
|
308
|
+
* @param value 步长
|
|
309
|
+
* @returns 新 schema
|
|
310
|
+
*/
|
|
311
|
+
step(value: number): Schema<T> {
|
|
312
|
+
return this.#patch({ step: value })
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 对象:拒绝未识别的键
|
|
317
|
+
*
|
|
318
|
+
* 缺省行为是"保留并 warn",适配器这类结构明确的配置可以开严格模式。
|
|
319
|
+
* @returns 新 schema
|
|
320
|
+
*/
|
|
321
|
+
strict(): Schema<T> {
|
|
322
|
+
return this.#patch({ strict: true })
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 数组:允许单值自动包成数组
|
|
327
|
+
*
|
|
328
|
+
* 让 `masterQQ: 123` 与 `masterQQ: [123]` 都能用 —— 旧配置最常见的困惑点。
|
|
329
|
+
* @returns 新 schema
|
|
330
|
+
*/
|
|
331
|
+
single(): Schema<T> {
|
|
332
|
+
return this.#patch({ wrapSingle: true })
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ───────────────────────────── 解析 ───────────────────────────── */
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 解析并校验
|
|
339
|
+
* @param input 待校验值
|
|
340
|
+
* @returns 结果对象;失败时带全部问题,不抛错
|
|
341
|
+
*/
|
|
342
|
+
safeParse(input: unknown): ParseResult<T> {
|
|
343
|
+
const issues: SchemaIssue[] = []
|
|
344
|
+
const value = this.#validate(input, "", issues)
|
|
345
|
+
const failed = value === INVALID || issues.some(i => i.severity === "error")
|
|
346
|
+
return failed ? { ok: false, issues } : { ok: true, value: value as T, issues }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 解析并校验,失败抛错
|
|
351
|
+
* @param input 待校验值
|
|
352
|
+
* @returns 校验后的值(已填默认、已强制转换)
|
|
353
|
+
* @throws SchemaError 校验失败
|
|
354
|
+
*/
|
|
355
|
+
parse(input: unknown): T {
|
|
356
|
+
const result = this.safeParse(input)
|
|
357
|
+
if (!result.ok) throw new SchemaError(result.issues)
|
|
358
|
+
return result.value
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 只取默认值,不需要输入
|
|
363
|
+
*
|
|
364
|
+
* 用于生成初始配置文件。
|
|
365
|
+
* @returns 全部默认值构成的对象
|
|
366
|
+
* @throws SchemaError 当 schema 里有无默认值的必填项
|
|
367
|
+
*/
|
|
368
|
+
defaults(): T {
|
|
369
|
+
return this.parse(undefined)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 降级为 WebUI 表单描述
|
|
374
|
+
* @returns 表单描述结构体
|
|
375
|
+
*/
|
|
376
|
+
describe(): SchemaDescriptor {
|
|
377
|
+
const def = this.#def
|
|
378
|
+
const out: SchemaDescriptor = { type: this.#descriptorType() }
|
|
379
|
+
|
|
380
|
+
if (def.meta.title !== undefined) out.title = def.meta.title
|
|
381
|
+
if (def.meta.description !== undefined) out.description = def.meta.description
|
|
382
|
+
if (def.hasDefault) out.default = deepClone(def.defaultValue)
|
|
383
|
+
if (!def.optional && !def.hasDefault) out.required = true
|
|
384
|
+
if (def.meta.secret) out.secret = true
|
|
385
|
+
if (def.meta.readonly) out.readonly = true
|
|
386
|
+
if (def.meta.widget !== undefined) out.widget = def.meta.widget
|
|
387
|
+
if (def.meta.group !== undefined) out.group = def.meta.group
|
|
388
|
+
if (def.meta.order !== undefined) out.order = def.meta.order
|
|
389
|
+
if (def.meta.placeholder !== undefined) out.placeholder = def.meta.placeholder
|
|
390
|
+
if (def.meta.showWhen !== undefined) out.showWhen = def.meta.showWhen
|
|
391
|
+
if (def.min !== undefined) out.min = def.min
|
|
392
|
+
if (def.max !== undefined) out.max = def.max
|
|
393
|
+
if (def.step !== undefined) out.step = def.step
|
|
394
|
+
if (def.pattern !== undefined) out.pattern = def.pattern.source
|
|
395
|
+
|
|
396
|
+
if (def.kind === "object" && def.shape) {
|
|
397
|
+
const properties: Record<string, SchemaDescriptor> = {}
|
|
398
|
+
for (const [key, child] of Object.entries(def.shape)) properties[key] = child.describe()
|
|
399
|
+
out.properties = properties
|
|
400
|
+
}
|
|
401
|
+
if (def.kind === "array" && def.element) out.items = def.element.describe()
|
|
402
|
+
if (def.kind === "record" && def.valueSchema) out.values = def.valueSchema.describe()
|
|
403
|
+
if (def.kind === "enum" && def.enumItems) out.enum = def.enumItems.map(item => ({ ...item }))
|
|
404
|
+
if (def.kind === "literal") out.enum = [{ value: def.literalValue as string }]
|
|
405
|
+
|
|
406
|
+
return out
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 对象 schema:取子字段 schema
|
|
411
|
+
* @param key 字段名
|
|
412
|
+
* @returns 子 schema;不是对象或字段不存在时 undefined
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
*
|
|
417
|
+
*/
|
|
418
|
+
field(key: string): Schema<any> | undefined {
|
|
419
|
+
return this.#def.shape?.[key]
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 收集全部标了 `secret()` 的字段路径
|
|
424
|
+
*
|
|
425
|
+
* 日志脱敏与 WebUI 读取脱敏都用它,保证"标一次、处处生效"。
|
|
426
|
+
* @returns 点分路径列表
|
|
427
|
+
*/
|
|
428
|
+
secretPaths(): string[] {
|
|
429
|
+
const out: string[] = []
|
|
430
|
+
this.#collectSecrets("", out)
|
|
431
|
+
return out
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* ───────────────────────────── 内部 ───────────────────────────── */
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* 克隆并打补丁
|
|
438
|
+
* @param patch 要覆盖的定义片段
|
|
439
|
+
* @returns 新 schema
|
|
440
|
+
*/
|
|
441
|
+
#patch<U = T>(patch: Partial<SchemaDef> & { meta?: SchemaMeta }): Schema<U> {
|
|
442
|
+
return new Schema<U>({
|
|
443
|
+
...this.#def,
|
|
444
|
+
...patch,
|
|
445
|
+
meta: { ...this.#def.meta, ...(patch.meta ?? {}) }
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* 映射到表单描述的类型名
|
|
451
|
+
* @returns 描述类型
|
|
452
|
+
*/
|
|
453
|
+
#descriptorType(): SchemaDescriptor["type"] {
|
|
454
|
+
switch (this.#def.kind) {
|
|
455
|
+
case "literal":
|
|
456
|
+
return "enum"
|
|
457
|
+
case "string":
|
|
458
|
+
case "number":
|
|
459
|
+
case "boolean":
|
|
460
|
+
case "enum":
|
|
461
|
+
case "array":
|
|
462
|
+
case "object":
|
|
463
|
+
case "record":
|
|
464
|
+
return this.#def.kind
|
|
465
|
+
default:
|
|
466
|
+
return "unknown"
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* 递归收集敏感字段路径
|
|
472
|
+
* @param prefix 当前路径前缀
|
|
473
|
+
* @param out 收集容器
|
|
474
|
+
*/
|
|
475
|
+
#collectSecrets(prefix: string, out: string[]): void {
|
|
476
|
+
if (this.#def.meta.secret) out.push(prefix)
|
|
477
|
+
if (this.#def.shape) {
|
|
478
|
+
for (const [key, child] of Object.entries(this.#def.shape)) {
|
|
479
|
+
child.#collectSecrets(prefix === "" ? key : `${prefix}.${key}`, out)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (this.#def.element) this.#def.element.#collectSecrets(prefix === "" ? "*" : `${prefix}.*`, out)
|
|
483
|
+
if (this.#def.valueSchema) this.#def.valueSchema.#collectSecrets(prefix === "" ? "*" : `${prefix}.*`, out)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* 记录一条问题
|
|
488
|
+
* @param issues 收集容器
|
|
489
|
+
* @param path 路径
|
|
490
|
+
* @param message 说明
|
|
491
|
+
* @param severity 严重程度
|
|
492
|
+
*/
|
|
493
|
+
#fail(issues: SchemaIssue[], path: string, message: string, severity: IssueSeverity = "error"): void {
|
|
494
|
+
issues.push({ path, message, severity })
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* 核心校验
|
|
499
|
+
* @param input 输入值
|
|
500
|
+
* @param path 当前路径
|
|
501
|
+
* @param issues 问题收集容器
|
|
502
|
+
* @returns 校验后的值,或 INVALID
|
|
503
|
+
*/
|
|
504
|
+
#validate(input: unknown, path: string, issues: SchemaIssue[]): unknown {
|
|
505
|
+
const def = this.#def
|
|
506
|
+
|
|
507
|
+
// 缺失值处理:默认值 → 可选 → 对象自动物化 → 报缺失
|
|
508
|
+
if (input === undefined || input === null) {
|
|
509
|
+
if (def.hasDefault) return this.#materializeDefault(path, issues)
|
|
510
|
+
if (def.optional) return undefined
|
|
511
|
+
if (def.kind === "object") return this.#validateObject({}, path, issues)
|
|
512
|
+
if (def.kind === "unknown") return undefined
|
|
513
|
+
this.#fail(issues, path, "缺少必填项")
|
|
514
|
+
return INVALID
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
let value: unknown
|
|
518
|
+
switch (def.kind) {
|
|
519
|
+
case "string":
|
|
520
|
+
value = this.#validateString(input, path, issues)
|
|
521
|
+
break
|
|
522
|
+
case "number":
|
|
523
|
+
value = this.#validateNumber(input, path, issues)
|
|
524
|
+
break
|
|
525
|
+
case "boolean":
|
|
526
|
+
value = this.#validateBoolean(input, path, issues)
|
|
527
|
+
break
|
|
528
|
+
case "enum":
|
|
529
|
+
value = this.#validateEnum(input, path, issues)
|
|
530
|
+
break
|
|
531
|
+
case "literal":
|
|
532
|
+
value = input === def.literalValue ? input : this.#reject(issues, path, `必须为 ${String(def.literalValue)}`)
|
|
533
|
+
break
|
|
534
|
+
case "array":
|
|
535
|
+
value = this.#validateArray(input, path, issues)
|
|
536
|
+
break
|
|
537
|
+
case "object":
|
|
538
|
+
value = this.#validateObject(input, path, issues)
|
|
539
|
+
break
|
|
540
|
+
case "record":
|
|
541
|
+
value = this.#validateRecord(input, path, issues)
|
|
542
|
+
break
|
|
543
|
+
default:
|
|
544
|
+
value = input
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (value === INVALID) return INVALID
|
|
548
|
+
|
|
549
|
+
for (const rule of def.checks ?? []) {
|
|
550
|
+
if (!rule.test(value as never)) {
|
|
551
|
+
this.#fail(issues, path, rule.message)
|
|
552
|
+
return INVALID
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return value
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* 记录错误并返回 INVALID
|
|
560
|
+
* @param issues 问题收集容器
|
|
561
|
+
* @param path 路径
|
|
562
|
+
* @param message 说明
|
|
563
|
+
* @returns INVALID
|
|
564
|
+
*/
|
|
565
|
+
#reject(issues: SchemaIssue[], path: string, message: string): typeof INVALID {
|
|
566
|
+
this.#fail(issues, path, message)
|
|
567
|
+
return INVALID
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* 把声明的默认值也过一遍校验
|
|
572
|
+
*
|
|
573
|
+
* 默认值必须走同一条校验/转换链,否则 `defaults()` 与 `parse(用户文件)`
|
|
574
|
+
* 会给出**形态不同的同一份配置**(例如 duration 的默认 `0` 与文件里读回的
|
|
575
|
+
* `"0"`),下游按路径比对时就会在每次启动时误报"配置变了"。
|
|
576
|
+
*
|
|
577
|
+
* 默认值不合法属于插件作者的 bug,不是用户的错:这里只记 warn 并退回原始
|
|
578
|
+
* 默认值,让机器人照常启动,而不是让用户面对一个自己无法修复的启动失败。
|
|
579
|
+
* @param path 当前路径
|
|
580
|
+
* @param issues 问题收集容器
|
|
581
|
+
* @returns 规范化后的默认值
|
|
582
|
+
*/
|
|
583
|
+
#materializeDefault(path: string, issues: SchemaIssue[]): unknown {
|
|
584
|
+
const raw = deepClone(this.#def.defaultValue)
|
|
585
|
+
if (raw === undefined || raw === null) return raw
|
|
586
|
+
|
|
587
|
+
const probe: SchemaIssue[] = []
|
|
588
|
+
const value = this.#validate(raw, path, probe)
|
|
589
|
+
if (value === INVALID || probe.some(i => i.severity === "error")) {
|
|
590
|
+
const detail = probe.map(i => i.message).join(";")
|
|
591
|
+
issues.push({
|
|
592
|
+
path,
|
|
593
|
+
message: `schema 声明的默认值不符合自身规则(${detail}),请联系插件作者`,
|
|
594
|
+
severity: "warn"
|
|
595
|
+
})
|
|
596
|
+
return raw
|
|
597
|
+
}
|
|
598
|
+
issues.push(...probe)
|
|
599
|
+
return value
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* 校验字符串
|
|
604
|
+
* @param input 输入
|
|
605
|
+
* @param path 路径
|
|
606
|
+
* @param issues 问题容器
|
|
607
|
+
* @returns 字符串或 INVALID
|
|
608
|
+
*/
|
|
609
|
+
#validateString(input: unknown, path: string, issues: SchemaIssue[]): unknown {
|
|
610
|
+
const def = this.#def
|
|
611
|
+
// 数字/布尔宽容转字符串:YAML 里 `token: 12345` 会被解析成数字
|
|
612
|
+
let text: string
|
|
613
|
+
if (typeof input === "string") text = input
|
|
614
|
+
else if (typeof input === "number" && def.allowNumber) return input
|
|
615
|
+
else if (typeof input === "number" || typeof input === "boolean") text = String(input)
|
|
616
|
+
else return this.#reject(issues, path, "应为字符串")
|
|
617
|
+
|
|
618
|
+
if (def.trim !== false) text = text.trim()
|
|
619
|
+
if (def.min !== undefined && [...text].length < def.min) {
|
|
620
|
+
return this.#reject(issues, path, `长度不能少于 ${def.min}`)
|
|
621
|
+
}
|
|
622
|
+
if (def.max !== undefined && [...text].length > def.max) {
|
|
623
|
+
return this.#reject(issues, path, `长度不能超过 ${def.max}`)
|
|
624
|
+
}
|
|
625
|
+
if (def.pattern && !def.pattern.test(text)) {
|
|
626
|
+
return this.#reject(issues, path, `格式不正确(需匹配 ${def.pattern.source})`)
|
|
627
|
+
}
|
|
628
|
+
return text
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* 校验数值
|
|
633
|
+
* @param input 输入
|
|
634
|
+
* @param path 路径
|
|
635
|
+
* @param issues 问题容器
|
|
636
|
+
* @returns 数值或 INVALID
|
|
637
|
+
*/
|
|
638
|
+
#validateNumber(input: unknown, path: string, issues: SchemaIssue[]): unknown {
|
|
639
|
+
const def = this.#def
|
|
640
|
+
let num: number
|
|
641
|
+
if (typeof input === "number") num = input
|
|
642
|
+
else if (typeof input === "string" && input.trim() !== "" && Number.isFinite(Number(input))) {
|
|
643
|
+
num = Number(input)
|
|
644
|
+
} else return this.#reject(issues, path, "应为数字")
|
|
645
|
+
|
|
646
|
+
if (!Number.isFinite(num)) return this.#reject(issues, path, "应为有限数字")
|
|
647
|
+
if (def.int && !Number.isInteger(num)) return this.#reject(issues, path, "应为整数")
|
|
648
|
+
if (def.min !== undefined && num < def.min) return this.#reject(issues, path, `不能小于 ${def.min}`)
|
|
649
|
+
if (def.max !== undefined && num > def.max) return this.#reject(issues, path, `不能大于 ${def.max}`)
|
|
650
|
+
return num
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* 校验布尔
|
|
655
|
+
* @param input 输入
|
|
656
|
+
* @param path 路径
|
|
657
|
+
* @param issues 问题容器
|
|
658
|
+
* @returns 布尔或 INVALID
|
|
659
|
+
*/
|
|
660
|
+
#validateBoolean(input: unknown, path: string, issues: SchemaIssue[]): unknown {
|
|
661
|
+
if (typeof input === "boolean") return input
|
|
662
|
+
if (typeof input === "string") {
|
|
663
|
+
const text = input.trim().toLowerCase()
|
|
664
|
+
if (["true", "1", "yes", "on", "是"].includes(text)) return true
|
|
665
|
+
if (["false", "0", "no", "off", "否"].includes(text)) return false
|
|
666
|
+
}
|
|
667
|
+
if (input === 1) return true
|
|
668
|
+
if (input === 0) return false
|
|
669
|
+
return this.#reject(issues, path, "应为布尔值")
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* 校验枚举
|
|
674
|
+
* @param input 输入
|
|
675
|
+
* @param path 路径
|
|
676
|
+
* @param issues 问题容器
|
|
677
|
+
* @returns 枚举值或 INVALID
|
|
678
|
+
*/
|
|
679
|
+
#validateEnum(input: unknown, path: string, issues: SchemaIssue[]): unknown {
|
|
680
|
+
const items = this.#def.enumItems ?? []
|
|
681
|
+
for (const item of items) {
|
|
682
|
+
if (item.value === input) return input
|
|
683
|
+
// WebUI 的 select 只会回传字符串,数字枚举需要按文本比一次
|
|
684
|
+
if (String(item.value) === String(input)) return item.value
|
|
685
|
+
}
|
|
686
|
+
const allowed = items.map(i => String(i.value)).join(" / ")
|
|
687
|
+
return this.#reject(issues, path, `只能是 ${allowed}`)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* 校验数组
|
|
692
|
+
* @param input 输入
|
|
693
|
+
* @param path 路径
|
|
694
|
+
* @param issues 问题容器
|
|
695
|
+
* @returns 数组或 INVALID
|
|
696
|
+
*/
|
|
697
|
+
#validateArray(input: unknown, path: string, issues: SchemaIssue[]): unknown {
|
|
698
|
+
const def = this.#def
|
|
699
|
+
let list: unknown[]
|
|
700
|
+
if (Array.isArray(input)) list = input
|
|
701
|
+
else if (def.wrapSingle) list = [input]
|
|
702
|
+
else return this.#reject(issues, path, "应为数组")
|
|
703
|
+
|
|
704
|
+
if (def.min !== undefined && list.length < def.min) {
|
|
705
|
+
return this.#reject(issues, path, `至少需要 ${def.min} 项`)
|
|
706
|
+
}
|
|
707
|
+
if (def.max !== undefined && list.length > def.max) {
|
|
708
|
+
return this.#reject(issues, path, `最多 ${def.max} 项`)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const element = def.element
|
|
712
|
+
if (!element) return deepClone(list)
|
|
713
|
+
|
|
714
|
+
const out: unknown[] = []
|
|
715
|
+
let bad = false
|
|
716
|
+
for (let i = 0; i < list.length; i++) {
|
|
717
|
+
const item = element.#validate(list[i], `${path}[${i}]`, issues)
|
|
718
|
+
if (item === INVALID) bad = true
|
|
719
|
+
else out.push(item)
|
|
720
|
+
}
|
|
721
|
+
return bad ? INVALID : out
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* 校验对象
|
|
726
|
+
* @param input 输入
|
|
727
|
+
* @param path 路径
|
|
728
|
+
* @param issues 问题容器
|
|
729
|
+
* @returns 对象或 INVALID
|
|
730
|
+
*/
|
|
731
|
+
#validateObject(input: unknown, path: string, issues: SchemaIssue[]): unknown {
|
|
732
|
+
const def = this.#def
|
|
733
|
+
if (!isPlainObject(input)) return this.#reject(issues, path, "应为对象")
|
|
734
|
+
|
|
735
|
+
const shape = def.shape ?? {}
|
|
736
|
+
const out: Record<string, unknown> = {}
|
|
737
|
+
let bad = false
|
|
738
|
+
|
|
739
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
740
|
+
const childPath = path === "" ? key : `${path}.${key}`
|
|
741
|
+
const value = child.#validate(input[key], childPath, issues)
|
|
742
|
+
if (value === INVALID) bad = true
|
|
743
|
+
else if (value !== undefined) out[key] = value
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
for (const key of Object.keys(input)) {
|
|
747
|
+
if (Object.prototype.hasOwnProperty.call(shape, key)) continue
|
|
748
|
+
const childPath = path === "" ? key : `${path}.${key}`
|
|
749
|
+
if (def.strict) {
|
|
750
|
+
this.#fail(issues, childPath, "无法识别的配置项")
|
|
751
|
+
bad = true
|
|
752
|
+
} else {
|
|
753
|
+
// 保留但提示:可能是拼写错误,也可能来自暂时卸载的插件
|
|
754
|
+
this.#fail(issues, childPath, "无法识别的配置项,已原样保留", "warn")
|
|
755
|
+
out[key] = deepClone(input[key])
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return bad ? INVALID : out
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* 校验字典
|
|
764
|
+
* @param input 输入
|
|
765
|
+
* @param path 路径
|
|
766
|
+
* @param issues 问题容器
|
|
767
|
+
* @returns 字典或 INVALID
|
|
768
|
+
*/
|
|
769
|
+
#validateRecord(input: unknown, path: string, issues: SchemaIssue[]): unknown {
|
|
770
|
+
if (!isPlainObject(input)) return this.#reject(issues, path, "应为键值对象")
|
|
771
|
+
const valueSchema = this.#def.valueSchema
|
|
772
|
+
if (!valueSchema) return deepClone(input)
|
|
773
|
+
|
|
774
|
+
const out: Record<string, unknown> = {}
|
|
775
|
+
let bad = false
|
|
776
|
+
for (const [key, raw] of Object.entries(input)) {
|
|
777
|
+
const childPath = path === "" ? key : `${path}.${key}`
|
|
778
|
+
const value = valueSchema.#validate(raw, childPath, issues)
|
|
779
|
+
if (value === INVALID) bad = true
|
|
780
|
+
else out[key] = value
|
|
781
|
+
}
|
|
782
|
+
return bad ? INVALID : out
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** 从 schema 反推 TS 类型 */
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
*
|
|
790
|
+
*/
|
|
791
|
+
export type Infer<S> = S extends Schema<infer T> ? T : never
|
|
792
|
+
|
|
793
|
+
/** 让交叉类型在编辑器提示里展开成扁平对象 */
|
|
794
|
+
type Simplify<T> = { [K in keyof T]: T[K] } & {}
|
|
795
|
+
|
|
796
|
+
/** 对象 schema 的字段表 */
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
*
|
|
800
|
+
*/
|
|
801
|
+
export type Shape = Record<string, Schema<any>>
|
|
802
|
+
|
|
803
|
+
/** 可缺省的键(输出类型含 undefined 的那些) */
|
|
804
|
+
type OptionalKeys<S extends Shape> = {
|
|
805
|
+
[K in keyof S]: undefined extends Infer<S[K]> ? K : never
|
|
806
|
+
}[keyof S]
|
|
807
|
+
|
|
808
|
+
/** 对象 schema 的输出类型 */
|
|
809
|
+
export type ObjectOutput<S extends Shape> = Simplify<
|
|
810
|
+
{ [K in Exclude<keyof S, OptionalKeys<S>>]: Infer<S[K]> } & { [K in OptionalKeys<S>]?: Infer<S[K]> }
|
|
811
|
+
>
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* 创建基础节点
|
|
815
|
+
* @param def 定义片段
|
|
816
|
+
* @returns schema
|
|
817
|
+
*/
|
|
818
|
+
function make<T>(def: Omit<SchemaDef, "meta"> & { meta?: SchemaMeta }): Schema<T> {
|
|
819
|
+
return new Schema<T>({ ...def, meta: def.meta ?? {} })
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* 字符串
|
|
824
|
+
* @returns 字符串 schema
|
|
825
|
+
*/
|
|
826
|
+
function string(): Schema<string> {
|
|
827
|
+
return make<string>({ kind: "string" })
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* 数字
|
|
832
|
+
* @returns 数字 schema
|
|
833
|
+
*/
|
|
834
|
+
function number(): Schema<number> {
|
|
835
|
+
return make<number>({ kind: "number" })
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* 布尔
|
|
840
|
+
* @returns 布尔 schema
|
|
841
|
+
*/
|
|
842
|
+
function boolean(): Schema<boolean> {
|
|
843
|
+
return make<boolean>({ kind: "boolean" })
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* 字面量
|
|
848
|
+
* @param value 唯一允许的值
|
|
849
|
+
* @returns 字面量 schema
|
|
850
|
+
*/
|
|
851
|
+
function literal<const V extends string | number | boolean>(value: V): Schema<V> {
|
|
852
|
+
return make<V>({ kind: "literal", literalValue: value })
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* 枚举
|
|
857
|
+
* @param values 候选值
|
|
858
|
+
* @param labels 值到展示文案的映射
|
|
859
|
+
* @returns 枚举 schema
|
|
860
|
+
*/
|
|
861
|
+
function enumOf<const V extends readonly (string | number | boolean)[]>(
|
|
862
|
+
values: V,
|
|
863
|
+
labels?: Partial<Record<string, string>>
|
|
864
|
+
): Schema<V[number]> {
|
|
865
|
+
const items: SchemaEnumItem[] = values.map(value => {
|
|
866
|
+
const label = labels?.[String(value)]
|
|
867
|
+
return label === undefined ? { value } : { value, label }
|
|
868
|
+
})
|
|
869
|
+
return make<V[number]>({ kind: "enum", enumItems: items, meta: { widget: "select" } })
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* 带说明的枚举(WebUI 下拉框里显示 label 与 description)
|
|
874
|
+
* @param items 候选项
|
|
875
|
+
* @returns 枚举 schema
|
|
876
|
+
*/
|
|
877
|
+
function select<const V extends readonly SchemaEnumItem[]>(items: V): Schema<V[number]["value"]> {
|
|
878
|
+
return make<V[number]["value"]>({
|
|
879
|
+
kind: "enum",
|
|
880
|
+
enumItems: items.map(item => ({ ...item })),
|
|
881
|
+
meta: { widget: "select" }
|
|
882
|
+
})
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* 数组
|
|
887
|
+
* @param element 元素 schema
|
|
888
|
+
* @returns 数组 schema
|
|
889
|
+
*/
|
|
890
|
+
function array<S extends Schema<unknown>>(element: S): Schema<Infer<S>[]> {
|
|
891
|
+
return make<Infer<S>[]>({ kind: "array", element })
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* 对象
|
|
896
|
+
* @param shape 字段表
|
|
897
|
+
* @returns 对象 schema
|
|
898
|
+
*/
|
|
899
|
+
function object<S extends Shape>(shape: S): Schema<ObjectOutput<S>> {
|
|
900
|
+
return make<ObjectOutput<S>>({ kind: "object", shape })
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* 字典(键任意、值同构)
|
|
905
|
+
* @param valueSchema 值 schema
|
|
906
|
+
* @returns 字典 schema
|
|
907
|
+
*/
|
|
908
|
+
function record<S extends Schema<unknown>>(valueSchema: S): Schema<Record<string, Infer<S>>> {
|
|
909
|
+
return make<Record<string, Infer<S>>>({ kind: "record", valueSchema, meta: { widget: "keyValue" } })
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* 任意值(不校验)
|
|
914
|
+
*
|
|
915
|
+
* 兼容出口:插件存在确实无法描述的结构时使用,但 WebUI 仅能提供 JSON 编辑框。
|
|
916
|
+
* @returns 任意值 schema
|
|
917
|
+
*/
|
|
918
|
+
function unknownValue(): Schema<unknown> {
|
|
919
|
+
return make<unknown>({ kind: "unknown", meta: { widget: "code" } })
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* 时长(`"30s"` / `"7d"` / 毫秒数)
|
|
924
|
+
*
|
|
925
|
+
* 数值写法与字符串写法都原样保留,不互相转换 —— 见 `SchemaDef.allowNumber`。
|
|
926
|
+
* @returns 时长 schema
|
|
927
|
+
*/
|
|
928
|
+
function duration(): Schema<string | number> {
|
|
929
|
+
return make<string | number>({ kind: "string", meta: { widget: "duration" }, allowNumber: true }).check(
|
|
930
|
+
"时长格式应为毫秒数或形如 30s / 5m / 1h / 7d",
|
|
931
|
+
value => typeof value === "number" || parseDuration(value, Number.NaN) === parseDuration(value, 0)
|
|
932
|
+
)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* cron 表达式
|
|
937
|
+
* @returns cron schema
|
|
938
|
+
*/
|
|
939
|
+
function cron(): Schema<string> {
|
|
940
|
+
return make<string>({ kind: "string", meta: { widget: "cron" } }).check("cron 表达式应为 5 或 6 段", value => {
|
|
941
|
+
const parts = value.trim().split(/\s+/)
|
|
942
|
+
return parts.length === 5 || parts.length === 6
|
|
943
|
+
})
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* 端口号
|
|
948
|
+
* @returns 端口 schema
|
|
949
|
+
*/
|
|
950
|
+
function port(): Schema<number> {
|
|
951
|
+
return make<number>({ kind: "number", meta: { widget: "number" }, int: true, min: 1, max: 65535 })
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* 密码 / 令牌(自动脱敏)
|
|
956
|
+
* @returns 敏感字符串 schema
|
|
957
|
+
*/
|
|
958
|
+
function password(): Schema<string> {
|
|
959
|
+
return string().secret()
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* 多行文本
|
|
964
|
+
* @returns 文本域 schema
|
|
965
|
+
*/
|
|
966
|
+
function text(): Schema<string> {
|
|
967
|
+
return string().widget("textarea")
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* 字符串标签数组(WebUI 用 tag 输入框)
|
|
972
|
+
* @returns 标签数组 schema
|
|
973
|
+
*/
|
|
974
|
+
function tags(): Schema<string[]> {
|
|
975
|
+
return array(string()).widget("tags").single()
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* id 列表(QQ 号 / 群号,允许写单个值)
|
|
980
|
+
* @returns id 数组 schema
|
|
981
|
+
*/
|
|
982
|
+
function ids(): Schema<string[]> {
|
|
983
|
+
return array(string().pattern(/^\d{1,20}$/, "应为纯数字 id"))
|
|
984
|
+
.widget("tags")
|
|
985
|
+
.single()
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* 目录路径
|
|
990
|
+
* @returns 目录 schema
|
|
991
|
+
*/
|
|
992
|
+
function dir(): Schema<string> {
|
|
993
|
+
return string().widget("dir")
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* 文件路径
|
|
998
|
+
* @returns 文件 schema
|
|
999
|
+
*/
|
|
1000
|
+
function file(): Schema<string> {
|
|
1001
|
+
return string().widget("file")
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* schema 构造器集合
|
|
1006
|
+
*
|
|
1007
|
+
* 用法:
|
|
1008
|
+
* ```ts
|
|
1009
|
+
* const conf = s.object({
|
|
1010
|
+
* port: s.port().default(3000).title("监听端口"),
|
|
1011
|
+
* mode: s.enum(["ws", "ws-reverse"]).default("ws-reverse").title("连接方式")
|
|
1012
|
+
* })
|
|
1013
|
+
* type Conf = Infer<typeof conf>
|
|
1014
|
+
* ```
|
|
1015
|
+
*/
|
|
1016
|
+
export const s = {
|
|
1017
|
+
string,
|
|
1018
|
+
number,
|
|
1019
|
+
boolean,
|
|
1020
|
+
literal,
|
|
1021
|
+
enum: enumOf,
|
|
1022
|
+
select,
|
|
1023
|
+
array,
|
|
1024
|
+
object,
|
|
1025
|
+
record,
|
|
1026
|
+
unknown: unknownValue,
|
|
1027
|
+
duration,
|
|
1028
|
+
cron,
|
|
1029
|
+
port,
|
|
1030
|
+
password,
|
|
1031
|
+
text,
|
|
1032
|
+
tags,
|
|
1033
|
+
ids,
|
|
1034
|
+
dir,
|
|
1035
|
+
file
|
|
1036
|
+
} as const
|