@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,259 @@
1
+ /**
2
+ * 模块职责:日志行的解析与彩色格式化
3
+ * 依赖方向:依赖 util/text
4
+ * 生命周期:纯函数
5
+ * 注意事项:自行实现而不使用 `pino-pretty`,理由与 rotate.ts 相同:pino-pretty 作为
6
+ * transport 运行于 worker 线程,单文件打包后无法定位入口;作为同步
7
+ * prettifier 又会引入一系列依赖。此处以 150 行完成,且可按需对齐中日韩
8
+ * 全角字符(`visualWidth`)。
9
+ *
10
+ * 颜色遵循约定:`NO_COLOR` 有值即禁用,`FORCE_COLOR` 有值即启用,
11
+ * 否则依据 stdout 是否为 TTY 判定。
12
+ */
13
+ import { inspect } from "node:util"
14
+ import type { LogLevel } from "@yunzai-ng/types"
15
+ import { padVisualEnd, visualWidth } from "../util/text.js"
16
+
17
+ /** pino 的数字级别 → 级别名 */
18
+ const LEVEL_NAMES: Record<number, LogLevel> = {
19
+ 10: "trace",
20
+ 20: "debug",
21
+ 30: "info",
22
+ 40: "warn",
23
+ 50: "error",
24
+ 60: "fatal"
25
+ }
26
+
27
+ /** 级别名 → pino 数字级别 */
28
+ export const LEVEL_VALUES: Record<Exclude<LogLevel, "silent">, number> = {
29
+ trace: 10,
30
+ debug: 20,
31
+ info: 30,
32
+ warn: 40,
33
+ error: 50,
34
+ fatal: 60
35
+ }
36
+
37
+ /** 控制台里显示的级别标签(定宽 5 列,便于竖向对齐) */
38
+ const LEVEL_LABELS: Record<LogLevel, string> = {
39
+ trace: "TRACE",
40
+ debug: "DEBUG",
41
+ info: "INFO ",
42
+ warn: "WARN ",
43
+ error: "ERROR",
44
+ fatal: "FATAL",
45
+ silent: " "
46
+ }
47
+
48
+ /** ANSI 前景色码 */
49
+ const LEVEL_COLORS: Record<LogLevel, string> = {
50
+ trace: "90", // 亮黑(灰)
51
+ debug: "36", // 青
52
+ info: "32", // 绿
53
+ warn: "33", // 黄
54
+ error: "31", // 红
55
+ fatal: "35", // 洋红
56
+ silent: "0"
57
+ }
58
+
59
+ /** pino 注入的、不该当作业务字段打印出来的键 */
60
+ const RESERVED_KEYS = new Set(["level", "time", "msg", "pid", "hostname", "v", "err", "scope"])
61
+
62
+ /**
63
+ * 判断当前是否应输出颜色
64
+ * @returns 是否启用颜色
65
+ */
66
+ export function supportsColor(): boolean {
67
+ if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "") return false
68
+ if (process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== "") return true
69
+ return process.stdout.isTTY === true
70
+ }
71
+
72
+ /**
73
+ * 给文本加 ANSI 颜色
74
+ * @param code 颜色码,如 `"32"`
75
+ * @param text 文本
76
+ * @param enabled 是否启用;false 时原样返回
77
+ * @returns 带颜色的文本
78
+ */
79
+ export function paint(code: string, text: string, enabled: boolean): string {
80
+ return enabled ? `\u001b[${code}m${text}\u001b[0m` : text
81
+ }
82
+
83
+ /** 解析后的日志记录 */
84
+ export interface LogRecord {
85
+ /** 级别名 */
86
+ level: LogLevel
87
+ /** 毫秒时间戳 */
88
+ time: number
89
+ /** 主消息 */
90
+ msg: string
91
+ /** 作用域(插件名 / 适配器名 / 账号 id) */
92
+ scope?: string
93
+ /** 错误堆栈 */
94
+ stack?: string
95
+ /** 其余业务字段 */
96
+ fields?: Record<string, unknown>
97
+ }
98
+
99
+ /**
100
+ * 解析 pino 输出的一行 JSON
101
+ * @param line 日志行(含尾部换行)
102
+ * @returns 解析结果;不是合法 JSON 时 undefined
103
+ */
104
+ export function parseRecord(line: string): LogRecord | undefined {
105
+ const text = line.trim()
106
+ if (text === "" || text[0] !== "{") return undefined
107
+
108
+ let raw: Record<string, unknown>
109
+ try {
110
+ raw = JSON.parse(text) as Record<string, unknown>
111
+ } catch {
112
+ return undefined
113
+ }
114
+
115
+ const levelNum = typeof raw.level === "number" ? raw.level : 30
116
+ const err = raw.err as { stack?: unknown; message?: unknown } | undefined
117
+
118
+ const fields: Record<string, unknown> = {}
119
+ for (const [key, value] of Object.entries(raw)) {
120
+ if (!RESERVED_KEYS.has(key)) fields[key] = value
121
+ }
122
+
123
+ return {
124
+ level: LEVEL_NAMES[levelNum] ?? "info",
125
+ time: typeof raw.time === "number" ? raw.time : Date.now(),
126
+ msg: typeof raw.msg === "string" ? raw.msg : typeof err?.message === "string" ? err.message : "",
127
+ scope: typeof raw.scope === "string" ? raw.scope : undefined,
128
+ stack: typeof err?.stack === "string" ? err.stack : undefined,
129
+ fields: Object.keys(fields).length > 0 ? fields : undefined
130
+ }
131
+ }
132
+
133
+ /**
134
+ * 格式化时间为 `HH:mm:ss.SSS`
135
+ * @param time 毫秒时间戳
136
+ * @returns 时间字符串
137
+ */
138
+ function formatTime(time: number): string {
139
+ const d = new Date(time)
140
+ const hh = String(d.getHours()).padStart(2, "0")
141
+ const mm = String(d.getMinutes()).padStart(2, "0")
142
+ const ss = String(d.getSeconds()).padStart(2, "0")
143
+ const ms = String(d.getMilliseconds()).padStart(3, "0")
144
+ return `${hh}:${mm}:${ss}.${ms}`
145
+ }
146
+
147
+ /** 控制台格式化选项 */
148
+ export interface FormatOptions {
149
+ /** 是否着色 */
150
+ color: boolean
151
+ /** 作用域列的对齐宽度,缺省 14;设 0 不对齐 */
152
+ scopeWidth?: number
153
+ /** 是否打印错误堆栈,缺省 true */
154
+ stack?: boolean
155
+ }
156
+
157
+ /**
158
+ * 把日志记录格式化为一行(或多行,含堆栈)控制台文本
159
+ * @param rec 日志记录
160
+ * @param opts 格式化选项
161
+ * @returns 带尾部换行的文本
162
+ */
163
+ export function formatRecord(rec: LogRecord, opts: FormatOptions): string {
164
+ const { color } = opts
165
+ const levelColor = LEVEL_COLORS[rec.level]
166
+
167
+ const time = paint("90", formatTime(rec.time), color)
168
+ const level = paint(levelColor, LEVEL_LABELS[rec.level], color)
169
+
170
+ let scope = ""
171
+ if (rec.scope) {
172
+ const width = opts.scopeWidth ?? 14
173
+ // 作用域超宽时不截断(宁可错行也不让用户看不出是哪个插件),
174
+ // 只在未超宽时补齐
175
+ const label = width > 0 && visualWidth(rec.scope) < width ? padVisualEnd(rec.scope, width) : rec.scope
176
+ scope = ` ${paint("36", label, color)}`
177
+ }
178
+
179
+ let extra = ""
180
+ if (rec.fields) {
181
+ const parts: string[] = []
182
+ for (const [key, value] of Object.entries(rec.fields)) {
183
+ parts.push(`${paint("90", `${key}=`, color)}${stringifyField(value)}`)
184
+ }
185
+ if (parts.length > 0) extra = ` ${parts.join(" ")}`
186
+ }
187
+
188
+ let out = `${time} ${level}${scope} ${rec.msg}${extra}\n`
189
+
190
+ if (rec.stack && opts.stack !== false) {
191
+ out += `${paint("90", indent(rec.stack), color)}\n`
192
+ }
193
+ return out
194
+ }
195
+
196
+ /**
197
+ * 缩进多行文本
198
+ * @param text 文本
199
+ * @returns 每行前加四空格
200
+ */
201
+ function indent(text: string): string {
202
+ return text
203
+ .split("\n")
204
+ .map(line => ` ${line}`)
205
+ .join("\n")
206
+ }
207
+
208
+ /**
209
+ * 将字段值转换为紧凑的单行文本
210
+ * @param value 字段值
211
+ * @returns 单行字符串
212
+ */
213
+ function stringifyField(value: unknown): string {
214
+ if (typeof value === "string") return value
215
+ if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value)
216
+ return inspect(value, { depth: 2, breakLength: Number.POSITIVE_INFINITY, compact: true, colors: false })
217
+ }
218
+
219
+ /**
220
+ * 将任意日志参数拼接为消息文本
221
+ *
222
+ * 不直接交由 pino 处理:pino 仅将第一个参数视为消息,其余参数仅在第一个参数含
223
+ * `%s`/`%d` 占位符时才被消费,否则**静默丢弃**。而 `logger.info("查询", uid, data)`
224
+ * 这类 console.log 写法很常见,故此处统一按 console.log 语义拼接。
225
+ * @param args 日志参数
226
+ * @returns 拼接后的消息与提取出的错误
227
+ */
228
+ export function joinArgs(args: readonly unknown[]): { msg: string; err?: Error } {
229
+ const parts: string[] = []
230
+ let err: Error | undefined
231
+
232
+ for (const arg of args) {
233
+ if (arg instanceof Error) {
234
+ // 第一个 Error 单独提出来,让 pino 的 err 序列化器保留堆栈
235
+ if (!err) err = arg
236
+ parts.push(arg.message)
237
+ continue
238
+ }
239
+ if (typeof arg === "string") {
240
+ parts.push(arg)
241
+ continue
242
+ }
243
+ if (arg === undefined) {
244
+ parts.push("undefined")
245
+ continue
246
+ }
247
+ if (arg === null) {
248
+ parts.push("null")
249
+ continue
250
+ }
251
+ if (typeof arg === "object") {
252
+ parts.push(inspect(arg, { depth: 3, breakLength: 120, compact: 3, colors: false }))
253
+ continue
254
+ }
255
+ parts.push(String(arg))
256
+ }
257
+
258
+ return { msg: parts.join(" "), err }
259
+ }
@@ -0,0 +1,427 @@
1
+ /**
2
+ * 模块职责:日志枢纽(pino 实例、控制台/文件双路输出、运行时改级别、尾部缓冲)
3
+ * 依赖方向:依赖 logger/format、logger/rotate、util
4
+ * 生命周期:应用级单例,`close()` 时刷盘
5
+ * 注意事项:三个不显然的设计决定 ——
6
+ *
7
+ * 1) **pino 实例常驻 trace,级别过滤放在包装层做**。pino 的 child
8
+ * logger 在创建时固化 level,改父级不会传播给已有子级;而 WebUI
9
+ * 上"把日志级别调成 debug"必须立刻对所有插件生效。所以过滤前移到
10
+ * `#emit`,顺带连 `joinArgs` 的字符串拼接都省掉了,比 pino 自带
11
+ * 的级别检查更省。
12
+ *
13
+ * 2) **参数按 console.log 语义拼接**(见 format.ts 的 `joinArgs`)。
14
+ * pino 原生仅识别第一个参数,`logger.info("查询", uid)` 将丢弃 uid。
15
+ *
16
+ * 3) **不采用 pino.multistream**。控制台与文件的级别过滤在自有的
17
+ * dispatcher 中完成,同时将日志写入 WebUI 的环形缓冲,一次解析三处使用。
18
+ */
19
+ import pino from "pino"
20
+ import type { Disposer, LogBindings, Logger, LogLevel } from "@yunzai-ng/types"
21
+ import { LEVEL_VALUES, type LogRecord, formatRecord, joinArgs, parseRecord, supportsColor } from "./format.js"
22
+ import { RotatingFileWriter } from "./rotate.js"
23
+
24
+ /** 日志配置 */
25
+ export interface LoggerConfig {
26
+ /** 全局级别,缺省 `"info"` */
27
+ level?: LogLevel
28
+ /** 文件输出级别,缺省与 `level` 相同 */
29
+ fileLevel?: LogLevel
30
+ /** 控制台输出级别,缺省与 `level` 相同 */
31
+ consoleLevel?: LogLevel
32
+ /** 日志目录;不给则只输出到控制台 */
33
+ dir?: string
34
+ /** 日志文件名前缀,缺省 `"app"` */
35
+ basename?: string
36
+ /** 是否输出到控制台,缺省 true */
37
+ console?: boolean
38
+ /** 是否着色,缺省自动探测 */
39
+ color?: boolean
40
+ /** 单文件字节上限 */
41
+ maxSize?: number
42
+ /** 日志保留天数 */
43
+ keepDays?: number
44
+ /** 日志文件数上限 */
45
+ maxFiles?: number
46
+ /** 尾部缓冲条数(供 WebUI 回看),缺省 500 */
47
+ tailSize?: number
48
+ /** 控制台作用域列宽,缺省 14 */
49
+ scopeWidth?: number
50
+ }
51
+
52
+ /** 尾部日志查询条件 */
53
+ export interface TailQuery {
54
+ /** 最多返回条数 */
55
+ limit?: number
56
+ /** 最低级别 */
57
+ level?: LogLevel
58
+ /** 作用域精确匹配 */
59
+ scope?: string
60
+ /** 消息子串匹配(不区分大小写) */
61
+ keyword?: string
62
+ }
63
+
64
+ /** 默认尾部缓冲条数 */
65
+ const DEFAULT_TAIL = 500
66
+
67
+ /**
68
+ * 级别名转数字,`silent` 视为最高
69
+ * @param level 级别名
70
+ * @returns 数字级别
71
+ */
72
+ function levelValue(level: LogLevel): number {
73
+ return level === "silent" ? Number.POSITIVE_INFINITY : LEVEL_VALUES[level]
74
+ }
75
+
76
+ /**
77
+ * 日志枢纽
78
+ *
79
+ * 内核只创建一个;插件通过 `ctx.logger`(即 `hub.root.child({ scope })`)取用。
80
+ */
81
+ export class LoggerHub {
82
+ /** pino 实例(级别恒为 trace,过滤在包装层做) */
83
+ readonly #pino: pino.Logger
84
+ /** 文件写入器 */
85
+ readonly #writer: RotatingFileWriter | undefined
86
+ /** 环形缓冲 */
87
+ readonly #ring: (LogRecord | undefined)[]
88
+ /** 环形缓冲写指针 */
89
+ #ringPos = 0
90
+ /** 环形缓冲是否已绕回(用于判断有效长度) */
91
+ #ringFull = false
92
+ /** 实时订阅者 */
93
+ readonly #subscribers = new Set<(rec: LogRecord) => void>()
94
+ /** 防止订阅者内部再打日志导致无限递归 */
95
+ #notifying = false
96
+
97
+ /** 全局级别 */
98
+ #level: LogLevel
99
+ /** 控制台级别数字 */
100
+ #consoleValue: number
101
+ /** 文件级别数字 */
102
+ #fileValue: number
103
+ /** 生效阈值:两路里更低的那个,低于它的日志直接不生成 */
104
+ #threshold: number
105
+
106
+ /** 是否输出控制台 */
107
+ readonly #console: boolean
108
+ /** 是否着色 */
109
+ readonly #color: boolean
110
+ /** 作用域列宽 */
111
+ readonly #scopeWidth: number
112
+
113
+ /** 根日志器 */
114
+ readonly root: Logger
115
+
116
+ /**
117
+ * @param config 日志配置
118
+ */
119
+ constructor(config: LoggerConfig = {}) {
120
+ this.#level = config.level ?? "info"
121
+ this.#console = config.console !== false
122
+ this.#color = config.color ?? supportsColor()
123
+ this.#scopeWidth = config.scopeWidth ?? 14
124
+ this.#ring = new Array<LogRecord | undefined>(Math.max(50, config.tailSize ?? DEFAULT_TAIL))
125
+
126
+ this.#consoleValue = this.#console ? levelValue(config.consoleLevel ?? this.#level) : Number.POSITIVE_INFINITY
127
+ this.#fileValue = config.dir ? levelValue(config.fileLevel ?? this.#level) : Number.POSITIVE_INFINITY
128
+ this.#threshold = Math.min(this.#consoleValue, this.#fileValue)
129
+
130
+ this.#writer = config.dir
131
+ ? new RotatingFileWriter({
132
+ dir: config.dir,
133
+ basename: config.basename,
134
+ maxSize: config.maxSize,
135
+ keepDays: config.keepDays,
136
+ maxFiles: config.maxFiles
137
+ })
138
+ : undefined
139
+
140
+ this.#pino = pino(
141
+ {
142
+ level: "trace",
143
+ // 去掉 pid/hostname:单机机器人用不上,白占日志体积
144
+ base: undefined,
145
+ serializers: { err: pino.stdSerializers.err }
146
+ },
147
+ { write: (line: string) => this.#dispatch(line) }
148
+ )
149
+
150
+ this.root = new HubLogger(this, this.#pino)
151
+ }
152
+
153
+ /** 当前全局级别 */
154
+ get level(): LogLevel {
155
+ return this.#level
156
+ }
157
+
158
+ /** 当前日志文件路径;未启用文件输出时 undefined */
159
+ get file(): string | undefined {
160
+ return this.#writer?.file
161
+ }
162
+
163
+ /**
164
+ * 判断某级别当前是否会被输出
165
+ *
166
+ * 由 `HubLogger` 在每次打日志前调用;命中 false 时连消息都不拼。
167
+ * @param level 目标级别
168
+ * @returns 是否启用
169
+ */
170
+ enabled(level: LogLevel): boolean {
171
+ return levelValue(level) >= this.#threshold
172
+ }
173
+
174
+ /**
175
+ * 运行时修改级别
176
+ *
177
+ * 立刻对所有已创建的子日志器生效 —— 这正是把过滤放在包装层的原因。
178
+ * @param level 新级别
179
+ * @param target 只改某一路:`"console"` / `"file"`;缺省两路都改
180
+ */
181
+ setLevel(level: LogLevel, target?: "console" | "file"): void {
182
+ const value = levelValue(level)
183
+ if (target === "console") {
184
+ if (this.#console) this.#consoleValue = value
185
+ } else if (target === "file") {
186
+ if (this.#writer) this.#fileValue = value
187
+ } else {
188
+ this.#level = level
189
+ if (this.#console) this.#consoleValue = value
190
+ if (this.#writer) this.#fileValue = value
191
+ }
192
+ this.#threshold = Math.min(this.#consoleValue, this.#fileValue)
193
+ }
194
+
195
+ /**
196
+ * 取尾部日志
197
+ * @param query 查询条件
198
+ * @returns 按时间正序的日志记录
199
+ */
200
+ tail(query: TailQuery = {}): LogRecord[] {
201
+ const size = this.#ring.length
202
+ const count = this.#ringFull ? size : this.#ringPos
203
+ const minLevel = query.level ? levelValue(query.level) : 0
204
+ const keyword = query.keyword?.toLowerCase()
205
+
206
+ const out: LogRecord[] = []
207
+ for (let i = 0; i < count; i++) {
208
+ // 从最旧的一条开始按环形顺序读
209
+ const index = this.#ringFull ? (this.#ringPos + i) % size : i
210
+ const rec = this.#ring[index]
211
+ if (!rec) continue
212
+ if (levelValue(rec.level) < minLevel) continue
213
+ if (query.scope && rec.scope !== query.scope) continue
214
+ if (keyword && !rec.msg.toLowerCase().includes(keyword)) continue
215
+ out.push(rec)
216
+ }
217
+
218
+ const limit = query.limit ?? out.length
219
+ return limit >= out.length ? out : out.slice(out.length - limit)
220
+ }
221
+
222
+ /**
223
+ * 订阅实时日志(WebUI 的 SSE / WebSocket 推送用)
224
+ * @param fn 回调;**不要在里面打日志**,会被静默丢弃以防递归
225
+ * @returns 取消订阅
226
+ */
227
+ subscribe(fn: (rec: LogRecord) => void): Disposer {
228
+ this.#subscribers.add(fn)
229
+ return () => void this.#subscribers.delete(fn)
230
+ }
231
+
232
+ /** 同步刷盘 */
233
+ flush(): void {
234
+ this.#writer?.flushSync()
235
+ }
236
+
237
+ /** 刷盘并关闭文件 */
238
+ close(): void {
239
+ this.#subscribers.clear()
240
+ this.#writer?.close()
241
+ }
242
+
243
+ /**
244
+ * 分发一行 pino 输出
245
+ *
246
+ * 一次解析,三处使用:控制台、文件、环形缓冲/订阅者。
247
+ * @param line pino 生成的 JSON 行
248
+ */
249
+ #dispatch(line: string): void {
250
+ const rec = parseRecord(line)
251
+ if (!rec) {
252
+ // 无法解析时原样输出至控制台,优于直接丢弃
253
+ if (this.#console) process.stdout.write(line)
254
+ return
255
+ }
256
+
257
+ const value = levelValue(rec.level)
258
+
259
+ if (value >= this.#consoleValue) {
260
+ const text = formatRecord(rec, { color: this.#color, scopeWidth: this.#scopeWidth })
261
+ // warn 及以上走 stderr,便于 `yzng start 2> error.log` 单独收集
262
+ if (value >= LEVEL_VALUES.warn) process.stderr.write(text)
263
+ else process.stdout.write(text)
264
+ }
265
+
266
+ if (this.#writer && value >= this.#fileValue) {
267
+ this.#writer.write(line)
268
+ // fatal 立即落盘:这行之后进程很可能就没了
269
+ if (value >= LEVEL_VALUES.fatal) this.#writer.flushSync()
270
+ }
271
+
272
+ this.#ring[this.#ringPos] = rec
273
+ this.#ringPos = (this.#ringPos + 1) % this.#ring.length
274
+ if (this.#ringPos === 0) this.#ringFull = true
275
+
276
+ this.#notify(rec)
277
+ }
278
+
279
+ /**
280
+ * 通知订阅者
281
+ * @param rec 日志记录
282
+ */
283
+ #notify(rec: LogRecord): void {
284
+ if (this.#subscribers.size === 0 || this.#notifying) return
285
+ this.#notifying = true
286
+ try {
287
+ for (const fn of this.#subscribers) {
288
+ try {
289
+ fn(rec)
290
+ } catch {
291
+ // 订阅者出错不能影响日志本身
292
+ }
293
+ }
294
+ } finally {
295
+ this.#notifying = false
296
+ }
297
+ }
298
+ }
299
+
300
+ /**
301
+ * 面向插件的日志器实现
302
+ *
303
+ * 只是 pino 的薄包装:加上级别前置过滤、console.log 风格参数拼接、`mark`。
304
+ */
305
+ class HubLogger implements Logger {
306
+ /** 所属枢纽 */
307
+ readonly #hub: LoggerHub
308
+ /** 底层 pino 实例(已绑定 scope 等字段) */
309
+ readonly #pino: pino.Logger
310
+
311
+ /**
312
+ * @param hub 日志枢纽
313
+ * @param instance pino 实例
314
+ */
315
+ constructor(hub: LoggerHub, instance: pino.Logger) {
316
+ this.#hub = hub
317
+ this.#pino = instance
318
+ }
319
+
320
+ /** 当前生效级别 */
321
+ get level(): LogLevel {
322
+ return this.#hub.level
323
+ }
324
+
325
+ /**
326
+ * 最细粒度的追踪日志
327
+ * @param msg 消息
328
+ * @param args 附加参数
329
+ */
330
+ trace(msg: unknown, ...args: unknown[]): void {
331
+ this.#emit("trace", msg, args)
332
+ }
333
+
334
+ /**
335
+ * 调试信息
336
+ * @param msg 消息
337
+ * @param args 附加参数
338
+ */
339
+ debug(msg: unknown, ...args: unknown[]): void {
340
+ this.#emit("debug", msg, args)
341
+ }
342
+
343
+ /**
344
+ * 常规信息
345
+ * @param msg 消息
346
+ * @param args 附加参数
347
+ */
348
+ info(msg: unknown, ...args: unknown[]): void {
349
+ this.#emit("info", msg, args)
350
+ }
351
+
352
+ /**
353
+ * 需要留意但不影响运行
354
+ * @param msg 消息
355
+ * @param args 附加参数
356
+ */
357
+ warn(msg: unknown, ...args: unknown[]): void {
358
+ this.#emit("warn", msg, args)
359
+ }
360
+
361
+ /**
362
+ * 出错但可继续
363
+ * @param msg 消息
364
+ * @param args 附加参数
365
+ */
366
+ error(msg: unknown, ...args: unknown[]): void {
367
+ this.#emit("error", msg, args)
368
+ }
369
+
370
+ /**
371
+ * 致命错误
372
+ * @param msg 消息
373
+ * @param args 附加参数
374
+ */
375
+ fatal(msg: unknown, ...args: unknown[]): void {
376
+ this.#emit("fatal", msg, args)
377
+ }
378
+
379
+ /**
380
+ * 重要信息,等价于 `info`
381
+ * @param msg 消息
382
+ * @param args 附加参数
383
+ */
384
+ mark(msg: unknown, ...args: unknown[]): void {
385
+ this.#emit("info", msg, args)
386
+ }
387
+
388
+ /**
389
+ * 派生子日志器
390
+ * @param bindings 固定字段
391
+ * @returns 新日志器
392
+ */
393
+ child(bindings: LogBindings): Logger {
394
+ return new HubLogger(this.#hub, this.#pino.child(bindings))
395
+ }
396
+
397
+ /**
398
+ * 判断级别是否启用
399
+ * @param level 目标级别
400
+ * @returns 是否启用
401
+ */
402
+ isLevelEnabled(level: LogLevel): boolean {
403
+ return this.#hub.enabled(level)
404
+ }
405
+
406
+ /**
407
+ * 实际写日志
408
+ * @param level 级别
409
+ * @param msg 首个参数
410
+ * @param args 其余参数
411
+ */
412
+ #emit(level: Exclude<LogLevel, "silent">, msg: unknown, args: readonly unknown[]): void {
413
+ if (!this.#hub.enabled(level)) return
414
+ const joined = joinArgs(args.length === 0 ? [msg] : [msg, ...args])
415
+ if (joined.err) this.#pino[level]({ err: joined.err }, joined.msg)
416
+ else this.#pino[level](joined.msg)
417
+ }
418
+ }
419
+
420
+ /**
421
+ * 创建日志枢纽
422
+ * @param config 日志配置
423
+ * @returns 日志枢纽
424
+ */
425
+ export function createLoggerHub(config: LoggerConfig = {}): LoggerHub {
426
+ return new LoggerHub(config)
427
+ }