@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,135 @@
1
+ /**
2
+ * 模块职责:`ServiceRegistry` 行为测试
3
+ * 依赖方向:测试文件
4
+ * 生命周期:每个用例一个注册表
5
+ * 注意事项:重点覆盖"取代 runtime.js 硬编码 getter"这项设计的边界:重复注册须抛错、
6
+ * 缺失须给出可操作的错误、卸载后不得残留失效引用、等待者不得阻止进程退出。
7
+ */
8
+ import { describe, expect, it } from "vitest"
9
+ import { createServiceRegistry } from "./services.js"
10
+
11
+ describe("ServiceRegistry", () => {
12
+ it("provide 后可被 get / has / require 取到", () => {
13
+ const reg = createServiceRegistry()
14
+ const api = { hello: () => "hi" }
15
+ reg.provide("mihoyo.api", api, "mhy-game")
16
+
17
+ expect(reg.has("mihoyo.api")).toBe(true)
18
+ expect(reg.get<typeof api>("mihoyo.api")).toBe(api)
19
+ expect(reg.require<typeof api>("mihoyo.api", "other")).toBe(api)
20
+ expect(reg.size).toBe(1)
21
+ })
22
+
23
+ it("键名非法时立即抛错", () => {
24
+ const reg = createServiceRegistry()
25
+ expect(() => reg.provide("2bad", 1, "p")).toThrow(/不合法/)
26
+ expect(() => reg.provide("has space", 1, "p")).toThrow(/不合法/)
27
+ expect(() => reg.provide("ok.key-1:sub", 1, "p")).not.toThrow()
28
+ })
29
+
30
+ it("同键重复注册抛错,且错误里点明是谁先占的", () => {
31
+ const reg = createServiceRegistry()
32
+ reg.provide("x.y", 1, "first")
33
+ expect(() => reg.provide("x.y", 2, "second")).toThrow(/first/)
34
+ // 冲突不该破坏已有记录
35
+ expect(reg.get("x.y")).toBe(1)
36
+ })
37
+
38
+ it("require 缺失时的错误同时说明谁要用、要什么、现在有什么", () => {
39
+ const reg = createServiceRegistry()
40
+ reg.provide("a.b", 1, "p1")
41
+ try {
42
+ reg.require("missing.key", "mhy-game")
43
+ expect.unreachable("require 应当抛错")
44
+ } catch (err) {
45
+ const msg = (err as Error).message
46
+ expect(msg).toContain("mhy-game")
47
+ expect(msg).toContain("missing.key")
48
+ expect(msg).toContain("a.b")
49
+ }
50
+ })
51
+
52
+ it("没有任何服务时 require 的错误也说得清楚", () => {
53
+ const reg = createServiceRegistry()
54
+ expect(() => reg.require("k", "p")).toThrow(/没有任何插件提供服务/)
55
+ })
56
+
57
+ it("disposer 摘除服务;重复调用无副作用", () => {
58
+ const reg = createServiceRegistry()
59
+ const undo = reg.provide("k", 1, "p")
60
+ undo()
61
+ undo()
62
+ expect(reg.has("k")).toBe(false)
63
+ expect(reg.size).toBe(0)
64
+ })
65
+
66
+ it("延迟到达的卸载 disposer 不会摘除后注册者的同名服务", () => {
67
+ const reg = createServiceRegistry()
68
+ const stale = reg.provide("k", "old", "old-plugin")
69
+ // 模拟:旧插件的服务先由 removeByOwner 摘除,新插件随后注册,旧 disposer 最后到达
70
+ reg.removeByOwner("old-plugin")
71
+ reg.provide("k", "new", "new-plugin")
72
+ stale()
73
+ expect(reg.get("k")).toBe("new")
74
+ })
75
+
76
+ it("waitFor 在服务已就绪时立即返回", async () => {
77
+ const reg = createServiceRegistry()
78
+ reg.provide("k", 42, "p")
79
+ await expect(reg.waitFor<number>("k")).resolves.toBe(42)
80
+ })
81
+
82
+ it("waitFor 等到后来的 provide", async () => {
83
+ const reg = createServiceRegistry()
84
+ const pending = reg.waitFor<string>("late.key")
85
+ expect(reg.pending()).toEqual(["late.key"])
86
+ reg.provide("late.key", "arrived", "p")
87
+ await expect(pending).resolves.toBe("arrived")
88
+ // 兑现后等待队列要清空,否则 list() 里的 waiting 永远挂着
89
+ expect(reg.pending()).toEqual([])
90
+ })
91
+
92
+ it("waitFor 超时返回 undefined 而不是抛错", async () => {
93
+ const reg = createServiceRegistry()
94
+ await expect(reg.waitFor("never", 10)).resolves.toBeUndefined()
95
+ })
96
+
97
+ it("多个等待者会被同一次 provide 一起唤醒", async () => {
98
+ const reg = createServiceRegistry()
99
+ const all = Promise.all([reg.waitFor<number>("k"), reg.waitFor<number>("k"), reg.waitFor<number>("k")])
100
+ reg.provide("k", 7, "p")
101
+ await expect(all).resolves.toEqual([7, 7, 7])
102
+ })
103
+
104
+ it("removeByOwner 摘除该插件的全部服务并返回键名", () => {
105
+ const reg = createServiceRegistry()
106
+ reg.provide("a", 1, "p1")
107
+ reg.provide("b", 2, "p1")
108
+ reg.provide("c", 3, "p2")
109
+
110
+ expect(reg.removeByOwner("p1").sort()).toEqual(["a", "b"])
111
+ expect(reg.size).toBe(1)
112
+ expect(reg.get("c")).toBe(3)
113
+ })
114
+
115
+ it("list 按键排序并带上等待者数量", async () => {
116
+ const reg = createServiceRegistry()
117
+ reg.provide("z.svc", 1, "p2")
118
+ reg.provide("a.svc", 2, "p1")
119
+ void reg.waitFor("a.svc")
120
+
121
+ const list = reg.list()
122
+ expect(list.map(i => i.key)).toEqual(["a.svc", "z.svc"])
123
+ expect(list[0]?.owner).toBe("p1")
124
+ // a.svc 已就绪,waitFor 会立即返回、不进等待队列
125
+ expect(list[0]?.waiting).toBe(0)
126
+ })
127
+
128
+ it("clear 唤醒全部等待者,避免停机时被阻塞", async () => {
129
+ const reg = createServiceRegistry()
130
+ const waiting = reg.waitFor("never.coming", 0)
131
+ reg.clear()
132
+ await expect(waiting).resolves.toBeUndefined()
133
+ expect(reg.size).toBe(0)
134
+ })
135
+ })
@@ -0,0 +1,261 @@
1
+ /**
2
+ * 模块职责:插件间服务注册表(provide / inject / require / waitFor)
3
+ * 依赖方向:依赖 util/defer、util/duration、类型包
4
+ * 生命周期:应用级单例;每个插件卸载时其提供的服务被自动摘除
5
+ * 注意事项:这是本框架对「插件间协作」的唯一答案。提供方 `ctx.provide("mihoyo.api", impl)`,
6
+ * 使用方 `ctx.inject("mihoyo.api")`;内核只维护一张 `key → 值` 的表,对表里装的是什么
7
+ * 毫无认知,故内核到任何插件都是零依赖。
8
+ *
9
+ * 它取代两种坏做法:把业务对象硬写成内核的 getter(那会让内核反向依赖插件,删掉一个插件
10
+ * 内核就起不来),以及插件之间靠 `global.xxx` 或跨插件相对路径互相摸(路径一改全体崩,
11
+ * 且无从知道谁在用谁)。
12
+ *
13
+ * 键的所有者被记录下来,插件卸载时它提供的服务一起消失 —— 使用方拿到的是 undefined,
14
+ * 而不是一个指向已卸载模块的失效引用。
15
+ */
16
+ import type { Disposer, DurationLike } from "@yunzai-ng/types"
17
+ import { defer, type Deferred } from "../util/defer.js"
18
+ import { parseDuration } from "../util/duration.js"
19
+
20
+ /** `waitFor` 的默认等待上限 */
21
+ const DEFAULT_WAIT = 30_000
22
+
23
+ /** 服务键的合法形式:字母开头,允许点号分域 */
24
+ const KEY_RE = /^[a-z][a-z0-9]*(?:[.:-][a-z0-9]+)*$/i
25
+
26
+ /** 一条服务记录 */
27
+ interface ServiceEntry {
28
+ /** 服务实例 */
29
+ value: unknown
30
+ /** 提供方插件名 */
31
+ owner: string
32
+ /** 注册时间(毫秒) */
33
+ since: number
34
+ }
35
+
36
+ /** 服务的对外描述(WebUI / `yzng doctor` 用) */
37
+ export interface ServiceInfo {
38
+ /** 服务键 */
39
+ key: string
40
+ /** 提供方插件名 */
41
+ owner: string
42
+ /** 注册时间(毫秒) */
43
+ since: number
44
+ /** 当前有多少个等待者(诊断"谁在等一个没人提供的服务") */
45
+ waiting: number
46
+ }
47
+
48
+ /**
49
+ * 服务注册表
50
+ *
51
+ * 线程模型:同进程单线程,无锁。
52
+ */
53
+ export class ServiceRegistry {
54
+ /** 已注册的服务 */
55
+ readonly #entries = new Map<string, ServiceEntry>()
56
+ /** 等待中的 `waitFor` 调用,按键分组 */
57
+ readonly #waiters = new Map<string, Set<Deferred<unknown>>>()
58
+
59
+ /** 已注册的服务数 */
60
+ get size(): number {
61
+ return this.#entries.size
62
+ }
63
+
64
+ /**
65
+ * 注册服务
66
+ *
67
+ * 同一键被两个插件同时提供时**抛出异常**,而非由后者覆盖前者:此类冲突
68
+ * 多数源于插件作者复制代码后未修改键名,静默覆盖只会使排障难以进行。
69
+ * @param key 服务键,建议 `"<插件域>.<能力>"`
70
+ * @param value 服务实例
71
+ * @param owner 提供方插件名
72
+ * @returns 注销句柄;注销后等待者重新回到"等待"状态
73
+ * @throws 键名非法或已被他人占用时
74
+ */
75
+ provide<T>(key: string, value: T, owner: string): Disposer {
76
+ if (!KEY_RE.test(key)) {
77
+ throw new Error(`服务键 ${key} 不合法:需以字母开头,只含字母数字与 . : -`)
78
+ }
79
+ const exists = this.#entries.get(key)
80
+ if (exists) {
81
+ throw new Error(`服务 ${key} 已由插件 ${exists.owner} 提供,${owner} 不能重复注册`)
82
+ }
83
+
84
+ const entry: ServiceEntry = { value, owner, since: Date.now() }
85
+ this.#entries.set(key, entry)
86
+ this.#flushWaiters(key, value)
87
+
88
+ let disposed = false
89
+ return () => {
90
+ if (disposed) return
91
+ disposed = true
92
+ // 仅当仍为自身那份记录时才删除:避免延迟到达的卸载摘除他方刚注册的服务
93
+ if (this.#entries.get(key) === entry) this.#entries.delete(key)
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 取服务
99
+ * @param key 服务键
100
+ * @returns 服务实例;未注册时 undefined
101
+ */
102
+ get<T>(key: string): T | undefined {
103
+ return this.#entries.get(key)?.value as T | undefined
104
+ }
105
+
106
+ /**
107
+ * 是否已注册
108
+ * @param key 服务键
109
+ * @returns 是否存在
110
+ */
111
+ has(key: string): boolean {
112
+ return this.#entries.has(key)
113
+ }
114
+
115
+ /**
116
+ * 取服务,缺失即抛错
117
+ *
118
+ * 错误信息中必须同时出现"由谁使用"与"需要什么",否则使用者仅看到
119
+ * "服务不存在",无从判断应当安装哪一个插件。
120
+ * @param key 服务键
121
+ * @param requester 使用方插件名,用于错误信息
122
+ * @returns 服务实例
123
+ * @throws 服务未注册时
124
+ */
125
+ require<T>(key: string, requester = "未知插件"): T {
126
+ const entry = this.#entries.get(key)
127
+ if (!entry) {
128
+ const available = [...this.#entries.keys()].sort()
129
+ const hint = available.length > 0 ? `当前可用:${available.join("、")}` : "当前没有任何插件提供服务"
130
+ throw new Error(`插件 ${requester} 需要服务 ${key},但没有插件提供它。${hint}`)
131
+ }
132
+ return entry.value as T
133
+ }
134
+
135
+ /**
136
+ * 等待服务就绪
137
+ *
138
+ * 用于弱依赖:A 需要使用 B 的能力,但 B 可能晚于 A 加载,亦可能并未安装。
139
+ * 超时返回 undefined 而非抛错 —— 弱依赖的语义即"不存在时亦可继续运行"。
140
+ * @param key 服务键
141
+ * @param timeout 等待上限,缺省 30s;传 0 表示不限时
142
+ * @returns 服务实例;超时返回 undefined
143
+ */
144
+ async waitFor<T>(key: string, timeout?: DurationLike): Promise<T | undefined> {
145
+ const ready = this.#entries.get(key)
146
+ if (ready) return ready.value as T
147
+
148
+ const ms = parseDuration(timeout, DEFAULT_WAIT)
149
+ const waiter = defer<unknown>()
150
+ let bucket = this.#waiters.get(key)
151
+ if (!bucket) {
152
+ bucket = new Set()
153
+ this.#waiters.set(key, bucket)
154
+ }
155
+ bucket.add(waiter)
156
+
157
+ /** 从等待队列中摘除自身,并一并清除空桶 */
158
+ const cleanup = (): void => {
159
+ bucket!.delete(waiter)
160
+ if (bucket!.size === 0) this.#waiters.delete(key)
161
+ }
162
+
163
+ if (ms <= 0) {
164
+ try {
165
+ return (await waiter.promise) as T
166
+ } finally {
167
+ cleanup()
168
+ }
169
+ }
170
+
171
+ // 超时按"没等到"处理:resolve(undefined) 而非 reject,
172
+ // 调用方就不必为一个正常结局写 try/catch
173
+ const timer = setTimeout(() => waiter.resolve(undefined), ms)
174
+ // unref:只剩这个等待时进程仍应能退出,否则插件写错一个 key 就永远关不掉
175
+ if (typeof timer.unref === "function") timer.unref()
176
+
177
+ try {
178
+ return (await waiter.promise) as T | undefined
179
+ } finally {
180
+ clearTimeout(timer)
181
+ cleanup()
182
+ }
183
+ }
184
+
185
+ /**
186
+ * 摘除某插件提供的全部服务
187
+ *
188
+ * 插件卸载时由宿主调用。尽管每个 `provide` 均返回 disposer 且已登记进
189
+ * DisposalRegistry,此处仍作为兜底:插件若在 setup 之外的异步回调中 provide,
190
+ * 有可能绕过登记。
191
+ * @param owner 插件名
192
+ * @returns 被摘除的服务键
193
+ */
194
+ removeByOwner(owner: string): string[] {
195
+ const removed: string[] = []
196
+ for (const [key, entry] of this.#entries) {
197
+ if (entry.owner !== owner) continue
198
+ this.#entries.delete(key)
199
+ removed.push(key)
200
+ }
201
+ return removed
202
+ }
203
+
204
+ /**
205
+ * 列出全部服务
206
+ * @returns 服务描述,按键名排序
207
+ */
208
+ list(): ServiceInfo[] {
209
+ return [...this.#entries.entries()]
210
+ .map(([key, entry]) => ({
211
+ key,
212
+ owner: entry.owner,
213
+ since: entry.since,
214
+ waiting: this.#waiters.get(key)?.size ?? 0
215
+ }))
216
+ .sort((a, b) => a.key.localeCompare(b.key))
217
+ }
218
+
219
+ /**
220
+ * 列出"存在等待者但无提供方"的服务键
221
+ *
222
+ * `yzng doctor` 以其给出"可能缺少某个插件"的提示。
223
+ * @returns 键名数组
224
+ */
225
+ pending(): string[] {
226
+ return [...this.#waiters.keys()].filter(key => !this.#entries.has(key)).sort()
227
+ }
228
+
229
+ /**
230
+ * 清空注册表并唤醒所有等待者
231
+ *
232
+ * 停机时调用,避免等待者阻止进程退出。
233
+ */
234
+ clear(): void {
235
+ this.#entries.clear()
236
+ for (const bucket of this.#waiters.values()) {
237
+ for (const waiter of bucket) waiter.resolve(undefined)
238
+ }
239
+ this.#waiters.clear()
240
+ }
241
+
242
+ /**
243
+ * 唤醒某个键上的全部等待者
244
+ * @param key 服务键
245
+ * @param value 服务实例
246
+ */
247
+ #flushWaiters(key: string, value: unknown): void {
248
+ const bucket = this.#waiters.get(key)
249
+ if (!bucket) return
250
+ // 先取快照再唤醒:resolve 的 finally 会改动这个 Set
251
+ for (const waiter of [...bucket]) waiter.resolve(value)
252
+ }
253
+ }
254
+
255
+ /**
256
+ * 创建服务注册表
257
+ * @returns 注册表实例
258
+ */
259
+ export function createServiceRegistry(): ServiceRegistry {
260
+ return new ServiceRegistry()
261
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * 模块职责:tar.gz 解包器的行为测试
3
+ * 依赖方向:测试文件,依赖 plugin/tar
4
+ * 生命周期:每个用例一套临时目录,afterEach 全部删除
5
+ * 注意事项:归档由 `buildTarGz` 现场构造,不使用预置的二进制夹具。解包器的校验对象是
6
+ * 归档头部的字节内容,而预置夹具无法表达"路径写成 `../`"这类恶意结构 ——
7
+ * 需要断言的正是这些结构被拒绝。
8
+ */
9
+ import { readFile, mkdtemp, rm } from "node:fs/promises"
10
+ import { tmpdir } from "node:os"
11
+ import { join } from "node:path"
12
+ import { afterEach, describe, expect, it } from "vitest"
13
+ import { isFile } from "../util/fs.js"
14
+ import { buildTarGz, extractTarGz, joinWithin, singleRoot } from "./tar.js"
15
+
16
+ let root: string | undefined
17
+
18
+ /**
19
+ * 建一个临时目录,并返回其中的解包目标目录
20
+ *
21
+ * 目标目录嵌在临时根目录之内,因此"越界写入"若真的发生,落点也在本用例的清理范围内,
22
+ * 不会残留到系统临时目录并影响后续用例。
23
+ * @returns 解包目标目录绝对路径
24
+ */
25
+ async function temp(): Promise<string> {
26
+ root = await mkdtemp(join(tmpdir(), "yzng-tar-"))
27
+ return join(root, "dest")
28
+ }
29
+
30
+ afterEach(async () => {
31
+ if (root !== undefined) await rm(root, { recursive: true, force: true })
32
+ root = undefined
33
+ })
34
+
35
+ describe("extractTarGz", () => {
36
+ it("解出文件与目录,返回写入的相对路径", async () => {
37
+ const dir = await temp()
38
+ const archive = buildTarGz([
39
+ { path: "pkg/", content: "", type: "5" },
40
+ { path: "pkg/index.js", content: "export default 1\n" },
41
+ { path: "pkg/lib/util.js", content: "export const a = 1\n" }
42
+ ])
43
+
44
+ const written = await extractTarGz(archive, dir)
45
+
46
+ expect(written).toEqual(["pkg/index.js", "pkg/lib/util.js"])
47
+ expect(await readFile(join(dir, "pkg", "index.js"), "utf8")).toBe("export default 1\n")
48
+ expect(await readFile(join(dir, "pkg", "lib", "util.js"), "utf8")).toBe("export const a = 1\n")
49
+ })
50
+
51
+ it("strip 剥掉 GitHub 归档外层的仓库名目录", async () => {
52
+ const dir = await temp()
53
+ const archive = buildTarGz([{ path: "repo-abc123/index.js", content: "1" }])
54
+
55
+ const written = await extractTarGz(archive, dir, { strip: 1 })
56
+
57
+ expect(written).toEqual(["index.js"])
58
+ expect(await isFile(join(dir, "index.js"))).toBe(true)
59
+ })
60
+
61
+ it("剥空的条目被跳过,而不是写到目标目录本身", async () => {
62
+ const dir = await temp()
63
+ const archive = buildTarGz([{ path: "repo/", content: "", type: "5" }, { path: "repo/a.js", content: "1" }])
64
+
65
+ const written = await extractTarGz(archive, dir, { strip: 1 })
66
+ expect(written).toEqual(["a.js"])
67
+ })
68
+
69
+ it("越出目标目录的条目被丢弃,其余条目照常解出", async () => {
70
+ const dir = await temp()
71
+ const archive = buildTarGz([
72
+ { path: "../escaped.js", content: "恶意内容" },
73
+ { path: "a/../../escaped2.js", content: "恶意内容" },
74
+ { path: "ok.js", content: "正常内容" }
75
+ ])
76
+
77
+ const written = await extractTarGz(archive, dir)
78
+
79
+ expect(written).toEqual(["ok.js"])
80
+ expect(await isFile(join(dir, "..", "escaped.js"))).toBe(false)
81
+ expect(await isFile(join(dir, "..", "escaped2.js"))).toBe(false)
82
+ })
83
+
84
+ it("绝对路径条目被丢弃", async () => {
85
+ const dir = await temp()
86
+ const archive = buildTarGz([{ path: "/tmp/yzng-absolute.js", content: "恶意内容" }, { path: "ok.js", content: "1" }])
87
+
88
+ const written = await extractTarGz(archive, dir)
89
+ expect(written).toEqual(["ok.js"])
90
+ })
91
+
92
+ it("软链接与硬链接条目被丢弃", async () => {
93
+ const dir = await temp()
94
+ const archive = buildTarGz([
95
+ { path: "link.js", content: "", type: "2" },
96
+ { path: "hard.js", content: "", type: "1" },
97
+ { path: "real.js", content: "1" }
98
+ ])
99
+
100
+ const written = await extractTarGz(archive, dir)
101
+
102
+ expect(written).toEqual(["real.js"])
103
+ expect(await isFile(join(dir, "link.js"))).toBe(false)
104
+ })
105
+
106
+ it("pax 扩展头部声明的长路径生效", async () => {
107
+ const dir = await temp()
108
+ const long = `deep/${"seg/".repeat(30)}index.js`
109
+ const record = `path=${long}\n`
110
+ const archive = buildTarGz([
111
+ { path: "PaxHeader", content: `${record.length + 4} ${record}`, type: "x" },
112
+ { path: "short.js", content: "1" }
113
+ ])
114
+
115
+ const written = await extractTarGz(archive, dir)
116
+ expect(written).toEqual([long])
117
+ })
118
+
119
+ it("条目数超过上限时抛错", async () => {
120
+ const dir = await temp()
121
+ const archive = buildTarGz([
122
+ { path: "a.js", content: "1" },
123
+ { path: "b.js", content: "1" },
124
+ { path: "c.js", content: "1" }
125
+ ])
126
+
127
+ await expect(extractTarGz(archive, dir, { maxEntries: 2 })).rejects.toThrow("条目数超过上限")
128
+ })
129
+
130
+ it("解包后体积超过上限时抛错", async () => {
131
+ const dir = await temp()
132
+ const archive = buildTarGz([{ path: "big.js", content: "x".repeat(4096) }])
133
+
134
+ await expect(extractTarGz(archive, dir, { maxBytes: 1024 })).rejects.toThrow("体积超过上限")
135
+ })
136
+ })
137
+
138
+ describe("singleRoot", () => {
139
+ it("唯一顶层目录时返回该目录名", () => {
140
+ expect(singleRoot(["repo-1/index.js", "repo-1/lib/a.js"])).toBe("repo-1")
141
+ })
142
+
143
+ it("存在顶层文件或多个顶层目录时返回 undefined", () => {
144
+ expect(singleRoot(["index.js", "lib/a.js"])).toBeUndefined()
145
+ expect(singleRoot(["a/x.js", "b/y.js"])).toBeUndefined()
146
+ })
147
+ })
148
+
149
+ describe("joinWithin", () => {
150
+ it("越界名称一律抛错", () => {
151
+ expect(() => joinWithin("/plugins", "../etc/passwd")).toThrow("路径越界")
152
+ expect(() => joinWithin("/plugins", "/etc/passwd")).toThrow("路径越界")
153
+ })
154
+
155
+ it("正常名称拼出目标路径", () => {
156
+ expect(joinWithin("/plugins", "demo")).toContain("demo")
157
+ })
158
+ })