@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,324 @@
1
+ /**
2
+ * 模块职责:SQLite 关系存储(语句缓存、异步事务、迁移)
3
+ * 依赖方向:动态依赖可选原生模块 better-sqlite3;依赖 util/{lru,queue}、类型包
4
+ * 生命周期:`openSql()` 打开,`close()` 关闭
5
+ * 注意事项:**不引 ORM**,只暴露 run/all/get/transaction/migrate。
6
+ *
7
+ * **事务手写 BEGIN/COMMIT 并用互斥锁串行化**:better-sqlite3 的 `db.transaction()`
8
+ * 只收同步函数,而本框架的接口是异步的(要容得下别的驱动)。一条连接上开不了两个
9
+ * 事务,故事务之间必须排队;嵌套事务改用 SAVEPOINT,不重复抢锁。
10
+ *
11
+ * **语句缓存有界。** `prepare()` 有真实开销,但无上限的缓存在动态拼 SQL 的插件手里
12
+ * 就是内存泄漏。
13
+ */
14
+ import type { SqlHandle, SqlMigration, SqlParam } from "@yunzai-ng/types"
15
+ import { dirname } from "node:path"
16
+ import { LruCache } from "../util/lru.js"
17
+ import { Semaphore } from "../util/queue.js"
18
+ import { ensureDir } from "../util/fs.js"
19
+
20
+ /** 语句缓存条目上限 */
21
+ const STMT_CACHE_MAX = 200
22
+
23
+ /** 迁移记录表名 */
24
+ const MIGRATION_TABLE = "_yzng_migrations"
25
+
26
+ /** better-sqlite3 的语句最小面 */
27
+ interface SqliteStatement {
28
+ /** 执行不取行 */
29
+ run(...params: SqlParam[]): { changes: number; lastInsertRowid: number | bigint }
30
+ /** 取全部行 */
31
+ all(...params: SqlParam[]): unknown[]
32
+ /** 取首行 */
33
+ get(...params: SqlParam[]): unknown
34
+ }
35
+
36
+ /** better-sqlite3 的连接最小面 */
37
+ interface SqliteDb {
38
+ /** 预编译语句 */
39
+ prepare(sql: string): SqliteStatement
40
+ /** 直接执行脚本(可含多条语句,不支持绑定参数) */
41
+ exec(sql: string): void
42
+ /** 读写 pragma */
43
+ pragma(source: string): unknown
44
+ /** 关闭连接 */
45
+ close(): void
46
+ }
47
+
48
+ /** better-sqlite3 的构造签名 */
49
+ type SqliteCtor = new (file: string, opts?: { readonly?: boolean }) => SqliteDb
50
+
51
+ /** 一条连接上的共享状态 */
52
+ interface SqlState {
53
+ /** 连接 */
54
+ db: SqliteDb
55
+ /** 语句缓存 */
56
+ stmts: LruCache<SqliteStatement>
57
+ /** 事务互斥锁 */
58
+ mutex: Semaphore
59
+ /** SAVEPOINT 命名计数器 */
60
+ savepoint: number
61
+ }
62
+
63
+ /**
64
+ * 尝试加载 better-sqlite3
65
+ * @returns 构造函数;模块不可用时 undefined
66
+ */
67
+ export async function loadSqlite(): Promise<SqliteCtor | undefined> {
68
+ try {
69
+ const mod = (await import("better-sqlite3")) as unknown as { default?: SqliteCtor }
70
+ return mod.default
71
+ } catch {
72
+ return undefined
73
+ }
74
+ }
75
+
76
+ /**
77
+ * SQLite 句柄
78
+ *
79
+ * 根句柄由 `openSql()` 创建;事务内传给回调的是 `depth > 0` 的子句柄,
80
+ * 它们共享同一条连接,因此**不能**在事务回调里持有子句柄留到事务外使用。
81
+ */
82
+ export class SqliteHandle implements SqlHandle {
83
+ /** 共享状态 */
84
+ readonly #state: SqlState
85
+ /** 事务嵌套深度,0 表示不在事务中 */
86
+ readonly #depth: number
87
+
88
+ /**
89
+ * @param state 共享状态
90
+ * @param depth 事务嵌套深度
91
+ */
92
+ constructor(state: SqlState, depth = 0) {
93
+ this.#state = state
94
+ this.#depth = depth
95
+ }
96
+
97
+ /**
98
+ * 执行不返回行的语句
99
+ *
100
+ * 不带参数且含多条语句时走 `exec()`,方便迁移里一次建表 + 建索引;
101
+ * 这种情况下无法报告受影响行数,返回 0。
102
+ * @param sql SQL 文本
103
+ * @param params 绑定参数
104
+ * @returns 受影响行数与最后插入 id
105
+ */
106
+ async run(sql: string, params: SqlParam[] = []): Promise<{ changes: number; lastInsertRowid: number | bigint }> {
107
+ if (params.length === 0 && isMultiStatement(sql)) {
108
+ this.#state.db.exec(sql)
109
+ return { changes: 0, lastInsertRowid: 0 }
110
+ }
111
+ return this.#prepare(sql).run(...params)
112
+ }
113
+
114
+ /**
115
+ * 查询多行
116
+ * @param sql SQL 文本
117
+ * @param params 绑定参数
118
+ * @returns 结果行数组
119
+ */
120
+ async all<T = Record<string, unknown>>(sql: string, params: SqlParam[] = []): Promise<T[]> {
121
+ return this.#prepare(sql).all(...params) as T[]
122
+ }
123
+
124
+ /**
125
+ * 查询首行
126
+ * @param sql SQL 文本
127
+ * @param params 绑定参数
128
+ * @returns 首行;无结果时 undefined
129
+ */
130
+ async get<T = Record<string, unknown>>(sql: string, params: SqlParam[] = []): Promise<T | undefined> {
131
+ return (this.#prepare(sql).get(...params) as T | undefined) ?? undefined
132
+ }
133
+
134
+ /**
135
+ * 在事务中执行
136
+ *
137
+ * 最外层用 `BEGIN IMMEDIATE`(立刻拿写锁,避免升级锁时才发现冲突而整段重来);
138
+ * 嵌套层用 SAVEPOINT,实现"内层回滚不牵连外层"。
139
+ * @param fn 事务体,抛错即回滚
140
+ * @returns 事务体的返回值
141
+ */
142
+ async transaction<T>(fn: (tx: SqlHandle) => Promise<T>): Promise<T> {
143
+ if (this.#depth > 0) return this.#savepoint(fn)
144
+ // 同一条连接上不能并发开事务,排队等前一个事务结束
145
+ return this.#state.mutex.use(async () => {
146
+ const tx = new SqliteHandle(this.#state, 1)
147
+ this.#state.db.exec("BEGIN IMMEDIATE")
148
+ try {
149
+ const result = await fn(tx)
150
+ this.#state.db.exec("COMMIT")
151
+ return result
152
+ } catch (err) {
153
+ // 回滚本身也可能失败(连接已断),此时保留原始错误更有诊断价值
154
+ try {
155
+ this.#state.db.exec("ROLLBACK")
156
+ } catch {
157
+ /* 忽略 */
158
+ }
159
+ throw err
160
+ }
161
+ })
162
+ }
163
+
164
+ /**
165
+ * 应用迁移
166
+ *
167
+ * 每条迁移单独一个事务:失败只回滚这一条,已成功的保持生效,修好后重跑从断点继续。
168
+ * 版本号落在迁移表里,故"这个库到哪一版了"有确定答案,不必靠"存在则忽略"的 try/catch。
169
+ * @param migrations 迁移列表
170
+ * @throws 版本号未严格递增,或某条迁移执行失败
171
+ */
172
+ async migrate(migrations: SqlMigration[]): Promise<void> {
173
+ await this.run(
174
+ `CREATE TABLE IF NOT EXISTS ${MIGRATION_TABLE} (
175
+ version INTEGER PRIMARY KEY,
176
+ name TEXT NOT NULL,
177
+ applied_at INTEGER NOT NULL
178
+ )`
179
+ )
180
+
181
+ const sorted = [...migrations].sort((a, b) => a.version - b.version)
182
+ for (let i = 1; i < sorted.length; i++) {
183
+ if (sorted[i]!.version === sorted[i - 1]!.version) {
184
+ throw new Error(`迁移版本号重复:${sorted[i]!.version}`)
185
+ }
186
+ }
187
+
188
+ const rows = await this.all<{ version: number }>(`SELECT version FROM ${MIGRATION_TABLE}`)
189
+ const applied = new Set(rows.map(r => r.version))
190
+
191
+ for (const migration of sorted) {
192
+ if (applied.has(migration.version)) continue
193
+ await this.transaction(async tx => {
194
+ await migration.up(tx)
195
+ await tx.run(`INSERT INTO ${MIGRATION_TABLE} (version, name, applied_at) VALUES (?, ?, ?)`, [
196
+ migration.version,
197
+ migration.name,
198
+ Date.now()
199
+ ])
200
+ })
201
+ }
202
+ }
203
+
204
+ /**
205
+ * 嵌套事务:SAVEPOINT
206
+ * @param fn 事务体
207
+ * @returns 事务体的返回值
208
+ */
209
+ async #savepoint<T>(fn: (tx: SqlHandle) => Promise<T>): Promise<T> {
210
+ const name = `yzng_sp_${++this.#state.savepoint}`
211
+ const tx = new SqliteHandle(this.#state, this.#depth + 1)
212
+ this.#state.db.exec(`SAVEPOINT ${name}`)
213
+ try {
214
+ const result = await fn(tx)
215
+ this.#state.db.exec(`RELEASE ${name}`)
216
+ return result
217
+ } catch (err) {
218
+ try {
219
+ this.#state.db.exec(`ROLLBACK TO ${name}`)
220
+ this.#state.db.exec(`RELEASE ${name}`)
221
+ } catch {
222
+ /* 忽略 */
223
+ }
224
+ throw err
225
+ }
226
+ }
227
+
228
+ /**
229
+ * 取(并缓存)预编译语句
230
+ * @param sql SQL 文本
231
+ * @returns 预编译语句
232
+ */
233
+ #prepare(sql: string): SqliteStatement {
234
+ const cached = this.#state.stmts.get(sql)
235
+ if (cached) return cached
236
+ const stmt = this.#state.db.prepare(sql)
237
+ this.#state.stmts.set(sql, stmt)
238
+ return stmt
239
+ }
240
+ }
241
+
242
+ /** 打开 SQL 存储的参数 */
243
+ export interface OpenSqlOptions {
244
+ /** 数据库文件绝对路径 */
245
+ file: string
246
+ /** 构造函数,缺省自动加载 */
247
+ ctor?: SqliteCtor
248
+ }
249
+
250
+ /** 已打开的 SQL 存储 */
251
+ export interface SqlStore {
252
+ /** 句柄 */
253
+ readonly handle: SqlHandle
254
+ /** 数据库文件路径 */
255
+ readonly file: string
256
+ /** 关闭连接 */
257
+ close(): void
258
+ }
259
+
260
+ /**
261
+ * 打开 SQLite 数据库
262
+ * @param opts 参数
263
+ * @returns 已打开的 SQL 存储;原生模块不可用时 undefined(调用方应降级为纯 KV)
264
+ */
265
+ export async function openSql(opts: OpenSqlOptions): Promise<SqlStore | undefined> {
266
+ const ctor = opts.ctor ?? (await loadSqlite())
267
+ if (!ctor) return undefined
268
+
269
+ await ensureDir(dirname(opts.file))
270
+ const db = new ctor(opts.file)
271
+
272
+ // WAL:读不阻塞写,且断电时不会像 rollback journal 那样留下半个事务
273
+ db.pragma("journal_mode = WAL")
274
+ // NORMAL 在 WAL 下依然崩溃安全(只可能丢最后一个未 checkpoint 的事务),
275
+ // 却比 FULL 快一个数量级;机器人数据不是账本,这个取舍是合适的
276
+ db.pragma("synchronous = NORMAL")
277
+ db.pragma("foreign_keys = ON")
278
+ // 被别的连接短暂锁住时等一会儿再报错,而不是立刻抛 SQLITE_BUSY
279
+ db.pragma("busy_timeout = 5000")
280
+
281
+ const state: SqlState = {
282
+ db,
283
+ stmts: new LruCache<SqliteStatement>({ max: STMT_CACHE_MAX }),
284
+ mutex: new Semaphore(1),
285
+ savepoint: 0
286
+ }
287
+
288
+ return {
289
+ handle: new SqliteHandle(state),
290
+ file: opts.file,
291
+ close: () => {
292
+ // 缓存里的语句必须先丢掉:连接关了之后再用它们会直接崩进程
293
+ state.stmts.clear()
294
+ db.close()
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * 判断 SQL 是否包含多条语句
301
+ *
302
+ * 只做粗判:跳过字符串字面量后看是否还有 `;` 跟着非空白内容。
303
+ * 粗判失败的后果仅仅是走 `prepare()` 然后由 SQLite 自己报错,可接受。
304
+ * @param sql SQL 文本
305
+ * @returns 是否多语句
306
+ */
307
+ function isMultiStatement(sql: string): boolean {
308
+ let inString = false
309
+ let quote = ""
310
+ for (let i = 0; i < sql.length; i++) {
311
+ const ch = sql[i]!
312
+ if (inString) {
313
+ if (ch === quote) inString = false
314
+ continue
315
+ }
316
+ if (ch === "'" || ch === '"') {
317
+ inString = true
318
+ quote = ch
319
+ continue
320
+ }
321
+ if (ch === ";" && sql.slice(i + 1).trim() !== "") return true
322
+ }
323
+ return false
324
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * 模块职责:测试替身(假日志器、录制型子系统接缝、最小应用视图)
3
+ * 依赖方向:依赖 plugin/hooks、类型包;只被 `*.test.ts` 使用
4
+ * 生命周期:每个用例新建一套
5
+ * 注意事项:这些替身**刻意采用真实实现而非 mock 框架的自动桩** —— 注册后必须可查得到、
6
+ * 注销后必须可查得已不存在,否则"卸载是否确实归还了资源"这类断言无从落地。
7
+ *
8
+ * 目前仅用于内核自身的测试。将来若需使插件作者亦可借其编写单元测试,
9
+ * 须同时放开分层门禁中 `@yunzai-ng/core/*` 的深引限制 ——
10
+ * 那是一次独立的、需要写入文档的决定,不在此处附带开放。
11
+ */
12
+ import type {
13
+ AdapterProvider,
14
+ AppView,
15
+ HttpClient,
16
+ KvDriver,
17
+ LogLevel,
18
+ Logger,
19
+ RenderRequest,
20
+ RendererProvider,
21
+ SqlHandle
22
+ } from "@yunzai-ng/types"
23
+ import type {
24
+ CommandRegistration,
25
+ KernelHooks,
26
+ MiddlewareRegistration,
27
+ TaskRegistration
28
+ } from "../plugin/hooks.js"
29
+
30
+ /** 带行记录的假日志器 */
31
+ export interface FakeLogger extends Logger {
32
+ /** 已记录的日志行,形如 `"warn 消息 附加参数"` */
33
+ readonly lines: string[]
34
+ }
35
+
36
+ /**
37
+ * 创建假日志器
38
+ *
39
+ * 断言"出错时给了提示"而不是"抛异常中断"时用它 —— 框架里大量路径的正确行为
40
+ * 就是"记一条能照着解决的 warn 然后继续跑",没有日志断言就测不到。
41
+ * @returns 假日志器
42
+ */
43
+ export function fakeLogger(): FakeLogger {
44
+ const lines: string[] = []
45
+ /** 生成某个级别的记录函数 */
46
+ const push =
47
+ (level: string) =>
48
+ (msg: unknown, ...args: unknown[]): void => {
49
+ const extra = args.map(a => (a instanceof Error ? a.message : String(a))).join(" ")
50
+ lines.push(`${level} ${String(msg)} ${extra}`.trim())
51
+ }
52
+
53
+ const logger: FakeLogger = {
54
+ lines,
55
+ level: "trace" as LogLevel,
56
+ trace: push("trace"),
57
+ debug: push("debug"),
58
+ info: push("info"),
59
+ warn: push("warn"),
60
+ error: push("error"),
61
+ fatal: push("fatal"),
62
+ mark: push("info"),
63
+ child: () => logger,
64
+ isLevelEnabled: () => true
65
+ }
66
+ return logger
67
+ }
68
+
69
+ /** 一条静态目录挂载记录 */
70
+ export interface StaticMount {
71
+ /** URL 作用域;面板挂载记为归属标识(如 `plugin:webui-next`) */
72
+ scope: string
73
+ /** URL 前缀;面板挂载记为 `"/"` */
74
+ urlPath: string
75
+ /** 本地目录 */
76
+ dir: string
77
+ /** 是否为单页应用挂载(`ctx.panel()` 记为真) */
78
+ spa: boolean
79
+ }
80
+
81
+ /** 一条路由记录 */
82
+ export interface RouteMount {
83
+ /** URL 作用域 */
84
+ scope: string
85
+ /** 方法,WebSocket 记为 `"WS"` */
86
+ method: string
87
+ /** 路径 */
88
+ path: string
89
+ }
90
+
91
+ /** 录制到的全部注册 */
92
+ export interface Recorded {
93
+ /** 命令 */
94
+ commands: CommandRegistration[]
95
+ /** 重新索引的次数(按命令去重前的调用计数) */
96
+ reindexed: CommandRegistration[]
97
+ /** 中间件 */
98
+ middlewares: MiddlewareRegistration[]
99
+ /** 定时任务 */
100
+ tasks: TaskRegistration[]
101
+ /** 路由与 WebSocket */
102
+ routes: RouteMount[]
103
+ /** 静态目录 */
104
+ statics: StaticMount[]
105
+ /** 适配器 */
106
+ adapters: AdapterProvider[]
107
+ /** 渲染器 */
108
+ renderers: RendererProvider[]
109
+ /** 收到的渲染请求(断言 templateRoot/resourceRoot 是否算对) */
110
+ renders: RenderRequest[]
111
+ /** KV 驱动 */
112
+ kvDrivers: KvDriver[]
113
+ /** 已打开的 SQL 库,形如 `"插件名/库名"` */
114
+ sqlOpened: string[]
115
+ /** 被要求关闭全部库的插件名 */
116
+ sqlClosed: string[]
117
+ }
118
+
119
+ /** 录制型接缝与其录制结果 */
120
+ export interface RecordingHooks {
121
+ /** 交给上下文的接缝 */
122
+ hooks: KernelHooks
123
+ /** 录制结果 */
124
+ recorded: Recorded
125
+ }
126
+
127
+ /**
128
+ * 从数组里移除某项的 disposer
129
+ * @param list 数组
130
+ * @param item 待移除项
131
+ * @returns 注销函数
132
+ */
133
+ function remover<T>(list: T[], item: T): () => void {
134
+ return () => {
135
+ const index = list.indexOf(item)
136
+ if (index >= 0) list.splice(index, 1)
137
+ }
138
+ }
139
+
140
+ /**
141
+ * 创建录制型子系统接缝
142
+ *
143
+ * 每个 `register` 均确实将登记写入数组,返回的 disposer 亦确实将其摘除。
144
+ * 因此"插件卸载后残留多少条命令"可直接断言 `recorded.commands.length === 0`。
145
+ * @returns 接缝与录制结果
146
+ */
147
+ export function recordingHooks(): RecordingHooks {
148
+ const recorded: Recorded = {
149
+ commands: [],
150
+ reindexed: [],
151
+ middlewares: [],
152
+ tasks: [],
153
+ routes: [],
154
+ statics: [],
155
+ adapters: [],
156
+ renderers: [],
157
+ renders: [],
158
+ kvDrivers: [],
159
+ sqlOpened: [],
160
+ sqlClosed: []
161
+ }
162
+
163
+ /** 假 SQL 句柄:只记录被调用过,不真的建库 */
164
+ const sqlHandle = {
165
+ file: ":fake:",
166
+ exec: () => undefined,
167
+ run: () => ({ changes: 0, lastInsertRowid: 0 }),
168
+ get: () => undefined,
169
+ all: () => [],
170
+ transaction: <T>(fn: () => T) => fn(),
171
+ pragma: () => undefined
172
+ } as unknown as SqlHandle
173
+
174
+ const hooks: KernelHooks = {
175
+ commands: {
176
+ register: reg => {
177
+ recorded.commands.push(reg)
178
+ return remover(recorded.commands, reg)
179
+ },
180
+ reindex: reg => void recorded.reindexed.push(reg)
181
+ },
182
+ middlewares: {
183
+ register: reg => {
184
+ recorded.middlewares.push(reg)
185
+ return remover(recorded.middlewares, reg)
186
+ }
187
+ },
188
+ tasks: {
189
+ register: reg => {
190
+ recorded.tasks.push(reg)
191
+ return remover(recorded.tasks, reg)
192
+ }
193
+ },
194
+ server: {
195
+ info: { enabled: true, host: "127.0.0.1", port: 2536, publicUrl: "http://127.0.0.1:2536" },
196
+ route: (scope, method, path) => {
197
+ const mount: RouteMount = { scope, method, path }
198
+ recorded.routes.push(mount)
199
+ return remover(recorded.routes, mount)
200
+ },
201
+ websocket: (scope, path) => {
202
+ const mount: RouteMount = { scope, method: "WS", path }
203
+ recorded.routes.push(mount)
204
+ return remover(recorded.routes, mount)
205
+ },
206
+ static: (scope, urlPath, dir) => {
207
+ const mount: StaticMount = { scope, urlPath, dir, spa: false }
208
+ recorded.statics.push(mount)
209
+ return remover(recorded.statics, mount)
210
+ },
211
+ panel: (owner, dir) => {
212
+ const mount: StaticMount = { scope: owner, urlPath: "/", dir, spa: true }
213
+ recorded.statics.push(mount)
214
+ return remover(recorded.statics, mount)
215
+ },
216
+ // 只按 urlPath 相等判断:录制型接缝不实现路径表,而调用方(内核挂载内置面板)
217
+ // 问的只是"根路径有没有人接管"这一种情况
218
+ claimant: path => recorded.statics.find(m => m.urlPath === path)?.scope
219
+ },
220
+ render: {
221
+ register: provider => {
222
+ recorded.renderers.push(provider)
223
+ return remover(recorded.renderers, provider)
224
+ },
225
+ // 一张 3 字节的假图:断言"渲染结果被包成 ImageSegment"用得到,
226
+ // 不需要真的产生 PNG
227
+ render: async req => {
228
+ recorded.renders.push(req)
229
+ return { images: [new Uint8Array([1, 2, 3])], cost: 1, renderer: "fake" }
230
+ }
231
+ },
232
+ adapters: {
233
+ register: provider => {
234
+ recorded.adapters.push(provider)
235
+ return remover(recorded.adapters, provider)
236
+ }
237
+ },
238
+ kvDrivers: {
239
+ register: driver => {
240
+ recorded.kvDrivers.push(driver)
241
+ return remover(recorded.kvDrivers, driver)
242
+ }
243
+ },
244
+ sql: {
245
+ open: async (plugin, name) => {
246
+ recorded.sqlOpened.push(`${plugin}/${name}`)
247
+ return sqlHandle
248
+ },
249
+ close: async plugin => void recorded.sqlClosed.push(plugin)
250
+ },
251
+ bots: { pick: () => undefined }
252
+ }
253
+
254
+ return { hooks, recorded }
255
+ }
256
+
257
+ /**
258
+ * 创建最小应用视图
259
+ *
260
+ * 只填测试真正会读的字段,其余用类型断言留空 —— `AppView` 聚合了适配器、
261
+ * 账号、策略等一大票子系统视图,为一个上下文测试把它们全造出来毫无收益。
262
+ * 一旦某个用例真的要读某字段,就在这里补上真的实现。
263
+ * @returns 应用视图
264
+ */
265
+ export function fakeAppView(): AppView {
266
+ return {
267
+ version: "0.0.0-test",
268
+ startedAt: 0,
269
+ usage: () => ({ rss: 0, heapUsed: 0, heapTotal: 0, external: 0, uptime: 0, cpu: 0 })
270
+ } as unknown as AppView
271
+ }
272
+
273
+ /**
274
+ * 创建假 HTTP 客户端
275
+ *
276
+ * 任何调用均抛错:网络请求出现在单元测试中几乎总属设计问题,应使其显式失败。
277
+ *
278
+ * 方法列表与 `HttpClient` 逐一对齐,且**不使用 as unknown as 断言** ——
279
+ * 经断言的替身对象会掩盖"契约已变更"一事:真实客户端新增一个方法后,
280
+ * 只有被测代码调用它时才在运行时抛错,而编译期不会报告任何问题。
281
+ * @returns HTTP 客户端
282
+ */
283
+ export function fakeHttp(): HttpClient {
284
+ /** 统一拒绝 */
285
+ const fail = (): never => {
286
+ throw new Error("测试中不应发起真实 HTTP 请求")
287
+ }
288
+ const client: HttpClient = {
289
+ request: fail,
290
+ get: fail,
291
+ post: fail,
292
+ buffer: fail,
293
+ download: fail,
294
+ extend: () => client
295
+ }
296
+ return client
297
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 模块职责:`@yunzai-ng/core/testing` 的入口,汇总内核提供的测试替身
3
+ * 依赖方向:只做转发
4
+ * 生命周期:无
5
+ * 注意事项:这些替身刻意**不**从主入口 `@yunzai-ng/core` 导出 —— 一并导出会使测试替身
6
+ * 进入生产依赖图(见 src/index.ts 的说明)。适配器与插件作者编写测试时:
7
+ * ```ts
8
+ * import { createMockAdapter, fakeLogger } from "@yunzai-ng/core/testing"
9
+ * ```
10
+ *
11
+ * 与内核自身那些 `*.test.ts` 的分工:后者是内部单元测试,直接以相对路径 import
12
+ * 内部模块;此处是**对外公开**的替身,因此每次改动均应视为 API 变更。
13
+ */
14
+ export * from "./fake.js"
15
+ export * from "./mock-adapter.js"