Move/rename the transport/bridge code into a new @snowluma/channel layout and extract hook implementations into channel-specific packages. Many bridge sources were deleted and equivalent channel files added (adapter/ctx/interface/channel/hook/socket pieces moved/renamed), along with tests and configs relocated under packages/channel. RoadMap.md replaced with a detailed v3 architecture plan describing Channel/ChannelCtx/Core/CoreCtx/Hub boundaries and migration steps. Also apply related core package updates (account/core reshaping and package.json adjustments) to match the new architecture.
492 lines
20 KiB
Markdown
492 lines
20 KiB
Markdown
# SnowLuma v3 — 目标架构
|
||
|
||
> v2 已完成:UID/UIN Service(SQLite 持久化)、tsx+map Debug、包拆分、proto-defs 剥离、消息 hot-path 优化(17.5μs → 4.7μs)、NapCat 风格 Api 层、Bridge/Account 分层(v2 发布在 `@snowluma/bridge`,v3 重命名为 `@snowluma/channel`)。
|
||
|
||
---
|
||
|
||
## 0. 数据流总览(一图概览所有权)
|
||
|
||
```
|
||
┌─ @snowluma/channel-hook ─────────┐ ┌─ @snowluma/channel-socket ──────┐
|
||
│ HookManager + HookChannel │ │ SocketChannel (future) │
|
||
│ — 仅 send/recv + pid/hook state — │ │ — 仅 send/recv + socket state — │
|
||
└────────────┬─────────────────────┘ └─────────────┬───────────────────┘
|
||
│ Channel 实例 │
|
||
└───────────┬──────────────────────────┘
|
||
│ 通过 ChannelAdapter 注册
|
||
▼
|
||
┌─ @snowluma/core ──── Hub(主入口 / 持有一切)──────────────┐
|
||
│ │
|
||
│ 内部:Map<uin, Core> │
|
||
│ │
|
||
│ channel available ──► ctx = capability(channel) │
|
||
│ ──► core = new Core(ctx) │
|
||
│ ──► emit 'core-online' { uin, apis, events, ... } │
|
||
│ │
|
||
│ channel gone ──► core.dispose() + emit 'core-offline' { uin } │
|
||
└─────────────────────────────────────┬─────────────────────────────────┘
|
||
│ 顶层 lifecycle 事件(仅 2 个)
|
||
▼
|
||
┌─ @snowluma/onebot ──────────────────────────────────────────────────────┐
|
||
│ hub.events.on('core-online', ctx => { │
|
||
│ this.byUin.set(ctx.uin, ctx); │
|
||
│ ctx.events.on('group_message', e => …); // 每账号 bus │
|
||
│ }); │
|
||
│ // 处理 OneBot action: │
|
||
│ this.byUin.get(uin)?.apis.message.sendGroup(...) │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**核心约定**:
|
||
- `Channel` / adapter 包 **只**做 send/recv + 自己的内部状态(pid set / hook session / socket fd)。完全无业务。
|
||
- `Hub` 是**主入口**,独占持有 Channels + Cores。`Core` 实例由它构造,外部拿不到 Core 类本身。
|
||
- `Core` 通过 **ctx**(capability,一个 POJO)拿到 send/recv,从不直接持有 Channel。
|
||
- 外部消费者(OneBot / WebUI)只拿 `CoreCtx`(Core 的对外投影:`{ uin, apis, events, identity }`),不知道 Core 类的存在。
|
||
- 每个账号一个 `events`(在 Core 上),不做全局 bus;Hub 只 emit 两个顶层生命周期事件。
|
||
|
||
---
|
||
|
||
## 1. 目标包结构
|
||
|
||
```
|
||
SnowLuma/
|
||
├── packages/
|
||
│ │
|
||
│ │ ── Infrastructure(无业务) ──
|
||
│ ├── common/ logger / packet-sender / protocol-types / async-context
|
||
│ ├── proto-defs/ protobuf TS 接口定义
|
||
│ ├── proton/ protobuf codec 编译时生成器
|
||
│ ├── runtime/ native .node 产物清单
|
||
│ ├── ui/ Tailwind v4 + shadcn 组件(前端)
|
||
│ │
|
||
│ │ ── Protocol(解析层,纯函数) ──
|
||
│ ├── protocol/ events / identity-service / msg-push / oidb / highway /
|
||
│ │ element-builder / packet-pipeline / event-bus
|
||
│ │
|
||
│ │ ── Channel & adapters(transport 端口与实现) ──
|
||
│ ├── channel/ ⭐ Channel 接口 + 抽象类 + ChannelAdapter 端口
|
||
│ │ —— 只定义接口和最小抽象,**不**含 Hub
|
||
│ ├── channel-hook/ ⭐ HookManager + HookChannel + HookAdapter
|
||
│ │ —— 现 @snowluma/channel 大部分内容搬来
|
||
│ ├── channel-socket/ ⭐ 未来:SocketChannel + SocketAdapter
|
||
│ │
|
||
│ │ ── Core(主入口 + 业务封装) ──
|
||
│ ├── core/ ⭐ Core + Hub + CoreCtx + ApiHub + 协议封装
|
||
│ │ —— Hub 在这里(避免 channel↔core 循环)
|
||
│ │
|
||
│ │ ── Inbound(外部协议消费 CoreCtx) ──
|
||
│ ├── onebot/ OneBot v11 (HTTP + WS)
|
||
│ ├── websocket/ 底层 WS server(被 onebot / webui-backend 共用)
|
||
│ ├── webui-backend/ ⭐ Hono server + REST/WS(拆自 webui)
|
||
│ ├── webui-frontend/ ⭐ Vite SPA(拆自 webui)
|
||
│ └── sdk/ TS SDK
|
||
│
|
||
├── package.json
|
||
└── pnpm-workspace.yaml
|
||
```
|
||
|
||
⭐ 标记的是 v3 改动。
|
||
|
||
### 1.1 关键变动
|
||
|
||
| 当前 | 目标 | 备注 |
|
||
|---|---|---|
|
||
| `@snowluma/bridge`(hook + transport + adapter + manager) | `@snowluma/channel`(仅接口 + abstract Channel)+ `@snowluma/channel-hook`(hook 实现) | 包名 v2 叫 bridge,v3 重命名;hook 全套迁出独立包 |
|
||
| `@snowluma/core`(含 Account + AccountManager) | `core`:Core + Hub | Account → Core;AccountManager 折进 Hub |
|
||
| —— | `channel-socket` | 新建 stub |
|
||
| `@snowluma/webui` | `webui-backend` + `webui-frontend` | 前后端拆分 |
|
||
|
||
### 1.2 依赖 DAG(无环)
|
||
|
||
```
|
||
common ◄── proton, proto-defs, protocol, runtime, ui, channel
|
||
proto-defs ◄── protocol
|
||
proton ◄── protocol
|
||
protocol ◄── core
|
||
channel ◄── channel-hook, channel-socket, core
|
||
channel-hook ◄── core
|
||
channel-socket ◄── core
|
||
core ◄── onebot, webui-backend
|
||
websocket ◄── onebot, webui-backend
|
||
ui ◄── webui-frontend
|
||
```
|
||
|
||
**核心要点**:`core` 是唯一一处同时知道 `channel` 端口 + 业务实现的地方。Channel 包不反向依赖 core,因此 Hub **住在 core**,而不是 channel —— 名字保留 `Hub` 是因为它语义上仍是「channel 集群的指挥官」。
|
||
|
||
---
|
||
|
||
## 2. 三个关键类型 / 接口(设计核心)
|
||
|
||
整个系统由 **3 个类型** 串起来。每个职责单一、边界清晰。
|
||
|
||
### 2.1 `Channel`(在 `@snowluma/channel`)—— 纯 transport
|
||
|
||
```ts
|
||
// packages/channel/src/channel.ts
|
||
export type ChannelKind = 'inject' | 'socket';
|
||
|
||
export interface Channel {
|
||
readonly kind: ChannelKind;
|
||
readonly uin: string;
|
||
readonly id: string; // adapter:uin 形式
|
||
|
||
sendRawPacket(cmd: string, body: Uint8Array): Promise<SendPacketResult>;
|
||
setPacketHandler(h: ((pkt: PacketInfo) => void) | null): void;
|
||
deliverPacket(pkt: PacketInfo): void; // adapter 内部用
|
||
dispose(): void;
|
||
}
|
||
```
|
||
|
||
Adapter 包(`channel-hook` / `channel-socket`)的具体类(`HookChannel` / `SocketChannel`)实现 `Channel`。它们的内部状态(PID set、hook session、socket fd)对外不可见。
|
||
|
||
### 2.2 `ChannelCtx`(在 `@snowluma/channel`)—— channel → core 的能力 POJO
|
||
|
||
```ts
|
||
// packages/channel/src/channel-ctx.ts
|
||
export interface ChannelCtx {
|
||
readonly uin: string;
|
||
readonly kind: ChannelKind;
|
||
readonly sendRawPacket: (cmd: string, body: Uint8Array) => Promise<SendPacketResult>;
|
||
readonly onPacket: (h: (pkt: PacketInfo) => void) => void;
|
||
readonly dispose: () => void;
|
||
}
|
||
|
||
// Hub 用 makeCtx(channel) 把 Channel 投影成 ChannelCtx:
|
||
export function makeChannelCtx(b: Channel): ChannelCtx {
|
||
return {
|
||
uin: b.uin, kind: b.kind,
|
||
sendRawPacket: (c, x) => b.sendRawPacket(c, x),
|
||
onPacket: (h) => b.setPacketHandler(h),
|
||
dispose: () => b.dispose(),
|
||
};
|
||
}
|
||
```
|
||
|
||
**关键设计**:`Core` 只接收 `ChannelCtx`(capability POJO),**不**持有 `Channel` 类引用。这样 `core` 包不依赖任何 adapter 实现细节,连 Channel 抽象类都不用 import。
|
||
|
||
### 2.3 `CoreCtx`(在 `@snowluma/core`)—— core → onebot 的能力 POJO
|
||
|
||
```ts
|
||
// packages/core/src/core-ctx.ts
|
||
export interface CoreCtx {
|
||
readonly uin: string;
|
||
readonly kind: ChannelKind;
|
||
readonly apis: ApiHub; // 所有 typed api(message/contacts/...)
|
||
readonly events: EventBus; // 该账号的事件(per-uin)
|
||
readonly identity: IdentityService; // UID/UIN/group roster
|
||
}
|
||
```
|
||
|
||
**关键设计**:OneBot 拿到的就是这个 POJO 投影,**不**持有 `Core` 类引用。Hub 在 `core-online` 事件里把 `CoreCtx` 透出去;OneBot 按 uin 存起来用。
|
||
|
||
---
|
||
|
||
## 3. `Hub` —— 主入口完整实现
|
||
|
||
所有所有权、生命周期、装配都在这里。**外部 main.ts 只跟它打交道。**
|
||
|
||
```ts
|
||
// packages/core/src/hub.ts
|
||
import { Channel, ChannelAdapter, ChannelAdapterHost, makeChannelCtx } from '@snowluma/channel';
|
||
import { Core } from './core';
|
||
import type { CoreCtx } from './core-ctx';
|
||
import { TypedEmitter } from '@snowluma/common/typed-emitter';
|
||
|
||
type Lifecycle = {
|
||
'core-online': CoreCtx;
|
||
'core-offline': { uin: string };
|
||
};
|
||
|
||
export class Hub {
|
||
private readonly cores = new Map<string, Core>();
|
||
private readonly adapters: ChannelAdapter[] = [];
|
||
readonly events = new TypedEmitter<Lifecycle>();
|
||
|
||
registerAdapter(a: ChannelAdapter): void {
|
||
this.adapters.push(a);
|
||
}
|
||
|
||
async start(): Promise<void> {
|
||
const host: ChannelAdapterHost = {
|
||
onChannelReady: (b) => this.onChannelReady(b),
|
||
onChannelGone: (uin) => this.onChannelGone(uin),
|
||
};
|
||
await Promise.all(this.adapters.map((a) => a.start(host)));
|
||
}
|
||
|
||
async stop(): Promise<void> {
|
||
await Promise.all(this.adapters.map((a) => a.stop()));
|
||
for (const c of this.cores.values()) c.dispose();
|
||
this.cores.clear();
|
||
}
|
||
|
||
// ─── private ───────────────────────────────
|
||
|
||
private onChannelReady(channel: Channel): void {
|
||
const stale = this.cores.get(channel.uin);
|
||
if (stale) {
|
||
stale.dispose();
|
||
this.events.emit('core-offline', { uin: channel.uin });
|
||
}
|
||
const core = new Core(makeChannelCtx(channel));
|
||
this.cores.set(channel.uin, core);
|
||
this.events.emit('core-online', core.publicCtx);
|
||
}
|
||
|
||
private onChannelGone(uin: string): void {
|
||
const core = this.cores.get(uin);
|
||
if (!core) return;
|
||
core.dispose();
|
||
this.cores.delete(uin);
|
||
this.events.emit('core-offline', { uin });
|
||
}
|
||
}
|
||
```
|
||
|
||
## 4. `Core` 实现要点
|
||
|
||
```ts
|
||
// packages/core/src/core.ts
|
||
import type { ChannelCtx } from '@snowluma/channel';
|
||
import { EventBus } from '@snowluma/protocol/event-bus';
|
||
import { IdentityService } from '@snowluma/protocol/identity-service';
|
||
import { IncomingPacketPipeline } from '@snowluma/protocol/packet-pipeline';
|
||
import { buildApiHub, type ApiHub } from './apis';
|
||
import type { CoreCtx } from './core-ctx';
|
||
|
||
export class Core {
|
||
readonly events = new EventBus(); // ← 每账号一个 bus
|
||
readonly identity: IdentityService;
|
||
readonly apis: ApiHub;
|
||
private readonly pipeline: IncomingPacketPipeline;
|
||
|
||
constructor(private readonly channelCtx: ChannelCtx) {
|
||
this.identity = IdentityService.openForUin(channelCtx.uin);
|
||
this.pipeline = new IncomingPacketPipeline(this.events, this.identity);
|
||
this.apis = buildApiHub(this); // apis 内部持 Core 引用,调 sendRawPacket
|
||
channelCtx.onPacket((pkt) => this.pipeline.process(pkt));
|
||
}
|
||
|
||
// 给外部消费者的投影(只读 POJO,不暴露 Core 实例)
|
||
get publicCtx(): CoreCtx {
|
||
return {
|
||
uin: this.channelCtx.uin,
|
||
kind: this.channelCtx.kind,
|
||
apis: this.apis,
|
||
events: this.events,
|
||
identity: this.identity,
|
||
};
|
||
}
|
||
|
||
// apis 内部通过 core 调发包
|
||
sendRawPacket(cmd: string, body: Uint8Array) {
|
||
return this.channelCtx.sendRawPacket(cmd, body);
|
||
}
|
||
|
||
dispose(): void {
|
||
this.channelCtx.dispose();
|
||
this.identity.close();
|
||
}
|
||
}
|
||
```
|
||
|
||
## 5. OneBot 消费 `CoreCtx`
|
||
|
||
```ts
|
||
// packages/onebot/src/manager.ts
|
||
import type { Hub, CoreCtx } from '@snowluma/core';
|
||
|
||
export class OneBotManager {
|
||
private readonly byUin = new Map<string, CoreCtx>();
|
||
|
||
bind(hub: Hub): void {
|
||
hub.events.on('core-online', (ctx) => this.attach(ctx));
|
||
hub.events.on('core-offline', ({ uin }) => this.detach(uin));
|
||
}
|
||
|
||
private attach(ctx: CoreCtx): void {
|
||
this.byUin.set(ctx.uin, ctx);
|
||
ctx.events.on('group_message', (e) => this.dispatchGroupMessage(ctx, e));
|
||
ctx.events.on('private_message',(e) => this.dispatchPrivateMessage(ctx, e));
|
||
// ... 其它 kind
|
||
}
|
||
|
||
private detach(uin: string): void {
|
||
this.byUin.delete(uin);
|
||
// ctx.events 随 Core dispose 一起释放
|
||
}
|
||
|
||
// 处理 OneBot HTTP/WS action:用 uin 查到 ctx,直接调 apis
|
||
async handleSendGroupMsg(uin: string, groupId: number, msg: OneBotMsg) {
|
||
const ctx = this.byUin.get(uin);
|
||
if (!ctx) throw new Error(`unknown uin: ${uin}`);
|
||
return ctx.apis.message.sendGroup(groupId, msg); // ← 直接调封装好的 api
|
||
}
|
||
}
|
||
```
|
||
|
||
## 6. 主入口(`packages/core/src/index.ts`)
|
||
|
||
```ts
|
||
import { HookAdapter } from '@snowluma/channel-hook';
|
||
import { OneBotManager } from '@snowluma/onebot';
|
||
import { Hub } from './hub';
|
||
|
||
const hub = new Hub();
|
||
hub.registerAdapter(new HookAdapter({ autoLoadOnDiscovery: true }));
|
||
// hub.registerAdapter(new SocketAdapter()); // 未来
|
||
|
||
const onebot = new OneBotManager();
|
||
onebot.bind(hub);
|
||
|
||
await hub.start();
|
||
await initWebUI(port, onebot, /* hookManager from adapter */);
|
||
```
|
||
|
||
9 行装配,每行都是有意义的所有权声明。
|
||
|
||
---
|
||
|
||
## 7. 为什么这样设计优雅
|
||
|
||
### 7.1 单向数据流
|
||
|
||
```
|
||
adapter → Channel → ChannelCtx → Core → CoreCtx → OneBot
|
||
transport capability capability
|
||
```
|
||
|
||
**没有反向引用**:Core 不知道 Channel 类,OneBot 不知道 Core 类。每层只看到「下一层的能力 POJO」。
|
||
|
||
### 7.2 单一所有者
|
||
|
||
`Hub.cores: Map<uin, Core>` 是 **唯一** 持有 Core 的地方。OneBot 只持 `CoreCtx`(投影,不算所有权)。Core 销毁 = 从这个 Map 里删 = 所有 ctx 同时失效。无需引用计数、无需 GC 协调。
|
||
|
||
### 7.3 每账号独立 EventBus
|
||
|
||
Core 自带 `events: EventBus`,互不干扰。OneBot 通过 `core-online` 拿到 ctx 后立即 subscribe,`core-offline` 时 Core 已 dispose,bus 自然失效,不会泄漏。**没有全局 bus**,没有「哪个账号的事件」问题。
|
||
|
||
### 7.4 完全可测
|
||
|
||
- 测 Channel:mock `setPacketHandler` 回调(已在 `account-private-routing.test.ts` 用 `FakeChannel` 验证)
|
||
- 测 Core:传 `ChannelCtx` POJO 即可,无需真 Channel
|
||
- 测 OneBot:emit 一个 `core-online` + 假 `CoreCtx`
|
||
- 测 Hub:用 fake `ChannelAdapter`
|
||
|
||
每层都能独立测试。
|
||
|
||
### 7.5 内部还要避免 ctx 漫天的两个小招
|
||
|
||
**(a) Api 类捕获 deps(已做)**
|
||
|
||
```ts
|
||
class MessageApi {
|
||
constructor(private readonly core: Core) {}
|
||
// 方法签名只有业务参数:
|
||
async sendGroup(groupId: number, msg: Msg) { this.core.sendRawPacket(...) }
|
||
}
|
||
```
|
||
|
||
**(b) AsyncLocalStorage 处理日志 / trace(建议引入)**
|
||
|
||
```ts
|
||
// packages/common/src/async-context.ts
|
||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||
const als = new AsyncLocalStorage<{ uin?: string; trace?: string }>();
|
||
|
||
export const runInContext = <T>(c: { uin?: string; trace?: string }, fn: () => T) =>
|
||
als.run(c, fn);
|
||
export const ctx = () => als.getStore() ?? {};
|
||
|
||
// logger 自动 prefix [uin],业务代码不用接 log 参数
|
||
```
|
||
|
||
在 `Core.constructor` 给 `onPacket` 包一层:
|
||
```ts
|
||
channelCtx.onPacket((pkt) => {
|
||
runInContext({ uin: channelCtx.uin, trace: `pkt:${pkt.seqId}` }, () => {
|
||
this.pipeline.process(pkt);
|
||
});
|
||
});
|
||
```
|
||
|
||
所有下游 `log.info()` 自动带 `[10001]` 前缀,不污染业务签名。开销 ~50ns/call。
|
||
|
||
---
|
||
|
||
## 8. 不要的设计(保持简单)
|
||
|
||
| 方案 | 不做的原因 |
|
||
|---|---|
|
||
| **`CoreFactory` 抽象** | Core 只一种,多此一举。直接 `new Core(ctx)` |
|
||
| **全局 EventBus** | 每账号自己的 bus 更清晰,OneBot 按 uin 订阅,不混杂 |
|
||
| **`AccountManager` 独立层** | 多余的中间层,折进 `Hub` |
|
||
| **`InboundAdapter` 抽象** | OneBot / WebUI 数量有限且固定,直接 `bm.events.on(...)` 即可,不抽插件层 |
|
||
| **DI 容器(tsyringe 等)** | 9 行手动装配已经清晰,DI 引入反射/装饰器元数据反而难调 |
|
||
| **Effect monad / Reader monad** | TS 缺 do-notation,类型噪音 > 收益 |
|
||
| **CQRS / Event-sourced state** | 当前状态量级(10 账号 × 100 群)远未到 |
|
||
|
||
---
|
||
|
||
## 9. 迁移路线(每步独立可 ship)
|
||
|
||
> typecheck + 全测试通过才进下一步。
|
||
|
||
**Step 1** —— ⭐ 重命名 + 拆现有的 `@snowluma/bridge`
|
||
- `packages/bridge/` 重命名为 `packages/channel/`;package.json name 改为 `@snowluma/channel`
|
||
- 新建 `packages/channel-hook/`,从 `@snowluma/channel` 搬:`hook-manager.ts` / `hook-session.ts` / `hook-packet-client.ts` / `injector.ts` / `pipe-watcher.ts` / `qq-hook-client.ts` / `qq-port-probe.ts` / `hook-channel.ts`(原 inject-bridge.ts)/ `hook-adapter.ts`(原 inject-adapter.ts)
|
||
- `@snowluma/channel` 留:`channel.ts`(abstract,原 bridge.ts)/ `channel-interface.ts` / `channel-ctx.ts`(新)/ `adapter.ts`(端口)+ `makeChannelCtx` 工具
|
||
- 删除:`manager.ts`(原 BridgeManager)(搬到 core/hub.ts)/ `socket-channel.ts` + `socket-adapter.ts`(搬到 `channel-socket` stub)
|
||
|
||
**Step 2** —— ⭐ 重塑 `@snowluma/core`
|
||
- 删除:`account/account.ts` / `account-interface.ts` / `account-context.ts` / `account-manager.ts`
|
||
- 新建:`core.ts`(吸收 Account 业务,构造接 `ChannelCtx`)/ `core-ctx.ts` / `hub.ts`
|
||
- `apis/` 内部 `AccountContext` → `Core`(直接拿 Core 类型)
|
||
- `index.ts` 9 行装配(见 §6)
|
||
|
||
**Step 3** —— OneBot 适配 CoreCtx
|
||
- 全局 `AccountInterface` → `CoreCtx`
|
||
- `OneBotManager.bind(hub)` 订阅 `'core-online'` / `'core-offline'`
|
||
- 内部 `byUin.get(uin).apis.<area>.method()` 调 api
|
||
|
||
**Step 4** —— 引入 `AsyncLocalStorage`
|
||
- `packages/common/src/async-context.ts`
|
||
- `logger.ts` 自动读 ALS,加 `[uin]` 前缀
|
||
- `Core.constructor` 在 `onPacket` 回调外包 `runInContext`
|
||
|
||
**Step 5** —— `channel-socket` stub 包(占位)
|
||
- 把 `socket-channel.ts` / `socket-adapter.ts` 搬过来
|
||
- 实现 placeholder,让 `ChannelAdapter` 端口齐
|
||
|
||
**Step 6** —— ⭐ 拆 `webui` 为 `webui-backend` + `webui-frontend`
|
||
- Hono server + REST/WS → `webui-backend`,依赖 `@snowluma/core`(消费 `CoreCtx`)
|
||
- Vite SPA + UI → `webui-frontend`,依赖 `@snowluma/ui`
|
||
|
||
---
|
||
|
||
## 10. 设计原则速查
|
||
|
||
- **单向数据流**:`adapter → Channel → ChannelCtx → Core → CoreCtx → OneBot`,无反向引用
|
||
- **能力 POJO 跨层传递**:层间用 `{ uin, sendRawPacket, ... }` / `{ uin, apis, events, ... }` 这种纯数据,不传类实例
|
||
- **单一所有者**:`Hub.cores` 是 Core 唯一容器,消费者只持投影
|
||
- **每账号自带 EventBus**:避免全局 bus 混杂,订阅生命周期与 Core 绑定
|
||
- **Class 封装 deps**:构造时捕获,方法签名只剩业务参数
|
||
- **AsyncLocalStorage 跨切面**:日志 / trace 不进显式参数
|
||
- **不引入过度抽象**:no factory / no DI / no Effect monad / no global bus
|
||
- **可测**:每层接 POJO,fake 一下就能测
|
||
|
||
---
|
||
|
||
## 11. v2.0.0 已完成
|
||
|
||
- ✅ UID/UIN Service + SQLite 持久化
|
||
- ✅ tsx 免打包 Debug
|
||
- ✅ Common / OneBot / Core / Bridge / Protocol / proto-defs 拆包
|
||
- ✅ 消息 hot-path 优化(17.5μs → 4.7μs,better-sqlite3 + prepared-statement cache)
|
||
- ✅ NapCat 风格 Api 层(13 Api 类)
|
||
- ✅ Bridge / Account 严格分层(v2 中间形态;v3 重命名为 Channel + Core,并用 ctx 替代直接引用)
|
||
- ✅ `@snowluma/bridge` 完整 transport 包(v3 重命名为 `@snowluma/channel`)
|
||
|
||
|
||
|