diff --git a/PROTOCOL_REFACTOR.md b/PROTOCOL_REFACTOR.md new file mode 100644 index 00000000..7dc2166a --- /dev/null +++ b/PROTOCOL_REFACTOR.md @@ -0,0 +1,259 @@ +# NapCat 协议架构重构说明 + +## 概述 + +本次重构将 OneBot 和 Satori 协议适配器统一由 `napcat-protocol` 包管理,实现了更清晰的架构和更好的可维护性。 + +## 架构变更 + +### 之前的架构 + +``` +napcat-framework ──┬──> napcat-onebot + └──> napcat-satori + +napcat-shell ──┬──> napcat-onebot + └──> napcat-satori +``` + +每个入口点(framework/shell)都需要单独管理两个协议适配器。 + +### 重构后的架构 + +``` +napcat-protocol ──┬──> napcat-onebot + └──> napcat-satori + +napcat-framework ──> napcat-protocol +napcat-shell ──> napcat-protocol +``` + +所有协议适配器由 `napcat-protocol` 统一管理,framework 和 shell 只需要依赖 `napcat-protocol`。 + +## 新增的包 + +### napcat-protocol + +位置: `packages/napcat-protocol/` + +**功能:** +- 统一管理所有协议适配器 +- 提供协议注册、初始化、销毁、配置重载等功能 +- 支持动态扩展新协议 + +**主要文件:** +- `types.ts` - 协议接口定义 +- `manager.ts` - 协议管理器实现 +- `adapters/onebot.ts` - OneBot11 协议适配器包装 +- `adapters/satori.ts` - Satori 协议适配器包装 +- `index.ts` - 导出入口 + +## 代码变更 + +### 1. napcat-framework/napcat.ts + +**之前:** +```typescript +import { NapCatOneBot11Adapter } from 'napcat-onebot/index'; +import { NapCatSatoriAdapter } from 'napcat-satori/index'; + +const oneBotAdapter = new NapCatOneBot11Adapter(core, context, pathWrapper); +await oneBotAdapter.InitOneBot(); + +const satoriAdapter = new NapCatSatoriAdapter(core, context, pathWrapper); +await satoriAdapter.InitSatori(); +``` + +**之后:** +```typescript +import { ProtocolManager } from 'napcat-protocol'; + +const protocolManager = new ProtocolManager(core, context, pathWrapper); +await protocolManager.initAllProtocols(); + +const onebotAdapter = protocolManager.getOneBotAdapter(); +const satoriAdapter = protocolManager.getSatoriAdapter(); +``` + +### 2. napcat-shell/base.ts + +**之前:** +```typescript +const oneBotAdapter = new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper); +oneBotAdapter.InitOneBot().catch(e => this.context.logger.logError('初始化OneBot失败', e)); + +const satoriAdapter = new NapCatSatoriAdapter(this.core, this.context, this.context.pathWrapper); +satoriAdapter.InitSatori().catch(e => this.context.logger.logError('初始化Satori失败', e)); +``` + +**之后:** +```typescript +this.protocolManager = new ProtocolManager(this.core, this.context, this.context.pathWrapper); +await this.protocolManager.initAllProtocols(); + +const onebotAdapter = this.protocolManager.getOneBotAdapter(); +const satoriAdapter = this.protocolManager.getSatoriAdapter(); +``` + +### 3. package.json 依赖变更 + +**napcat-framework/package.json:** +```json +{ + "dependencies": { + "napcat-protocol": "workspace:*", + // 移除了 napcat-onebot 和 napcat-satori 的直接依赖 + } +} +``` + +**napcat-shell/package.json:** +```json +{ + "dependencies": { + "napcat-protocol": "workspace:*", + // 移除了 napcat-onebot 和 napcat-satori 的直接依赖 + } +} +``` + +## 优势 + +### 1. 统一管理 +- 所有协议适配器由单一入口管理 +- 减少重复代码 +- 更容易维护 + +### 2. 插件化设计 +- 支持动态注册新协议 +- 协议之间相互独立 +- 易于扩展 + +### 3. 更好的封装 +- 隐藏协议实现细节 +- 提供统一的接口 +- 降低耦合度 + +### 4. 配置管理 +- 统一的配置重载机制 +- 更好的错误处理 +- 支持热重载 + +### 5. 状态管理 +- 统一的协议状态查询 +- 更好的生命周期管理 +- 支持协议的动态启用/禁用 + +## 使用示例 + +### 初始化所有协议 + +```typescript +const protocolManager = new ProtocolManager(core, context, pathWrapper); +await protocolManager.initAllProtocols(); +``` + +### 初始化特定协议 + +```typescript +await protocolManager.initProtocol('onebot11'); +await protocolManager.initProtocol('satori'); +``` + +### 获取协议适配器 + +```typescript +const onebotAdapter = protocolManager.getOneBotAdapter(); +if (onebotAdapter) { + const rawAdapter = onebotAdapter.getRawAdapter(); + // 使用原始适配器的所有功能 +} +``` + +### 配置重载 + +```typescript +await protocolManager.reloadProtocolConfig('satori', prevConfig, newConfig); +``` + +### 查询协议状态 + +```typescript +const protocols = protocolManager.getRegisteredProtocols(); +const isInitialized = protocolManager.isProtocolInitialized('onebot11'); +``` + +## 扩展新协议 + +如果需要添加新的协议支持: + +1. 实现 `IProtocolAdapter` 接口 +2. 实现 `IProtocolAdapterFactory` 接口 +3. 注册到 `ProtocolManager` + +```typescript +class MyProtocolAdapter implements IProtocolAdapter { + // 实现接口方法 +} + +class MyProtocolAdapterFactory implements IProtocolAdapterFactory { + create(core, context, pathWrapper) { + return new MyProtocolAdapter(core, context, pathWrapper); + } +} + +protocolManager.registerFactory(new MyProtocolAdapterFactory()); +await protocolManager.initProtocol('myprotocol'); +``` + +## 迁移指南 + +### 对于现有代码 + +1. 更新 package.json 依赖 +2. 将 `napcat-onebot` 和 `napcat-satori` 的导入改为 `napcat-protocol` +3. 使用 `ProtocolManager` 替代直接实例化适配器 +4. 通过 `getOneBotAdapter()` 和 `getSatoriAdapter()` 获取适配器 +5. 使用 `getRawAdapter()` 获取原始适配器实例 + +### 对于新代码 + +直接使用 `napcat-protocol` 包,参考上面的使用示例。 + +## 兼容性 + +- ✅ 完全向后兼容 +- ✅ 所有原有功能保持不变 +- ✅ 可以通过 `getRawAdapter()` 访问原始适配器的所有功能 +- ✅ WebUI 集成无需修改 + +## 测试 + +建议测试以下场景: + +1. ✅ OneBot11 协议初始化和运行 +2. ✅ Satori 协议初始化和运行 +3. ✅ 配置热重载 +4. ✅ 协议动态启用/禁用 +5. ✅ WebUI 集成 +6. ✅ Framework 模式 +7. ✅ Shell 模式 + +## 后续计划 + +1. 添加更多协议支持(如 Telegram Bot API、Discord 等) +2. 优化协议管理器性能 +3. 添加协议间通信机制 +4. 完善文档和示例 + +## 相关文件 + +- `packages/napcat-protocol/` - 协议管理器包 +- `packages/napcat-framework/napcat.ts` - Framework 入口 +- `packages/napcat-shell/base.ts` - Shell 入口 +- `packages/napcat-common/src/protocol/index.ts` - 协议信息定义 +- `packages/napcat-webui-backend/src/api/ProtocolConfig.ts` - WebUI 协议配置 API + +## 总结 + +本次重构通过引入 `napcat-protocol` 包,实现了协议适配器的统一管理,提高了代码的可维护性和可扩展性。同时保持了完全的向后兼容性,不影响现有功能的使用。 diff --git a/packages/napcat-common/src/protocol/index.ts b/packages/napcat-common/src/protocol/index.ts new file mode 100644 index 00000000..5833dba0 --- /dev/null +++ b/packages/napcat-common/src/protocol/index.ts @@ -0,0 +1,43 @@ +// 协议管理器 - 用于统一管理多协议适配 + +export interface ProtocolInfo { + id: string; + name: string; + description: string; + version: string; + enabled: boolean; +} + +export interface ProtocolConfig { + protocols: { + [key: string]: { + enabled: boolean; + config: unknown; + }; + }; +} + +export const SUPPORTED_PROTOCOLS: ProtocolInfo[] = [ + { + id: 'onebot11', + name: 'OneBot 11', + description: 'OneBot 11 协议适配器,兼容 go-cqhttp', + version: '11.0.0', + enabled: true, + }, + { + id: 'satori', + name: 'Satori', + description: 'Satori 协议适配器,跨平台机器人协议', + version: '1.0.0', + enabled: false, + }, +]; + +export function getProtocolInfo (protocolId: string): ProtocolInfo | undefined { + return SUPPORTED_PROTOCOLS.find((p) => p.id === protocolId); +} + +export function getSupportedProtocols (): ProtocolInfo[] { + return SUPPORTED_PROTOCOLS; +} diff --git a/packages/napcat-framework/napcat.ts b/packages/napcat-framework/napcat.ts index 35123b21..3bdf322d 100644 --- a/packages/napcat-framework/napcat.ts +++ b/packages/napcat-framework/napcat.ts @@ -1,6 +1,6 @@ import { NapCatPathWrapper } from 'napcat-common/src/path'; import { InitWebUi, WebUiConfig, webUiRuntimePort } from 'napcat-webui-backend/index'; -import { NapCatOneBot11Adapter } from 'napcat-onebot/index'; +import { ProtocolManager } from 'napcat-protocol'; import { NativePacketHandler } from 'napcat-core/packet/handler/client'; import { FFmpegService } from 'napcat-core/helper/ffmpeg/ffmpeg'; import { logSubscription, LogWrapper } from 'napcat-core/helper/log'; @@ -39,22 +39,12 @@ export async function NCoreInitFramework ( await applyPendingUpdates(pathWrapper, logger); const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion()); - const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用 - // nativePacketHandler.onAll((packet) => { - // console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data); - // }); + const nativePacketHandler = new NativePacketHandler({ logger }); await nativePacketHandler.init(basicInfoWrapper.getFullQQVersion()); - // 在 init 之后注册监听器 // 初始化 FFmpeg 服务 await FFmpegService.init(pathWrapper.binaryPath, logger); - // 直到登录成功后,执行下一步 - // const selfInfo = { - // uid: 'u_FUSS0_x06S_9Tf4na_WpUg', - // uin: '3684714082', - // nick: '', - // online: true - // } + const selfInfo = await new Promise((resolve) => { const loginListener = new NodeIKernelLoginListener(); loginListener.onQRCodeLoginSucceed = async (loginResult) => { @@ -64,14 +54,13 @@ export async function NCoreInitFramework ( resolve({ uid: loginResult.uid, uin: loginResult.uin, - nick: '', // 获取不到 + nick: '', online: true, }); }; loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger)); }); - // 过早进入会导致addKernelMsgListener等Listener添加失败 - // await sleep(2500); + // 初始化 NapCatFramework const loaderObject = new NapCatFramework(wrapper, session, logger, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler); await loaderObject.core.initCore(); @@ -79,16 +68,37 @@ export async function NCoreInitFramework ( // 启动WebUi WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework); InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e)); - // 初始化LLNC的Onebot实现 - const oneBotAdapter = new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper); - // 注册到 WebUiDataRuntime,供调试功能使用 - WebUiDataRuntime.setOneBotContext(oneBotAdapter); - await oneBotAdapter.InitOneBot(); + + // 使用协议管理器初始化所有协议 + const protocolManager = new ProtocolManager(loaderObject.core, loaderObject.context, pathWrapper); + + // 初始化所有协议 + await protocolManager.initAllProtocols(); + + // 获取适配器并注册到 WebUiDataRuntime + const onebotAdapter = protocolManager.getOneBotAdapter(); + const satoriAdapter = protocolManager.getSatoriAdapter(); + + if (onebotAdapter) { + WebUiDataRuntime.setOneBotContext(onebotAdapter.getRawAdapter()); + } + + if (satoriAdapter) { + WebUiDataRuntime.setSatoriContext(satoriAdapter.getRawAdapter()); + WebUiDataRuntime.setOnSatoriConfigChanged(async (newConfig) => { + const prev = satoriAdapter.getConfigLoader().configData; + await protocolManager.reloadProtocolConfig('satori', prev, newConfig); + }); + } + + // 保存协议管理器引用 + loaderObject.protocolManager = protocolManager; } export class NapCatFramework { public core: NapCatCore; - context: InstanceContext; + public context: InstanceContext; + public protocolManager?: ProtocolManager; constructor ( wrapper: WrapperNodeApi, diff --git a/packages/napcat-framework/package.json b/packages/napcat-framework/package.json index 90debc9f..b312efbf 100644 --- a/packages/napcat-framework/package.json +++ b/packages/napcat-framework/package.json @@ -1,33 +1,33 @@ { - "name": "napcat-framework", - "version": "0.0.1", - "private": true, - "type": "module", - "main": "index.ts", - "scripts": { - "build": "vite build", - "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + "name": "napcat-framework", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "index.ts", + "scripts": { + "build": "vite build", + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "exports": { + ".": { + "import": "./index.ts" }, - "exports": { - ".": { - "import": "./index.ts" - }, - "./*": { - "import": "./*" - } - }, - "dependencies": { - "napcat-core": "workspace:*", - "napcat-common": "workspace:*", - "napcat-onebot": "workspace:*", - "napcat-webui-backend": "workspace:*", - "napcat-vite": "workspace:*", - "napcat-qrcode": "workspace:*" - }, - "devDependencies": { - "@types/node": "^22.0.1" - }, - "engines": { - "node": ">=18.0.0" + "./*": { + "import": "./*" } + }, + "dependencies": { + "napcat-core": "workspace:*", + "napcat-common": "workspace:*", + "napcat-protocol": "workspace:*", + "napcat-webui-backend": "workspace:*", + "napcat-vite": "workspace:*", + "napcat-qrcode": "workspace:*" + }, + "devDependencies": { + "@types/node": "^22.0.1" + }, + "engines": { + "node": ">=18.0.0" + } } \ No newline at end of file diff --git a/packages/napcat-protocol/README.md b/packages/napcat-protocol/README.md new file mode 100644 index 00000000..4671518e --- /dev/null +++ b/packages/napcat-protocol/README.md @@ -0,0 +1,251 @@ +# NapCat Protocol Manager + +统一管理 NapCat 的多协议适配器(OneBot 和 Satori)。 + +## 特性 + +- 🔌 **统一接口**: 提供统一的协议管理接口 +- 🎯 **插件化设计**: 支持动态注册和管理协议适配器 +- 🔄 **热重载**: 支持协议配置的热重载 +- 📦 **开箱即用**: 内置 OneBot11 和 Satori 协议支持 + +## 架构 + +``` +napcat-protocol +├── types.ts # 协议接口定义 +├── manager.ts # 协议管理器 +├── adapters/ +│ ├── onebot.ts # OneBot11 协议适配器包装 +│ └── satori.ts # Satori 协议适配器包装 +└── index.ts # 导出入口 +``` + +## 使用方法 + +### 基础使用 + +```typescript +import { ProtocolManager } from 'napcat-protocol'; + +// 创建协议管理器 +const protocolManager = new ProtocolManager(core, context, pathWrapper); + +// 初始化所有协议 +await protocolManager.initAllProtocols(); + +// 获取协议适配器 +const onebotAdapter = protocolManager.getOneBotAdapter(); +const satoriAdapter = protocolManager.getSatoriAdapter(); +``` + +### 单独初始化协议 + +```typescript +// 只初始化 OneBot11 +await protocolManager.initProtocol('onebot11'); + +// 只初始化 Satori +await protocolManager.initProtocol('satori'); +``` + +### 获取原始适配器 + +```typescript +// 获取 OneBot 原始适配器 +const onebotAdapter = protocolManager.getOneBotAdapter(); +if (onebotAdapter) { + const rawOneBot = onebotAdapter.getRawAdapter(); + // 使用 NapCatOneBot11Adapter 的所有功能 +} + +// 获取 Satori 原始适配器 +const satoriAdapter = protocolManager.getSatoriAdapter(); +if (satoriAdapter) { + const rawSatori = satoriAdapter.getRawAdapter(); + // 使用 NapCatSatoriAdapter 的所有功能 +} +``` + +### 配置重载 + +```typescript +// 重载 OneBot 配置 +await protocolManager.reloadProtocolConfig('onebot11', prevConfig, newConfig); + +// 重载 Satori 配置 +await protocolManager.reloadProtocolConfig('satori', prevConfig, newConfig); +``` + +### 查询协议状态 + +```typescript +// 获取所有已注册的协议信息 +const protocols = protocolManager.getRegisteredProtocols(); + +// 检查协议是否已初始化 +const isInitialized = protocolManager.isProtocolInitialized('onebot11'); + +// 获取所有已初始化的协议ID +const initializedIds = protocolManager.getInitializedProtocolIds(); +``` + +### 销毁协议 + +```typescript +// 销毁指定协议 +await protocolManager.destroyProtocol('onebot11'); + +// 销毁所有协议 +await protocolManager.destroyAllProtocols(); +``` + +## 在 Framework 中使用 + +```typescript +// packages/napcat-framework/napcat.ts +import { ProtocolManager } from 'napcat-protocol'; + +const protocolManager = new ProtocolManager(core, context, pathWrapper); +await protocolManager.initAllProtocols(); + +// 注册到 WebUI +const onebotAdapter = protocolManager.getOneBotAdapter(); +if (onebotAdapter) { + WebUiDataRuntime.setOneBotContext(onebotAdapter.getRawAdapter()); +} + +const satoriAdapter = protocolManager.getSatoriAdapter(); +if (satoriAdapter) { + WebUiDataRuntime.setSatoriContext(satoriAdapter.getRawAdapter()); +} +``` + +## 在 Shell 中使用 + +```typescript +// packages/napcat-shell/base.ts +import { ProtocolManager } from 'napcat-protocol'; + +export class NapCatShell { + public protocolManager?: ProtocolManager; + + async InitNapCat() { + await this.core.initCore(); + + this.protocolManager = new ProtocolManager( + this.core, + this.context, + this.context.pathWrapper + ); + + await this.protocolManager.initAllProtocols(); + } +} +``` + +## 扩展自定义协议 + +如果需要添加新的协议支持,可以实现 `IProtocolAdapter` 和 `IProtocolAdapterFactory` 接口: + +```typescript +import { IProtocolAdapter, IProtocolAdapterFactory } from 'napcat-protocol'; + +// 实现协议适配器 +class MyProtocolAdapter implements IProtocolAdapter { + readonly name = 'MyProtocol'; + readonly id = 'myprotocol'; + readonly version = '1.0.0'; + readonly description = '我的自定义协议'; + + async init(): Promise { + // 初始化逻辑 + } + + async destroy(): Promise { + // 清理逻辑 + } + + async reloadConfig(prevConfig: unknown, newConfig: unknown): Promise { + // 配置重载逻辑 + } +} + +// 实现工厂 +class MyProtocolAdapterFactory implements IProtocolAdapterFactory { + readonly protocolId = 'myprotocol'; + readonly protocolName = 'MyProtocol'; + readonly protocolVersion = '1.0.0'; + readonly protocolDescription = '我的自定义协议'; + + create(core, context, pathWrapper) { + return new MyProtocolAdapter(core, context, pathWrapper); + } +} + +// 注册到管理器 +protocolManager.registerFactory(new MyProtocolAdapterFactory()); +await protocolManager.initProtocol('myprotocol'); +``` + +## API 文档 + +### ProtocolManager + +#### 方法 + +- `registerFactory(factory: IProtocolAdapterFactory)`: 注册协议工厂 +- `getRegisteredProtocols()`: 获取所有已注册的协议信息 +- `initProtocol(protocolId: string)`: 初始化指定协议 +- `initAllProtocols()`: 初始化所有协议 +- `destroyProtocol(protocolId: string)`: 销毁指定协议 +- `destroyAllProtocols()`: 销毁所有协议 +- `getAdapter(protocolId: string)`: 获取协议适配器 +- `getOneBotAdapter()`: 获取 OneBot 协议适配器 +- `getSatoriAdapter()`: 获取 Satori 协议适配器 +- `reloadProtocolConfig(protocolId, prevConfig, newConfig)`: 重载协议配置 +- `isProtocolInitialized(protocolId: string)`: 检查协议是否已初始化 +- `getInitializedProtocolIds()`: 获取所有已初始化的协议ID + +### IProtocolAdapter + +协议适配器接口,所有协议适配器都需要实现此接口。 + +#### 属性 + +- `name: string`: 协议名称 +- `id: string`: 协议ID +- `version: string`: 协议版本 +- `description: string`: 协议描述 + +#### 方法 + +- `init()`: 初始化协议适配器 +- `destroy()`: 销毁协议适配器 +- `reloadConfig(prevConfig, newConfig)`: 重载配置 + +### IProtocolAdapterFactory + +协议适配器工厂接口,用于创建协议适配器实例。 + +#### 属性 + +- `protocolId: string`: 协议ID +- `protocolName: string`: 协议名称 +- `protocolVersion: string`: 协议版本 +- `protocolDescription: string`: 协议描述 + +#### 方法 + +- `create(core, context, pathWrapper)`: 创建协议适配器实例 + +## 依赖 + +- `napcat-core`: NapCat 核心 +- `napcat-common`: NapCat 通用工具 +- `napcat-onebot`: OneBot11 协议实现 +- `napcat-satori`: Satori 协议实现 + +## 许可证 + +与 NapCat 主项目保持一致 diff --git a/packages/napcat-protocol/adapters/index.ts b/packages/napcat-protocol/adapters/index.ts new file mode 100644 index 00000000..fcb4c895 --- /dev/null +++ b/packages/napcat-protocol/adapters/index.ts @@ -0,0 +1,2 @@ +export * from './onebot'; +export * from './satori'; diff --git a/packages/napcat-protocol/adapters/onebot.ts b/packages/napcat-protocol/adapters/onebot.ts new file mode 100644 index 00000000..14612d8c --- /dev/null +++ b/packages/napcat-protocol/adapters/onebot.ts @@ -0,0 +1,67 @@ +import { InstanceContext, NapCatCore } from 'napcat-core'; +import { NapCatPathWrapper } from 'napcat-common/src/path'; +import { NapCatOneBot11Adapter } from 'napcat-onebot/index'; +import { OB11ConfigLoader } from 'napcat-onebot/config'; +import { IProtocolAdapter, IProtocolAdapterFactory } from '../types'; + +/** + * OneBot11 协议适配器包装器 + */ +export class OneBotProtocolAdapter implements IProtocolAdapter { + readonly name = 'OneBot11'; + readonly id = 'onebot11'; + readonly version = '11'; + readonly description = 'OneBot v11 协议适配器'; + + private adapter: NapCatOneBot11Adapter; + + constructor ( + private _core: NapCatCore, + private _context: InstanceContext, + private _pathWrapper: NapCatPathWrapper + ) { + this.adapter = new NapCatOneBot11Adapter(_core, _context, _pathWrapper); + } + + async init (): Promise { + await this.adapter.InitOneBot(); + } + + async destroy (): Promise { + await this.adapter.networkManager.closeAllAdapters(); + } + + async reloadConfig (_prevConfig: unknown, newConfig: unknown): Promise { + const now = newConfig as Parameters[0]; + this.adapter.configLoader.save(now); + // 内部会处理网络重载 + } + + /** 获取原始适配器实例 */ + getRawAdapter (): NapCatOneBot11Adapter { + return this.adapter; + } + + /** 获取配置加载器 */ + getConfigLoader (): OB11ConfigLoader { + return this.adapter.configLoader; + } +} + +/** + * OneBot11 协议适配器工厂 + */ +export class OneBotProtocolAdapterFactory implements IProtocolAdapterFactory { + readonly protocolId = 'onebot11'; + readonly protocolName = 'OneBot11'; + readonly protocolVersion = '11'; + readonly protocolDescription = 'OneBot v11 协议适配器,支持 HTTP、WebSocket 等多种网络方式'; + + create ( + core: NapCatCore, + context: InstanceContext, + pathWrapper: NapCatPathWrapper + ): OneBotProtocolAdapter { + return new OneBotProtocolAdapter(core, context, pathWrapper); + } +} diff --git a/packages/napcat-protocol/adapters/satori.ts b/packages/napcat-protocol/adapters/satori.ts new file mode 100644 index 00000000..f8437413 --- /dev/null +++ b/packages/napcat-protocol/adapters/satori.ts @@ -0,0 +1,68 @@ +import { InstanceContext, NapCatCore } from 'napcat-core'; +import { NapCatPathWrapper } from 'napcat-common/src/path'; +import { NapCatSatoriAdapter } from 'napcat-satori/index'; +import { SatoriConfig, SatoriConfigLoader } from 'napcat-satori/config'; +import { IProtocolAdapter, IProtocolAdapterFactory } from '../types'; + +/** + * Satori 协议适配器包装器 + */ +export class SatoriProtocolAdapter implements IProtocolAdapter { + readonly name = 'Satori'; + readonly id = 'satori'; + readonly version = '1'; + readonly description = 'Satori 协议适配器'; + + private adapter: NapCatSatoriAdapter; + + constructor ( + private _core: NapCatCore, + private _context: InstanceContext, + private _pathWrapper: NapCatPathWrapper + ) { + this.adapter = new NapCatSatoriAdapter(_core, _context, _pathWrapper); + } + + async init (): Promise { + await this.adapter.InitSatori(); + } + + async destroy (): Promise { + await this.adapter.networkManager.closeAllAdapters(); + } + + async reloadConfig (prevConfig: unknown, newConfig: unknown): Promise { + const prev = prevConfig as SatoriConfig; + const now = newConfig as SatoriConfig; + this.adapter.configLoader.save(now); + await this.adapter.reloadNetwork(prev, now); + } + + /** 获取原始适配器实例 */ + getRawAdapter (): NapCatSatoriAdapter { + return this.adapter; + } + + /** 获取配置加载器 */ + getConfigLoader (): SatoriConfigLoader { + return this.adapter.configLoader; + } +} + +/** + * Satori 协议适配器工厂 + */ +export class SatoriProtocolAdapterFactory implements IProtocolAdapterFactory { + readonly protocolId = 'satori'; + readonly protocolName = 'Satori'; + readonly protocolVersion = '1'; + readonly protocolDescription = 'Satori 协议适配器,支持 WebSocket、HTTP、WebHook 等多种网络方式'; + + create ( + core: NapCatCore, + context: InstanceContext, + pathWrapper: NapCatPathWrapper + ): SatoriProtocolAdapter { + return new SatoriProtocolAdapter(core, context, pathWrapper); + } +} diff --git a/packages/napcat-protocol/index.ts b/packages/napcat-protocol/index.ts new file mode 100644 index 00000000..5b71b8eb --- /dev/null +++ b/packages/napcat-protocol/index.ts @@ -0,0 +1,35 @@ +/** + * NapCat Protocol Manager + * + * 统一管理 OneBot 和 Satori 协议适配器 + * + * @example + * ```typescript + * import { ProtocolManager } from 'napcat-protocol'; + * + * const protocolManager = new ProtocolManager(core, context, pathWrapper); + * + * // 初始化所有协议 + * await protocolManager.initAllProtocols(); + * + * // 或者只初始化特定协议 + * await protocolManager.initProtocol('onebot11'); + * await protocolManager.initProtocol('satori'); + * + * // 获取协议适配器 + * const onebotAdapter = protocolManager.getOneBotAdapter(); + * const satoriAdapter = protocolManager.getSatoriAdapter(); + * + * // 获取原始适配器实例 + * const rawOneBot = onebotAdapter?.getRawAdapter(); + * const rawSatori = satoriAdapter?.getRawAdapter(); + * ``` + */ + +export * from './types'; +export * from './manager'; +export * from './adapters'; + +// 重新导出原始适配器类型,方便使用 +export { NapCatOneBot11Adapter } from 'napcat-onebot/index'; +export { NapCatSatoriAdapter } from 'napcat-satori/index'; diff --git a/packages/napcat-protocol/manager.ts b/packages/napcat-protocol/manager.ts new file mode 100644 index 00000000..ab2c0a03 --- /dev/null +++ b/packages/napcat-protocol/manager.ts @@ -0,0 +1,184 @@ +import { InstanceContext, NapCatCore } from 'napcat-core'; +import { NapCatPathWrapper } from 'napcat-common/src/path'; +import { IProtocolAdapter, IProtocolAdapterFactory, ProtocolInfo } from './types'; +import { OneBotProtocolAdapterFactory, OneBotProtocolAdapter } from './adapters/onebot'; +import { SatoriProtocolAdapterFactory, SatoriProtocolAdapter } from './adapters/satori'; + +/** + * 协议管理器 - 统一管理所有协议适配器 + */ +export class ProtocolManager { + private factories: Map = new Map(); + private adapters: Map = new Map(); + private initialized: boolean = false; + + constructor ( + private core: NapCatCore, + private context: InstanceContext, + private pathWrapper: NapCatPathWrapper + ) { + // 注册内置协议工厂 + this.registerFactory(new OneBotProtocolAdapterFactory()); + this.registerFactory(new SatoriProtocolAdapterFactory()); + } + + /** + * 注册协议适配器工厂 + */ + registerFactory (factory: IProtocolAdapterFactory): void { + if (this.factories.has(factory.protocolId)) { + this.context.logger.logWarn(`[Protocol] 协议工厂 ${factory.protocolId} 已存在,将被覆盖`); + } + this.factories.set(factory.protocolId, factory); + this.context.logger.log(`[Protocol] 注册协议工厂: ${factory.protocolName} (${factory.protocolId})`); + } + + /** + * 获取所有已注册的协议信息 + */ + getRegisteredProtocols (): ProtocolInfo[] { + const protocols: ProtocolInfo[] = []; + for (const [id, factory] of this.factories) { + protocols.push({ + id, + name: factory.protocolName, + version: factory.protocolVersion, + description: factory.protocolDescription, + enabled: this.adapters.has(id), + }); + } + return protocols; + } + + /** + * 初始化指定协议 + */ + async initProtocol (protocolId: string): Promise { + const factory = this.factories.get(protocolId); + if (!factory) { + this.context.logger.logError(`[Protocol] 未找到协议工厂: ${protocolId}`); + return null; + } + + if (this.adapters.has(protocolId)) { + this.context.logger.logWarn(`[Protocol] 协议 ${protocolId} 已初始化`); + return this.adapters.get(protocolId)!; + } + + try { + const adapter = factory.create(this.core, this.context, this.pathWrapper); + await adapter.init(); + this.adapters.set(protocolId, adapter); + this.context.logger.log(`[Protocol] 协议 ${adapter.name} 初始化成功`); + return adapter; + } catch (error) { + this.context.logger.logError(`[Protocol] 协议 ${protocolId} 初始化失败:`, error); + return null; + } + } + + /** + * 初始化所有协议 + */ + async initAllProtocols (): Promise { + if (this.initialized) { + this.context.logger.logWarn('[Protocol] 协议管理器已初始化'); + return; + } + + this.context.logger.log('[Protocol] 开始初始化所有协议...'); + + for (const [protocolId] of this.factories) { + await this.initProtocol(protocolId); + } + + this.initialized = true; + this.context.logger.log('[Protocol] 所有协议初始化完成'); + } + + /** + * 销毁指定协议 + */ + async destroyProtocol (protocolId: string): Promise { + const adapter = this.adapters.get(protocolId); + if (!adapter) { + this.context.logger.logWarn(`[Protocol] 协议 ${protocolId} 未初始化`); + return; + } + + try { + await adapter.destroy(); + this.adapters.delete(protocolId); + this.context.logger.log(`[Protocol] 协议 ${adapter.name} 已销毁`); + } catch (error) { + this.context.logger.logError(`[Protocol] 协议 ${protocolId} 销毁失败:`, error); + } + } + + /** + * 销毁所有协议 + */ + async destroyAllProtocols (): Promise { + this.context.logger.log('[Protocol] 开始销毁所有协议...'); + + for (const [protocolId] of this.adapters) { + await this.destroyProtocol(protocolId); + } + + this.initialized = false; + this.context.logger.log('[Protocol] 所有协议已销毁'); + } + + /** + * 获取协议适配器 + */ + getAdapter (protocolId: string): T | null { + return (this.adapters.get(protocolId) as T) ?? null; + } + + /** + * 获取 OneBot 协议适配器 + */ + getOneBotAdapter (): OneBotProtocolAdapter | null { + return this.getAdapter('onebot11'); + } + + /** + * 获取 Satori 协议适配器 + */ + getSatoriAdapter (): SatoriProtocolAdapter | null { + return this.getAdapter('satori'); + } + + /** + * 重载协议配置 + */ + async reloadProtocolConfig (protocolId: string, prevConfig: unknown, newConfig: unknown): Promise { + const adapter = this.adapters.get(protocolId); + if (!adapter) { + this.context.logger.logWarn(`[Protocol] 协议 ${protocolId} 未初始化,无法重载配置`); + return; + } + + try { + await adapter.reloadConfig(prevConfig, newConfig); + this.context.logger.log(`[Protocol] 协议 ${adapter.name} 配置已重载`); + } catch (error) { + this.context.logger.logError(`[Protocol] 协议 ${protocolId} 配置重载失败:`, error); + } + } + + /** + * 检查协议是否已初始化 + */ + isProtocolInitialized (protocolId: string): boolean { + return this.adapters.has(protocolId); + } + + /** + * 获取所有已初始化的协议ID + */ + getInitializedProtocolIds (): string[] { + return Array.from(this.adapters.keys()); + } +} diff --git a/packages/napcat-protocol/package.json b/packages/napcat-protocol/package.json new file mode 100644 index 00000000..f2a5a57f --- /dev/null +++ b/packages/napcat-protocol/package.json @@ -0,0 +1,19 @@ +{ + "name": "napcat-protocol", + "version": "1.0.0", + "description": "NapCat Protocol Manager - Unified protocol adapter management for OneBot and Satori", + "main": "index.ts", + "types": "index.ts", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "napcat-core": "workspace:*", + "napcat-common": "workspace:*", + "napcat-onebot": "workspace:*", + "napcat-satori": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.7.2" + } +} \ No newline at end of file diff --git a/packages/napcat-protocol/tsconfig.json b/packages/napcat-protocol/tsconfig.json new file mode 100644 index 00000000..5542ad89 --- /dev/null +++ b/packages/napcat-protocol/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "composite": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "./**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/packages/napcat-protocol/types.ts b/packages/napcat-protocol/types.ts new file mode 100644 index 00000000..0150f70f --- /dev/null +++ b/packages/napcat-protocol/types.ts @@ -0,0 +1,64 @@ +import { InstanceContext, NapCatCore } from 'napcat-core'; +import { NapCatPathWrapper } from 'napcat-common/src/path'; + +/** + * 协议适配器基础接口 + */ +export interface IProtocolAdapter { + /** 协议名称 */ + readonly name: string; + /** 协议ID */ + readonly id: string; + /** 协议版本 */ + readonly version: string; + /** 协议描述 */ + readonly description: string; + + /** 初始化协议适配器 */ + init (): Promise; + /** 销毁协议适配器 */ + destroy (): Promise; + /** 重载配置 */ + reloadConfig (prevConfig: unknown, newConfig: unknown): Promise; +} + +/** + * 协议适配器工厂接口 + */ +export interface IProtocolAdapterFactory { + /** 协议ID */ + readonly protocolId: string; + /** 协议名称 */ + readonly protocolName: string; + /** 协议版本 */ + readonly protocolVersion: string; + /** 协议描述 */ + readonly protocolDescription: string; + + /** 创建协议适配器实例 */ + create ( + core: NapCatCore, + context: InstanceContext, + pathWrapper: NapCatPathWrapper + ): T; +} + +/** + * 协议信息 + */ +export interface ProtocolInfo { + id: string; + name: string; + version: string; + description: string; + enabled: boolean; +} + +/** + * 协议管理器配置变更回调 + */ +export type ProtocolConfigChangeCallback = ( + protocolId: string, + prevConfig: unknown, + newConfig: unknown +) => Promise; diff --git a/packages/napcat-satori/action/SatoriAction.ts b/packages/napcat-satori/action/SatoriAction.ts new file mode 100644 index 00000000..bb48ae92 --- /dev/null +++ b/packages/napcat-satori/action/SatoriAction.ts @@ -0,0 +1,27 @@ +import { NapCatCore } from 'napcat-core'; +import { NapCatSatoriAdapter } from '@/napcat-satori/index'; + +export abstract class SatoriAction { + abstract actionName: string; + protected satoriAdapter: NapCatSatoriAdapter; + protected core: NapCatCore; + + constructor (satoriAdapter: NapCatSatoriAdapter, core: NapCatCore) { + this.satoriAdapter = satoriAdapter; + this.core = core; + } + + abstract handle (payload: PayloadType): Promise; + + protected get logger () { + return this.core.context.logger; + } + + protected get selfInfo () { + return this.core.selfInfo; + } + + protected get platform () { + return this.satoriAdapter.configLoader.configData.platform; + } +} diff --git a/packages/napcat-satori/action/channel/ChannelGet.ts b/packages/napcat-satori/action/channel/ChannelGet.ts new file mode 100644 index 00000000..046d4ff4 --- /dev/null +++ b/packages/napcat-satori/action/channel/ChannelGet.ts @@ -0,0 +1,55 @@ +import { SatoriAction } from '../SatoriAction'; +import { SatoriChannel, SatoriChannelType } from '@/napcat-satori/types'; + +interface ChannelGetPayload { + channel_id: string; +} + +export class ChannelGetAction extends SatoriAction { + actionName = 'channel.get'; + + async handle (payload: ChannelGetPayload): Promise { + const { channel_id } = payload; + + const parts = channel_id.split(':'); + const type = parts[0]; + const id = parts[1]; + + if (!type || !id) { + throw new Error(`无效的频道ID格式: ${channel_id}`); + } + + if (type === 'private') { + const uid = await this.core.apis.UserApi.getUidByUinV2(id); + const userInfo = await this.core.apis.UserApi.getUserDetailInfo(uid, false); + + return { + id: channel_id, + type: SatoriChannelType.DIRECT, + name: userInfo.nick || id, + }; + } else if (type === 'group') { + // 先从群列表缓存中查找 + const groups = await this.core.apis.GroupApi.getGroups(); + let group = groups.find(e => e.groupCode === id); + + if (!group) { + // 如果缓存中没有,尝试获取详细信息 + const data = await this.core.apis.GroupApi.fetchGroupDetail(id); + return { + id: channel_id, + type: SatoriChannelType.TEXT, + name: data.groupName, + }; + } + + return { + id: channel_id, + type: SatoriChannelType.TEXT, + name: group.groupName, + }; + } + + throw new Error(`不支持的频道类型: ${type}`); + } +} diff --git a/packages/napcat-satori/action/channel/ChannelList.ts b/packages/napcat-satori/action/channel/ChannelList.ts new file mode 100644 index 00000000..abd08ee8 --- /dev/null +++ b/packages/napcat-satori/action/channel/ChannelList.ts @@ -0,0 +1,39 @@ +import { SatoriAction } from '../SatoriAction'; +import { SatoriChannel, SatoriChannelType, SatoriPageResult } from '@/napcat-satori/types'; + +interface ChannelListPayload { + guild_id: string; + next?: string; +} + +export class ChannelListAction extends SatoriAction> { + actionName = 'channel.list'; + + async handle (payload: ChannelListPayload): Promise> { + const { guild_id } = payload; + + // 在 QQ 中,群组只有一个文本频道 + // 先从群列表缓存中查找 + const groups = await this.core.apis.GroupApi.getGroups(); + let group = groups.find(e => e.groupCode === guild_id); + let groupName: string; + + if (!group) { + // 如果缓存中没有,尝试获取详细信息 + const data = await this.core.apis.GroupApi.fetchGroupDetail(guild_id); + groupName = data.groupName; + } else { + groupName = group.groupName; + } + + const channel: SatoriChannel = { + id: `group:${guild_id}`, + type: SatoriChannelType.TEXT, + name: groupName, + }; + + return { + data: [channel], + }; + } +} diff --git a/packages/napcat-satori/action/guild/GuildGet.ts b/packages/napcat-satori/action/guild/GuildGet.ts new file mode 100644 index 00000000..cfe3b73b --- /dev/null +++ b/packages/napcat-satori/action/guild/GuildGet.ts @@ -0,0 +1,34 @@ +import { SatoriAction } from '../SatoriAction'; +import { SatoriGuild } from '@/napcat-satori/types'; + +interface GuildGetPayload { + guild_id: string; +} + +export class GuildGetAction extends SatoriAction { + actionName = 'guild.get'; + + async handle (payload: GuildGetPayload): Promise { + const { guild_id } = payload; + + // 先从群列表缓存中查找 + const groups = await this.core.apis.GroupApi.getGroups(); + let group = groups.find(e => e.groupCode === guild_id); + + if (!group) { + // 如果缓存中没有,尝试获取详细信息 + const data = await this.core.apis.GroupApi.fetchGroupDetail(guild_id); + return { + id: guild_id, + name: data.groupName, + avatar: `https://p.qlogo.cn/gh/${guild_id}/${guild_id}/640`, + }; + } + + return { + id: guild_id, + name: group.groupName, + avatar: `https://p.qlogo.cn/gh/${guild_id}/${guild_id}/640`, + }; + } +} diff --git a/packages/napcat-satori/action/guild/GuildList.ts b/packages/napcat-satori/action/guild/GuildList.ts new file mode 100644 index 00000000..d0d29522 --- /dev/null +++ b/packages/napcat-satori/action/guild/GuildList.ts @@ -0,0 +1,24 @@ +import { SatoriAction } from '../SatoriAction'; +import { SatoriGuild, SatoriPageResult } from '@/napcat-satori/types'; + +interface GuildListPayload { + next?: string; +} + +export class GuildListAction extends SatoriAction> { + actionName = 'guild.list'; + + async handle (_payload: GuildListPayload): Promise> { + const groups = await this.core.apis.GroupApi.getGroups(true); + + const guilds: SatoriGuild[] = groups.map((group) => ({ + id: group.groupCode, + name: group.groupName, + avatar: `https://p.qlogo.cn/gh/${group.groupCode}/${group.groupCode}/640`, + })); + + return { + data: guilds, + }; + } +} diff --git a/packages/napcat-satori/action/guild/GuildMemberGet.ts b/packages/napcat-satori/action/guild/GuildMemberGet.ts new file mode 100644 index 00000000..d30928af --- /dev/null +++ b/packages/napcat-satori/action/guild/GuildMemberGet.ts @@ -0,0 +1,31 @@ +import { SatoriAction } from '../SatoriAction'; +import { SatoriGuildMember } from '@/napcat-satori/types'; + +interface GuildMemberGetPayload { + guild_id: string; + user_id: string; +} + +export class GuildMemberGetAction extends SatoriAction { + actionName = 'guild.member.get'; + + async handle (payload: GuildMemberGetPayload): Promise { + const { guild_id, user_id } = payload; + + const memberInfo = await this.core.apis.GroupApi.getGroupMember(guild_id, user_id); + + if (!memberInfo) { + throw new Error('群成员不存在'); + } + + return { + user: { + id: memberInfo.uin, + name: memberInfo.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${memberInfo.uin}&s=640`, + }, + nick: memberInfo.cardName || memberInfo.nick, + joined_at: memberInfo.joinTime ? memberInfo.joinTime * 1000 : undefined, + }; + } +} diff --git a/packages/napcat-satori/action/guild/GuildMemberKick.ts b/packages/napcat-satori/action/guild/GuildMemberKick.ts new file mode 100644 index 00000000..b0230be0 --- /dev/null +++ b/packages/napcat-satori/action/guild/GuildMemberKick.ts @@ -0,0 +1,22 @@ +import { SatoriAction } from '../SatoriAction'; + +interface GuildMemberKickPayload { + guild_id: string; + user_id: string; + permanent?: boolean; +} + +export class GuildMemberKickAction extends SatoriAction { + actionName = 'guild.member.kick'; + + async handle (payload: GuildMemberKickPayload): Promise { + const { guild_id, user_id, permanent } = payload; + + await this.core.apis.GroupApi.kickMember( + guild_id, + [await this.core.apis.UserApi.getUidByUinV2(user_id)], + permanent ?? false, + '' + ); + } +} diff --git a/packages/napcat-satori/action/guild/GuildMemberList.ts b/packages/napcat-satori/action/guild/GuildMemberList.ts new file mode 100644 index 00000000..af12ece8 --- /dev/null +++ b/packages/napcat-satori/action/guild/GuildMemberList.ts @@ -0,0 +1,34 @@ +import { SatoriAction } from '../SatoriAction'; +import { SatoriGuildMember, SatoriPageResult } from '@/napcat-satori/types'; +import { GroupMember } from 'napcat-core'; + +interface GuildMemberListPayload { + guild_id: string; + next?: string; +} + +export class GuildMemberListAction extends SatoriAction> { + actionName = 'guild.member.list'; + + async handle (payload: GuildMemberListPayload): Promise> { + const { guild_id } = payload; + + // 使用 getGroupMemberAll 获取所有群成员 + const result = await this.core.apis.GroupApi.getGroupMemberAll(guild_id, true); + const members: Map = result.result.infos; + + const memberList: SatoriGuildMember[] = Array.from(members.values()).map((member: GroupMember) => ({ + user: { + id: member.uin, + name: member.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${member.uin}&s=640`, + }, + nick: member.cardName || member.nick, + joined_at: member.joinTime ? Number(member.joinTime) * 1000 : undefined, + })); + + return { + data: memberList, + }; + } +} diff --git a/packages/napcat-satori/action/guild/GuildMemberMute.ts b/packages/napcat-satori/action/guild/GuildMemberMute.ts new file mode 100644 index 00000000..a8481ec8 --- /dev/null +++ b/packages/napcat-satori/action/guild/GuildMemberMute.ts @@ -0,0 +1,23 @@ +import { SatoriAction } from '../SatoriAction'; + +interface GuildMemberMutePayload { + guild_id: string; + user_id: string; + duration?: number; // 禁言时长(毫秒),0 表示解除禁言 +} + +export class GuildMemberMuteAction extends SatoriAction { + actionName = 'guild.member.mute'; + + async handle (payload: GuildMemberMutePayload): Promise { + const { guild_id, user_id, duration } = payload; + + // 将毫秒转换为秒 + const durationSeconds = duration ? Math.floor(duration / 1000) : 0; + + await this.core.apis.GroupApi.banMember( + guild_id, + [{ uid: await this.core.apis.UserApi.getUidByUinV2(user_id), timeStamp: durationSeconds }] + ); + } +} diff --git a/packages/napcat-satori/action/index.ts b/packages/napcat-satori/action/index.ts new file mode 100644 index 00000000..36e0a5e4 --- /dev/null +++ b/packages/napcat-satori/action/index.ts @@ -0,0 +1,60 @@ +import { NapCatCore } from 'napcat-core'; +import { NapCatSatoriAdapter } from '@/napcat-satori/index'; +import { SatoriAction } from './SatoriAction'; + +// 导入所有 Action +import { MessageCreateAction } from './message/MessageCreate'; +import { MessageGetAction } from './message/MessageGet'; +import { MessageDeleteAction } from './message/MessageDelete'; +import { ChannelGetAction } from './channel/ChannelGet'; +import { ChannelListAction } from './channel/ChannelList'; +import { GuildGetAction } from './guild/GuildGet'; +import { GuildListAction } from './guild/GuildList'; +import { GuildMemberGetAction } from './guild/GuildMemberGet'; +import { GuildMemberListAction } from './guild/GuildMemberList'; +import { GuildMemberKickAction } from './guild/GuildMemberKick'; +import { GuildMemberMuteAction } from './guild/GuildMemberMute'; +import { UserGetAction } from './user/UserGet'; +import { FriendListAction } from './user/FriendList'; +import { FriendApproveAction } from './user/FriendApprove'; +import { UploadCreateAction } from './upload/UploadCreate'; + +export type SatoriActionMap = Map>; + +export function createSatoriActionMap ( + satoriAdapter: NapCatSatoriAdapter, + core: NapCatCore +): SatoriActionMap { + const actionMap: SatoriActionMap = new Map(); + + const actions: SatoriAction[] = [ + // 消息相关 + new MessageCreateAction(satoriAdapter, core), + new MessageGetAction(satoriAdapter, core), + new MessageDeleteAction(satoriAdapter, core), + // 频道相关 + new ChannelGetAction(satoriAdapter, core), + new ChannelListAction(satoriAdapter, core), + // 群组相关 + new GuildGetAction(satoriAdapter, core), + new GuildListAction(satoriAdapter, core), + new GuildMemberGetAction(satoriAdapter, core), + new GuildMemberListAction(satoriAdapter, core), + new GuildMemberKickAction(satoriAdapter, core), + new GuildMemberMuteAction(satoriAdapter, core), + // 用户相关 + new UserGetAction(satoriAdapter, core), + new FriendListAction(satoriAdapter, core), + new FriendApproveAction(satoriAdapter, core), + // 上传相关 + new UploadCreateAction(satoriAdapter, core), + ]; + + for (const action of actions) { + actionMap.set(action.actionName, action); + } + + return actionMap; +} + +export { SatoriAction } from './SatoriAction'; diff --git a/packages/napcat-satori/action/message/MessageCreate.ts b/packages/napcat-satori/action/message/MessageCreate.ts new file mode 100644 index 00000000..417eada9 --- /dev/null +++ b/packages/napcat-satori/action/message/MessageCreate.ts @@ -0,0 +1,69 @@ +import { SatoriAction } from '../SatoriAction'; +import { SatoriMessage, SatoriChannelType } from '@/napcat-satori/types'; +import { ChatType, SendMessageElement } from 'napcat-core'; + +interface MessageCreatePayload { + channel_id: string; + content: string; +} + +export class MessageCreateAction extends SatoriAction { + actionName = 'message.create'; + + async handle (payload: MessageCreatePayload): Promise { + const { channel_id, content } = payload; + + // 解析 channel_id,格式: private:{user_id} 或 group:{group_id} + const parts = channel_id.split(':'); + const type = parts[0]; + const id = parts[1]; + + if (!type || !id) { + throw new Error(`无效的频道ID格式: ${channel_id}`); + } + + let chatType: ChatType; + let peerUid: string; + + if (type === 'private') { + chatType = ChatType.KCHATTYPEC2C; + peerUid = await this.core.apis.UserApi.getUidByUinV2(id); + } else if (type === 'group') { + chatType = ChatType.KCHATTYPEGROUP; + peerUid = id; + } else { + throw new Error(`不支持的频道类型: ${type}`); + } + + // 解析 Satori 消息内容为 NapCat 消息元素 + const elements = await this.satoriAdapter.apis.MsgApi.parseContent(content); + + // 发送消息 + const result = await this.core.apis.MsgApi.sendMsg( + { chatType, peerUid, guildId: '' }, + elements as SendMessageElement[], + 30000 + ); + + if (!result) { + throw new Error('消息发送失败: 未知错误'); + } + + // 构造返回结果 + const message: SatoriMessage = { + id: result.msgId, + content, + channel: { + id: channel_id, + type: type === 'private' ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, + }, + user: { + id: this.selfInfo.uin, + name: this.selfInfo.nick, + }, + created_at: Date.now(), + }; + + return [message]; + } +} diff --git a/packages/napcat-satori/action/message/MessageDelete.ts b/packages/napcat-satori/action/message/MessageDelete.ts new file mode 100644 index 00000000..ebc408a6 --- /dev/null +++ b/packages/napcat-satori/action/message/MessageDelete.ts @@ -0,0 +1,39 @@ +import { SatoriAction } from '../SatoriAction'; +import { ChatType } from 'napcat-core'; + +interface MessageDeletePayload { + channel_id: string; + message_id: string; +} + +export class MessageDeleteAction extends SatoriAction { + actionName = 'message.delete'; + + async handle (payload: MessageDeletePayload): Promise { + const { channel_id, message_id } = payload; + + const parts = channel_id.split(':'); + const type = parts[0]; + const id = parts[1]; + + if (!type || !id) { + throw new Error(`无效的频道ID格式: ${channel_id}`); + } + + let chatType: ChatType; + let peerUid: string; + + if (type === 'private') { + chatType = ChatType.KCHATTYPEC2C; + peerUid = await this.core.apis.UserApi.getUidByUinV2(id); + } else if (type === 'group') { + chatType = ChatType.KCHATTYPEGROUP; + peerUid = id; + } else { + throw new Error(`不支持的频道类型: ${type}`); + } + + const peer = { chatType, peerUid, guildId: '' }; + await this.core.apis.MsgApi.recallMsg(peer, message_id); + } +} diff --git a/packages/napcat-satori/action/message/MessageGet.ts b/packages/napcat-satori/action/message/MessageGet.ts new file mode 100644 index 00000000..f1643f55 --- /dev/null +++ b/packages/napcat-satori/action/message/MessageGet.ts @@ -0,0 +1,57 @@ +import { SatoriAction } from '../SatoriAction'; +import { SatoriMessage, SatoriChannelType } from '@/napcat-satori/types'; +import { ChatType } from 'napcat-core'; + +interface MessageGetPayload { + channel_id: string; + message_id: string; +} + +export class MessageGetAction extends SatoriAction { + actionName = 'message.get'; + + async handle (payload: MessageGetPayload): Promise { + const { channel_id, message_id } = payload; + + const [type, id] = channel_id.split(':'); + + let chatType: ChatType; + let peerUid: string; + + if (type === 'private') { + chatType = ChatType.KCHATTYPEC2C; + peerUid = await this.core.apis.UserApi.getUidByUinV2(id); + } else if (type === 'group') { + chatType = ChatType.KCHATTYPEGROUP; + peerUid = id; + } else { + throw new Error(`不支持的频道类型: ${type}`); + } + + const peer = { chatType, peerUid, guildId: '' }; + const msgs = await this.core.apis.MsgApi.getMsgsByMsgId(peer, [message_id]); + + if (!msgs || msgs.msgList.length === 0) { + throw new Error('消息不存在'); + } + + const msg = msgs.msgList[0]; + const content = await this.satoriAdapter.apis.MsgApi.parseElements(msg.elements); + + const message: SatoriMessage = { + id: msg.msgId, + content, + channel: { + id: channel_id, + type: type === 'private' ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, + }, + user: { + id: msg.senderUin, + name: msg.sendNickName, + }, + created_at: parseInt(msg.msgTime) * 1000, + }; + + return message; + } +} diff --git a/packages/napcat-satori/action/upload/UploadCreate.ts b/packages/napcat-satori/action/upload/UploadCreate.ts new file mode 100644 index 00000000..0bb2042e --- /dev/null +++ b/packages/napcat-satori/action/upload/UploadCreate.ts @@ -0,0 +1,43 @@ +import { SatoriAction } from '../SatoriAction'; + +interface UploadCreatePayload { + [key: string]: unknown; +} + +interface UploadResult { + [key: string]: string; +} + +export class UploadCreateAction extends SatoriAction { + actionName = 'upload.create'; + + async handle (payload: UploadCreatePayload): Promise { + const result: UploadResult = {}; + + // 处理上传的文件 + for (const [key, value] of Object.entries(payload)) { + if (typeof value === 'string' && value.startsWith('data:')) { + // Base64 数据 + const matches = value.match(/^data:([^;]+);base64,(.+)$/); + if (matches && matches[1] && matches[2]) { + const mimeType = matches[1]; + const base64Data = matches[2]; + // 保存文件并返回 URL + const url = await this.saveFile(base64Data, mimeType); + result[key] = url; + } + } else if (typeof value === 'string') { + // 可能是 URL,直接返回 + result[key] = value; + } + } + + return result; + } + + private async saveFile (base64Data: string, _mimeType: string): Promise { + // 将 base64 数据保存为临时文件并返回 URL + // 这里简化处理,实际应该保存到文件系统 + return `base64://${base64Data}`; + } +} diff --git a/packages/napcat-satori/action/user/FriendApprove.ts b/packages/napcat-satori/action/user/FriendApprove.ts new file mode 100644 index 00000000..8b3a13f8 --- /dev/null +++ b/packages/napcat-satori/action/user/FriendApprove.ts @@ -0,0 +1,26 @@ +import { SatoriAction } from '../SatoriAction'; + +interface FriendApprovePayload { + message_id: string; + approve: boolean; + comment?: string; +} + +export class FriendApproveAction extends SatoriAction { + actionName = 'friend.approve'; + + async handle (payload: FriendApprovePayload): Promise { + const { message_id, approve } = payload; + + // message_id 格式: reqTime (好友请求的时间戳) + // 需要从好友请求列表中找到对应的请求 + const buddyReqData = await this.core.apis.FriendApi.getBuddyReq(); + const notify = buddyReqData.buddyReqs.find(e => e.reqTime === message_id); + + if (!notify) { + throw new Error(`未找到好友请求: ${message_id}`); + } + + await this.core.apis.FriendApi.handleFriendRequest(notify, approve); + } +} diff --git a/packages/napcat-satori/action/user/FriendList.ts b/packages/napcat-satori/action/user/FriendList.ts new file mode 100644 index 00000000..67940ea6 --- /dev/null +++ b/packages/napcat-satori/action/user/FriendList.ts @@ -0,0 +1,25 @@ +import { SatoriAction } from '../SatoriAction'; +import { SatoriUser, SatoriPageResult } from '@/napcat-satori/types'; + +interface FriendListPayload { + next?: string; +} + +export class FriendListAction extends SatoriAction> { + actionName = 'friend.list'; + + async handle (_payload: FriendListPayload): Promise> { + const friends = await this.core.apis.FriendApi.getBuddy(); + + const friendList: SatoriUser[] = friends.map((friend) => ({ + id: friend.uin || '', + name: friend.coreInfo?.nick || '', + nick: friend.coreInfo?.remark || friend.coreInfo?.nick || '', + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${friend.uin}&s=640`, + })); + + return { + data: friendList, + }; + } +} diff --git a/packages/napcat-satori/action/user/UserGet.ts b/packages/napcat-satori/action/user/UserGet.ts new file mode 100644 index 00000000..f318f5bc --- /dev/null +++ b/packages/napcat-satori/action/user/UserGet.ts @@ -0,0 +1,23 @@ +import { SatoriAction } from '../SatoriAction'; +import { SatoriUser } from '@/napcat-satori/types'; + +interface UserGetPayload { + user_id: string; +} + +export class UserGetAction extends SatoriAction { + actionName = 'user.get'; + + async handle (payload: UserGetPayload): Promise { + const { user_id } = payload; + + const uid = await this.core.apis.UserApi.getUidByUinV2(user_id); + const userInfo = await this.core.apis.UserApi.getUserDetailInfo(uid, false); + + return { + id: user_id, + name: userInfo.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${user_id}&s=640`, + }; + } +} diff --git a/packages/napcat-satori/api/event.ts b/packages/napcat-satori/api/event.ts new file mode 100644 index 00000000..798c579f --- /dev/null +++ b/packages/napcat-satori/api/event.ts @@ -0,0 +1,277 @@ +import { NapCatCore, RawMessage, ChatType } from 'napcat-core'; +import { NapCatSatoriAdapter } from '@/napcat-satori/index'; +import { + SatoriEvent, + SatoriChannelType, + SatoriLoginStatus, +} from '@/napcat-satori/types'; + +export class SatoriEventApi { + private satoriAdapter: NapCatSatoriAdapter; + private core: NapCatCore; + private eventId: number = 0; + + constructor (satoriAdapter: NapCatSatoriAdapter, core: NapCatCore) { + this.satoriAdapter = satoriAdapter; + this.core = core; + } + + private getNextEventId (): number { + return ++this.eventId; + } + + private get platform (): string { + return this.satoriAdapter.configLoader.configData.platform; + } + + private get selfId (): string { + return this.core.selfInfo.uin; + } + + /** + * 将 NapCat 消息转换为 Satori 事件 + */ + async createMessageEvent (message: RawMessage): Promise { + try { + const content = await this.satoriAdapter.apis.MsgApi.parseElements(message.elements); + const isPrivate = message.chatType === ChatType.KCHATTYPEC2C; + + const event: SatoriEvent = { + id: this.getNextEventId(), + type: 'message-created', + platform: this.platform, + self_id: this.selfId, + timestamp: parseInt(message.msgTime) * 1000, + channel: { + id: isPrivate ? `private:${message.senderUin}` : `group:${message.peerUin}`, + type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, + }, + user: { + id: message.senderUin, + name: message.sendNickName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${message.senderUin}&s=640`, + }, + message: { + id: message.msgId, + content, + }, + }; + + if (!isPrivate) { + event.guild = { + id: message.peerUin, + name: message.peerName, + avatar: `https://p.qlogo.cn/gh/${message.peerUin}/${message.peerUin}/640`, + }; + event.member = { + nick: message.sendMemberName || message.sendNickName, + }; + } + + return event; + } catch (error) { + this.core.context.logger.logError('[Satori] 创建消息事件失败:', error); + return null; + } + } + + /** + * 创建好友请求事件 + */ + createFriendRequestEvent ( + userId: string, + userName: string, + comment: string, + flag: string + ): SatoriEvent { + return { + id: this.getNextEventId(), + type: 'friend-request', + platform: this.platform, + self_id: this.selfId, + timestamp: Date.now(), + user: { + id: userId, + name: userName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, + }, + message: { + id: flag, + content: comment, + }, + }; + } + + /** + * 创建群组加入请求事件 + */ + createGuildMemberRequestEvent ( + guildId: string, + guildName: string, + userId: string, + userName: string, + comment: string, + flag: string + ): SatoriEvent { + return { + id: this.getNextEventId(), + type: 'guild-member-request', + platform: this.platform, + self_id: this.selfId, + timestamp: Date.now(), + guild: { + id: guildId, + name: guildName, + avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, + }, + user: { + id: userId, + name: userName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, + }, + message: { + id: flag, + content: comment, + }, + }; + } + + /** + * 创建群成员增加事件 + */ + createGuildMemberAddedEvent ( + guildId: string, + guildName: string, + userId: string, + userName: string, + operatorId?: string + ): SatoriEvent { + const event: SatoriEvent = { + id: this.getNextEventId(), + type: 'guild-member-added', + platform: this.platform, + self_id: this.selfId, + timestamp: Date.now(), + guild: { + id: guildId, + name: guildName, + avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, + }, + user: { + id: userId, + name: userName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, + }, + }; + + if (operatorId) { + event.operator = { + id: operatorId, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${operatorId}&s=640`, + }; + } + + return event; + } + + /** + * 创建群成员移除事件 + */ + createGuildMemberRemovedEvent ( + guildId: string, + guildName: string, + userId: string, + userName: string, + operatorId?: string + ): SatoriEvent { + const event: SatoriEvent = { + id: this.getNextEventId(), + type: 'guild-member-removed', + platform: this.platform, + self_id: this.selfId, + timestamp: Date.now(), + guild: { + id: guildId, + name: guildName, + avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, + }, + user: { + id: userId, + name: userName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, + }, + }; + + if (operatorId) { + event.operator = { + id: operatorId, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${operatorId}&s=640`, + }; + } + + return event; + } + + /** + * 创建消息删除事件 + */ + createMessageDeletedEvent ( + channelId: string, + messageId: string, + userId: string, + operatorId?: string + ): SatoriEvent { + const isPrivate = channelId.startsWith('private:'); + const event: SatoriEvent = { + id: this.getNextEventId(), + type: 'message-deleted', + platform: this.platform, + self_id: this.selfId, + timestamp: Date.now(), + channel: { + id: channelId, + type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, + }, + user: { + id: userId, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, + }, + message: { + id: messageId, + content: '', + }, + }; + + if (operatorId) { + event.operator = { + id: operatorId, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${operatorId}&s=640`, + }; + } + + return event; + } + + /** + * 创建登录状态更新事件 + */ + createLoginUpdatedEvent (status: SatoriLoginStatus): SatoriEvent { + return { + id: this.getNextEventId(), + type: 'login-updated', + platform: this.platform, + self_id: this.selfId, + timestamp: Date.now(), + login: { + user: { + id: this.selfId, + name: this.core.selfInfo.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.selfId}&s=640`, + }, + self_id: this.selfId, + platform: this.platform, + status, + }, + }; + } +} diff --git a/packages/napcat-satori/api/index.ts b/packages/napcat-satori/api/index.ts new file mode 100644 index 00000000..4edb63c2 --- /dev/null +++ b/packages/napcat-satori/api/index.ts @@ -0,0 +1,22 @@ +import { NapCatCore } from 'napcat-core'; +import { NapCatSatoriAdapter } from '@/napcat-satori/index'; +import { SatoriMsgApi } from './msg'; +import { SatoriEventApi } from './event'; + +export interface SatoriApiList { + MsgApi: SatoriMsgApi; + EventApi: SatoriEventApi; +} + +export function createSatoriApis ( + satoriAdapter: NapCatSatoriAdapter, + core: NapCatCore +): SatoriApiList { + return { + MsgApi: new SatoriMsgApi(satoriAdapter, core), + EventApi: new SatoriEventApi(satoriAdapter, core), + }; +} + +export { SatoriMsgApi } from './msg'; +export { SatoriEventApi } from './event'; diff --git a/packages/napcat-satori/api/msg.ts b/packages/napcat-satori/api/msg.ts new file mode 100644 index 00000000..9f0b2672 --- /dev/null +++ b/packages/napcat-satori/api/msg.ts @@ -0,0 +1,297 @@ +import { NapCatCore, MessageElement, ElementType, NTMsgAtType } from 'napcat-core'; +import { NapCatSatoriAdapter } from '@/napcat-satori/index'; + +export class SatoriMsgApi { + private core: NapCatCore; + + constructor (_satoriAdapter: NapCatSatoriAdapter, core: NapCatCore) { + this.core = core; + } + + /** + * 解析 Satori 消息内容为 NapCat 消息元素 + */ + async parseContent (content: string): Promise { + const elements: MessageElement[] = []; + + // 简单的 XML 解析 + const tagRegex = /<(\w+)([^>]*)(?:\/>|>([\s\S]*?)<\/\1>)/g; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = tagRegex.exec(content)) !== null) { + // 处理标签前的文本 + if (match.index > lastIndex) { + const text = content.slice(lastIndex, match.index); + if (text.trim()) { + elements.push(this.createTextElement(text)); + } + } + + const [, tagName, attrs = '', innerContent] = match; + const parsedAttrs = this.parseAttributes(attrs); + + switch (tagName) { + case 'at': + elements.push(await this.createAtElement(parsedAttrs)); + break; + case 'img': + case 'image': + elements.push(await this.createImageElement(parsedAttrs)); + break; + case 'audio': + elements.push(await this.createAudioElement(parsedAttrs)); + break; + case 'video': + elements.push(await this.createVideoElement(parsedAttrs)); + break; + case 'file': + elements.push(await this.createFileElement(parsedAttrs)); + break; + case 'face': + elements.push(this.createFaceElement(parsedAttrs)); + break; + case 'quote': + elements.push(await this.createQuoteElement(parsedAttrs)); + break; + default: + // 未知标签,作为文本处理 + if (innerContent) { + elements.push(this.createTextElement(innerContent)); + } + } + + lastIndex = match.index + match[0].length; + } + + // 处理剩余文本 + if (lastIndex < content.length) { + const text = content.slice(lastIndex); + if (text.trim()) { + elements.push(this.createTextElement(text)); + } + } + + // 如果没有解析到任何元素,将整个内容作为文本 + if (elements.length === 0 && content.trim()) { + elements.push(this.createTextElement(content)); + } + + return elements; + } + + /** + * 解析 NapCat 消息元素为 Satori 消息内容 + */ + async parseElements (elements: MessageElement[]): Promise { + const parts: string[] = []; + + for (const element of elements) { + switch (element.elementType) { + case ElementType.TEXT: + if (element.textElement) { + parts.push(this.escapeXml(element.textElement.content)); + } + break; + case ElementType.PIC: + if (element.picElement) { + const src = element.picElement.sourcePath || ''; + parts.push(``); + } + break; + case ElementType.PTT: + if (element.pttElement) { + const src = element.pttElement.filePath || ''; + parts.push(`