@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.
Files changed (357) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +51 -0
  3. package/dist/adapter/accounts.d.ts +174 -0
  4. package/dist/adapter/accounts.d.ts.map +1 -0
  5. package/dist/adapter/accounts.js +653 -0
  6. package/dist/adapter/accounts.js.map +1 -0
  7. package/dist/adapter/bots.d.ts +142 -0
  8. package/dist/adapter/bots.d.ts.map +1 -0
  9. package/dist/adapter/bots.js +454 -0
  10. package/dist/adapter/bots.js.map +1 -0
  11. package/dist/adapter/host.d.ts +114 -0
  12. package/dist/adapter/host.d.ts.map +1 -0
  13. package/dist/adapter/host.js +90 -0
  14. package/dist/adapter/host.js.map +1 -0
  15. package/dist/adapter/login.d.ts +174 -0
  16. package/dist/adapter/login.d.ts.map +1 -0
  17. package/dist/adapter/login.js +345 -0
  18. package/dist/adapter/login.js.map +1 -0
  19. package/dist/adapter/registry.d.ts +102 -0
  20. package/dist/adapter/registry.d.ts.map +1 -0
  21. package/dist/adapter/registry.js +150 -0
  22. package/dist/adapter/registry.js.map +1 -0
  23. package/dist/config/core-config.d.ts +151 -0
  24. package/dist/config/core-config.d.ts.map +1 -0
  25. package/dist/config/core-config.js +338 -0
  26. package/dist/config/core-config.js.map +1 -0
  27. package/dist/config/schema.d.ts +449 -0
  28. package/dist/config/schema.d.ts.map +1 -0
  29. package/dist/config/schema.js +871 -0
  30. package/dist/config/schema.js.map +1 -0
  31. package/dist/config/store.d.ts +184 -0
  32. package/dist/config/store.d.ts.map +1 -0
  33. package/dist/config/store.js +428 -0
  34. package/dist/config/store.js.map +1 -0
  35. package/dist/config/yaml.d.ts +23 -0
  36. package/dist/config/yaml.d.ts.map +1 -0
  37. package/dist/config/yaml.js +108 -0
  38. package/dist/config/yaml.js.map +1 -0
  39. package/dist/http/client.d.ts +82 -0
  40. package/dist/http/client.d.ts.map +1 -0
  41. package/dist/http/client.js +658 -0
  42. package/dist/http/client.js.map +1 -0
  43. package/dist/index.d.ts +77 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +94 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/kernel/app.d.ts +244 -0
  48. package/dist/kernel/app.d.ts.map +1 -0
  49. package/dist/kernel/app.js +654 -0
  50. package/dist/kernel/app.js.map +1 -0
  51. package/dist/kernel/kv-sink.d.ts +34 -0
  52. package/dist/kernel/kv-sink.d.ts.map +1 -0
  53. package/dist/kernel/kv-sink.js +25 -0
  54. package/dist/kernel/kv-sink.js.map +1 -0
  55. package/dist/kernel/policy.d.ts +77 -0
  56. package/dist/kernel/policy.d.ts.map +1 -0
  57. package/dist/kernel/policy.js +99 -0
  58. package/dist/kernel/policy.js.map +1 -0
  59. package/dist/kernel/runtime.d.ts +117 -0
  60. package/dist/kernel/runtime.d.ts.map +1 -0
  61. package/dist/kernel/runtime.js +197 -0
  62. package/dist/kernel/runtime.js.map +1 -0
  63. package/dist/kernel/sql-sink.d.ts +36 -0
  64. package/dist/kernel/sql-sink.d.ts.map +1 -0
  65. package/dist/kernel/sql-sink.js +114 -0
  66. package/dist/kernel/sql-sink.js.map +1 -0
  67. package/dist/logger/format.d.ts +67 -0
  68. package/dist/logger/format.d.ts.map +1 -0
  69. package/dist/logger/format.js +218 -0
  70. package/dist/logger/format.js.map +1 -0
  71. package/dist/logger/index.d.ts +97 -0
  72. package/dist/logger/index.d.ts.map +1 -0
  73. package/dist/logger/index.js +363 -0
  74. package/dist/logger/index.js.map +1 -0
  75. package/dist/logger/rotate.d.ts +47 -0
  76. package/dist/logger/rotate.d.ts.map +1 -0
  77. package/dist/logger/rotate.js +242 -0
  78. package/dist/logger/rotate.js.map +1 -0
  79. package/dist/message/segment.d.ts +255 -0
  80. package/dist/message/segment.d.ts.map +1 -0
  81. package/dist/message/segment.js +510 -0
  82. package/dist/message/segment.js.map +1 -0
  83. package/dist/message/split.d.ts +34 -0
  84. package/dist/message/split.d.ts.map +1 -0
  85. package/dist/message/split.js +79 -0
  86. package/dist/message/split.js.map +1 -0
  87. package/dist/message/target.d.ts +29 -0
  88. package/dist/message/target.d.ts.map +1 -0
  89. package/dist/message/target.js +34 -0
  90. package/dist/message/target.js.map +1 -0
  91. package/dist/pipeline/cooldown.d.ts +84 -0
  92. package/dist/pipeline/cooldown.d.ts.map +1 -0
  93. package/dist/pipeline/cooldown.js +94 -0
  94. package/dist/pipeline/cooldown.js.map +1 -0
  95. package/dist/pipeline/dispatch.d.ts +98 -0
  96. package/dist/pipeline/dispatch.d.ts.map +1 -0
  97. package/dist/pipeline/dispatch.js +323 -0
  98. package/dist/pipeline/dispatch.js.map +1 -0
  99. package/dist/pipeline/event.d.ts +130 -0
  100. package/dist/pipeline/event.d.ts.map +1 -0
  101. package/dist/pipeline/event.js +538 -0
  102. package/dist/pipeline/event.js.map +1 -0
  103. package/dist/pipeline/middleware.d.ts +63 -0
  104. package/dist/pipeline/middleware.d.ts.map +1 -0
  105. package/dist/pipeline/middleware.js +174 -0
  106. package/dist/pipeline/middleware.js.map +1 -0
  107. package/dist/pipeline/prompt.d.ts +80 -0
  108. package/dist/pipeline/prompt.d.ts.map +1 -0
  109. package/dist/pipeline/prompt.js +155 -0
  110. package/dist/pipeline/prompt.js.map +1 -0
  111. package/dist/pipeline/router.d.ts +98 -0
  112. package/dist/pipeline/router.d.ts.map +1 -0
  113. package/dist/pipeline/router.js +489 -0
  114. package/dist/pipeline/router.js.map +1 -0
  115. package/dist/platform/detect.d.ts +24 -0
  116. package/dist/platform/detect.d.ts.map +1 -0
  117. package/dist/platform/detect.js +178 -0
  118. package/dist/platform/detect.js.map +1 -0
  119. package/dist/platform/paths.d.ts +38 -0
  120. package/dist/platform/paths.d.ts.map +1 -0
  121. package/dist/platform/paths.js +129 -0
  122. package/dist/platform/paths.js.map +1 -0
  123. package/dist/platform/system.d.ts +71 -0
  124. package/dist/platform/system.d.ts.map +1 -0
  125. package/dist/platform/system.js +242 -0
  126. package/dist/platform/system.js.map +1 -0
  127. package/dist/plugin/context.d.ts +92 -0
  128. package/dist/plugin/context.d.ts.map +1 -0
  129. package/dist/plugin/context.js +573 -0
  130. package/dist/plugin/context.js.map +1 -0
  131. package/dist/plugin/define.d.ts +92 -0
  132. package/dist/plugin/define.d.ts.map +1 -0
  133. package/dist/plugin/define.js +82 -0
  134. package/dist/plugin/define.js.map +1 -0
  135. package/dist/plugin/discover.d.ts +106 -0
  136. package/dist/plugin/discover.d.ts.map +1 -0
  137. package/dist/plugin/discover.js +254 -0
  138. package/dist/plugin/discover.js.map +1 -0
  139. package/dist/plugin/events.d.ts +109 -0
  140. package/dist/plugin/events.d.ts.map +1 -0
  141. package/dist/plugin/events.js +209 -0
  142. package/dist/plugin/events.js.map +1 -0
  143. package/dist/plugin/hooks.d.ts +266 -0
  144. package/dist/plugin/hooks.d.ts.map +1 -0
  145. package/dist/plugin/hooks.js +84 -0
  146. package/dist/plugin/hooks.js.map +1 -0
  147. package/dist/plugin/host.d.ts +160 -0
  148. package/dist/plugin/host.d.ts.map +1 -0
  149. package/dist/plugin/host.js +546 -0
  150. package/dist/plugin/host.js.map +1 -0
  151. package/dist/plugin/market.d.ts +264 -0
  152. package/dist/plugin/market.d.ts.map +1 -0
  153. package/dist/plugin/market.js +700 -0
  154. package/dist/plugin/market.js.map +1 -0
  155. package/dist/plugin/services.d.ts +116 -0
  156. package/dist/plugin/services.d.ts.map +1 -0
  157. package/dist/plugin/services.js +213 -0
  158. package/dist/plugin/services.js.map +1 -0
  159. package/dist/plugin/tar.d.ts +65 -0
  160. package/dist/plugin/tar.d.ts.map +1 -0
  161. package/dist/plugin/tar.js +277 -0
  162. package/dist/plugin/tar.js.map +1 -0
  163. package/dist/render/registry.d.ts +127 -0
  164. package/dist/render/registry.d.ts.map +1 -0
  165. package/dist/render/registry.js +306 -0
  166. package/dist/render/registry.js.map +1 -0
  167. package/dist/scheduler/index.d.ts +67 -0
  168. package/dist/scheduler/index.d.ts.map +1 -0
  169. package/dist/scheduler/index.js +279 -0
  170. package/dist/scheduler/index.js.map +1 -0
  171. package/dist/server/api.d.ts +179 -0
  172. package/dist/server/api.d.ts.map +1 -0
  173. package/dist/server/api.js +602 -0
  174. package/dist/server/api.js.map +1 -0
  175. package/dist/server/auth.d.ts +83 -0
  176. package/dist/server/auth.d.ts.map +1 -0
  177. package/dist/server/auth.js +178 -0
  178. package/dist/server/auth.js.map +1 -0
  179. package/dist/server/browse.d.ts +60 -0
  180. package/dist/server/browse.d.ts.map +1 -0
  181. package/dist/server/browse.js +187 -0
  182. package/dist/server/browse.js.map +1 -0
  183. package/dist/server/files.d.ts +27 -0
  184. package/dist/server/files.d.ts.map +1 -0
  185. package/dist/server/files.js +76 -0
  186. package/dist/server/files.js.map +1 -0
  187. package/dist/server/index.d.ts +189 -0
  188. package/dist/server/index.d.ts.map +1 -0
  189. package/dist/server/index.js +1123 -0
  190. package/dist/server/index.js.map +1 -0
  191. package/dist/server/table.d.ts +105 -0
  192. package/dist/server/table.d.ts.map +1 -0
  193. package/dist/server/table.js +289 -0
  194. package/dist/server/table.js.map +1 -0
  195. package/dist/store/index.d.ts +50 -0
  196. package/dist/store/index.d.ts.map +1 -0
  197. package/dist/store/index.js +120 -0
  198. package/dist/store/index.js.map +1 -0
  199. package/dist/store/json.d.ts +73 -0
  200. package/dist/store/json.d.ts.map +1 -0
  201. package/dist/store/json.js +174 -0
  202. package/dist/store/json.js.map +1 -0
  203. package/dist/store/kv.d.ts +126 -0
  204. package/dist/store/kv.d.ts.map +1 -0
  205. package/dist/store/kv.js +232 -0
  206. package/dist/store/kv.js.map +1 -0
  207. package/dist/store/level.d.ts +103 -0
  208. package/dist/store/level.d.ts.map +1 -0
  209. package/dist/store/level.js +119 -0
  210. package/dist/store/level.js.map +1 -0
  211. package/dist/store/memory.d.ts +61 -0
  212. package/dist/store/memory.d.ts.map +1 -0
  213. package/dist/store/memory.js +94 -0
  214. package/dist/store/memory.js.map +1 -0
  215. package/dist/store/sql.d.ts +142 -0
  216. package/dist/store/sql.d.ts.map +1 -0
  217. package/dist/store/sql.js +246 -0
  218. package/dist/store/sql.js.map +1 -0
  219. package/dist/testing/fake.d.ts +109 -0
  220. package/dist/testing/fake.d.ts.map +1 -0
  221. package/dist/testing/fake.js +196 -0
  222. package/dist/testing/fake.js.map +1 -0
  223. package/dist/testing/index.d.ts +16 -0
  224. package/dist/testing/index.d.ts.map +1 -0
  225. package/dist/testing/index.js +16 -0
  226. package/dist/testing/index.js.map +1 -0
  227. package/dist/testing/mock-adapter.d.ts +205 -0
  228. package/dist/testing/mock-adapter.d.ts.map +1 -0
  229. package/dist/testing/mock-adapter.js +309 -0
  230. package/dist/testing/mock-adapter.js.map +1 -0
  231. package/dist/util/deep.d.ts +108 -0
  232. package/dist/util/deep.d.ts.map +1 -0
  233. package/dist/util/deep.js +275 -0
  234. package/dist/util/deep.js.map +1 -0
  235. package/dist/util/defer.d.ts +107 -0
  236. package/dist/util/defer.d.ts.map +1 -0
  237. package/dist/util/defer.js +163 -0
  238. package/dist/util/defer.js.map +1 -0
  239. package/dist/util/dispose.d.ts +88 -0
  240. package/dist/util/dispose.d.ts.map +1 -0
  241. package/dist/util/dispose.js +143 -0
  242. package/dist/util/dispose.js.map +1 -0
  243. package/dist/util/duration.d.ts +28 -0
  244. package/dist/util/duration.d.ts.map +1 -0
  245. package/dist/util/duration.js +77 -0
  246. package/dist/util/duration.js.map +1 -0
  247. package/dist/util/fs.d.ts +110 -0
  248. package/dist/util/fs.d.ts.map +1 -0
  249. package/dist/util/fs.js +269 -0
  250. package/dist/util/fs.js.map +1 -0
  251. package/dist/util/id.d.ts +49 -0
  252. package/dist/util/id.d.ts.map +1 -0
  253. package/dist/util/id.js +87 -0
  254. package/dist/util/id.js.map +1 -0
  255. package/dist/util/lru.d.ts +74 -0
  256. package/dist/util/lru.d.ts.map +1 -0
  257. package/dist/util/lru.js +156 -0
  258. package/dist/util/lru.js.map +1 -0
  259. package/dist/util/queue.d.ts +64 -0
  260. package/dist/util/queue.d.ts.map +1 -0
  261. package/dist/util/queue.js +147 -0
  262. package/dist/util/queue.js.map +1 -0
  263. package/dist/util/text.d.ts +86 -0
  264. package/dist/util/text.d.ts.map +1 -0
  265. package/dist/util/text.js +182 -0
  266. package/dist/util/text.js.map +1 -0
  267. package/package.json +44 -0
  268. package/src/adapter/accounts.ts +758 -0
  269. package/src/adapter/bots.ts +582 -0
  270. package/src/adapter/host.ts +224 -0
  271. package/src/adapter/login.ts +481 -0
  272. package/src/adapter/registry.ts +202 -0
  273. package/src/config/core-config.test.ts +60 -0
  274. package/src/config/core-config.ts +401 -0
  275. package/src/config/schema.test.ts +179 -0
  276. package/src/config/schema.ts +1036 -0
  277. package/src/config/store.test.ts +245 -0
  278. package/src/config/store.ts +506 -0
  279. package/src/config/yaml.ts +119 -0
  280. package/src/http/client.test.ts +568 -0
  281. package/src/http/client.ts +777 -0
  282. package/src/index.ts +106 -0
  283. package/src/kernel/app.test.ts +267 -0
  284. package/src/kernel/app.ts +843 -0
  285. package/src/kernel/kv-sink.ts +58 -0
  286. package/src/kernel/policy.ts +120 -0
  287. package/src/kernel/runtime.test.ts +542 -0
  288. package/src/kernel/runtime.ts +307 -0
  289. package/src/kernel/sql-sink.ts +154 -0
  290. package/src/logger/format.ts +259 -0
  291. package/src/logger/index.ts +427 -0
  292. package/src/logger/rotate.ts +265 -0
  293. package/src/message/segment.test.ts +335 -0
  294. package/src/message/segment.ts +569 -0
  295. package/src/message/split.ts +97 -0
  296. package/src/message/target.ts +48 -0
  297. package/src/pipeline/cooldown.ts +136 -0
  298. package/src/pipeline/dispatch.ts +419 -0
  299. package/src/pipeline/event.ts +735 -0
  300. package/src/pipeline/middleware.test.ts +426 -0
  301. package/src/pipeline/middleware.ts +199 -0
  302. package/src/pipeline/prompt.ts +212 -0
  303. package/src/pipeline/router.test.ts +432 -0
  304. package/src/pipeline/router.ts +575 -0
  305. package/src/platform/detect.ts +178 -0
  306. package/src/platform/paths.ts +147 -0
  307. package/src/platform/system.test.ts +102 -0
  308. package/src/platform/system.ts +289 -0
  309. package/src/plugin/context.test.ts +459 -0
  310. package/src/plugin/context.ts +757 -0
  311. package/src/plugin/define.test.ts +120 -0
  312. package/src/plugin/define.ts +142 -0
  313. package/src/plugin/discover.test.ts +253 -0
  314. package/src/plugin/discover.ts +353 -0
  315. package/src/plugin/events.test.ts +162 -0
  316. package/src/plugin/events.ts +266 -0
  317. package/src/plugin/hooks.ts +373 -0
  318. package/src/plugin/host.test.ts +696 -0
  319. package/src/plugin/host.ts +738 -0
  320. package/src/plugin/market.test.ts +781 -0
  321. package/src/plugin/market.ts +870 -0
  322. package/src/plugin/services.test.ts +135 -0
  323. package/src/plugin/services.ts +261 -0
  324. package/src/plugin/tar.test.ts +158 -0
  325. package/src/plugin/tar.ts +309 -0
  326. package/src/render/registry.test.ts +127 -0
  327. package/src/render/registry.ts +439 -0
  328. package/src/scheduler/index.ts +357 -0
  329. package/src/server/api.test.ts +802 -0
  330. package/src/server/api.ts +869 -0
  331. package/src/server/auth.ts +201 -0
  332. package/src/server/browse.test.ts +193 -0
  333. package/src/server/browse.ts +240 -0
  334. package/src/server/files.ts +97 -0
  335. package/src/server/index.test.ts +566 -0
  336. package/src/server/index.ts +1386 -0
  337. package/src/server/table.ts +351 -0
  338. package/src/store/index.ts +156 -0
  339. package/src/store/json.ts +198 -0
  340. package/src/store/kv.test.ts +307 -0
  341. package/src/store/kv.ts +256 -0
  342. package/src/store/level.ts +167 -0
  343. package/src/store/memory.ts +109 -0
  344. package/src/store/sql.test.ts +204 -0
  345. package/src/store/sql.ts +324 -0
  346. package/src/testing/fake.ts +297 -0
  347. package/src/testing/index.ts +15 -0
  348. package/src/testing/mock-adapter.ts +588 -0
  349. package/src/util/deep.ts +272 -0
  350. package/src/util/defer.ts +211 -0
  351. package/src/util/dispose.ts +169 -0
  352. package/src/util/duration.ts +82 -0
  353. package/src/util/fs.ts +279 -0
  354. package/src/util/id.ts +94 -0
  355. package/src/util/lru.ts +185 -0
  356. package/src/util/queue.ts +167 -0
  357. package/src/util/text.ts +189 -0
