@yunzai-ng/core 0.1.0 → 0.2.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.
@@ -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
+ }
@@ -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"
@@ -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,
@@ -0,0 +1,166 @@
1
+ /**
2
+ * 模块职责:主目录探测顺序的测试
3
+ * 依赖方向:测试文件,依赖 platform/paths.ts
4
+ * 生命周期:每个用例一个临时目录,用后即弃
5
+ * 注意事项:本文件固定的是「数据放在哪里」这一条 —— 它错了的表现不是报错,而是
6
+ * 使用者的配置、账号与历史记录**看起来凭空消失**(实则仍在旧位置)。
7
+ * 因此这里既覆盖「新装落在当前目录」,也覆盖「既有安装不被静默搬家」。
8
+ *
9
+ * 用 `process.chdir` 而非注入一个 cwd 参数:被测对象正是「读取进程工作目录」
10
+ * 这一行为,参数化会把它测成一个纯函数,那样恰好绕过了要测的东西。
11
+ * vitest 的 forks 池令每个测试文件独占一个进程,chdir 不会波及其他文件。
12
+ */
13
+ import { mkdtemp, mkdir, writeFile } from "node:fs/promises"
14
+ import { tmpdir } from "node:os"
15
+ import { join, resolve } from "node:path"
16
+ import { afterEach, beforeEach, describe, expect, it } from "vitest"
17
+ import { legacyInstance, resolvePaths } from "./paths.js"
18
+
19
+ /** 进入用例前的工作目录与环境变量,用后还原 */
20
+ let origin: string
21
+ let env: NodeJS.ProcessEnv
22
+ /** 本用例的临时根目录 */
23
+ let base: string
24
+
25
+ /**
26
+ * 推出 `defaultHome()` 在本平台会给出的位置
27
+ *
28
+ * 仅 Windows 与 Linux 可经环境变量改写;macOS 固定在 `~/Library`,无从在测试中挪动,
29
+ * 故相应用例在该平台上跳过。
30
+ * @param root 充当 `LOCALAPPDATA` / `XDG_DATA_HOME` 的目录
31
+ * @returns 系统默认位置;本平台无法改写时 undefined
32
+ */
33
+ function legacyHomeFor(root: string): string | undefined {
34
+ if (process.platform === "win32") return join(root, "YunzaiNG")
35
+ if (process.platform === "linux") return join(root, "yunzai-ng")
36
+ return undefined
37
+ }
38
+
39
+ beforeEach(async () => {
40
+ origin = process.cwd()
41
+ env = { ...process.env }
42
+ base = await mkdtemp(join(tmpdir(), "yzng-paths-"))
43
+ // 三者都会参与探测,逐一清掉,否则用例会读到开发机上真实的取值
44
+ delete process.env["YZNG_HOME"]
45
+ delete process.env["LOCALAPPDATA"]
46
+ delete process.env["XDG_DATA_HOME"]
47
+ })
48
+
49
+ afterEach(() => {
50
+ process.chdir(origin)
51
+ process.env = env
52
+ })
53
+
54
+ describe("主目录探测", () => {
55
+ it("什么都不给时落在当前目录", async () => {
56
+ const dir = join(base, "新装")
57
+ await mkdir(dir)
58
+ process.chdir(dir)
59
+
60
+ // resolve 而非直接比较:macOS 的 /var 是 /private/var 的符号链接,
61
+ // mkdtemp 给出前者而 cwd 给出后者
62
+ expect(resolvePaths().home).toBe(resolve(process.cwd()))
63
+ expect(resolvePaths().config).toBe(join(resolve(process.cwd()), "config"))
64
+ })
65
+
66
+ it("显式参数为相对路径时按当前目录解析", async () => {
67
+ process.chdir(base)
68
+ const got = resolvePaths({ home: "./实例甲" }).home
69
+
70
+ expect(got).toBe(join(resolve(base), "实例甲"))
71
+ })
72
+
73
+ it("YZNG_HOME 压过当前目录", async () => {
74
+ const other = join(base, "另一处")
75
+ await mkdir(other)
76
+ process.chdir(base)
77
+ process.env["YZNG_HOME"] = other
78
+
79
+ expect(resolvePaths().home).toBe(resolve(other))
80
+ })
81
+
82
+ it("显式参数压过 YZNG_HOME", async () => {
83
+ const a = join(base, "参数")
84
+ const b = join(base, "环境变量")
85
+ await mkdir(a)
86
+ await mkdir(b)
87
+ process.chdir(base)
88
+ process.env["YZNG_HOME"] = b
89
+
90
+ expect(resolvePaths({ home: a }).home).toBe(resolve(a))
91
+ })
92
+
93
+ it("便携标记压过当前目录", async () => {
94
+ const install = join(base, "便携根")
95
+ // runtime 传 <install>/node_modules/@yunzai-ng/core,与真实安装的层级一致:
96
+ // findPortableRoot 自该处上溯三级才回到 install
97
+ const runtime = join(install, "node_modules", "@yunzai-ng", "core")
98
+ await mkdir(runtime, { recursive: true })
99
+ await writeFile(join(install, ".portable"), "")
100
+ const cwd = join(base, "别处")
101
+ await mkdir(cwd)
102
+ process.chdir(cwd)
103
+
104
+ expect(resolvePaths({ runtime }).home).toBe(resolve(install))
105
+ })
106
+ })
107
+
108
+ describe("旧默认位置上的实例", () => {
109
+ /** 在 `dir` 下造出一个看起来已在用的实例 */
110
+ const makeInstance = async (dir: string): Promise<void> => {
111
+ await mkdir(join(dir, "config"), { recursive: true })
112
+ await writeFile(join(dir, "config", "core.yaml"), "server:\n port: 2536\n")
113
+ }
114
+
115
+ /**
116
+ * 把系统默认位置指到临时目录下
117
+ * @returns 系统默认位置;本平台无法改写时 undefined
118
+ */
119
+ const pointLegacyAt = (): string | undefined => {
120
+ const root = join(base, "系统默认位置")
121
+ const legacy = legacyHomeFor(root)
122
+ if (legacy === undefined) return undefined
123
+ if (process.platform === "win32") process.env["LOCALAPPDATA"] = root
124
+ else process.env["XDG_DATA_HOME"] = root
125
+ return legacy
126
+ }
127
+
128
+ it.skipIf(legacyHomeFor("x") === undefined)("旧位置有实例也不影响主目录 —— 不做静默回落", async () => {
129
+ const legacy = pointLegacyAt()
130
+ if (legacy === undefined) return
131
+ await makeInstance(legacy)
132
+ const cwd = join(base, "空目录")
133
+ await mkdir(cwd)
134
+ process.chdir(cwd)
135
+
136
+ // 回落会让「默认在当前目录」在任何装过旧版的机器上都不成立
137
+ expect(resolvePaths().home).toBe(resolve(cwd))
138
+ })
139
+
140
+ it.skipIf(legacyHomeFor("x") === undefined)("legacyInstance 报出旧实例,供 CLI 提示", async () => {
141
+ const legacy = pointLegacyAt()
142
+ if (legacy === undefined) return
143
+ await makeInstance(legacy)
144
+ const cwd = join(base, "新家")
145
+ await mkdir(cwd)
146
+
147
+ expect(legacyInstance(cwd)).toBe(legacy)
148
+ })
149
+
150
+ it.skipIf(legacyHomeFor("x") === undefined)("旧位置没有实例时不提示", async () => {
151
+ const legacy = pointLegacyAt()
152
+ if (legacy === undefined) return
153
+ // 目录存在但没有 config/:这是"随手建过一个空目录",不是一个实例
154
+ await mkdir(legacy, { recursive: true })
155
+
156
+ expect(legacyInstance(join(base, "新家"))).toBeUndefined()
157
+ })
158
+
159
+ it.skipIf(legacyHomeFor("x") === undefined)("本次用的就是旧位置时不提示 —— 没有可搬的家", async () => {
160
+ const legacy = pointLegacyAt()
161
+ if (legacy === undefined) return
162
+ await makeInstance(legacy)
163
+
164
+ expect(legacyInstance(legacy)).toBeUndefined()
165
+ })
166
+ })
@@ -2,13 +2,18 @@
2
2
  * 模块职责:确定并创建运行时目录布局
