@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,306 @@
1
+ import { SubsystemUnavailableError } from "../plugin/hooks.js";
2
+ import { TimeoutError, withTimeout } from "../util/defer.js";
3
+ import { formatBytes } from "../util/duration.js";
4
+ /** 可用状态的缓存时长(毫秒) */
5
+ const AVAILABLE_TTL_OK = 60_000;
6
+ /** 不可用状态的缓存时长(毫秒):比可用短,见文件头第 2 点 */
7
+ const AVAILABLE_TTL_FAIL = 10_000;
8
+ /** 渲染的兜底超时(毫秒),仅当配置读不到时使用 */
9
+ const FALLBACK_TIMEOUT = 60_000;
10
+ /**
11
+ * 取错误的可读描述
12
+ * @param err 任意抛出物
13
+ * @returns 描述文本
14
+ */
15
+ function errText(err) {
16
+ return err instanceof Error ? err.message : String(err);
17
+ }
18
+ /**
19
+ * 渲染注册表
20
+ *
21
+ * 既是 `ctx.registerRenderer()` 的落点,也是 `ctx.render()` / `e.render()` 的执行处。
22
+ */
23
+ export class RenderRegistry {
24
+ /** 日志器 */
25
+ #logger;
26
+ /** 取配置 */
27
+ #policy;
28
+ /** 渲染器 id → 条目,按注册顺序 */
29
+ #entries = new Map();
30
+ /** 正在进行的渲染器关闭操作,`stop()` 要等它们结束 */
31
+ #closing = new Set();
32
+ /** 渲染结束通知,见构造参数的说明 */
33
+ #onDone;
34
+ /**
35
+ * @param opts 构造参数
36
+ */
37
+ constructor(opts) {
38
+ this.#logger = opts.logger.child({ scope: "render" });
39
+ this.#policy = opts.policy;
40
+ this.#onDone = opts.onDone;
41
+ }
42
+ /** 已注册的渲染器数量 */
43
+ get size() {
44
+ return this.#entries.size;
45
+ }
46
+ /**
47
+ * 注册一个渲染器
48
+ * @param provider 渲染器实现
49
+ * @param owner 提供方插件名
50
+ * @returns 注销句柄
51
+ * @throws 该 id 已被别的插件占用时
52
+ */
53
+ register(provider, owner) {
54
+ const existing = this.#entries.get(provider.id);
55
+ if (existing !== undefined) {
56
+ // 与适配器注册表同样的理由:id 是配置项 render.default 引用实现的凭据,
57
+ // 悄悄覆盖会让用户以为自己用的是另一个渲染器
58
+ throw new Error(`渲染器 id ${provider.id} 已被插件 ${existing.owner} 注册,插件 ${owner} 不能重复注册;请改用不同的 id`);
59
+ }
60
+ const entry = {
61
+ provider,
62
+ owner,
63
+ ok: undefined,
64
+ checkedAt: 0,
65
+ probing: undefined,
66
+ lastError: undefined,
67
+ succeeded: 0,
68
+ failed: 0
69
+ };
70
+ this.#entries.set(provider.id, entry);
71
+ this.#logger.info(`渲染器就绪:${provider.name ?? provider.id}(${provider.id},来自插件 ${owner})`);
72
+ return () => this.#remove(provider.id, entry);
73
+ }
74
+ /**
75
+ * 执行一次渲染
76
+ * @param req 渲染请求(模板根等已由插件上下文补全)
77
+ * @returns 渲染结果
78
+ * @throws 没有任何渲染器、没有可用渲染器、或全部渲染器都失败时
79
+ */
80
+ async render(req) {
81
+ if (this.#entries.size === 0) {
82
+ throw new SubsystemUnavailableError("渲染", "没有安装任何渲染器插件,建议安装 renderer-puppeteer");
83
+ }
84
+ const policy = this.#policy();
85
+ const full = this.#applyDefaults(req, policy);
86
+ const timeout = policy.timeout > 0 ? policy.timeout : FALLBACK_TIMEOUT;
87
+ const attempts = Math.max(0, policy.retry) + 1;
88
+ /**
89
+ * 整个 `render()` 的起点
90
+ *
91
+ * 全灭时上报的耗时取这一档,而非最后一次尝试的耗时:使用者等的是「这次渲染
92
+ * 花了多久才失败」,那包含全部渲染器的全部重试。成功路径不用它 ——
93
+ * 那里取渲染器自报的 `result.cost`,与「出图快慢」对得上。
94
+ */
95
+ const startedAll = Date.now();
96
+ /** 每个渲染器的失败原因,全灭时一起抛出 */
97
+ const reasons = [];
98
+ for (const entry of this.#candidates(policy.default)) {
99
+ const id = entry.provider.id;
100
+ if (!(await this.#available(entry))) {
101
+ reasons.push(`${id}:不可用(${entry.lastError ?? "available() 返回 false"})`);
102
+ continue;
103
+ }
104
+ for (let attempt = 1; attempt <= attempts; attempt++) {
105
+ const started = Date.now();
106
+ try {
107
+ const result = await withTimeout(entry.provider.render(full), timeout, `渲染器 ${id} 超时(${timeout}ms)`);
108
+ entry.succeeded++;
109
+ entry.lastError = undefined;
110
+ const bytes = result.images.reduce((sum, img) => sum + img.byteLength, 0);
111
+ // 级别由 debug 提到 info:渲染是最耗时的一环(数百毫秒到数秒),
112
+ // 「消息进来了、图还没出」这段空白期正需要这行来说明进展。
113
+ // 附上字节数 —— 出图异常大往往是模板里漏了尺寸约束,只看张数看不出来
114
+ this.#logger.info(`渲染完成:${req.template} 由 ${id} 出图 ${result.images.length} 张(${formatBytes(bytes)}),耗时 ${result.cost}ms`);
115
+ this.#onDone?.({
116
+ renderer: id,
117
+ template: req.template,
118
+ images: result.images.length,
119
+ bytes,
120
+ cost: result.cost,
121
+ ok: true
122
+ });
123
+ return result;
124
+ }
125
+ catch (err) {
126
+ entry.failed++;
127
+ entry.lastError = errText(err);
128
+ // 令可用性缓存作废:浏览器崩溃须能在下一次渲染时被发现,见文件头第 2 点
129
+ this.#invalidate(entry);
130
+ const cost = Date.now() - started;
131
+ const kind = err instanceof TimeoutError ? "超时" : "失败";
132
+ if (attempt < attempts) {
133
+ this.#logger.warn(`渲染${kind}(${id},${req.template},${cost}ms),第 ${attempt}/${attempts} 次重试`);
134
+ continue;
135
+ }
136
+ reasons.push(`${id}:${entry.lastError}`);
137
+ this.#logger.warn(`渲染器 ${id} 已重试 ${attempts} 次仍${kind}(${req.template})`);
138
+ }
139
+ }
140
+ }
141
+ // 逐条汇总而不是只报最后一个:见文件头第 3 点
142
+ const summary = `模板 ${req.template} 渲染失败,已尝试 ${this.#entries.size} 个渲染器:\n ${reasons.join("\n ")}`;
143
+ // 全灭也报一次:统计要算成功率,只在成功时报则分母恒等于分子。
144
+ // `renderer` 取空串 —— 这一次没有任何渲染器出图,填谁都是错的
145
+ this.#onDone?.({
146
+ renderer: "",
147
+ template: req.template,
148
+ images: 0,
149
+ bytes: 0,
150
+ cost: Date.now() - startedAll,
151
+ ok: false,
152
+ error: summary
153
+ });
154
+ throw new Error(summary);
155
+ }
156
+ /**
157
+ * 列出已注册的渲染器
158
+ * @returns 渲染器描述数组,首选排在最前
159
+ */
160
+ list() {
161
+ const preferred = this.#policy().default;
162
+ return [...this.#candidates(preferred)].map(entry => ({
163
+ id: entry.provider.id,
164
+ name: entry.provider.name ?? entry.provider.id,
165
+ owner: entry.owner,
166
+ preferred: entry.provider.id === preferred,
167
+ available: entry.ok,
168
+ lastError: entry.lastError,
169
+ succeeded: entry.succeeded,
170
+ failed: entry.failed
171
+ }));
172
+ }
173
+ /**
174
+ * 摘除某插件注册的全部渲染器
175
+ *
176
+ * 兜底路径:插件上下文正常会逐条调用 Disposer,但插件 setup 中途抛错时
177
+ * 可能存在未经 ctx 的登记。
178
+ * @param owner 插件名
179
+ * @returns 摘除的条数
180
+ */
181
+ removePlugin(owner) {
182
+ let n = 0;
183
+ for (const [id, entry] of [...this.#entries]) {
184
+ if (entry.owner === owner) {
185
+ this.#remove(id, entry);
186
+ n++;
187
+ }
188
+ }
189
+ return n;
190
+ }
191
+ /**
192
+ * 关闭全部渲染器(停机时调用)
193
+ *
194
+ * 会等待插件卸载时那些"发出去就不管"的 `dispose()` 收尾 —— 否则进程退出时
195
+ * 可能留下没杀掉的 Chromium 子进程。
196
+ */
197
+ async stop() {
198
+ for (const [id, entry] of [...this.#entries])
199
+ this.#remove(id, entry);
200
+ if (this.#closing.size > 0)
201
+ await Promise.allSettled([...this.#closing]);
202
+ this.#closing.clear();
203
+ }
204
+ /**
205
+ * 按优先级排出候选渲染器
206
+ *
207
+ * 首选(`render.default`)排在第一位,其余按注册顺序。首选未注册时不报错 ——
208
+ * 使用者可能仅是尚未安装该插件,"能够产出图片"比"由指定渲染器产出图片"更为重要,
209
+ * 但选中其他渲染器时会留下一条日志。
210
+ * @param preferred 首选渲染器 id
211
+ * @returns 候选条目,按尝试顺序
212
+ */
213
+ *#candidates(preferred) {
214
+ const first = this.#entries.get(preferred);
215
+ if (first !== undefined)
216
+ yield first;
217
+ for (const entry of this.#entries.values()) {
218
+ if (entry !== first)
219
+ yield entry;
220
+ }
221
+ }
222
+ /**
223
+ * 判断渲染器当前可用
224
+ *
225
+ * 带 TTL 缓存 + 单例探测,见文件头第 1 点。
226
+ * @param entry 条目
227
+ * @returns 是否可用
228
+ */
229
+ async #available(entry) {
230
+ const now = Date.now();
231
+ if (entry.ok !== undefined) {
232
+ const ttl = entry.ok ? AVAILABLE_TTL_OK : AVAILABLE_TTL_FAIL;
233
+ if (now - entry.checkedAt < ttl)
234
+ return entry.ok;
235
+ }
236
+ // 已有探测在跑就搭它的车:十条消息同时出图不该触发十次 Chromium 探测
237
+ if (entry.probing !== undefined)
238
+ return entry.probing;
239
+ const probing = (async () => {
240
+ try {
241
+ return await entry.provider.available();
242
+ }
243
+ catch (err) {
244
+ // available() 自己抛错等同于不可用:一个探测都做不成的渲染器不能拿来出图
245
+ entry.lastError = `available() 抛出异常:${errText(err)}`;
246
+ return false;
247
+ }
248
+ })().then(ok => {
249
+ entry.ok = ok;
250
+ entry.checkedAt = Date.now();
251
+ entry.probing = undefined;
252
+ return ok;
253
+ });
254
+ entry.probing = probing;
255
+ return probing;
256
+ }
257
+ /**
258
+ * 让可用性缓存作废
259
+ * @param entry 条目
260
+ */
261
+ #invalidate(entry) {
262
+ entry.ok = undefined;
263
+ entry.checkedAt = 0;
264
+ }
265
+ /**
266
+ * 补齐全局缺省值
267
+ *
268
+ * 仅填充插件未指定的项:插件中硬编码的 `quality: 100` 表明其有相应理由,
269
+ * 全局配置不应覆盖它。
270
+ * @param req 原始请求
271
+ * @param policy 当前配置
272
+ * @returns 补全后的请求
273
+ */
274
+ #applyDefaults(req, policy) {
275
+ return {
276
+ ...req,
277
+ quality: req.quality ?? policy.quality,
278
+ timeout: req.timeout ?? policy.timeout,
279
+ viewport: { ...req.viewport, scale: req.viewport?.scale ?? policy.scale }
280
+ };
281
+ }
282
+ /**
283
+ * 摘除一条注册并关闭渲染器
284
+ *
285
+ * 比对 entry 而不是只看 id:插件 A 卸载得晚、插件 B 已经用同一个 id 注册了
286
+ * 新实现时,A 的 Disposer 不应将 B 的实现摘除。
287
+ * @param id 渲染器 id
288
+ * @param entry 注册时的条目
289
+ */
290
+ #remove(id, entry) {
291
+ if (this.#entries.get(id) !== entry)
292
+ return;
293
+ this.#entries.delete(id);
294
+ const dispose = entry.provider.dispose;
295
+ if (dispose === undefined) {
296
+ this.#logger.info(`渲染器 ${id} 已摘除(插件 ${entry.owner})`);
297
+ return;
298
+ }
299
+ // Disposer 是同步的,而关浏览器是异步的:把 promise 记下来交给 stop() 等,
300
+ // 不能就这么丢掉 —— 丢掉的后果是一个残留的 Chromium 进程
301
+ const closing = Promise.resolve(dispose.call(entry.provider)).then(() => void this.#logger.info(`渲染器 ${id} 已摘除并关闭(插件 ${entry.owner})`), (err) => void this.#logger.error(`关闭渲染器 ${id} 时出错:${errText(err)}`, err));
302
+ this.#closing.add(closing);
303
+ void closing.finally(() => void this.#closing.delete(closing));
304
+ }
305
+ }
306
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/render/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * 模块职责:定时任务调度器 —— 实现 `TaskSink`,兼作 WebUI 的任务查询表
3
+ * 依赖方向:依赖类型包、plugin/hooks 的接缝定义、util/*、croner;**不认识任何插件实现**
4
+ * 生命周期:随内核创建;每个任务随注册插件卸载而注销;`stop()` 停机时清空
5
+ * 注意事项:每个任务都是注册表里的一行:`list()` 可查、Disposer 可注销、`stop()` 一次清空 ——
6
+ * 插件各自 `setInterval` 的话,热重载后会残留无人回收的定时器。四项刻意的选择:
7
+ *
8
+ * **重叠策略自己实现,不用 croner 的 `protect`。** 那个只对 cron 生效,而 `interval`
9
+ * 走 `setInterval`;两条路各用一套判定会让 `overlap` 的语义随 `kind` 漂移。
10
+ *
11
+ * **`queue` 策略有积压上限。** 一个每分钟触发、每次跑十分钟的任务会排出无限长的队列,
12
+ * 超过 `MAX_QUEUE` 者丢弃并计入 `skipped`。
13
+ *
14
+ * **超时后不再无限等待。** 先 abort 让守约定的任务自行收尾,但任务可能不响应 signal
15
+ * (同步死循环、不收 signal 的第三方调用)。只 abort 而不停止等待会让 `running` 永久为
16
+ * true,此后每轮都被跳过 —— 看着还在跑,实则再也不会触发。故宽限 `ABORT_GRACE` 后
17
+ * 停止等待,并记 error。
18
+ *
19
+ * **执行出错同样计为已执行。** `lastRun` 在开始时就写,不等成功 —— 隐去失败的那次会让
20
+ * 人误认为调度器没工作。
21
+ */
22
+ import type { Disposer, Logger, TaskInfo } from "@yunzai-ng/types";
23
+ import type { TaskRegistration, TaskSink } from "../plugin/hooks.js";
24
+ /** 调度器构造参数 */
25
+ export interface SchedulerOptions {
26
+ /** 基础日志器 */
27
+ readonly logger: Logger;
28
+ }
29
+ /**
30
+ * 定时任务调度器
31
+ *
32
+ * 既是 `ctx.cron()` / `ctx.interval()` 的落点(`TaskSink`),也是 WebUI
33
+ * "定时任务"页面的数据来源(`list()`)。
34
+ */
35
+ export declare class Scheduler implements TaskSink {
36
+ #private;
37
+ /**
38
+ * @param opts 构造参数
39
+ */
40
+ constructor(opts: SchedulerOptions);
41
+ /** 当前登记的任务数 */
42
+ get size(): number;
43
+ /**
44
+ * 登记一个任务
45
+ * @param reg 登记内容
46
+ * @returns 注销句柄
47
+ * @throws cron 表达式非法、或 interval 间隔非法时
48
+ */
49
+ register(reg: TaskRegistration): Disposer;
50
+ /**
51
+ * 列出全部任务
52
+ * @returns 任务信息数组,按登记顺序
53
+ */
54
+ list(): TaskInfo[];
55
+ /**
56
+ * 注销某插件的全部任务
57
+ *
58
+ * 兜底路径:插件上下文正常会逐条调 Disposer,但插件 setup 抛在半路时
59
+ * 可能有登记没走 ctx。
60
+ * @param plugin 插件名
61
+ * @returns 注销的任务数
62
+ */
63
+ removePlugin(plugin: string): number;
64
+ /** 停止全部任务(停机时调用) */
65
+ stop(): void;
66
+ }
67
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scheduler/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAU,QAAQ,EAAe,MAAM,kBAAkB,CAAA;AAEvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAiDpE,cAAc;AACd,MAAM,WAAW,gBAAgB;IAC/B,YAAY;IACZ,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAWD;;;;;GAKG;AACH,qBAAa,SAAU,YAAW,QAAQ;;IAQxC;;OAEG;gBACS,IAAI,EAAE,gBAAgB;IAIlC,eAAe;IACf,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,gBAAgB,GAAG,QAAQ;IAqDzC;;;OAGG;IACH,IAAI,IAAI,QAAQ,EAAE;IAalB;;;;;;;OAOG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAepC,oBAAoB;IACpB,IAAI,IAAI,IAAI;CAkGb"}
@@ -0,0 +1,279 @@
1
+ import { Cron } from "croner";
2
+ import { TimeoutError, withTimeout } from "../util/defer.js";
3
+ import { formatDuration, parseDuration } from "../util/duration.js";
4
+ /** `queue` 策略下的积压上限,见文件头第 2 点 */
5
+ const MAX_QUEUE = 3;
6
+ /** 超时 abort 之后再宽限多久才放弃等待(毫秒),见文件头第 3 点 */
7
+ const ABORT_GRACE = 5_000;
8
+ /**
9
+ * 取错误的可读描述
10
+ * @param err 任意抛出物
11
+ * @returns 描述文本
12
+ */
13
+ function errText(err) {
14
+ return err instanceof Error ? err.message : String(err);
15
+ }
16
+ /**
17
+ * 定时任务调度器
18
+ *
19
+ * 既是 `ctx.cron()` / `ctx.interval()` 的落点(`TaskSink`),也是 WebUI
20
+ * "定时任务"页面的数据来源(`list()`)。
21
+ */
22
+ export class Scheduler {
23
+ /** 日志器 */
24
+ #logger;
25
+ /** 全部任务,按登记顺序 */
26
+ #entries = [];
27
+ /** 是否已停机:停机后不再接受新任务 */
28
+ #stopped = false;
29
+ /**
30
+ * @param opts 构造参数
31
+ */
32
+ constructor(opts) {
33
+ this.#logger = opts.logger.child({ scope: "scheduler" });
34
+ }
35
+ /** 当前登记的任务数 */
36
+ get size() {
37
+ return this.#entries.length;
38
+ }
39
+ /**
40
+ * 登记一个任务
41
+ * @param reg 登记内容
42
+ * @returns 注销句柄
43
+ * @throws cron 表达式非法、或 interval 间隔非法时
44
+ */
45
+ register(reg) {
46
+ const options = reg.options;
47
+ const state = { lastRun: undefined, lastCost: undefined, running: false, skipped: 0, queued: 0 };
48
+ // 间隔任务允许写 "5m" 这类字符串:插件作者不该为了 ctx.interval 去手算毫秒
49
+ const intervalMs = reg.kind === "interval" ? parseDuration(reg.schedule, 0) : 0;
50
+ const schedule = reg.kind === "interval" ? `每 ${formatDuration(intervalMs)}` : String(reg.schedule);
51
+ const name = options.name ?? schedule;
52
+ const logger = this.#logger.child({ task: name, plugin: reg.plugin });
53
+ const runner = this.#createRunner(name, reg.fn, options, state, logger);
54
+ let job;
55
+ if (reg.kind === "interval") {
56
+ // 上界是 setInterval 的 32 位溢出点:超过它 Node 会立刻、反复触发,
57
+ // 与"每 25 天跑一次"的意图正好相反
58
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0 || intervalMs > 2_147_483_647) {
59
+ throw new Error(`任务 ${name} 的间隔非法(${String(reg.schedule)}):必须是 1 毫秒到 24.8 天之间的时长`);
60
+ }
61
+ if (intervalMs < 1_000)
62
+ logger.warn(`任务 ${name} 的间隔只有 ${intervalMs}ms,请确认这是有意的`);
63
+ job = createIntervalJob(intervalMs, runner);
64
+ }
65
+ else {
66
+ job = createCronJob(String(reg.schedule), options.timezone, runner, name);
67
+ }
68
+ const entry = { plugin: reg.plugin, name, schedule, state, job };
69
+ // 停机之后仍有插件在登记任务:不能默默持有一个永不回收的定时器,
70
+ // 直接停掉并返回空 Disposer,与 DisposalRegistry 的同类处理保持一致
71
+ if (this.#stopped) {
72
+ job.stop();
73
+ logger.debug(`调度器已停止,任务 ${name} 不再启动`);
74
+ return () => { };
75
+ }
76
+ this.#entries.push(entry);
77
+ this.#logger.info(`任务已登记:${name}(${schedule},来自插件 ${reg.plugin})`);
78
+ // 立即执行一次也走 runner:它内部不 await,所以不会拖慢插件 setup
79
+ if (options.immediate === true)
80
+ runner();
81
+ return () => {
82
+ const at = this.#entries.indexOf(entry);
83
+ if (at < 0)
84
+ return;
85
+ this.#entries.splice(at, 1);
86
+ job.stop();
87
+ // 已在跑的那一轮不会被打断(没有安全的中断手段),但积压一律作废:
88
+ // 插件都卸载了,再补跑几轮只会去操作已经回收掉的资源
89
+ state.queued = 0;
90
+ logger.debug(`任务已注销:${name}`);
91
+ };
92
+ }
93
+ /**
94
+ * 列出全部任务
95
+ * @returns 任务信息数组,按登记顺序
96
+ */
97
+ list() {
98
+ return this.#entries.map(entry => ({
99
+ name: entry.name,
100
+ schedule: entry.schedule,
101
+ plugin: entry.plugin,
102
+ nextRun: entry.job.nextRun(),
103
+ lastRun: entry.state.lastRun,
104
+ lastCost: entry.state.lastCost,
105
+ running: entry.state.running,
106
+ skipped: entry.state.skipped
107
+ }));
108
+ }
109
+ /**
110
+ * 注销某插件的全部任务
111
+ *
112
+ * 兜底路径:插件上下文正常会逐条调 Disposer,但插件 setup 抛在半路时
113
+ * 可能有登记没走 ctx。
114
+ * @param plugin 插件名
115
+ * @returns 注销的任务数
116
+ */
117
+ removePlugin(plugin) {
118
+ let n = 0;
119
+ // 自后向前删除:正序遍历会因 splice 改变下标而遗漏相邻的同名任务
120
+ for (let i = this.#entries.length - 1; i >= 0; i--) {
121
+ const entry = this.#entries[i];
122
+ if (entry === undefined || entry.plugin !== plugin)
123
+ continue;
124
+ this.#entries.splice(i, 1);
125
+ entry.job.stop();
126
+ entry.state.queued = 0;
127
+ n++;
128
+ }
129
+ if (n > 0)
130
+ this.#logger.debug(`已注销插件 ${plugin} 的 ${n} 个任务`);
131
+ return n;
132
+ }
133
+ /** 停止全部任务(停机时调用) */
134
+ stop() {
135
+ this.#stopped = true;
136
+ for (const entry of this.#entries) {
137
+ entry.job.stop();
138
+ entry.state.queued = 0;
139
+ }
140
+ this.#entries.length = 0;
141
+ }
142
+ /**
143
+ * 造出任务的触发回调
144
+ *
145
+ * 重叠策略、超时、错误处理全在这里,因此 cron 与 interval 两种触发器
146
+ * 的行为完全一致,见文件头第 1 点。
147
+ * @param name 任务名
148
+ * @param fn 任务体
149
+ * @param options 任务选项
150
+ * @param state 运行时状态
151
+ * @param logger 任务日志器
152
+ * @returns 触发回调(同步返回,不会向触发器抛错)
153
+ */
154
+ #createRunner(name, fn, options, state, logger) {
155
+ const timeoutMs = parseDuration(options.timeout, 0);
156
+ const queueing = options.overlap === "queue";
157
+ /** 跑一轮,永不 reject */
158
+ const once = async () => {
159
+ const controller = new AbortController();
160
+ const started = Date.now();
161
+ state.lastRun = started;
162
+ let timer;
163
+ if (timeoutMs > 0) {
164
+ // unref:任务卡住不该让进程无法退出
165
+ timer = setTimeout(() => controller.abort(), timeoutMs);
166
+ timer.unref?.();
167
+ }
168
+ try {
169
+ const task = Promise.resolve(fn(controller.signal));
170
+ if (timeoutMs > 0) {
171
+ // 此处已接住 task 的 rejection:宽限期过后调度器不再等待它,
172
+ // 但其延迟到达的失败不得转为 unhandledRejection 而终止进程
173
+ task.catch(() => { });
174
+ await withTimeout(task, timeoutMs + ABORT_GRACE, `任务 ${name} 超时 ${timeoutMs}ms 后仍未结束`);
175
+ }
176
+ else {
177
+ await task;
178
+ }
179
+ }
180
+ catch (err) {
181
+ if (err instanceof TimeoutError) {
182
+ // 见文件头第 3 点:任务无视了 abort 信号,只能放弃等待
183
+ logger.error(`任务 ${name} 超时后未响应中止信号,已放弃等待(任务可能仍在后台运行):${errText(err)}`);
184
+ }
185
+ else if (controller.signal.aborted) {
186
+ logger.warn(`任务 ${name} 因超时被中止(${timeoutMs}ms)`);
187
+ }
188
+ else {
189
+ logger.error(`任务 ${name} 执行失败:${errText(err)}`, err);
190
+ }
191
+ }
192
+ finally {
193
+ if (timer !== undefined)
194
+ clearTimeout(timer);
195
+ state.lastCost = Date.now() - started;
196
+ }
197
+ };
198
+ /** 连跑:先跑一轮,再把积压清空 */
199
+ const drain = async () => {
200
+ state.running = true;
201
+ try {
202
+ await once();
203
+ while (state.queued > 0) {
204
+ state.queued--;
205
+ await once();
206
+ }
207
+ }
208
+ finally {
209
+ state.running = false;
210
+ }
211
+ };
212
+ return () => {
213
+ if (!state.running) {
214
+ void drain();
215
+ return;
216
+ }
217
+ if (queueing && state.queued < MAX_QUEUE) {
218
+ state.queued++;
219
+ logger.debug(`任务 ${name} 上一轮未结束,本轮排队(积压 ${state.queued})`);
220
+ return;
221
+ }
222
+ state.skipped++;
223
+ const why = queueing ? `积压已达上限 ${MAX_QUEUE}` : "上一轮未结束";
224
+ // 第一次与每 10 次一报:一个长期重叠的任务不该把日志刷满,
225
+ // 但也不能一声不响 —— skipped 会一直涨,WebUI 上看得见
226
+ if (state.skipped === 1 || state.skipped % 10 === 0) {
227
+ logger.warn(`任务 ${name} ${why},跳过本轮(累计跳过 ${state.skipped} 次)`);
228
+ }
229
+ else {
230
+ logger.debug(`任务 ${name} ${why},跳过本轮`);
231
+ }
232
+ };
233
+ }
234
+ }
235
+ /**
236
+ * 造一个 cron 触发器
237
+ * @param pattern cron 表达式
238
+ * @param timezone 时区,缺省系统时区
239
+ * @param tick 触发回调
240
+ * @param name 任务名,仅用于报错
241
+ * @returns 触发器
242
+ * @throws 表达式非法时
243
+ */
244
+ function createCronJob(pattern, timezone, tick, name) {
245
+ let cron;
246
+ try {
247
+ // 三个参数写全:croner 的签名是 (pattern, fnOrOptions1, fnOrOptions2),
248
+ // 只传两个时选项会被当成回调
249
+ cron = new Cron(pattern, tick, { timezone, unref: true });
250
+ }
251
+ catch (err) {
252
+ // croner 的原始报错只说"哪个字段不认识",补上任务名与表达式才够排查
253
+ throw new Error(`任务 ${name} 的 cron 表达式非法(${pattern}):${errText(err)}`);
254
+ }
255
+ return {
256
+ nextRun: () => cron.nextRun()?.getTime(),
257
+ stop: () => void cron.stop()
258
+ };
259
+ }
260
+ /**
261
+ * 造一个固定间隔触发器
262
+ * @param ms 间隔毫秒
263
+ * @param tick 触发回调
264
+ * @returns 触发器
265
+ */
266
+ function createIntervalJob(ms, tick) {
267
+ let nextAt = Date.now() + ms;
268
+ const timer = setInterval(() => {
269
+ nextAt = Date.now() + ms;
270
+ tick();
271
+ }, ms);
272
+ // unref:只有定时任务在等的时候,进程应该可以正常退出
273
+ timer.unref?.();
274
+ return {
275
+ nextRun: () => nextAt,
276
+ stop: () => clearInterval(timer)
277
+ };
278
+ }
279
+ //# sourceMappingURL=index.js.map