@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,289 @@
1
+ /**
2
+ * 模块职责:探测本机的磁盘与 GPU —— 概览页两个进度组件的数据来源
3
+ * 依赖方向:仅依赖 node 内置模块与本目录的 detect;不认识 HTTP,也不认识面板
4
+ * 生命周期:无状态函数 + 一份短命缓存;缓存生存期见 `CACHE_MS`
5
+ * 注意事项:**CPU 与内存不在本模块。** 那两项已在 `sampleUsage()` 里并随 `GET /api/overview`
6
+ * 发给面板;此处再供一份就是同一事实的第二个来源,采样时刻还不同 —— 面板上会出现
7
+ * 「CPU 卡片和 CPU 环不是一个数」。四个不显而易见的决定:
8
+ *
9
+ * **磁盘走 `fs.statfs`,不调外部命令。** Windows 上 `Get-CimInstance` 实测约 813ms,
10
+ * 而 `statfs` 逐盘合计 3ms、字节数一致 —— 差两个数量级是因为起一个 PowerShell 进程就要
11
+ * 半秒,而这个端点被 5 秒一次地轮询。
12
+ *
13
+ * **「有几个分区」按平台分别回答。** Windows 探盘符;Linux 与安卓读 `/proc/mounts` 只留
14
+ * 真实块设备;macOS 与未知系统只报 `/` —— 为一个进度条搬 `getmntinfo` 不值得。
15
+ *
16
+ * **GPU 只认 nvidia-smi,取不到就当没有这件事。** 测不到时**不返回 `gpus` 字段**而非
17
+ * 返回空数组:空数组会被前端读成「确实有 0 块显卡」,而 0% 会被读成「GPU 空闲」。
18
+ *
19
+ * **`nvidia-smi` 不存在这件事只查一次。** 没有 N 卡的机器上每 5 秒 spawn 一个必然
20
+ * ENOENT 的进程,一天是一万七千次。
21
+ */
22
+ import { constants as fsConstants } from "node:fs"
23
+ import { access, readFile, statfs } from "node:fs/promises"
24
+ import { execFile } from "node:child_process"
25
+ import { detectPlatform } from "./detect.js"
26
+
27
+ /**
28
+ * 快照缓存生存期
29
+ *
30
+ * 取 4 秒而非 5 秒:面板按 5 秒轮询,缓存若与轮询同为 5 秒,两个周期的相位差会让
31
+ * 每隔几次出现一次「这次读的是上一次的数」。比轮询间隔短一点,则每次轮询必然重新采。
32
+ */
33
+ const CACHE_MS = 4000
34
+
35
+ /** 探测盘符的范围,理由同 server/browse.ts:A、B 是软驱字母 */
36
+ const DRIVE_LETTERS = "CDEFGHIJKLMNOPQRSTUVWXYZ"
37
+
38
+ /** 最多报告几个分区 */
39
+ const MAX_DISKS = 12
40
+
41
+ /** nvidia-smi 的超时毫秒 */
42
+ const GPU_TIMEOUT_MS = 3000
43
+
44
+ /**
45
+ * `/proc/mounts` 里不当作「分区」的文件系统类型
46
+ *
47
+ * squashfs 是 snap 包的只读镜像,一台装了十几个 snap 的 Ubuntu 上会有十几条,
48
+ * 且每条都「100% 已用」—— 那是只读镜像的常态,不是磁盘快满了。
49
+ */
50
+ const PSEUDO_FS = new Set(["squashfs", "iso9660", "ramfs", "tmpfs", "devtmpfs", "overlay", "aufs"])
51
+
52
+ /** 一个分区的占用 */
53
+ export interface DiskInfo {
54
+ /** 挂载点;Windows 上形如 `C:\` */
55
+ readonly mount: string
56
+ /** 总容量(字节) */
57
+ readonly total: number
58
+ /** 可用容量(字节),取非特权用户可用的那个数 */
59
+ readonly free: number
60
+ /** 已用容量(字节),即 `total - free` */
61
+ readonly used: number
62
+ }
63
+
64
+ /** 一块显卡 */
65
+ export interface GpuInfo {
66
+ /** 型号名 */
67
+ readonly name: string
68
+ /** 占用率(0-1);nvidia-smi 未给出时不出现 */
69
+ readonly load?: number
70
+ /** 显存已用(字节) */
71
+ readonly memoryUsed?: number
72
+ /** 显存总量(字节) */
73
+ readonly memoryTotal?: number
74
+ }
75
+
76
+ /** `GET /api/system` 的响应 */
77
+ export interface SystemInfo {
78
+ /** 各分区占用;一个都探不到时为空数组 */
79
+ readonly disks: readonly DiskInfo[]
80
+ /**
81
+ * 各显卡
82
+ *
83
+ * **测不到时本字段不出现**,与「有 0 块显卡」相区分,见文件头第 3 条。
84
+ */
85
+ readonly gpus?: readonly GpuInfo[]
86
+ }
87
+
88
+ /** 上一次快照与其采样时刻 */
89
+ let cache: { at: number; value: SystemInfo } | undefined
90
+
91
+ /** 是否已确认本机没有 nvidia-smi,见文件头第 4 条 */
92
+ let noNvidiaSmi = false
93
+
94
+ /**
95
+ * 列举现有的盘符
96
+ *
97
+ * Node 没有列举盘符的 API,只能逐个字母试。`access` 而非 `statfs`:后者在未插盘的
98
+ * 读卡器上会等到超时,前者只问「能不能访问」。
99
+ * @returns 形如 `C:\` 的盘根数组,按字母升序
100
+ */
101
+ export async function listDriveRoots(): Promise<string[]> {
102
+ const probes = [...DRIVE_LETTERS].map(async letter => {
103
+ const root = `${letter}:\\`
104
+ try {
105
+ await access(root, fsConstants.R_OK)
106
+ return root
107
+ } catch {
108
+ return undefined
109
+ }
110
+ })
111
+ const found = await Promise.all(probes)
112
+ return found.filter((root): root is string => root !== undefined)
113
+ }
114
+
115
+ /**
116
+ * 从 `/proc/mounts` 里挑出真实分区的挂载点
117
+ *
118
+ * 只留设备名以 `/` 开头的行:那是真实块设备,从而一举排除 proc、sysfs、cgroup、
119
+ * devpts 等一切伪文件系统 —— 逐个列举它们的名字迟早会漏。再按类型排掉只读镜像
120
+ * (见 `PSEUDO_FS`),并按挂载点去重(bind mount 会让同一处出现两次)。
121
+ * @returns 挂载点数组;读不到该文件时为空
122
+ */
123
+ async function readMountPoints(): Promise<string[]> {
124
+ let text: string
125
+ try {
126
+ text = await readFile("/proc/mounts", "utf8")
127
+ } catch {
128
+ return []
129
+ }
130
+
131
+ const seen = new Set<string>()
132
+ for (const line of text.split("\n")) {
133
+ const [device, rawMount, fstype] = line.split(/\s+/)
134
+ if (device === undefined || rawMount === undefined || fstype === undefined) continue
135
+ if (!device.startsWith("/")) continue
136
+ if (PSEUDO_FS.has(fstype)) continue
137
+ // /proc/mounts 用八进制转义空格与制表符,还原后才是真实路径
138
+ const mount = rawMount.replace(/\\040/g, " ").replace(/\\011/g, "\t")
139
+ seen.add(mount)
140
+ }
141
+ return [...seen].sort((a, b) => a.localeCompare(b))
142
+ }
143
+
144
+ /**
145
+ * 本机上要报告哪些挂载点
146
+ * @returns 挂载点数组
147
+ */
148
+ async function mountsToProbe(): Promise<string[]> {
149
+ const os = detectPlatform().os
150
+ if (os === "windows") return listDriveRoots()
151
+ if (os === "linux" || os === "android") {
152
+ const mounts = await readMountPoints()
153
+ // 读不到 /proc/mounts(权限受限的容器)时至少报根分区,而不是什么都不报
154
+ return mounts.length > 0 ? mounts : ["/"]
155
+ }
156
+ return ["/"]
157
+ }
158
+
159
+ /**
160
+ * 量一个挂载点
161
+ *
162
+ * 取 `bavail` 而不是 `bfree`:后者含为 root 预留的那一部分(ext4 默认 5%),
163
+ * 而面板上的「可用」要回答的是「我还能写进去多少」。
164
+ * @param mount 挂载点
165
+ * @returns 占用;量不到时 undefined
166
+ */
167
+ async function measure(mount: string): Promise<DiskInfo | undefined> {
168
+ try {
169
+ const st = await statfs(mount)
170
+ const total = Number(st.blocks) * Number(st.bsize)
171
+ const free = Number(st.bavail) * Number(st.bsize)
172
+ // 容量为 0 的挂载点无从画进度条:空的光驱、已卸载的容器层都会这样
173
+ if (!Number.isFinite(total) || total <= 0) return undefined
174
+ const capped = Math.min(Math.max(free, 0), total)
175
+ return { mount, total, free: capped, used: total - capped }
176
+ } catch {
177
+ // 权限不足、设备已拔出、网络盘掉线:跳过这一个,不影响其余
178
+ return undefined
179
+ }
180
+ }
181
+
182
+ /**
183
+ * 探测各分区占用
184
+ * @returns 各分区;一个都探不到时为空数组
185
+ */
186
+ export async function probeDisks(): Promise<DiskInfo[]> {
187
+ const mounts = await mountsToProbe()
188
+ const measured = await Promise.all(mounts.map(measure))
189
+ return measured.filter((item): item is DiskInfo => item !== undefined).slice(0, MAX_DISKS)
190
+ }
191
+
192
+ /**
193
+ * 跑一次 nvidia-smi
194
+ *
195
+ * 参数以数组传递、不经 shell。`windowsHide` 免得每 5 秒在 Windows 上闪一个黑框。
196
+ * @returns 标准输出;命令不存在或失败时 undefined
197
+ */
198
+ function runNvidiaSmi(): Promise<string | undefined> {
199
+ return new Promise(resolve => {
200
+ execFile(
201
+ "nvidia-smi",
202
+ ["--query-gpu=name,utilization.gpu,memory.used,memory.total", "--format=csv,noheader,nounits"],
203
+ { timeout: GPU_TIMEOUT_MS, windowsHide: true },
204
+ (err, stdout) => {
205
+ if (err) {
206
+ // ENOENT 意为本机没装驱动工具,此后不必再试;超时或其他错误则留待下次
207
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") noNvidiaSmi = true
208
+ resolve(undefined)
209
+ return
210
+ }
211
+ resolve(stdout)
212
+ }
213
+ )
214
+ })
215
+ }
216
+
217
+ /**
218
+ * 解析 nvidia-smi 的 CSV 输出
219
+ *
220
+ * 取不到的字段 nvidia-smi 会写 `[N/A]`(如虚拟化环境下的占用率),此时该字段
221
+ * **不出现**而不是记 0 —— 理由同文件头第 3 条。
222
+ * @param raw 标准输出
223
+ * @returns 各显卡;一行都解不出时为空数组
224
+ */
225
+ export function parseNvidiaSmi(raw: string): GpuInfo[] {
226
+ const gpus: GpuInfo[] = []
227
+ for (const line of raw.split("\n")) {
228
+ const cols = line.split(",").map(part => part.trim())
229
+ const [name, load, used, total] = cols
230
+ if (name === undefined || name === "") continue
231
+
232
+ /**
233
+ * 一列数字,`[N/A]` 与非数字一律作「没有这个数」
234
+ * @param text 该列原文
235
+ * @returns 数值;取不到时 undefined
236
+ */
237
+ const num = (text: string | undefined): number | undefined => {
238
+ if (text === undefined) return undefined
239
+ const value = Number(text)
240
+ return Number.isFinite(value) ? value : undefined
241
+ }
242
+
243
+ const pct = num(load)
244
+ const usedMb = num(used)
245
+ const totalMb = num(total)
246
+ gpus.push({
247
+ name,
248
+ ...(pct === undefined ? {} : { load: Math.min(1, Math.max(0, pct / 100)) }),
249
+ ...(usedMb === undefined ? {} : { memoryUsed: usedMb * 1024 * 1024 }),
250
+ ...(totalMb === undefined ? {} : { memoryTotal: totalMb * 1024 * 1024 })
251
+ })
252
+ }
253
+ return gpus
254
+ }
255
+
256
+ /**
257
+ * 探测显卡
258
+ * @returns 各显卡;测不到时 undefined(与「有 0 块」相区分)
259
+ */
260
+ export async function probeGpus(): Promise<GpuInfo[] | undefined> {
261
+ if (noNvidiaSmi) return undefined
262
+ const raw = await runNvidiaSmi()
263
+ if (raw === undefined) return undefined
264
+ const gpus = parseNvidiaSmi(raw)
265
+ return gpus.length > 0 ? gpus : undefined
266
+ }
267
+
268
+ /**
269
+ * 采一份系统快照
270
+ *
271
+ * 磁盘与显卡并发探测:两者互不相关,串行只是把 nvidia-smi 的耗时加到磁盘上。
272
+ * @param force 是否绕过缓存,仅测试用
273
+ * @returns 系统快照
274
+ */
275
+ export async function sampleSystem(force = false): Promise<SystemInfo> {
276
+ const now = Date.now()
277
+ if (!force && cache !== undefined && now - cache.at < CACHE_MS) return cache.value
278
+
279
+ const [disks, gpus] = await Promise.all([probeDisks(), probeGpus()])
280
+ const value: SystemInfo = { disks, ...(gpus === undefined ? {} : { gpus }) }
281
+ cache = { at: now, value }
282
+ return value
283
+ }
284
+
285
+ /** 清掉缓存与「没有 nvidia-smi」的记忆,仅测试用 */
286
+ export function resetSystemCache(): void {
287
+ cache = undefined
288
+ noNvidiaSmi = false
289
+ }