@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,653 @@
|
|
|
1
|
+
import { backoffDelay } from "../util/defer.js";
|
|
2
|
+
import { DisposalRegistry } from "../util/dispose.js";
|
|
3
|
+
import { uuid } from "../util/id.js";
|
|
4
|
+
import { createBotFacade } from "./bots.js";
|
|
5
|
+
/** KV 里存账号记录的键前缀 */
|
|
6
|
+
const RECORD_PREFIX = "account:";
|
|
7
|
+
/** 重连退避参数:最长每 1 分钟一次,既可较快恢复又不至于对对端造成过载 */
|
|
8
|
+
const RECONNECT_BACKOFF = { baseDelay: 2_000, maxDelay: 60_000, factor: 2, jitter: 0.25 };
|
|
9
|
+
/** 每多少次失败落一条 warn(其余落 debug),避免日志刷屏 */
|
|
10
|
+
const WARN_EVERY = 10;
|
|
11
|
+
/**
|
|
12
|
+
* 取错误的可读描述
|
|
13
|
+
* @param err 任意抛出物
|
|
14
|
+
* @returns 描述文本
|
|
15
|
+
*/
|
|
16
|
+
function errText(err) {
|
|
17
|
+
return err instanceof Error ? err.message : String(err);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 账号管理器
|
|
21
|
+
*
|
|
22
|
+
* 既是 WebUI 的增删改查入口,也是 `AccountsView` 的实现(插件通过
|
|
23
|
+
* `ctx.app.accounts` 只读观察)。
|
|
24
|
+
*/
|
|
25
|
+
export class AccountManager {
|
|
26
|
+
/** 日志器 */
|
|
27
|
+
#logger;
|
|
28
|
+
/** KV 命名空间 */
|
|
29
|
+
#kv;
|
|
30
|
+
/** 适配器注册表 */
|
|
31
|
+
#adapters;
|
|
32
|
+
/** Bot 注册表 */
|
|
33
|
+
#bots;
|
|
34
|
+
/** 宿主工厂 */
|
|
35
|
+
#createHost;
|
|
36
|
+
/** 事件总线 */
|
|
37
|
+
#events;
|
|
38
|
+
/** 发送策略视图 */
|
|
39
|
+
#sendPolicy;
|
|
40
|
+
/** 账号 id → 运行时 */
|
|
41
|
+
#runtimes = new Map();
|
|
42
|
+
/** 适配器注册表的监听句柄,停机时摘除 */
|
|
43
|
+
#unhook;
|
|
44
|
+
/** 是否已停机 */
|
|
45
|
+
#stopped = false;
|
|
46
|
+
/**
|
|
47
|
+
* @param opts 构造参数
|
|
48
|
+
*/
|
|
49
|
+
constructor(opts) {
|
|
50
|
+
this.#logger = opts.logger.child({ scope: "account" });
|
|
51
|
+
this.#kv = opts.kv;
|
|
52
|
+
this.#adapters = opts.adapters;
|
|
53
|
+
this.#bots = opts.bots;
|
|
54
|
+
this.#createHost = opts.createHost;
|
|
55
|
+
this.#events = opts.events;
|
|
56
|
+
this.#sendPolicy = opts.sendPolicy;
|
|
57
|
+
// 适配器热插拔(文件头第 4 点):卸载即下线,重新注册即连回来。
|
|
58
|
+
// 在构造函数中自行装配,而不交由 kernel/runtime.ts 装配:遗漏的后果是
|
|
59
|
+
// "适配器插件更新之后账号不再自动恢复",这种缺失不报错、不留痕,
|
|
60
|
+
// 只会使人认为热重载不可用 —— 可以消除的装配步骤不应留给调用方。
|
|
61
|
+
this.#unhook = [
|
|
62
|
+
opts.adapters.onUnregister((id, entry) => this.detachAdapter(id, entry)),
|
|
63
|
+
opts.adapters.onRegister(id => this.onAdapterRegistered(id))
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
/** 账号总数 */
|
|
67
|
+
get size() {
|
|
68
|
+
return this.#runtimes.size;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 从 KV 载入全部账号记录(不连接)
|
|
72
|
+
*
|
|
73
|
+
* 与 `startAll()` 分开:内核 `load` 阶段就该知道有哪些账号(WebUI 要能显示
|
|
74
|
+
* 列表),但连接要等适配器插件全部注册完 —— 否则先加载的账号会因为
|
|
75
|
+
* "适配器未注册"白失败一次。
|
|
76
|
+
* @returns 载入的账号数
|
|
77
|
+
*/
|
|
78
|
+
async load() {
|
|
79
|
+
for await (const [key, value] of this.#kv.entries(RECORD_PREFIX)) {
|
|
80
|
+
const record = value;
|
|
81
|
+
// 手工编辑过 KV、或旧版本残留的坏记录:跳过而不是让整个内核起不来
|
|
82
|
+
if (record === undefined || typeof record.id !== "string" || typeof record.adapterId !== "string") {
|
|
83
|
+
this.#logger.warn(`跳过无法解析的账号记录 ${key}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
this.#runtimes.set(record.id, {
|
|
87
|
+
record,
|
|
88
|
+
status: record.enabled ? "offline" : "disabled",
|
|
89
|
+
error: undefined,
|
|
90
|
+
since: Date.now(),
|
|
91
|
+
retries: 0,
|
|
92
|
+
controller: undefined,
|
|
93
|
+
registry: undefined,
|
|
94
|
+
driver: undefined,
|
|
95
|
+
facade: undefined,
|
|
96
|
+
timer: undefined,
|
|
97
|
+
connecting: undefined,
|
|
98
|
+
closing: undefined,
|
|
99
|
+
removed: false
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
this.#logger.info(`载入 ${this.#runtimes.size} 个账号`);
|
|
103
|
+
return this.#runtimes.size;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 连接全部已启用的账号
|
|
107
|
+
*
|
|
108
|
+
* 并发连接:一个账号的对端没起来不该让后面的账号排队等它退避完。
|
|
109
|
+
* 单个账号失败只落日志(`connect` 内部已转成 error 状态 + 重连),
|
|
110
|
+
* 所以这里 `allSettled` 之后不需要检查结果。
|
|
111
|
+
*/
|
|
112
|
+
async startAll() {
|
|
113
|
+
const targets = [...this.#runtimes.values()].filter(rt => rt.record.enabled);
|
|
114
|
+
if (targets.length === 0) {
|
|
115
|
+
this.#logger.info("没有已启用的账号,等待在 WebUI 中添加");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
await Promise.allSettled(targets.map(rt => this.connect(rt.record.id)));
|
|
119
|
+
}
|
|
120
|
+
/* ─────────────────────────── AccountsView ─────────────────────────── */
|
|
121
|
+
/**
|
|
122
|
+
* 列出全部账号及其状态
|
|
123
|
+
* @returns 状态快照数组
|
|
124
|
+
*/
|
|
125
|
+
list() {
|
|
126
|
+
return [...this.#runtimes.values()].map(rt => this.#snapshot(rt));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 按记录 id 取账号状态
|
|
130
|
+
* @param id 账号记录 id
|
|
131
|
+
* @returns 状态快照;不存在时 undefined
|
|
132
|
+
*/
|
|
133
|
+
get(id) {
|
|
134
|
+
const rt = this.#runtimes.get(id);
|
|
135
|
+
return rt === undefined ? undefined : this.#snapshot(rt);
|
|
136
|
+
}
|
|
137
|
+
/* ─────────────────────────── 增删改 ─────────────────────────── */
|
|
138
|
+
/**
|
|
139
|
+
* 新建账号
|
|
140
|
+
*
|
|
141
|
+
* 配置先过 `validateAccount()` 再落盘:非法配置一旦存入,只会在下次启动时
|
|
142
|
+
* 表现为一条无从判断原因的连接失败。
|
|
143
|
+
* @param adapterId 适配器 id
|
|
144
|
+
* @param config 账号配置(WebUI 提交的原始对象)
|
|
145
|
+
* @param label 备注名
|
|
146
|
+
* @param enabled 是否立即启用并连接,缺省 true
|
|
147
|
+
* @returns 新建的记录
|
|
148
|
+
* @throws 适配器未注册或配置校验失败时
|
|
149
|
+
*/
|
|
150
|
+
async create(adapterId, config, label, enabled = true) {
|
|
151
|
+
const provider = this.#requireAdapter(adapterId);
|
|
152
|
+
const normalized = this.#validate(provider, config);
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
const record = {
|
|
155
|
+
id: uuid(),
|
|
156
|
+
adapterId,
|
|
157
|
+
enabled,
|
|
158
|
+
config: normalized,
|
|
159
|
+
createdAt: now,
|
|
160
|
+
updatedAt: now
|
|
161
|
+
};
|
|
162
|
+
if (label !== undefined && label !== "")
|
|
163
|
+
record.label = label;
|
|
164
|
+
await this.#persist(record);
|
|
165
|
+
this.#runtimes.set(record.id, {
|
|
166
|
+
record,
|
|
167
|
+
status: enabled ? "offline" : "disabled",
|
|
168
|
+
error: undefined,
|
|
169
|
+
since: now,
|
|
170
|
+
retries: 0,
|
|
171
|
+
controller: undefined,
|
|
172
|
+
registry: undefined,
|
|
173
|
+
driver: undefined,
|
|
174
|
+
facade: undefined,
|
|
175
|
+
timer: undefined,
|
|
176
|
+
connecting: undefined,
|
|
177
|
+
closing: undefined,
|
|
178
|
+
removed: false
|
|
179
|
+
});
|
|
180
|
+
this.#logger.info(`新增账号 ${this.#describe(record)}`);
|
|
181
|
+
if (enabled)
|
|
182
|
+
await this.connect(record.id);
|
|
183
|
+
return record;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 修改账号配置
|
|
187
|
+
*
|
|
188
|
+
* 改完必须重连:连接参数(地址、token)变了而 socket 还是旧的,
|
|
189
|
+
* 用户会以为"改了没生效"。
|
|
190
|
+
* @param id 账号记录 id
|
|
191
|
+
* @param patch 要改的字段
|
|
192
|
+
* @returns 更新后的记录
|
|
193
|
+
* @throws 账号不存在或配置校验失败时
|
|
194
|
+
*/
|
|
195
|
+
async update(id, patch) {
|
|
196
|
+
const rt = this.#require(id);
|
|
197
|
+
const provider = this.#requireAdapter(rt.record.adapterId);
|
|
198
|
+
const next = { ...rt.record, updatedAt: Date.now() };
|
|
199
|
+
if (patch.config !== undefined)
|
|
200
|
+
next.config = this.#validate(provider, patch.config);
|
|
201
|
+
if (patch.label !== undefined)
|
|
202
|
+
next.label = patch.label;
|
|
203
|
+
if (patch.enabled !== undefined)
|
|
204
|
+
next.enabled = patch.enabled;
|
|
205
|
+
await this.#persist(next);
|
|
206
|
+
rt.record = next;
|
|
207
|
+
await this.disconnect(id, "配置已修改");
|
|
208
|
+
if (next.enabled)
|
|
209
|
+
await this.connect(id);
|
|
210
|
+
else
|
|
211
|
+
this.#setStatus(rt, "disabled", undefined);
|
|
212
|
+
return next;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* 启用 / 禁用账号
|
|
216
|
+
* @param id 账号记录 id
|
|
217
|
+
* @param enabled 是否启用
|
|
218
|
+
* @returns 更新后的记录
|
|
219
|
+
* @throws 账号不存在时
|
|
220
|
+
*/
|
|
221
|
+
async setEnabled(id, enabled) {
|
|
222
|
+
return this.update(id, { enabled });
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 删除账号
|
|
226
|
+
* @param id 账号记录 id
|
|
227
|
+
* @returns 是否删掉了(不存在时 false)
|
|
228
|
+
*/
|
|
229
|
+
async remove(id) {
|
|
230
|
+
const rt = this.#runtimes.get(id);
|
|
231
|
+
if (rt === undefined)
|
|
232
|
+
return false;
|
|
233
|
+
// 先标记再断开:断开过程里到达的重连调度要能看到"这个账号已经没了"
|
|
234
|
+
rt.removed = true;
|
|
235
|
+
await this.disconnect(id, "账号已删除");
|
|
236
|
+
this.#runtimes.delete(id);
|
|
237
|
+
await this.#kv.del(`${RECORD_PREFIX}${id}`);
|
|
238
|
+
this.#logger.info(`删除账号 ${this.#describe(rt.record)}`);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
/* ─────────────────────────── 连接 / 断开 ─────────────────────────── */
|
|
242
|
+
/**
|
|
243
|
+
* 连接一个账号
|
|
244
|
+
*
|
|
245
|
+
* 幂等且防重入:已在线直接返回,正在连接则复用同一个 promise(文件头第 2 点)。
|
|
246
|
+
* 失败**不抛出异常** —— 账号无法连接属于常态(对端未启动),转为 `error` 状态并
|
|
247
|
+
* 安排重连即可;抛出异常只会使 WebUI 的批量操作在第一个账号处中断。
|
|
248
|
+
* @param id 账号记录 id
|
|
249
|
+
* @throws 账号不存在时
|
|
250
|
+
*/
|
|
251
|
+
async connect(id) {
|
|
252
|
+
const rt = this.#require(id);
|
|
253
|
+
if (this.#stopped)
|
|
254
|
+
return;
|
|
255
|
+
if (rt.status === "online" && rt.driver !== undefined)
|
|
256
|
+
return;
|
|
257
|
+
if (rt.connecting !== undefined)
|
|
258
|
+
return rt.connecting;
|
|
259
|
+
this.#clearTimer(rt);
|
|
260
|
+
const task = this.#doConnect(rt).finally(() => {
|
|
261
|
+
rt.connecting = undefined;
|
|
262
|
+
});
|
|
263
|
+
rt.connecting = task;
|
|
264
|
+
return task;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 断开一个账号
|
|
268
|
+
* @param id 账号记录 id
|
|
269
|
+
* @param reason 断开原因(落日志与 `bot/offline`)
|
|
270
|
+
*/
|
|
271
|
+
async disconnect(id, reason = "手动断开") {
|
|
272
|
+
const rt = this.#runtimes.get(id);
|
|
273
|
+
if (rt === undefined)
|
|
274
|
+
return;
|
|
275
|
+
this.#clearTimer(rt);
|
|
276
|
+
// **先 abort 再等待**。被动接入的适配器(反向 WS、HTTP 上报)的 `connect()`
|
|
277
|
+
// 需一直等到对端连入才返回,可能长达十余秒。先 await 即等于令停机被一个
|
|
278
|
+
// "尚未发生的连接"阻塞;abort 之后驱动会立即从等待中退出,该 await
|
|
279
|
+
// 仅用于收尾。顺序颠倒会使 `stop()` 阻塞至该等待超时为止。
|
|
280
|
+
rt.controller?.abort();
|
|
281
|
+
if (rt.connecting !== undefined)
|
|
282
|
+
await rt.connecting.catch(() => undefined);
|
|
283
|
+
await this.#teardown(rt, reason);
|
|
284
|
+
if (!rt.removed)
|
|
285
|
+
this.#setStatus(rt, rt.record.enabled ? "offline" : "disabled", undefined);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 重连一个账号
|
|
289
|
+
* @param id 账号记录 id
|
|
290
|
+
*/
|
|
291
|
+
async reconnect(id) {
|
|
292
|
+
await this.disconnect(id, "手动重连");
|
|
293
|
+
await this.connect(id);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* 适配器插件被卸载:同步让它名下的账号下线
|
|
297
|
+
*
|
|
298
|
+
* **同步部分**必须在本函数返回前完成(改状态 + abort + 从 Bot 注册表摘除),
|
|
299
|
+
* 因为调用方是同步的 `Disposer`。异步的 socket 关闭挂在 `rt.closing` 上,
|
|
300
|
+
* 由 `stop()` 兜底 await。见文件头第 4 点。
|
|
301
|
+
* @param adapterId 适配器 id
|
|
302
|
+
* @param entry 摘除的注册条目(仅用于日志)
|
|
303
|
+
*/
|
|
304
|
+
detachAdapter(adapterId, entry) {
|
|
305
|
+
const owner = entry?.owner;
|
|
306
|
+
const reason = owner === undefined ? `适配器 ${adapterId} 已卸载` : `适配器插件 ${owner} 已卸载`;
|
|
307
|
+
for (const rt of this.#runtimes.values()) {
|
|
308
|
+
if (rt.record.adapterId !== adapterId)
|
|
309
|
+
continue;
|
|
310
|
+
this.#clearTimer(rt);
|
|
311
|
+
// 同步切断事件投递:宿主的 signal 一 abort,submit() 立刻开始丢事件
|
|
312
|
+
rt.controller?.abort();
|
|
313
|
+
const facade = rt.facade;
|
|
314
|
+
if (facade !== undefined) {
|
|
315
|
+
// 同步让门面失效并摘出注册表:插件的 pickBot() 立刻拿不到它,
|
|
316
|
+
// 已经拿在手上的那个也会在下一次调用时明确报错
|
|
317
|
+
facade.close();
|
|
318
|
+
this.#bots.remove(rt.record.id);
|
|
319
|
+
if (rt.status === "online")
|
|
320
|
+
this.#events.emitDetached("bot/offline", facade, reason);
|
|
321
|
+
}
|
|
322
|
+
this.#setStatus(rt, rt.record.enabled ? "offline" : "disabled", reason);
|
|
323
|
+
// 异步收尾(关 socket、回收登记):不能 await,但要让 stop() 有机会等到
|
|
324
|
+
rt.closing = this.#teardown(rt, reason);
|
|
325
|
+
void rt.closing.catch(() => undefined);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 适配器插件重新注册:把它名下已启用的账号连回来
|
|
330
|
+
*
|
|
331
|
+
* 热重载的另一半。不 await:注册通知也发生在同步路径上。
|
|
332
|
+
* @param adapterId 适配器 id
|
|
333
|
+
*/
|
|
334
|
+
onAdapterRegistered(adapterId) {
|
|
335
|
+
if (this.#stopped)
|
|
336
|
+
return;
|
|
337
|
+
for (const rt of this.#runtimes.values()) {
|
|
338
|
+
if (rt.record.adapterId !== adapterId)
|
|
339
|
+
continue;
|
|
340
|
+
if (!rt.record.enabled || rt.status === "online" || rt.status === "connecting")
|
|
341
|
+
continue;
|
|
342
|
+
this.#logger.info(`适配器 ${adapterId} 已重新注册,正在重连账号 ${this.#describe(rt.record)}`);
|
|
343
|
+
void this.connect(rt.record.id).catch(() => undefined);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* 停机:断开全部账号
|
|
348
|
+
*
|
|
349
|
+
* 并发断开并等到全部收尾,包括 `detachAdapter` 留下的异步尾巴 ——
|
|
350
|
+
* 否则进程退出时会留下没关的 socket,NapCat 侧要等到 TCP 超时才发现。
|
|
351
|
+
*/
|
|
352
|
+
async stop() {
|
|
353
|
+
this.#stopped = true;
|
|
354
|
+
// 先摘监听:停机过程中若有插件正好在卸载,detachAdapter 再来一遍
|
|
355
|
+
// 只会和下面的 disconnect 抢同一个运行时
|
|
356
|
+
for (const off of this.#unhook)
|
|
357
|
+
off();
|
|
358
|
+
this.#unhook.length = 0;
|
|
359
|
+
const tasks = [];
|
|
360
|
+
for (const rt of this.#runtimes.values()) {
|
|
361
|
+
this.#clearTimer(rt);
|
|
362
|
+
if (rt.closing !== undefined)
|
|
363
|
+
tasks.push(rt.closing.catch(() => undefined));
|
|
364
|
+
tasks.push(this.disconnect(rt.record.id, "内核正在停止").catch(() => undefined));
|
|
365
|
+
}
|
|
366
|
+
await Promise.allSettled(tasks);
|
|
367
|
+
}
|
|
368
|
+
/* ─────────────────────────── 内部 ─────────────────────────── */
|
|
369
|
+
/**
|
|
370
|
+
* 真正的连接过程
|
|
371
|
+
* @param rt 账号运行时
|
|
372
|
+
*/
|
|
373
|
+
async #doConnect(rt) {
|
|
374
|
+
const record = rt.record;
|
|
375
|
+
const provider = this.#adapters.get(record.adapterId);
|
|
376
|
+
if (provider === undefined) {
|
|
377
|
+
// 不算失败到要重连:适配器插件没装,重试一万次也是一样的结果。
|
|
378
|
+
// 等 onAdapterRegistered 把它叫回来。
|
|
379
|
+
this.#setStatus(rt, "error", `适配器 ${record.adapterId} 未注册`);
|
|
380
|
+
this.#logger.warn(`账号 ${this.#describe(record)} 无法连接:适配器 ${record.adapterId} 未注册`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
this.#setStatus(rt, "connecting", undefined);
|
|
384
|
+
const controller = new AbortController();
|
|
385
|
+
const registry = new DisposalRegistry(`account:${record.id}`);
|
|
386
|
+
rt.controller = controller;
|
|
387
|
+
rt.registry = registry;
|
|
388
|
+
let handle;
|
|
389
|
+
// 提到 try 外面:`rt.driver` 要等连上之后才赋值,而 `connect()` 抛错恰恰是
|
|
390
|
+
// 最常见的失败路径,那时只有这个引用能把半成品驱动交给 #teardown 关掉
|
|
391
|
+
let driver;
|
|
392
|
+
try {
|
|
393
|
+
handle = this.#createHost({
|
|
394
|
+
record,
|
|
395
|
+
controller,
|
|
396
|
+
registry,
|
|
397
|
+
setStatus: (status, detail) => {
|
|
398
|
+
// 适配器自报状态(如"正在扫码")。它报 offline 时走完整的下线流程
|
|
399
|
+
// 而不是仅修改一个字段:socket 已不存在,Bot 注册表中的对应条目也必须摘除。
|
|
400
|
+
if (status === "offline" && rt.status === "online") {
|
|
401
|
+
void this.#onDriverOffline(rt, detail?.error ?? "适配器报告已下线");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
this.#setStatus(rt, status, detail?.error);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
driver = await provider.createBot(provider.validateAccount(record.config), handle.host);
|
|
408
|
+
if (controller.signal.aborted) {
|
|
409
|
+
// 连接期间账号被禁用/删除了:把刚建好的 driver 立刻收掉,
|
|
410
|
+
// 否则它会成为一个没人引用、还在收消息的幽灵
|
|
411
|
+
await driver.disconnect().catch(() => undefined);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
await driver.connect();
|
|
415
|
+
if (controller.signal.aborted) {
|
|
416
|
+
await driver.disconnect().catch(() => undefined);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const facade = createBotFacade({
|
|
420
|
+
driver,
|
|
421
|
+
accountId: record.id,
|
|
422
|
+
logger: this.#logger.child({ account: record.id }),
|
|
423
|
+
policy: this.#sendPolicy,
|
|
424
|
+
// 门面自己不认识总线(见 adapter/bots.ts 文件头),由此处代为转发。
|
|
425
|
+
// emitDetached:统计插件绝不该拖慢发送主链路
|
|
426
|
+
onSent: info => this.#events.emitDetached("message/sent", info)
|
|
427
|
+
});
|
|
428
|
+
rt.driver = driver;
|
|
429
|
+
rt.facade = facade;
|
|
430
|
+
this.#bots.add(facade);
|
|
431
|
+
// 绑在最后一步:宿主此前收到的事件一律丢弃(宿主文件头闸门二),
|
|
432
|
+
// 因为在门面就绪之前 e.reply() 无处可发
|
|
433
|
+
handle.attach(facade);
|
|
434
|
+
// 回填 selfId:WebUI 要显示"这个账号是哪个 QQ",而它只有连上才知道
|
|
435
|
+
if (driver.selfId !== "" && record.selfId !== driver.selfId) {
|
|
436
|
+
record.selfId = driver.selfId;
|
|
437
|
+
record.updatedAt = Date.now();
|
|
438
|
+
await this.#persist(record).catch((err) => {
|
|
439
|
+
this.#logger.warn(`回填账号 ${record.id} 的 selfId 失败:${errText(err)}`);
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
rt.retries = 0;
|
|
443
|
+
this.#setStatus(rt, "online", undefined);
|
|
444
|
+
this.#logger.info(`账号上线:${this.#describe(record)}(${driver.nickname || driver.selfId})`);
|
|
445
|
+
this.#events.emitDetached("bot/online", facade);
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
handle?.detach();
|
|
449
|
+
// 补登记再收拾:失败点在 `rt.driver = driver` 之前时(`connect()` 抛错就是),
|
|
450
|
+
// #teardown 看不到这个驱动,它带着的 socket 与定时器就没人关了 —— 而
|
|
451
|
+
// #scheduleReconnect 每次重试都新建一个驱动,漏一个就是永久漏一个
|
|
452
|
+
rt.driver ??= driver;
|
|
453
|
+
// **必须在 #teardown 之前读**:它自己会 abort 这个 controller,读晚了
|
|
454
|
+
// 就永远是 true,于是真正的连接失败也不再上报 error、也不再重连
|
|
455
|
+
const deliberate = controller.signal.aborted;
|
|
456
|
+
await this.#teardown(rt, "连接失败");
|
|
457
|
+
// 被 abort 打断的不算"连接失败",那是 disconnect/stop 主动要求停下来。
|
|
458
|
+
// 照常报 error 并排重连的话,`disconnect()` 里的 #clearTimer 早已跑过,
|
|
459
|
+
// 这个新排的定时器没人清,几秒后会把一个刚被停掉的账号重新连起来
|
|
460
|
+
if (deliberate)
|
|
461
|
+
return;
|
|
462
|
+
this.#setStatus(rt, "error", errText(err));
|
|
463
|
+
this.#scheduleReconnect(rt, err);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* 驱动侧断线:清理并安排重连
|
|
468
|
+
* @param rt 账号运行时
|
|
469
|
+
* @param reason 原因
|
|
470
|
+
*/
|
|
471
|
+
async #onDriverOffline(rt, reason) {
|
|
472
|
+
if (rt.status !== "online" && rt.status !== "connecting")
|
|
473
|
+
return;
|
|
474
|
+
await this.#teardown(rt, reason);
|
|
475
|
+
if (rt.removed || this.#stopped)
|
|
476
|
+
return;
|
|
477
|
+
this.#setStatus(rt, rt.record.enabled ? "offline" : "disabled", reason);
|
|
478
|
+
if (rt.record.enabled)
|
|
479
|
+
this.#scheduleReconnect(rt, new Error(reason));
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* 收拾一次连接留下的一切
|
|
483
|
+
*
|
|
484
|
+
* 顺序是刻意的:abort(停止投递)→ 摘 Bot 注册表(`pickBot` 拿不到它)→
|
|
485
|
+
* 回收登记(路由、缓存、定时器)→ 关 socket。反过来会有"已断开却还在
|
|
486
|
+
* 处理事件"的窗口。
|
|
487
|
+
* @param rt 账号运行时
|
|
488
|
+
* @param reason 原因
|
|
489
|
+
*/
|
|
490
|
+
async #teardown(rt, reason) {
|
|
491
|
+
const driver = rt.driver;
|
|
492
|
+
const facade = rt.facade;
|
|
493
|
+
const wasOnline = rt.status === "online";
|
|
494
|
+
rt.controller?.abort();
|
|
495
|
+
rt.controller = undefined;
|
|
496
|
+
rt.driver = undefined;
|
|
497
|
+
rt.facade = undefined;
|
|
498
|
+
if (facade !== undefined) {
|
|
499
|
+
// 幂等:detachAdapter 可能已经 close 过了
|
|
500
|
+
facade.close();
|
|
501
|
+
this.#bots.remove(rt.record.id);
|
|
502
|
+
}
|
|
503
|
+
const registry = rt.registry;
|
|
504
|
+
rt.registry = undefined;
|
|
505
|
+
if (registry !== undefined) {
|
|
506
|
+
const failures = registry.dispose();
|
|
507
|
+
for (const f of failures) {
|
|
508
|
+
this.#logger.warn(`回收账号 ${rt.record.id} 的 ${f.label} 时出错:${errText(f.error)}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (driver !== undefined) {
|
|
512
|
+
try {
|
|
513
|
+
await driver.disconnect();
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
// 断开失败没有补救手段,落日志即可 —— 进程退出时 socket 总会关掉
|
|
517
|
+
this.#logger.warn(`断开账号 ${rt.record.id} 时出错:${errText(err)}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (wasOnline) {
|
|
521
|
+
this.#logger.info(`账号下线:${this.#describe(rt.record)}(${reason})`);
|
|
522
|
+
if (facade !== undefined)
|
|
523
|
+
this.#events.emitDetached("bot/offline", facade, reason);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* 安排一次重连
|
|
528
|
+
* @param rt 账号运行时
|
|
529
|
+
* @param err 触发重连的错误
|
|
530
|
+
*/
|
|
531
|
+
#scheduleReconnect(rt, err) {
|
|
532
|
+
if (this.#stopped || rt.removed || !rt.record.enabled)
|
|
533
|
+
return;
|
|
534
|
+
this.#clearTimer(rt);
|
|
535
|
+
rt.retries++;
|
|
536
|
+
const delay = backoffDelay(rt.retries, RECONNECT_BACKOFF);
|
|
537
|
+
const text = `账号 ${this.#describe(rt.record)} 连接失败(第 ${rt.retries} 次):${errText(err)};${Math.round(delay / 1000)} 秒后重试`;
|
|
538
|
+
// 对端长期不在线时不刷屏:首次与每 10 次一条 warn,其余降级到 debug
|
|
539
|
+
if (rt.retries === 1 || rt.retries % WARN_EVERY === 0)
|
|
540
|
+
this.#logger.warn(text);
|
|
541
|
+
else
|
|
542
|
+
this.#logger.debug(text);
|
|
543
|
+
rt.timer = setTimeout(() => {
|
|
544
|
+
rt.timer = undefined;
|
|
545
|
+
void this.connect(rt.record.id).catch(() => undefined);
|
|
546
|
+
}, delay);
|
|
547
|
+
rt.timer.unref?.();
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* 清掉待触发的重连
|
|
551
|
+
* @param rt 账号运行时
|
|
552
|
+
*/
|
|
553
|
+
#clearTimer(rt) {
|
|
554
|
+
if (rt.timer === undefined)
|
|
555
|
+
return;
|
|
556
|
+
clearTimeout(rt.timer);
|
|
557
|
+
rt.timer = undefined;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* 落一次状态变化
|
|
561
|
+
* @param rt 账号运行时
|
|
562
|
+
* @param status 新状态
|
|
563
|
+
* @param error 错误信息
|
|
564
|
+
*/
|
|
565
|
+
#setStatus(rt, status, error) {
|
|
566
|
+
if (rt.status === status && rt.error === error)
|
|
567
|
+
return;
|
|
568
|
+
rt.status = status;
|
|
569
|
+
rt.error = error;
|
|
570
|
+
rt.since = Date.now();
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* 生成状态快照
|
|
574
|
+
* @param rt 账号运行时
|
|
575
|
+
* @returns 快照
|
|
576
|
+
*/
|
|
577
|
+
#snapshot(rt) {
|
|
578
|
+
// 复制记录:WebUI 拿到的对象不该是内部状态的别名,改一下就悄悄改了内核
|
|
579
|
+
const state = {
|
|
580
|
+
record: { ...rt.record, config: { ...rt.record.config } },
|
|
581
|
+
status: rt.status,
|
|
582
|
+
since: rt.since,
|
|
583
|
+
retries: rt.retries
|
|
584
|
+
};
|
|
585
|
+
if (rt.error !== undefined)
|
|
586
|
+
state.error = rt.error;
|
|
587
|
+
const nickname = rt.facade?.nickname;
|
|
588
|
+
if (nickname !== undefined && nickname !== "")
|
|
589
|
+
state.nickname = nickname;
|
|
590
|
+
return state;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* 写一条记录进 KV
|
|
594
|
+
* @param record 账号记录
|
|
595
|
+
*/
|
|
596
|
+
async #persist(record) {
|
|
597
|
+
await this.#kv.set(`${RECORD_PREFIX}${record.id}`, record);
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* 取运行时,不存在即抛
|
|
601
|
+
* @param id 账号记录 id
|
|
602
|
+
* @returns 运行时
|
|
603
|
+
* @throws 账号不存在时
|
|
604
|
+
*/
|
|
605
|
+
#require(id) {
|
|
606
|
+
const rt = this.#runtimes.get(id);
|
|
607
|
+
if (rt === undefined)
|
|
608
|
+
throw new Error(`账号 ${id} 不存在`);
|
|
609
|
+
return rt;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* 取适配器,未注册即抛
|
|
613
|
+
* @param adapterId 适配器 id
|
|
614
|
+
* @returns 适配器
|
|
615
|
+
* @throws 未注册时
|
|
616
|
+
*/
|
|
617
|
+
#requireAdapter(adapterId) {
|
|
618
|
+
const provider = this.#adapters.get(adapterId);
|
|
619
|
+
if (provider === undefined) {
|
|
620
|
+
const available = this.#adapters
|
|
621
|
+
.list()
|
|
622
|
+
.map(a => a.id)
|
|
623
|
+
.join("、");
|
|
624
|
+
throw new Error(`适配器 ${adapterId} 未注册${available === "" ? "" : `,可用:${available}`}`);
|
|
625
|
+
}
|
|
626
|
+
return provider;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* 走一遍适配器的配置校验
|
|
630
|
+
* @param provider 适配器
|
|
631
|
+
* @param config 原始配置
|
|
632
|
+
* @returns 规范化后的配置
|
|
633
|
+
* @throws 校验不通过时(错误信息会展示给用户)
|
|
634
|
+
*/
|
|
635
|
+
#validate(provider, config) {
|
|
636
|
+
try {
|
|
637
|
+
return provider.validateAccount(config);
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
throw new Error(`账号配置不符合适配器 ${provider.name} 的要求:${errText(err)}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* 账号的可读描述
|
|
645
|
+
* @param record 账号记录
|
|
646
|
+
* @returns 描述文本
|
|
647
|
+
*/
|
|
648
|
+
#describe(record) {
|
|
649
|
+
const who = record.label ?? record.selfId ?? record.id.slice(0, 8);
|
|
650
|
+
return `${who}@${record.adapterId}`;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
//# sourceMappingURL=accounts.js.map
|