@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,582 @@
1
+ /**
2
+ * 模块职责:Bot 门面(`BotFacade`)与 Bot 注册表(实现 `BotRegistryView`)
3
+ * 依赖方向:依赖类型包、message/*、util/*;**不认识适配器实现,也不认识账号管理器**
4
+ * 生命周期:门面随账号连接建立、随断开销毁;注册表随内核创建
5
+ * 注意事项:门面存在的理由是**把发送语义收进内核**,使适配器只需实现「把这一段消息段发出去」。
6
+ * 以下四件一律由门面负责:`splitLength` 超长切分、`sequential` 同会话串行(`KeyedQueue`,
7
+ * 键与 `e.prompt()` 共用 `targetKey`)、`sendTimeout` 发送超时、`recallAfter` /
8
+ * `autoForward`。故 `SendOptions` 四个字段里**只有 `quote` 会传到驱动**。
9
+ *
10
+ * **超时只是「不再等」,取消不了已发出的请求** —— 消息可能仍然送达,故超时后绝不自动重试,
11
+ * 否则用户会收到两条一样的回复。
12
+ *
13
+ * **只有发送类调用套 `sendTimeout`。** 查询类不套:`getGroupMemberList` 在千人群上本就要
14
+ * 十几秒,按「发一条消息」的尺度掐掉它是错的,那属于适配器自己的 HTTP 超时。
15
+ */
16
+ import type {
17
+ BotApi,
18
+ BotCapability,
19
+ BotDriver,
20
+ BotRegistryView,
21
+ ForwardNode,
22
+ GroupInfo,
23
+ Logger,
24
+ MemberInfo,
25
+ MemberListOptions,
26
+ MessageContent,
27
+ MessageSentInfo,
28
+ Segment,
29
+ SendOptions,
30
+ SendResult,
31
+ SendTarget,
32
+ UserInfo
33
+ } from "@yunzai-ng/types"
34
+ import { toSegments } from "../message/segment.js"
35
+ import { splitMessage } from "../message/split.js"
36
+ import { describeTarget, targetKey } from "../message/target.js"
37
+ import { withTimeout } from "../util/defer.js"
38
+ import { KeyedQueue } from "../util/queue.js"
39
+
40
+ /**
41
+ * `BotApi` 上的可选方法名
42
+ *
43
+ * 门面必须**存在性对齐**驱动:`pipeline/event.ts` 的 `createRequest` 即依靠
44
+ * `bot.handleGroupRequest === undefined` 判断"该平台是否支持同意加群"。
45
+ * 若门面无条件补齐这些方法(即便内部转发至一个不存在的实现),
46
+ * 该处判断将永远为真,错误亦由"能力探测失败"转变为"调用时于运行时抛错"。
47
+ */
48
+ const OPTIONAL_METHODS = [
49
+ "sendForward",
50
+ "setGroupCard",
51
+ "muteGroupMember",
52
+ "muteGroupAll",
53
+ "kickGroupMember",
54
+ "quitGroup",
55
+ "handleFriendRequest",
56
+ "handleGroupRequest",
57
+ "fetchHistory",
58
+ "getMessage",
59
+ "uploadGroupFile",
60
+ "setReaction"
61
+ ] as const satisfies readonly (keyof BotApi)[]
62
+
63
+ /** 需要套用 `sendTimeout` 的可选方法 */
64
+ const TIMED_METHODS: ReadonlySet<string> = new Set(["sendForward"])
65
+
66
+ /**
67
+ * 门面需要的发送策略视图
68
+ *
69
+ * 三个字段都在**每次发送时**读取,因此实现方可以用 getter 直连当前配置快照:
70
+ * 用户在 WebUI 里把 `splitLength` 从 3000 改成 1000,下一条消息就生效,
71
+ * 不需要重连账号。
72
+ */
73
+ export interface SendPolicyView {
74
+ /** 单条消息最大文本长度;`<= 0` 表示不切分 */
75
+ readonly splitLength: number
76
+ /** 单条消息的发送超时(毫秒) */
77
+ readonly sendTimeout: number
78
+ /** 同一会话内是否串行发送 */
79
+ readonly sequential: boolean
80
+ }
81
+
82
+ /** Bot 门面构造参数 */
83
+ export interface BotFacadeOptions {
84
+ /** 适配器创建的驱动 */
85
+ readonly driver: BotDriver
86
+ /** 账号记录 id */
87
+ readonly accountId: string
88
+ /** 日志器 */
89
+ readonly logger: Logger
90
+ /** 发送策略视图 */
91
+ readonly policy: SendPolicyView
92
+ /**
93
+ * 一条消息发出后的回调,用于触发 `message/sent` 总线事件
94
+ *
95
+ * 取回调而非直接传 `CoreEventBus`:本文件的职责边界是「把发送语义收进内核」,
96
+ * 认识事件总线就等于认识插件系统,而门面连账号管理器都不认识(见文件头)。
97
+ * 回调由账号管理器传入 —— 它本来就持有总线。
98
+ *
99
+ * 省略即不通知,`createBotFacade` 的现有调用方因此无须改动。
100
+ */
101
+ readonly onSent?: (info: MessageSentInfo) => void
102
+ }
103
+
104
+ /** Bot 门面:插件拿到的 `BotApi` 实体 */
105
+ export interface BotFacade extends BotApi {
106
+ /** 账号记录 id(`BotApi` 里没有,注册表与 WebUI 用它定位) */
107
+ readonly accountId: string
108
+ /**
109
+ * 关闭门面
110
+ *
111
+ * 此后所有调用立刻 reject,待撤回的定时器一并取消。
112
+ * **不会**调 `driver.disconnect()` —— 驱动的生死由账号管理器负责,
113
+ * 门面只管"别再让插件用这个壳子"。
114
+ */
115
+ close(): void
116
+ }
117
+
118
+ /**
119
+ * 取错误的可读描述
120
+ * @param err 任意抛出物
121
+ * @returns 描述文本
122
+ */
123
+ function errText(err: unknown): string {
124
+ return err instanceof Error ? err.message : String(err)
125
+ }
126
+
127
+ /**
128
+ * 创建一个 Bot 门面
129
+ *
130
+ * 刻意返回**对象字面量**而不是 class 实例:可选方法必须按驱动的实际实现
131
+ * 逐个决定"装不装",见 `OPTIONAL_METHODS` 的说明。class 做不到这件事 ——
132
+ * 原型上的方法总是存在。
133
+ * @param opts 构造参数
134
+ * @returns Bot 门面
135
+ */
136
+ export function createBotFacade(opts: BotFacadeOptions): BotFacade {
137
+ const { driver, accountId, logger, policy, onSent } = opts
138
+ /** 同会话串行发送队列 */
139
+ const queue = new KeyedQueue()
140
+ /** 待触发的自动撤回定时器 */
141
+ const timers = new Set<NodeJS.Timeout>()
142
+ /** 是否已关闭 */
143
+ let closed = false
144
+ /** 是否已就"平台不支持撤回"提醒过一次 */
145
+ let warnedRecall = false
146
+
147
+ /**
148
+ * 确认门面仍然可用
149
+ * @throws 门面已关闭时
150
+ */
151
+ const ensureAlive = (): void => {
152
+ if (closed) {
153
+ throw new Error(
154
+ `账号 ${accountId} 已断开,这个 Bot 句柄失效了:请在用到时重新 ctx.pickBot(),不要把 Bot 存进模块变量`
155
+ )
156
+ }
157
+ }
158
+
159
+ /**
160
+ * 安排自动撤回
161
+ * @param messageId 消息 id
162
+ * @param ms 延时毫秒
163
+ */
164
+ const scheduleRecall = (messageId: string, ms: number): void => {
165
+ if (!driver.caps.has("recall")) {
166
+ // 只提醒一次:这是插件作者的能力误判,每条消息都刷一行没有新信息
167
+ if (!warnedRecall) {
168
+ warnedRecall = true
169
+ logger.warn(`适配器 ${driver.adapterId} 不支持撤回,recallAfter 将被忽略`)
170
+ }
171
+ return
172
+ }
173
+ // 平台没返回消息 id 就无从撤回,静默跳过(不是错误,有平台确实不返回)
174
+ if (messageId === "") return
175
+
176
+ const timer = setTimeout(() => {
177
+ timers.delete(timer)
178
+ if (closed) return
179
+ void driver.recallMessage(messageId).catch((err: unknown) => {
180
+ // 撤回失败很常见(消息已过撤回时限、被管理员先撤了),不值得报错
181
+ logger.debug(`自动撤回消息 ${messageId} 失败:${errText(err)}`)
182
+ })
183
+ }, ms)
184
+ // 不使"等待撤回"阻止进程退出:停机时该消息留存于群内并无影响
185
+ timer.unref?.()
186
+ timers.add(timer)
187
+ }
188
+
189
+ /**
190
+ * 发出一条(已切分好的)消息
191
+ * @param target 发送目标
192
+ * @param chunk 消息段
193
+ * @param quote 引用的消息 id
194
+ * @returns 发送结果
195
+ */
196
+ const sendChunk = async (target: SendTarget, chunk: Segment[], quote: string | undefined): Promise<SendResult> => {
197
+ const pending = quote === undefined ? driver.sendMessage(target, chunk) : driver.sendMessage(target, chunk, { quote })
198
+ return await withTimeout(pending, policy.sendTimeout, `向${describeTarget(target)}发送消息超时`)
199
+ }
200
+
201
+ /**
202
+ * 把切分结果包成合并转发节点
203
+ * @param chunks 切分结果
204
+ * @returns 转发节点数组
205
+ */
206
+ const forwardNodes = (chunks: Segment[][]): ForwardNode[] =>
207
+ chunks.map(message => ({ uid: driver.selfId, name: driver.nickname, message }))
208
+
209
+ /**
210
+ * 完成一次发送(切分、转发、撤回都在这里)
211
+ * @param target 发送目标
212
+ * @param segments 已展平的消息段
213
+ * @param opts 发送选项
214
+ * @returns 发送结果
215
+ */
216
+ const deliver = async (
217
+ target: SendTarget,
218
+ segments: Segment[],
219
+ opts: SendOptions | undefined
220
+ ): Promise<SendResult> => {
221
+ // 再确认一次:串行队列里排在后面的任务可能等到了断开之后才轮到
222
+ ensureAlive()
223
+
224
+ const chunks = splitMessage(segments, opts?.splitLength ?? policy.splitLength)
225
+ const recallAfter = opts?.recallAfter ?? 0
226
+ const results: SendResult[] = []
227
+
228
+ // 合并转发:连发五条长文本会把屏幕刷满,折成一个卡片友好得多
229
+ const forward = driver.sendForward
230
+ if (chunks.length > 1 && opts?.autoForward === true) {
231
+ if (forward !== undefined && driver.caps.has("forward")) {
232
+ const result = await withTimeout(
233
+ forward.call(driver, target, forwardNodes(chunks)),
234
+ policy.sendTimeout,
235
+ `向${describeTarget(target)}发送合并转发超时`
236
+ )
237
+ if (recallAfter > 0) scheduleRecall(result.messageId, recallAfter)
238
+ return result
239
+ }
240
+ logger.debug(`适配器 ${driver.adapterId} 不支持合并转发,autoForward 退回逐条发送`)
241
+ }
242
+
243
+ for (const [i, chunk] of chunks.entries()) {
244
+ // 只有第一条带引用:每条都引用会让整屏都是回复箭头,
245
+ // 而用户想看到的是"这一串是对那条消息的回答"
246
+ results.push(await sendChunk(target, chunk, i === 0 ? opts?.quote : undefined))
247
+ }
248
+ if (recallAfter > 0) for (const r of results) scheduleRecall(r.messageId, recallAfter)
249
+
250
+ const first = results[0]
251
+ // splitMessage 保证至少一条,这里只是让类型收窄;真到了这一步说明切分坏了
252
+ if (first === undefined) return { ok: false, messageId: "", time: Date.now() }
253
+ if (results.length === 1) return first
254
+ // 多条时返回**第一条**的 id:插件拿它去撤回或引用,用户看到的也是从第一条开始
255
+ return {
256
+ ok: results.every(r => r.ok),
257
+ messageId: first.messageId,
258
+ time: first.time,
259
+ raw: results.map(r => r.raw)
260
+ }
261
+ }
262
+
263
+ /**
264
+ * 记一行「已发出」并通知总线
265
+ *
266
+ * 日志取 info 级:与「收到消息」成对。只记进来不记出去,则一次问答在日志里
267
+ * 只剩上半句 —— 排障时无从判断是命令没跑,还是跑完了但平台没收。
268
+ *
269
+ * 图文构成逐类计数而非只记总段数:「发了 3 段」看不出是三行字还是两张图,
270
+ * 而「图片发不出去」与「文字发不出去」是两类完全不同的故障。
271
+ * @param target 发送目标
272
+ * @param segments 已展平的消息段
273
+ * @param cost 耗时毫秒
274
+ * @param ok 平台是否接收
275
+ */
276
+ const report = (target: SendTarget, segments: Segment[], cost: number, ok: boolean): void => {
277
+ /** 各类型段的计数 */
278
+ const kinds: Record<string, number> = {}
279
+ for (const s of segments) kinds[s.type] = (kinds[s.type] ?? 0) + 1
280
+ const shape = Object.entries(kinds)
281
+ .map(([type, n]) => `${type}×${n}`)
282
+ .join(" ")
283
+ const verb = ok ? "已发出" : "发送失败"
284
+ logger.info(`[${driver.platform}:${driver.selfId}] ${verb} → ${describeTarget(target)}:${shape},耗时 ${cost}ms`)
285
+ // 回调自身抛错不该让「消息已发出」变成「发送失败」:统计插件的问题不是发送的问题
286
+ try {
287
+ onSent?.({
288
+ accountId,
289
+ adapterId: driver.adapterId,
290
+ platform: driver.platform,
291
+ target,
292
+ segments: segments.length,
293
+ kinds,
294
+ cost,
295
+ ok
296
+ })
297
+ } catch (err) {
298
+ logger.debug(`message/sent 通知回调抛错:${errText(err)}`)
299
+ }
300
+ }
301
+
302
+ /**
303
+ * 发送消息
304
+ * @param target 发送目标
305
+ * @param content 消息内容
306
+ * @param opts 发送选项
307
+ * @returns 发送结果
308
+ */
309
+ const sendMessage = async (
310
+ target: SendTarget,
311
+ content: MessageContent,
312
+ opts?: SendOptions
313
+ ): Promise<SendResult> => {
314
+ ensureAlive()
315
+ const segments = toSegments(content)
316
+ if (segments.length === 0) {
317
+ // 与 `e.reply()` 一致:空内容几乎总是拼装时全部为 undefined 的逻辑缺陷。
318
+ // 告警而非抛错 —— 因一句未能拼装出的提示语而使整个插件失败并不适当。
319
+ logger.warn(`试图向${describeTarget(target)}发送空消息,已忽略`)
320
+ return { ok: false, messageId: "", time: Date.now() }
321
+ }
322
+
323
+ /**
324
+ * 实际发送
325
+ *
326
+ * 计时起点在**队列之内**:`sequential` 为真时,一条消息可能先在队列里等上
327
+ * 数秒才轮到自己发。把等待算进「发送耗时」会使统计出的耗时随并发量起伏,
328
+ * 而那反映的是排队深度、不是平台的响应快慢。
329
+ */
330
+ const task = async (): Promise<SendResult> => {
331
+ const started = Date.now()
332
+ try {
333
+ const result = await deliver(target, segments, opts)
334
+ report(target, segments, Date.now() - started, result.ok)
335
+ return result
336
+ } catch (err) {
337
+ // 抛错也报一次:统计要算成功率,只在成功时报则分母恒等于分子。
338
+ // 报完照旧抛出 —— 调用方的错误处理不受影响
339
+ report(target, segments, Date.now() - started, false)
340
+ throw err
341
+ }
342
+ }
343
+ // 同会话串行:并发发送时平台不保证到达顺序,用户会看到图片跑到说明文字前面
344
+ return policy.sequential ? await queue.run(targetKey(target), task) : await task()
345
+ }
346
+
347
+ const facade: BotFacade = {
348
+ accountId,
349
+ // 一律用 getter 转发:`selfId` 在握手完成后才知道、`nickname` 会被用户改、
350
+ // `online` 会在重连中反复变化。快照下来就全是过期数据。
351
+ get selfId(): string {
352
+ return driver.selfId
353
+ },
354
+ get platform(): string {
355
+ return driver.platform
356
+ },
357
+ get adapterId(): string {
358
+ return driver.adapterId
359
+ },
360
+ get nickname(): string {
361
+ return driver.nickname
362
+ },
363
+ get online(): boolean {
364
+ return !closed && driver.online
365
+ },
366
+ get caps(): ReadonlySet<BotCapability> {
367
+ return driver.caps
368
+ },
369
+ sendMessage,
370
+ // 以下透传方法全部写成 async:`ensureAlive()` 若同步抛出,
371
+ // 调用方的 `.catch()` 接不住,会变成一个 unhandled 异常
372
+ recallMessage: async (messageId: string): Promise<boolean> => {
373
+ ensureAlive()
374
+ return await driver.recallMessage(messageId)
375
+ },
376
+ getSelfInfo: async (): Promise<UserInfo> => {
377
+ ensureAlive()
378
+ return await driver.getSelfInfo()
379
+ },
380
+ getFriend: async (uid: string): Promise<UserInfo | undefined> => {
381
+ ensureAlive()
382
+ return await driver.getFriend(uid)
383
+ },
384
+ getFriendList: async (): Promise<UserInfo[]> => {
385
+ ensureAlive()
386
+ return await driver.getFriendList()
387
+ },
388
+ getGroup: async (gid: string): Promise<GroupInfo | undefined> => {
389
+ ensureAlive()
390
+ return await driver.getGroup(gid)
391
+ },
392
+ getGroupList: async (): Promise<GroupInfo[]> => {
393
+ ensureAlive()
394
+ return await driver.getGroupList()
395
+ },
396
+ getGroupMember: async (gid: string, uid: string): Promise<MemberInfo | undefined> => {
397
+ ensureAlive()
398
+ return await driver.getGroupMember(gid, uid)
399
+ },
400
+ getGroupMemberList: async (gid: string, listOpts?: MemberListOptions): Promise<MemberInfo[]> => {
401
+ ensureAlive()
402
+ return await driver.getGroupMemberList(gid, listOpts)
403
+ },
404
+ callApi: async <T = unknown>(action: string, params?: Record<string, unknown>): Promise<T> => {
405
+ ensureAlive()
406
+ return await driver.callApi<T>(action, params)
407
+ },
408
+ close: (): void => {
409
+ if (closed) return
410
+ closed = true
411
+ for (const timer of timers) clearTimeout(timer)
412
+ timers.clear()
413
+ }
414
+ }
415
+
416
+ for (const name of OPTIONAL_METHODS) {
417
+ const fn = driver[name]
418
+ // 驱动没实现就不装 —— 这正是本函数不写成 class 的原因
419
+ if (typeof fn !== "function") continue
420
+ const timed = TIMED_METHODS.has(name)
421
+ Reflect.set(facade, name, async (...args: unknown[]): Promise<unknown> => {
422
+ ensureAlive()
423
+ const pending = Reflect.apply(fn, driver, args) as Promise<unknown>
424
+ return await (timed ? withTimeout(pending, policy.sendTimeout, `调用 ${name} 超时`) : pending)
425
+ })
426
+ }
427
+
428
+ return facade
429
+ }
430
+
431
+ /** 注册表内部条目 */
432
+ interface BotEntry {
433
+ /** 门面 */
434
+ readonly facade: BotFacade
435
+ /**
436
+ * 建索引时的 selfId 快照
437
+ *
438
+ * 用它而不是 `facade.selfId` 摘索引:驱动重连后 selfId 理论上可变,
439
+ * 若按当前值去删,旧索引就会永久残留。
440
+ */
441
+ readonly selfId: string
442
+ }
443
+
444
+ /** Bot 注册表构造参数 */
445
+ export interface BotRegistryOptions {
446
+ /** 日志器 */
447
+ readonly logger: Logger
448
+ }
449
+
450
+ /**
451
+ * Bot 注册表
452
+ *
453
+ * 只登记**已连接**的账号:`BotRegistryView.get()` 的契约是"未连接时 undefined",
454
+ * 因此账号管理器在 `connect()` 成功后才 `add`,断开时立刻 `remove`。
455
+ * 让离线账号留在表里会让插件的 `pickBot()` 拿到一个永远发不出消息的壳子。
456
+ */
457
+ export class BotRegistry implements BotRegistryView {
458
+ /** 日志器 */
459
+ readonly #logger: Logger
460
+ /** 账号记录 id → 条目 */
461
+ readonly #byAccount = new Map<string, BotEntry>()
462
+ /** 平台 selfId → 条目(同一个 QQ 可能有两条账号记录,例如正向 WS 与 HTTP 各一条) */
463
+ readonly #bySelfId = new Map<string, BotEntry[]>()
464
+
465
+ /**
466
+ * @param opts 构造参数
467
+ */
468
+ constructor(opts: BotRegistryOptions) {
469
+ this.#logger = opts.logger.child({ scope: "bot" })
470
+ }
471
+
472
+ /** 在线 Bot 数量 */
473
+ get size(): number {
474
+ let n = 0
475
+ // 不走 online() —— 那会为了数个数分配一个数组
476
+ for (const entry of this.#byAccount.values()) if (entry.facade.online) n++
477
+ return n
478
+ }
479
+
480
+ /** 已登记的账号数(含短暂离线的) */
481
+ get total(): number {
482
+ return this.#byAccount.size
483
+ }
484
+
485
+ /**
486
+ * 登记一个已连接的账号
487
+ *
488
+ * 必须在驱动知道自己的 selfId **之后**调用(即 `connect()` 解析之后),
489
+ * 否则 selfId 索引会建在空串上。重连走"remove → add",索引因此自愈。
490
+ * @param facade Bot 门面
491
+ * @returns 被顶掉的旧门面(同一账号重复登记时),没有则 undefined
492
+ */
493
+ add(facade: BotFacade): BotFacade | undefined {
494
+ const previous = this.remove(facade.accountId)
495
+ const entry: BotEntry = { facade, selfId: facade.selfId }
496
+ this.#byAccount.set(facade.accountId, entry)
497
+
498
+ let list = this.#bySelfId.get(entry.selfId)
499
+ if (list === undefined) {
500
+ list = []
501
+ this.#bySelfId.set(entry.selfId, list)
502
+ }
503
+ list.push(entry)
504
+
505
+ if (list.length > 1) {
506
+ // 不是错误:一个 QQ 挂两条链路(正向 WS + HTTP 回调)是合法用法。
507
+ // 但这意味着 bySelfId 存在歧义,此处提示一句,以免排障时误判至他处
508
+ this.#logger.info(`账号 ${facade.selfId} 有 ${list.length} 条链路在线,bySelfId 只会返回其中一条`)
509
+ }
510
+ return previous
511
+ }
512
+
513
+ /**
514
+ * 摘除一个账号
515
+ * @param accountId 账号记录 id
516
+ * @returns 被摘除的门面;未登记时 undefined
517
+ */
518
+ remove(accountId: string): BotFacade | undefined {
519
+ const entry = this.#byAccount.get(accountId)
520
+ if (entry === undefined) return undefined
521
+ this.#byAccount.delete(accountId)
522
+
523
+ const list = this.#bySelfId.get(entry.selfId)
524
+ if (list !== undefined) {
525
+ const at = list.indexOf(entry)
526
+ if (at >= 0) list.splice(at, 1)
527
+ if (list.length === 0) this.#bySelfId.delete(entry.selfId)
528
+ }
529
+ return entry.facade
530
+ }
531
+
532
+ /**
533
+ * 按账号记录 id 取 Bot
534
+ * @param accountId 账号记录 id
535
+ * @returns Bot,未连接时 undefined
536
+ */
537
+ get(accountId: string): BotApi | undefined {
538
+ return this.#byAccount.get(accountId)?.facade
539
+ }
540
+
541
+ /**
542
+ * 按平台 selfId 取 Bot
543
+ *
544
+ * 同一个 QQ 有多条链路时优先返回在线的那条;全都不在线则返回第一条 ——
545
+ * 而不是 undefined。重连的瞬间返回"找不到这个账号"会让插件走进
546
+ * "配置错了吗"的排查方向,让驱动报一个真实的网络错误更有指向性。
547
+ * @param selfId 平台账号 id
548
+ * @returns Bot,未连接时 undefined
549
+ */
550
+ bySelfId(selfId: string): BotApi | undefined {
551
+ const list = this.#bySelfId.get(selfId)
552
+ if (list === undefined) return undefined
553
+ for (const entry of list) if (entry.facade.online) return entry.facade
554
+ return list[0]?.facade
555
+ }
556
+
557
+ /**
558
+ * 列出所有在线 Bot
559
+ * @returns Bot 数组
560
+ */
561
+ online(): BotApi[] {
562
+ const out: BotApi[] = []
563
+ for (const entry of this.#byAccount.values()) if (entry.facade.online) out.push(entry.facade)
564
+ return out
565
+ }
566
+
567
+ /**
568
+ * 列出全部门面(含离线),停机时逐个关闭用
569
+ * @returns 门面数组
570
+ */
571
+ all(): BotFacade[] {
572
+ const out: BotFacade[] = []
573
+ for (const entry of this.#byAccount.values()) out.push(entry.facade)
574
+ return out
575
+ }
576
+
577
+ /** 清空登记(不关闭门面,由账号管理器负责) */
578
+ clear(): void {
579
+ this.#byAccount.clear()
580
+ this.#bySelfId.clear()
581
+ }
582
+ }