@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,735 @@
1
+ /**
2
+ * 模块职责:把适配器产出的 `Incoming*Event` 补全成插件可用的运行时事件
3
+ * 依赖方向:依赖类型包、message/segment、pipeline/prompt、util/id、util/duration;不依赖适配器实现
4
+ * 生命周期:每条事件一个实例,处理完即被 GC;不持有跨事件状态
5
+ * 注意事项:**适配器只负责翻译报文,派生字段全部由这里算** —— 摊到各适配器里会让 `e.atBot` 在
6
+ * 不同协议下语义不一致,插件只能两边都试。
7
+ *
8
+ * 派生字段(`atMe` / `atUsers` / `images` / `text`)用 getter + 私有缓存:中间件改写
9
+ * `message` 后调一次 `refresh()` 重算,而不是每次读取都重新遍历 —— 路由匹配阶段 `e.text`
10
+ * 会被读几十次。
11
+ *
12
+ * 实例**刻意不 freeze、不 seal**:`EventExtensions` 的正规用法就是插件在事件上挂自有字段。
13
+ *
14
+ * `e.render()` 依赖「当前执行的是哪个插件」(模板根随插件而定),故该绑定由 dispatch 在调
15
+ * 每个 handler 之前经 `bind()` 替换,而不写进事件的构造参数 —— 一条消息会依次流经多个插件。
16
+ */
17
+ import type {
18
+ BotApi,
19
+ ChannelInfo,
20
+ CommandMatch,
21
+ GroupInfo,
22
+ GroupRole,
23
+ ImageSegment,
24
+ IncomingEvent,
25
+ IncomingMessageEvent,
26
+ IncomingNoticeEvent,
27
+ IncomingMetaEvent,
28
+ IncomingRequestEvent,
29
+ Logger,
30
+ MemberInfo,
31
+ MessageContent,
32
+ MessageEvent,
33
+ MessageScene,
34
+ MetaEvent,
35
+ NoticeEvent,
36
+ PromptOptions,
37
+ QuoteInfo,
38
+ RenderablePage,
39
+ RenderOptions,
40
+ RenderedImage,
41
+ ReplyOptions,
42
+ RequestEvent,
43
+ Segment,
44
+ SendOptions,
45
+ SendResult,
46
+ SendTarget,
47
+ UserInfo
48
+ } from "@yunzai-ng/types"
49
+ import { atUsersOf, hasAtAll, imagesOf, seg, textOf, toSegments } from "../message/segment.js"
50
+ import { createEventId } from "../util/id.js"
51
+ import { parseDuration } from "../util/duration.js"
52
+ import { DEFAULT_PROMPT_TIMEOUT, PromptRegistry, sessionKeyOf } from "./prompt.js"
53
+
54
+ /**
55
+ * 昵称后面允许被一并剥掉的呼唤语气字符
56
+ *
57
+ * 刻意**不含** `.` `!` `~` `/` `#` —— 这些在各家插件里都是真实的命令前缀,
58
+ * 剥掉会把 `云崽 .help` 变成 `help` 从而匹配不上。这里只剥"叫人"用的标点。
59
+ */
60
+ const NICKNAME_TAIL_RE = /^[\s,,、::]+/
61
+
62
+ /**
63
+ * 事件工厂需要的策略视图
64
+ *
65
+ * 只取用得到的两项而不是直接依赖 `KernelPolicy`:事件构造与"主人是谁"之间
66
+ * 只有这么一点耦合,写成最小接口后单测里给个字面量就能跑。
67
+ */
68
+ export interface EventPolicyView {
69
+ /** 机器人昵称列表,用于把"云崽 #体力"识别为对我说话 */
70
+ readonly nicknames: readonly string[]
71
+ /**
72
+ * 判断是否主人
73
+ * @param uid 用户 id
74
+ * @returns 是否主人
75
+ */
76
+ isMaster(uid: string): boolean
77
+ }
78
+
79
+ /**
80
+ * 当前正在执行的插件在事件上的投影
81
+ *
82
+ * `PluginContext` 结构上天然满足它,因此 dispatch 直接把上下文传进来即可,
83
+ * 不需要额外包一层。
84
+ */
85
+ export interface PluginRuntimeView {
86
+ /** 插件名 */
87
+ readonly name: string
88
+ /** 插件卸载信号 */
89
+ readonly signal: AbortSignal
90
+ /**
91
+ * 渲染 TSX 页面(模板根已绑定到该插件)
92
+ * @param page 已渲染好的页面
93
+ * @param opts 渲染选项
94
+ * @returns 图片段
95
+ */
96
+ render(page: RenderablePage, opts?: RenderOptions): Promise<RenderedImage>
97
+
98
+ /**
99
+ * 渲染字符串模板(模板根已绑定到该插件)
100
+ * @param template 模板路径
101
+ * @param data 模板数据
102
+ * @param opts 渲染选项
103
+ * @returns 图片段
104
+ */
105
+ render(template: string, data?: Record<string, unknown>, opts?: RenderOptions): Promise<RenderedImage>
106
+ }
107
+
108
+ /** 内核内部持有的消息事件:比插件看到的多一个绑定入口 */
109
+ export interface RuntimeMessageEvent extends MessageEvent {
110
+ /**
111
+ * 绑定当前正在执行的插件
112
+ *
113
+ * 只影响 `render()`/`renderReply()`/`prompt()` 的归属。dispatch 在每个
114
+ * handler 前后成对调用;传 `undefined` 表示"当前不在任何插件里"。
115
+ * @param view 插件运行时视图
116
+ */
117
+ bind(view: PluginRuntimeView | undefined): void
118
+ }
119
+
120
+ /** 事件工厂构造参数 */
121
+ export interface EventFactoryOptions {
122
+ /** 日志器 */
123
+ readonly logger: Logger
124
+ /** 策略视图 */
125
+ readonly policy: EventPolicyView
126
+ /** `prompt` 等待者登记表 */
127
+ readonly prompts: PromptRegistry
128
+ }
129
+
130
+ /**
131
+ * 取错误的可读描述
132
+ * @param err 任意抛出物
133
+ * @returns 描述文本
134
+ */
135
+ function errText(err: unknown): string {
136
+ return err instanceof Error ? err.message : String(err)
137
+ }
138
+
139
+ /**
140
+ * 推断通知事件应该回到哪里
141
+ *
142
+ * 有群号回群、否则回给相关用户。两者都没有(如纯连接类通知)返回 undefined,
143
+ * 由 `reply()` 抛错 —— 静默丢弃会让插件作者以为消息发出去了。
144
+ * @param n 通知事件
145
+ * @returns 发送目标;无法推断时 undefined
146
+ */
147
+ function noticeTargetOf(n: IncomingNoticeEvent): SendTarget | undefined {
148
+ if (n.gid !== undefined) return { scene: "group", gid: n.gid }
149
+ if (n.uid !== undefined) return { scene: "private", uid: n.uid }
150
+ return undefined
151
+ }
152
+
153
+ /** 运行时消息事件实现 */
154
+ class MessageEventImpl implements RuntimeMessageEvent {
155
+ /** 事件大类 */
156
+ readonly kind = "message" as const
157
+
158
+ /** 事件唯一 id */
159
+ readonly id: string
160
+ /** 发生时间(毫秒) */
161
+ readonly time: number
162
+ /** 平台标识 */
163
+ readonly platform: string
164
+ /** 接收该事件的账号 id */
165
+ readonly selfId: string
166
+ /** 接收该事件的 Bot */
167
+ readonly bot: BotApi
168
+ /** 平台原始报文 */
169
+ readonly raw: unknown
170
+ /** 本次事件的临时状态袋 */
171
+ readonly state: Record<string, unknown> = {}
172
+
173
+ /** 场景 */
174
+ readonly scene: MessageScene
175
+ /** 平台细分类型 */
176
+ readonly subType: string
177
+ /** 平台消息 id */
178
+ readonly messageId: string
179
+ /** 会话内序号 */
180
+ readonly seq?: number
181
+
182
+ /** 消息段(可改写,改后需 `refresh()`) */
183
+ message: Segment[]
184
+ /** 纯文本视图 */
185
+ text = ""
186
+
187
+ /** 发送者 */
188
+ readonly sender: UserInfo | MemberInfo
189
+ /** 所在群 */
190
+ readonly group?: GroupInfo
191
+ /** 所在子频道 */
192
+ readonly channel?: ChannelInfo
193
+ /** 引用的消息 */
194
+ readonly quote?: QuoteInfo
195
+
196
+ /** 命令匹配结果 */
197
+ command?: CommandMatch
198
+
199
+ /** 日志器 */
200
+ readonly #logger: Logger
201
+ /** 策略视图 */
202
+ readonly #policy: EventPolicyView
203
+ /** 等待者登记表 */
204
+ readonly #prompts: PromptRegistry
205
+
206
+ /** 回复目标(构造时算一次,场景与群号都是只读的) */
207
+ readonly #target: SendTarget
208
+ /** 发送者在群里的角色 */
209
+ readonly #role: GroupRole | undefined
210
+ /** 是否主人 */
211
+ readonly #isMaster: boolean
212
+
213
+ /** 是否 @ 了自己 */
214
+ #atMe = false
215
+ /** 被 @ 的用户 */
216
+ #atUsers: readonly string[] = []
217
+ /** 图片段(含引用消息里的) */
218
+ #images: readonly ImageSegment[] = []
219
+ /** 是否已被阻断 */
220
+ #stopped = false
221
+ /** 当前执行中的插件 */
222
+ #binding: PluginRuntimeView | undefined
223
+
224
+ /**
225
+ * @param incoming 适配器产出的消息事件
226
+ * @param bot 接收该事件的 Bot
227
+ * @param opts 工厂依赖
228
+ */
229
+ constructor(incoming: IncomingMessageEvent, bot: BotApi, opts: EventFactoryOptions) {
230
+ this.#logger = opts.logger
231
+ this.#policy = opts.policy
232
+ this.#prompts = opts.prompts
233
+
234
+ this.id = incoming.id ?? createEventId()
235
+ this.time = incoming.time ?? Date.now()
236
+ this.platform = bot.platform
237
+ this.selfId = bot.selfId
238
+ this.bot = bot
239
+ this.raw = incoming.raw
240
+
241
+ this.scene = incoming.scene
242
+ this.subType = incoming.subType ?? incoming.scene
243
+ this.messageId = incoming.messageId
244
+ this.seq = incoming.seq
245
+ // 不复制数组:适配器交出来就不该再持有它,复制只是白白多一次分配。
246
+ // 中间件改写 message 是被允许的,改的就是这一份。
247
+ this.message = incoming.message
248
+ this.sender = incoming.sender
249
+ this.group = incoming.group
250
+ this.channel = incoming.channel
251
+ this.quote = incoming.quote
252
+
253
+ this.#role = "role" in incoming.sender ? incoming.sender.role : undefined
254
+ this.#isMaster = opts.policy.isMaster(incoming.sender.uid)
255
+ this.#target = this.#computeTarget()
256
+ this.refresh()
257
+ }
258
+
259
+ /** 是否 @ 了自己(含 @全体、含昵称呼唤) */
260
+ get atMe(): boolean {
261
+ return this.#atMe
262
+ }
263
+
264
+ /** 被 @ 的用户 id 列表 */
265
+ get atUsers(): readonly string[] {
266
+ return this.#atUsers
267
+ }
268
+
269
+ /** 消息中的图片段(含引用消息里的图) */
270
+ get images(): readonly ImageSegment[] {
271
+ return this.#images
272
+ }
273
+
274
+ /** 发送者是否主人 */
275
+ get isMaster(): boolean {
276
+ return this.#isMaster
277
+ }
278
+
279
+ /** 发送者是否群管理(群主也算) */
280
+ get isGroupAdmin(): boolean {
281
+ return this.#role === "owner" || this.#role === "admin"
282
+ }
283
+
284
+ /** 发送者是否群主 */
285
+ get isGroupOwner(): boolean {
286
+ return this.#role === "owner"
287
+ }
288
+
289
+ /** 是否私聊 */
290
+ get isPrivate(): boolean {
291
+ return this.scene === "private"
292
+ }
293
+
294
+ /** 是否群聊(频道不算,频道判断用 `scene === "guild"`) */
295
+ get isGroup(): boolean {
296
+ return this.scene === "group"
297
+ }
298
+
299
+ /** 回复该消息时应发往的目标 */
300
+ get target(): SendTarget {
301
+ return this.#target
302
+ }
303
+
304
+ /** 是否已被阻断 */
305
+ get stopped(): boolean {
306
+ return this.#stopped
307
+ }
308
+
309
+ /** 阻断后续中间件与命令匹配 */
310
+ stop(): void {
311
+ this.#stopped = true
312
+ }
313
+
314
+ /**
315
+ * 绑定当前正在执行的插件
316
+ * @param view 插件运行时视图
317
+ */
318
+ bind(view: PluginRuntimeView | undefined): void {
319
+ this.#binding = view
320
+ }
321
+
322
+ /** 改写 `message` 后重算派生字段 */
323
+ refresh(): void {
324
+ const message = this.message
325
+ const atUsers = atUsersOf(message)
326
+ const images = imagesOf(message)
327
+
328
+ // 引用消息里的图也算进来:这是"识图"类指令唯一的数据来源。
329
+ // 放在自身图片之后 —— 用户自己发的图优先级更高。
330
+ const quoted = this.quote?.message
331
+ if (quoted !== undefined && quoted.length > 0) images.push(...imagesOf(quoted))
332
+
333
+ let atMe = hasAtAll(message)
334
+ if (!atMe) {
335
+ for (const uid of atUsers) {
336
+ if (uid === this.selfId) {
337
+ atMe = true
338
+ break
339
+ }
340
+ }
341
+ }
342
+
343
+ let text = textOf(message)
344
+ const stripped = this.#stripNickname(text)
345
+ if (stripped !== undefined) {
346
+ // 昵称已经从 text 里剥掉:这样 `云崽 #体力` 与 `#体力` 走同一条路由,
347
+ // 插件不必自己处理昵称前缀
348
+ text = stripped
349
+ atMe = true
350
+ }
351
+
352
+ this.text = text
353
+ this.#atUsers = atUsers
354
+ this.#images = images
355
+ this.#atMe = atMe
356
+ }
357
+
358
+ /**
359
+ * 回复
360
+ * @param content 消息内容
361
+ * @param opts 回复选项
362
+ * @returns 发送结果
363
+ */
364
+ async reply(content: MessageContent, opts: ReplyOptions = {}): Promise<SendResult> {
365
+ const segments = toSegments(content)
366
+ if (segments.length === 0) {
367
+ // 空内容送到平台上通常是一个含义不明的 API 报错。这里拦下并指名道姓,
368
+ // 因为源头几乎总是插件里某个分支忘了赋值。
369
+ this.#logger.warn(`插件 ${this.#binding?.name ?? "未知"} 回复了空内容,已忽略(事件 ${this.id})`)
370
+ return { ok: false, messageId: "", time: Date.now() }
371
+ }
372
+
373
+ const payload: Segment[] =
374
+ opts.at === true && this.isGroup ? [seg.at(this.sender.uid, this.sender.name), ...segments] : segments
375
+
376
+ const send: SendOptions = {}
377
+ if (opts.quote === true) send.quote = this.messageId
378
+ else if (typeof opts.quote === "string") send.quote = opts.quote
379
+ if (opts.recallAfter !== undefined) send.recallAfter = opts.recallAfter
380
+ if (opts.autoForward !== undefined) send.autoForward = opts.autoForward
381
+ if (opts.splitLength !== undefined) send.splitLength = opts.splitLength
382
+
383
+ return this.bot.sendMessage(this.#target, payload, send)
384
+ }
385
+
386
+ /**
387
+ * 渲染 TSX 页面为图片段(不发送)
388
+ * @param page 已渲染好的页面
389
+ * @param opts 渲染选项
390
+ * @returns 图片段
391
+ */
392
+ async render(page: RenderablePage, opts?: RenderOptions): Promise<RenderedImage>
393
+ /**
394
+ * 渲染字符串模板为图片段(不发送)
395
+ * @param template 模板路径,相对本插件模板根
396
+ * @param data 模板数据
397
+ * @param opts 渲染选项
398
+ * @returns 图片段
399
+ */
400
+ async render(template: string, data?: Record<string, unknown>, opts?: RenderOptions): Promise<RenderedImage>
401
+ /**
402
+ * 渲染实现
403
+ * @param first 页面或模板路径
404
+ * @param second TSX 通路为渲染选项,字符串通路为模板数据
405
+ * @param third 字符串通路的渲染选项
406
+ * @returns 图片段
407
+ * @throws 不在插件执行上下文里调用时
408
+ */
409
+ async render(
410
+ first: RenderablePage | string,
411
+ second?: Record<string, unknown> | RenderOptions,
412
+ third?: RenderOptions
413
+ ): Promise<RenderedImage> {
414
+ const binding = this.#binding
415
+ if (binding === undefined) {
416
+ throw new Error(
417
+ "e.render() 只能在插件的命令处理函数或中间件里调用:内核要靠「当前插件」定位模板根目录。" +
418
+ "若要在 setTimeout 等脱离上下文的回调里渲染,请改用 ctx.render()"
419
+ )
420
+ }
421
+ // 两条通路在此汇合后原样转交给上下文,由其统一判别形态 —— 此处再判一次
422
+ // 只会让"形态如何判定"散落在两个文件里
423
+ if (typeof first === "string") return binding.render(first, second as Record<string, unknown> | undefined, third)
424
+ return binding.render(first, second as RenderOptions | undefined)
425
+ }
426
+
427
+ /**
428
+ * 渲染 TSX 页面并直接回复
429
+ * @param page 已渲染好的页面
430
+ * @param opts 渲染与回复选项
431
+ * @returns 发送结果
432
+ */
433
+ async renderReply(page: RenderablePage, opts?: RenderOptions & ReplyOptions): Promise<SendResult>
434
+ /**
435
+ * 渲染字符串模板并直接回复
436
+ * @param template 模板路径
437
+ * @param data 模板数据
438
+ * @param opts 渲染与回复选项
439
+ * @returns 发送结果
440
+ */
441
+ async renderReply(
442
+ template: string,
443
+ data?: Record<string, unknown>,
444
+ opts?: RenderOptions & ReplyOptions
445
+ ): Promise<SendResult>
446
+ /**
447
+ * 渲染并回复的实现
448
+ * @param first 页面或模板路径
449
+ * @param second TSX 通路为选项,字符串通路为模板数据
450
+ * @param third 字符串通路的选项
451
+ * @returns 发送结果
452
+ */
453
+ async renderReply(
454
+ first: RenderablePage | string,
455
+ second?: Record<string, unknown> | (RenderOptions & ReplyOptions),
456
+ third?: RenderOptions & ReplyOptions
457
+ ): Promise<SendResult> {
458
+ // 选项在两条通路里位置不同:TSX 通路是第二参,字符串通路是第三参。
459
+ // reply 也要用到同一份选项(quote / recallAfter 等),故先归一
460
+ const opts = typeof first === "string" ? third : (second as (RenderOptions & ReplyOptions) | undefined)
461
+ const image =
462
+ typeof first === "string"
463
+ ? await this.render(first, second as Record<string, unknown> | undefined, third)
464
+ : await this.render(first, second as (RenderOptions & ReplyOptions) | undefined)
465
+ return this.reply(image, opts)
466
+ }
467
+
468
+ /**
469
+ * 撤回本条消息
470
+ * @returns 是否成功
471
+ */
472
+ async recall(): Promise<boolean> {
473
+ if (!this.bot.caps.has("recall")) return false
474
+ try {
475
+ return await this.bot.recallMessage(this.messageId)
476
+ } catch (err) {
477
+ // 撤回失败是家常便饭(超过 2 分钟、没有管理权限),不值得往上抛
478
+ this.#logger.debug(`撤回消息 ${this.messageId} 失败:${errText(err)}`)
479
+ return false
480
+ }
481
+ }
482
+
483
+ /**
484
+ * 等待同一会话的下一条消息
485
+ * @param opts 等待选项
486
+ * @returns 下一条消息事件;超时或插件卸载时 undefined
487
+ */
488
+ async prompt(opts: PromptOptions = {}): Promise<MessageEvent | undefined> {
489
+ if (opts.tip !== undefined) await this.reply(opts.tip)
490
+ const binding = this.#binding
491
+ return this.#prompts.wait({
492
+ key: sessionKeyOf(this),
493
+ uid: this.sender.uid,
494
+ sameUser: opts.sameUser !== false,
495
+ timeout: parseDuration(opts.timeout, DEFAULT_PROMPT_TIMEOUT),
496
+ filter: opts.filter,
497
+ plugin: binding?.name ?? "未知",
498
+ signal: binding?.signal
499
+ })
500
+ }
501
+
502
+ /**
503
+ * 剥除开头的机器人昵称
504
+ *
505
+ * 取**最长**匹配而非遍历顺序上的第一个:昵称配置为 `["云", "云崽"]` 时,
506
+ * 先命中 `云` 会将 `云崽帮我看看` 剥为 `崽帮我看看`。一次遍历即可求得最长者,
507
+ * 不必为此排序(昵称列表可能在 WebUI 中随时修改,排序结果无法长期缓存)。
508
+ * @param text 当前纯文本
509
+ * @returns 剥除昵称后的文本;未命中昵称时 undefined
510
+ */
511
+ #stripNickname(text: string): string | undefined {
512
+ if (text === "") return undefined
513
+ let best = ""
514
+ for (const nick of this.#policy.nicknames) {
515
+ if (nick === "" || nick.length <= best.length) continue
516
+ if (text.startsWith(nick)) best = nick
517
+ }
518
+ if (best === "") return undefined
519
+ return text.slice(best.length).replace(NICKNAME_TAIL_RE, "")
520
+ }
521
+
522
+ /**
523
+ * 算出回复目标
524
+ * @returns 发送目标
525
+ */
526
+ #computeTarget(): SendTarget {
527
+ switch (this.scene) {
528
+ case "group": {
529
+ const gid = this.group?.gid
530
+ if (gid !== undefined) return { scene: "group", gid }
531
+ break
532
+ }
533
+ case "guild": {
534
+ const ch = this.channel
535
+ if (ch !== undefined) return { scene: "guild", guildId: ch.guildId, channelId: ch.channelId }
536
+ break
537
+ }
538
+ case "private": {
539
+ const gid = this.group?.gid
540
+ // 群临时会话带上来源群号:部分平台(OneBot 的 temp 会话)非它不可发
541
+ return gid === undefined
542
+ ? { scene: "private", uid: this.sender.uid }
543
+ : { scene: "private", uid: this.sender.uid, gid }
544
+ }
545
+ }
546
+ // 适配器声明了场景却没给对应的会话标识,属于适配器 bug。
547
+ // 退化成私聊回给发送者:至少用户能看到回复,而不是静默失败。
548
+ this.#logger.error(
549
+ `事件 ${this.id} 的场景为 ${this.scene} 但缺少${this.scene === "group" ? "群号" : "频道信息"},` +
550
+ `已退化为私聊回复;请检查适配器 ${this.bot.adapterId}`
551
+ )
552
+ return { scene: "private", uid: this.sender.uid }
553
+ }
554
+ }
555
+
556
+ /**
557
+ * 运行时事件工厂
558
+ *
559
+ * 内核里唯一构造事件的地方。适配器把 `IncomingEvent` 交给 `host.submit()`,
560
+ * 由 dispatch 调这里补全成运行时形态。
561
+ */
562
+ export class EventFactory {
563
+ /** 工厂依赖 */
564
+ readonly #opts: EventFactoryOptions
565
+
566
+ /**
567
+ * @param opts 构造参数
568
+ */
569
+ constructor(opts: EventFactoryOptions) {
570
+ this.#opts = opts
571
+ }
572
+
573
+ /**
574
+ * 按事件大类分派构造
575
+ * @param incoming 适配器产出的事件
576
+ * @param bot 接收该事件的 Bot
577
+ * @returns 运行时事件
578
+ */
579
+ create(incoming: IncomingEvent, bot: BotApi): MessageEvent | NoticeEvent | RequestEvent | MetaEvent {
580
+ switch (incoming.kind) {
581
+ case "message":
582
+ return this.createMessage(incoming, bot)
583
+ case "notice":
584
+ return this.createNotice(incoming, bot)
585
+ case "request":
586
+ return this.createRequest(incoming, bot)
587
+ case "meta":
588
+ return this.createMeta(incoming, bot)
589
+ }
590
+ }
591
+
592
+ /**
593
+ * 构造运行时消息事件
594
+ * @param incoming 适配器产出的消息事件
595
+ * @param bot 接收该事件的 Bot
596
+ * @returns 运行时消息事件
597
+ */
598
+ createMessage(incoming: IncomingMessageEvent, bot: BotApi): RuntimeMessageEvent {
599
+ return new MessageEventImpl(incoming, bot, this.#opts)
600
+ }
601
+
602
+ /**
603
+ * 构造运行时通知事件
604
+ * @param incoming 适配器产出的通知事件
605
+ * @param bot 接收该事件的 Bot
606
+ * @returns 运行时通知事件
607
+ */
608
+ createNotice(incoming: IncomingNoticeEvent, bot: BotApi): NoticeEvent {
609
+ const target = noticeTargetOf(incoming)
610
+ let stopped = false
611
+ const event = {
612
+ ...incoming,
613
+ id: incoming.id ?? createEventId(),
614
+ time: incoming.time ?? Date.now(),
615
+ platform: bot.platform,
616
+ selfId: bot.selfId,
617
+ bot,
618
+ raw: incoming.raw,
619
+ state: {} as Record<string, unknown>,
620
+ stop(): void {
621
+ stopped = true
622
+ },
623
+ // 写成 getter 而不是普通字段:`stop()` 必须能被下游立刻看见
624
+ get stopped(): boolean {
625
+ return stopped
626
+ },
627
+ /**
628
+ * 就地回复
629
+ * @param content 消息内容
630
+ * @returns 发送结果
631
+ * @throws 该通知没有可回复的会话时
632
+ */
633
+ async reply(content: MessageContent): Promise<SendResult> {
634
+ if (target === undefined) {
635
+ throw new Error(`通知事件 ${incoming.noticeType} 没有可回复的会话(既无群号也无用户 id)`)
636
+ }
637
+ return bot.sendMessage(target, content)
638
+ }
639
+ }
640
+ return event as NoticeEvent
641
+ }
642
+
643
+ /**
644
+ * 构造运行时请求事件
645
+ * @param incoming 适配器产出的请求事件
646
+ * @param bot 接收该事件的 Bot
647
+ * @returns 运行时请求事件
648
+ */
649
+ createRequest(incoming: IncomingRequestEvent, bot: BotApi): RequestEvent {
650
+ let stopped = false
651
+
652
+ /**
653
+ * 落到平台的同意/拒绝
654
+ *
655
+ * 好友与加群的第三个参数含义不同:好友是"同意后设置的备注",
656
+ * 加群是"拒绝的理由"。该不对称源自 OneBot,在此处封装以免插件误用。
657
+ * @param approve 是否同意
658
+ * @param extra 备注或理由
659
+ */
660
+ const handle = async (approve: boolean, extra?: string): Promise<void> => {
661
+ if (incoming.requestType === "friend") {
662
+ if (bot.handleFriendRequest === undefined) {
663
+ throw new Error(`账号 ${bot.selfId} 的适配器未实现处理好友请求`)
664
+ }
665
+ await bot.handleFriendRequest(incoming.flag, approve, approve ? extra : undefined)
666
+ return
667
+ }
668
+ if (bot.handleGroupRequest === undefined) {
669
+ throw new Error(`账号 ${bot.selfId} 的适配器未实现处理加群请求`)
670
+ }
671
+ await bot.handleGroupRequest(incoming.flag, approve, approve ? undefined : extra)
672
+ }
673
+
674
+ const event = {
675
+ ...incoming,
676
+ id: incoming.id ?? createEventId(),
677
+ time: incoming.time ?? Date.now(),
678
+ platform: bot.platform,
679
+ selfId: bot.selfId,
680
+ bot,
681
+ raw: incoming.raw,
682
+ state: {} as Record<string, unknown>,
683
+ stop(): void {
684
+ stopped = true
685
+ },
686
+ /** 是否已被阻断 */
687
+ get stopped(): boolean {
688
+ return stopped
689
+ },
690
+ /**
691
+ * 同意
692
+ * @param extra 好友请求的备注
693
+ */
694
+ async approve(extra?: string): Promise<void> {
695
+ await handle(true, extra)
696
+ },
697
+ /**
698
+ * 拒绝
699
+ * @param reason 拒绝理由
700
+ */
701
+ async reject(reason?: string): Promise<void> {
702
+ await handle(false, reason)
703
+ }
704
+ }
705
+ return event as RequestEvent
706
+ }
707
+
708
+ /**
709
+ * 构造运行时元事件
710
+ * @param incoming 适配器产出的元事件
711
+ * @param bot 接收该事件的 Bot
712
+ * @returns 运行时元事件
713
+ */
714
+ createMeta(incoming: IncomingMetaEvent, bot: BotApi): MetaEvent {
715
+ let stopped = false
716
+ const event = {
717
+ ...incoming,
718
+ id: incoming.id ?? createEventId(),
719
+ time: incoming.time ?? Date.now(),
720
+ platform: bot.platform,
721
+ selfId: bot.selfId,
722
+ bot,
723
+ raw: incoming.raw,
724
+ state: {} as Record<string, unknown>,
725
+ stop(): void {
726
+ stopped = true
727
+ },
728
+ /** 是否已被阻断 */
729
+ get stopped(): boolean {
730
+ return stopped
731
+ }
732
+ }
733
+ return event as MetaEvent
734
+ }
735
+ }