@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,272 @@
1
+ /**
2
+ * 模块职责:对象深操作(合并、克隆、按路径读写、差异路径计算)
3
+ * 依赖方向:无
4
+ * 生命周期:纯函数
5
+ * 注意事项:配置系统的地基。`diffPaths` 算出实际变化的叶子路径,故配置变更只通知
6
+ * 关心那些路径的订阅者,而不是让每个读配置的地方全量重算。
7
+ *
8
+ * **数组是整体替换,不逐元素合并。** 配置里的数组(主人列表、黑名单)语义上是
9
+ * "一个值",逐元素合并会让使用者无法删除默认项。
10
+ */
11
+
12
+ /** 可作为配置节点的普通对象 */
13
+ export type PlainObject = Record<string, unknown>
14
+
15
+ /**
16
+ * 判断是否为普通对象(排除数组、null、Date、Buffer、类实例等)
17
+ * @param value 待判定值
18
+ * @returns 是否为普通对象
19
+ */
20
+ export function isPlainObject(value: unknown): value is PlainObject {
21
+ if (value === null || typeof value !== "object") return false
22
+ if (Array.isArray(value)) return false
23
+ const proto = Object.getPrototypeOf(value) as object | null
24
+ return proto === Object.prototype || proto === null
25
+ }
26
+
27
+ /**
28
+ * 深克隆
29
+ *
30
+ * 优先用 `structuredClone`(Node 17+ 内置,能处理 Map/Set/Date/循环引用),
31
+ * 遇到函数等不可克隆值时退回浅拷贝语义。
32
+ * @param value 源值
33
+ * @returns 克隆结果
34
+ */
35
+ export function deepClone<T>(value: T): T {
36
+ try {
37
+ return structuredClone(value)
38
+ } catch {
39
+ return cloneFallback(value)
40
+ }
41
+ }
42
+
43
+ /**
44
+ * structuredClone 失败时的回退克隆(跳过不可克隆值)
45
+ * @param value 源值
46
+ * @returns 克隆结果
47
+ */
48
+ function cloneFallback<T>(value: T): T {
49
+ if (Array.isArray(value)) return value.map(item => cloneFallback(item)) as unknown as T
50
+ if (isPlainObject(value)) {
51
+ const out: PlainObject = {}
52
+ for (const [k, v] of Object.entries(value)) out[k] = cloneFallback(v)
53
+ return out as T
54
+ }
55
+ return value
56
+ }
57
+
58
+ /**
59
+ * 深合并:把 `source` 覆盖到 `target` 的副本上
60
+ *
61
+ * - 普通对象递归合并
62
+ * - 数组、`Date` 等整体替换
63
+ * - `source` 中值为 `undefined` 的键被忽略(不会把 target 的值抹成 undefined)
64
+ * @param target 基准值(不被修改)
65
+ * @param source 覆盖值
66
+ * @returns 合并后的新对象
67
+ */
68
+ export function deepMerge<T extends PlainObject>(target: T, source: PlainObject | undefined): T {
69
+ if (!source) return deepClone(target)
70
+ const out: PlainObject = deepClone(target) as PlainObject
71
+
72
+ for (const [key, value] of Object.entries(source)) {
73
+ if (value === undefined) continue
74
+ // 拒绝原型链污染:合并的数据可能来自 WebUI 提交的 JSON
75
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue
76
+
77
+ const existing = out[key]
78
+ if (isPlainObject(value) && isPlainObject(existing)) {
79
+ out[key] = deepMerge(existing, value)
80
+ } else {
81
+ out[key] = deepClone(value)
82
+ }
83
+ }
84
+ return out as T
85
+ }
86
+
87
+ /**
88
+ * 深比较
89
+ * @param a 左值
90
+ * @param b 右值
91
+ * @returns 是否深度相等
92
+ */
93
+ export function deepEqual(a: unknown, b: unknown): boolean {
94
+ if (a === b) return true
95
+ if (typeof a !== typeof b) return false
96
+ if (a === null || b === null) return false
97
+
98
+ if (Array.isArray(a) || Array.isArray(b)) {
99
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false
100
+ return a.every((item, i) => deepEqual(item, b[i]))
101
+ }
102
+
103
+ if (a instanceof Date || b instanceof Date) {
104
+ return a instanceof Date && b instanceof Date && a.getTime() === b.getTime()
105
+ }
106
+
107
+ if (typeof a === "object" && typeof b === "object") {
108
+ const ao = a as PlainObject
109
+ const bo = b as PlainObject
110
+ const aKeys = Object.keys(ao)
111
+ const bKeys = Object.keys(bo)
112
+ if (aKeys.length !== bKeys.length) return false
113
+ return aKeys.every(k => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k]))
114
+ }
115
+
116
+ return false
117
+ }
118
+
119
+ /**
120
+ * 把点分路径切成键数组
121
+ *
122
+ * 支持 `a.b[0].c` 与 `a.b.0.c` 两种写法。
123
+ * @param path 路径字符串
124
+ * @returns 键数组
125
+ */
126
+ export function splitPath(path: string): string[] {
127
+ if (path === "") return []
128
+ return path
129
+ .replace(/\[(\d+)\]/g, ".$1")
130
+ .split(".")
131
+ .filter(seg => seg !== "")
132
+ }
133
+
134
+ /**
135
+ * 按路径取值
136
+ * @param obj 源对象
137
+ * @param path 点分路径;空串返回 obj 本身
138
+ * @returns 值;路径不存在时 undefined
139
+ */
140
+ export function getPath(obj: unknown, path: string): unknown {
141
+ const keys = splitPath(path)
142
+ let cur: unknown = obj
143
+ for (const key of keys) {
144
+ if (cur === null || cur === undefined) return undefined
145
+ if (typeof cur !== "object") return undefined
146
+ cur = (cur as PlainObject)[key]
147
+ }
148
+ return cur
149
+ }
150
+
151
+ /**
152
+ * 按路径写值(原地修改)
153
+ *
154
+ * 中间层不存在时自动创建:下一级键是纯数字则建数组,否则建对象。
155
+ * @param obj 目标对象
156
+ * @param path 点分路径
157
+ * @param value 值
158
+ * @throws 当路径为空或中间层是不可写入的标量时
159
+ */
160
+ export function setPath(obj: PlainObject, path: string, value: unknown): void {
161
+ const keys = splitPath(path)
162
+ if (keys.length === 0) throw new Error("setPath: 路径不能为空")
163
+
164
+ let cur: PlainObject | unknown[] = obj
165
+ for (let i = 0; i < keys.length - 1; i++) {
166
+ const key = keys[i]!
167
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
168
+ throw new Error(`setPath: 非法路径片段 ${key}`)
169
+ }
170
+ const next = (cur as PlainObject)[key]
171
+ if (isPlainObject(next) || Array.isArray(next)) {
172
+ cur = next as PlainObject | unknown[]
173
+ } else if (next === undefined || next === null) {
174
+ const created: PlainObject | unknown[] = /^\d+$/.test(keys[i + 1]!) ? [] : {}
175
+ ;(cur as PlainObject)[key] = created
176
+ cur = created
177
+ } else {
178
+ throw new Error(`setPath: ${keys.slice(0, i + 1).join(".")} 已是标量,无法继续写入`)
179
+ }
180
+ }
181
+
182
+ const last = keys[keys.length - 1]!
183
+ if (last === "__proto__" || last === "constructor" || last === "prototype") {
184
+ throw new Error(`setPath: 非法路径片段 ${last}`)
185
+ }
186
+ ;(cur as PlainObject)[last] = value
187
+ }
188
+
189
+ /**
190
+ * 按路径删除(原地修改)
191
+ * @param obj 目标对象
192
+ * @param path 点分路径
193
+ * @returns 是否确实删掉了什么
194
+ */
195
+ export function deletePath(obj: PlainObject, path: string): boolean {
196
+ const keys = splitPath(path)
197
+ if (keys.length === 0) return false
198
+
199
+ const parentPath = keys.slice(0, -1).join(".")
200
+ const parent = parentPath === "" ? obj : getPath(obj, parentPath)
201
+ if (parent === null || typeof parent !== "object") return false
202
+
203
+ const last = keys[keys.length - 1]!
204
+ if (Array.isArray(parent)) {
205
+ const index = Number(last)
206
+ if (!Number.isInteger(index) || index < 0 || index >= parent.length) return false
207
+ parent.splice(index, 1)
208
+ return true
209
+ }
210
+ if (!Object.prototype.hasOwnProperty.call(parent, last)) return false
211
+ delete (parent as PlainObject)[last]
212
+ return true
213
+ }
214
+
215
+ /**
216
+ * 计算两个对象间发生变化的**叶子路径**
217
+ *
218
+ * 这是细粒度配置失效的核心:改了 `bot.masterQQ` 就只通知订阅
219
+ * `bot.masterQQ`(或其祖先 `bot`)的人,不打扰订阅 `render.*` 的人。
220
+ *
221
+ * 数组视为叶子(整体变化),不下钻元素。
222
+ * @param before 旧值
223
+ * @param after 新值
224
+ * @param prefix 内部递归用的路径前缀
225
+ * @returns 变化路径列表,已去重
226
+ */
227
+ export function diffPaths(before: unknown, after: unknown, prefix = ""): string[] {
228
+ if (deepEqual(before, after)) return []
229
+
230
+ // 任一侧不是普通对象 → 当前节点整体变化
231
+ if (!isPlainObject(before) || !isPlainObject(after)) {
232
+ return prefix === "" ? [""] : [prefix]
233
+ }
234
+
235
+ const out: string[] = []
236
+ const keys = new Set([...Object.keys(before), ...Object.keys(after)])
237
+ for (const key of keys) {
238
+ const path = prefix === "" ? key : `${prefix}.${key}`
239
+ out.push(...diffPaths(before[key], after[key], path))
240
+ }
241
+ return out
242
+ }
243
+
244
+ /**
245
+ * 判断某条变化路径是否影响到订阅路径
246
+ *
247
+ * 双向前缀匹配:改 `bot` 影响订阅 `bot.masterQQ` 的人(父变子受影响),
248
+ * 改 `bot.masterQQ` 也影响订阅 `bot` 的人(子变父受影响)。
249
+ * @param changed 发生变化的路径
250
+ * @param watched 订阅的路径;空串表示订阅全部
251
+ * @returns 是否受影响
252
+ */
253
+ export function pathAffects(changed: string, watched: string): boolean {
254
+ if (watched === "" || changed === "") return true
255
+ if (changed === watched) return true
256
+ return changed.startsWith(`${watched}.`) || watched.startsWith(`${changed}.`)
257
+ }
258
+
259
+ /**
260
+ * 摘出对象里的指定路径,组成新对象
261
+ * @param obj 源对象
262
+ * @param paths 路径列表
263
+ * @returns 只含这些路径的新对象
264
+ */
265
+ export function pickPaths(obj: PlainObject, paths: readonly string[]): PlainObject {
266
+ const out: PlainObject = {}
267
+ for (const path of paths) {
268
+ const value = getPath(obj, path)
269
+ if (value !== undefined) setPath(out, path, deepClone(value))
270
+ }
271
+ return out
272
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * 模块职责:Promise 原语(外部可控 Promise、超时、休眠、重试)
3
+ * 依赖方向:仅依赖类型包
4
+ * 生命周期:纯函数 / 短生命周期对象
5
+ * 注意事项:所有会挂起的等待均必须可被取消,否则热重载时插件已卸载、
6
+ * 而 Promise 仍持有定时器,即构成内存泄漏。因此 `Deferred` 暴露
7
+ * `reject`,`withTimeout` 必定清除 timer,`sleep` 支持 signal。
8
+ */
9
+
10
+ /** 超时错误:与普通业务错误相区分,便于上层决定"重试"或"放弃" */
11
+ export class TimeoutError extends Error {
12
+ /** 错误名,便于 `err.name === "TimeoutError"` 判定 */
13
+ override readonly name = "TimeoutError"
14
+
15
+ /**
16
+ * @param message 错误信息
17
+ */
18
+ constructor(message = "操作超时") {
19
+ super(message)
20
+ }
21
+ }
22
+
23
+ /** 中止错误:signal 触发时抛出 */
24
+ export class AbortError extends Error {
25
+ /** 错误名 */
26
+ override readonly name = "AbortError"
27
+
28
+ /**
29
+ * @param message 错误信息
30
+ */
31
+ constructor(message = "操作已被取消") {
32
+ super(message)
33
+ }
34
+ }
35
+
36
+ /** 外部可控的 Promise */
37
+ export interface Deferred<T> {
38
+ /** Promise 本体 */
39
+ readonly promise: Promise<T>
40
+ /**
41
+ * 兑现
42
+ * @param value 结果值
43
+ */
44
+ resolve(value: T | PromiseLike<T>): void
45
+ /**
46
+ * 拒绝
47
+ * @param reason 原因
48
+ */
49
+ reject(reason?: unknown): void
50
+ /** 是否已结束(兑现或拒绝) */
51
+ readonly settled: boolean
52
+ }
53
+
54
+ /**
55
+ * 创建外部可控的 Promise
56
+ *
57
+ * 用于把"回调/事件"风格的等待(如适配器 echo 应答、`e.prompt`)转成 await。
58
+ * 重复调用 resolve/reject 会被忽略,不会抛错。
59
+ * @returns Deferred 句柄
60
+ */
61
+ export function defer<T>(): Deferred<T> {
62
+ let resolveFn!: (value: T | PromiseLike<T>) => void
63
+ let rejectFn!: (reason?: unknown) => void
64
+ let settled = false
65
+
66
+ const promise = new Promise<T>((res, rej) => {
67
+ resolveFn = res
68
+ rejectFn = rej
69
+ })
70
+
71
+ return {
72
+ promise,
73
+ resolve(value) {
74
+ if (settled) return
75
+ settled = true
76
+ resolveFn(value)
77
+ },
78
+ reject(reason) {
79
+ if (settled) return
80
+ settled = true
81
+ rejectFn(reason)
82
+ },
83
+ get settled() {
84
+ return settled
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * 休眠
91
+ * @param ms 毫秒
92
+ * @param signal 可选中止信号;触发时以 AbortError 拒绝
93
+ * @returns 到期后兑现
94
+ */
95
+ export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
96
+ if (ms <= 0) return Promise.resolve()
97
+ return new Promise((resolve, reject) => {
98
+ if (signal?.aborted) {
99
+ reject(new AbortError())
100
+ return
101
+ }
102
+ const timer = setTimeout(() => {
103
+ signal?.removeEventListener("abort", onAbort)
104
+ resolve()
105
+ }, ms)
106
+ // Node 下 unref 让"只剩一个 sleep"时进程仍能退出
107
+ if (typeof timer.unref === "function") timer.unref()
108
+
109
+ /** 中止回调 */
110
+ function onAbort(): void {
111
+ clearTimeout(timer)
112
+ reject(new AbortError())
113
+ }
114
+ signal?.addEventListener("abort", onAbort, { once: true })
115
+ })
116
+ }
117
+
118
+ /**
119
+ * 给 Promise 套超时
120
+ *
121
+ * 注意:超时只是让调用方不再等待,**并不能真正取消**底层操作。需要真取消
122
+ * 的场景(HTTP 请求)请把 signal 传给底层。
123
+ * @param promise 被包装的 Promise
124
+ * @param ms 超时毫秒;<=0 表示不限时
125
+ * @param message 超时错误信息
126
+ * @returns 原结果,或以 TimeoutError 拒绝
127
+ */
128
+ export function withTimeout<T>(promise: Promise<T>, ms: number, message?: string): Promise<T> {
129
+ if (ms <= 0) return promise
130
+ return new Promise<T>((resolve, reject) => {
131
+ const timer = setTimeout(() => reject(new TimeoutError(message)), ms)
132
+ if (typeof timer.unref === "function") timer.unref()
133
+ promise.then(
134
+ value => {
135
+ clearTimeout(timer)
136
+ resolve(value)
137
+ },
138
+ reason => {
139
+ clearTimeout(timer)
140
+ reject(reason)
141
+ }
142
+ )
143
+ })
144
+ }
145
+
146
+ /** 重试选项 */
147
+ export interface RetryOptions {
148
+ /** 最大尝试次数(含首次),缺省 3 */
149
+ attempts?: number
150
+ /** 首次退避毫秒,缺省 500 */
151
+ baseDelay?: number
152
+ /** 退避上限毫秒,缺省 30000 */
153
+ maxDelay?: number
154
+ /** 退避倍率,缺省 2 */
155
+ factor?: number
156
+ /** 抖动比例 0~1,缺省 0.2;用于避免多账号重连时步调一致而对服务端造成过载 */
157
+ jitter?: number
158
+ /** 中止信号 */
159
+ signal?: AbortSignal
160
+ /**
161
+ * 每次失败后的回调
162
+ * @param err 本次错误
163
+ * @param attempt 已尝试次数(从 1 开始)
164
+ * @param delay 即将等待的毫秒
165
+ */
166
+ onRetry?: (err: unknown, attempt: number, delay: number) => void
167
+ }
168
+
169
+ /**
170
+ * 计算指数退避(带抖动)的等待时长
171
+ * @param attempt 已尝试次数(从 1 开始)
172
+ * @param opts 退避参数
173
+ * @returns 毫秒
174
+ */
175
+ export function backoffDelay(attempt: number, opts: RetryOptions = {}): number {
176
+ const base = opts.baseDelay ?? 500
177
+ const factor = opts.factor ?? 2
178
+ const max = opts.maxDelay ?? 30_000
179
+ const jitter = opts.jitter ?? 0.2
180
+
181
+ const raw = Math.min(max, base * factor ** Math.max(0, attempt - 1))
182
+ // 抖动只往下浮动,保证不会超过 maxDelay
183
+ const delta = raw * jitter * Math.random()
184
+ return Math.round(raw - delta)
185
+ }
186
+
187
+ /**
188
+ * 带指数退避的重试
189
+ * @param task 任务;参数为已尝试次数(从 1 开始)
190
+ * @param opts 重试选项
191
+ * @returns 任务结果
192
+ * @throws 最后一次的错误
193
+ */
194
+ export async function retry<T>(task: (attempt: number) => Promise<T>, opts: RetryOptions = {}): Promise<T> {
195
+ const attempts = Math.max(1, opts.attempts ?? 3)
196
+ let lastError: unknown
197
+
198
+ for (let attempt = 1; attempt <= attempts; attempt++) {
199
+ if (opts.signal?.aborted) throw new AbortError()
200
+ try {
201
+ return await task(attempt)
202
+ } catch (err) {
203
+ lastError = err
204
+ if (attempt >= attempts) break
205
+ const delay = backoffDelay(attempt, opts)
206
+ opts.onRetry?.(err, attempt, delay)
207
+ await sleep(delay, opts.signal)
208
+ }
209
+ }
210
+ throw lastError
211
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * 模块职责:资源回收登记簿
3
+ * 依赖方向:依赖类型包的 Disposer
4
+ * 生命周期:与其宿主(插件实例 / 账号连接 / 应用)同寿
5
+ * 注意事项:**内核提供给插件的每一个注册型 API 都返回 Disposer**,并自动登记到该插件的
6
+ * registry。插件卸载时逆序全量回收,故插件作者不需要(也无从忘记)手工清理 ——
7
+ * 否则改十次代码就有十份定时器在跑同一件事。
8
+ *
9
+ * 逆序回收是有意的:后注册的往往依赖先注册的(如先 provide 服务再
10
+ * 注册用到它的命令),倒着拆才不会拆到一半引用空对象。
11
+ */
12
+ import type { Disposer } from "@yunzai-ng/types"
13
+
14
+ /** 回收过程中收集到的错误 */
15
+ export interface DisposeFailure {
16
+ /** 登记时的标签 */
17
+ label: string
18
+ /** 抛出的错误 */
19
+ error: unknown
20
+ }
21
+
22
+ /**
23
+ * 资源回收登记簿
24
+ *
25
+ * 线程模型:同进程单线程,无需加锁;但 `dispose()` 可能重入
26
+ * (插件在 disposer 里又触发卸载),故有 `#disposed` 幂等保护。
27
+ */
28
+ export class DisposalRegistry {
29
+ /** 登记的回收器,按登记顺序存放 */
30
+ readonly #entries: { label: string; dispose: Disposer }[] = []
31
+ /** 是否已回收 */
32
+ #disposed = false
33
+ /** 归属标识,仅用于日志 */
34
+ readonly #owner: string
35
+
36
+ /**
37
+ * @param owner 归属标识(插件名等),出错时用于定位
38
+ */
39
+ constructor(owner: string) {
40
+ this.#owner = owner
41
+ }
42
+
43
+ /** 是否已回收 */
44
+ get disposed(): boolean {
45
+ return this.#disposed
46
+ }
47
+
48
+ /** 尚未回收的登记项数量 */
49
+ get size(): number {
50
+ return this.#entries.length
51
+ }
52
+
53
+ /**
54
+ * 登记一个回收器
55
+ *
56
+ * 若 registry 已回收,传入的 disposer 会**立即执行** —— 这防止"卸载竞态":
57
+ * 异步初始化的插件在卸载后才完成注册,资源没人回收。
58
+ * @param dispose 回收函数
59
+ * @param label 标签,用于报错定位
60
+ * @returns 取消登记并执行回收的函数;重复调用无副作用
61
+ */
62
+ add(dispose: Disposer, label = "anonymous"): Disposer {
63
+ if (this.#disposed) {
64
+ this.#runOne({ label, dispose })
65
+ return () => undefined
66
+ }
67
+
68
+ const entry = { label, dispose }
69
+ this.#entries.push(entry)
70
+
71
+ let done = false
72
+ return () => {
73
+ if (done) return
74
+ done = true
75
+ const index = this.#entries.indexOf(entry)
76
+ if (index >= 0) this.#entries.splice(index, 1)
77
+ this.#runOne(entry)
78
+ }
79
+ }
80
+
81
+ /**
82
+ * 登记一个 `Map`/`Set` 条目的清理
83
+ * @param collection 容器
84
+ * @param key 键
85
+ * @param label 标签
86
+ * @returns 回收函数
87
+ */
88
+ addEntry<K>(collection: { delete(key: K): unknown }, key: K, label = "entry"): Disposer {
89
+ return this.add(() => void collection.delete(key), label)
90
+ }
91
+
92
+ /**
93
+ * 登记一个定时器
94
+ *
95
+ * 顺带 `unref`,让"只剩定时器"时进程能正常退出。
96
+ * @param timer 定时器句柄
97
+ * @param label 标签
98
+ * @returns 回收函数
99
+ */
100
+ addTimer(timer: NodeJS.Timeout, label = "timer"): Disposer {
101
+ if (typeof timer.unref === "function") timer.unref()
102
+ return this.add(() => clearTimeout(timer), label)
103
+ }
104
+
105
+ /**
106
+ * 创建从属子登记簿
107
+ *
108
+ * 父簿回收时自动带走子簿。用于"插件里的账号连接"这类嵌套生命周期。
109
+ * @param owner 子簿归属标识
110
+ * @returns 子登记簿
111
+ */
112
+ child(owner: string): DisposalRegistry {
113
+ const sub = new DisposalRegistry(`${this.#owner}/${owner}`)
114
+ this.add(() => void sub.dispose(), `child:${owner}`)
115
+ return sub
116
+ }
117
+
118
+ /**
119
+ * 逆序回收全部登记项
120
+ *
121
+ * 单个 disposer 抛错**不会**中断后续回收 —— 一个插件写错清理逻辑不应该
122
+ * 导致其余资源全部泄漏。所有错误汇总返回,由调用方(插件宿主)记日志。
123
+ * @returns 回收过程中发生的错误列表;无错时为空数组
124
+ */
125
+ dispose(): DisposeFailure[] {
126
+ if (this.#disposed) return []
127
+ this.#disposed = true
128
+
129
+ const failures: DisposeFailure[] = []
130
+ // 逆序:后注册的可能依赖先注册的
131
+ for (let i = this.#entries.length - 1; i >= 0; i--) {
132
+ const failure = this.#runOne(this.#entries[i]!)
133
+ if (failure) failures.push(failure)
134
+ }
135
+ this.#entries.length = 0
136
+ return failures
137
+ }
138
+
139
+ /**
140
+ * 执行单个回收器并捕获错误
141
+ * @param entry 登记项
142
+ * @returns 出错时返回错误信息,否则 undefined
143
+ */
144
+ #runOne(entry: { label: string; dispose: Disposer }): DisposeFailure | undefined {
145
+ try {
146
+ entry.dispose()
147
+ return undefined
148
+ } catch (error) {
149
+ return { label: `${this.#owner}:${entry.label}`, error }
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * 把多个 Disposer 合成一个
156
+ * @param disposers 回收函数列表(可含 undefined,便于条件注册)
157
+ * @returns 合成后的回收函数;逆序执行,单个抛错不影响其余
158
+ */
159
+ export function combineDisposers(...disposers: (Disposer | undefined)[]): Disposer {
160
+ return () => {
161
+ for (let i = disposers.length - 1; i >= 0; i--) {
162
+ try {
163
+ disposers[i]?.()
164
+ } catch {
165
+ // 合成 disposer 拿不到 logger,只能忽略;需要记录的场景请用 DisposalRegistry
166
+ }
167
+ }
168
+ }
169
+ }