@@ -0,0 +1,351 @@
1
+ /**
2
+ * 模块职责:路径表 —— 注册 / 注销 / 按请求路径查找,含参数与通配段
3
+ * 依赖方向:只依赖 `@yunzai-ng/types`;不认识 Fastify,也不认识业务
4
+ * 生命周期:随共享服务器创建;插件卸载时按 Disposer 逐条摘除
5
+ * 注意事项:**为什么要自己维护一张表,而不是直接用 Fastify 的路由树**
6
+ *
7
+ * Fastify(find-my-way)只能加路由,没有删除单条路由的 API。而
8
+ * "插件能装能卸能热重载"是本框架的核心不变量:重载一次插件如果留下
9
+ * 一条死路由,第二次注册同一路径就会被 duplicate route 直接抛错,
10
+ * 插件从此装不上。
11
+ *
12
+ * 于是服务器只在 Fastify 上注册两条 catch-all(`/` 与 `/*`),真正
13
+ * 的分发走这张表:注册是 Map/数组操作,注销就是删掉一项。代价是参数
14
+ * 解析与优先级排序要自己写 —— 也就是本文件。
15
+ *
16
+ * 排序在**注册时**一次算好(`#dynamic` 始终有序),查找路径上没有
17
+ * sort、没有正则编译,理由同 pipeline/router.ts:注册低频、查找高频。
18
+ */
19
+ import type { Disposer, HttpMethod } from "@yunzai-ng/types"
20
+
21
+ /** 全部支持的 HTTP 方法,顺序即 `Allow` 响应头里的顺序 */
22
+ export const HTTP_METHODS: readonly HttpMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]
23
+
24
+ /** 静态段的具体度权重 */
25
+ const RANK_STATIC = 3
26
+
27
+ /** 参数段(`:id`)的具体度权重 */
28
+ const RANK_PARAM = 2
29
+
30
+ /** 通配段(`*rest`)的具体度权重 */
31
+ const RANK_WILDCARD = 1
32
+
33
+ /** 通配段没写名字时的缺省参数名 */
34
+ const DEFAULT_WILDCARD_NAME = "*"
35
+
36
+ /**
37
+ * 判断一个字符串是否是受支持的 HTTP 方法
38
+ * @param value 待判断的值(通常来自 `request.method`)
39
+ * @returns 是否受支持
40
+ */
41
+ export function isHttpMethod(value: string): value is HttpMethod {
42
+ return (HTTP_METHODS as readonly string[]).includes(value)
43
+ }
44
+
45
+ /**
46
+ * 解码一个路径段
47
+ *
48
+ * 失败时原样返回而不抛错:畸形的 `%` 转义应当表现为"路由不存在"(404),
49
+ * 而非 500 —— 后者会将每个扫描器的探测请求都记为一条 error 日志。
50
+ * @param raw 原始段
51
+ * @returns 解码后的段
52
+ */
53
+ function decodeSegment(raw: string): string {
54
+ if (!raw.includes("%")) return raw
55
+ try {
56
+ return decodeURIComponent(raw)
57
+ } catch {
58
+ return raw
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 把路径切成已解码的段数组
64
+ *
65
+ * 先切后解码,顺序不能反:`%2F` 是段内的字面斜杠,先解码会让它变成分隔符,
66
+ * 那就等于把 `..%2F..%2Fetc` 这类穿越尝试翻译成了真正的路径分隔。
67
+ * @param path 路径(可含查询串之前的部分)
68
+ * @returns 已解码的非空段数组
69
+ */
70
+ export function splitSegments(path: string): string[] {
71
+ const out: string[] = []
72
+ for (const raw of path.split("/")) {
73
+ if (raw === "" || raw === ".") continue
74
+ out.push(decodeSegment(raw))
75
+ }
76
+ return out
77
+ }
78
+
79
+ /**
80
+ * 归一化一个 URL 模式
81
+ *
82
+ * `scope` 与 `path` 各自可能带或不带首尾斜杠,插件作者两种写法都会用。
83
+ * @param scope URL 前缀,如 `/plugin/mhy-game`
84
+ * @param path 相对 scope 的路径,如 `webhook/:id`
85
+ * @returns 形如 `/plugin/mhy-game/webhook/:id` 的模式;根路径为 `/`
86
+ */
87
+ export function joinPattern(scope: string, path: string): string {
88
+ const segs = [...splitRaw(scope), ...splitRaw(path)]
89
+ return segs.length === 0 ? "/" : `/${segs.join("/")}`
90
+ }
91
+
92
+ /**
93
+ * 切分但**不**解码
94
+ *
95
+ * 模式是插件作者手写的,本来就是解码后的形态;对它做 `decodeURIComponent`
96
+ * 会把 `%` 当转义处理,反而改坏了模式。
97
+ * @param path 路径
98
+ * @returns 非空段数组
99
+ */
100
+ function splitRaw(path: string): string[] {
101
+ return path.split("/").filter(seg => seg !== "" && seg !== ".")
102
+ }
103
+
104
+ /** 一个已编译的路径段 */
105
+ interface PathSegment {
106
+ /** 段类型 */
107
+ readonly kind: "static" | "param" | "wildcard"
108
+ /** 静态段为字面值;参数段与通配段为参数名 */
109
+ readonly value: string
110
+ }
111
+
112
+ /** 表内一项 */
113
+ interface Bound<T> {
114
+ /** 归一化后的完整模式 */
115
+ readonly pattern: string
116
+ /** 承载的值(路由处理函数、WebSocket 处理函数、静态目录……) */
117
+ readonly value: T
118
+ /** 编译后的段 */
119
+ readonly segments: readonly PathSegment[]
120
+ /** 各段的具体度权重,用于排序 */
121
+ readonly ranks: readonly number[]
122
+ /** 注册序号,同具体度时保证先注册者优先且顺序稳定 */
123
+ readonly seq: number
124
+ }
125
+
126
+ /** 一次查找命中 */
127
+ export interface PathHit<T> {
128
+ /** 命中的模式 */
129
+ readonly pattern: string
130
+ /** 承载的值 */
131
+ readonly value: T
132
+ /** 路径参数;无参数时是同一个空对象 */
133
+ readonly params: Readonly<Record<string, string>>
134
+ }
135
+
136
+ /** 没有参数时共用的空对象,省掉每次请求一次分配 */
137
+ const NO_PARAMS: Readonly<Record<string, string>> = Object.freeze({})
138
+
139
+ /**
140
+ * 编译一个模式
141
+ * @param pattern 已归一化的模式
142
+ * @returns 段数组
143
+ * @throws 参数名为空、或通配段不在末尾时
144
+ */
145
+ function compileSegments(pattern: string): PathSegment[] {
146
+ const raw = splitRaw(pattern)
147
+ const out: PathSegment[] = []
148
+ for (let i = 0; i < raw.length; i++) {
149
+ const seg = raw[i]!
150
+ if (seg.startsWith(":")) {
151
+ const name = seg.slice(1)
152
+ if (name === "") throw new Error(`路由模式 ${pattern} 里有一个没写名字的参数段(应形如 /x/:id)`)
153
+ out.push({ kind: "param", value: name })
154
+ continue
155
+ }
156
+ if (seg.startsWith("*")) {
157
+ if (i !== raw.length - 1) {
158
+ throw new Error(`路由模式 ${pattern} 的通配段只能置于末尾:其将匹配后续的全部路径`)
159
+ }
160
+ out.push({ kind: "wildcard", value: seg.slice(1) === "" ? DEFAULT_WILDCARD_NAME : seg.slice(1) })
161
+ continue
162
+ }
163
+ out.push({ kind: "static", value: seg })
164
+ }
165
+ return out
166
+ }
167
+
168
+ /**
169
+ * 取段的具体度权重
170
+ * @param segment 段
171
+ * @returns 权重
172
+ */
173
+ function rankOf(segment: PathSegment): number {
174
+ if (segment.kind === "static") return RANK_STATIC
175
+ return segment.kind === "param" ? RANK_PARAM : RANK_WILDCARD
176
+ }
177
+
178
+ /**
179
+ * 比较两项的匹配优先级
180
+ *
181
+ * 逐段比权重:`/api/config` 必须比 `/api/:name` 先试,`/api/:name` 又必须比
182
+ * `/api/*` 先试。前缀相同时段数多者更具体(`/a/b/*` 比 `/a/*` 更具体)。
183
+ * 完全同级时按注册顺序,保证行为可预期。
184
+ * @param a 项 a
185
+ * @param b 项 b
186
+ * @returns 排序结果,负数表示 a 先试
187
+ */
188
+ function bySpecificity<T>(a: Bound<T>, b: Bound<T>): number {
189
+ const n = Math.min(a.ranks.length, b.ranks.length)
190
+ for (let i = 0; i < n; i++) {
191
+ const diff = b.ranks[i]! - a.ranks[i]!
192
+ if (diff !== 0) return diff
193
+ }
194
+ if (a.ranks.length !== b.ranks.length) return b.ranks.length - a.ranks.length
195
+ return a.seq - b.seq
196
+ }
197
+
198
+ /**
199
+ * 按已排好的顺序插入
200
+ * @param list 已排序数组(就地修改)
201
+ * @param item 待插入项
202
+ */
203
+ function insertSorted<T>(list: Bound<T>[], item: Bound<T>): void {
204
+ let lo = 0
205
+ let hi = list.length
206
+ while (lo < hi) {
207
+ const mid = (lo + hi) >>> 1
208
+ if (bySpecificity(list[mid]!, item) <= 0) lo = mid + 1
209
+ else hi = mid
210
+ }
211
+ list.splice(lo, 0, item)
212
+ }
213
+
214
+ /**
215
+ * 一张路径表
216
+ *
217
+ * 静态模式(无参数无通配)走 Map 精确命中,其余按具体度顺序线性试。
218
+ * HTTP 路由、WebSocket 路径、静态目录挂载三者的查找规则完全一样,
219
+ * 所以共用这一个实现,只是承载的 `T` 不同。
220
+ */
221
+ export class PathTable<T> {
222
+ /** 无参数模式:路径 → 项 */
223
+ readonly #exact = new Map<string, Bound<T>>()
224
+ /** 含参数或通配的模式,按具体度预排序 */
225
+ #dynamic: Bound<T>[] = []
226
+ /** 注册序号发号器 */
227
+ #seq = 0
228
+
229
+ /** 已注册的条数 */
230
+ get size(): number {
231
+ return this.#exact.size + this.#dynamic.length
232
+ }
233
+
234
+ /**
235
+ * 注册一条
236
+ * @param pattern 已归一化的模式
237
+ * @param value 承载的值
238
+ * @returns 注销句柄;幂等
239
+ * @throws 模式非法、或同一模式重复注册时
240
+ */
241
+ add(pattern: string, value: T): Disposer {
242
+ if (this.has(pattern)) {
243
+ // 不静默覆盖:两个插件争用同一个路径时,"后者生效"将表现为前者无故失效。
244
+ // 插件的 scope 天然带有插件名,可能冲突的只有同一插件内部的重复注册
245
+ throw new Error(`路径 ${pattern} 已经注册过了。热重载请先执行上一次注册返回的 Disposer`)
246
+ }
247
+
248
+ const segments = compileSegments(pattern)
249
+ const item: Bound<T> = {
250
+ pattern,
251
+ value,
252
+ segments,
253
+ ranks: segments.map(rankOf),
254
+ seq: this.#seq++
255
+ }
256
+
257
+ if (segments.every(seg => seg.kind === "static")) this.#exact.set(pattern, item)
258
+ else insertSorted(this.#dynamic, item)
259
+
260
+ let removed = false
261
+ return () => {
262
+ if (removed) return
263
+ removed = true
264
+ if (this.#exact.get(pattern) === item) this.#exact.delete(pattern)
265
+ const at = this.#dynamic.indexOf(item)
266
+ if (at >= 0) this.#dynamic.splice(at, 1)
267
+ }
268
+ }
269
+
270
+ /**
271
+ * 某个模式是否已注册
272
+ * @param pattern 已归一化的模式
273
+ * @returns 是否已注册
274
+ */
275
+ has(pattern: string): boolean {
276
+ return this.#exact.has(pattern) || this.#dynamic.some(item => item.pattern === pattern)
277
+ }
278
+
279
+ /**
280
+ * 按已解码的路径段查找
281
+ * @param parts 已解码的路径段(`splitSegments()` 的产物)
282
+ * @returns 命中项;未命中时 undefined
283
+ */
284
+ find(parts: readonly string[]): PathHit<T> | undefined {
285
+ const exact = this.#exact.get(parts.length === 0 ? "/" : `/${parts.join("/")}`)
286
+ if (exact !== undefined) return { pattern: exact.pattern, value: exact.value, params: NO_PARAMS }
287
+
288
+ for (const item of this.#dynamic) {
289
+ const params = matchSegments(item.segments, parts)
290
+ if (params !== undefined) return { pattern: item.pattern, value: item.value, params }
291
+ }
292
+ return undefined
293
+ }
294
+
295
+ /**
296
+ * 导出全部模式,按匹配顺序
297
+ * @returns 模式数组
298
+ */
299
+ list(): string[] {
300
+ return this.entries().map(item => item.pattern)
301
+ }
302
+
303
+ /**
304
+ * 导出全部注册项,按匹配顺序
305
+ *
306
+ * 面板需要展示的是"哪条路由属于哪个插件、是否需要鉴权",仅有模式字符串不足,
307
+ * 因此连同 `value` 一并给出。
308
+ * @returns 模式与承载值的数组
309
+ */
310
+ entries(): { pattern: string; value: T }[] {
311
+ return [...this.#exact.values(), ...this.#dynamic]
312
+ .sort(bySpecificity)
313
+ .map(item => ({ pattern: item.pattern, value: item.value }))
314
+ }
315
+
316
+ /** 清空 */
317
+ clear(): void {
318
+ this.#exact.clear()
319
+ this.#dynamic = []
320
+ }
321
+ }
322
+
323
+ /**
324
+ * 拿一组段去匹配请求路径
325
+ * @param segments 编译后的模式段
326
+ * @param parts 请求路径的段
327
+ * @returns 路径参数;不匹配时 undefined
328
+ */
329
+ function matchSegments(
330
+ segments: readonly PathSegment[],
331
+ parts: readonly string[]
332
+ ): Readonly<Record<string, string>> | undefined {
333
+ const params: Record<string, string> = {}
334
+ for (let i = 0; i < segments.length; i++) {
335
+ const seg = segments[i]!
336
+ if (seg.kind === "wildcard") {
337
+ // 通配段匹配剩余的全部路径,包括"无剩余内容"的情形(`/files/*` 亦匹配 `/files`)
338
+ params[seg.value] = parts.slice(i).join("/")
339
+ return params
340
+ }
341
+ const part = parts[i]
342
+ if (part === undefined) return undefined
343
+ if (seg.kind === "static") {
344
+ if (part !== seg.value) return undefined
345
+ continue
346
+ }
347
+ if (part === "") return undefined
348
+ params[seg.value] = part
349
+ }
350
+ return parts.length === segments.length ? params : undefined
351
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * 模块职责:JSON 文件 KV 驱动(纯 JS 兜底,Termux/无编译环境用)
3
+ * 依赖方向:依赖 util/{fs,deep}、类型包
4
+ * 生命周期:`open()` 载入全量数据,`close()` 落盘;进程存续期间数据常驻内存
5
+ * 注意事项:**这是兜底方案,不是主力**。全量数据常驻内存 + 每次写盘重写整个文件,
6
+ * 数据量大了必然吃内存与 IO;只在原生模块装不上(Android/Termux 常见)
7
+ * 时启用,`level` 驱动才是默认。
8
+ *
9
+ * 写盘做了两件事防丢数据:一是合并窗口内的多次写只落一次盘,
10
+ * 二是走 `atomicWrite`(同目录临时文件 + rename),断电不会留下半个文件。
11
+ * 进程退出前必须 `close()`,否则最后一个窗口内的写入会丢。
12
+ */
13
+ import type { KvBatchOp, KvDriver, StoredEnvelope } from "@yunzai-ng/types"
14
+ import { atomicWrite, ensureDir, readText } from "../util/fs.js"
15
+ import { deepClone } from "../util/deep.js"
16
+ import { dirname } from "node:path"
17
+ import { isExpired } from "./kv.js"
18
+
19
+ /** 落盘合并窗口(毫秒) */
20
+ const FLUSH_DELAY = 300
21
+
22
+ /** 文件格式版本,将来变更结构时用于迁移判断 */
23
+ const FORMAT = 1
24
+
25
+ /** 落盘结构 */
26
+ interface JsonFileShape {
27
+ /** 格式版本 */
28
+ format: number
29
+ /** 数据表 */
30
+ data: Record<string, StoredEnvelope>
31
+ }
32
+
33
+ /** JSON 文件 KV 驱动 */
34
+ export class JsonKvDriver implements KvDriver {
35
+ /** 驱动 id */
36
+ readonly id = "json"
37
+
38
+ /** 文件绝对路径 */
39
+ readonly #file: string
40
+ /** 数据表 */
41
+ #map = new Map<string, StoredEnvelope>()
42
+ /** 落盘定时器 */
43
+ #timer: NodeJS.Timeout | undefined
44
+ /** 是否有未落盘的改动 */
45
+ #dirty = false
46
+ /** 正在进行的落盘,用于串行化避免两次写互相覆盖 */
47
+ #writing: Promise<void> = Promise.resolve()
48
+ /** 是否已关闭 */
49
+ #closed = false
50
+
51
+ /**
52
+ * @param file 数据文件绝对路径
53
+ */
54
+ constructor(file: string) {
55
+ this.#file = file
56
+ }
57
+
58
+ /** 键数量 */
59
+ get size(): number {
60
+ return this.#map.size
61
+ }
62
+
63
+ /**
64
+ * 载入数据
65
+ *
66
+ * 文件损坏时不抛错也不删除:改名为 `.corrupt-<时间戳>` 留证,然后从空库启动。
67
+ * 存储坏了让机器人起不来,比丢一点缓存严重得多。
68
+ */
69
+ async open(): Promise<void> {
70
+ this.#closed = false
71
+ await ensureDir(dirname(this.#file))
72
+ const text = await readText(this.#file)
73
+ if (text === undefined || text.trim() === "") return
74
+
75
+ try {
76
+ const parsed = JSON.parse(text) as JsonFileShape
77
+ const data = parsed.format === FORMAT ? parsed.data : undefined
78
+ if (!data || typeof data !== "object") throw new Error("结构不符")
79
+ this.#map = new Map(Object.entries(data))
80
+ // 载入时清掉过期键,避免它们一直占内存直到被访问
81
+ for (const [key, env] of [...this.#map]) if (isExpired(env)) this.#map.delete(key)
82
+ } catch (err) {
83
+ const backup = `${this.#file}.corrupt-${Date.now()}`
84
+ await atomicWrite(backup, text).catch(() => undefined)
85
+ throw new Error(`KV 数据文件解析失败,已备份到 ${backup}:${err instanceof Error ? err.message : String(err)}`)
86
+ }
87
+ }
88
+
89
+ /** 落盘并停止后续写入 */
90
+ async close(): Promise<void> {
91
+ this.#closed = true
92
+ if (this.#timer) {
93
+ clearTimeout(this.#timer)
94
+ this.#timer = undefined
95
+ }
96
+ await this.flush()
97
+ }
98
+
99
+ /**
100
+ * 读取
101
+ * @param key 完整键
102
+ * @returns 值信封副本
103
+ */
104
+ async get(key: string): Promise<StoredEnvelope | undefined> {
105
+ const env = this.#map.get(key)
106
+ return env === undefined ? undefined : deepClone(env)
107
+ }
108
+
109
+ /**
110
+ * 写入
111
+ * @param key 完整键
112
+ * @param value 值信封
113
+ */
114
+ async set(key: string, value: StoredEnvelope): Promise<void> {
115
+ this.#map.set(key, deepClone(value))
116
+ this.#schedule()
117
+ }
118
+
119
+ /**
120
+ * 删除
121
+ * @param key 完整键
122
+ */
123
+ async del(key: string): Promise<void> {
124
+ if (this.#map.delete(key)) this.#schedule()
125
+ }
126
+
127
+ /**
128
+ * 批量操作(只触发一次落盘)
129
+ * @param ops 操作列表
130
+ */
131
+ async batch(ops: KvBatchOp[]): Promise<void> {
132
+ let changed = false
133
+ for (const op of ops) {
134
+ if (op.op === "set") {
135
+ this.#map.set(op.key, deepClone(op.value))
136
+ changed = true
137
+ } else if (this.#map.delete(op.key)) changed = true
138
+ }
139
+ if (changed) this.#schedule()
140
+ }
141
+
142
+ /**
143
+ * 按前缀扫描
144
+ * @param prefix 键前缀
145
+ * @returns 键值对迭代器
146
+ */
147
+ async *scan(prefix: string): AsyncIterableIterator<readonly [string, StoredEnvelope]> {
148
+ for (const key of [...this.#map.keys()]) {
149
+ if (!key.startsWith(prefix)) continue
150
+ const env = this.#map.get(key)
151
+ if (env === undefined) continue
152
+ yield [key, deepClone(env)]
153
+ }
154
+ }
155
+
156
+ /**
157
+ * 原生原子自增(同步读改写,单线程下天然原子)
158
+ * @param key 完整键
159
+ * @param by 增量
160
+ * @param expireAt 首次创建时的过期时间点
161
+ * @returns 自增后的值
162
+ */
163
+ async incr(key: string, by: number, expireAt?: number): Promise<number> {
164
+ const env = this.#map.get(key)
165
+ const alive = env !== undefined && !isExpired(env)
166
+ const current = alive && typeof env.v === "number" ? env.v : 0
167
+ const next = current + by
168
+ const keep = alive ? env.e : expireAt
169
+ this.#map.set(key, keep === undefined ? { v: next } : { v: next, e: keep })
170
+ this.#schedule()
171
+ return next
172
+ }
173
+
174
+ /** 立即落盘(`close()` 与测试用) */
175
+ async flush(): Promise<void> {
176
+ if (!this.#dirty) return await this.#writing
177
+ this.#dirty = false
178
+
179
+ const snapshot: JsonFileShape = { format: FORMAT, data: Object.fromEntries(this.#map) }
180
+ // 串在上一次写之后:两次并发的 atomicWrite 会竞争同一个目标文件
181
+ this.#writing = this.#writing.then(async () => {
182
+ await atomicWrite(this.#file, JSON.stringify(snapshot))
183
+ })
184
+ await this.#writing
185
+ }
186
+
187
+ /** 安排一次延迟落盘 */
188
+ #schedule(): void {
189
+ this.#dirty = true
190
+ if (this.#closed || this.#timer) return
191
+ this.#timer = setTimeout(() => {
192
+ this.#timer = undefined
193
+ void this.flush().catch(() => undefined)
194
+ }, FLUSH_DELAY)
195
+ // unref:等待落盘不该拖住进程退出,退出路径上有 close() 兜底
196
+ if (typeof this.#timer.unref === "function") this.#timer.unref()
197
+ }
198
+ }