@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,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:`AdapterHost` 实现 —— 内核给每个账号的宿主能力面
|
|
3
|
+
* 依赖方向:依赖类型包、plugin/hooks 的 `ServerSink`、util/*;**不认识任何适配器实现**
|
|
4
|
+
* 生命周期:一个账号一个实例;随账号断开而回收(`DisposalRegistry` 逆序)
|
|
5
|
+
* 注意事项:适配器插件只拿到这个对象,拿不到内核内部。它解决三件事:
|
|
6
|
+
*
|
|
7
|
+
* **事件投递的闸门。** `BotDriver` 的契约写明「内核会在卸载时调 `disconnect()`,并断言
|
|
8
|
+
* 其后不再有事件投递进来」。这条断言由此处兑现:`signal` 一经 abort,`submit()` 立即开始
|
|
9
|
+
* 丢弃并计数 —— 不能指望适配器一定彻底停掉自己的 socket 回调,否则热重载后会收到重复回复。
|
|
10
|
+
*
|
|
11
|
+
* **服务器晚绑定。** HTTP 服务器装配得比适配器插件晚,故 `server` 是 getter、`ServerSink`
|
|
12
|
+
* 是取值函数:适配器在 `createBot()` 里存下 host,真要挂 webhook 时读到的是当时的实际状态。
|
|
13
|
+
* 服务器没开时 `info.enabled` 为 false,适配器据此告知使用者「该模式需要内置服务器」。
|
|
14
|
+
*
|
|
15
|
+
* **资源回收。** `route` / `websocket` / `createCache` / `onDispose` 全部登记进账号自己的
|
|
16
|
+
* `DisposalRegistry`,账号一断开即全部回收,适配器不必自行记录。
|
|
17
|
+
*
|
|
18
|
+
* 工厂分两层是刻意的:内核装配时绑定一次全局依赖,账号管理器每次连接只补「该账号」专属
|
|
19
|
+
* 的几项 —— 于是账号管理器不必认识分发器与服务器,`adapter` 层也不会反向依赖 `pipeline`。
|
|
20
|
+
*/
|
|
21
|
+
import type {
|
|
22
|
+
AccountRecord,
|
|
23
|
+
AccountStatus,
|
|
24
|
+
AdapterHost,
|
|
25
|
+
BotApi,
|
|
26
|
+
ContactCache,
|
|
27
|
+
ContactCacheOptions,
|
|
28
|
+
Disposer,
|
|
29
|
+
IncomingEvent,
|
|
30
|
+
KvNamespace,
|
|
31
|
+
Logger,
|
|
32
|
+
PolicyView,
|
|
33
|
+
RouteHandler,
|
|
34
|
+
RouteOptions,
|
|
35
|
+
ServerInfo,
|
|
36
|
+
WebSocketHandler,
|
|
37
|
+
WebSocketOptions
|
|
38
|
+
} from "@yunzai-ng/types"
|
|
39
|
+
import type { ServerSink } from "../plugin/hooks.js"
|
|
40
|
+
import type { DisposalRegistry } from "../util/dispose.js"
|
|
41
|
+
import { createCache } from "../util/lru.js"
|
|
42
|
+
|
|
43
|
+
/** 状态上报的附加信息 */
|
|
44
|
+
export interface StatusDetail {
|
|
45
|
+
/** 错误消息 */
|
|
46
|
+
error?: string
|
|
47
|
+
/** 账号昵称 */
|
|
48
|
+
nickname?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 宿主工厂的全局依赖(内核装配时绑定一次) */
|
|
52
|
+
export interface AdapterHostDeps {
|
|
53
|
+
/**
|
|
54
|
+
* 基础日志器
|
|
55
|
+
*
|
|
56
|
+
* 工厂会自己派生带 `adapter`/`account` 字段的 child —— `AdapterHost.logger`
|
|
57
|
+
* 的契约是"已带这两个字段",由这里兑现而不是靠调用方记得。
|
|
58
|
+
*/
|
|
59
|
+
readonly logger: Logger
|
|
60
|
+
/**
|
|
61
|
+
* 适配器数据的 KV 根命名空间
|
|
62
|
+
*
|
|
63
|
+
* 每个账号取得的是 `<root>/<adapterId>/<accountId>`:两个账号即使运行同一个
|
|
64
|
+
* 适配器亦不会相互覆写键,而同一账号重连后仍可读回其此前写入的数据
|
|
65
|
+
*(NapCat 的 `echo` 序号与登录态缓存均依赖该机制)。
|
|
66
|
+
*/
|
|
67
|
+
readonly kv: KvNamespace
|
|
68
|
+
/** 内核策略视图 */
|
|
69
|
+
readonly policy: PolicyView
|
|
70
|
+
/**
|
|
71
|
+
* 取当前的服务器登记面
|
|
72
|
+
*
|
|
73
|
+
* 取函数而非直接传值:服务器子系统装好之前 `hooks.server` 是"不可用"占位,
|
|
74
|
+
* 之后会被整体替换掉。见文件头第 2 点。
|
|
75
|
+
*/
|
|
76
|
+
readonly server: () => ServerSink
|
|
77
|
+
/**
|
|
78
|
+
* 把事件送进管线
|
|
79
|
+
*
|
|
80
|
+
* 由内核注入(最终指向 `EventDispatcher.submit`)。宿主不认识分发器,
|
|
81
|
+
* 否则 adapter 层就反过来依赖 pipeline 层了。
|
|
82
|
+
* @param event 通用事件
|
|
83
|
+
* @param bot 收到该事件的 Bot
|
|
84
|
+
*/
|
|
85
|
+
readonly dispatch: (event: IncomingEvent, bot: BotApi) => void
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 单次连接的账号侧参数 */
|
|
89
|
+
export interface AdapterHostParams {
|
|
90
|
+
/** 账号记录 */
|
|
91
|
+
readonly record: AccountRecord
|
|
92
|
+
/** 本次连接的取消信号源 */
|
|
93
|
+
readonly controller: AbortController
|
|
94
|
+
/** 本次连接的回收登记簿 */
|
|
95
|
+
readonly registry: DisposalRegistry
|
|
96
|
+
/**
|
|
97
|
+
* 上报账号状态
|
|
98
|
+
* @param status 新状态
|
|
99
|
+
* @param detail 附加信息
|
|
100
|
+
*/
|
|
101
|
+
readonly setStatus: (status: AccountStatus, detail?: StatusDetail) => void
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 宿主句柄:`host` 交给适配器,其余留给账号管理器 */
|
|
105
|
+
export interface AdapterHostHandle {
|
|
106
|
+
/** 交给 `createBot()` 的宿主对象 */
|
|
107
|
+
readonly host: AdapterHost
|
|
108
|
+
/**
|
|
109
|
+
* 绑定事件归属的 Bot
|
|
110
|
+
*
|
|
111
|
+
* 必须在 `createBot()` 返回、门面建好之后调用。宿主先于驱动存在
|
|
112
|
+
* (驱动是靠宿主创建出来的),所以这个槽位只能后填。
|
|
113
|
+
* @param bot Bot 门面
|
|
114
|
+
*/
|
|
115
|
+
attach(bot: BotApi): void
|
|
116
|
+
/** 解绑:此后事件一律丢弃 */
|
|
117
|
+
detach(): void
|
|
118
|
+
/** 被丢弃的事件数(诊断用:非 0 说明适配器停止不彻底) */
|
|
119
|
+
readonly dropped: number
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 宿主工厂 */
|
|
123
|
+
export type AdapterHostFactory = (params: AdapterHostParams) => AdapterHostHandle
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 绑定全局依赖,得到宿主工厂
|
|
127
|
+
* @param deps 全局依赖
|
|
128
|
+
* @returns 宿主工厂
|
|
129
|
+
*/
|
|
130
|
+
export function createAdapterHostFactory(deps: AdapterHostDeps): AdapterHostFactory {
|
|
131
|
+
return params => createAdapterHost(deps, params)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 创建一个账号宿主
|
|
136
|
+
* @param deps 全局依赖
|
|
137
|
+
* @param params 账号侧参数
|
|
138
|
+
* @returns 宿主句柄
|
|
139
|
+
*/
|
|
140
|
+
export function createAdapterHost(deps: AdapterHostDeps, params: AdapterHostParams): AdapterHostHandle {
|
|
141
|
+
const { record, registry, setStatus } = params
|
|
142
|
+
const adapterId = record.adapterId
|
|
143
|
+
const accountId = record.id
|
|
144
|
+
const signal = params.controller.signal
|
|
145
|
+
const logger = deps.logger.child({ adapter: adapterId, account: accountId })
|
|
146
|
+
/** 路由挂载前缀 */
|
|
147
|
+
const scope = `/adapter/${adapterId}`
|
|
148
|
+
|
|
149
|
+
/** 事件归属的 Bot;`createBot()` 之后才有 */
|
|
150
|
+
let bot: BotApi | undefined
|
|
151
|
+
/** 丢弃的事件数 */
|
|
152
|
+
let dropped = 0
|
|
153
|
+
/** 是否已就"投递到无主宿主"提醒过 */
|
|
154
|
+
let warnedNoBot = false
|
|
155
|
+
|
|
156
|
+
const host: AdapterHost = {
|
|
157
|
+
logger,
|
|
158
|
+
signal,
|
|
159
|
+
policy: deps.policy,
|
|
160
|
+
kv: deps.kv.sub(adapterId).sub(accountId),
|
|
161
|
+
// 快照 + 复制 config:适配器往 config 上随手写字段是常事,
|
|
162
|
+
// 若给的是原对象,那些临时字段会被账号管理器一并持久化下去,
|
|
163
|
+
// 下次启动时又被当成用户配置读回来
|
|
164
|
+
account: { ...record, config: { ...record.config } },
|
|
165
|
+
|
|
166
|
+
// getter:服务器可能在适配器加载之后才启动,见文件头第 2 点
|
|
167
|
+
get server(): ServerInfo {
|
|
168
|
+
return deps.server().info
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
submit: (event: IncomingEvent): void => {
|
|
172
|
+
// 闸门一:账号已停用 / 插件已卸载。这是 BotDriver 契约里
|
|
173
|
+
// "断开后不再有事件投递进来"的兑现处
|
|
174
|
+
if (signal.aborted) {
|
|
175
|
+
dropped++
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
// 闸门二:尚未绑定 Bot。适配器在 connect() 完成前即收到事件,
|
|
179
|
+
// 此时不存在可用于回复的对象,继续处理只会在管线中抛错
|
|
180
|
+
if (bot === undefined) {
|
|
181
|
+
dropped++
|
|
182
|
+
if (!warnedNoBot) {
|
|
183
|
+
warnedNoBot = true
|
|
184
|
+
logger.warn("在 Bot 就绪前收到事件,已丢弃:请在 connect() 解析之后再开始投递事件")
|
|
185
|
+
}
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
deps.dispatch(event, bot)
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
route: (method: "GET" | "POST", path: string, handler: RouteHandler, routeOpts?: RouteOptions): Disposer =>
|
|
192
|
+
// 登记进账号的回收簿:账号一断开,webhook 端点必须跟着消失,
|
|
193
|
+
// 否则平台还在往一个没人接的地址推事件
|
|
194
|
+
registry.add(deps.server().route(scope, method, path, handler, routeOpts), `route:${method} ${scope}${path}`),
|
|
195
|
+
|
|
196
|
+
websocket: (path: string, handler: WebSocketHandler, wsOpts?: WebSocketOptions): Disposer =>
|
|
197
|
+
registry.add(deps.server().websocket(scope, path, handler, wsOpts), `ws:${scope}${path}`),
|
|
198
|
+
|
|
199
|
+
createCache: <V>(cacheOpts: ContactCacheOptions): ContactCache<V> => {
|
|
200
|
+
const cache = createCache<V>(cacheOpts)
|
|
201
|
+
// clear 而不是置 undefined:适配器可能还握着这个缓存对象的引用,
|
|
202
|
+
// 清空能立刻把内存还回去,之后即使被误用也只是全部 miss
|
|
203
|
+
registry.add(() => cache.clear(), "cache")
|
|
204
|
+
return cache
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
onDispose: (fn: Disposer): void => void registry.add(fn, "adapter:onDispose"),
|
|
208
|
+
|
|
209
|
+
setStatus: (status: AccountStatus, detail?: StatusDetail): void => setStatus(status, detail)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
host,
|
|
214
|
+
attach: (next: BotApi): void => {
|
|
215
|
+
bot = next
|
|
216
|
+
},
|
|
217
|
+
detach: (): void => {
|
|
218
|
+
bot = undefined
|
|
219
|
+
},
|
|
220
|
+
get dropped(): number {
|
|
221
|
+
return dropped
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:交互式登录会话(实现 `LoginSession`)与登录会话管理器
|
|
3
|
+
* 依赖方向:依赖类型包与 util/*;通过注入的回调创建账号,**不认识账号管理器**
|
|
4
|
+
* 生命周期:一次登录一个会话;成功/失败/取消后短暂留存供 WebUI 展示结果,然后自毁
|
|
5
|
+
* 注意事项:内核不知道扫码、短信验证码、填 token 有什么区别,只提供两个原语:**推一步给用户看**
|
|
6
|
+
* (`push`)与**等用户答一次**(`ask`)。扫码是 push(qrcode) + push(progress),短信是
|
|
7
|
+
* ask(手机号) + ask(验证码) —— 故前端不必为每个新平台改一行代码。三处必须做对:
|
|
8
|
+
*
|
|
9
|
+
* **`start()` 同步返回。** 登录要等用户操作、动辄几十秒,HTTP 请求不能挂在那儿,故
|
|
10
|
+
* `login()` 在后台跑,前端拿会话 id 去轮询或订阅。
|
|
11
|
+
*
|
|
12
|
+
* **`ask` 带序号。** 用户连点两次提交或网络重发时,迟到的那次答复不能拿去满足**下一个**
|
|
13
|
+
* 问题(把手机号填进验证码框),故 `answer()` 必须带 `seq`,对不上就拒绝。
|
|
14
|
+
*
|
|
15
|
+
* **一切都有上限**:步骤数、每次提问的超时、整个会话的 TTL,结束即自毁 —— 一个人点开
|
|
16
|
+
* 登录页就走开,不该留下一个永远挂着的 promise 和一张常驻内存的二维码。
|
|
17
|
+
*/
|
|
18
|
+
import type {
|
|
19
|
+
AccountRecord,
|
|
20
|
+
AdapterRegistryView,
|
|
21
|
+
LoginPrompt,
|
|
22
|
+
LoginSession,
|
|
23
|
+
LoginStep,
|
|
24
|
+
Logger
|
|
25
|
+
} from "@yunzai-ng/types"
|
|
26
|
+
import { parseDuration } from "../util/duration.js"
|
|
27
|
+
import { createEventId } from "../util/id.js"
|
|
28
|
+
|
|
29
|
+
/** 单次提问的缺省超时(毫秒),与 `LoginPrompt.timeout` 文档一致 */
|
|
30
|
+
export const DEFAULT_LOGIN_PROMPT_TIMEOUT = 180_000
|
|
31
|
+
|
|
32
|
+
/** 整个登录会话的硬上限(毫秒):扫码最多两三分钟,十分钟足够宽裕 */
|
|
33
|
+
export const DEFAULT_LOGIN_TTL = 600_000
|
|
34
|
+
|
|
35
|
+
/** 已结束的会话保留多久供前端读取结果(毫秒) */
|
|
36
|
+
const RESULT_TTL = 120_000
|
|
37
|
+
|
|
38
|
+
/** 步骤数组上限:`progress` 刷屏或反复刷新二维码时不能无限增长 */
|
|
39
|
+
const MAX_STEPS = 40
|
|
40
|
+
|
|
41
|
+
/** 登录会话状态 */
|
|
42
|
+
export type LoginStatus = "running" | "done" | "failed" | "cancelled"
|
|
43
|
+
|
|
44
|
+
/** 正在等待用户回答的提问 */
|
|
45
|
+
export interface PendingPrompt {
|
|
46
|
+
/** 提问序号,回答时必须带回 */
|
|
47
|
+
readonly seq: number
|
|
48
|
+
/** 提问内容 */
|
|
49
|
+
readonly prompt: LoginPrompt
|
|
50
|
+
/** 超时时刻(毫秒时间戳),前端据此显示倒计时 */
|
|
51
|
+
readonly deadline: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 登录会话快照,WebUI 的唯一数据来源 */
|
|
55
|
+
export interface LoginSnapshot {
|
|
56
|
+
/** 会话 id */
|
|
57
|
+
readonly id: string
|
|
58
|
+
/** 适配器 id */
|
|
59
|
+
readonly adapterId: string
|
|
60
|
+
/** 登录方式 id */
|
|
61
|
+
readonly mode: string
|
|
62
|
+
/** 当前状态 */
|
|
63
|
+
readonly status: LoginStatus
|
|
64
|
+
/** 已推送的步骤,按时间顺序 */
|
|
65
|
+
readonly steps: readonly LoginStep[]
|
|
66
|
+
/** 正在等待的提问;没有则 undefined */
|
|
67
|
+
readonly pending: PendingPrompt | undefined
|
|
68
|
+
/** 失败原因 */
|
|
69
|
+
readonly error: string | undefined
|
|
70
|
+
/** 成功时创建出的账号记录 id */
|
|
71
|
+
readonly accountId: string | undefined
|
|
72
|
+
/** 开始时刻(毫秒时间戳) */
|
|
73
|
+
readonly startedAt: number
|
|
74
|
+
/** 最后变化时刻(毫秒时间戳),前端据此判断是否需要重绘 */
|
|
75
|
+
readonly updatedAt: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 登录管理器构造参数 */
|
|
79
|
+
export interface LoginManagerOptions {
|
|
80
|
+
/** 日志器 */
|
|
81
|
+
readonly logger: Logger
|
|
82
|
+
/** 适配器注册表视图 */
|
|
83
|
+
readonly adapters: AdapterRegistryView
|
|
84
|
+
/**
|
|
85
|
+
* 登录成功后落账号
|
|
86
|
+
*
|
|
87
|
+
* 注入而非直接依赖账号管理器:否则 login → accounts → login 就绕成环了。
|
|
88
|
+
* @param adapterId 适配器 id
|
|
89
|
+
* @param config 登录产出的账号配置
|
|
90
|
+
* @param label 账号备注名
|
|
91
|
+
* @returns 新建的账号记录
|
|
92
|
+
*/
|
|
93
|
+
readonly createAccount: (adapterId: string, config: Record<string, unknown>, label?: string) => Promise<AccountRecord>
|
|
94
|
+
/** 会话硬上限,缺省 `DEFAULT_LOGIN_TTL` */
|
|
95
|
+
readonly ttl?: number
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 会话内部状态 */
|
|
99
|
+
interface SessionState {
|
|
100
|
+
/** 会话 id */
|
|
101
|
+
readonly id: string
|
|
102
|
+
/** 适配器 id */
|
|
103
|
+
readonly adapterId: string
|
|
104
|
+
/** 登录方式 */
|
|
105
|
+
readonly mode: string
|
|
106
|
+
/** 账号备注名 */
|
|
107
|
+
readonly label: string | undefined
|
|
108
|
+
/** 取消信号源 */
|
|
109
|
+
readonly controller: AbortController
|
|
110
|
+
/** 已推送步骤 */
|
|
111
|
+
readonly steps: LoginStep[]
|
|
112
|
+
/** 当前状态 */
|
|
113
|
+
status: LoginStatus
|
|
114
|
+
/** 正在等待的提问 */
|
|
115
|
+
pending: PendingPrompt | undefined
|
|
116
|
+
/** 交付答复 */
|
|
117
|
+
resolveAsk: ((value: unknown) => void) | undefined
|
|
118
|
+
/** 终止等待 */
|
|
119
|
+
rejectAsk: ((err: Error) => void) | undefined
|
|
120
|
+
/** 会话 TTL 定时器 */
|
|
121
|
+
ttlTimer: NodeJS.Timeout | undefined
|
|
122
|
+
/** 结束后自毁定时器 */
|
|
123
|
+
reapTimer: NodeJS.Timeout | undefined
|
|
124
|
+
/** 提问序号发号器 */
|
|
125
|
+
seq: number
|
|
126
|
+
/** 失败原因 */
|
|
127
|
+
error: string | undefined
|
|
128
|
+
/** 成功创建的账号 id */
|
|
129
|
+
accountId: string | undefined
|
|
130
|
+
/** 开始时刻 */
|
|
131
|
+
readonly startedAt: number
|
|
132
|
+
/** 最后变化时刻 */
|
|
133
|
+
updatedAt: number
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 取错误的可读描述
|
|
138
|
+
* @param err 任意抛出物
|
|
139
|
+
* @returns 描述文本
|
|
140
|
+
*/
|
|
141
|
+
function errText(err: unknown): string {
|
|
142
|
+
return err instanceof Error ? err.message : String(err)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 交互式登录会话管理器
|
|
147
|
+
*
|
|
148
|
+
* WebUI 侧的用法:`start()` 拿到 id → 轮询/订阅 `snapshot(id)` → 遇到
|
|
149
|
+
* `pending` 就渲染输入框,用户提交后 `answer(id, seq, value)` → 直到
|
|
150
|
+
* `status` 不再是 `running`。
|
|
151
|
+
*/
|
|
152
|
+
export class LoginManager {
|
|
153
|
+
/** 日志器 */
|
|
154
|
+
readonly #logger: Logger
|
|
155
|
+
/** 适配器注册表 */
|
|
156
|
+
readonly #adapters: AdapterRegistryView
|
|
157
|
+
/** 账号创建回调 */
|
|
158
|
+
readonly #createAccount: LoginManagerOptions["createAccount"]
|
|
159
|
+
/** 会话硬上限 */
|
|
160
|
+
readonly #ttl: number
|
|
161
|
+
/** 会话 id → 状态 */
|
|
162
|
+
readonly #sessions = new Map<string, SessionState>()
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param opts 构造参数
|
|
166
|
+
*/
|
|
167
|
+
constructor(opts: LoginManagerOptions) {
|
|
168
|
+
this.#logger = opts.logger.child({ scope: "login" })
|
|
169
|
+
this.#adapters = opts.adapters
|
|
170
|
+
this.#createAccount = opts.createAccount
|
|
171
|
+
this.#ttl = opts.ttl ?? DEFAULT_LOGIN_TTL
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 进行中的会话数 */
|
|
175
|
+
get running(): number {
|
|
176
|
+
let n = 0
|
|
177
|
+
for (const s of this.#sessions.values()) if (s.status === "running") n++
|
|
178
|
+
return n
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 开始一次交互式登录
|
|
183
|
+
*
|
|
184
|
+
* **同步返回**,登录流程在后台跑,见文件头第 1 点。
|
|
185
|
+
* @param adapterId 适配器 id
|
|
186
|
+
* @param mode 登录方式 id
|
|
187
|
+
* @param label 账号备注名
|
|
188
|
+
* @returns 初始快照
|
|
189
|
+
* @throws 适配器不存在、不支持交互式登录、或方式 id 无效时
|
|
190
|
+
*/
|
|
191
|
+
start(adapterId: string, mode: string, label?: string): LoginSnapshot {
|
|
192
|
+
const provider = this.#adapters.get(adapterId)
|
|
193
|
+
if (provider === undefined) throw new Error(`适配器 ${adapterId} 未注册:请先安装并启用对应的适配器插件`)
|
|
194
|
+
|
|
195
|
+
const login = provider.login
|
|
196
|
+
if (login === undefined) {
|
|
197
|
+
throw new Error(`适配器 ${provider.name} 不支持交互式登录,请在"添加账号"里直接填写配置`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const modes = provider.loginModes ?? []
|
|
201
|
+
if (modes.length > 0 && !modes.some(m => m.id === mode)) {
|
|
202
|
+
const names = modes.map(m => `${m.id}(${m.name})`).join("、")
|
|
203
|
+
throw new Error(`适配器 ${provider.name} 没有名为 ${mode} 的登录方式,可用:${names}`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const id = createEventId("login")
|
|
207
|
+
const now = Date.now()
|
|
208
|
+
const state: SessionState = {
|
|
209
|
+
id,
|
|
210
|
+
adapterId,
|
|
211
|
+
mode,
|
|
212
|
+
label,
|
|
213
|
+
controller: new AbortController(),
|
|
214
|
+
steps: [],
|
|
215
|
+
status: "running",
|
|
216
|
+
pending: undefined,
|
|
217
|
+
resolveAsk: undefined,
|
|
218
|
+
rejectAsk: undefined,
|
|
219
|
+
ttlTimer: undefined,
|
|
220
|
+
reapTimer: undefined,
|
|
221
|
+
seq: 0,
|
|
222
|
+
error: undefined,
|
|
223
|
+
accountId: undefined,
|
|
224
|
+
startedAt: now,
|
|
225
|
+
updatedAt: now
|
|
226
|
+
}
|
|
227
|
+
this.#sessions.set(id, state)
|
|
228
|
+
|
|
229
|
+
state.ttlTimer = setTimeout(() => {
|
|
230
|
+
this.#abort(state, `登录超时(超过 ${Math.round(this.#ttl / 1000)} 秒未完成)`, "failed")
|
|
231
|
+
}, this.#ttl)
|
|
232
|
+
state.ttlTimer.unref?.()
|
|
233
|
+
|
|
234
|
+
const session = this.#createSession(state)
|
|
235
|
+
this.#logger.info(`开始登录:适配器 ${adapterId},方式 ${mode},会话 ${id}`)
|
|
236
|
+
|
|
237
|
+
// 后台跑:不 await,也不让它的 rejection 冒成 unhandledRejection
|
|
238
|
+
void login(session, mode).then(
|
|
239
|
+
config => void this.#succeed(state, config),
|
|
240
|
+
(err: unknown) => this.#fail(state, err)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return this.snapshotOf(state)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 读一个会话的快照
|
|
248
|
+
* @param id 会话 id
|
|
249
|
+
* @returns 快照;会话不存在(或已自毁)时 undefined
|
|
250
|
+
*/
|
|
251
|
+
snapshot(id: string): LoginSnapshot | undefined {
|
|
252
|
+
const state = this.#sessions.get(id)
|
|
253
|
+
return state === undefined ? undefined : this.snapshotOf(state)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 列出全部会话
|
|
258
|
+
* @returns 快照数组,按开始时间
|
|
259
|
+
*/
|
|
260
|
+
list(): LoginSnapshot[] {
|
|
261
|
+
return [...this.#sessions.values()].map(s => this.snapshotOf(s))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 提交用户的答复
|
|
266
|
+
* @param id 会话 id
|
|
267
|
+
* @param seq 提问序号,必须与 `pending.seq` 一致
|
|
268
|
+
* @param value 用户输入
|
|
269
|
+
* @returns 是否被接受
|
|
270
|
+
*/
|
|
271
|
+
answer(id: string, seq: number, value: unknown): boolean {
|
|
272
|
+
const state = this.#sessions.get(id)
|
|
273
|
+
if (state === undefined || state.status !== "running") return false
|
|
274
|
+
const pending = state.pending
|
|
275
|
+
if (pending === undefined) return false
|
|
276
|
+
// 序号不符即丢弃:见文件头第 2 点
|
|
277
|
+
if (pending.seq !== seq) {
|
|
278
|
+
this.#logger.debug(`会话 ${id} 收到过期答复(seq ${seq},当前 ${pending.seq}),已忽略`)
|
|
279
|
+
return false
|
|
280
|
+
}
|
|
281
|
+
state.resolveAsk?.(value)
|
|
282
|
+
return true
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 取消一个会话
|
|
287
|
+
* @param id 会话 id
|
|
288
|
+
* @returns 是否取消成功(会话不存在或已结束时 false)
|
|
289
|
+
*/
|
|
290
|
+
cancel(id: string): boolean {
|
|
291
|
+
const state = this.#sessions.get(id)
|
|
292
|
+
if (state === undefined || state.status !== "running") return false
|
|
293
|
+
this.#abort(state, "用户取消了登录", "cancelled")
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** 取消全部会话并清理(停机时调用) */
|
|
298
|
+
stop(): void {
|
|
299
|
+
for (const state of [...this.#sessions.values()]) {
|
|
300
|
+
if (state.status === "running") this.#abort(state, "内核正在停止", "cancelled")
|
|
301
|
+
if (state.ttlTimer !== undefined) clearTimeout(state.ttlTimer)
|
|
302
|
+
if (state.reapTimer !== undefined) clearTimeout(state.reapTimer)
|
|
303
|
+
}
|
|
304
|
+
this.#sessions.clear()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 生成快照
|
|
309
|
+
* @param state 会话状态
|
|
310
|
+
* @returns 快照
|
|
311
|
+
*/
|
|
312
|
+
snapshotOf(state: SessionState): LoginSnapshot {
|
|
313
|
+
return {
|
|
314
|
+
id: state.id,
|
|
315
|
+
adapterId: state.adapterId,
|
|
316
|
+
mode: state.mode,
|
|
317
|
+
status: state.status,
|
|
318
|
+
// 复制数组:前端持有的快照不该跟着后续 push 变化,否则"这一帧看到了什么"
|
|
319
|
+
// 就说不清了。步骤有上限,复制成本可控。
|
|
320
|
+
steps: [...state.steps],
|
|
321
|
+
pending: state.pending,
|
|
322
|
+
error: state.error,
|
|
323
|
+
accountId: state.accountId,
|
|
324
|
+
startedAt: state.startedAt,
|
|
325
|
+
updatedAt: state.updatedAt
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 造出交给适配器的 `LoginSession`
|
|
331
|
+
* @param state 会话状态
|
|
332
|
+
* @returns 登录会话
|
|
333
|
+
*/
|
|
334
|
+
#createSession(state: SessionState): LoginSession {
|
|
335
|
+
const logger = this.#logger.child({ session: state.id })
|
|
336
|
+
return {
|
|
337
|
+
id: state.id,
|
|
338
|
+
logger,
|
|
339
|
+
signal: state.controller.signal,
|
|
340
|
+
|
|
341
|
+
push: (step: LoginStep): void => {
|
|
342
|
+
if (state.status !== "running") return
|
|
343
|
+
const steps = state.steps
|
|
344
|
+
const last = steps[steps.length - 1]
|
|
345
|
+
// 进度覆盖上一条进度:否则一个每秒 push 一次的适配器将累积出数百条
|
|
346
|
+
// 只有最后一条有意义的记录
|
|
347
|
+
if (step.type === "progress" && last?.type === "progress") steps[steps.length - 1] = step
|
|
348
|
+
else steps.push(step)
|
|
349
|
+
// 超出上限丢最旧的:二维码可能是几十 KB 的 base64,不能无限累积
|
|
350
|
+
while (steps.length > MAX_STEPS) steps.shift()
|
|
351
|
+
state.updatedAt = Date.now()
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
ask: <T = string>(prompt: LoginPrompt): Promise<T> => this.#ask<T>(state, prompt)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 等待一次用户输入
|
|
360
|
+
*
|
|
361
|
+
* 与 `pipeline/prompt.ts` 同构:超时、会话取消、用户答复三个终止条件
|
|
362
|
+
* 都汇到一个只许生效一次的 `settle` 上。
|
|
363
|
+
* @param state 会话状态
|
|
364
|
+
* @param prompt 提问内容
|
|
365
|
+
* @returns 用户输入
|
|
366
|
+
* @throws 超时、被取消、或适配器并发提问时
|
|
367
|
+
*/
|
|
368
|
+
#ask<T>(state: SessionState, prompt: LoginPrompt): Promise<T> {
|
|
369
|
+
if (state.status !== "running") return Promise.reject(new Error("登录会话已结束,不能再提问"))
|
|
370
|
+
if (state.pending !== undefined) {
|
|
371
|
+
// 前端一次只渲染一个输入框,同时问两个问题必然有一个永远收不到答复
|
|
372
|
+
return Promise.reject(new Error("上一次提问还没得到答复:登录流程必须串行提问"))
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const ms = parseDuration(prompt.timeout, DEFAULT_LOGIN_PROMPT_TIMEOUT)
|
|
376
|
+
const seq = ++state.seq
|
|
377
|
+
|
|
378
|
+
return new Promise<T>((resolve, reject) => {
|
|
379
|
+
let done = false
|
|
380
|
+
let timer: NodeJS.Timeout | undefined
|
|
381
|
+
|
|
382
|
+
/** 统一收尾 */
|
|
383
|
+
const settle = (fn: () => void): void => {
|
|
384
|
+
if (done) return
|
|
385
|
+
done = true
|
|
386
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
387
|
+
state.pending = undefined
|
|
388
|
+
state.resolveAsk = undefined
|
|
389
|
+
state.rejectAsk = undefined
|
|
390
|
+
state.updatedAt = Date.now()
|
|
391
|
+
fn()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
state.pending = { seq, prompt, deadline: Date.now() + ms }
|
|
395
|
+
state.resolveAsk = (value: unknown): void => settle(() => resolve(value as T))
|
|
396
|
+
state.rejectAsk = (err: Error): void => settle(() => reject(err))
|
|
397
|
+
state.updatedAt = Date.now()
|
|
398
|
+
|
|
399
|
+
if (ms > 0) {
|
|
400
|
+
timer = setTimeout(() => {
|
|
401
|
+
settle(() => reject(new Error(`等待"${prompt.label}"超时`)))
|
|
402
|
+
}, ms)
|
|
403
|
+
timer.unref?.()
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 中止一个会话
|
|
410
|
+
* @param state 会话状态
|
|
411
|
+
* @param reason 原因
|
|
412
|
+
* @param status 终态
|
|
413
|
+
*/
|
|
414
|
+
#abort(state: SessionState, reason: string, status: LoginStatus): void {
|
|
415
|
+
if (state.status !== "running") return
|
|
416
|
+
// 先落终态再 abort:适配器的 catch 里可能回读 snapshot,看到的应该是终态
|
|
417
|
+
state.status = status
|
|
418
|
+
state.error = reason
|
|
419
|
+
state.updatedAt = Date.now()
|
|
420
|
+
state.rejectAsk?.(new Error(reason))
|
|
421
|
+
state.controller.abort()
|
|
422
|
+
this.#finish(state)
|
|
423
|
+
this.#logger.info(`登录会话 ${state.id} 结束:${reason}`)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* 登录成功:落账号
|
|
428
|
+
* @param state 会话状态
|
|
429
|
+
* @param config 登录产出的账号配置
|
|
430
|
+
*/
|
|
431
|
+
async #succeed(state: SessionState, config: Record<string, unknown>): Promise<void> {
|
|
432
|
+
if (state.status !== "running") return
|
|
433
|
+
try {
|
|
434
|
+
const record = await this.#createAccount(state.adapterId, config, state.label)
|
|
435
|
+
state.accountId = record.id
|
|
436
|
+
state.status = "done"
|
|
437
|
+
state.updatedAt = Date.now()
|
|
438
|
+
this.#finish(state)
|
|
439
|
+
this.#logger.info(`登录成功:会话 ${state.id} → 账号 ${record.id}`)
|
|
440
|
+
} catch (err) {
|
|
441
|
+
// 登录本身成功了但存不下来(磁盘满、配置校验不过),这仍然是失败:
|
|
442
|
+
// 让用户看到原因,而不是一个"成功了但列表里没有"的账号
|
|
443
|
+
this.#fail(state, err)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* 登录失败
|
|
449
|
+
* @param state 会话状态
|
|
450
|
+
* @param err 错误
|
|
451
|
+
*/
|
|
452
|
+
#fail(state: SessionState, err: unknown): void {
|
|
453
|
+
// 已被取消/超时的会话不要把状态改成 failed —— 用户点了取消,
|
|
454
|
+
// 适配器随后抛出的 AbortError 只是取消的回声
|
|
455
|
+
if (state.status !== "running") return
|
|
456
|
+
state.status = "failed"
|
|
457
|
+
state.error = errText(err)
|
|
458
|
+
state.updatedAt = Date.now()
|
|
459
|
+
state.controller.abort()
|
|
460
|
+
this.#finish(state)
|
|
461
|
+
this.#logger.warn(`登录失败:会话 ${state.id}:${state.error}`)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* 结束善后:停 TTL、安排自毁
|
|
466
|
+
* @param state 会话状态
|
|
467
|
+
*/
|
|
468
|
+
#finish(state: SessionState): void {
|
|
469
|
+
if (state.ttlTimer !== undefined) {
|
|
470
|
+
clearTimeout(state.ttlTimer)
|
|
471
|
+
state.ttlTimer = undefined
|
|
472
|
+
}
|
|
473
|
+
if (state.reapTimer !== undefined) return
|
|
474
|
+
// 保留一段时间供前端读取结果,之后自毁 —— 否则一个长期运行的实例将累积
|
|
475
|
+
// 无数条历史登录记录(每条都可能挂着一张二维码)
|
|
476
|
+
state.reapTimer = setTimeout(() => {
|
|
477
|
+
this.#sessions.delete(state.id)
|
|
478
|
+
}, RESULT_TTL)
|
|
479
|
+
state.reapTimer.unref?.()
|
|
480
|
+
}
|
|
481
|
+
}
|