@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,212 @@
1
+ /**
2
+ * 模块职责:`e.prompt()` 的等待者登记表(会话内"等下一条消息")
3
+ * 依赖方向:仅依赖类型包
4
+ * 生命周期:随内核创建;每个等待者自带超时与插件卸载信号,不会长期驻留
5
+ * 注意事项:**等待者集中登记,不挂在插件实例上。** 挂在实例上的上下文会随热重载
6
+ * 一并悬空:使用者仍在等输入,而插件已被替换,此后他回复的消息永远得不到
7
+ * 响应,那个闭包也永远不被释放。
8
+ *
9
+ * 集中登记后**同时**绑定三个终止条件:
10
+ * 超时、插件卸载信号、被消息满足。任一条件触发均会摘除登记,
11
+ * 因此不存在永久驻留的等待者。
12
+ *
13
+ * 同一会话可存在多个等待者,按 FIFO 交付:先登记者先取得。
14
+ * 随机交付或全部唤醒均将使"两个插件同时向使用者提问"的行为不可预测。
15
+ */
16
+ import type { Disposer, MessageEvent } from "@yunzai-ng/types"
17
+ import { targetKey } from "../message/target.js"
18
+
19
+ /** 缺省等待超时(毫秒) */
20
+ export const DEFAULT_PROMPT_TIMEOUT = 60_000
21
+
22
+ /** 一个等待者 */
23
+ interface Waiter {
24
+ /** 会话键 */
25
+ readonly key: string
26
+ /** 发起等待的用户 */
27
+ readonly uid: string
28
+ /** 是否只接受同一用户 */
29
+ readonly sameUser: boolean
30
+ /** 额外过滤 */
31
+ readonly filter: ((e: MessageEvent) => boolean) | undefined
32
+ /** 发起等待的插件名(日志与按插件取消用) */
33
+ readonly plugin: string
34
+ /** 结束等待 */
35
+ resolve(e: MessageEvent | undefined): void
36
+ /** 摘除全部终止条件 */
37
+ cleanup(): void
38
+ }
39
+
40
+ /** 登记一个等待者所需的参数 */
41
+ export interface PromptWaitParams {
42
+ /** 会话键,用 `sessionKeyOf()` 计算 */
43
+ readonly key: string
44
+ /** 发起等待的用户 id */
45
+ readonly uid: string
46
+ /** 是否只接受同一用户的消息 */
47
+ readonly sameUser: boolean
48
+ /** 超时毫秒 */
49
+ readonly timeout: number
50
+ /** 额外过滤;返回 false 表示这条不算,继续等 */
51
+ readonly filter?: ((e: MessageEvent) => boolean) | undefined
52
+ /** 发起等待的插件名 */
53
+ readonly plugin: string
54
+ /** 插件卸载信号;abort 时等待以 undefined 结束 */
55
+ readonly signal?: AbortSignal | undefined
56
+ }
57
+
58
+ /**
59
+ * 计算会话键
60
+ *
61
+ * `selfId` 参与计算:同一个群中接入两个账号时,各自的等待互不干扰。
62
+ * 缺少它,A 号提出的问题会被回复给 B 号的等待者。
63
+ *
64
+ * 会话部分复用 `targetKey()`:发送队列用的是同一个函数,两处对"哪个算同一个
65
+ * 会话"的理解因此不可能走样。
66
+ * @param e 消息事件
67
+ * @returns 会话键
68
+ */
69
+ export function sessionKeyOf(e: MessageEvent): string {
70
+ return `${e.platform}:${e.selfId}:${targetKey(e.target)}`
71
+ }
72
+
73
+ /** `e.prompt()` 的等待者登记表 */
74
+ export class PromptRegistry {
75
+ /** 会话键 → 该会话上的等待者队列(FIFO) */
76
+ readonly #waiters = new Map<string, Waiter[]>()
77
+
78
+ /** 当前等待中的数量 */
79
+ get pending(): number {
80
+ let n = 0
81
+ for (const list of this.#waiters.values()) n += list.length
82
+ return n
83
+ }
84
+
85
+ /**
86
+ * 登记一个等待者
87
+ * @param params 等待参数
88
+ * @returns 下一条满足条件的消息;超时或插件卸载时 undefined
89
+ */
90
+ wait(params: PromptWaitParams): Promise<MessageEvent | undefined> {
91
+ return new Promise<MessageEvent | undefined>(resolve => {
92
+ let done = false
93
+ let timer: ReturnType<typeof setTimeout> | undefined
94
+ let offSignal: Disposer | undefined
95
+
96
+ /** 统一收尾:摘登记、清定时器、解绑信号 */
97
+ const cleanup = (): void => {
98
+ if (timer !== undefined) clearTimeout(timer)
99
+ offSignal?.()
100
+ this.#detach(params.key, waiter)
101
+ }
102
+
103
+ /** 只允许结束一次 */
104
+ const finish = (e: MessageEvent | undefined): void => {
105
+ if (done) return
106
+ done = true
107
+ cleanup()
108
+ resolve(e)
109
+ }
110
+
111
+ const waiter: Waiter = {
112
+ key: params.key,
113
+ uid: params.uid,
114
+ sameUser: params.sameUser,
115
+ filter: params.filter,
116
+ plugin: params.plugin,
117
+ resolve: finish,
118
+ cleanup
119
+ }
120
+
121
+ if (params.timeout > 0) {
122
+ timer = setTimeout(() => finish(undefined), params.timeout)
123
+ // 不使一个等待中的 prompt 阻止进程退出:使用者可能始终不再回复,
124
+ // 而停机不应为此额外等待 60 秒
125
+ timer.unref?.()
126
+ }
127
+
128
+ const signal = params.signal
129
+ if (signal !== undefined) {
130
+ if (signal.aborted) {
131
+ finish(undefined)
132
+ return
133
+ }
134
+ /** 插件卸载即结束等待 */
135
+ const onAbort = (): void => finish(undefined)
136
+ signal.addEventListener("abort", onAbort, { once: true })
137
+ offSignal = () => signal.removeEventListener("abort", onAbort)
138
+ }
139
+
140
+ let list = this.#waiters.get(params.key)
141
+ if (list === undefined) {
142
+ list = []
143
+ this.#waiters.set(params.key, list)
144
+ }
145
+ list.push(waiter)
146
+ })
147
+ }
148
+
149
+ /**
150
+ * 把消息投给该会话上的等待者
151
+ *
152
+ * 命中即消费:这条消息不再进入命令路由。否则"用户回答的内容恰好是个命令"
153
+ * 就会既满足等待又触发命令,两条回复一起发出来。
154
+ * @param e 消息事件
155
+ * @returns 是否被某个等待者消费
156
+ */
157
+ offer(e: MessageEvent): boolean {
158
+ const key = sessionKeyOf(e)
159
+ const list = this.#waiters.get(key)
160
+ if (list === undefined || list.length === 0) return false
161
+
162
+ for (const waiter of [...list]) {
163
+ if (waiter.sameUser && waiter.uid !== e.sender.uid) continue
164
+ if (waiter.filter !== undefined && !waiter.filter(e)) continue
165
+ waiter.resolve(e)
166
+ return true
167
+ }
168
+ return false
169
+ }
170
+
171
+ /**
172
+ * 取消某插件的全部等待
173
+ *
174
+ * 插件的 `ctx.signal` 已经能覆盖正常卸载路径,这里是兜底:`prompt` 若被
175
+ * 转手到别的插件(服务注入场景)调用,signal 可能对不上。
176
+ * @param plugin 插件名
177
+ * @returns 取消的数量
178
+ */
179
+ cancelPlugin(plugin: string): number {
180
+ let n = 0
181
+ for (const list of [...this.#waiters.values()]) {
182
+ for (const waiter of [...list]) {
183
+ if (waiter.plugin === plugin) {
184
+ waiter.resolve(undefined)
185
+ n++
186
+ }
187
+ }
188
+ }
189
+ return n
190
+ }
191
+
192
+ /** 结束全部等待(停机时调用) */
193
+ clear(): void {
194
+ for (const list of [...this.#waiters.values()]) {
195
+ for (const waiter of [...list]) waiter.resolve(undefined)
196
+ }
197
+ this.#waiters.clear()
198
+ }
199
+
200
+ /**
201
+ * 将等待者从队列中摘除
202
+ * @param key 会话键
203
+ * @param waiter 等待者
204
+ */
205
+ #detach(key: string, waiter: Waiter): void {
206
+ const list = this.#waiters.get(key)
207
+ if (list === undefined) return
208
+ const at = list.indexOf(waiter)
209
+ if (at >= 0) list.splice(at, 1)
210
+ if (list.length === 0) this.#waiters.delete(key)
211
+ }
212
+ }
@@ -0,0 +1,432 @@
1
+ /**
2
+ * 模块职责:命令路由器的单元测试 —— 分桶索引、优先级、权限静态门、正则处理
3
+ * 依赖方向:测试文件,依赖 pipeline/router 与 testing/fake
4
+ * 生命周期:每个用例新建一个路由器,无共享状态
5
+ * 注意事项:这里**故意不建真事件**。`match()` 只读 8 个字段
6
+ * (text / scene / sender.uid / selfId / isGroup / atMe / isMaster /
7
+ * isGroupAdmin),为此启动一整套 EventFactory + BotApi 只会把
8
+ * "路由到底按什么匹配"埋进无关噪音里 —— 端到端那一层由
9
+ * kernel/runtime.test.ts 覆盖,这里专测索引与门禁本身。
10
+ *
11
+ * 分桶是这个文件最容易悄悄失效的优化:写错了功能照样正确,只是退化成
12
+ * 全表扫描。所以有几处断言直接盯着"另一个桶里的正则一次都没被 exec",
13
+ * 而不只是断言"没匹配上"。
14
+ */
15
+ import { describe, expect, it, vi } from "vitest"
16
+ import type { CommandOptions, CommandPattern, MessageEvent, MessageScene } from "@yunzai-ng/types"
17
+ import type { CommandRegistration } from "../plugin/hooks.js"
18
+ import { fakeLogger, type FakeLogger } from "../testing/fake.js"
19
+ import { CommandRouter } from "./router.js"
20
+
21
+ /** 构造假消息事件的入参 */
22
+ interface MsgSpec {
23
+ /** 纯文本 */
24
+ text: string
25
+ /** 场景,缺省私聊 */
26
+ scene?: MessageScene
27
+ /** 发送者,缺省 20000 */
28
+ uid?: string
29
+ /** 接收账号,缺省 10000 */
30
+ selfId?: string
31
+ /** 是否 @ 了机器人 */
32
+ atMe?: boolean
33
+ /** 是否主人 */
34
+ isMaster?: boolean
35
+ /** 是否群管 */
36
+ isGroupAdmin?: boolean
37
+ }
38
+
39
+ /**
40
+ * 造一个够路由用的假消息事件
41
+ * @param spec 文本或完整描述
42
+ * @returns 消息事件
43
+ */
44
+ function msg(spec: MsgSpec | string): MessageEvent {
45
+ const s: MsgSpec = typeof spec === "string" ? { text: spec } : spec
46
+ const scene = s.scene ?? "private"
47
+ return {
48
+ kind: "message",
49
+ scene,
50
+ text: s.text,
51
+ selfId: s.selfId ?? "10000",
52
+ sender: { uid: s.uid ?? "20000", name: "阿测" },
53
+ isPrivate: scene === "private",
54
+ isGroup: scene === "group",
55
+ atMe: s.atMe ?? false,
56
+ isMaster: s.isMaster ?? false,
57
+ isGroupAdmin: s.isGroupAdmin ?? false,
58
+ isGroupOwner: false
59
+ } as unknown as MessageEvent
60
+ }
61
+
62
+ /**
63
+ * 造一条命令登记
64
+ * @param patterns 模式或模式数组
65
+ * @param options 命令选项
66
+ * @param plugin 插件名,断言顺序时用它当标记
67
+ * @returns 登记内容
68
+ */
69
+ function reg(
70
+ patterns: CommandPattern | CommandPattern[],
71
+ options: CommandOptions = {},
72
+ plugin = "p"
73
+ ): CommandRegistration {
74
+ return {
75
+ plugin,
76
+ patterns: Array.isArray(patterns) ? patterns : [patterns],
77
+ options,
78
+ handler: () => undefined
79
+ }
80
+ }
81
+
82
+ /** 建一个路由器与它的日志器 */
83
+ function make(isDisabled?: (r: CommandRegistration) => boolean): { router: CommandRouter; logger: FakeLogger } {
84
+ const logger = fakeLogger()
85
+ const router = new CommandRouter(isDisabled === undefined ? { logger } : { logger, isDisabled })
86
+ return { router, logger }
87
+ }
88
+
89
+ /**
90
+ * 取命中的插件名序列,断言顺序时比逐个挖字段清楚
91
+ * @param router 路由器
92
+ * @param spec 消息
93
+ * @returns 插件名数组
94
+ */
95
+ function hits(router: CommandRouter, spec: MsgSpec | string): string[] {
96
+ return router.match(msg(spec)).map(c => c.reg.plugin)
97
+ }
98
+
99
+ describe("字符串模式", () => {
100
+ it("按前缀命中,填好 trigger/rest/captures", () => {
101
+ const { router } = make()
102
+ router.register(reg("#查角色"))
103
+
104
+ const [hit] = router.match(msg("#查角色 刻晴 命座"))
105
+ expect(hit?.match.name).toBe("#查角色")
106
+ expect(hit?.match.pattern).toBe("#查角色")
107
+ expect(hit?.match.trigger).toBe("#查角色")
108
+ // rest 已 trim:插件里最常见的一行就是 `const name = e.command.rest`
109
+ expect(hit?.match.rest).toBe("刻晴 命座")
110
+ expect(hit?.match.groups).toEqual({})
111
+ expect(hit?.match.captures).toEqual(["#查角色"])
112
+ expect(hit?.match.plugin).toBe("p")
113
+ })
114
+
115
+ it("前缀不在开头时不命中", () => {
116
+ const { router } = make()
117
+ router.register(reg("#ping"))
118
+
119
+ expect(router.match(msg("你好 #ping"))).toEqual([])
120
+ expect(router.match(msg("ping"))).toEqual([])
121
+ })
122
+
123
+ it("anywhere 允许触发词出现在任意位置", () => {
124
+ const { router } = make()
125
+ router.register(reg("体力", { anywhere: true }))
126
+
127
+ const [hit] = router.match(msg("帮我看看体力还剩多少"))
128
+ expect(hit?.match.trigger).toBe("体力")
129
+ expect(hit?.match.rest).toBe("还剩多少")
130
+ })
131
+
132
+ it("空串模式被拒绝,同一条命令的其余别名照旧可用", () => {
133
+ const { router, logger } = make()
134
+ router.register(reg(["", "#ok"]))
135
+
136
+ expect(hits(router, "#ok")).toEqual(["p"])
137
+ expect(logger.lines.some(l => l.includes("无效"))).toBe(true)
138
+ })
139
+
140
+ it("一条命令的模式全都无效时不会被触发,但会留下告警", () => {
141
+ const { router, logger } = make()
142
+ router.register(reg(""))
143
+
144
+ expect(router.match(msg("随便说点什么"))).toEqual([])
145
+ expect(logger.lines.some(l => l.includes("没有任何有效模式"))).toBe(true)
146
+ // 条目本身还在注册表里:WebUI 得能显示"这条命令坏了"
147
+ expect(router.size).toBe(1)
148
+ })
149
+ })
150
+
151
+ describe("正则模式", () => {
152
+ it("命名捕获与数字捕获都填进 match,未参与的组归一成空串", () => {
153
+ const { router } = make()
154
+ router.register(reg(/^#查(?<name>\S+?)(\d+)?命座/))
155
+
156
+ const [hit] = router.match(msg("#查刻晴命座 顺便看天赋"))
157
+ expect(hit?.match.groups).toEqual({ name: "刻晴" })
158
+ expect(hit?.match.captures).toEqual(["#查刻晴命座", "刻晴", ""])
159
+ expect(hit?.match.rest).toBe("顺便看天赋")
160
+ })
161
+
162
+ it("g/y 标志被去掉:同一条命令连发两次都要命中", () => {
163
+ const { router, logger } = make()
164
+ router.register(reg(/^#dup/g))
165
+
166
+ // 不去标志的话第二次 exec 会从上次的 lastIndex 继续找,表现为"命令只灵一次"
167
+ expect(router.match(msg("#dup"))).toHaveLength(1)
168
+ expect(router.match(msg("#dup"))).toHaveLength(1)
169
+ expect(logger.lines.some(l => l.includes("g/y"))).toBe(true)
170
+ })
171
+
172
+ it("正则的 name 取 source,不含斜杠与标志", () => {
173
+ const { router } = make()
174
+ router.register(reg(/^#状态$/i))
175
+
176
+ expect(router.list()[0]?.name).toBe("^#状态$")
177
+ expect(router.list()[0]?.patterns).toEqual(["^#状态$"])
178
+ })
179
+ })
180
+
181
+ describe("首字符分桶", () => {
182
+ it("首字符不同的命令连 exec 都不会被调到", () => {
183
+ const { router } = make()
184
+ const re = /^#ping/
185
+ const spy = vi.spyOn(re, "exec")
186
+ router.register(reg(re))
187
+
188
+ expect(router.match(msg("!别的命令"))).toEqual([])
189
+ // 这一条是分桶优化本身的断言:功能正确但退化成全表扫描时它会红
190
+ expect(spy).not.toHaveBeenCalled()
191
+
192
+ expect(router.match(msg("#ping"))).toHaveLength(1)
193
+ expect(spy).toHaveBeenCalledTimes(1)
194
+ })
195
+
196
+ it("提不出字面前缀的正则每条消息都要试", () => {
197
+ const { router } = make()
198
+ router.register(reg(/^\d+/))
199
+
200
+ const [hit] = router.match(msg("42 天后"))
201
+ expect(hit?.match.trigger).toBe("42")
202
+ expect(hit?.match.rest).toBe("天后")
203
+ })
204
+
205
+ it("带 m 标志的正则不分桶:第二行开头也算命中", () => {
206
+ const { router } = make()
207
+ router.register(reg(/^#x/m))
208
+
209
+ // 首字符是"啊",按 `#` 桶找不到;正因为 m 标志落进了每条都试的那份名单才命中
210
+ expect(hits(router, "啊\n#x")).toEqual(["p"])
211
+ })
212
+
213
+ it("带 i 标志且字母开头的正则不分桶:大小写都能命中", () => {
214
+ const { router } = make()
215
+ router.register(reg(/^abc/i))
216
+
217
+ expect(hits(router, "ABC 大写")).toEqual(["p"])
218
+ expect(hits(router, "abc 小写")).toEqual(["p"])
219
+ })
220
+
221
+ it("同一条命令的多个别名落在同一个桶里也只命中一次", () => {
222
+ const { router } = make()
223
+ router.register(reg(["#a", "#ab"]))
224
+
225
+ const found = router.match(msg("#ab"))
226
+ expect(found).toHaveLength(1)
227
+ // 按声明顺序取第一个能匹配上的模式
228
+ expect(found[0]?.match.pattern).toBe("#a")
229
+ })
230
+
231
+ it("一条命令同时有可分桶与不可分桶的模式时也只命中一次", () => {
232
+ const { router } = make()
233
+ router.register(reg(["#x", /#x/]))
234
+
235
+ expect(router.match(msg("#x"))).toHaveLength(1)
236
+ })
237
+ })
238
+
239
+ describe("优先级与顺序", () => {
240
+ it("priority 小者在前,同优先级按注册先后", () => {
241
+ const { router } = make()
242
+ router.register(reg("#a", {}, "先注册"))
243
+ router.register(reg("#a", { priority: 50 }, "更优先"))
244
+ router.register(reg("#a", {}, "后注册"))
245
+
246
+ expect(hits(router, "#a")).toEqual(["更优先", "先注册", "后注册"])
247
+ })
248
+
249
+ it("桶内与不分桶的候选按同一套优先级归并", () => {
250
+ const a = make()
251
+ a.router.register(reg("#x", {}, "桶内"))
252
+ a.router.register(reg(/#x/, { priority: 50 }, "不分桶"))
253
+ expect(hits(a.router, "#x")).toEqual(["不分桶", "桶内"])
254
+
255
+ const b = make()
256
+ b.router.register(reg("#x", {}, "桶内"))
257
+ b.router.register(reg(/#x/, { priority: 150 }, "不分桶"))
258
+ expect(hits(b.router, "#x")).toEqual(["桶内", "不分桶"])
259
+ })
260
+ })
261
+
262
+ describe("静态门禁", () => {
263
+ it("还没 action() 的命令被跳过", () => {
264
+ const { router } = make()
265
+ const r = reg("#半成品")
266
+ r.handler = undefined
267
+ router.register(r)
268
+
269
+ expect(router.match(msg("#半成品"))).toEqual([])
270
+ })
271
+
272
+ it("被禁用的命令被跳过,但仍出现在 list() 里且标了 disabled", () => {
273
+ const { router } = make(r => r.options.group === "关掉的")
274
+ router.register(reg("#关", { group: "关掉的" }))
275
+ router.register(reg("#开"))
276
+
277
+ expect(router.match(msg("#关"))).toEqual([])
278
+ expect(hits(router, "#开")).toEqual(["p"])
279
+ expect(router.list().map(i => i.disabled)).toEqual([true, false])
280
+ })
281
+
282
+ it("scene 限定生效,单值与数组两种写法都算", () => {
283
+ const { router } = make()
284
+ router.register(reg("#群限", { scene: "group" }, "仅群"))
285
+ router.register(reg("#多限", { scene: ["private", "guild"] }, "私聊与频道"))
286
+
287
+ expect(hits(router, "#群限")).toEqual([])
288
+ expect(hits(router, { text: "#群限", scene: "group" })).toEqual(["仅群"])
289
+ expect(hits(router, "#多限")).toEqual(["私聊与频道"])
290
+ expect(hits(router, { text: "#多限", scene: "group" })).toEqual([])
291
+ })
292
+
293
+ it("默认不响应机器人自己发的消息,ignoreSelf:false 才放行", () => {
294
+ const { router } = make()
295
+ router.register(reg("#复读", {}, "默认"))
296
+ router.register(reg("#复读", { ignoreSelf: false }, "放行"))
297
+
298
+ // 自己发的:只有显式声明放行的那条能命中,否则复读类命令会自我触发成死循环
299
+ expect(hits(router, { text: "#复读", uid: "10000", selfId: "10000" })).toEqual(["放行"])
300
+ expect(hits(router, { text: "#复读", uid: "20000" })).toEqual(["默认", "放行"])
301
+ })
302
+
303
+ it("atMe 只约束群聊,私聊天然算对我说话", () => {
304
+ const { router } = make()
305
+ router.register(reg("#要艾特", { atMe: true }))
306
+
307
+ expect(hits(router, { text: "#要艾特", scene: "group" })).toEqual([])
308
+ expect(hits(router, { text: "#要艾特", scene: "group", atMe: true })).toEqual(["p"])
309
+ expect(hits(router, "#要艾特")).toEqual(["p"])
310
+ })
311
+
312
+ it("master 门只对主人开", () => {
313
+ const { router } = make()
314
+ router.register(reg("#重启", { master: true }))
315
+
316
+ expect(hits(router, "#重启")).toEqual([])
317
+ expect(hits(router, { text: "#重启", isMaster: true })).toEqual(["p"])
318
+ })
319
+
320
+ it("admin 门对群管开,主人不受群管限制", () => {
321
+ const { router } = make()
322
+ router.register(reg("#踢人", { admin: true }))
323
+
324
+ expect(hits(router, { text: "#踢人", scene: "group" })).toEqual([])
325
+ expect(hits(router, { text: "#踢人", scene: "group", isGroupAdmin: true })).toEqual(["p"])
326
+ // 主人在自己不是管理员的群里也该能用管理命令
327
+ expect(hits(router, { text: "#踢人", scene: "group", isMaster: true })).toEqual(["p"])
328
+ })
329
+ })
330
+
331
+ describe("注册表维护", () => {
332
+ it("注销后自所有桶中完整摘除", () => {
333
+ const { router } = make()
334
+ const off = router.register(reg(["#a", "!b"]))
335
+ expect(hits(router, "#a")).toEqual(["p"])
336
+ expect(hits(router, "!b")).toEqual(["p"])
337
+
338
+ off()
339
+
340
+ expect(router.match(msg("#a"))).toEqual([])
341
+ expect(router.match(msg("!b"))).toEqual([])
342
+ expect(router.size).toBe(0)
343
+ })
344
+
345
+ it("重复注册同一条登记是幂等的", () => {
346
+ const { router } = make()
347
+ const r = reg("#once")
348
+ router.register(r)
349
+ router.register(r)
350
+
351
+ expect(router.size).toBe(1)
352
+ expect(router.match(msg("#once"))).toHaveLength(1)
353
+ })
354
+
355
+ it("reindex 让后加的别名生效,展示名仍是主模式", () => {
356
+ const { router } = make()
357
+ const r = reg("#体力")
358
+ router.register(r)
359
+ expect(router.match(msg("#树脂"))).toEqual([])
360
+
361
+ r.patterns.push("#树脂")
362
+ router.reindex(r)
363
+
364
+ expect(hits(router, "#树脂")).toEqual(["p"])
365
+ expect(hits(router, "#体力")).toEqual(["p"])
366
+ expect(router.list()[0]?.name).toBe("#体力")
367
+ // 重建而不是追加:别名不该在桶里留下重复条目
368
+ expect(router.match(msg("#体力"))).toHaveLength(1)
369
+ })
370
+
371
+ it("reindex 未注册过的登记是空操作", () => {
372
+ const { router } = make()
373
+ router.reindex(reg("#没注册过"))
374
+ expect(router.size).toBe(0)
375
+ })
376
+
377
+ it("removePlugin 只摘该插件的命令", () => {
378
+ const { router } = make()
379
+ router.register(reg("#a", {}, "甲"))
380
+ router.register(reg("#b", {}, "甲"))
381
+ router.register(reg("#c", {}, "乙"))
382
+
383
+ expect(router.removePlugin("甲")).toBe(2)
384
+ expect(router.size).toBe(1)
385
+ expect(hits(router, "#c")).toEqual(["乙"])
386
+ })
387
+
388
+ it("clear 清空一切", () => {
389
+ const { router } = make()
390
+ router.register(reg("#a"))
391
+ router.register(reg(/^\d+/))
392
+
393
+ router.clear()
394
+
395
+ expect(router.size).toBe(0)
396
+ expect(router.list()).toEqual([])
397
+ expect(router.match(msg("#a"))).toEqual([])
398
+ expect(router.match(msg("42"))).toEqual([])
399
+ })
400
+
401
+ it("list() 导出 WebUI 需要的全部字段,按匹配顺序", () => {
402
+ const { router } = make()
403
+ router.register(reg("#后但更优先", { priority: 10 }, "乙"))
404
+ router.register(
405
+ reg("#帮助", { desc: "看帮助", usage: "#帮助 [页码]", group: "系统", hidden: true, master: true }, "甲")
406
+ )
407
+
408
+ expect(router.list()).toEqual([
409
+ {
410
+ name: "#后但更优先",
411
+ patterns: ["#后但更优先"],
412
+ plugin: "乙",
413
+ master: false,
414
+ admin: false,
415
+ hidden: false,
416
+ disabled: false
417
+ },
418
+ {
419
+ name: "#帮助",
420
+ patterns: ["#帮助"],
421
+ plugin: "甲",
422
+ master: true,
423
+ admin: false,
424
+ hidden: true,
425
+ disabled: false,
426
+ desc: "看帮助",
427
+ usage: "#帮助 [页码]",
428
+ group: "系统"
429
+ }
430
+ ])
431
+ })
432
+ })