@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,202 @@
1
+ /**
2
+ * 模块职责:适配器注册表(实现 `AdapterSink` 与 `AdapterRegistryView`)
3
+ * 依赖方向:仅依赖类型包与 plugin/hooks 的接缝定义;**不认识账号管理器**
4
+ * 生命周期:随内核创建;每条注册随提供方插件卸载而摘除
5
+ * 注意事项:这是"账号登录也是插件"的登记处 —— 内核对 QQ 协议零认知,只知道
6
+ * "某个插件声称能把一份账号配置变成一个能收发消息的 BotDriver"。
7
+ *
8
+ * 摘除一条适配器时必须先断开它名下的全部账号,但 `Disposer` 是**同步**的
9
+ * (见 util/dispose.ts:插件卸载走 `DisposalRegistry.dispose()`,不 await)。
10
+ * 于是这里只做同步通知:`onUnregister` 的监听者(账号管理器)必须在回调里
11
+ * **同步**把账号标成下线、让 `AdapterHost.submit()` 立刻开始丢弃事件,
12
+ * 真正的 socket 关闭再异步收尾。否则会出现"插件已卸载但事件还在往管线里灌",
13
+ * 症状是热重载后收到重复回复。
14
+ */
15
+ import type { AdapterProvider, AdapterRegistryView, Disposer, Logger } from "@yunzai-ng/types"
16
+ import type { AdapterSink } from "../plugin/hooks.js"
17
+
18
+ /** 适配器描述(`list()` 的元素类型,WebUI 的"添加账号"页面据此渲染) */
19
+ export type AdapterSummary = Pick<
20
+ AdapterProvider,
21
+ "id" | "name" | "description" | "platform" | "accountSchema" | "loginModes"
22
+ >
23
+
24
+ /** 注册表内部条目 */
25
+ export interface AdapterEntry {
26
+ /** 适配器实现 */
27
+ readonly provider: AdapterProvider
28
+ /** 提供方插件名 */
29
+ readonly owner: string
30
+ }
31
+
32
+ /** 摘除通知监听器 */
33
+ export type AdapterUnregisterListener = (id: string, entry: AdapterEntry) => void
34
+
35
+ /** 注册通知监听器 */
36
+ export type AdapterRegisterListener = (id: string, entry: AdapterEntry) => void
37
+
38
+ /** 适配器注册表构造参数 */
39
+ export interface AdapterRegistryOptions {
40
+ /** 日志器 */
41
+ readonly logger: Logger
42
+ }
43
+
44
+ /**
45
+ * 适配器注册表
46
+ *
47
+ * 既是 `ctx.registerAdapter()` 的落点(`AdapterSink`),也是 WebUI 与账号管理器
48
+ * 的查询入口(`AdapterRegistryView`)。
49
+ */
50
+ export class AdapterRegistry implements AdapterSink, AdapterRegistryView {
51
+ /** 日志器 */
52
+ readonly #logger: Logger
53
+ /** 适配器 id → 条目 */
54
+ readonly #entries = new Map<string, AdapterEntry>()
55
+ /** 摘除通知监听器 */
56
+ readonly #listeners = new Set<AdapterUnregisterListener>()
57
+ /** 注册通知监听器 */
58
+ readonly #onRegister = new Set<AdapterRegisterListener>()
59
+
60
+ /**
61
+ * @param opts 构造参数
62
+ */
63
+ constructor(opts: AdapterRegistryOptions) {
64
+ this.#logger = opts.logger.child({ scope: "adapter" })
65
+ }
66
+
67
+ /** 已注册的适配器数量 */
68
+ get size(): number {
69
+ return this.#entries.size
70
+ }
71
+
72
+ /**
73
+ * 注册一个适配器
74
+ * @param provider 适配器实现
75
+ * @param owner 提供方插件名
76
+ * @returns 注销句柄
77
+ * @throws 该 id 已被别的插件占用时
78
+ */
79
+ register(provider: AdapterProvider, owner: string): Disposer {
80
+ const existing = this.#entries.get(provider.id)
81
+ if (existing !== undefined) {
82
+ // 不做"后来者覆盖":id 是账号记录里引用适配器的唯一凭据,
83
+ // 悄悄换掉实现会让已存的账号连到另一套协议上去
84
+ throw new Error(
85
+ `适配器 id ${provider.id} 已被插件 ${existing.owner} 注册,插件 ${owner} 不能重复注册;` +
86
+ `请改用不同的 id(账号记录靠它定位适配器实现)`
87
+ )
88
+ }
89
+
90
+ const entry: AdapterEntry = { provider, owner }
91
+ this.#entries.set(provider.id, entry)
92
+ this.#logger.info(`适配器就绪:${provider.name}(${provider.id},平台 ${provider.platform},来自插件 ${owner})`)
93
+ for (const listener of [...this.#onRegister]) {
94
+ try {
95
+ listener(provider.id, entry)
96
+ } catch (err) {
97
+ this.#logger.error(`处理适配器 ${provider.id} 的注册通知时出错`, err)
98
+ }
99
+ }
100
+ return () => this.#remove(provider.id, entry)
101
+ }
102
+
103
+ /**
104
+ * 按 id 取适配器
105
+ * @param id 适配器 id
106
+ * @returns 适配器实现;未注册时 undefined
107
+ */
108
+ get(id: string): AdapterProvider | undefined {
109
+ return this.#entries.get(id)?.provider
110
+ }
111
+
112
+ /**
113
+ * 按 id 取条目(含提供方插件名)
114
+ * @param id 适配器 id
115
+ * @returns 条目;未注册时 undefined
116
+ */
117
+ entry(id: string): AdapterEntry | undefined {
118
+ return this.#entries.get(id)
119
+ }
120
+
121
+ /**
122
+ * 列出已注册的适配器
123
+ * @returns 适配器描述数组,按注册顺序
124
+ */
125
+ list(): AdapterSummary[] {
126
+ return [...this.#entries.values()].map(({ provider }) => ({
127
+ id: provider.id,
128
+ name: provider.name,
129
+ description: provider.description,
130
+ platform: provider.platform,
131
+ accountSchema: provider.accountSchema,
132
+ loginModes: provider.loginModes
133
+ }))
134
+ }
135
+
136
+ /**
137
+ * 登记注册通知
138
+ *
139
+ * 为热重载而存在:适配器插件重新加载后,其名下账号须自行重连,
140
+ * 否则用户须在面板中逐个点击连接。
141
+ * @param listener 监听器
142
+ * @returns 取消登记
143
+ */
144
+ onRegister(listener: AdapterRegisterListener): Disposer {
145
+ this.#onRegister.add(listener)
146
+ return () => void this.#onRegister.delete(listener)
147
+ }
148
+
149
+ /**
150
+ * 登记摘除通知
151
+ *
152
+ * 回调在 `Disposer` 里**同步**执行,因此实现方不能在里面 await 网络操作,
153
+ * 只能做"立刻停止投递事件"这类同步动作,见文件头。
154
+ * @param listener 监听器
155
+ * @returns 取消登记
156
+ */
157
+ onUnregister(listener: AdapterUnregisterListener): Disposer {
158
+ this.#listeners.add(listener)
159
+ return () => void this.#listeners.delete(listener)
160
+ }
161
+
162
+ /**
163
+ * 摘除某插件注册的全部适配器
164
+ *
165
+ * 兜底路径:插件上下文正常会逐条调 Disposer,但插件 setup 抛在半路时
166
+ * 可能有登记没走 ctx。
167
+ * @param owner 插件名
168
+ * @returns 摘除的条数
169
+ */
170
+ removePlugin(owner: string): number {
171
+ let n = 0
172
+ for (const [id, entry] of [...this.#entries]) {
173
+ if (entry.owner === owner) {
174
+ this.#remove(id, entry)
175
+ n++
176
+ }
177
+ }
178
+ return n
179
+ }
180
+
181
+ /**
182
+ * 摘除一条注册
183
+ *
184
+ * 比对 entry 而不是只看 id:插件 A 卸载得晚、插件 B 已经用同一个 id 注册了
185
+ * 新实现时,A 的 Disposer 不应将 B 的实现摘除。
186
+ * @param id 适配器 id
187
+ * @param entry 注册时的条目
188
+ */
189
+ #remove(id: string, entry: AdapterEntry): void {
190
+ if (this.#entries.get(id) !== entry) return
191
+ // 先通知再删:监听者要能通过 entry 拿到 provider 做收尾
192
+ for (const listener of [...this.#listeners]) {
193
+ try {
194
+ listener(id, entry)
195
+ } catch (err) {
196
+ this.#logger.error(`处理适配器 ${id} 的摘除通知时出错`, err)
197
+ }
198
+ }
199
+ this.#entries.delete(id)
200
+ this.#logger.info(`适配器 ${id} 已摘除(插件 ${entry.owner})`)
201
+ }
202
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 模块职责:内核配置 schema 的回归测试
3
+ * 依赖方向:测试文件
4
+ * 生命周期:纯内存
5
+ * 注意事项:这里锁住的是**不变量**(默认值自洽、YAML 可回环、敏感字段被标记、
6
+ * 对外监听必须有令牌),而不是逐条字段的取值 —— 后者会随产品调整而变。
7
+ */
8
+ import { describe, expect, it } from "vitest"
9
+ import { CORE_SECRET_PATHS, coreConfigSchema, loggerSettingsOf, serverSecurityWarning } from "./core-config.js"
10
+ import { parseYaml, serializeYaml } from "./yaml.js"
11
+
12
+ describe("内核配置", () => {
13
+ const defaults = coreConfigSchema.defaults()
14
+
15
+ it("默认值自洽:不给任何输入也能得到一份合法配置", () => {
16
+ expect(coreConfigSchema.safeParse(undefined).ok).toBe(true)
17
+ expect(coreConfigSchema.safeParse(defaults).ok).toBe(true)
18
+ expect(defaults.server.port).toBe(2536)
19
+ expect(defaults.bot.prefix).toEqual(["#", "*", "%"])
20
+ expect(defaults.store.driver).toBe("auto")
21
+ })
22
+
23
+ it("序列化成带注释的 YAML 后能原样解析回来", () => {
24
+ const text = serializeYaml(defaults, { descriptor: coreConfigSchema.describe() })
25
+ const back = coreConfigSchema.parse(parseYaml(text))
26
+ expect(back).toEqual(defaults)
27
+ // 注释确实生成了,用户打开文件不是一片裸键值
28
+ expect(text).toContain("# 主人账号")
29
+ expect(text).toContain("可选值:")
30
+ })
31
+
32
+ it("令牌被标记为敏感字段,导出与日志需脱敏", () => {
33
+ expect(CORE_SECRET_PATHS).toContain("server.token")
34
+ })
35
+
36
+ it("对外监听但没设令牌时给出警告", () => {
37
+ expect(serverSecurityWarning(defaults)).toBeUndefined()
38
+
39
+ const exposed = coreConfigSchema.parse({ server: { host: "0.0.0.0" } })
40
+ expect(serverSecurityWarning(exposed)).toMatch(/未设置访问令牌/)
41
+
42
+ const weak = coreConfigSchema.parse({ server: { host: "0.0.0.0", token: "short" } })
43
+ expect(serverSecurityWarning(weak)).toMatch(/太短/)
44
+
45
+ const ok = coreConfigSchema.parse({ server: { host: "0.0.0.0", token: "0123456789abcdef01" } })
46
+ expect(serverSecurityWarning(ok)).toBeUndefined()
47
+ })
48
+
49
+ it("日志设置把 MB 换算成字节", () => {
50
+ const settings = loggerSettingsOf(defaults)
51
+ expect(settings.maxSize).toBe(8 * 1024 * 1024)
52
+ expect(settings.consoleLevel).toBeUndefined()
53
+ })
54
+
55
+ it("非法端口与越界质量被拦下并报出路径", () => {
56
+ const bad = coreConfigSchema.safeParse({ server: { port: 99999 }, render: { quality: 10 } })
57
+ expect(bad.ok).toBe(false)
58
+ expect(bad.issues.map(i => i.path)).toEqual(expect.arrayContaining(["server.port", "render.quality"]))
59
+ })
60
+ })
@@ -0,0 +1,401 @@
1
+ /**
2
+ * 模块职责:内核自身的配置 schema(`config/yunzai.yaml`)
3
+ * 依赖方向:依赖 config/schema、config/store
4
+ * 生命周期:应用启动时声明一次
5
+ * 注意事项:这里只放**内核真正会读**的配置项。规矩是「谁消费、谁声明」—— 插件的配置
6
+ * 由插件自己 `ctx.config` 声明、各自成文件,不往内核配置里塞业务开关。
7
+ *
8
+ * 每一项都写了 `title`/`desc`/`group`,WebUI 的表单和 YAML 注释都由此生成,
9
+ * 不存在"改了校验忘了改表单"的可能。
10
+ */
11
+ import type { DeepReadonly } from "@yunzai-ng/types"
12
+ import { s, type Infer } from "./schema.js"
13
+ import type { ConfigFile, ConfigStore } from "./store.js"
14
+
15
+ /** 内核配置名(对应 `config/yunzai.yaml`) */
16
+ export const CORE_CONFIG_NAME = "yunzai"
17
+
18
+ /**
19
+ * 内核配置 schema
20
+ *
21
+ * 分组约定:`基础` / `日志` / `存储` / `面板` / `插件` / `消息` / `渲染` / `网络`,
22
+ * WebUI 按此分栏。
23
+ */
24
+ export const coreConfigSchema = s.object({
25
+ bot: s
26
+ .object({
27
+ masterQQ: s
28
+ .ids()
29
+ .default([])
30
+ .title("主人账号")
31
+ .desc("拥有全部权限的用户 id。留空则首次通过 WebUI 绑定。"),
32
+ prefix: s
33
+ .tags()
34
+ .default(["#", "*", "%"])
35
+ .title("命令前缀")
36
+ .desc("只有以这些字符开头的消息才会进入命令路由。留空表示不限制前缀(不建议,会显著增加匹配开销)。"),
37
+ nickname: s
38
+ .tags()
39
+ .default([])
40
+ .title("机器人昵称")
41
+ .desc("群里以昵称开头的消息等同于 @机器人。"),
42
+ ignoreSelf: s
43
+ .boolean()
44
+ .default(true)
45
+ .title("忽略自身消息")
46
+ .desc("关闭后机器人会处理自己发出的消息,容易形成死循环,仅调试时开启。"),
47
+ onlyMaster: s
48
+ .boolean()
49
+ .default(false)
50
+ .title("维护模式")
51
+ .desc("开启后只响应主人,用于线上排障时把其他人挡在外面。")
52
+ })
53
+ .title("基础")
54
+ .group("基础")
55
+ .order(10),
56
+
57
+ log: s
58
+ .object({
59
+ level: s
60
+ .enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"], {
61
+ trace: "全部",
62
+ debug: "调试",
63
+ info: "常规",
64
+ warn: "警告",
65
+ error: "错误",
66
+ fatal: "致命",
67
+ silent: "关闭"
68
+ })
69
+ .default("info")
70
+ .title("日志级别"),
71
+ consoleLevel: s
72
+ .enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"])
73
+ .optional()
74
+ .title("控制台级别")
75
+ .desc("留空则跟随日志级别。常见用法:文件记 debug、控制台只看 info。"),
76
+ fileLevel: s
77
+ .enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"])
78
+ .optional()
79
+ .title("文件级别")
80
+ .desc("留空则跟随日志级别。"),
81
+ color: s.boolean().default(true).title("彩色输出").desc("重定向到文件或在不支持 ANSI 的终端里会自动关闭。"),
82
+ keepDays: s.number().int().min(1).max(365).default(14).title("日志保留天数"),
83
+ maxSize: s
84
+ .number()
85
+ .int()
86
+ .min(1)
87
+ .default(8)
88
+ .title("单文件上限")
89
+ .desc("单位 MB,超过后切分为 .1 / .2 …"),
90
+ maxFiles: s.number().int().min(1).default(100).title("日志文件数上限")
91
+ })
92
+ .title("日志")
93
+ .group("日志")
94
+ .order(20),
95
+
96
+ store: s
97
+ .object({
98
+ driver: s
99
+ .select([
100
+ { value: "auto", label: "自动", description: "优先内嵌 level,装不上时退回 JSON 文件" },
101
+ { value: "level", label: "内嵌 LevelDB", description: "默认;性能好,需原生模块" },
102
+ { value: "json", label: "JSON 文件", description: "纯 JS,Termux 上最稳" },
103
+ { value: "memory", label: "仅内存", description: "重启即丢,仅测试用" }
104
+ ])
105
+ .default("auto")
106
+ .title("键值存储驱动")
107
+ .desc("缺省自带内嵌存储,无须另装任何服务;Redis 由 store-redis 插件提供,属可选。"),
108
+ dir: s
109
+ .dir()
110
+ .optional()
111
+ .title("数据目录")
112
+ .desc("留空则使用应用数据目录下的 store/。"),
113
+ sqlite: s
114
+ .boolean()
115
+ .default(true)
116
+ .title("启用 SQLite")
117
+ .desc("关系型数据(抽卡记录、面板缓存)用 better-sqlite3;原生模块不可用时自动降级为纯 KV。")
118
+ })
119
+ .title("存储")
120
+ .group("存储")
121
+ .order(30),
122
+
123
+ server: s
124
+ .object({
125
+ enable: s.boolean().default(true).title("启用面板与 HTTP 服务"),
126
+ host: s
127
+ .string()
128
+ .default("127.0.0.1")
129
+ .title("监听地址")
130
+ .desc("默认只监听本机。改为 0.0.0.0 对外暴露时必须设置访问令牌。"),
131
+ port: s.port().default(2536).title("监听端口"),
132
+ token: s
133
+ .password()
134
+ .optional()
135
+ .title("访问令牌")
136
+ .desc("非本机访问时必填。令牌走请求头而非 Cookie,天然免疫 CSRF。留空时首次启动会自动生成并打印在日志里。"),
137
+ readonly: s
138
+ .boolean()
139
+ .default(false)
140
+ .title("只读模式")
141
+ .desc("开启后面板只能查看,不能改配置、不能重启插件。"),
142
+ publicUrl: s
143
+ .string()
144
+ .optional()
145
+ .title("外部访问地址")
146
+ .desc("反向代理场景下用于拼接回调 URL,如 https://bot.example.com。")
147
+ })
148
+ .title("面板")
149
+ .group("面板")
150
+ .order(40),
151
+
152
+ plugins: s
153
+ .object({
154
+ dirs: s
155
+ .array(s.dir())
156
+ .default([])
157
+ .title("额外插件目录")
158
+ .desc("除内置 plugins/ 之外要扫描的目录,支持相对应用根目录的路径。"),
159
+ disabled: s
160
+ .tags()
161
+ .default([])
162
+ .title("禁用的插件")
163
+ .desc("按插件名精确匹配。禁用后完全不加载,不占内存。"),
164
+ hotReload: s
165
+ .boolean()
166
+ .default(false)
167
+ .title("热重载")
168
+ .desc("开发用。改动插件文件后自动卸载重载;卸载会回收该插件登记的全部资源(定时任务、监听器、路由)。"),
169
+ loadTimeout: s
170
+ .duration()
171
+ .default("30s")
172
+ .title("单插件加载超时")
173
+ .desc("超时的插件被跳过并记错误,不拖垮启动流程。")
174
+ })
175
+ .title("插件")
176
+ .group("插件")
177
+ .order(50),
178
+
179
+ market: s
180
+ .object({
181
+ sources: s
182
+ .array(s.string())
183
+ .default(["https://raw.githubusercontent.com/yunzai-ng/plugin-index/main/index.json"])
184
+ .title("索引地址")
185
+ .desc(
186
+ "插件市场的索引文件地址,可填多个。靠前的地址优先,同名插件以先出现者为准," +
187
+ "因此私有索引应置于官方索引之前。索引文件为 JSON,形如 { plugins: [...] }。"
188
+ ),
189
+ mirror: s
190
+ .string()
191
+ .default("")
192
+ .title("镜像前缀")
193
+ .desc(
194
+ "仅对 github.com、raw.githubusercontent.com、codeload.github.com 三个主机生效," +
195
+ "拼接方式为 前缀 + 原始地址,例如 https://gh-proxy.org/。留空表示直接连接。"
196
+ ),
197
+ cacheTtl: s
198
+ .duration()
199
+ .default("1h")
200
+ .title("索引缓存有效期")
201
+ .desc("有效期内不再请求索引。缓存同时保存在磁盘,因此在无网络时仍可展示上次获取到的列表。"),
202
+ timeout: s.duration().default("15s").title("索引请求超时").desc("单个索引地址的请求超时。插件下载与克隆另有更长的超时。")
203
+ })
204
+ .title("插件市场")
205
+ .group("插件")
206
+ .order(55),
207
+
208
+ message: s
209
+ .object({
210
+ cooldown: s
211
+ .duration()
212
+ .default(0)
213
+ .title("全局冷却")
214
+ .desc("同一用户两条命令之间的最小间隔。0 表示不限制;命令自身声明的冷却优先。"),
215
+ splitLength: s
216
+ .number()
217
+ .int()
218
+ .min(100)
219
+ .max(20000)
220
+ .default(3000)
221
+ .title("长文本切分长度")
222
+ .desc("超长文本按此长度切分发送,切分点优先取换行与空白处。"),
223
+ sendTimeout: s.duration().default("30s").title("发送超时"),
224
+ sequential: s
225
+ .boolean()
226
+ .default(true)
227
+ .title("同会话串行发送")
228
+ .desc("保证同一群/私聊内消息按调用顺序到达。关闭可提高吞吐,但多条消息可能乱序。"),
229
+ concurrency: s
230
+ .number()
231
+ .int()
232
+ .min(1)
233
+ .max(256)
234
+ .optional()
235
+ .title("命令处理并发上限")
236
+ .desc(
237
+ "同时执行的命令处理函数数量上限。留空表示不限制。" +
238
+ "低内存设备(Termux)设成 2~4 可避免多张图同时渲染把内存吃满;" +
239
+ "设得过小会让交互式命令(等用户回话的那类)互相排队。" +
240
+ "本项改后需重启生效 —— 其余消息与渲染选项都是即时生效的。"
241
+ )
242
+ })
243
+ .title("消息")
244
+ .group("消息")
245
+ .order(60),
246
+
247
+ render: s
248
+ .object({
249
+ default: s
250
+ .string()
251
+ .default("puppeteer")
252
+ .title("默认渲染器")
253
+ .desc("渲染器由插件注册,这里填其注册名。"),
254
+ timeout: s.duration().default("60s").title("单次渲染超时"),
255
+ retry: s.number().int().min(0).max(5).default(1).title("渲染失败重试次数"),
256
+ quality: s
257
+ .number()
258
+ .int()
259
+ .min(50)
260
+ .max(100)
261
+ .default(90)
262
+ .title("图片质量")
263
+ .desc("对 jpeg/webp 生效。降低可显著减小图片体积。"),
264
+ scale: s
265
+ .number()
266
+ .min(0.5)
267
+ .max(3)
268
+ .default(1)
269
+ .title("缩放倍率")
270
+ .desc("出图的像素密度:2 表示两倍图,更清晰但更占内存与耗时。低内存设备建议 1。")
271
+ })
272
+ .title("渲染")
273
+ .group("渲染")
274
+ .order(70),
275
+
276
+ net: s
277
+ .object({
278
+ proxy: s
279
+ .string()
280
+ .optional()
281
+ .title("HTTP 代理")
282
+ .desc("形如 http://127.0.0.1:7890。留空则读取环境变量 HTTPS_PROXY / HTTP_PROXY。"),
283
+ timeout: s.duration().default("20s").title("请求超时"),
284
+ retry: s.number().int().min(0).max(5).default(2).title("请求重试次数").desc("仅对幂等请求与网络层错误重试。"),
285
+ userAgent: s
286
+ .string()
287
+ .optional()
288
+ .title("User-Agent")
289
+ .desc("留空使用框架默认 UA。米游社相关请求由插件自行覆盖,不受此项影响。")
290
+ })
291
+ .title("网络")
292
+ .group("网络")
293
+ .order(80)
294
+ })
295
+
296
+ /** 内核配置类型 */
297
+ export type CoreConfig = Infer<typeof coreConfigSchema>
298
+
299
+ /**
300
+ * 内核配置的只读快照类型
301
+ *
302
+ * 即 `ConfigFile.get()` 的返回类型。下面几个读配置的辅助函数都收这个而不是
303
+ * `CoreConfig`:`DeepReadonly` 会把 `string[]` 变成 `readonly string[]`,
304
+ * 后者赋给前者是类型错误,于是"拿 get() 的结果直接调用"会编译不过 ——
305
+ * 那是最自然的调用方式,不该被迫在调用点写断言。
306
+ * 反过来,可变的 `CoreConfig` 传进来仍然合法。
307
+ */
308
+ export type CoreConfigSnapshot = DeepReadonly<CoreConfig>
309
+
310
+ /** 内核配置句柄 */
311
+ export type CoreConfigHandle = ConfigFile<CoreConfig>
312
+
313
+ /**
314
+ * 在配置仓库里声明内核配置
315
+ * @param store 配置仓库
316
+ * @returns 内核配置句柄
317
+ */
318
+ export async function defineCoreConfig(store: ConfigStore): Promise<CoreConfigHandle> {
319
+ return store.define(CORE_CONFIG_NAME, coreConfigSchema, {
320
+ title: "内核",
321
+ notes: [
322
+ "插件的配置各自成文件,不要往这里加 —— 谁消费、谁声明。",
323
+ "面板地址默认 http://127.0.0.1:2536"
324
+ ]
325
+ })
326
+ }
327
+
328
+ /**
329
+ * 从内核配置中取出日志器所需的部分
330
+ *
331
+ * 日志器必须在配置加载**之前**先行就绪(否则加载配置时发生的错误无处记录),
332
+ * 因此此处提供一个"配置就绪后再调整级别"的映射函数。
333
+ * @param config 内核配置
334
+ * @returns 日志相关设置
335
+ */
336
+ export function loggerSettingsOf(config: CoreConfigSnapshot): {
337
+ /** 总级别 */
338
+ level: CoreConfig["log"]["level"]
339
+ /** 控制台级别 */
340
+ consoleLevel: CoreConfig["log"]["level"] | undefined
341
+ /** 文件级别 */
342
+ fileLevel: CoreConfig["log"]["level"] | undefined
343
+ /** 单文件字节上限 */
344
+ maxSize: number
345
+ /** 保留天数 */
346
+ keepDays: number
347
+ /** 文件数上限 */
348
+ maxFiles: number
349
+ /** 是否彩色 */
350
+ color: boolean
351
+ } {
352
+ return {
353
+ level: config.log.level,
354
+ consoleLevel: config.log.consoleLevel,
355
+ fileLevel: config.log.fileLevel,
356
+ maxSize: config.log.maxSize * 1024 * 1024,
357
+ keepDays: config.log.keepDays,
358
+ maxFiles: config.log.maxFiles,
359
+ color: config.log.color
360
+ }
361
+ }
362
+
363
+ /**
364
+ * 判断当前监听配置是否对外暴露却没有设令牌
365
+ *
366
+ * 安全底线:非本机监听必须设令牌。
367
+ * @param config 内核配置
368
+ * @returns 不安全时返回原因,安全时返回 undefined
369
+ */
370
+ export function serverSecurityWarning(config: CoreConfigSnapshot): string | undefined {
371
+ const host = config.server.host
372
+ const isLocal = host === "127.0.0.1" || host === "localhost" || host === "::1"
373
+ if (isLocal) return undefined
374
+ if (!config.server.token) return `面板监听于 ${host} 但未设置访问令牌,任意来源均可修改本实例配置`
375
+ if (config.server.token.length < 16) return "面板访问令牌太短,建议至少 16 位"
376
+ return undefined
377
+ }
378
+
379
+ /**
380
+ * 用配置里的日志器设置更新运行中的日志器
381
+ *
382
+ * 抽成函数是为了让 `kernel/app.ts` 与配置的 `onChange` 复用同一段逻辑。
383
+ * @param logger 日志器(需支持 setLevel 的具体实现)
384
+ * @param config 内核配置
385
+ */
386
+ export function applyLogLevel(
387
+ logger: { setLevel(level: CoreConfig["log"]["level"], target?: "console" | "file"): void },
388
+ config: CoreConfigSnapshot
389
+ ): void {
390
+ logger.setLevel(config.log.level)
391
+ if (config.log.consoleLevel) logger.setLevel(config.log.consoleLevel, "console")
392
+ if (config.log.fileLevel) logger.setLevel(config.log.fileLevel, "file")
393
+ }
394
+
395
+ /** 供 CLI `yzng doctor` 使用的字段说明表(避免文档与实现脱节) */
396
+ export function describeCoreConfig(): ReturnType<typeof coreConfigSchema.describe> {
397
+ return coreConfigSchema.describe()
398
+ }
399
+
400
+ /** 内核配置里所有敏感字段的路径,日志与面板导出时需脱敏 */
401
+ export const CORE_SECRET_PATHS: readonly string[] = coreConfigSchema.secretPaths()