@yunzai-ng/core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/core-config.d.ts +1 -1
- package/dist/store/sql.js +4 -4
- package/package.json +12 -2
- package/src/adapter/accounts.ts +1 -1
- package/src/adapter/bots.ts +1 -1
- package/src/adapter/host.ts +1 -1
- package/src/adapter/login.ts +1 -1
- package/src/kernel/app.ts +843 -843
- package/src/kernel/policy.ts +120 -120
- package/src/kernel/runtime.ts +1 -1
- package/src/pipeline/event.ts +1 -1
- package/src/platform/system.ts +1 -1
- package/src/plugin/market.ts +1 -1
- package/src/plugin/services.ts +1 -1
- package/src/render/registry.ts +1 -1
- package/src/scheduler/index.ts +1 -1
- package/src/server/auth.ts +1 -1
- package/src/server/files.ts +1 -1
- package/src/server/index.ts +1 -1
- package/src/store/index.ts +156 -156
- package/src/store/kv.ts +256 -256
- package/src/store/sql.ts +324 -324
package/src/kernel/policy.ts
CHANGED
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 模块职责:内核策略 —— 主人、命令前缀、昵称、维护模式
|
|
3
|
-
* 依赖方向:依赖 config/core-config 与类型包;不依赖任何子系统
|
|
4
|
-
* 生命周期:与应用同寿,无需回收
|
|
5
|
-
* 注意事项:所有取值均**即时读取配置**而不缓存,故改完主人号对下一条消息即生效,不必重启。
|
|
6
|
-
* `ConfigFile.get()` 在配置未变更时返回同一个对象,故"即时读取"只是一次属性访问。
|
|
7
|
-
*
|
|
8
|
-
* 此处刻意**不做鉴权**:`addMaster` 对任何调用方均开放。凭据校验是入口的职责
|
|
9
|
-
* (WebUI 经由令牌,命令经由 `isMaster`),策略层仅负责"名单的内容"。
|
|
10
|
-
* 两者混于一层将导致"经由修改配置绕过鉴权"这类漏洞。
|
|
11
|
-
*/
|
|
12
|
-
import type { PolicyView } from "@yunzai-ng/types"
|
|
13
|
-
import type { CoreConfigHandle } from "../config/core-config.js"
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 内核策略
|
|
17
|
-
*
|
|
18
|
-
* 实现类型包里的 `PolicyView`(适配器与插件只看得到那个只读接口),
|
|
19
|
-
* 另外多给内核自己用的几项:前缀、昵称、维护模式。
|
|
20
|
-
*/
|
|
21
|
-
export class KernelPolicy implements PolicyView {
|
|
22
|
-
/** 内核配置句柄 */
|
|
23
|
-
readonly #config: CoreConfigHandle
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @param config 内核配置句柄
|
|
27
|
-
*/
|
|
28
|
-
constructor(config: CoreConfigHandle) {
|
|
29
|
-
this.#config = config
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** 主人账号列表 */
|
|
33
|
-
get masters(): readonly string[] {
|
|
34
|
-
return this.#config.get().bot.masterQQ
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* 命令前缀
|
|
39
|
-
*
|
|
40
|
-
* 空数组表示不限制前缀。命令路由据此分桶,见 pipeline 层。
|
|
41
|
-
*/
|
|
42
|
-
get prefixes(): readonly string[] {
|
|
43
|
-
return this.#config.get().bot.prefix
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** 机器人昵称(群里以昵称开头等同于 @ 机器人) */
|
|
47
|
-
get nicknames(): readonly string[] {
|
|
48
|
-
return this.#config.get().bot.nickname
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** 是否忽略自身发出的消息 */
|
|
52
|
-
get ignoreSelf(): boolean {
|
|
53
|
-
return this.#config.get().bot.ignoreSelf
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** 是否处于维护模式(只响应主人) */
|
|
57
|
-
get maintenance(): boolean {
|
|
58
|
-
return this.#config.get().bot.onlyMaster
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 判断是否主人
|
|
63
|
-
*
|
|
64
|
-
* 主人名单为空时一律返回 false —— 不存在"没配主人就人人是主人"的默认,
|
|
65
|
-
* 那会让首次启动的机器人对全世界开放管理命令。
|
|
66
|
-
* @param uid 用户 id
|
|
67
|
-
* @returns 是否主人
|
|
68
|
-
*/
|
|
69
|
-
isMaster(uid: string): boolean {
|
|
70
|
-
if (uid === "") return false
|
|
71
|
-
return this.masters.includes(uid)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 判断该用户当前是否应被响应
|
|
76
|
-
*
|
|
77
|
-
* 维护模式下只放行主人。这是**唯一**的全局开关判定点,管线里不要再各自判一次。
|
|
78
|
-
* @param uid 用户 id
|
|
79
|
-
* @returns 是否响应
|
|
80
|
-
*/
|
|
81
|
-
canRespond(uid: string): boolean {
|
|
82
|
-
return !this.maintenance || this.isMaster(uid)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 添加主人并落盘
|
|
87
|
-
* @param uid 用户 id(纯数字)
|
|
88
|
-
* @returns 是否真的添加了(已在名单里时 false)
|
|
89
|
-
* @throws SchemaError uid 不是合法 id 时;此时配置与文件都不变
|
|
90
|
-
*/
|
|
91
|
-
async addMaster(uid: string): Promise<boolean> {
|
|
92
|
-
const current = this.masters
|
|
93
|
-
if (current.includes(uid)) return false
|
|
94
|
-
// 整个数组一起写:deepMerge 对数组是整体替换而非追加,
|
|
95
|
-
// 只传新增项会把原有主人全冲掉
|
|
96
|
-
await this.#config.patch({ bot: { masterQQ: [...current, uid] } })
|
|
97
|
-
return true
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* 移除主人并落盘
|
|
102
|
-
* @param uid 用户 id
|
|
103
|
-
* @returns 是否真的移除了
|
|
104
|
-
*/
|
|
105
|
-
async removeMaster(uid: string): Promise<boolean> {
|
|
106
|
-
const current = this.masters
|
|
107
|
-
if (!current.includes(uid)) return false
|
|
108
|
-
await this.#config.patch({ bot: { masterQQ: current.filter(id => id !== uid) } })
|
|
109
|
-
return true
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* 创建内核策略
|
|
115
|
-
* @param config 内核配置句柄
|
|
116
|
-
* @returns 内核策略
|
|
117
|
-
*/
|
|
118
|
-
export function createPolicy(config: CoreConfigHandle): KernelPolicy {
|
|
119
|
-
return new KernelPolicy(config)
|
|
120
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:内核策略 —— 主人、命令前缀、昵称、维护模式
|
|
3
|
+
* 依赖方向:依赖 config/core-config 与类型包;不依赖任何子系统
|
|
4
|
+
* 生命周期:与应用同寿,无需回收
|
|
5
|
+
* 注意事项:所有取值均**即时读取配置**而不缓存,故改完主人号对下一条消息即生效,不必重启。
|
|
6
|
+
* `ConfigFile.get()` 在配置未变更时返回同一个对象,故"即时读取"只是一次属性访问。
|
|
7
|
+
*
|
|
8
|
+
* 此处刻意**不做鉴权**:`addMaster` 对任何调用方均开放。凭据校验是入口的职责
|
|
9
|
+
* (WebUI 经由令牌,命令经由 `isMaster`),策略层仅负责"名单的内容"。
|
|
10
|
+
* 两者混于一层将导致"经由修改配置绕过鉴权"这类漏洞。
|
|
11
|
+
*/
|
|
12
|
+
import type { PolicyView } from "@yunzai-ng/types"
|
|
13
|
+
import type { CoreConfigHandle } from "../config/core-config.js"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 内核策略
|
|
17
|
+
*
|
|
18
|
+
* 实现类型包里的 `PolicyView`(适配器与插件只看得到那个只读接口),
|
|
19
|
+
* 另外多给内核自己用的几项:前缀、昵称、维护模式。
|
|
20
|
+
*/
|
|
21
|
+
export class KernelPolicy implements PolicyView {
|
|
22
|
+
/** 内核配置句柄 */
|
|
23
|
+
readonly #config: CoreConfigHandle
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param config 内核配置句柄
|
|
27
|
+
*/
|
|
28
|
+
constructor(config: CoreConfigHandle) {
|
|
29
|
+
this.#config = config
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 主人账号列表 */
|
|
33
|
+
get masters(): readonly string[] {
|
|
34
|
+
return this.#config.get().bot.masterQQ
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 命令前缀
|
|
39
|
+
*
|
|
40
|
+
* 空数组表示不限制前缀。命令路由据此分桶,见 pipeline 层。
|
|
41
|
+
*/
|
|
42
|
+
get prefixes(): readonly string[] {
|
|
43
|
+
return this.#config.get().bot.prefix
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 机器人昵称(群里以昵称开头等同于 @ 机器人) */
|
|
47
|
+
get nicknames(): readonly string[] {
|
|
48
|
+
return this.#config.get().bot.nickname
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 是否忽略自身发出的消息 */
|
|
52
|
+
get ignoreSelf(): boolean {
|
|
53
|
+
return this.#config.get().bot.ignoreSelf
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 是否处于维护模式(只响应主人) */
|
|
57
|
+
get maintenance(): boolean {
|
|
58
|
+
return this.#config.get().bot.onlyMaster
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 判断是否主人
|
|
63
|
+
*
|
|
64
|
+
* 主人名单为空时一律返回 false —— 不存在"没配主人就人人是主人"的默认,
|
|
65
|
+
* 那会让首次启动的机器人对全世界开放管理命令。
|
|
66
|
+
* @param uid 用户 id
|
|
67
|
+
* @returns 是否主人
|
|
68
|
+
*/
|
|
69
|
+
isMaster(uid: string): boolean {
|
|
70
|
+
if (uid === "") return false
|
|
71
|
+
return this.masters.includes(uid)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 判断该用户当前是否应被响应
|
|
76
|
+
*
|
|
77
|
+
* 维护模式下只放行主人。这是**唯一**的全局开关判定点,管线里不要再各自判一次。
|
|
78
|
+
* @param uid 用户 id
|
|
79
|
+
* @returns 是否响应
|
|
80
|
+
*/
|
|
81
|
+
canRespond(uid: string): boolean {
|
|
82
|
+
return !this.maintenance || this.isMaster(uid)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 添加主人并落盘
|
|
87
|
+
* @param uid 用户 id(纯数字)
|
|
88
|
+
* @returns 是否真的添加了(已在名单里时 false)
|
|
89
|
+
* @throws SchemaError uid 不是合法 id 时;此时配置与文件都不变
|
|
90
|
+
*/
|
|
91
|
+
async addMaster(uid: string): Promise<boolean> {
|
|
92
|
+
const current = this.masters
|
|
93
|
+
if (current.includes(uid)) return false
|
|
94
|
+
// 整个数组一起写:deepMerge 对数组是整体替换而非追加,
|
|
95
|
+
// 只传新增项会把原有主人全冲掉
|
|
96
|
+
await this.#config.patch({ bot: { masterQQ: [...current, uid] } })
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 移除主人并落盘
|
|
102
|
+
* @param uid 用户 id
|
|
103
|
+
* @returns 是否真的移除了
|
|
104
|
+
*/
|
|
105
|
+
async removeMaster(uid: string): Promise<boolean> {
|
|
106
|
+
const current = this.masters
|
|
107
|
+
if (!current.includes(uid)) return false
|
|
108
|
+
await this.#config.patch({ bot: { masterQQ: current.filter(id => id !== uid) } })
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 创建内核策略
|
|
115
|
+
* @param config 内核配置句柄
|
|
116
|
+
* @returns 内核策略
|
|
117
|
+
*/
|
|
118
|
+
export function createPolicy(config: CoreConfigHandle): KernelPolicy {
|
|
119
|
+
return new KernelPolicy(config)
|
|
120
|
+
}
|
package/src/kernel/runtime.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* **配置一律取 getter 或取值函数,不缓存快照。** 面板改了 `message.splitLength` 应当自下
|
|
18
18
|
* 一条消息即生效。唯一的例外是 `message.concurrency` —— 信号量容量在创建时固定,这一点
|
|
19
|
-
* 已写进它的配置说明。
|
|
19
|
+
* 已写进它的配置说明。
|
|
20
20
|
*/
|
|
21
21
|
import type { Disposer, Logger } from "@yunzai-ng/types"
|
|
22
22
|
import { AccountManager } from "../adapter/accounts.js"
|
package/src/pipeline/event.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 实例**刻意不 freeze、不 seal**:`EventExtensions` 的正规用法就是插件在事件上挂自有字段。
|
|
13
13
|
*
|
|
14
14
|
* `e.render()` 依赖「当前执行的是哪个插件」(模板根随插件而定),故该绑定由 dispatch 在调
|
|
15
|
-
* 每个 handler 之前经 `bind()` 替换,而不写进事件的构造参数 —— 一条消息会依次流经多个插件。
|
|
15
|
+
* 每个 handler 之前经 `bind()` 替换,而不写进事件的构造参数 —— 一条消息会依次流经多个插件。
|
|
16
16
|
*/
|
|
17
17
|
import type {
|
|
18
18
|
BotApi,
|
package/src/platform/system.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 返回空数组:空数组会被前端读成「确实有 0 块显卡」,而 0% 会被读成「GPU 空闲」。
|
|
18
18
|
*
|
|
19
19
|
* **`nvidia-smi` 不存在这件事只查一次。** 没有 N 卡的机器上每 5 秒 spawn 一个必然
|
|
20
|
-
* ENOENT 的进程,一天是一万七千次。
|
|
20
|
+
* ENOENT 的进程,一天是一万七千次。
|
|
21
21
|
*/
|
|
22
22
|
import { constants as fsConstants } from "node:fs"
|
|
23
23
|
import { access, readFile, statfs } from "node:fs/promises"
|
package/src/plugin/market.ts
CHANGED
|
@@ -623,7 +623,7 @@ export class PluginMarket {
|
|
|
623
623
|
await this.#move(root, target)
|
|
624
624
|
this.#deps.logger.info(`插件 ${name}@${version} 已安装至 ${target}`)
|
|
625
625
|
if (needsDependencies) this.#deps.logger.warn(`插件 ${name} 声明了运行时依赖,需在其目录内自行执行包管理器安装`)
|
|
626
|
-
// 带 `.git` 的目录此后可就地拉取;归档装出来的每次更新都要整目录重下
|
|
626
|
+
// 带 `.git` 的目录此后可就地拉取;归档装出来的每次更新都要整目录重下
|
|
627
627
|
return { name, dir: target, via, version, needsDependencies, updatable: via === "git" ? "pull" : "reinstall" }
|
|
628
628
|
} finally {
|
|
629
629
|
await rm(staging, { recursive: true, force: true })
|
package/src/plugin/services.ts
CHANGED
package/src/render/registry.ts
CHANGED
package/src/scheduler/index.ts
CHANGED
package/src/server/auth.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 等于把「长度对不对」变成一个旁路信号;先摘要则两边恒为 32 字节。
|
|
17
17
|
*
|
|
18
18
|
* **没设令牌时只放行本机**,判据是 TCP 对端地址,且服务器强制 `trustProxy: false` ——
|
|
19
|
-
* 否则任何人都能靠一个 `X-Forwarded-For: 127.0.0.1` 把自己伪装成本机。
|
|
19
|
+
* 否则任何人都能靠一个 `X-Forwarded-For: 127.0.0.1` 把自己伪装成本机。
|
|
20
20
|
*/
|
|
21
21
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto"
|
|
22
22
|
|
package/src/server/files.ts
CHANGED
package/src/server/index.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 等进了 `wsHandler` 再关连接,对方只看到一次没有理由的断线。
|
|
18
18
|
*
|
|
19
19
|
* **静态资源刻意不鉴权。** 浏览器请求文档时设不了请求头,要鉴权就只剩 Cookie 一条路,
|
|
20
|
-
* 那会把 auth.ts 刻意避开的 CSRF 面重新引进来。HTML/JS 本身不是机密,要保护的是 API。
|
|
20
|
+
* 那会把 auth.ts 刻意避开的 CSRF 面重新引进来。HTML/JS 本身不是机密,要保护的是 API。
|
|
21
21
|
*/
|
|
22
22
|
import { isAbsolute } from "node:path"
|
|
23
23
|
import type { IncomingMessage } from "node:http"
|
package/src/store/index.ts
CHANGED
|
@@ -1,156 +1,156 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 模块职责:KV 存储的装配(驱动选择、自动降级、根命名空间)
|
|
3
|
-
* 依赖方向:依赖 store/{kv,memory,json,level}、util/fs、类型包
|
|
4
|
-
* 生命周期:应用级单例,`close()` 时落盘
|
|
5
|
-
* 注意事项:**存储不可用不等于机器人不可用。** `auto` 模式按 level → json → memory 逐级降级,
|
|
6
|
-
* 每次降级记一条 warn 说明原因;最坏退到纯内存(重启丢缓存),但机器人始终能收发消息。
|
|
7
|
-
*
|
|
8
|
-
* 外部注册的驱动(如 store-redis 插件)通过 `registerKvDriver()` 挂进来,
|
|
9
|
-
* 与内置驱动一视同仁 —— 这正是"一切皆可为插件"在存储层的落点。
|
|
10
|
-
*/
|
|
11
|
-
import { join } from "node:path"
|
|
12
|
-
import type { KvDriver, KvNamespace, Logger } from "@yunzai-ng/types"
|
|
13
|
-
import { Kv } from "./kv.js"
|
|
14
|
-
import { MemoryKvDriver } from "./memory.js"
|
|
15
|
-
import { JsonKvDriver } from "./json.js"
|
|
16
|
-
import { LevelKvDriver, loadLevel } from "./level.js"
|
|
17
|
-
|
|
18
|
-
/** 内置驱动 id */
|
|
19
|
-
export type BuiltinDriverId = "auto" | "level" | "json" | "memory"
|
|
20
|
-
|
|
21
|
-
/** 驱动工厂:拿到数据目录,产出一个尚未 open 的驱动 */
|
|
22
|
-
export type KvDriverFactory = (dir: string) => Promise<KvDriver> | KvDriver
|
|
23
|
-
|
|
24
|
-
/** 外部注册的驱动表(插件注册的驱动进这里) */
|
|
25
|
-
const externalDrivers = new Map<string, KvDriverFactory>()
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 注册一个 KV 驱动
|
|
29
|
-
*
|
|
30
|
-
* 供 `store-redis` 这类插件调用;配置里把 `store.driver` 写成同一个 id 即可启用。
|
|
31
|
-
* @param id 驱动 id
|
|
32
|
-
* @param factory 驱动工厂
|
|
33
|
-
* @returns 取消注册
|
|
34
|
-
* @throws id 与内置驱动冲突时
|
|
35
|
-
*/
|
|
36
|
-
export function registerKvDriver(id: string, factory: KvDriverFactory): () => void {
|
|
37
|
-
if (id === "auto" || id === "level" || id === "json" || id === "memory") {
|
|
38
|
-
throw new Error(`驱动 id ${id} 与内置驱动冲突`)
|
|
39
|
-
}
|
|
40
|
-
externalDrivers.set(id, factory)
|
|
41
|
-
return () => void externalDrivers.delete(id)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** 打开 KV 存储的参数 */
|
|
45
|
-
export interface OpenKvOptions {
|
|
46
|
-
/** 数据目录(KV 数据会放在其子目录里) */
|
|
47
|
-
dir: string
|
|
48
|
-
/** 驱动 id,缺省 `auto` */
|
|
49
|
-
driver?: string
|
|
50
|
-
/** 日志器 */
|
|
51
|
-
logger: Logger
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** 已打开的 KV 存储 */
|
|
55
|
-
export interface KvStore {
|
|
56
|
-
/** 实际生效的驱动 id(`auto` 会被解析为具体驱动) */
|
|
57
|
-
readonly driver: string
|
|
58
|
-
/** 根命名空间 */
|
|
59
|
-
readonly root: KvNamespace
|
|
60
|
-
/**
|
|
61
|
-
* 取一个子命名空间
|
|
62
|
-
* @param name 命名空间名
|
|
63
|
-
* @returns KV 视图
|
|
64
|
-
*/
|
|
65
|
-
namespace(name: string): KvNamespace
|
|
66
|
-
/** 关闭并落盘 */
|
|
67
|
-
close(): Promise<void>
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 打开 KV 存储
|
|
72
|
-
*
|
|
73
|
-
* `auto` 的降级顺序:level(性能最优)→ json(纯 JS,Termux 兜底)→ memory。
|
|
74
|
-
* 显式指定驱动时同样会降级,但会将"所指定的驱动为何不可用"记为 warn,
|
|
75
|
-
* 而非静默替换 —— 使用者配置了 redis 却运行于内存驱动之上而不知情是最糟的情形。
|
|
76
|
-
* @param opts 参数
|
|
77
|
-
* @returns 已打开的 KV 存储
|
|
78
|
-
*/
|
|
79
|
-
export async function openKv(opts: OpenKvOptions): Promise<KvStore> {
|
|
80
|
-
const logger = opts.logger.child({ scope: "store" })
|
|
81
|
-
const requested = opts.driver ?? "auto"
|
|
82
|
-
const chain = resolveChain(requested)
|
|
83
|
-
|
|
84
|
-
let driver: KvDriver | undefined
|
|
85
|
-
for (const id of chain) {
|
|
86
|
-
try {
|
|
87
|
-
const candidate = await createDriver(id, opts.dir)
|
|
88
|
-
if (!candidate) {
|
|
89
|
-
logger.warn(`KV 驱动 ${id} 不可用(未安装或未注册),尝试下一个`)
|
|
90
|
-
continue
|
|
91
|
-
}
|
|
92
|
-
await candidate.open()
|
|
93
|
-
driver = candidate
|
|
94
|
-
if (id !== requested && requested !== "auto") {
|
|
95
|
-
logger.warn(`配置指定的 KV 驱动 ${requested} 不可用,已降级为 ${id}`)
|
|
96
|
-
}
|
|
97
|
-
break
|
|
98
|
-
} catch (err) {
|
|
99
|
-
logger.warn(`KV 驱动 ${id} 打开失败,尝试下一个`, err)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (!driver) {
|
|
104
|
-
// chain 末尾恒为 memory,理论上到不了这里;留着是为了让失败可诊断而不是空指针
|
|
105
|
-
throw new Error("所有 KV 驱动均不可用")
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
logger.info(`KV 存储就绪:驱动 ${driver.id}`)
|
|
109
|
-
const root = new Kv(driver)
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
driver: driver.id,
|
|
113
|
-
root,
|
|
114
|
-
namespace: (name: string) => root.sub(name),
|
|
115
|
-
close: async () => {
|
|
116
|
-
await driver.close()
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* 把请求的驱动 id 展开成降级链
|
|
123
|
-
* @param requested 请求的驱动 id
|
|
124
|
-
* @returns 依次尝试的驱动 id 列表,末尾恒为 memory
|
|
125
|
-
*/
|
|
126
|
-
function resolveChain(requested: string): string[] {
|
|
127
|
-
if (requested === "auto") return ["level", "json", "memory"]
|
|
128
|
-
if (requested === "memory") return ["memory"]
|
|
129
|
-
if (requested === "json") return ["json", "memory"]
|
|
130
|
-
if (requested === "level") return ["level", "json", "memory"]
|
|
131
|
-
// 外部驱动(redis 等)失败后退回内置默认链
|
|
132
|
-
return [requested, "level", "json", "memory"]
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* 按 id 造驱动
|
|
137
|
-
* @param id 驱动 id
|
|
138
|
-
* @param dir 数据目录
|
|
139
|
-
* @returns 驱动实例;不可用时 undefined
|
|
140
|
-
*/
|
|
141
|
-
async function createDriver(id: string, dir: string): Promise<KvDriver | undefined> {
|
|
142
|
-
switch (id) {
|
|
143
|
-
case "memory":
|
|
144
|
-
return new MemoryKvDriver()
|
|
145
|
-
case "json":
|
|
146
|
-
return new JsonKvDriver(join(dir, "kv.json"))
|
|
147
|
-
case "level": {
|
|
148
|
-
const ctor = await loadLevel()
|
|
149
|
-
return ctor ? new LevelKvDriver(join(dir, "kv"), ctor) : undefined
|
|
150
|
-
}
|
|
151
|
-
default: {
|
|
152
|
-
const factory = externalDrivers.get(id)
|
|
153
|
-
return factory ? await factory(dir) : undefined
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:KV 存储的装配(驱动选择、自动降级、根命名空间)
|
|
3
|
+
* 依赖方向:依赖 store/{kv,memory,json,level}、util/fs、类型包
|
|
4
|
+
* 生命周期:应用级单例,`close()` 时落盘
|
|
5
|
+
* 注意事项:**存储不可用不等于机器人不可用。** `auto` 模式按 level → json → memory 逐级降级,
|
|
6
|
+
* 每次降级记一条 warn 说明原因;最坏退到纯内存(重启丢缓存),但机器人始终能收发消息。
|
|
7
|
+
*
|
|
8
|
+
* 外部注册的驱动(如 store-redis 插件)通过 `registerKvDriver()` 挂进来,
|
|
9
|
+
* 与内置驱动一视同仁 —— 这正是"一切皆可为插件"在存储层的落点。
|
|
10
|
+
*/
|
|
11
|
+
import { join } from "node:path"
|
|
12
|
+
import type { KvDriver, KvNamespace, Logger } from "@yunzai-ng/types"
|
|
13
|
+
import { Kv } from "./kv.js"
|
|
14
|
+
import { MemoryKvDriver } from "./memory.js"
|
|
15
|
+
import { JsonKvDriver } from "./json.js"
|
|
16
|
+
import { LevelKvDriver, loadLevel } from "./level.js"
|
|
17
|
+
|
|
18
|
+
/** 内置驱动 id */
|
|
19
|
+
export type BuiltinDriverId = "auto" | "level" | "json" | "memory"
|
|
20
|
+
|
|
21
|
+
/** 驱动工厂:拿到数据目录,产出一个尚未 open 的驱动 */
|
|
22
|
+
export type KvDriverFactory = (dir: string) => Promise<KvDriver> | KvDriver
|
|
23
|
+
|
|
24
|
+
/** 外部注册的驱动表(插件注册的驱动进这里) */
|
|
25
|
+
const externalDrivers = new Map<string, KvDriverFactory>()
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 注册一个 KV 驱动
|
|
29
|
+
*
|
|
30
|
+
* 供 `store-redis` 这类插件调用;配置里把 `store.driver` 写成同一个 id 即可启用。
|
|
31
|
+
* @param id 驱动 id
|
|
32
|
+
* @param factory 驱动工厂
|
|
33
|
+
* @returns 取消注册
|
|
34
|
+
* @throws id 与内置驱动冲突时
|
|
35
|
+
*/
|
|
36
|
+
export function registerKvDriver(id: string, factory: KvDriverFactory): () => void {
|
|
37
|
+
if (id === "auto" || id === "level" || id === "json" || id === "memory") {
|
|
38
|
+
throw new Error(`驱动 id ${id} 与内置驱动冲突`)
|
|
39
|
+
}
|
|
40
|
+
externalDrivers.set(id, factory)
|
|
41
|
+
return () => void externalDrivers.delete(id)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 打开 KV 存储的参数 */
|
|
45
|
+
export interface OpenKvOptions {
|
|
46
|
+
/** 数据目录(KV 数据会放在其子目录里) */
|
|
47
|
+
dir: string
|
|
48
|
+
/** 驱动 id,缺省 `auto` */
|
|
49
|
+
driver?: string
|
|
50
|
+
/** 日志器 */
|
|
51
|
+
logger: Logger
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 已打开的 KV 存储 */
|
|
55
|
+
export interface KvStore {
|
|
56
|
+
/** 实际生效的驱动 id(`auto` 会被解析为具体驱动) */
|
|
57
|
+
readonly driver: string
|
|
58
|
+
/** 根命名空间 */
|
|
59
|
+
readonly root: KvNamespace
|
|
60
|
+
/**
|
|
61
|
+
* 取一个子命名空间
|
|
62
|
+
* @param name 命名空间名
|
|
63
|
+
* @returns KV 视图
|
|
64
|
+
*/
|
|
65
|
+
namespace(name: string): KvNamespace
|
|
66
|
+
/** 关闭并落盘 */
|
|
67
|
+
close(): Promise<void>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 打开 KV 存储
|
|
72
|
+
*
|
|
73
|
+
* `auto` 的降级顺序:level(性能最优)→ json(纯 JS,Termux 兜底)→ memory。
|
|
74
|
+
* 显式指定驱动时同样会降级,但会将"所指定的驱动为何不可用"记为 warn,
|
|
75
|
+
* 而非静默替换 —— 使用者配置了 redis 却运行于内存驱动之上而不知情是最糟的情形。
|
|
76
|
+
* @param opts 参数
|
|
77
|
+
* @returns 已打开的 KV 存储
|
|
78
|
+
*/
|
|
79
|
+
export async function openKv(opts: OpenKvOptions): Promise<KvStore> {
|
|
80
|
+
const logger = opts.logger.child({ scope: "store" })
|
|
81
|
+
const requested = opts.driver ?? "auto"
|
|
82
|
+
const chain = resolveChain(requested)
|
|
83
|
+
|
|
84
|
+
let driver: KvDriver | undefined
|
|
85
|
+
for (const id of chain) {
|
|
86
|
+
try {
|
|
87
|
+
const candidate = await createDriver(id, opts.dir)
|
|
88
|
+
if (!candidate) {
|
|
89
|
+
logger.warn(`KV 驱动 ${id} 不可用(未安装或未注册),尝试下一个`)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
await candidate.open()
|
|
93
|
+
driver = candidate
|
|
94
|
+
if (id !== requested && requested !== "auto") {
|
|
95
|
+
logger.warn(`配置指定的 KV 驱动 ${requested} 不可用,已降级为 ${id}`)
|
|
96
|
+
}
|
|
97
|
+
break
|
|
98
|
+
} catch (err) {
|
|
99
|
+
logger.warn(`KV 驱动 ${id} 打开失败,尝试下一个`, err)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!driver) {
|
|
104
|
+
// chain 末尾恒为 memory,理论上到不了这里;留着是为了让失败可诊断而不是空指针
|
|
105
|
+
throw new Error("所有 KV 驱动均不可用")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
logger.info(`KV 存储就绪:驱动 ${driver.id}`)
|
|
109
|
+
const root = new Kv(driver)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
driver: driver.id,
|
|
113
|
+
root,
|
|
114
|
+
namespace: (name: string) => root.sub(name),
|
|
115
|
+
close: async () => {
|
|
116
|
+
await driver.close()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 把请求的驱动 id 展开成降级链
|
|
123
|
+
* @param requested 请求的驱动 id
|
|
124
|
+
* @returns 依次尝试的驱动 id 列表,末尾恒为 memory
|
|
125
|
+
*/
|
|
126
|
+
function resolveChain(requested: string): string[] {
|
|
127
|
+
if (requested === "auto") return ["level", "json", "memory"]
|
|
128
|
+
if (requested === "memory") return ["memory"]
|
|
129
|
+
if (requested === "json") return ["json", "memory"]
|
|
130
|
+
if (requested === "level") return ["level", "json", "memory"]
|
|
131
|
+
// 外部驱动(redis 等)失败后退回内置默认链
|
|
132
|
+
return [requested, "level", "json", "memory"]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 按 id 造驱动
|
|
137
|
+
* @param id 驱动 id
|
|
138
|
+
* @param dir 数据目录
|
|
139
|
+
* @returns 驱动实例;不可用时 undefined
|
|
140
|
+
*/
|
|
141
|
+
async function createDriver(id: string, dir: string): Promise<KvDriver | undefined> {
|
|
142
|
+
switch (id) {
|
|
143
|
+
case "memory":
|
|
144
|
+
return new MemoryKvDriver()
|
|
145
|
+
case "json":
|
|
146
|
+
return new JsonKvDriver(join(dir, "kv.json"))
|
|
147
|
+
case "level": {
|
|
148
|
+
const ctor = await loadLevel()
|
|
149
|
+
return ctor ? new LevelKvDriver(join(dir, "kv"), ctor) : undefined
|
|
150
|
+
}
|
|
151
|
+
default: {
|
|
152
|
+
const factory = externalDrivers.get(id)
|
|
153
|
+
return factory ? await factory(dir) : undefined
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|