@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,34 @@
1
+ /**
2
+ * 算出发送目标的会话键
3
+ *
4
+ * 私聊**刻意忽略** `gid`:群临时会话与直接私聊是同一个人的同一个聊天窗口,
5
+ * 分成两个键会让"从群里发起的对话"与"私聊里的对话"各排一条队。
6
+ * @param target 发送目标
7
+ * @returns 会话键
8
+ */
9
+ export function targetKey(target) {
10
+ switch (target.scene) {
11
+ case "group":
12
+ return `g:${target.gid}`;
13
+ case "guild":
14
+ return `c:${target.guildId}/${target.channelId}`;
15
+ case "private":
16
+ return `p:${target.uid}`;
17
+ }
18
+ }
19
+ /**
20
+ * 将发送目标渲染为一段可读描述
21
+ * @param target 发送目标
22
+ * @returns 描述文本,如 `群 123456`
23
+ */
24
+ export function describeTarget(target) {
25
+ switch (target.scene) {
26
+ case "group":
27
+ return `群 ${target.gid}`;
28
+ case "guild":
29
+ return `频道 ${target.guildId}/${target.channelId}`;
30
+ case "private":
31
+ return target.gid === undefined ? `私聊 ${target.uid}` : `临时会话 ${target.uid}(来自群 ${target.gid})`;
32
+ }
33
+ }
34
+ //# sourceMappingURL=target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target.js","sourceRoot":"","sources":["../../src/message/target.ts"],"names":[],"mappings":"AAcA;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,MAAkB;IAC1C,QAAQ,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,KAAK,OAAO;YACV,OAAO,KAAK,MAAM,CAAC,GAAG,EAAE,CAAA;QAC1B,KAAK,OAAO;YACV,OAAO,KAAK,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,SAAS,EAAE,CAAA;QAClD,KAAK,SAAS;YACZ,OAAO,KAAK,MAAM,CAAC,GAAG,EAAE,CAAA;IAC5B,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAkB;IAC/C,QAAQ,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,KAAK,OAAO;YACV,OAAO,KAAK,MAAM,CAAC,GAAG,EAAE,CAAA;QAC1B,KAAK,OAAO;YACV,OAAO,MAAM,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,SAAS,EAAE,CAAA;QACnD,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,MAAM,CAAC,GAAG,QAAQ,MAAM,CAAC,GAAG,GAAG,CAAA;IAClG,CAAC;AACH,CAAC"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 模块职责:命令冷却(CD)的判定与计费
3
+ * 依赖方向:依赖类型包与 util/lru、util/duration
4
+ * 生命周期:随内核创建;纯内存,不落盘
5
+ * 注意事项:**刻意不落盘、不进 Redis**。把 CD 写成 Redis key 会带来两个问题:
6
+ * 没装 Redis 就起不来,且每条命令都要一次网络往返。
7
+ *
8
+ * CD 的本质是"几秒到几分钟内别重复触发",重启后清零完全无害
9
+ * (甚至是用户期望的)。因此用有界的 LRU + TTL 存在内存里:
10
+ * 热路径上零 I/O,且条目数有硬上限,不会像裸 Map 那样无限增长。
11
+ *
12
+ * 主人不受 CD 限制:调试时被自己设的 5 分钟 CD 锁在门外是最没必要的
13
+ * 挫败感。这条是内核的既定行为,不是插件可配项。
14
+ */
15
+ import type { CooldownScope, MessageEvent } from "@yunzai-ng/types";
16
+ /** 冷却判定结果 */
17
+ export type CooldownDecision = {
18
+ /** 可以执行 */
19
+ readonly ok: true;
20
+ } | {
21
+ /** 处于冷却中 */
22
+ readonly ok: false;
23
+ /** 剩余毫秒 */
24
+ readonly remaining: number;
25
+ };
26
+ /** 冷却存储构造参数 */
27
+ export interface CooldownStoreOptions {
28
+ /** 条目上限,缺省 20000 */
29
+ readonly max?: number;
30
+ }
31
+ /**
32
+ * 按作用域构造冷却键
33
+ *
34
+ * `group` 作用域在私聊中退化为 `user`:否则私聊消息不具备 gid,全部私聊用户
35
+ * 将共享同一把锁,成为事实上的全局冷却 —— 该缺陷静默存在,且仅在生产环境暴露。
36
+ * @param plugin 插件名
37
+ * @param command 命令名
38
+ * @param scope 作用域
39
+ * @param e 消息事件
40
+ * @returns 冷却键
41
+ */
42
+ export declare function cooldownKey(plugin: string, command: string, scope: CooldownScope, e: MessageEvent): string;
43
+ /**
44
+ * 命令冷却存储
45
+ *
46
+ * 判定与计费是**同一个动作**(`hit`):分成 `check` + `mark` 两步的话,
47
+ * 两次调用之间同一用户的第二条消息就能挤进来,CD 形同虚设。
48
+ */
49
+ export declare class CooldownStore {
50
+ #private;
51
+ /**
52
+ * @param opts 构造参数
53
+ */
54
+ constructor(opts?: CooldownStoreOptions);
55
+ /** 当前跟踪的冷却条目数 */
56
+ get size(): number;
57
+ /**
58
+ * 判定冷却,未冷却则立即占用
59
+ * @param key 冷却键
60
+ * @param ms 冷却毫秒;`<= 0` 视为不设冷却
61
+ * @returns 判定结果
62
+ */
63
+ hit(key: string, ms: number): CooldownDecision;
64
+ /**
65
+ * 提前解除某条冷却
66
+ *
67
+ * 给"命令执行失败了,不该占用用户的 CD"这种场景用 —— 让用户为一次
68
+ * 接口超时等 5 分钟是不合理的。
69
+ * @param key 冷却键
70
+ */
71
+ reset(key: string): void;
72
+ /** 清空全部冷却 */
73
+ clear(): void;
74
+ }
75
+ /**
76
+ * 生成冷却提示语
77
+ *
78
+ * 支持 `{time}` 占位符,插件写 `cooldownTip: "冷静一下,还有 {time}"` 即可。
79
+ * @param template 插件配置的提示语模板
80
+ * @param remaining 剩余毫秒
81
+ * @returns 提示文本
82
+ */
83
+ export declare function renderCooldownTip(template: string, remaining: number): string;
84
+ //# sourceMappingURL=cooldown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cooldown.d.ts","sourceRoot":"","sources":["../../src/pipeline/cooldown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAOnE,aAAa;AACb,MAAM,MAAM,gBAAgB,GACxB;IACE,WAAW;IACX,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAClB,GACD;IACE,YAAY;IACZ,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAA;IAClB,WAAW;IACX,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B,CAAA;AAEL,eAAe;AACf,MAAM,WAAW,oBAAoB;IACnC,oBAAoB;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC,EAAE,YAAY,GAAG,MAAM,CAa1G;AAED;;;;;GAKG;AACH,qBAAa,aAAa;;IAIxB;;OAEG;gBACS,IAAI,GAAE,oBAAyB;IAM3C,iBAAiB;IACjB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,gBAAgB;IAW9C;;;;;;OAMG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIxB,aAAa;IACb,KAAK,IAAI,IAAI;CAGd;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAG7E"}
@@ -0,0 +1,94 @@
1
+ import { LruCache } from "../util/lru.js";
2
+ import { formatDuration } from "../util/duration.js";
3
+ /** 默认最多同时跟踪多少条冷却记录 */
4
+ const DEFAULT_MAX = 20_000;
5
+ /**
6
+ * 按作用域构造冷却键
7
+ *
8
+ * `group` 作用域在私聊中退化为 `user`:否则私聊消息不具备 gid,全部私聊用户
9
+ * 将共享同一把锁,成为事实上的全局冷却 —— 该缺陷静默存在,且仅在生产环境暴露。
10
+ * @param plugin 插件名
11
+ * @param command 命令名
12
+ * @param scope 作用域
13
+ * @param e 消息事件
14
+ * @returns 冷却键
15
+ */
16
+ export function cooldownKey(plugin, command, scope, e) {
17
+ const head = `${plugin}/${command}`;
18
+ const gid = e.group?.gid;
19
+ switch (scope) {
20
+ case "global":
21
+ return `${head}|*`;
22
+ case "group":
23
+ return gid === undefined ? `${head}|u:${e.sender.uid}` : `${head}|g:${gid}`;
24
+ case "groupUser":
25
+ return gid === undefined ? `${head}|u:${e.sender.uid}` : `${head}|g:${gid}:u:${e.sender.uid}`;
26
+ case "user":
27
+ return `${head}|u:${e.sender.uid}`;
28
+ }
29
+ }
30
+ /**
31
+ * 命令冷却存储
32
+ *
33
+ * 判定与计费是**同一个动作**(`hit`):分成 `check` + `mark` 两步的话,
34
+ * 两次调用之间同一用户的第二条消息就能挤进来,CD 形同虚设。
35
+ */
36
+ export class CooldownStore {
37
+ /** 键 → 冷却结束时间点(毫秒) */
38
+ #cache;
39
+ /**
40
+ * @param opts 构造参数
41
+ */
42
+ constructor(opts = {}) {
43
+ // ttl 设 0(不过期):每个条目在写入时带上自己的 ms,
44
+ // 各命令的 CD 长度不同,用统一 TTL 会算错剩余时间
45
+ this.#cache = new LruCache({ max: opts.max ?? DEFAULT_MAX, ttl: 0 });
46
+ }
47
+ /** 当前跟踪的冷却条目数 */
48
+ get size() {
49
+ return this.#cache.size;
50
+ }
51
+ /**
52
+ * 判定冷却,未冷却则立即占用
53
+ * @param key 冷却键
54
+ * @param ms 冷却毫秒;`<= 0` 视为不设冷却
55
+ * @returns 判定结果
56
+ */
57
+ hit(key, ms) {
58
+ if (ms <= 0)
59
+ return { ok: true };
60
+ const now = Date.now();
61
+ const until = this.#cache.get(key);
62
+ if (until !== undefined && until > now)
63
+ return { ok: false, remaining: until - now };
64
+ this.#cache.set(key, now + ms, ms);
65
+ return { ok: true };
66
+ }
67
+ /**
68
+ * 提前解除某条冷却
69
+ *
70
+ * 给"命令执行失败了,不该占用用户的 CD"这种场景用 —— 让用户为一次
71
+ * 接口超时等 5 分钟是不合理的。
72
+ * @param key 冷却键
73
+ */
74
+ reset(key) {
75
+ this.#cache.delete(key);
76
+ }
77
+ /** 清空全部冷却 */
78
+ clear() {
79
+ this.#cache.clear();
80
+ }
81
+ }
82
+ /**
83
+ * 生成冷却提示语
84
+ *
85
+ * 支持 `{time}` 占位符,插件写 `cooldownTip: "冷静一下,还有 {time}"` 即可。
86
+ * @param template 插件配置的提示语模板
87
+ * @param remaining 剩余毫秒
88
+ * @returns 提示文本
89
+ */
90
+ export function renderCooldownTip(template, remaining) {
91
+ const time = formatDuration(remaining);
92
+ return template.includes("{time}") ? template.replaceAll("{time}", time) : template;
93
+ }
94
+ //# sourceMappingURL=cooldown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cooldown.js","sourceRoot":"","sources":["../../src/pipeline/cooldown.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * 模块职责:事件分发编排 —— 把一条 `IncomingEvent` 走完"策略 → 中间件 → 等待 → 路由 → 冷却 → 处理"
3
+ * 依赖方向:依赖管线内各子模块与事件总线;不依赖适配器实现,也不被适配器 import
4
+ * 生命周期:随内核创建;无跨事件状态(冷却与等待都在各自的模块里)
5
+ * 注意事项:这是**唯一**决定"一条消息会发生什么"的地方,顺序即语义:
6
+ *
7
+ * 1. 维护模式 / 忽略自身 —— 在构造事件对象**之前**判掉,省掉全部派生计算
8
+ * 2. 中间件 —— 可改写 `message`、可注入字段、可 `stop()`
9
+ * 3. `e.prompt()` 等待者 —— 放在中间件之后(让改写生效)、路由之前
10
+ * (用户正在回答问题时不该同时触发命令,否则会收到两条回复)
11
+ * 4. 命令路由 —— 只做纯查询与零成本静态过滤
12
+ * 5. 冷却 —— 命中之后才计费,且主人豁免
13
+ * 6. 处理函数 —— 返回 `false` 继续找下一条,`block !== false` 则命中即止
14
+ *
15
+ * `submit()` **永不抛出**。适配器在 socket 回调里调它,一个未捕获的
16
+ * rejection 会变成 `unhandledRejection` 进而带崩整个进程。
17
+ */
18
+ import type { BotApi, IncomingEvent, Logger } from "@yunzai-ng/types";
19
+ import type { CoreEventBus } from "../plugin/events.js";
20
+ import type { CooldownStore } from "./cooldown.js";
21
+ import type { EventFactory, PluginRuntimeView } from "./event.js";
22
+ import type { MiddlewarePipeline } from "./middleware.js";
23
+ import type { PromptRegistry } from "./prompt.js";
24
+ import type { CommandRouter } from "./router.js";
25
+ /**
26
+ * 分发器需要的策略视图
27
+ *
28
+ * 仅取两项:**维护模式的判定仅在此一处**,管线下游不再重复判定,
29
+ * 否则"消息在哪一层被丢弃"将成为无法回答的问题。
30
+ */
31
+ export interface DispatchPolicyView {
32
+ /** 是否忽略机器人自己发出的消息 */
33
+ readonly ignoreSelf: boolean;
34
+ /**
35
+ * 该用户当前是否应被响应
36
+ * @param uid 用户 id
37
+ * @returns 是否响应
38
+ */
39
+ canRespond(uid: string): boolean;
40
+ }
41
+ /** 分发器构造参数 */
42
+ export interface EventDispatcherOptions {
43
+ /** 日志器 */
44
+ readonly logger: Logger;
45
+ /** 策略视图 */
46
+ readonly policy: DispatchPolicyView;
47
+ /** 内核事件总线 */
48
+ readonly events: CoreEventBus;
49
+ /** 事件工厂 */
50
+ readonly factory: EventFactory;
51
+ /** 中间件管线 */
52
+ readonly middlewares: MiddlewarePipeline;
53
+ /** 命令路由 */
54
+ readonly router: CommandRouter;
55
+ /** 冷却存储 */
56
+ readonly cooldown: CooldownStore;
57
+ /** `prompt` 等待者登记表 */
58
+ readonly prompts: PromptRegistry;
59
+ /**
60
+ * 按插件名取运行时视图
61
+ *
62
+ * 由插件宿主注入。`e.render()` 要靠它定位"当前插件"的模板根,
63
+ * `e.prompt()` 要靠它拿卸载信号。分发器自己不认识 PluginHost,
64
+ * 否则 host → context → hooks → dispatch → host 就绕成环了。
65
+ * @param name 插件名
66
+ * @returns 插件运行时视图;插件已卸载时 undefined
67
+ */
68
+ readonly plugins: (name: string) => PluginRuntimeView | undefined;
69
+ /**
70
+ * 命令处理的并发上限,对应配置项 `message.concurrency`
71
+ *
72
+ * 省略或 `<= 0` 表示不限制,此时连信号量都不创建(零开销)。
73
+ * 闸门的位置有讲究,见 `#route` 的注释。
74
+ */
75
+ readonly concurrency?: number;
76
+ }
77
+ /** 事件分发器 */
78
+ export declare class EventDispatcher {
79
+ #private;
80
+ /**
81
+ * @param opts 构造参数
82
+ */
83
+ constructor(opts: EventDispatcherOptions);
84
+ /** 累计处理的事件数 */
85
+ get handled(): number;
86
+ /** 当前因并发上限而排队等待的命令数;未配置上限时恒为 0 */
87
+ get queued(): number;
88
+ /**
89
+ * 投递一条事件并处理完毕
90
+ *
91
+ * 适配器唯一的入口。**永不抛出**:任何错误都在这里落日志并转成
92
+ * `pipeline/error` 事件,适配器不需要 try/catch。
93
+ * @param incoming 适配器产出的事件
94
+ * @param bot 接收该事件的 Bot
95
+ */
96
+ submit(incoming: IncomingEvent, bot: BotApi): Promise<void>;
97
+ }
98
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/pipeline/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAEV,MAAM,EAEN,aAAa,EAGb,MAAM,EAIP,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAKvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAuB,MAAM,YAAY,CAAA;AACtF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,aAAa,CAAA;AAEjE;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,qBAAqB;IACrB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;IAC5B;;;;OAIG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACjC;AAED,cAAc;AACd,MAAM,WAAW,sBAAsB;IACrC,UAAU;IACV,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,WAAW;IACX,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;IACnC,aAAa;IACb,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,WAAW;IACX,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAC9B,YAAY;IACZ,QAAQ,CAAC,WAAW,EAAE,kBAAkB,CAAA;IACxC,WAAW;IACX,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;IAC9B,WAAW;IACX,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAA;IAChC,sBAAsB;IACtB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,iBAAiB,GAAG,SAAS,CAAA;IACjE;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAWD,YAAY;AACZ,qBAAa,eAAe;;IA6B1B;;OAEG;gBACS,IAAI,EAAE,sBAAsB;IAkBxC,eAAe;IACf,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,kCAAkC;IAClC,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED;;;;;;;OAOG;IACG,MAAM,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAqPlE"}
@@ -0,0 +1,323 @@
1
+ import { describeMessage } from "../message/segment.js";
2
+ import { parseDuration } from "../util/duration.js";
3
+ import { Semaphore } from "../util/queue.js";
4
+ import { cooldownKey, renderCooldownTip } from "./cooldown.js";
5
+ /**
6
+ * 取错误的可读描述
7
+ * @param err 任意抛出物
8
+ * @returns 描述文本
9
+ */
10
+ function errText(err) {
11
+ return err instanceof Error ? (err.stack ?? err.message) : String(err);
12
+ }
13
+ /** 事件分发器 */
14
+ export class EventDispatcher {
15
+ /** 日志器 */
16
+ #logger;
17
+ /** 策略视图 */
18
+ #policy;
19
+ /** 事件总线 */
20
+ #events;
21
+ /** 事件工厂 */
22
+ #factory;
23
+ /** 中间件管线 */
24
+ #middlewares;
25
+ /** 命令路由 */
26
+ #router;
27
+ /** 冷却存储 */
28
+ #cooldown;
29
+ /** 等待者登记表 */
30
+ #prompts;
31
+ /** 插件运行时视图查询 */
32
+ #plugins;
33
+ /** 命令执行闸门;未配置并发上限时 undefined */
34
+ #gate;
35
+ /** 闸门积压到多少条开始告警 */
36
+ #backlogWarnAt;
37
+ /** 累计处理的事件数,用于 WebUI 的运行状态 */
38
+ #handled = 0;
39
+ /** 已就积压告警过的次数,用于降频 */
40
+ #backlogWarns = 0;
41
+ /**
42
+ * @param opts 构造参数
43
+ */
44
+ constructor(opts) {
45
+ this.#logger = opts.logger;
46
+ this.#policy = opts.policy;
47
+ this.#events = opts.events;
48
+ this.#factory = opts.factory;
49
+ this.#middlewares = opts.middlewares;
50
+ this.#router = opts.router;
51
+ this.#cooldown = opts.cooldown;
52
+ this.#prompts = opts.prompts;
53
+ this.#plugins = opts.plugins;
54
+ const limit = opts.concurrency ?? 0;
55
+ this.#gate = limit > 0 ? new Semaphore(limit) : undefined;
56
+ // 阈值取"上限的十倍":并发 1 的时候 10 条积压就值得说一句,
57
+ // 并发 16 的时候 10 条只是正常波峰。下限 50 免得小上限下刷屏
58
+ this.#backlogWarnAt = Math.max(limit * 10, 50);
59
+ }
60
+ /** 累计处理的事件数 */
61
+ get handled() {
62
+ return this.#handled;
63
+ }
64
+ /** 当前因并发上限而排队等待的命令数;未配置上限时恒为 0 */
65
+ get queued() {
66
+ return this.#gate?.pending ?? 0;
67
+ }
68
+ /**
69
+ * 投递一条事件并处理完毕
70
+ *
71
+ * 适配器唯一的入口。**永不抛出**:任何错误都在这里落日志并转成
72
+ * `pipeline/error` 事件,适配器不需要 try/catch。
73
+ * @param incoming 适配器产出的事件
74
+ * @param bot 接收该事件的 Bot
75
+ */
76
+ async submit(incoming, bot) {
77
+ let event;
78
+ try {
79
+ if (incoming.kind === "message") {
80
+ if (!this.#acceptMessage(incoming, bot))
81
+ return;
82
+ const e = this.#factory.createMessage(incoming, bot);
83
+ event = e;
84
+ this.#handled++;
85
+ await this.#processMessage(e);
86
+ return;
87
+ }
88
+ // 请求事件同样受维护模式约束:同意加群也是一种"对普通用户的响应"。
89
+ // 通知与元事件不受约束 —— "有人退群就清理他的数据"这类插件在维护模式下
90
+ // 也必须继续工作,否则维护结束后数据就对不上了。
91
+ if (incoming.kind === "request" && !this.#acceptRequest(incoming))
92
+ return;
93
+ const other = this.#factory.create(incoming, bot);
94
+ event = other;
95
+ this.#handled++;
96
+ await this.#processOther(other);
97
+ }
98
+ catch (err) {
99
+ if (event === undefined) {
100
+ // 连事件对象都没构造出来,说明适配器交上来的报文不合规
101
+ this.#logger.error(`构造事件失败(适配器 ${bot.adapterId}):${errText(err)}`);
102
+ return;
103
+ }
104
+ this.#logger.error(`处理事件 ${event.id} 时抛出未捕获错误:${errText(err)}`);
105
+ // 用 detached:错误处理插件自己再抛错也不该影响这条调用链
106
+ this.#events.emitDetached("pipeline/error", event, err);
107
+ }
108
+ }
109
+ /**
110
+ * 消息事件的准入判定
111
+ * @param incoming 适配器产出的消息事件
112
+ * @param bot 接收该事件的 Bot
113
+ * @returns 是否继续处理
114
+ */
115
+ #acceptMessage(incoming, bot) {
116
+ if (this.#policy.ignoreSelf && incoming.sender.uid === bot.selfId)
117
+ return false;
118
+ if (!this.#policy.canRespond(incoming.sender.uid)) {
119
+ if (this.#logger.isLevelEnabled("trace")) {
120
+ this.#logger.trace(`维护模式:忽略 ${incoming.sender.uid} 的消息`);
121
+ }
122
+ return false;
123
+ }
124
+ return true;
125
+ }
126
+ /**
127
+ * 请求事件的准入判定
128
+ * @param incoming 适配器产出的请求事件
129
+ * @returns 是否继续处理
130
+ */
131
+ #acceptRequest(incoming) {
132
+ return this.#policy.canRespond(incoming.uid);
133
+ }
134
+ /**
135
+ * 处理消息事件
136
+ * @param e 运行时消息事件
137
+ */
138
+ async #processMessage(e) {
139
+ this.#logIncoming(e);
140
+ // core 里先路由再广播:`message` 总线事件的契约是"命令路由之后触发",
141
+ // 统计类插件因此能看到 e.command 是否有值
142
+ await this.#middlewares.run(e, async () => {
143
+ await this.#route(e);
144
+ await this.#events.emit("message", e);
145
+ });
146
+ }
147
+ /**
148
+ * 处理通知 / 请求 / 元事件
149
+ *
150
+ * 这三类不存在"核心处理"—— 内核不代替插件决定是否同意入群请求。广播本身即为核心,
151
+ * 因此置于 `run` 的 core 位置:中间件调用 `stop()` 后自然不会广播,
152
+ * 无须在外层再判定一次 `stopped`。
153
+ * @param event 运行时事件
154
+ */
155
+ async #processOther(event) {
156
+ await this.#middlewares.run(event, async () => {
157
+ switch (event.kind) {
158
+ case "notice":
159
+ await this.#events.emit("notice", event);
160
+ return;
161
+ case "request":
162
+ await this.#events.emit("request", event);
163
+ return;
164
+ case "meta":
165
+ await this.#events.emit("meta", event);
166
+ return;
167
+ }
168
+ });
169
+ }
170
+ /**
171
+ * 等待者投递 + 命令路由
172
+ *
173
+ * 并发闸门刻意开在**这里**而不是 `submit()` 入口,原因是 `e.prompt()`:
174
+ * 一个正在等用户回话的命令会一直占着许可,而"能结束这次等待"的下一条消息
175
+ * 必须先取得许可方能被处理 —— 闸门置于入口即构成死锁。等待者投递不需要
176
+ * 许可(它仅是唤醒一个已在执行中的处理函数),因此在 `offer()` 之后再取许可
177
+ * 既拦住了昂贵的命令处理,又不会将交互式流程锁死。
178
+ *
179
+ * 代价是中间件与 `message` 总线事件不受限制。它们本应是轻量的,
180
+ * 真正可能同时开启大量浏览器页面的是命令处理函数。
181
+ * @param e 运行时消息事件
182
+ */
183
+ async #route(e) {
184
+ // 命中等待者即消费掉这条消息,不再进路由
185
+ if (this.#prompts.offer(e))
186
+ return;
187
+ const candidates = this.#router.match(e);
188
+ if (candidates.length === 0)
189
+ return;
190
+ const gate = this.#gate;
191
+ if (gate === undefined)
192
+ return this.#invokeAll(e, candidates);
193
+ // 不丢弃积压,只告警:事件对象在这一步之前就已经构造出来了,
194
+ // 丢掉它省不下多少内存,却会让用户的命令无声消失
195
+ if (gate.pending >= this.#backlogWarnAt) {
196
+ this.#backlogWarns++;
197
+ if (this.#backlogWarns === 1 || this.#backlogWarns % 100 === 0) {
198
+ this.#logger.warn(`命令处理积压 ${gate.pending} 条(并发上限 ${gate.active} 已满)。` +
199
+ `若非突发流量,请调大配置项 message.concurrency 或排查耗时过长的命令`);
200
+ }
201
+ }
202
+ return gate.use(() => this.#invokeAll(e, candidates));
203
+ }
204
+ /**
205
+ * 依次尝试候选命令,直到有一条命中并阻断
206
+ * @param e 运行时消息事件
207
+ * @param candidates 候选命令
208
+ */
209
+ async #invokeAll(e, candidates) {
210
+ for (const candidate of candidates) {
211
+ if (e.stopped)
212
+ break;
213
+ const handled = await this.#invoke(e, candidate.reg, candidate.match);
214
+ if (!handled)
215
+ continue;
216
+ // 缺省阻断:一条消息通常只该有一个功能响应。声明 block: false 的
217
+ // 命令(如日志、统计类)才会让后面的命令继续匹配。
218
+ if (candidate.reg.options.block !== false)
219
+ break;
220
+ }
221
+ }
222
+ /**
223
+ * 执行一条命令(含冷却计费)
224
+ * @param e 运行时消息事件
225
+ * @param reg 命令登记
226
+ * @param match 匹配结果
227
+ * @returns 是否算作"已处理"
228
+ */
229
+ async #invoke(e, reg, match) {
230
+ const handler = reg.handler;
231
+ if (handler === undefined)
232
+ return false;
233
+ // 冷却判定放在这里而不是路由里:路由是纯查询,WebUI 要能反复调用它做预览,
234
+ // 而 hit() 会真的占用配额。只有确定要执行才计费。
235
+ const ms = parseDuration(reg.options.cooldown, 0);
236
+ let cdKey;
237
+ if (ms > 0 && !e.isMaster) {
238
+ cdKey = cooldownKey(reg.plugin, match.name, reg.options.cooldownScope ?? "user", e);
239
+ const decision = this.#cooldown.hit(cdKey, ms);
240
+ if (!decision.ok) {
241
+ const tip = reg.options.cooldownTip;
242
+ if (tip !== undefined && tip !== "")
243
+ await e.reply(renderCooldownTip(tip, decision.remaining));
244
+ // 冷却中也算"已处理":继续往下匹配的话,用户会看到"刷太快反而
245
+ // 触发了另一个功能",比什么都不回更费解
246
+ return true;
247
+ }
248
+ }
249
+ e.command = match;
250
+ e.bind(this.#plugins(reg.plugin));
251
+ const started = Date.now();
252
+ // 命中即记:这行与「处理完毕」成对出现,缺了它则一条命令耗时很长时,
253
+ // 使用者只看到消息进来了而不知道正在跑哪个命令
254
+ this.#logger.info(`命令命中 ${reg.plugin}:${match.name}`);
255
+ try {
256
+ const result = await handler(e);
257
+ if (result === false) {
258
+ // 插件显式表示"我不处理这条",把刚占用的 CD 退回去。
259
+ // 不发 command/done:这条命令并未处理该消息,计入调用统计会虚高
260
+ if (cdKey !== undefined)
261
+ this.#cooldown.reset(cdKey);
262
+ return false;
263
+ }
264
+ const cost = Date.now() - started;
265
+ // 级别由 debug 提到 info:默认级别是 info,包在 debug 里等于这行永不出现,
266
+ // 使用者看到的是「消息进来了,然后没有下文」
267
+ this.#logger.info(`命令 ${reg.plugin}:${match.name} 处理完毕,耗时 ${cost}ms`);
268
+ this.#events.emitDetached("command/done", e, { plugin: reg.plugin, command: match.name, cost, ok: true });
269
+ return true;
270
+ }
271
+ catch (err) {
272
+ // 执行失败不该让用户为此等一个完整的 CD
273
+ if (cdKey !== undefined)
274
+ this.#cooldown.reset(cdKey);
275
+ const cost = Date.now() - started;
276
+ this.#logger.error(`插件 ${reg.plugin} 的命令 ${match.name} 执行出错(耗时 ${cost}ms):${errText(err)}`);
277
+ this.#events.emitDetached("pipeline/error", e, err);
278
+ // 失败同样发 command/done:统计要算成功率,只在成功时发则分母永远等于分子
279
+ this.#events.emitDetached("command/done", e, {
280
+ plugin: reg.plugin,
281
+ command: match.name,
282
+ cost,
283
+ ok: false,
284
+ error: err instanceof Error ? err.message : String(err)
285
+ });
286
+ // 出错也算已处理:继续匹配只会让用户收到一条不相关的回复
287
+ return true;
288
+ }
289
+ finally {
290
+ // 解绑:脱离处理函数后 e.render() 应当明确报错而不是渲染到上一个插件的
291
+ // 模板目录里去。异步回调里要渲染请用 ctx.render()。
292
+ e.bind(undefined);
293
+ }
294
+ }
295
+ /**
296
+ * 记录收到的消息
297
+ *
298
+ * **必带协议与账号。** 同一进程可同时挂多个平台的多个账号,只记
299
+ * 「[群 X] 某人: 内容」在排障时答不出「这条是哪个号收到的」——
300
+ * 而 `bot` 对象本就在手边。格式取 `platform:selfId`:平台名说明是哪个协议,
301
+ * selfId 说明是哪个号,二者都取自 `e.bot` 的 getter,不缓存。
302
+ * @param e 运行时消息事件
303
+ */
304
+ #logIncoming(e) {
305
+ if (!this.#logger.isLevelEnabled("info"))
306
+ return;
307
+ const who = `${e.sender.name ?? ""}(${e.sender.uid})`;
308
+ const body = describeMessage(e.message);
309
+ const via = `${e.bot.platform}:${e.bot.selfId}`;
310
+ switch (e.scene) {
311
+ case "group":
312
+ this.#logger.info(`[${via}][群 ${e.group?.name ?? e.group?.gid ?? "?"}] ${who}: ${body}`);
313
+ return;
314
+ case "guild":
315
+ this.#logger.info(`[${via}][频道 ${e.channel?.name ?? e.channel?.channelId ?? "?"}] ${who}: ${body}`);
316
+ return;
317
+ case "private":
318
+ this.#logger.info(`[${via}][私聊] ${who}: ${body}`);
319
+ return;
320
+ }
321
+ }
322
+ }
323
+ //# sourceMappingURL=dispatch.js.map