@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,439 @@
1
+ /**
2
+ * 模块职责:渲染注册表 —— 实现 `RenderSink`,管理渲染器插件并择一执行渲染
3
+ * 依赖方向:依赖类型包、plugin/hooks 的接缝定义、util/*;**不认识 puppeteer**
4
+ * 生命周期:随内核创建;每个渲染器随提供方插件卸载而摘除;`stop()` 关闭全部渲染器
5
+ * 注意事项:内核对「怎么把 HTML 变成图片」零认知,只做三件事:择一可用者、补齐全局默认值、
6
+ * 失败时指明失败方。四项刻意的选择:
7
+ *
8
+ * **`available()` 的结果要缓存。** 它的实现可能是探测 Chromium 是否存在这类几十毫秒的
9
+ * 磁盘操作,每渲染一张图探一次会计进每条消息的响应时间。故按 TTL 缓存,且**探测本身
10
+ * 单例化** —— 十条消息同时请求渲染不该产生十次并发探测。
11
+ *
12
+ * **失败后立即让可用性缓存作废。** 浏览器崩溃这件事必须在下一次渲染时被发现。不可用
13
+ * 状态的 TTL 更短:使用者刚装好 Chromium,不该再等一分钟。
14
+ *
15
+ * **只在「该渲染器整体不可用」时切换**,此前先重试同一个。`render.retry` 的语义是本次
16
+ * 渲染重试几次(浏览器繁忙、页面偶发超时)。全部失败时把每个渲染器的原因**逐条**汇总
17
+ * 抛出 —— 只报最后一个的话,真实原因常在第一个里。
18
+ *
19
+ * **此处不限并发。** 同时渲染多张图会不会耗尽内存,取决于渲染器自己页面池的大小,
20
+ * 只有渲染器插件知道那个数。内核再加一层信号量只会与插件内部的池互相干扰。
21
+ */
22
+ import type {
23
+ Disposer,
24
+ Logger,
25
+ RenderDoneInfo,
26
+ RenderRequest,
27
+ RenderResult,
28
+ RendererProvider
29
+ } from "@yunzai-ng/types"
30
+ import type { RenderSink } from "../plugin/hooks.js"
31
+ import { SubsystemUnavailableError } from "../plugin/hooks.js"
32
+ import { TimeoutError, withTimeout } from "../util/defer.js"
33
+ import { formatBytes } from "../util/duration.js"
34
+
35
+ /** 可用状态的缓存时长(毫秒) */
36
+ const AVAILABLE_TTL_OK = 60_000
37
+
38
+ /** 不可用状态的缓存时长(毫秒):比可用短,见文件头第 2 点 */
39
+ const AVAILABLE_TTL_FAIL = 10_000
40
+
41
+ /** 渲染的兜底超时(毫秒),仅当配置读不到时使用 */
42
+ const FALLBACK_TIMEOUT = 60_000
43
+
44
+ /** 渲染相关的配置视图(对应配置项 `render.*`) */
45
+ export interface RenderPolicy {
46
+ /** 首选渲染器 id,对应 `render.default` */
47
+ readonly default: string
48
+ /** 单次渲染超时(毫秒) */
49
+ readonly timeout: number
50
+ /** 渲染失败重试次数 */
51
+ readonly retry: number
52
+ /** 图片质量 0-100 */
53
+ readonly quality: number
54
+ /** 缩放倍率 */
55
+ readonly scale: number
56
+ }
57
+
58
+ /** 注册表内部条目 */
59
+ interface RendererEntry {
60
+ /** 渲染器实现 */
61
+ readonly provider: RendererProvider
62
+ /** 提供方插件名 */
63
+ readonly owner: string
64
+ /** 可用性缓存:结果 */
65
+ ok: boolean | undefined
66
+ /** 可用性缓存:判定时刻 */
67
+ checkedAt: number
68
+ /** 可用性缓存:正在进行的探测(单例化,见文件头第 1 点) */
69
+ probing: Promise<boolean> | undefined
70
+ /** 最近一次失败原因,供 WebUI 展示 */
71
+ lastError: string | undefined
72
+ /** 累计成功次数 */
73
+ succeeded: number
74
+ /** 累计失败次数 */
75
+ failed: number
76
+ }
77
+
78
+ /** 渲染器的对外描述(WebUI "渲染" 页面的数据来源) */
79
+ export interface RendererInfo {
80
+ /** 渲染器 id */
81
+ readonly id: string
82
+ /** 展示名 */
83
+ readonly name: string
84
+ /** 提供方插件名 */
85
+ readonly owner: string
86
+ /** 是否为当前首选 */
87
+ readonly preferred: boolean
88
+ /** 最近判定的可用性;从未判定过时 undefined */
89
+ readonly available: boolean | undefined
90
+ /** 最近失败原因 */
91
+ readonly lastError: string | undefined
92
+ /** 累计成功次数 */
93
+ readonly succeeded: number
94
+ /** 累计失败次数 */
95
+ readonly failed: number
96
+ }
97
+
98
+ /** 渲染注册表构造参数 */
99
+ export interface RenderRegistryOptions {
100
+ /** 基础日志器 */
101
+ readonly logger: Logger
102
+ /**
103
+ * 取当前渲染配置
104
+ *
105
+ * 取函数而非直接传值:`render.*` 在 WebUI 里随时可改,配置存储会就地更新,
106
+ * 缓存一份快照就意味着"改了配置要重启才生效"。
107
+ */
108
+ readonly policy: () => RenderPolicy
109
+ /**
110
+ * 一次渲染结束后的通知(成功或全部渲染器失败)
111
+ *
112
+ * 取回调而非直接传 `CoreEventBus`:本文件对渲染实现零认知,认识事件总线
113
+ * 就等于认识插件系统。回调由 `installRuntime` 传入 —— 它本来就持有总线。
114
+ *
115
+ * 省略即不通知,现有调用方(含测试)因此无须改动。
116
+ */
117
+ readonly onDone?: (info: RenderDoneInfo) => void
118
+ }
119
+
120
+ /**
121
+ * 取错误的可读描述
122
+ * @param err 任意抛出物
123
+ * @returns 描述文本
124
+ */
125
+ function errText(err: unknown): string {
126
+ return err instanceof Error ? err.message : String(err)
127
+ }
128
+
129
+ /**
130
+ * 渲染注册表
131
+ *
132
+ * 既是 `ctx.registerRenderer()` 的落点,也是 `ctx.render()` / `e.render()` 的执行处。
133
+ */
134
+ export class RenderRegistry implements RenderSink {
135
+ /** 日志器 */
136
+ readonly #logger: Logger
137
+ /** 取配置 */
138
+ readonly #policy: () => RenderPolicy
139
+ /** 渲染器 id → 条目,按注册顺序 */
140
+ readonly #entries = new Map<string, RendererEntry>()
141
+ /** 正在进行的渲染器关闭操作,`stop()` 要等它们结束 */
142
+ readonly #closing = new Set<Promise<void>>()
143
+ /** 渲染结束通知,见构造参数的说明 */
144
+ readonly #onDone: ((info: RenderDoneInfo) => void) | undefined
145
+
146
+ /**
147
+ * @param opts 构造参数
148
+ */
149
+ constructor(opts: RenderRegistryOptions) {
150
+ this.#logger = opts.logger.child({ scope: "render" })
151
+ this.#policy = opts.policy
152
+ this.#onDone = opts.onDone
153
+ }
154
+
155
+ /** 已注册的渲染器数量 */
156
+ get size(): number {
157
+ return this.#entries.size
158
+ }
159
+
160
+ /**
161
+ * 注册一个渲染器
162
+ * @param provider 渲染器实现
163
+ * @param owner 提供方插件名
164
+ * @returns 注销句柄
165
+ * @throws 该 id 已被别的插件占用时
166
+ */
167
+ register(provider: RendererProvider, owner: string): Disposer {
168
+ const existing = this.#entries.get(provider.id)
169
+ if (existing !== undefined) {
170
+ // 与适配器注册表同样的理由:id 是配置项 render.default 引用实现的凭据,
171
+ // 悄悄覆盖会让用户以为自己用的是另一个渲染器
172
+ throw new Error(
173
+ `渲染器 id ${provider.id} 已被插件 ${existing.owner} 注册,插件 ${owner} 不能重复注册;请改用不同的 id`
174
+ )
175
+ }
176
+
177
+ const entry: RendererEntry = {
178
+ provider,
179
+ owner,
180
+ ok: undefined,
181
+ checkedAt: 0,
182
+ probing: undefined,
183
+ lastError: undefined,
184
+ succeeded: 0,
185
+ failed: 0
186
+ }
187
+ this.#entries.set(provider.id, entry)
188
+ this.#logger.info(`渲染器就绪:${provider.name ?? provider.id}(${provider.id},来自插件 ${owner})`)
189
+
190
+ return () => this.#remove(provider.id, entry)
191
+ }
192
+
193
+ /**
194
+ * 执行一次渲染
195
+ * @param req 渲染请求(模板根等已由插件上下文补全)
196
+ * @returns 渲染结果
197
+ * @throws 没有任何渲染器、没有可用渲染器、或全部渲染器都失败时
198
+ */
199
+ async render(req: RenderRequest): Promise<RenderResult> {
200
+ if (this.#entries.size === 0) {
201
+ throw new SubsystemUnavailableError("渲染", "没有安装任何渲染器插件,建议安装 renderer-puppeteer")
202
+ }
203
+
204
+ const policy = this.#policy()
205
+ const full = this.#applyDefaults(req, policy)
206
+ const timeout = policy.timeout > 0 ? policy.timeout : FALLBACK_TIMEOUT
207
+ const attempts = Math.max(0, policy.retry) + 1
208
+
209
+ /**
210
+ * 整个 `render()` 的起点
211
+ *
212
+ * 全灭时上报的耗时取这一档,而非最后一次尝试的耗时:使用者等的是「这次渲染
213
+ * 花了多久才失败」,那包含全部渲染器的全部重试。成功路径不用它 ——
214
+ * 那里取渲染器自报的 `result.cost`,与「出图快慢」对得上。
215
+ */
216
+ const startedAll = Date.now()
217
+
218
+ /** 每个渲染器的失败原因,全灭时一起抛出 */
219
+ const reasons: string[] = []
220
+
221
+ for (const entry of this.#candidates(policy.default)) {
222
+ const id = entry.provider.id
223
+ if (!(await this.#available(entry))) {
224
+ reasons.push(`${id}:不可用(${entry.lastError ?? "available() 返回 false"})`)
225
+ continue
226
+ }
227
+
228
+ for (let attempt = 1; attempt <= attempts; attempt++) {
229
+ const started = Date.now()
230
+ try {
231
+ const result = await withTimeout(entry.provider.render(full), timeout, `渲染器 ${id} 超时(${timeout}ms)`)
232
+ entry.succeeded++
233
+ entry.lastError = undefined
234
+ const bytes = result.images.reduce((sum, img) => sum + img.byteLength, 0)
235
+ // 级别由 debug 提到 info:渲染是最耗时的一环(数百毫秒到数秒),
236
+ // 「消息进来了、图还没出」这段空白期正需要这行来说明进展。
237
+ // 附上字节数 —— 出图异常大往往是模板里漏了尺寸约束,只看张数看不出来
238
+ this.#logger.info(
239
+ `渲染完成:${req.template} 由 ${id} 出图 ${result.images.length} 张(${formatBytes(bytes)}),耗时 ${result.cost}ms`
240
+ )
241
+ this.#onDone?.({
242
+ renderer: id,
243
+ template: req.template,
244
+ images: result.images.length,
245
+ bytes,
246
+ cost: result.cost,
247
+ ok: true
248
+ })
249
+ return result
250
+ } catch (err) {
251
+ entry.failed++
252
+ entry.lastError = errText(err)
253
+ // 令可用性缓存作废:浏览器崩溃须能在下一次渲染时被发现,见文件头第 2 点
254
+ this.#invalidate(entry)
255
+
256
+ const cost = Date.now() - started
257
+ const kind = err instanceof TimeoutError ? "超时" : "失败"
258
+ if (attempt < attempts) {
259
+ this.#logger.warn(`渲染${kind}(${id},${req.template},${cost}ms),第 ${attempt}/${attempts} 次重试`)
260
+ continue
261
+ }
262
+ reasons.push(`${id}:${entry.lastError}`)
263
+ this.#logger.warn(`渲染器 ${id} 已重试 ${attempts} 次仍${kind}(${req.template})`)
264
+ }
265
+ }
266
+ }
267
+
268
+ // 逐条汇总而不是只报最后一个:见文件头第 3 点
269
+ const summary = `模板 ${req.template} 渲染失败,已尝试 ${this.#entries.size} 个渲染器:\n ${reasons.join("\n ")}`
270
+ // 全灭也报一次:统计要算成功率,只在成功时报则分母恒等于分子。
271
+ // `renderer` 取空串 —— 这一次没有任何渲染器出图,填谁都是错的
272
+ this.#onDone?.({
273
+ renderer: "",
274
+ template: req.template,
275
+ images: 0,
276
+ bytes: 0,
277
+ cost: Date.now() - startedAll,
278
+ ok: false,
279
+ error: summary
280
+ })
281
+ throw new Error(summary)
282
+ }
283
+
284
+ /**
285
+ * 列出已注册的渲染器
286
+ * @returns 渲染器描述数组,首选排在最前
287
+ */
288
+ list(): RendererInfo[] {
289
+ const preferred = this.#policy().default
290
+ return [...this.#candidates(preferred)].map(entry => ({
291
+ id: entry.provider.id,
292
+ name: entry.provider.name ?? entry.provider.id,
293
+ owner: entry.owner,
294
+ preferred: entry.provider.id === preferred,
295
+ available: entry.ok,
296
+ lastError: entry.lastError,
297
+ succeeded: entry.succeeded,
298
+ failed: entry.failed
299
+ }))
300
+ }
301
+
302
+ /**
303
+ * 摘除某插件注册的全部渲染器
304
+ *
305
+ * 兜底路径:插件上下文正常会逐条调用 Disposer,但插件 setup 中途抛错时
306
+ * 可能存在未经 ctx 的登记。
307
+ * @param owner 插件名
308
+ * @returns 摘除的条数
309
+ */
310
+ removePlugin(owner: string): number {
311
+ let n = 0
312
+ for (const [id, entry] of [...this.#entries]) {
313
+ if (entry.owner === owner) {
314
+ this.#remove(id, entry)
315
+ n++
316
+ }
317
+ }
318
+ return n
319
+ }
320
+
321
+ /**
322
+ * 关闭全部渲染器(停机时调用)
323
+ *
324
+ * 会等待插件卸载时那些"发出去就不管"的 `dispose()` 收尾 —— 否则进程退出时
325
+ * 可能留下没杀掉的 Chromium 子进程。
326
+ */
327
+ async stop(): Promise<void> {
328
+ for (const [id, entry] of [...this.#entries]) this.#remove(id, entry)
329
+ if (this.#closing.size > 0) await Promise.allSettled([...this.#closing])
330
+ this.#closing.clear()
331
+ }
332
+
333
+ /**
334
+ * 按优先级排出候选渲染器
335
+ *
336
+ * 首选(`render.default`)排在第一位,其余按注册顺序。首选未注册时不报错 ——
337
+ * 使用者可能仅是尚未安装该插件,"能够产出图片"比"由指定渲染器产出图片"更为重要,
338
+ * 但选中其他渲染器时会留下一条日志。
339
+ * @param preferred 首选渲染器 id
340
+ * @returns 候选条目,按尝试顺序
341
+ */
342
+ *#candidates(preferred: string): Generator<RendererEntry> {
343
+ const first = this.#entries.get(preferred)
344
+ if (first !== undefined) yield first
345
+ for (const entry of this.#entries.values()) {
346
+ if (entry !== first) yield entry
347
+ }
348
+ }
349
+
350
+ /**
351
+ * 判断渲染器当前可用
352
+ *
353
+ * 带 TTL 缓存 + 单例探测,见文件头第 1 点。
354
+ * @param entry 条目
355
+ * @returns 是否可用
356
+ */
357
+ async #available(entry: RendererEntry): Promise<boolean> {
358
+ const now = Date.now()
359
+ if (entry.ok !== undefined) {
360
+ const ttl = entry.ok ? AVAILABLE_TTL_OK : AVAILABLE_TTL_FAIL
361
+ if (now - entry.checkedAt < ttl) return entry.ok
362
+ }
363
+ // 已有探测在跑就搭它的车:十条消息同时出图不该触发十次 Chromium 探测
364
+ if (entry.probing !== undefined) return entry.probing
365
+
366
+ const probing = (async (): Promise<boolean> => {
367
+ try {
368
+ return await entry.provider.available()
369
+ } catch (err) {
370
+ // available() 自己抛错等同于不可用:一个探测都做不成的渲染器不能拿来出图
371
+ entry.lastError = `available() 抛出异常:${errText(err)}`
372
+ return false
373
+ }
374
+ })().then(ok => {
375
+ entry.ok = ok
376
+ entry.checkedAt = Date.now()
377
+ entry.probing = undefined
378
+ return ok
379
+ })
380
+
381
+ entry.probing = probing
382
+ return probing
383
+ }
384
+
385
+ /**
386
+ * 让可用性缓存作废
387
+ * @param entry 条目
388
+ */
389
+ #invalidate(entry: RendererEntry): void {
390
+ entry.ok = undefined
391
+ entry.checkedAt = 0
392
+ }
393
+
394
+ /**
395
+ * 补齐全局缺省值
396
+ *
397
+ * 仅填充插件未指定的项:插件中硬编码的 `quality: 100` 表明其有相应理由,
398
+ * 全局配置不应覆盖它。
399
+ * @param req 原始请求
400
+ * @param policy 当前配置
401
+ * @returns 补全后的请求
402
+ */
403
+ #applyDefaults(req: RenderRequest, policy: RenderPolicy): RenderRequest {
404
+ return {
405
+ ...req,
406
+ quality: req.quality ?? policy.quality,
407
+ timeout: req.timeout ?? policy.timeout,
408
+ viewport: { ...req.viewport, scale: req.viewport?.scale ?? policy.scale }
409
+ }
410
+ }
411
+
412
+ /**
413
+ * 摘除一条注册并关闭渲染器
414
+ *
415
+ * 比对 entry 而不是只看 id:插件 A 卸载得晚、插件 B 已经用同一个 id 注册了
416
+ * 新实现时,A 的 Disposer 不应将 B 的实现摘除。
417
+ * @param id 渲染器 id
418
+ * @param entry 注册时的条目
419
+ */
420
+ #remove(id: string, entry: RendererEntry): void {
421
+ if (this.#entries.get(id) !== entry) return
422
+ this.#entries.delete(id)
423
+
424
+ const dispose = entry.provider.dispose
425
+ if (dispose === undefined) {
426
+ this.#logger.info(`渲染器 ${id} 已摘除(插件 ${entry.owner})`)
427
+ return
428
+ }
429
+
430
+ // Disposer 是同步的,而关浏览器是异步的:把 promise 记下来交给 stop() 等,
431
+ // 不能就这么丢掉 —— 丢掉的后果是一个残留的 Chromium 进程
432
+ const closing = Promise.resolve(dispose.call(entry.provider)).then(
433
+ () => void this.#logger.info(`渲染器 ${id} 已摘除并关闭(插件 ${entry.owner})`),
434
+ (err: unknown) => void this.#logger.error(`关闭渲染器 ${id} 时出错:${errText(err)}`, err)
435
+ )
436
+ this.#closing.add(closing)
437
+ void closing.finally(() => void this.#closing.delete(closing))
438
+ }
439
+ }