3
3
  * 依赖方向:依赖 util/fs、platform/detect
4
4
  * 生命周期:启动时解析一次,之后只读
5
- * 注意事项:全程不使用 `process.cwd()` —— Windows 服务、开机自启或 pm2 启动时
6
- * 工作目录并非项目目录。启动时把八个目录一次性定死成绝对路径,
7
- * 其余模块只准通过 `app.paths` 取。
5
+ * 注意事项:优先级:显式参数 > `YZNG_HOME` > 便携模式标记 > 当前目录。
6
+ * 启动时把八个目录一次性定死成绝对路径,其余模块只准通过 `app.paths` 取。
8
7
  *
9
- * 优先级:显式参数 > `YZNG_HOME` 环境变量 > 便携模式标记 > 系统默认位置。
10
- * 便携模式(安装目录下有 `.portable` 文件)让 ZIP 解压即用、
11
- * 换机拷走整个文件夹就能迁移 —— Windows 用户最常见的诉求。
8
+ * **默认落在当前目录**:解压或克隆到一个文件夹、在其中 `yzng init`,数据就在
9
+ * 眼前,拷走整个文件夹即完成迁移。代价是本模块必须读 `process.cwd()`,而以
10
+ * Windows 服务、开机自启或 pm2 启动时工作目录并非项目目录 —— 那些场景必须
11
+ * 显式给出 `YZNG_HOME` 或启动器的工作目录,否则数据会落在启动器所在之处。
12
+ *
13
+ * 0.1.1 及更早的默认位置是系统目录(Windows 的 `%LOCALAPPDATA%\YunzaiNG` 等)。
14
+ * 此处**不**为其保留静默回落 —— 那会使「默认在当前目录」在任何装过旧版的机器上
15
+ * 都不成立。改由 {@link legacyInstance} 把旧实例报给 CLI 显式提示:换目录应当是
16
+ * 使用者看得见的一步,而不是内核悄悄替他挑一个。
12
17
  */
