@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.
- package/LICENSE +661 -0
- package/README.md +51 -0
- package/dist/adapter/accounts.d.ts +174 -0
- package/dist/adapter/accounts.d.ts.map +1 -0
- package/dist/adapter/accounts.js +653 -0
- package/dist/adapter/accounts.js.map +1 -0
- package/dist/adapter/bots.d.ts +142 -0
- package/dist/adapter/bots.d.ts.map +1 -0
- package/dist/adapter/bots.js +454 -0
- package/dist/adapter/bots.js.map +1 -0
- package/dist/adapter/host.d.ts +114 -0
- package/dist/adapter/host.d.ts.map +1 -0
- package/dist/adapter/host.js +90 -0
- package/dist/adapter/host.js.map +1 -0
- package/dist/adapter/login.d.ts +174 -0
- package/dist/adapter/login.d.ts.map +1 -0
- package/dist/adapter/login.js +345 -0
- package/dist/adapter/login.js.map +1 -0
- package/dist/adapter/registry.d.ts +102 -0
- package/dist/adapter/registry.d.ts.map +1 -0
- package/dist/adapter/registry.js +150 -0
- package/dist/adapter/registry.js.map +1 -0
- package/dist/config/core-config.d.ts +151 -0
- package/dist/config/core-config.d.ts.map +1 -0
- package/dist/config/core-config.js +338 -0
- package/dist/config/core-config.js.map +1 -0
- package/dist/config/schema.d.ts +449 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +871 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/store.d.ts +184 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +428 -0
- package/dist/config/store.js.map +1 -0
- package/dist/config/yaml.d.ts +23 -0
- package/dist/config/yaml.d.ts.map +1 -0
- package/dist/config/yaml.js +108 -0
- package/dist/config/yaml.js.map +1 -0
- package/dist/http/client.d.ts +82 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +658 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/app.d.ts +244 -0
- package/dist/kernel/app.d.ts.map +1 -0
- package/dist/kernel/app.js +654 -0
- package/dist/kernel/app.js.map +1 -0
- package/dist/kernel/kv-sink.d.ts +34 -0
- package/dist/kernel/kv-sink.d.ts.map +1 -0
- package/dist/kernel/kv-sink.js +25 -0
- package/dist/kernel/kv-sink.js.map +1 -0
- package/dist/kernel/policy.d.ts +77 -0
- package/dist/kernel/policy.d.ts.map +1 -0
- package/dist/kernel/policy.js +99 -0
- package/dist/kernel/policy.js.map +1 -0
- package/dist/kernel/runtime.d.ts +117 -0
- package/dist/kernel/runtime.d.ts.map +1 -0
- package/dist/kernel/runtime.js +197 -0
- package/dist/kernel/runtime.js.map +1 -0
- package/dist/kernel/sql-sink.d.ts +36 -0
- package/dist/kernel/sql-sink.d.ts.map +1 -0
- package/dist/kernel/sql-sink.js +114 -0
- package/dist/kernel/sql-sink.js.map +1 -0
- package/dist/logger/format.d.ts +67 -0
- package/dist/logger/format.d.ts.map +1 -0
- package/dist/logger/format.js +218 -0
- package/dist/logger/format.js.map +1 -0
- package/dist/logger/index.d.ts +97 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +363 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/rotate.d.ts +47 -0
- package/dist/logger/rotate.d.ts.map +1 -0
- package/dist/logger/rotate.js +242 -0
- package/dist/logger/rotate.js.map +1 -0
- package/dist/message/segment.d.ts +255 -0
- package/dist/message/segment.d.ts.map +1 -0
- package/dist/message/segment.js +510 -0
- package/dist/message/segment.js.map +1 -0
- package/dist/message/split.d.ts +34 -0
- package/dist/message/split.d.ts.map +1 -0
- package/dist/message/split.js +79 -0
- package/dist/message/split.js.map +1 -0
- package/dist/message/target.d.ts +29 -0
- package/dist/message/target.d.ts.map +1 -0
- package/dist/message/target.js +34 -0
- package/dist/message/target.js.map +1 -0
- package/dist/pipeline/cooldown.d.ts +84 -0
- package/dist/pipeline/cooldown.d.ts.map +1 -0
- package/dist/pipeline/cooldown.js +94 -0
- package/dist/pipeline/cooldown.js.map +1 -0
- package/dist/pipeline/dispatch.d.ts +98 -0
- package/dist/pipeline/dispatch.d.ts.map +1 -0
- package/dist/pipeline/dispatch.js +323 -0
- package/dist/pipeline/dispatch.js.map +1 -0
- package/dist/pipeline/event.d.ts +130 -0
- package/dist/pipeline/event.d.ts.map +1 -0
- package/dist/pipeline/event.js +538 -0
- package/dist/pipeline/event.js.map +1 -0
- package/dist/pipeline/middleware.d.ts +63 -0
- package/dist/pipeline/middleware.d.ts.map +1 -0
- package/dist/pipeline/middleware.js +174 -0
- package/dist/pipeline/middleware.js.map +1 -0
- package/dist/pipeline/prompt.d.ts +80 -0
- package/dist/pipeline/prompt.d.ts.map +1 -0
- package/dist/pipeline/prompt.js +155 -0
- package/dist/pipeline/prompt.js.map +1 -0
- package/dist/pipeline/router.d.ts +98 -0
- package/dist/pipeline/router.d.ts.map +1 -0
- package/dist/pipeline/router.js +489 -0
- package/dist/pipeline/router.js.map +1 -0
- package/dist/platform/detect.d.ts +24 -0
- package/dist/platform/detect.d.ts.map +1 -0
- package/dist/platform/detect.js +178 -0
- package/dist/platform/detect.js.map +1 -0
- package/dist/platform/paths.d.ts +38 -0
- package/dist/platform/paths.d.ts.map +1 -0
- package/dist/platform/paths.js +129 -0
- package/dist/platform/paths.js.map +1 -0
- package/dist/platform/system.d.ts +71 -0
- package/dist/platform/system.d.ts.map +1 -0
- package/dist/platform/system.js +242 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/plugin/context.d.ts +92 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +573 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +92 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +82 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/discover.d.ts +106 -0
- package/dist/plugin/discover.d.ts.map +1 -0
- package/dist/plugin/discover.js +254 -0
- package/dist/plugin/discover.js.map +1 -0
- package/dist/plugin/events.d.ts +109 -0
- package/dist/plugin/events.d.ts.map +1 -0
- package/dist/plugin/events.js +209 -0
- package/dist/plugin/events.js.map +1 -0
- package/dist/plugin/hooks.d.ts +266 -0
- package/dist/plugin/hooks.d.ts.map +1 -0
- package/dist/plugin/hooks.js +84 -0
- package/dist/plugin/hooks.js.map +1 -0
- package/dist/plugin/host.d.ts +160 -0
- package/dist/plugin/host.d.ts.map +1 -0
- package/dist/plugin/host.js +546 -0
- package/dist/plugin/host.js.map +1 -0
- package/dist/plugin/market.d.ts +264 -0
- package/dist/plugin/market.d.ts.map +1 -0
- package/dist/plugin/market.js +700 -0
- package/dist/plugin/market.js.map +1 -0
- package/dist/plugin/services.d.ts +116 -0
- package/dist/plugin/services.d.ts.map +1 -0
- package/dist/plugin/services.js +213 -0
- package/dist/plugin/services.js.map +1 -0
- package/dist/plugin/tar.d.ts +65 -0
- package/dist/plugin/tar.d.ts.map +1 -0
- package/dist/plugin/tar.js +277 -0
- package/dist/plugin/tar.js.map +1 -0
- package/dist/render/registry.d.ts +127 -0
- package/dist/render/registry.d.ts.map +1 -0
- package/dist/render/registry.js +306 -0
- package/dist/render/registry.js.map +1 -0
- package/dist/scheduler/index.d.ts +67 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +279 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/server/api.d.ts +179 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +602 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/auth.d.ts +83 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +178 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/browse.d.ts +60 -0
- package/dist/server/browse.d.ts.map +1 -0
- package/dist/server/browse.js +187 -0
- package/dist/server/browse.js.map +1 -0
- package/dist/server/files.d.ts +27 -0
- package/dist/server/files.d.ts.map +1 -0
- package/dist/server/files.js +76 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/index.d.ts +189 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1123 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/table.d.ts +105 -0
- package/dist/server/table.d.ts.map +1 -0
- package/dist/server/table.js +289 -0
- package/dist/server/table.js.map +1 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +120 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/json.d.ts +73 -0
- package/dist/store/json.d.ts.map +1 -0
- package/dist/store/json.js +174 -0
- package/dist/store/json.js.map +1 -0
- package/dist/store/kv.d.ts +126 -0
- package/dist/store/kv.d.ts.map +1 -0
- package/dist/store/kv.js +232 -0
- package/dist/store/kv.js.map +1 -0
- package/dist/store/level.d.ts +103 -0
- package/dist/store/level.d.ts.map +1 -0
- package/dist/store/level.js +119 -0
- package/dist/store/level.js.map +1 -0
- package/dist/store/memory.d.ts +61 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +94 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sql.d.ts +142 -0
- package/dist/store/sql.d.ts.map +1 -0
- package/dist/store/sql.js +246 -0
- package/dist/store/sql.js.map +1 -0
- package/dist/testing/fake.d.ts +109 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +196 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-adapter.d.ts +205 -0
- package/dist/testing/mock-adapter.d.ts.map +1 -0
- package/dist/testing/mock-adapter.js +309 -0
- package/dist/testing/mock-adapter.js.map +1 -0
- package/dist/util/deep.d.ts +108 -0
- package/dist/util/deep.d.ts.map +1 -0
- package/dist/util/deep.js +275 -0
- package/dist/util/deep.js.map +1 -0
- package/dist/util/defer.d.ts +107 -0
- package/dist/util/defer.d.ts.map +1 -0
- package/dist/util/defer.js +163 -0
- package/dist/util/defer.js.map +1 -0
- package/dist/util/dispose.d.ts +88 -0
- package/dist/util/dispose.d.ts.map +1 -0
- package/dist/util/dispose.js +143 -0
- package/dist/util/dispose.js.map +1 -0
- package/dist/util/duration.d.ts +28 -0
- package/dist/util/duration.d.ts.map +1 -0
- package/dist/util/duration.js +77 -0
- package/dist/util/duration.js.map +1 -0
- package/dist/util/fs.d.ts +110 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +269 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/id.d.ts +49 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +87 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/lru.d.ts +74 -0
- package/dist/util/lru.d.ts.map +1 -0
- package/dist/util/lru.js +156 -0
- package/dist/util/lru.js.map +1 -0
- package/dist/util/queue.d.ts +64 -0
- package/dist/util/queue.d.ts.map +1 -0
- package/dist/util/queue.js +147 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/text.d.ts +86 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +182 -0
- package/dist/util/text.js.map +1 -0
- package/package.json +44 -0
- package/src/adapter/accounts.ts +758 -0
- package/src/adapter/bots.ts +582 -0
- package/src/adapter/host.ts +224 -0
- package/src/adapter/login.ts +481 -0
- package/src/adapter/registry.ts +202 -0
- package/src/config/core-config.test.ts +60 -0
- package/src/config/core-config.ts +401 -0
- package/src/config/schema.test.ts +179 -0
- package/src/config/schema.ts +1036 -0
- package/src/config/store.test.ts +245 -0
- package/src/config/store.ts +506 -0
- package/src/config/yaml.ts +119 -0
- package/src/http/client.test.ts +568 -0
- package/src/http/client.ts +777 -0
- package/src/index.ts +106 -0
- package/src/kernel/app.test.ts +267 -0
- package/src/kernel/app.ts +843 -0
- package/src/kernel/kv-sink.ts +58 -0
- package/src/kernel/policy.ts +120 -0
- package/src/kernel/runtime.test.ts +542 -0
- package/src/kernel/runtime.ts +307 -0
- package/src/kernel/sql-sink.ts +154 -0
- package/src/logger/format.ts +259 -0
- package/src/logger/index.ts +427 -0
- package/src/logger/rotate.ts +265 -0
- package/src/message/segment.test.ts +335 -0
- package/src/message/segment.ts +569 -0
- package/src/message/split.ts +97 -0
- package/src/message/target.ts +48 -0
- package/src/pipeline/cooldown.ts +136 -0
- package/src/pipeline/dispatch.ts +419 -0
- package/src/pipeline/event.ts +735 -0
- package/src/pipeline/middleware.test.ts +426 -0
- package/src/pipeline/middleware.ts +199 -0
- package/src/pipeline/prompt.ts +212 -0
- package/src/pipeline/router.test.ts +432 -0
- package/src/pipeline/router.ts +575 -0
- package/src/platform/detect.ts +178 -0
- package/src/platform/paths.ts +147 -0
- package/src/platform/system.test.ts +102 -0
- package/src/platform/system.ts +289 -0
- package/src/plugin/context.test.ts +459 -0
- package/src/plugin/context.ts +757 -0
- package/src/plugin/define.test.ts +120 -0
- package/src/plugin/define.ts +142 -0
- package/src/plugin/discover.test.ts +253 -0
- package/src/plugin/discover.ts +353 -0
- package/src/plugin/events.test.ts +162 -0
- package/src/plugin/events.ts +266 -0
- package/src/plugin/hooks.ts +373 -0
- package/src/plugin/host.test.ts +696 -0
- package/src/plugin/host.ts +738 -0
- package/src/plugin/market.test.ts +781 -0
- package/src/plugin/market.ts +870 -0
- package/src/plugin/services.test.ts +135 -0
- package/src/plugin/services.ts +261 -0
- package/src/plugin/tar.test.ts +158 -0
- package/src/plugin/tar.ts +309 -0
- package/src/render/registry.test.ts +127 -0
- package/src/render/registry.ts +439 -0
- package/src/scheduler/index.ts +357 -0
- package/src/server/api.test.ts +802 -0
- package/src/server/api.ts +869 -0
- package/src/server/auth.ts +201 -0
- package/src/server/browse.test.ts +193 -0
- package/src/server/browse.ts +240 -0
- package/src/server/files.ts +97 -0
- package/src/server/index.test.ts +566 -0
- package/src/server/index.ts +1386 -0
- package/src/server/table.ts +351 -0
- package/src/store/index.ts +156 -0
- package/src/store/json.ts +198 -0
- package/src/store/kv.test.ts +307 -0
- package/src/store/kv.ts +256 -0
- package/src/store/level.ts +167 -0
- package/src/store/memory.ts +109 -0
- package/src/store/sql.test.ts +204 -0
- package/src/store/sql.ts +324 -0
- package/src/testing/fake.ts +297 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +588 -0
- package/src/util/deep.ts +272 -0
- package/src/util/defer.ts +211 -0
- package/src/util/dispose.ts +169 -0
- package/src/util/duration.ts +82 -0
- package/src/util/fs.ts +279 -0
- package/src/util/id.ts +94 -0
- package/src/util/lru.ts +185 -0
- package/src/util/queue.ts +167 -0
- 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
|
+
}
|