13
18
  import { homedir } from "node:os"
14
19
  import { dirname, isAbsolute, join, resolve } from "node:path"
@@ -63,6 +68,8 @@ function findPortableRoot(runtimeDir: string): string | undefined {
63
68
 
64
69
  /**
65
70
  * 系统默认的数据根目录
71
+ *
72
+ * 已不再是缺省取值,仅由 {@link legacyInstance} 用于查找旧实例,见文件头第 3 条。
66
73
  * @returns 绝对路径
67
74
  */
68
75
  function defaultHome(): string {
@@ -85,6 +92,44 @@ function defaultHome(): string {
85
92
  return join(home, APP_DIR_NAME_UNIX)
86
93
  }
87
94
 
95
+ /**
96
+ * 判断一个目录里是否已有实例
97
+ *
98
+ * 以 `config/` 是否存在为准:`ensurePaths` 建的七个目录中只有它必定装着文件
99
+ * (`ConfigStore` 会把缺省配置落盘),而空的 `data/`、`logs/` 无从区分
100
+ * 「一个实例」与「随手建的空目录」。
101
+ * @param dir 待判断的目录
102
+ * @returns 是否已有实例
103
+ */
104
+ function hasInstance(dir: string): boolean {
105
+ return existsSync(join(dir, "config"))
106
+ }
107
+
108
+ /**
109
+ * 未显式指定、也无便携标记时的主目录
110
+ *
111
+ * 见文件头第 2、3 条。
112
+ * @returns 绝对路径
113
+ */
114
+ function autoHome(): string {
115
+ return process.cwd()
116
+ }
117
+
118
+ /**
119
+ * 0.1.1 及更早的默认位置上是否还留着一个实例
120
+ *
121
+ * 供 CLI 提示使用:默认位置自 0.2.0 起改为当前目录,装过旧版的机器上那个实例仍在原处,
122
+ * 而使用者多半以为「配置全没了」。内核只负责报出位置,是否搬家由使用者决定 ——
123
+ * 自动迁移会在两个目录都有内容时无从判断该以谁为准。
124
+ * @param home 本次实际使用的主目录
125
+ * @returns 旧实例所在目录;不存在、或恰好就是本次所用的目录时 undefined
126
+ */
127
+ export function legacyInstance(home: string): string | undefined {
128
+ const legacy = defaultHome()
129
+ if (resolve(legacy) === resolve(home)) return undefined
130
+ return hasInstance(legacy) ? legacy : undefined
131
+ }
132
+
88
133
  /**
89
134
  * 解析目录布局
90
135
  *
@@ -97,7 +142,7 @@ export function resolvePaths(opts: ResolvePathsOptions = {}): RuntimePaths {
97
142
  const runtime = opts.runtime ? resolve(opts.runtime) : detectRuntimeDir()
98
143
 
99
144
  const explicit = opts.home ?? process.env.YZNG_HOME
100
- const home = explicit ? resolve(process.cwd(), explicit) : (findPortableRoot(runtime) ?? defaultHome())
145
+ const home = explicit ? resolve(process.cwd(), explicit) : (findPortableRoot(runtime) ?? autoHome())
101
146
 
102
147
  return Object.freeze({
103
148
  home,
@@ -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"
@@ -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 })
@@ -11,7 +11,7 @@
11
11
  * 且无从知道谁在用谁)。
12
12
  *
13
13
  * 键的所有者被记录下来,插件卸载时它提供的服务一起消失 —— 使用方拿到的是 undefined,
14
- * 而不是一个指向已卸载模块的失效引用。
14
+ * 而不是一个指向已卸载模块的失效引用。
15
15
  */
16
16
  import type { Disposer, DurationLike } from "@yunzai-ng/types"
17
17
  import { defer, type Deferred } from "../util/defer.js"
@@ -17,7 +17,7 @@
17
17
  * 抛出 —— 只报最后一个的话,真实原因常在第一个里。
18
18
  *
19
19
  * **此处不限并发。** 同时渲染多张图会不会耗尽内存,取决于渲染器自己页面池的大小,
20
- * 只有渲染器插件知道那个数。内核再加一层信号量只会与插件内部的池互相干扰。
20
+ * 只有渲染器插件知道那个数。内核再加一层信号量只会与插件内部的池互相干扰。
21
21
  */
22
22
  import type {
23
23
  Disposer,
@@ -17,7 +17,7 @@
17
17
  * 停止等待,并记 error。
18
18
  *
19
19
  * **执行出错同样计为已执行。** `lastRun` 在开始时就写,不等成功 —— 隐去失败的那次会让
20
- * 人误认为调度器没工作。
20
+ * 人误认为调度器没工作。
21
21
  */
22
22
  import type { Disposer, Logger, TaskFn, TaskInfo, TaskOptions } from "@yunzai-ng/types"
23
23
  import { Cron } from "croner"
@@ -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
 
@@ -13,7 +13,7 @@
13
13
  * 这类服务器最致命的漏洞,值得两道锁。
14
14
  *
15
15
  * **本文件不定位面板前端产物。** 面板是插件,产物目录由它自己解析并经 `ctx.panel()` 传入
16
- * —— 内核一旦掌握前端的目录约定,「替换面板」就退化成必须改内核。
16
+ * —— 内核一旦掌握前端的目录约定,「替换面板」就退化成必须改内核。
17
17
  */
18
18
  import { isDirectory, isFile, safeJoin } from "../util/fs.js"
19
19
 
@@ -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"