mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 22:51:13 +00:00
Refactor protocol management with napcat-protocol package
Introduced the new napcat-protocol package to unify protocol adapter management for OneBot and Satori. Updated napcat-framework and napcat-shell to use ProtocolManager instead of direct adapter instantiation. Added protocol info definitions to napcat-common, and integrated protocol configuration and management APIs into the web UI backend and frontend. This refactor improves maintainability, extensibility, and encapsulation of protocol logic, while maintaining backward compatibility.
This commit is contained in:
parent
7cd0e5b2a4
commit
506358e01a
259
PROTOCOL_REFACTOR.md
Normal file
259
PROTOCOL_REFACTOR.md
Normal file
@ -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` 包,实现了协议适配器的统一管理,提高了代码的可维护性和可扩展性。同时保持了完全的向后兼容性,不影响现有功能的使用。
|
||||
43
packages/napcat-common/src/protocol/index.ts
Normal file
43
packages/napcat-common/src/protocol/index.ts
Normal file
@ -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;
|
||||
}
|
||||
@ -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<SelfInfo>((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,
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
251
packages/napcat-protocol/README.md
Normal file
251
packages/napcat-protocol/README.md
Normal file
@ -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<void> {
|
||||
// 初始化逻辑
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
// 清理逻辑
|
||||
}
|
||||
|
||||
async reloadConfig(prevConfig: unknown, newConfig: unknown): Promise<void> {
|
||||
// 配置重载逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 实现工厂
|
||||
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<T>(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 主项目保持一致
|
||||
2
packages/napcat-protocol/adapters/index.ts
Normal file
2
packages/napcat-protocol/adapters/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './onebot';
|
||||
export * from './satori';
|
||||
67
packages/napcat-protocol/adapters/onebot.ts
Normal file
67
packages/napcat-protocol/adapters/onebot.ts
Normal file
@ -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<void> {
|
||||
await this.adapter.InitOneBot();
|
||||
}
|
||||
|
||||
async destroy (): Promise<void> {
|
||||
await this.adapter.networkManager.closeAllAdapters();
|
||||
}
|
||||
|
||||
async reloadConfig (_prevConfig: unknown, newConfig: unknown): Promise<void> {
|
||||
const now = newConfig as Parameters<typeof this.adapter.configLoader.save>[0];
|
||||
this.adapter.configLoader.save(now);
|
||||
// 内部会处理网络重载
|
||||
}
|
||||
|
||||
/** 获取原始适配器实例 */
|
||||
getRawAdapter (): NapCatOneBot11Adapter {
|
||||
return this.adapter;
|
||||
}
|
||||
|
||||
/** 获取配置加载器 */
|
||||
getConfigLoader (): OB11ConfigLoader {
|
||||
return this.adapter.configLoader;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OneBot11 协议适配器工厂
|
||||
*/
|
||||
export class OneBotProtocolAdapterFactory implements IProtocolAdapterFactory<OneBotProtocolAdapter> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
68
packages/napcat-protocol/adapters/satori.ts
Normal file
68
packages/napcat-protocol/adapters/satori.ts
Normal file
@ -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<void> {
|
||||
await this.adapter.InitSatori();
|
||||
}
|
||||
|
||||
async destroy (): Promise<void> {
|
||||
await this.adapter.networkManager.closeAllAdapters();
|
||||
}
|
||||
|
||||
async reloadConfig (prevConfig: unknown, newConfig: unknown): Promise<void> {
|
||||
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<SatoriProtocolAdapter> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
35
packages/napcat-protocol/index.ts
Normal file
35
packages/napcat-protocol/index.ts
Normal file
@ -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';
|
||||
184
packages/napcat-protocol/manager.ts
Normal file
184
packages/napcat-protocol/manager.ts
Normal file
@ -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<string, IProtocolAdapterFactory> = new Map();
|
||||
private adapters: Map<string, IProtocolAdapter> = 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<IProtocolAdapter | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
this.context.logger.log('[Protocol] 开始销毁所有协议...');
|
||||
|
||||
for (const [protocolId] of this.adapters) {
|
||||
await this.destroyProtocol(protocolId);
|
||||
}
|
||||
|
||||
this.initialized = false;
|
||||
this.context.logger.log('[Protocol] 所有协议已销毁');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取协议适配器
|
||||
*/
|
||||
getAdapter<T extends IProtocolAdapter = IProtocolAdapter> (protocolId: string): T | null {
|
||||
return (this.adapters.get(protocolId) as T) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OneBot 协议适配器
|
||||
*/
|
||||
getOneBotAdapter (): OneBotProtocolAdapter | null {
|
||||
return this.getAdapter<OneBotProtocolAdapter>('onebot11');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Satori 协议适配器
|
||||
*/
|
||||
getSatoriAdapter (): SatoriProtocolAdapter | null {
|
||||
return this.getAdapter<SatoriProtocolAdapter>('satori');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载协议配置
|
||||
*/
|
||||
async reloadProtocolConfig (protocolId: string, prevConfig: unknown, newConfig: unknown): Promise<void> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
19
packages/napcat-protocol/package.json
Normal file
19
packages/napcat-protocol/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
17
packages/napcat-protocol/tsconfig.json
Normal file
17
packages/napcat-protocol/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
64
packages/napcat-protocol/types.ts
Normal file
64
packages/napcat-protocol/types.ts
Normal file
@ -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<void>;
|
||||
/** 销毁协议适配器 */
|
||||
destroy (): Promise<void>;
|
||||
/** 重载配置 */
|
||||
reloadConfig (prevConfig: unknown, newConfig: unknown): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 协议适配器工厂接口
|
||||
*/
|
||||
export interface IProtocolAdapterFactory<T extends IProtocolAdapter = IProtocolAdapter> {
|
||||
/** 协议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<void>;
|
||||
27
packages/napcat-satori/action/SatoriAction.ts
Normal file
27
packages/napcat-satori/action/SatoriAction.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { NapCatCore } from 'napcat-core';
|
||||
import { NapCatSatoriAdapter } from '@/napcat-satori/index';
|
||||
|
||||
export abstract class SatoriAction<PayloadType, ReturnType> {
|
||||
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<ReturnType>;
|
||||
|
||||
protected get logger () {
|
||||
return this.core.context.logger;
|
||||
}
|
||||
|
||||
protected get selfInfo () {
|
||||
return this.core.selfInfo;
|
||||
}
|
||||
|
||||
protected get platform () {
|
||||
return this.satoriAdapter.configLoader.configData.platform;
|
||||
}
|
||||
}
|
||||
55
packages/napcat-satori/action/channel/ChannelGet.ts
Normal file
55
packages/napcat-satori/action/channel/ChannelGet.ts
Normal file
@ -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<ChannelGetPayload, SatoriChannel> {
|
||||
actionName = 'channel.get';
|
||||
|
||||
async handle (payload: ChannelGetPayload): Promise<SatoriChannel> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
39
packages/napcat-satori/action/channel/ChannelList.ts
Normal file
39
packages/napcat-satori/action/channel/ChannelList.ts
Normal file
@ -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<ChannelListPayload, SatoriPageResult<SatoriChannel>> {
|
||||
actionName = 'channel.list';
|
||||
|
||||
async handle (payload: ChannelListPayload): Promise<SatoriPageResult<SatoriChannel>> {
|
||||
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],
|
||||
};
|
||||
}
|
||||
}
|
||||
34
packages/napcat-satori/action/guild/GuildGet.ts
Normal file
34
packages/napcat-satori/action/guild/GuildGet.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { SatoriAction } from '../SatoriAction';
|
||||
import { SatoriGuild } from '@/napcat-satori/types';
|
||||
|
||||
interface GuildGetPayload {
|
||||
guild_id: string;
|
||||
}
|
||||
|
||||
export class GuildGetAction extends SatoriAction<GuildGetPayload, SatoriGuild> {
|
||||
actionName = 'guild.get';
|
||||
|
||||
async handle (payload: GuildGetPayload): Promise<SatoriGuild> {
|
||||
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`,
|
||||
};
|
||||
}
|
||||
}
|
||||
24
packages/napcat-satori/action/guild/GuildList.ts
Normal file
24
packages/napcat-satori/action/guild/GuildList.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { SatoriAction } from '../SatoriAction';
|
||||
import { SatoriGuild, SatoriPageResult } from '@/napcat-satori/types';
|
||||
|
||||
interface GuildListPayload {
|
||||
next?: string;
|
||||
}
|
||||
|
||||
export class GuildListAction extends SatoriAction<GuildListPayload, SatoriPageResult<SatoriGuild>> {
|
||||
actionName = 'guild.list';
|
||||
|
||||
async handle (_payload: GuildListPayload): Promise<SatoriPageResult<SatoriGuild>> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
31
packages/napcat-satori/action/guild/GuildMemberGet.ts
Normal file
31
packages/napcat-satori/action/guild/GuildMemberGet.ts
Normal file
@ -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<GuildMemberGetPayload, SatoriGuildMember> {
|
||||
actionName = 'guild.member.get';
|
||||
|
||||
async handle (payload: GuildMemberGetPayload): Promise<SatoriGuildMember> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
22
packages/napcat-satori/action/guild/GuildMemberKick.ts
Normal file
22
packages/napcat-satori/action/guild/GuildMemberKick.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { SatoriAction } from '../SatoriAction';
|
||||
|
||||
interface GuildMemberKickPayload {
|
||||
guild_id: string;
|
||||
user_id: string;
|
||||
permanent?: boolean;
|
||||
}
|
||||
|
||||
export class GuildMemberKickAction extends SatoriAction<GuildMemberKickPayload, void> {
|
||||
actionName = 'guild.member.kick';
|
||||
|
||||
async handle (payload: GuildMemberKickPayload): Promise<void> {
|
||||
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,
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
||||
34
packages/napcat-satori/action/guild/GuildMemberList.ts
Normal file
34
packages/napcat-satori/action/guild/GuildMemberList.ts
Normal file
@ -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<GuildMemberListPayload, SatoriPageResult<SatoriGuildMember>> {
|
||||
actionName = 'guild.member.list';
|
||||
|
||||
async handle (payload: GuildMemberListPayload): Promise<SatoriPageResult<SatoriGuildMember>> {
|
||||
const { guild_id } = payload;
|
||||
|
||||
// 使用 getGroupMemberAll 获取所有群成员
|
||||
const result = await this.core.apis.GroupApi.getGroupMemberAll(guild_id, true);
|
||||
const members: Map<string, GroupMember> = 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
23
packages/napcat-satori/action/guild/GuildMemberMute.ts
Normal file
23
packages/napcat-satori/action/guild/GuildMemberMute.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { SatoriAction } from '../SatoriAction';
|
||||
|
||||
interface GuildMemberMutePayload {
|
||||
guild_id: string;
|
||||
user_id: string;
|
||||
duration?: number; // 禁言时长(毫秒),0 表示解除禁言
|
||||
}
|
||||
|
||||
export class GuildMemberMuteAction extends SatoriAction<GuildMemberMutePayload, void> {
|
||||
actionName = 'guild.member.mute';
|
||||
|
||||
async handle (payload: GuildMemberMutePayload): Promise<void> {
|
||||
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 }]
|
||||
);
|
||||
}
|
||||
}
|
||||
60
packages/napcat-satori/action/index.ts
Normal file
60
packages/napcat-satori/action/index.ts
Normal file
@ -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<string, SatoriAction<unknown, unknown>>;
|
||||
|
||||
export function createSatoriActionMap (
|
||||
satoriAdapter: NapCatSatoriAdapter,
|
||||
core: NapCatCore
|
||||
): SatoriActionMap {
|
||||
const actionMap: SatoriActionMap = new Map();
|
||||
|
||||
const actions: SatoriAction<unknown, unknown>[] = [
|
||||
// 消息相关
|
||||
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';
|
||||
69
packages/napcat-satori/action/message/MessageCreate.ts
Normal file
69
packages/napcat-satori/action/message/MessageCreate.ts
Normal file
@ -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<MessageCreatePayload, SatoriMessage[]> {
|
||||
actionName = 'message.create';
|
||||
|
||||
async handle (payload: MessageCreatePayload): Promise<SatoriMessage[]> {
|
||||
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];
|
||||
}
|
||||
}
|
||||
39
packages/napcat-satori/action/message/MessageDelete.ts
Normal file
39
packages/napcat-satori/action/message/MessageDelete.ts
Normal file
@ -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<MessageDeletePayload, void> {
|
||||
actionName = 'message.delete';
|
||||
|
||||
async handle (payload: MessageDeletePayload): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
57
packages/napcat-satori/action/message/MessageGet.ts
Normal file
57
packages/napcat-satori/action/message/MessageGet.ts
Normal file
@ -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<MessageGetPayload, SatoriMessage> {
|
||||
actionName = 'message.get';
|
||||
|
||||
async handle (payload: MessageGetPayload): Promise<SatoriMessage> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
43
packages/napcat-satori/action/upload/UploadCreate.ts
Normal file
43
packages/napcat-satori/action/upload/UploadCreate.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { SatoriAction } from '../SatoriAction';
|
||||
|
||||
interface UploadCreatePayload {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface UploadResult {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export class UploadCreateAction extends SatoriAction<UploadCreatePayload, UploadResult> {
|
||||
actionName = 'upload.create';
|
||||
|
||||
async handle (payload: UploadCreatePayload): Promise<UploadResult> {
|
||||
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<string> {
|
||||
// 将 base64 数据保存为临时文件并返回 URL
|
||||
// 这里简化处理,实际应该保存到文件系统
|
||||
return `base64://${base64Data}`;
|
||||
}
|
||||
}
|
||||
26
packages/napcat-satori/action/user/FriendApprove.ts
Normal file
26
packages/napcat-satori/action/user/FriendApprove.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { SatoriAction } from '../SatoriAction';
|
||||
|
||||
interface FriendApprovePayload {
|
||||
message_id: string;
|
||||
approve: boolean;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export class FriendApproveAction extends SatoriAction<FriendApprovePayload, void> {
|
||||
actionName = 'friend.approve';
|
||||
|
||||
async handle (payload: FriendApprovePayload): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
25
packages/napcat-satori/action/user/FriendList.ts
Normal file
25
packages/napcat-satori/action/user/FriendList.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { SatoriAction } from '../SatoriAction';
|
||||
import { SatoriUser, SatoriPageResult } from '@/napcat-satori/types';
|
||||
|
||||
interface FriendListPayload {
|
||||
next?: string;
|
||||
}
|
||||
|
||||
export class FriendListAction extends SatoriAction<FriendListPayload, SatoriPageResult<SatoriUser>> {
|
||||
actionName = 'friend.list';
|
||||
|
||||
async handle (_payload: FriendListPayload): Promise<SatoriPageResult<SatoriUser>> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
23
packages/napcat-satori/action/user/UserGet.ts
Normal file
23
packages/napcat-satori/action/user/UserGet.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { SatoriAction } from '../SatoriAction';
|
||||
import { SatoriUser } from '@/napcat-satori/types';
|
||||
|
||||
interface UserGetPayload {
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
export class UserGetAction extends SatoriAction<UserGetPayload, SatoriUser> {
|
||||
actionName = 'user.get';
|
||||
|
||||
async handle (payload: UserGetPayload): Promise<SatoriUser> {
|
||||
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`,
|
||||
};
|
||||
}
|
||||
}
|
||||
277
packages/napcat-satori/api/event.ts
Normal file
277
packages/napcat-satori/api/event.ts
Normal file
@ -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<SatoriEvent | null> {
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
22
packages/napcat-satori/api/index.ts
Normal file
22
packages/napcat-satori/api/index.ts
Normal file
@ -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';
|
||||
297
packages/napcat-satori/api/msg.ts
Normal file
297
packages/napcat-satori/api/msg.ts
Normal file
@ -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<MessageElement[]> {
|
||||
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<string> {
|
||||
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(`<img src="${this.escapeXml(src)}"/>`);
|
||||
}
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
if (element.pttElement) {
|
||||
const src = element.pttElement.filePath || '';
|
||||
parts.push(`<audio src="${this.escapeXml(src)}"/>`);
|
||||
}
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
if (element.videoElement) {
|
||||
const src = element.videoElement.filePath || '';
|
||||
parts.push(`<video src="${this.escapeXml(src)}"/>`);
|
||||
}
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
if (element.fileElement) {
|
||||
const src = element.fileElement.filePath || '';
|
||||
parts.push(`<file src="${this.escapeXml(src)}"/>`);
|
||||
}
|
||||
break;
|
||||
case ElementType.FACE:
|
||||
if (element.faceElement) {
|
||||
parts.push(`<face id="${element.faceElement.faceIndex}"/>`);
|
||||
}
|
||||
break;
|
||||
case ElementType.REPLY:
|
||||
if (element.replyElement) {
|
||||
parts.push(`<quote id="${element.replyElement.sourceMsgIdInRecords}"/>`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// 其他类型暂不处理
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
private parseAttributes (attrString: string): Record<string, string> {
|
||||
const attrs: Record<string, string> = {};
|
||||
const attrRegex = /(\w+)=["']([^"']*)["']/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = attrRegex.exec(attrString)) !== null) {
|
||||
const key = match[1];
|
||||
const value = match[2];
|
||||
if (key !== undefined && value !== undefined) {
|
||||
attrs[key] = value;
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
private createTextElement (content: string): MessageElement {
|
||||
return {
|
||||
elementType: ElementType.TEXT,
|
||||
elementId: '',
|
||||
textElement: {
|
||||
content: this.unescapeXml(content),
|
||||
atType: NTMsgAtType.ATTYPEUNKNOWN,
|
||||
atUid: '',
|
||||
atTinyId: '',
|
||||
atNtUid: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async createAtElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const id = attrs['id'] || '';
|
||||
const type = attrs['type'];
|
||||
|
||||
if (type === 'all') {
|
||||
return {
|
||||
elementType: ElementType.TEXT,
|
||||
elementId: '',
|
||||
textElement: {
|
||||
content: '@全体成员',
|
||||
atType: NTMsgAtType.ATTYPEALL,
|
||||
atUid: '',
|
||||
atTinyId: '',
|
||||
atNtUid: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const uid = await this.core.apis.UserApi.getUidByUinV2(id);
|
||||
const userInfo = await this.core.apis.UserApi.getUserDetailInfo(uid, false);
|
||||
|
||||
return {
|
||||
elementType: ElementType.TEXT,
|
||||
elementId: '',
|
||||
textElement: {
|
||||
content: `@${userInfo.nick || id}`,
|
||||
atType: NTMsgAtType.ATTYPEONE,
|
||||
atUid: uid,
|
||||
atTinyId: '',
|
||||
atNtUid: uid,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async createImageElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const src = attrs['src'] || '';
|
||||
// 这里需要根据 src 类型处理(URL、base64、本地路径等)
|
||||
return {
|
||||
elementType: ElementType.PIC,
|
||||
elementId: '',
|
||||
picElement: {
|
||||
sourcePath: src,
|
||||
picWidth: parseInt(attrs['width'] || '0', 10),
|
||||
picHeight: parseInt(attrs['height'] || '0', 10),
|
||||
},
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private async createAudioElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const src = attrs['src'] || '';
|
||||
return {
|
||||
elementType: ElementType.PTT,
|
||||
elementId: '',
|
||||
pttElement: {
|
||||
filePath: src,
|
||||
duration: parseInt(attrs['duration'] || '0', 10),
|
||||
},
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private async createVideoElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const src = attrs['src'] || '';
|
||||
return {
|
||||
elementType: ElementType.VIDEO,
|
||||
elementId: '',
|
||||
videoElement: {
|
||||
filePath: src,
|
||||
videoMd5: '',
|
||||
thumbMd5: '',
|
||||
fileSize: '',
|
||||
},
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private async createFileElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const src = attrs['src'] || '';
|
||||
return {
|
||||
elementType: ElementType.FILE,
|
||||
elementId: '',
|
||||
fileElement: {
|
||||
filePath: src,
|
||||
fileName: attrs['title'] || '',
|
||||
fileSize: '',
|
||||
},
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private createFaceElement (attrs: Record<string, string>): MessageElement {
|
||||
return {
|
||||
elementType: ElementType.FACE,
|
||||
elementId: '',
|
||||
faceElement: {
|
||||
faceIndex: parseInt(attrs['id'] || '0', 10),
|
||||
faceType: 1,
|
||||
},
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private async createQuoteElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const id = attrs['id'] || '';
|
||||
return {
|
||||
elementType: ElementType.REPLY,
|
||||
elementId: '',
|
||||
replyElement: {
|
||||
sourceMsgIdInRecords: id,
|
||||
replayMsgSeq: '',
|
||||
replayMsgId: id,
|
||||
senderUin: '',
|
||||
senderUinStr: '',
|
||||
},
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private escapeXml (str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
private unescapeXml (str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
}
|
||||
65
packages/napcat-satori/config/config.ts
Normal file
65
packages/napcat-satori/config/config.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import Ajv from 'ajv';
|
||||
|
||||
// Satori WebSocket 服务器配置
|
||||
const SatoriWebSocketServerConfigSchema = Type.Object({
|
||||
name: Type.String({ default: 'satori-ws-server' }),
|
||||
enable: Type.Boolean({ default: false }),
|
||||
host: Type.String({ default: '127.0.0.1' }),
|
||||
port: Type.Number({ default: 5500 }),
|
||||
token: Type.String({ default: '' }),
|
||||
path: Type.String({ default: '/v1/events' }),
|
||||
debug: Type.Boolean({ default: false }),
|
||||
heartInterval: Type.Number({ default: 10000 }),
|
||||
});
|
||||
|
||||
// Satori WebHook 客户端配置
|
||||
const SatoriWebHookClientConfigSchema = Type.Object({
|
||||
name: Type.String({ default: 'satori-webhook-client' }),
|
||||
enable: Type.Boolean({ default: false }),
|
||||
url: Type.String({ default: 'http://localhost:8080/webhook' }),
|
||||
token: Type.String({ default: '' }),
|
||||
debug: Type.Boolean({ default: false }),
|
||||
});
|
||||
|
||||
// Satori HTTP 服务器配置
|
||||
const SatoriHttpServerConfigSchema = Type.Object({
|
||||
name: Type.String({ default: 'satori-http-server' }),
|
||||
enable: Type.Boolean({ default: false }),
|
||||
host: Type.String({ default: '127.0.0.1' }),
|
||||
port: Type.Number({ default: 5501 }),
|
||||
token: Type.String({ default: '' }),
|
||||
path: Type.String({ default: '/v1' }),
|
||||
debug: Type.Boolean({ default: false }),
|
||||
});
|
||||
|
||||
// Satori 网络配置
|
||||
const SatoriNetworkConfigSchema = Type.Object({
|
||||
websocketServers: Type.Array(SatoriWebSocketServerConfigSchema, { default: [] }),
|
||||
webhookClients: Type.Array(SatoriWebHookClientConfigSchema, { default: [] }),
|
||||
httpServers: Type.Array(SatoriHttpServerConfigSchema, { default: [] }),
|
||||
}, { default: {} });
|
||||
|
||||
// Satori 协议配置
|
||||
export const SatoriConfigSchema = Type.Object({
|
||||
network: SatoriNetworkConfigSchema,
|
||||
platform: Type.String({ default: 'qq' }),
|
||||
selfId: Type.String({ default: '' }),
|
||||
});
|
||||
|
||||
export type SatoriConfig = Static<typeof SatoriConfigSchema>;
|
||||
export type SatoriWebSocketServerConfig = Static<typeof SatoriWebSocketServerConfigSchema>;
|
||||
export type SatoriWebHookClientConfig = Static<typeof SatoriWebHookClientConfigSchema>;
|
||||
export type SatoriHttpServerConfig = Static<typeof SatoriHttpServerConfigSchema>;
|
||||
export type SatoriNetworkAdapterConfig = SatoriWebSocketServerConfig | SatoriWebHookClientConfig | SatoriHttpServerConfig;
|
||||
export type SatoriNetworkConfigKey = keyof SatoriConfig['network'];
|
||||
|
||||
export function loadSatoriConfig (config: Partial<SatoriConfig>): SatoriConfig {
|
||||
const ajv = new Ajv({ useDefaults: true, coerceTypes: true });
|
||||
const validate = ajv.compile(SatoriConfigSchema);
|
||||
const valid = validate(config);
|
||||
if (!valid) {
|
||||
throw new Error(ajv.errorsText(validate.errors));
|
||||
}
|
||||
return config as SatoriConfig;
|
||||
}
|
||||
59
packages/napcat-satori/config/index.ts
Normal file
59
packages/napcat-satori/config/index.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
import json5 from 'json5';
|
||||
import { SatoriConfig, SatoriConfigSchema, loadSatoriConfig } from './config';
|
||||
import { NapCatCore } from 'napcat-core';
|
||||
import { Static, TSchema } from '@sinclair/typebox';
|
||||
|
||||
export class SatoriConfigLoader<T extends TSchema = typeof SatoriConfigSchema> {
|
||||
public configData: Static<T>;
|
||||
private configPath: string;
|
||||
private core: NapCatCore;
|
||||
|
||||
constructor (core: NapCatCore, configBasePath: string, _schema: T) {
|
||||
this.core = core;
|
||||
const configFileName = `satori_${core.selfInfo.uin}.json`;
|
||||
this.configPath = `${configBasePath}/${configFileName}`;
|
||||
this.configData = this.loadConfig();
|
||||
}
|
||||
|
||||
private loadConfig (): Static<T> {
|
||||
let configData: Partial<Static<T>> = {};
|
||||
|
||||
if (existsSync(this.configPath)) {
|
||||
try {
|
||||
const fileContent = readFileSync(this.configPath, 'utf-8');
|
||||
configData = json5.parse(fileContent);
|
||||
} catch (e) {
|
||||
this.core.context.logger.logError('[Satori] 配置文件解析失败,使用默认配置', e);
|
||||
}
|
||||
}
|
||||
|
||||
const loadedConfig = loadSatoriConfig(configData as Partial<SatoriConfig>) as Static<T>;
|
||||
this.save(loadedConfig);
|
||||
return loadedConfig;
|
||||
}
|
||||
|
||||
public save (config: Static<T>): void {
|
||||
this.configData = config;
|
||||
const dir = dirname(this.configPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
public reload (): Static<T> {
|
||||
if (existsSync(this.configPath)) {
|
||||
try {
|
||||
const fileContent = readFileSync(this.configPath, 'utf-8');
|
||||
this.configData = loadSatoriConfig(json5.parse(fileContent)) as Static<T>;
|
||||
} catch (e) {
|
||||
this.core.context.logger.logError('[Satori] 配置文件重载失败', e);
|
||||
}
|
||||
}
|
||||
return this.configData;
|
||||
}
|
||||
}
|
||||
|
||||
export * from './config';
|
||||
316
packages/napcat-satori/index.ts
Normal file
316
packages/napcat-satori/index.ts
Normal file
@ -0,0 +1,316 @@
|
||||
import {
|
||||
ChatType,
|
||||
InstanceContext,
|
||||
NapCatCore,
|
||||
NodeIKernelBuddyListener,
|
||||
NodeIKernelGroupListener,
|
||||
NodeIKernelMsgListener,
|
||||
RawMessage,
|
||||
SendStatusType,
|
||||
NTMsgType,
|
||||
BuddyReqType,
|
||||
GroupNotifyMsgStatus,
|
||||
GroupNotifyMsgType,
|
||||
} from 'napcat-core';
|
||||
import { SatoriConfigLoader, SatoriConfig, SatoriConfigSchema, SatoriNetworkAdapterConfig } from '@/napcat-satori/config';
|
||||
import { NapCatPathWrapper } from 'napcat-common/src/path';
|
||||
import { createSatoriApis, SatoriApiList } from '@/napcat-satori/api';
|
||||
import { createSatoriActionMap, SatoriActionMap } from '@/napcat-satori/action';
|
||||
import {
|
||||
SatoriNetworkManager,
|
||||
SatoriWebSocketServerAdapter,
|
||||
SatoriHttpServerAdapter,
|
||||
SatoriWebHookClientAdapter,
|
||||
SatoriNetworkReloadType,
|
||||
ISatoriNetworkAdapter,
|
||||
} from '@/napcat-satori/network';
|
||||
import { SatoriLoginStatus } from '@/napcat-satori/types';
|
||||
import { MessageUnique } from 'napcat-common/src/message-unique';
|
||||
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
|
||||
|
||||
export class NapCatSatoriAdapter {
|
||||
readonly core: NapCatCore;
|
||||
readonly context: InstanceContext;
|
||||
|
||||
configLoader: SatoriConfigLoader;
|
||||
public apis: SatoriApiList;
|
||||
networkManager: SatoriNetworkManager;
|
||||
actions: SatoriActionMap;
|
||||
private readonly bootTime = Date.now() / 1000;
|
||||
|
||||
constructor (core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) {
|
||||
this.core = core;
|
||||
this.context = context;
|
||||
this.configLoader = new SatoriConfigLoader(core, pathWrapper.configPath, SatoriConfigSchema);
|
||||
this.apis = createSatoriApis(this, core);
|
||||
this.actions = createSatoriActionMap(this, core);
|
||||
this.networkManager = new SatoriNetworkManager();
|
||||
}
|
||||
|
||||
async createSatoriLog (config: SatoriConfig): Promise<string> {
|
||||
let log = '[network] 配置加载\n';
|
||||
for (const key of config.network.websocketServers) {
|
||||
log += `WebSocket服务: ${key.host}:${key.port}${key.path}, : ${key.enable ? '已启动' : '未启动'}\n`;
|
||||
}
|
||||
for (const key of config.network.httpServers) {
|
||||
log += `HTTP服务: ${key.host}:${key.port}${key.path}, : ${key.enable ? '已启动' : '未启动'}\n`;
|
||||
}
|
||||
for (const key of config.network.webhookClients) {
|
||||
log += `WebHook上报: ${key.url}, : ${key.enable ? '已启动' : '未启动'}\n`;
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
async InitSatori (): Promise<void> {
|
||||
const config = this.configLoader.configData;
|
||||
const serviceInfo = await this.createSatoriLog(config);
|
||||
this.context.logger.log(`[Notice] [Satori] ${serviceInfo}`);
|
||||
|
||||
// 注册网络适配器
|
||||
for (const wsConfig of config.network.websocketServers) {
|
||||
if (wsConfig.enable) {
|
||||
this.networkManager.registerAdapter(
|
||||
new SatoriWebSocketServerAdapter(wsConfig.name, wsConfig, this.core, this, this.actions)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const httpConfig of config.network.httpServers) {
|
||||
if (httpConfig.enable) {
|
||||
this.networkManager.registerAdapter(
|
||||
new SatoriHttpServerAdapter(httpConfig.name, httpConfig, this.core, this, this.actions)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const webhookConfig of config.network.webhookClients) {
|
||||
if (webhookConfig.enable) {
|
||||
this.networkManager.registerAdapter(
|
||||
new SatoriWebHookClientAdapter(webhookConfig.name, webhookConfig, this.core, this, this.actions)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await this.networkManager.openAllAdapters();
|
||||
|
||||
// 初始化监听器
|
||||
this.initMsgListener();
|
||||
this.initBuddyListener();
|
||||
this.initGroupListener();
|
||||
|
||||
// 发送登录成功事件
|
||||
const loginEvent = this.apis.EventApi.createLoginUpdatedEvent(SatoriLoginStatus.ONLINE);
|
||||
await this.networkManager.emitEvent(loginEvent);
|
||||
}
|
||||
|
||||
async reloadNetwork (prev: SatoriConfig, now: SatoriConfig): Promise<void> {
|
||||
const prevLog = await this.createSatoriLog(prev);
|
||||
const newLog = await this.createSatoriLog(now);
|
||||
this.context.logger.log(`[Notice] [Satori] 配置变更前:\n${prevLog}`);
|
||||
this.context.logger.log(`[Notice] [Satori] 配置变更后:\n${newLog}`);
|
||||
|
||||
await this.handleConfigChange(prev.network.websocketServers, now.network.websocketServers, SatoriWebSocketServerAdapter);
|
||||
await this.handleConfigChange(prev.network.httpServers, now.network.httpServers, SatoriHttpServerAdapter);
|
||||
await this.handleConfigChange(prev.network.webhookClients, now.network.webhookClients, SatoriWebHookClientAdapter);
|
||||
}
|
||||
|
||||
private async handleConfigChange<CT extends SatoriNetworkAdapterConfig> (
|
||||
prevConfig: SatoriNetworkAdapterConfig[],
|
||||
nowConfig: SatoriNetworkAdapterConfig[],
|
||||
adapterClass: new (
|
||||
...args: ConstructorParameters<typeof ISatoriNetworkAdapter<CT>>
|
||||
) => ISatoriNetworkAdapter<CT>
|
||||
): Promise<void> {
|
||||
// 比较旧的在新的找不到的回收
|
||||
for (const adapterConfig of prevConfig) {
|
||||
const existingAdapter = nowConfig.find((e) => e.name === adapterConfig.name);
|
||||
if (!existingAdapter) {
|
||||
const adapter = this.networkManager.findSomeAdapter(adapterConfig.name);
|
||||
if (adapter) {
|
||||
await this.networkManager.closeSomeAdaterWhenOpen([adapter]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通知新配置重载
|
||||
for (const adapterConfig of nowConfig) {
|
||||
const existingAdapter = this.networkManager.findSomeAdapter(adapterConfig.name);
|
||||
if (existingAdapter) {
|
||||
const networkChange = await existingAdapter.reload(adapterConfig);
|
||||
if (networkChange === SatoriNetworkReloadType.NetWorkClose) {
|
||||
await this.networkManager.closeSomeAdaterWhenOpen([existingAdapter]);
|
||||
}
|
||||
} else if (adapterConfig.enable) {
|
||||
const newAdapter = new adapterClass(adapterConfig.name, adapterConfig as CT, this.core, this, this.actions);
|
||||
await this.networkManager.registerAdapterAndOpen(newAdapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initMsgListener (): void {
|
||||
const msgListener = new NodeIKernelMsgListener();
|
||||
|
||||
msgListener.onRecvMsg = async (msgs) => {
|
||||
if (!this.networkManager.hasActiveAdapters()) return;
|
||||
|
||||
for (const msg of msgs) {
|
||||
if (this.bootTime > parseInt(msg.msgTime)) {
|
||||
continue;
|
||||
}
|
||||
msg.id = MessageUnique.createUniqueMsgId(
|
||||
{ chatType: msg.chatType, peerUid: msg.peerUid, guildId: '' },
|
||||
msg.msgId
|
||||
);
|
||||
await this.handleMessage(msg);
|
||||
}
|
||||
};
|
||||
|
||||
msgListener.onAddSendMsg = async (msg) => {
|
||||
try {
|
||||
if (msg.sendStatus === SendStatusType.KSEND_STATUS_SENDING) {
|
||||
const [updatemsgs] = await this.core.eventWrapper.registerListen(
|
||||
'NodeIKernelMsgListener/onMsgInfoListUpdate',
|
||||
(msgList: RawMessage[]) => {
|
||||
const report = msgList.find(
|
||||
(e) =>
|
||||
e.senderUin === this.core.selfInfo.uin &&
|
||||
e.sendStatus !== SendStatusType.KSEND_STATUS_SENDING &&
|
||||
e.msgId === msg.msgId
|
||||
);
|
||||
return !!report;
|
||||
},
|
||||
1,
|
||||
10 * 60 * 1000
|
||||
);
|
||||
const updatemsg = updatemsgs.find((e) => e.msgId === msg.msgId);
|
||||
if (updatemsg?.sendStatus === SendStatusType.KSEND_STATUS_SUCCESS) {
|
||||
updatemsg.id = MessageUnique.createUniqueMsgId(
|
||||
{ chatType: updatemsg.chatType, peerUid: updatemsg.peerUid, guildId: '' },
|
||||
updatemsg.msgId
|
||||
);
|
||||
await this.handleMessage(updatemsg);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.context.logger.logError('[Satori] 处理发送消息失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
msgListener.onMsgRecall = async (chatType: ChatType, uid: string, msgSeq: string) => {
|
||||
try {
|
||||
const peer = { chatType, peerUid: uid, guildId: '' };
|
||||
const msgs = await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, msgSeq);
|
||||
const msg = msgs.msgList.find((e) => e.msgType === NTMsgType.KMSGTYPEGRAYTIPS);
|
||||
|
||||
if (msg) {
|
||||
const channelId = chatType === ChatType.KCHATTYPEC2C
|
||||
? `private:${msg.senderUin}`
|
||||
: `group:${msg.peerUin}`;
|
||||
const event = this.apis.EventApi.createMessageDeletedEvent(
|
||||
channelId,
|
||||
msg.msgId,
|
||||
msg.senderUin
|
||||
);
|
||||
await this.networkManager.emitEvent(event);
|
||||
}
|
||||
} catch (error) {
|
||||
this.context.logger.logError('[Satori] 处理消息撤回失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
this.context.session.getMsgService().addKernelMsgListener(
|
||||
proxiedListenerOf(msgListener, this.context.logger)
|
||||
);
|
||||
}
|
||||
|
||||
private initBuddyListener (): void {
|
||||
const buddyListener = new NodeIKernelBuddyListener();
|
||||
|
||||
buddyListener.onBuddyReqChange = async (reqs) => {
|
||||
this.core.apis.FriendApi.clearBuddyReqUnreadCnt();
|
||||
|
||||
for (let i = 0; i < reqs.unreadNums; i++) {
|
||||
const req = reqs.buddyReqs[i];
|
||||
if (!req) continue;
|
||||
if (
|
||||
!!req.isInitiator ||
|
||||
(req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM) ||
|
||||
!req.isUnread
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const requesterUin = await this.core.apis.UserApi.getUinByUidV2(req.friendUid);
|
||||
const event = this.apis.EventApi.createFriendRequestEvent(
|
||||
requesterUin,
|
||||
req.friendNick || requesterUin,
|
||||
req.extWords,
|
||||
req.friendUid
|
||||
);
|
||||
await this.networkManager.emitEvent(event);
|
||||
} catch (error) {
|
||||
this.context.logger.logError('[Satori] 处理好友请求失败', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.context.session.getBuddyService().addKernelBuddyListener(
|
||||
proxiedListenerOf(buddyListener, this.context.logger)
|
||||
);
|
||||
}
|
||||
|
||||
private initGroupListener (): void {
|
||||
const groupListener = new NodeIKernelGroupListener();
|
||||
|
||||
groupListener.onGroupNotifiesUpdated = async (_, notifies) => {
|
||||
await this.core.apis.GroupApi.clearGroupNotifiesUnreadCount(false);
|
||||
if (!notifies[0]?.type) return;
|
||||
|
||||
for (const notify of notifies) {
|
||||
const notifyTime = parseInt(notify.seq) / 1000 / 1000;
|
||||
if (notifyTime < this.bootTime) continue;
|
||||
|
||||
try {
|
||||
if (
|
||||
[GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS].includes(notify.type) &&
|
||||
notify.status === GroupNotifyMsgStatus.KUNHANDLE
|
||||
) {
|
||||
const requestUin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid);
|
||||
const event = this.apis.EventApi.createGuildMemberRequestEvent(
|
||||
notify.group.groupCode,
|
||||
notify.group.groupName,
|
||||
requestUin,
|
||||
notify.user1.nickName || requestUin,
|
||||
notify.postscript,
|
||||
notify.seq
|
||||
);
|
||||
await this.networkManager.emitEvent(event);
|
||||
}
|
||||
} catch (error) {
|
||||
this.context.logger.logError('[Satori] 处理群通知失败', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.context.session.getGroupService().addKernelGroupListener(
|
||||
proxiedListenerOf(groupListener, this.context.logger)
|
||||
);
|
||||
}
|
||||
|
||||
private async handleMessage (message: RawMessage): Promise<void> {
|
||||
if (message.msgType === NTMsgType.KMSGTYPENULL) return;
|
||||
|
||||
try {
|
||||
const event = await this.apis.EventApi.createMessageEvent(message);
|
||||
if (event) {
|
||||
await this.networkManager.emitEvent(event);
|
||||
}
|
||||
} catch (error) {
|
||||
this.context.logger.logError('[Satori] 处理消息失败', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export * from './types';
|
||||
export * from './config';
|
||||
50
packages/napcat-satori/network/adapter.ts
Normal file
50
packages/napcat-satori/network/adapter.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { SatoriNetworkAdapterConfig } from '@/napcat-satori/config/config';
|
||||
import { LogWrapper } from 'napcat-core/helper/log';
|
||||
import { NapCatCore } from 'napcat-core';
|
||||
import { NapCatSatoriAdapter } from '@/napcat-satori/index';
|
||||
import { SatoriActionMap } from '@/napcat-satori/action';
|
||||
import { SatoriEvent } from '@/napcat-satori/types';
|
||||
|
||||
export enum SatoriNetworkReloadType {
|
||||
Normal = 0,
|
||||
NetWorkClose = 1,
|
||||
}
|
||||
|
||||
export type SatoriEmitEventContent = SatoriEvent;
|
||||
|
||||
export abstract class ISatoriNetworkAdapter<CT extends SatoriNetworkAdapterConfig> {
|
||||
name: string;
|
||||
isEnable: boolean = false;
|
||||
config: CT;
|
||||
readonly logger: LogWrapper;
|
||||
readonly core: NapCatCore;
|
||||
readonly satoriContext: NapCatSatoriAdapter;
|
||||
readonly actions: SatoriActionMap;
|
||||
|
||||
constructor (
|
||||
name: string,
|
||||
config: CT,
|
||||
core: NapCatCore,
|
||||
satoriContext: NapCatSatoriAdapter,
|
||||
actions: SatoriActionMap
|
||||
) {
|
||||
this.name = name;
|
||||
this.config = structuredClone(config);
|
||||
this.core = core;
|
||||
this.satoriContext = satoriContext;
|
||||
this.actions = actions;
|
||||
this.logger = core.context.logger;
|
||||
}
|
||||
|
||||
abstract onEvent<T extends SatoriEmitEventContent> (event: T): Promise<void>;
|
||||
|
||||
abstract open (): void | Promise<void>;
|
||||
|
||||
abstract close (): void | Promise<void>;
|
||||
|
||||
abstract reload (config: unknown): SatoriNetworkReloadType | Promise<SatoriNetworkReloadType>;
|
||||
|
||||
get isActive (): boolean {
|
||||
return this.isEnable;
|
||||
}
|
||||
}
|
||||
356
packages/napcat-satori/network/http-server.ts
Normal file
356
packages/napcat-satori/network/http-server.ts
Normal file
@ -0,0 +1,356 @@
|
||||
import express, { Express, Request, Response, NextFunction } from 'express';
|
||||
import { createServer, Server } from 'http';
|
||||
import { NapCatCore } from 'napcat-core';
|
||||
import { NapCatSatoriAdapter } from '@/napcat-satori/index';
|
||||
import { SatoriActionMap } from '@/napcat-satori/action';
|
||||
import { SatoriHttpServerConfig } from '@/napcat-satori/config/config';
|
||||
import {
|
||||
ISatoriNetworkAdapter,
|
||||
SatoriEmitEventContent,
|
||||
SatoriNetworkReloadType,
|
||||
} from './adapter';
|
||||
import { SatoriApiResponse, SatoriLoginStatus } from '@/napcat-satori/types';
|
||||
|
||||
export class SatoriHttpServerAdapter extends ISatoriNetworkAdapter<SatoriHttpServerConfig> {
|
||||
private app: Express | null = null;
|
||||
private server: Server | null = null;
|
||||
|
||||
constructor (
|
||||
name: string,
|
||||
config: SatoriHttpServerConfig,
|
||||
core: NapCatCore,
|
||||
satoriContext: NapCatSatoriAdapter,
|
||||
actions: SatoriActionMap
|
||||
) {
|
||||
super(name, config, core, satoriContext, actions);
|
||||
}
|
||||
|
||||
async open (): Promise<void> {
|
||||
if (this.isEnable) return;
|
||||
|
||||
try {
|
||||
this.app = express();
|
||||
this.app.use(express.json());
|
||||
|
||||
// Token 验证中间件
|
||||
this.app.use(this.config.path || '/v1', (req: Request, res: Response, next: NextFunction): void => {
|
||||
if (this.config.token) {
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
|
||||
if (token !== this.config.token) {
|
||||
res.status(401).json({ error: { code: 401, message: 'Unauthorized' } });
|
||||
return;
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// 注册 API 路由
|
||||
this.registerRoutes();
|
||||
|
||||
this.server = createServer(this.app);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.server!.listen(this.config.port, this.config.host, () => {
|
||||
this.logger.log(`[Satori] HTTP服务器已启动: http://${this.config.host}:${this.config.port}${this.config.path}`);
|
||||
resolve();
|
||||
});
|
||||
this.server!.on('error', reject);
|
||||
});
|
||||
|
||||
this.isEnable = true;
|
||||
} catch (error) {
|
||||
this.logger.logError(`[Satori] HTTP服务器启动失败: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
if (!this.isEnable) return;
|
||||
|
||||
if (this.server) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.server!.close(() => resolve());
|
||||
});
|
||||
this.server = null;
|
||||
}
|
||||
|
||||
this.app = null;
|
||||
this.isEnable = false;
|
||||
this.logger.log(`[Satori] HTTP服务器已关闭`);
|
||||
}
|
||||
|
||||
async reload (config: SatoriHttpServerConfig): Promise<SatoriNetworkReloadType> {
|
||||
const needRestart =
|
||||
this.config.host !== config.host ||
|
||||
this.config.port !== config.port ||
|
||||
this.config.path !== config.path;
|
||||
|
||||
this.config = structuredClone(config);
|
||||
|
||||
if (!config.enable) {
|
||||
return SatoriNetworkReloadType.NetWorkClose;
|
||||
}
|
||||
|
||||
if (needRestart && this.isEnable) {
|
||||
await this.close();
|
||||
await this.open();
|
||||
}
|
||||
|
||||
return SatoriNetworkReloadType.Normal;
|
||||
}
|
||||
|
||||
async onEvent<T extends SatoriEmitEventContent> (_event: T): Promise<void> {
|
||||
// HTTP 服务器不主动推送事件
|
||||
}
|
||||
|
||||
private registerRoutes (): void {
|
||||
if (!this.app) return;
|
||||
|
||||
const basePath = this.config.path || '/v1';
|
||||
const router = express.Router();
|
||||
|
||||
// 获取登录信息
|
||||
router.post('/login.get', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const result = {
|
||||
user: {
|
||||
id: this.core.selfInfo.uin,
|
||||
name: this.core.selfInfo.nick,
|
||||
avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.core.selfInfo.uin}&s=640`,
|
||||
},
|
||||
self_id: this.core.selfInfo.uin,
|
||||
platform: this.satoriContext.configLoader.configData.platform,
|
||||
status: SatoriLoginStatus.ONLINE,
|
||||
};
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取登录信息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
router.post('/message.create', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { channel_id, content } = req.body;
|
||||
if (!channel_id || !content) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('message.create')?.handle({ channel_id, content });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `发送消息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取消息
|
||||
router.post('/message.get', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { channel_id, message_id } = req.body;
|
||||
if (!channel_id || !message_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('message.get')?.handle({ channel_id, message_id });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取消息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除消息
|
||||
router.post('/message.delete', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { channel_id, message_id } = req.body;
|
||||
if (!channel_id || !message_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
await this.actions.get('message.delete')?.handle({ channel_id, message_id });
|
||||
this.sendSuccess(res, {});
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `删除消息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取频道信息
|
||||
router.post('/channel.get', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { channel_id } = req.body;
|
||||
if (!channel_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('channel.get')?.handle({ channel_id });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取频道信息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取频道列表
|
||||
router.post('/channel.list', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id, next } = req.body;
|
||||
if (!guild_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('channel.list')?.handle({ guild_id, next });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取频道列表失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取群组信息
|
||||
router.post('/guild.get', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id } = req.body;
|
||||
if (!guild_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('guild.get')?.handle({ guild_id });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取群组信息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取群组列表
|
||||
router.post('/guild.list', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { next } = req.body;
|
||||
const result = await this.actions.get('guild.list')?.handle({ next });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取群组列表失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取群成员信息
|
||||
router.post('/guild.member.get', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id, user_id } = req.body;
|
||||
if (!guild_id || !user_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('guild.member.get')?.handle({ guild_id, user_id });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取群成员信息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取群成员列表
|
||||
router.post('/guild.member.list', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id, next } = req.body;
|
||||
if (!guild_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('guild.member.list')?.handle({ guild_id, next });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取群成员列表失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取用户信息
|
||||
router.post('/user.get', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { user_id } = req.body;
|
||||
if (!user_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('user.get')?.handle({ user_id });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取用户信息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取好友列表
|
||||
router.post('/friend.list', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { next } = req.body;
|
||||
const result = await this.actions.get('friend.list')?.handle({ next });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取好友列表失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理好友请求
|
||||
router.post('/friend.approve', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { message_id, approve, comment } = req.body;
|
||||
if (message_id === undefined || approve === undefined) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
await this.actions.get('friend.approve')?.handle({ message_id, approve, comment });
|
||||
this.sendSuccess(res, {});
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `处理好友请求失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 踢出群成员
|
||||
router.post('/guild.member.kick', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id, user_id, permanent } = req.body;
|
||||
if (!guild_id || !user_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
await this.actions.get('guild.member.kick')?.handle({ guild_id, user_id, permanent });
|
||||
this.sendSuccess(res, {});
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `踢出群成员失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 禁言群成员
|
||||
router.post('/guild.member.mute', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id, user_id, duration } = req.body;
|
||||
if (!guild_id || !user_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
await this.actions.get('guild.member.mute')?.handle({ guild_id, user_id, duration });
|
||||
this.sendSuccess(res, {});
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `禁言群成员失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 上传文件
|
||||
router.post('/upload.create', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const result = await this.actions.get('upload.create')?.handle(req.body);
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `上传文件失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.app.use(basePath, router);
|
||||
}
|
||||
|
||||
private sendSuccess<T> (res: Response, data: T): void {
|
||||
const response: SatoriApiResponse<T> = { data };
|
||||
res.json(response);
|
||||
}
|
||||
|
||||
private sendError (res: Response, code: number, message: string): void {
|
||||
const response: SatoriApiResponse = { error: { code, message } };
|
||||
res.status(code >= 400 && code < 600 ? code : 500).json(response);
|
||||
}
|
||||
}
|
||||
72
packages/napcat-satori/network/index.ts
Normal file
72
packages/napcat-satori/network/index.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { ISatoriNetworkAdapter, SatoriEmitEventContent, SatoriNetworkReloadType } from './adapter';
|
||||
import { SatoriNetworkAdapterConfig } from '@/napcat-satori/config/config';
|
||||
|
||||
export class SatoriNetworkManager {
|
||||
adapters: Map<string, ISatoriNetworkAdapter<SatoriNetworkAdapterConfig>> = new Map();
|
||||
|
||||
async registerAdapter<T extends SatoriNetworkAdapterConfig> (
|
||||
adapter: ISatoriNetworkAdapter<T>
|
||||
): Promise<void> {
|
||||
this.adapters.set(adapter.name, adapter as ISatoriNetworkAdapter<SatoriNetworkAdapterConfig>);
|
||||
}
|
||||
|
||||
async registerAdapterAndOpen<T extends SatoriNetworkAdapterConfig> (
|
||||
adapter: ISatoriNetworkAdapter<T>
|
||||
): Promise<void> {
|
||||
await this.registerAdapter(adapter);
|
||||
await adapter.open();
|
||||
}
|
||||
|
||||
findSomeAdapter (name: string): ISatoriNetworkAdapter<SatoriNetworkAdapterConfig> | undefined {
|
||||
return this.adapters.get(name);
|
||||
}
|
||||
|
||||
async openAllAdapters (): Promise<void> {
|
||||
const openPromises = Array.from(this.adapters.values()).map((adapter) =>
|
||||
adapter.open().catch((e) => {
|
||||
adapter.logger.logError(`[Satori] 适配器 ${adapter.name} 启动失败: ${e}`);
|
||||
})
|
||||
);
|
||||
await Promise.all(openPromises);
|
||||
}
|
||||
|
||||
async closeAllAdapters (): Promise<void> {
|
||||
const closePromises = Array.from(this.adapters.values()).map((adapter) =>
|
||||
adapter.close().catch((e) => {
|
||||
adapter.logger.logError(`[Satori] 适配器 ${adapter.name} 关闭失败: ${e}`);
|
||||
})
|
||||
);
|
||||
await Promise.all(closePromises);
|
||||
}
|
||||
|
||||
async closeSomeAdaterWhenOpen (
|
||||
adapters: ISatoriNetworkAdapter<SatoriNetworkAdapterConfig>[]
|
||||
): Promise<void> {
|
||||
for (const adapter of adapters) {
|
||||
if (adapter.isActive) {
|
||||
await adapter.close();
|
||||
}
|
||||
this.adapters.delete(adapter.name);
|
||||
}
|
||||
}
|
||||
|
||||
async emitEvent<T extends SatoriEmitEventContent> (event: T): Promise<void> {
|
||||
const emitPromises = Array.from(this.adapters.values())
|
||||
.filter((adapter) => adapter.isActive)
|
||||
.map((adapter) =>
|
||||
adapter.onEvent(event).catch((e) => {
|
||||
adapter.logger.logError(`[Satori] 适配器 ${adapter.name} 事件发送失败: ${e}`);
|
||||
})
|
||||
);
|
||||
await Promise.all(emitPromises);
|
||||
}
|
||||
|
||||
hasActiveAdapters (): boolean {
|
||||
return Array.from(this.adapters.values()).some((adapter) => adapter.isActive);
|
||||
}
|
||||
}
|
||||
|
||||
export { ISatoriNetworkAdapter, SatoriEmitEventContent, SatoriNetworkReloadType } from './adapter';
|
||||
export { SatoriWebSocketServerAdapter } from './websocket-server';
|
||||
export { SatoriHttpServerAdapter } from './http-server';
|
||||
export { SatoriWebHookClientAdapter } from './webhook-client';
|
||||
95
packages/napcat-satori/network/webhook-client.ts
Normal file
95
packages/napcat-satori/network/webhook-client.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { NapCatCore } from 'napcat-core';
|
||||
import { NapCatSatoriAdapter } from '@/napcat-satori/index';
|
||||
import { SatoriActionMap } from '@/napcat-satori/action';
|
||||
import { SatoriWebHookClientConfig } from '@/napcat-satori/config/config';
|
||||
import {
|
||||
ISatoriNetworkAdapter,
|
||||
SatoriEmitEventContent,
|
||||
SatoriNetworkReloadType,
|
||||
} from './adapter';
|
||||
|
||||
export class SatoriWebHookClientAdapter extends ISatoriNetworkAdapter<SatoriWebHookClientConfig> {
|
||||
private eventQueue: SatoriEmitEventContent[] = [];
|
||||
private isSending: boolean = false;
|
||||
|
||||
constructor (
|
||||
name: string,
|
||||
config: SatoriWebHookClientConfig,
|
||||
core: NapCatCore,
|
||||
satoriContext: NapCatSatoriAdapter,
|
||||
actions: SatoriActionMap
|
||||
) {
|
||||
super(name, config, core, satoriContext, actions);
|
||||
}
|
||||
|
||||
async open (): Promise<void> {
|
||||
if (this.isEnable) return;
|
||||
this.isEnable = true;
|
||||
this.logger.log(`[Satori] WebHook客户端已启动: ${this.config.url}`);
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
if (!this.isEnable) return;
|
||||
this.isEnable = false;
|
||||
this.eventQueue = [];
|
||||
this.logger.log(`[Satori] WebHook客户端已关闭`);
|
||||
}
|
||||
|
||||
async reload (config: SatoriWebHookClientConfig): Promise<SatoriNetworkReloadType> {
|
||||
this.config = structuredClone(config);
|
||||
|
||||
if (!config.enable) {
|
||||
return SatoriNetworkReloadType.NetWorkClose;
|
||||
}
|
||||
|
||||
return SatoriNetworkReloadType.Normal;
|
||||
}
|
||||
|
||||
async onEvent<T extends SatoriEmitEventContent> (event: T): Promise<void> {
|
||||
if (!this.isEnable) return;
|
||||
|
||||
this.eventQueue.push(event);
|
||||
this.processQueue();
|
||||
}
|
||||
|
||||
private async processQueue (): Promise<void> {
|
||||
if (this.isSending || this.eventQueue.length === 0) return;
|
||||
|
||||
this.isSending = true;
|
||||
|
||||
while (this.eventQueue.length > 0) {
|
||||
const event = this.eventQueue.shift();
|
||||
if (event) {
|
||||
await this.sendEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
this.isSending = false;
|
||||
}
|
||||
|
||||
private async sendEvent (event: SatoriEmitEventContent): Promise<void> {
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (this.config.token) {
|
||||
headers['Authorization'] = `Bearer ${this.config.token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(this.config.url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(event),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.logger.logError(`[Satori] WebHook发送失败: ${response.status} ${response.statusText}`);
|
||||
} else if (this.config.debug) {
|
||||
this.logger.logDebug(`[Satori] WebHook发送成功: ${event.type}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.logError(`[Satori] WebHook发送错误: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
258
packages/napcat-satori/network/websocket-server.ts
Normal file
258
packages/napcat-satori/network/websocket-server.ts
Normal file
@ -0,0 +1,258 @@
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { createServer, Server, IncomingMessage } from 'http';
|
||||
import { NapCatCore } from 'napcat-core';
|
||||
import { NapCatSatoriAdapter } from '@/napcat-satori/index';
|
||||
import { SatoriActionMap } from '@/napcat-satori/action';
|
||||
import { SatoriWebSocketServerConfig } from '@/napcat-satori/config/config';
|
||||
import {
|
||||
ISatoriNetworkAdapter,
|
||||
SatoriEmitEventContent,
|
||||
SatoriNetworkReloadType,
|
||||
} from './adapter';
|
||||
import {
|
||||
SatoriOpcode,
|
||||
SatoriSignal,
|
||||
SatoriIdentifyBody,
|
||||
SatoriReadyBody,
|
||||
SatoriLoginStatus,
|
||||
} from '@/napcat-satori/types';
|
||||
|
||||
interface ClientInfo {
|
||||
ws: WebSocket;
|
||||
identified: boolean;
|
||||
sequence: number;
|
||||
}
|
||||
|
||||
export class SatoriWebSocketServerAdapter extends ISatoriNetworkAdapter<SatoriWebSocketServerConfig> {
|
||||
private server: Server | null = null;
|
||||
private wss: WebSocketServer | null = null;
|
||||
private clients: Map<WebSocket, ClientInfo> = new Map();
|
||||
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||
private eventSequence: number = 0;
|
||||
|
||||
constructor (
|
||||
name: string,
|
||||
config: SatoriWebSocketServerConfig,
|
||||
core: NapCatCore,
|
||||
satoriContext: NapCatSatoriAdapter,
|
||||
actions: SatoriActionMap
|
||||
) {
|
||||
super(name, config, core, satoriContext, actions);
|
||||
}
|
||||
|
||||
async open (): Promise<void> {
|
||||
if (this.isEnable) return;
|
||||
|
||||
try {
|
||||
this.server = createServer();
|
||||
this.wss = new WebSocketServer({
|
||||
server: this.server,
|
||||
path: this.config.path || '/v1/events',
|
||||
});
|
||||
|
||||
this.wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
|
||||
this.handleConnection(ws, req);
|
||||
});
|
||||
|
||||
this.wss.on('error', (error) => {
|
||||
this.logger.logError(`[Satori] WebSocket服务器错误: ${error.message}`);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.server!.listen(this.config.port, this.config.host, () => {
|
||||
this.logger.log(`[Satori] WebSocket服务器已启动: ws://${this.config.host}:${this.config.port}${this.config.path}`);
|
||||
resolve();
|
||||
});
|
||||
this.server!.on('error', reject);
|
||||
});
|
||||
|
||||
this.startHeartbeat();
|
||||
this.isEnable = true;
|
||||
} catch (error) {
|
||||
this.logger.logError(`[Satori] WebSocket服务器启动失败: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
if (!this.isEnable) return;
|
||||
|
||||
this.stopHeartbeat();
|
||||
|
||||
for (const [ws] of this.clients) {
|
||||
ws.close(1000, 'Server shutting down');
|
||||
}
|
||||
this.clients.clear();
|
||||
|
||||
if (this.wss) {
|
||||
this.wss.close();
|
||||
this.wss = null;
|
||||
}
|
||||
|
||||
if (this.server) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.server!.close(() => resolve());
|
||||
});
|
||||
this.server = null;
|
||||
}
|
||||
|
||||
this.isEnable = false;
|
||||
this.logger.log(`[Satori] WebSocket服务器已关闭`);
|
||||
}
|
||||
|
||||
async reload (config: SatoriWebSocketServerConfig): Promise<SatoriNetworkReloadType> {
|
||||
const needRestart =
|
||||
this.config.host !== config.host ||
|
||||
this.config.port !== config.port ||
|
||||
this.config.path !== config.path;
|
||||
|
||||
this.config = structuredClone(config);
|
||||
|
||||
if (!config.enable) {
|
||||
return SatoriNetworkReloadType.NetWorkClose;
|
||||
}
|
||||
|
||||
if (needRestart && this.isEnable) {
|
||||
await this.close();
|
||||
await this.open();
|
||||
}
|
||||
|
||||
return SatoriNetworkReloadType.Normal;
|
||||
}
|
||||
|
||||
async onEvent<T extends SatoriEmitEventContent> (event: T): Promise<void> {
|
||||
if (!this.isEnable) return;
|
||||
|
||||
this.eventSequence++;
|
||||
const signal: SatoriSignal<T> = {
|
||||
op: SatoriOpcode.EVENT,
|
||||
body: {
|
||||
...event,
|
||||
id: this.eventSequence,
|
||||
} as T,
|
||||
};
|
||||
|
||||
const message = JSON.stringify(signal);
|
||||
|
||||
for (const [ws, clientInfo] of this.clients) {
|
||||
if (clientInfo.identified && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(message);
|
||||
clientInfo.sequence = this.eventSequence;
|
||||
if (this.config.debug) {
|
||||
this.logger.logDebug(`[Satori] 发送事件: ${event.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleConnection (ws: WebSocket, req: IncomingMessage): void {
|
||||
const clientInfo: ClientInfo = {
|
||||
ws,
|
||||
identified: false,
|
||||
sequence: 0,
|
||||
};
|
||||
this.clients.set(ws, clientInfo);
|
||||
|
||||
this.logger.log(`[Satori] 新客户端连接: ${req.socket.remoteAddress}`);
|
||||
|
||||
ws.on('message', (data) => {
|
||||
this.handleMessage(ws, data.toString());
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
this.clients.delete(ws);
|
||||
this.logger.log(`[Satori] 客户端断开连接`);
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
this.logger.logError(`[Satori] 客户端错误: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
private handleMessage (ws: WebSocket, data: string): void {
|
||||
try {
|
||||
const signal: SatoriSignal = JSON.parse(data);
|
||||
const clientInfo = this.clients.get(ws);
|
||||
if (!clientInfo) return;
|
||||
|
||||
switch (signal.op) {
|
||||
case SatoriOpcode.IDENTIFY:
|
||||
this.handleIdentify(ws, clientInfo, signal.body as SatoriIdentifyBody);
|
||||
break;
|
||||
case SatoriOpcode.PING:
|
||||
this.sendPong(ws);
|
||||
break;
|
||||
default:
|
||||
this.logger.logDebug(`[Satori] 收到未知信令: ${signal.op}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.logError(`[Satori] 消息解析失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private handleIdentify (ws: WebSocket, clientInfo: ClientInfo, body: SatoriIdentifyBody): void {
|
||||
// 验证 token
|
||||
if (this.config.token && body.token !== this.config.token) {
|
||||
ws.close(4001, 'Invalid token');
|
||||
return;
|
||||
}
|
||||
|
||||
clientInfo.identified = true;
|
||||
if (body.sequence) {
|
||||
clientInfo.sequence = body.sequence;
|
||||
}
|
||||
|
||||
// 发送 READY 信令
|
||||
const readyBody: SatoriReadyBody = {
|
||||
logins: [{
|
||||
user: {
|
||||
id: this.core.selfInfo.uin,
|
||||
name: this.core.selfInfo.nick,
|
||||
avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.core.selfInfo.uin}&s=640`,
|
||||
},
|
||||
self_id: this.core.selfInfo.uin,
|
||||
platform: this.satoriContext.configLoader.configData.platform,
|
||||
status: SatoriLoginStatus.ONLINE,
|
||||
}],
|
||||
};
|
||||
|
||||
const readySignal: SatoriSignal<SatoriReadyBody> = {
|
||||
op: SatoriOpcode.READY,
|
||||
body: readyBody,
|
||||
};
|
||||
|
||||
ws.send(JSON.stringify(readySignal));
|
||||
this.logger.log(`[Satori] 客户端认证成功`);
|
||||
}
|
||||
|
||||
private sendPong (ws: WebSocket): void {
|
||||
const pongSignal: SatoriSignal = {
|
||||
op: SatoriOpcode.PONG,
|
||||
};
|
||||
ws.send(JSON.stringify(pongSignal));
|
||||
}
|
||||
|
||||
private startHeartbeat (): void {
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
for (const [ws, clientInfo] of this.clients) {
|
||||
if (ws.readyState === WebSocket.OPEN && clientInfo.identified) {
|
||||
// 检查客户端是否还活着
|
||||
if ((ws as any).isAlive === false) {
|
||||
ws.terminate();
|
||||
this.clients.delete(ws);
|
||||
continue;
|
||||
}
|
||||
(ws as any).isAlive = false;
|
||||
ws.ping();
|
||||
}
|
||||
}
|
||||
}, this.config.heartInterval || 10000);
|
||||
}
|
||||
|
||||
private stopHeartbeat (): void {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
packages/napcat-satori/package.json
Normal file
36
packages/napcat-satori/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "napcat-satori",
|
||||
"version": "1.0.0",
|
||||
"description": "Satori Protocol Adapter for NapCat",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./*"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"napcat-core": "workspace:*",
|
||||
"napcat-common": "workspace:*",
|
||||
"napcat-webui-backend": "workspace:*",
|
||||
"express": "^4.21.2",
|
||||
"ws": "^8.18.0",
|
||||
"@sinclair/typebox": "^0.34.33",
|
||||
"ajv": "^8.17.1",
|
||||
"json5": "^2.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
16
packages/napcat-satori/tsconfig.json
Normal file
16
packages/napcat-satori/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
341
packages/napcat-satori/types/index.ts
Normal file
341
packages/napcat-satori/types/index.ts
Normal file
@ -0,0 +1,341 @@
|
||||
// Satori Protocol Types
|
||||
// Reference: https://satori.js.org/zh-CN/protocol/
|
||||
|
||||
// ============ 基础类型 ============
|
||||
|
||||
export interface SatoriUser {
|
||||
id: string;
|
||||
name?: string;
|
||||
nick?: string;
|
||||
avatar?: string;
|
||||
is_bot?: boolean;
|
||||
}
|
||||
|
||||
export interface SatoriGuild {
|
||||
id: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export interface SatoriChannel {
|
||||
id: string;
|
||||
type: SatoriChannelType;
|
||||
name?: string;
|
||||
parent_id?: string;
|
||||
}
|
||||
|
||||
export enum SatoriChannelType {
|
||||
TEXT = 0,
|
||||
DIRECT = 1,
|
||||
CATEGORY = 2,
|
||||
VOICE = 3,
|
||||
}
|
||||
|
||||
export interface SatoriGuildMember {
|
||||
user?: SatoriUser;
|
||||
nick?: string;
|
||||
avatar?: string;
|
||||
joined_at?: number;
|
||||
}
|
||||
|
||||
export interface SatoriGuildRole {
|
||||
id: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface SatoriLogin {
|
||||
user?: SatoriUser;
|
||||
self_id?: string;
|
||||
platform?: string;
|
||||
status: SatoriLoginStatus;
|
||||
}
|
||||
|
||||
export enum SatoriLoginStatus {
|
||||
OFFLINE = 0,
|
||||
ONLINE = 1,
|
||||
CONNECT = 2,
|
||||
DISCONNECT = 3,
|
||||
RECONNECT = 4,
|
||||
}
|
||||
|
||||
// ============ 消息类型 ============
|
||||
|
||||
export interface SatoriMessage {
|
||||
id: string;
|
||||
content: string;
|
||||
channel?: SatoriChannel;
|
||||
guild?: SatoriGuild;
|
||||
member?: SatoriGuildMember;
|
||||
user?: SatoriUser;
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
}
|
||||
|
||||
// ============ 事件类型 ============
|
||||
|
||||
export interface SatoriEvent {
|
||||
id: number;
|
||||
type: string;
|
||||
platform: string;
|
||||
self_id: string;
|
||||
timestamp: number;
|
||||
argv?: SatoriArgv;
|
||||
button?: SatoriButton;
|
||||
channel?: SatoriChannel;
|
||||
guild?: SatoriGuild;
|
||||
login?: SatoriLogin;
|
||||
member?: SatoriGuildMember;
|
||||
message?: SatoriMessage;
|
||||
operator?: SatoriUser;
|
||||
role?: SatoriGuildRole;
|
||||
user?: SatoriUser;
|
||||
_type?: string;
|
||||
_data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SatoriArgv {
|
||||
name: string;
|
||||
arguments: unknown[];
|
||||
options: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SatoriButton {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// ============ API 请求/响应类型 ============
|
||||
|
||||
export interface SatoriApiRequest {
|
||||
method: string;
|
||||
body?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SatoriApiResponse<T = unknown> {
|
||||
data?: T;
|
||||
error?: {
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SatoriPageResult<T> {
|
||||
data: T[];
|
||||
next?: string;
|
||||
}
|
||||
|
||||
// ============ WebSocket 信令类型 ============
|
||||
|
||||
export enum SatoriOpcode {
|
||||
EVENT = 0,
|
||||
PING = 1,
|
||||
PONG = 2,
|
||||
IDENTIFY = 3,
|
||||
READY = 4,
|
||||
}
|
||||
|
||||
export interface SatoriSignal<T = unknown> {
|
||||
op: SatoriOpcode;
|
||||
body?: T;
|
||||
}
|
||||
|
||||
export interface SatoriIdentifyBody {
|
||||
token?: string;
|
||||
sequence?: number;
|
||||
}
|
||||
|
||||
export interface SatoriReadyBody {
|
||||
logins: SatoriLogin[];
|
||||
}
|
||||
|
||||
// ============ 消息元素类型 ============
|
||||
|
||||
export type SatoriElement =
|
||||
| SatoriTextElement
|
||||
| SatoriAtElement
|
||||
| SatoriSharpElement
|
||||
| SatoriAElement
|
||||
| SatoriImgElement
|
||||
| SatoriAudioElement
|
||||
| SatoriVideoElement
|
||||
| SatoriFileElement
|
||||
| SatoriBoldElement
|
||||
| SatoriItalicElement
|
||||
| SatoriUnderlineElement
|
||||
| SatoriStrikethroughElement
|
||||
| SatoriSpoilerElement
|
||||
| SatoriCodeElement
|
||||
| SatoriSupElement
|
||||
| SatoriSubElement
|
||||
| SatoriBrElement
|
||||
| SatoriParagraphElement
|
||||
| SatoriMessageElement
|
||||
| SatoriQuoteElement
|
||||
| SatoriAuthorElement
|
||||
| SatoriButtonElement;
|
||||
|
||||
export interface SatoriTextElement {
|
||||
type: 'text';
|
||||
attrs: {
|
||||
content: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SatoriAtElement {
|
||||
type: 'at';
|
||||
attrs: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
role?: string;
|
||||
type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SatoriSharpElement {
|
||||
type: 'sharp';
|
||||
attrs: {
|
||||
id: string;
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SatoriAElement {
|
||||
type: 'a';
|
||||
attrs: {
|
||||
href: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SatoriImgElement {
|
||||
type: 'img';
|
||||
attrs: {
|
||||
src: string;
|
||||
title?: string;
|
||||
cache?: boolean;
|
||||
timeout?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SatoriAudioElement {
|
||||
type: 'audio';
|
||||
attrs: {
|
||||
src: string;
|
||||
title?: string;
|
||||
cache?: boolean;
|
||||
timeout?: string;
|
||||
duration?: number;
|
||||
poster?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SatoriVideoElement {
|
||||
type: 'video';
|
||||
attrs: {
|
||||
src: string;
|
||||
title?: string;
|
||||
cache?: boolean;
|
||||
timeout?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number;
|
||||
poster?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SatoriFileElement {
|
||||
type: 'file';
|
||||
attrs: {
|
||||
src: string;
|
||||
title?: string;
|
||||
cache?: boolean;
|
||||
timeout?: string;
|
||||
poster?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SatoriBoldElement {
|
||||
type: 'b' | 'strong';
|
||||
children: SatoriElement[];
|
||||
}
|
||||
|
||||
export interface SatoriItalicElement {
|
||||
type: 'i' | 'em';
|
||||
children: SatoriElement[];
|
||||
}
|
||||
|
||||
export interface SatoriUnderlineElement {
|
||||
type: 'u' | 'ins';
|
||||
children: SatoriElement[];
|
||||
}
|
||||
|
||||
export interface SatoriStrikethroughElement {
|
||||
type: 's' | 'del';
|
||||
children: SatoriElement[];
|
||||
}
|
||||
|
||||
export interface SatoriSpoilerElement {
|
||||
type: 'spl';
|
||||
children: SatoriElement[];
|
||||
}
|
||||
|
||||
export interface SatoriCodeElement {
|
||||
type: 'code';
|
||||
children: SatoriElement[];
|
||||
}
|
||||
|
||||
export interface SatoriSupElement {
|
||||
type: 'sup';
|
||||
children: SatoriElement[];
|
||||
}
|
||||
|
||||
export interface SatoriSubElement {
|
||||
type: 'sub';
|
||||
children: SatoriElement[];
|
||||
}
|
||||
|
||||
export interface SatoriBrElement {
|
||||
type: 'br';
|
||||
}
|
||||
|
||||
export interface SatoriParagraphElement {
|
||||
type: 'p';
|
||||
children: SatoriElement[];
|
||||
}
|
||||
|
||||
export interface SatoriMessageElement {
|
||||
type: 'message';
|
||||
attrs?: {
|
||||
id?: string;
|
||||
forward?: boolean;
|
||||
};
|
||||
children: SatoriElement[];
|
||||
}
|
||||
|
||||
export interface SatoriQuoteElement {
|
||||
type: 'quote';
|
||||
attrs?: {
|
||||
id?: string;
|
||||
};
|
||||
children?: SatoriElement[];
|
||||
}
|
||||
|
||||
export interface SatoriAuthorElement {
|
||||
type: 'author';
|
||||
attrs: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SatoriButtonElement {
|
||||
type: 'button';
|
||||
attrs: {
|
||||
id?: string;
|
||||
type?: 'action' | 'link' | 'input';
|
||||
href?: string;
|
||||
text?: string;
|
||||
theme?: string;
|
||||
};
|
||||
}
|
||||
@ -22,7 +22,7 @@ import fs from 'fs';
|
||||
import os from 'os';
|
||||
import { LoginListItem, NodeIKernelLoginService } from 'napcat-core/services';
|
||||
import qrcode from 'napcat-qrcode/lib/main';
|
||||
import { NapCatOneBot11Adapter } from 'napcat-onebot/index';
|
||||
import { ProtocolManager } from 'napcat-protocol';
|
||||
import { InitWebUi } from 'napcat-webui-backend/index';
|
||||
import { WebUiDataRuntime } from 'napcat-webui-backend/src/helper/Data';
|
||||
import { napCatVersion } from 'napcat-common/src/version';
|
||||
@ -428,6 +428,7 @@ export async function NCoreInitShell () {
|
||||
export class NapCatShell {
|
||||
readonly core: NapCatCore;
|
||||
readonly context: InstanceContext;
|
||||
public protocolManager?: ProtocolManager;
|
||||
|
||||
constructor (
|
||||
wrapper: WrapperNodeApi,
|
||||
@ -452,11 +453,28 @@ export class NapCatShell {
|
||||
|
||||
async InitNapCat () {
|
||||
await this.core.initCore();
|
||||
const oneBotAdapter = new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper);
|
||||
// 注册到 WebUiDataRuntime,供调试功能使用
|
||||
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
|
||||
oneBotAdapter.InitOneBot()
|
||||
.catch(e => this.context.logger.logError('初始化OneBot失败', e));
|
||||
|
||||
// 使用协议管理器初始化所有协议
|
||||
this.protocolManager = new ProtocolManager(this.core, this.context, this.context.pathWrapper);
|
||||
|
||||
// 初始化所有协议
|
||||
await this.protocolManager.initAllProtocols();
|
||||
|
||||
// 获取适配器并注册到 WebUiDataRuntime
|
||||
const onebotAdapter = this.protocolManager.getOneBotAdapter();
|
||||
const satoriAdapter = this.protocolManager.getSatoriAdapter();
|
||||
|
||||
if (onebotAdapter) {
|
||||
WebUiDataRuntime.setOneBotContext(onebotAdapter.getRawAdapter());
|
||||
}
|
||||
|
||||
if (satoriAdapter) {
|
||||
WebUiDataRuntime.setSatoriContext(satoriAdapter.getRawAdapter());
|
||||
WebUiDataRuntime.setOnSatoriConfigChanged(async (newConfig) => {
|
||||
const prev = satoriAdapter.getConfigLoader().configData;
|
||||
await this.protocolManager!.reloadProtocolConfig('satori', prev, newConfig);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,34 +1,34 @@
|
||||
{
|
||||
"name": "napcat-shell",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode development",
|
||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||
"name": "napcat-shell",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode development",
|
||||
"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-qrcode": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.1",
|
||||
"napcat-vite": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"./*": {
|
||||
"import": "./*"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"napcat-core": "workspace:*",
|
||||
"napcat-common": "workspace:*",
|
||||
"napcat-protocol": "workspace:*",
|
||||
"napcat-webui-backend": "workspace:*",
|
||||
"napcat-qrcode": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.1",
|
||||
"napcat-vite": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,8 @@ const ShellBaseConfig = (source_map: boolean = false) =>
|
||||
'@/napcat-pty': resolve(__dirname, '../napcat-pty'),
|
||||
'@/napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'),
|
||||
'@/image-size': resolve(__dirname, '../image-size'),
|
||||
'@/napcat-satori': resolve(__dirname, '../napcat-satori'),
|
||||
'@/napcat-protocol': resolve(__dirname, '../napcat-protocol'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
137
packages/napcat-webui-backend/src/api/ProtocolConfig.ts
Normal file
137
packages/napcat-webui-backend/src/api/ProtocolConfig.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { webUiPathWrapper } from '@/napcat-webui-backend/index';
|
||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
||||
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
||||
import { isEmpty } from '@/napcat-webui-backend/src/utils/check';
|
||||
import json5 from 'json5';
|
||||
import { getSupportedProtocols } from 'napcat-common/src/protocol';
|
||||
|
||||
// 获取支持的协议列表
|
||||
export const GetSupportedProtocolsHandler: RequestHandler = (_req, res) => {
|
||||
const protocols = getSupportedProtocols();
|
||||
return sendSuccess(res, protocols);
|
||||
};
|
||||
|
||||
// 获取协议启用状态
|
||||
export const GetProtocolStatusHandler: RequestHandler = (_req, res) => {
|
||||
const isLogin = WebUiDataRuntime.getQQLoginStatus();
|
||||
if (!isLogin) {
|
||||
return sendError(res, 'Not Login');
|
||||
}
|
||||
|
||||
const uin = WebUiDataRuntime.getQQLoginUin();
|
||||
const protocols = getSupportedProtocols();
|
||||
const status: Record<string, boolean> = {};
|
||||
|
||||
for (const protocol of protocols) {
|
||||
const configPath = resolve(
|
||||
webUiPathWrapper.configPath,
|
||||
`./${protocol.id}_${uin}.json`
|
||||
);
|
||||
|
||||
if (existsSync(configPath)) {
|
||||
try {
|
||||
const content = readFileSync(configPath, 'utf-8');
|
||||
const config = json5.parse(content);
|
||||
// 检查是否有任何网络配置启用
|
||||
const network = config.network || {};
|
||||
const hasEnabled = Object.values(network).some((arr: any) =>
|
||||
Array.isArray(arr) && arr.some((item: any) => item.enable)
|
||||
);
|
||||
status[protocol.id] = hasEnabled;
|
||||
} catch {
|
||||
status[protocol.id] = false;
|
||||
}
|
||||
} else {
|
||||
status[protocol.id] = protocol.id === 'onebot11'; // OneBot11 默认启用
|
||||
}
|
||||
}
|
||||
|
||||
return sendSuccess(res, status);
|
||||
};
|
||||
|
||||
// 获取 Satori 配置
|
||||
export const SatoriGetConfigHandler: RequestHandler = (_req, res) => {
|
||||
const isLogin = WebUiDataRuntime.getQQLoginStatus();
|
||||
if (!isLogin) {
|
||||
return sendError(res, 'Not Login');
|
||||
}
|
||||
|
||||
const uin = WebUiDataRuntime.getQQLoginUin();
|
||||
const configFilePath = resolve(webUiPathWrapper.configPath, `./satori_${uin}.json`);
|
||||
|
||||
try {
|
||||
let configData: any = {
|
||||
network: {
|
||||
websocketServers: [],
|
||||
httpServers: [],
|
||||
webhookClients: [],
|
||||
},
|
||||
platform: 'qq',
|
||||
selfId: uin,
|
||||
};
|
||||
|
||||
if (existsSync(configFilePath)) {
|
||||
const content = readFileSync(configFilePath, 'utf-8');
|
||||
configData = json5.parse(content);
|
||||
}
|
||||
|
||||
return sendSuccess(res, configData);
|
||||
} catch (e) {
|
||||
return sendError(res, 'Config Get Error: ' + e);
|
||||
}
|
||||
};
|
||||
|
||||
// 写入 Satori 配置
|
||||
export const SatoriSetConfigHandler: RequestHandler = async (req, res) => {
|
||||
const isLogin = WebUiDataRuntime.getQQLoginStatus();
|
||||
if (!isLogin) {
|
||||
return sendError(res, 'Not Login');
|
||||
}
|
||||
|
||||
if (isEmpty(req.body.config)) {
|
||||
return sendError(res, 'config is empty');
|
||||
}
|
||||
|
||||
try {
|
||||
const config = json5.parse(req.body.config);
|
||||
await WebUiDataRuntime.setSatoriConfig(config);
|
||||
return sendSuccess(res, null);
|
||||
} catch (e) {
|
||||
return sendError(res, 'Error: ' + e);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取所有协议配置
|
||||
export const GetAllProtocolConfigsHandler: RequestHandler = (_req, res) => {
|
||||
const isLogin = WebUiDataRuntime.getQQLoginStatus();
|
||||
if (!isLogin) {
|
||||
return sendError(res, 'Not Login');
|
||||
}
|
||||
|
||||
const uin = WebUiDataRuntime.getQQLoginUin();
|
||||
const protocols = getSupportedProtocols();
|
||||
const configs: Record<string, any> = {};
|
||||
|
||||
for (const protocol of protocols) {
|
||||
const configPath = resolve(
|
||||
webUiPathWrapper.configPath,
|
||||
`./${protocol.id === 'onebot11' ? 'onebot11' : protocol.id}_${uin}.json`
|
||||
);
|
||||
|
||||
if (existsSync(configPath)) {
|
||||
try {
|
||||
const content = readFileSync(configPath, 'utf-8');
|
||||
configs[protocol.id] = json5.parse(content);
|
||||
} catch {
|
||||
configs[protocol.id] = null;
|
||||
}
|
||||
} else {
|
||||
configs[protocol.id] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return sendSuccess(res, configs);
|
||||
};
|
||||
@ -16,6 +16,7 @@ const LoginRuntime: LoginRuntimeType = {
|
||||
},
|
||||
QQVersion: 'unknown',
|
||||
OneBotContext: null,
|
||||
SatoriContext: null,
|
||||
onQQLoginStatusChange: async (status: boolean) => {
|
||||
LoginRuntime.QQLoginStatus = status;
|
||||
},
|
||||
@ -25,6 +26,9 @@ const LoginRuntime: LoginRuntimeType = {
|
||||
NapCatHelper: {
|
||||
onOB11ConfigChanged: async () => {
|
||||
|
||||
},
|
||||
onSatoriConfigChanged: async () => {
|
||||
|
||||
},
|
||||
onQuickLoginRequested: async () => {
|
||||
return { result: false, message: '' };
|
||||
@ -163,4 +167,20 @@ export const WebUiDataRuntime = {
|
||||
getOneBotContext (): any | null {
|
||||
return LoginRuntime.OneBotContext;
|
||||
},
|
||||
|
||||
setSatoriContext (context: any): void {
|
||||
LoginRuntime.SatoriContext = context;
|
||||
},
|
||||
|
||||
getSatoriContext (): any | null {
|
||||
return LoginRuntime.SatoriContext;
|
||||
},
|
||||
|
||||
setOnSatoriConfigChanged (func: LoginRuntimeType['NapCatHelper']['onSatoriConfigChanged']): void {
|
||||
LoginRuntime.NapCatHelper.onSatoriConfigChanged = func;
|
||||
},
|
||||
|
||||
setSatoriConfig: function (config) {
|
||||
return LoginRuntime.NapCatHelper.onSatoriConfigChanged(config);
|
||||
} as LoginRuntimeType['NapCatHelper']['onSatoriConfigChanged'],
|
||||
};
|
||||
|
||||
25
packages/napcat-webui-backend/src/router/ProtocolConfig.ts
Normal file
25
packages/napcat-webui-backend/src/router/ProtocolConfig.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Router } from 'express';
|
||||
import {
|
||||
GetSupportedProtocolsHandler,
|
||||
GetProtocolStatusHandler,
|
||||
SatoriGetConfigHandler,
|
||||
SatoriSetConfigHandler,
|
||||
GetAllProtocolConfigsHandler,
|
||||
} from '@/napcat-webui-backend/src/api/ProtocolConfig';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 获取支持的协议列表
|
||||
router.get('/protocols', GetSupportedProtocolsHandler);
|
||||
|
||||
// 获取协议启用状态
|
||||
router.get('/status', GetProtocolStatusHandler);
|
||||
|
||||
// 获取所有协议配置
|
||||
router.get('/all', GetAllProtocolConfigsHandler);
|
||||
|
||||
// Satori 配置
|
||||
router.get('/satori', SatoriGetConfigHandler);
|
||||
router.post('/satori', SatoriSetConfigHandler);
|
||||
|
||||
export { router as ProtocolConfigRouter };
|
||||
@ -16,6 +16,7 @@ import { FileRouter } from './File';
|
||||
import { WebUIConfigRouter } from './WebUIConfig';
|
||||
import { UpdateNapCatRouter } from './UpdateNapCat';
|
||||
import DebugRouter from '@/napcat-webui-backend/src/api/Debug';
|
||||
import { ProtocolConfigRouter } from './ProtocolConfig';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -44,5 +45,7 @@ router.use('/WebUIConfig', WebUIConfigRouter);
|
||||
router.use('/UpdateNapCat', UpdateNapCatRouter);
|
||||
// router:调试相关路由
|
||||
router.use('/Debug', DebugRouter);
|
||||
// router:协议配置相关路由
|
||||
router.use('/ProtocolConfig', ProtocolConfigRouter);
|
||||
|
||||
export { router as ALLRouter };
|
||||
|
||||
@ -1,4 +1,44 @@
|
||||
import type { OneBotConfig } from '@/napcat-webui-backend/src/onebot/config';
|
||||
|
||||
export interface SatoriConfig {
|
||||
network: {
|
||||
websocketServers: SatoriWebSocketServerConfig[];
|
||||
httpServers: SatoriHttpServerConfig[];
|
||||
webhookClients: SatoriWebHookClientConfig[];
|
||||
};
|
||||
platform: string;
|
||||
selfId: string;
|
||||
}
|
||||
|
||||
export interface SatoriWebSocketServerConfig {
|
||||
name: string;
|
||||
enable: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
token: string;
|
||||
path: string;
|
||||
debug: boolean;
|
||||
heartInterval: number;
|
||||
}
|
||||
|
||||
export interface SatoriHttpServerConfig {
|
||||
name: string;
|
||||
enable: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
token: string;
|
||||
path: string;
|
||||
debug: boolean;
|
||||
}
|
||||
|
||||
export interface SatoriWebHookClientConfig {
|
||||
name: string;
|
||||
enable: boolean;
|
||||
url: string;
|
||||
token: string;
|
||||
debug: boolean;
|
||||
}
|
||||
|
||||
export interface LoginListItem {
|
||||
uin: string;
|
||||
uid: string;
|
||||
@ -48,9 +88,11 @@ export interface LoginRuntimeType {
|
||||
onWebUiTokenChange: (token: string) => Promise<void>;
|
||||
WebUiConfigQuickFunction: () => Promise<void>;
|
||||
OneBotContext: any | null; // OneBot 上下文,用于调试功能
|
||||
SatoriContext: any | null; // Satori 上下文
|
||||
NapCatHelper: {
|
||||
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>;
|
||||
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
|
||||
onSatoriConfigChanged: (config: SatoriConfig) => Promise<void>;
|
||||
QQLoginList: string[];
|
||||
NewQQLoginList: LoginListItem[];
|
||||
};
|
||||
|
||||
@ -24,6 +24,7 @@ const WSDebug = lazy(() => import('@/pages/dashboard/debug/websocket'));
|
||||
const FileManagerPage = lazy(() => import('@/pages/dashboard/file_manager'));
|
||||
const LogsPage = lazy(() => import('@/pages/dashboard/logs'));
|
||||
const NetworkPage = lazy(() => import('@/pages/dashboard/network'));
|
||||
const ProtocolPage = lazy(() => import('@/pages/dashboard/protocol'));
|
||||
const TerminalPage = lazy(() => import('@/pages/dashboard/terminal'));
|
||||
|
||||
function App () {
|
||||
@ -42,7 +43,7 @@ function App () {
|
||||
);
|
||||
}
|
||||
|
||||
function AuthChecker ({ children }: { children: React.ReactNode }) {
|
||||
function AuthChecker ({ children }: { children: React.ReactNode; }) {
|
||||
const { isAuth } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -68,6 +69,7 @@ function AppRoutes () {
|
||||
<Route path='/' element={<IndexPage />}>
|
||||
<Route index element={<DashboardIndexPage />} />
|
||||
<Route path='network' element={<NetworkPage />} />
|
||||
<Route path='protocol' element={<ProtocolPage />} />
|
||||
<Route path='config' element={<ConfigPage />} />
|
||||
<Route path='logs' element={<LogsPage />} />
|
||||
<Route path='debug' element={<DebugPage />}>
|
||||
|
||||
@ -0,0 +1,135 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownTrigger,
|
||||
} from '@heroui/dropdown';
|
||||
import { Tooltip } from '@heroui/tooltip';
|
||||
import { FaRegCircleQuestion } from 'react-icons/fa6';
|
||||
import { IoAddCircleOutline } from 'react-icons/io5';
|
||||
import { LuGlobe, LuServer, LuWebhook } from 'react-icons/lu';
|
||||
|
||||
import { PlusIcon } from '../icons';
|
||||
|
||||
export interface SatoriAddButtonProps {
|
||||
onOpen: (key: SatoriNetworkConfigKey) => void;
|
||||
}
|
||||
|
||||
const SatoriAddButton: React.FC<SatoriAddButtonProps> = (props) => {
|
||||
const { onOpen } = props;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
classNames={{
|
||||
content: 'bg-opacity-30 backdrop-blur-md',
|
||||
}}
|
||||
placement='right'
|
||||
>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md"
|
||||
startContent={<IoAddCircleOutline className='text-2xl' />}
|
||||
>
|
||||
新建
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
aria-label='Create Satori Network Config'
|
||||
color='default'
|
||||
variant='flat'
|
||||
onAction={(key) => {
|
||||
onOpen(key as SatoriNetworkConfigKey);
|
||||
}}
|
||||
>
|
||||
<DropdownItem
|
||||
key='title'
|
||||
isReadOnly
|
||||
className='cursor-default hover:!bg-transparent'
|
||||
textValue='title'
|
||||
>
|
||||
<div className='flex items-center gap-2 justify-center'>
|
||||
<div className='w-5 h-5 -ml-3'>
|
||||
<PlusIcon />
|
||||
</div>
|
||||
<div className='text-primary-400'>新建 Satori 网络配置</div>
|
||||
</div>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key='websocketServers'
|
||||
textValue='websocketServers'
|
||||
startContent={<LuServer className='w-5 h-5' />}
|
||||
>
|
||||
<div className='flex gap-1 items-center'>
|
||||
WebSocket 服务器
|
||||
<Tooltip
|
||||
content='创建一个 Satori WebSocket 服务器,用于推送事件和接收指令。客户端通过 WebSocket 连接到此服务器接收实时事件。'
|
||||
showArrow
|
||||
className='max-w-64'
|
||||
>
|
||||
<Button
|
||||
isIconOnly
|
||||
radius='full'
|
||||
size='sm'
|
||||
variant='light'
|
||||
className='w-4 h-4 min-w-0'
|
||||
>
|
||||
<FaRegCircleQuestion />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key='httpServers'
|
||||
textValue='httpServers'
|
||||
startContent={<LuGlobe className='w-5 h-5' />}
|
||||
>
|
||||
<div className='flex gap-1 items-center'>
|
||||
HTTP 服务器
|
||||
<Tooltip
|
||||
content='创建一个 Satori HTTP API 服务器,提供 RESTful API 接口。客户端可以通过 HTTP 请求调用各种 API。'
|
||||
showArrow
|
||||
className='max-w-64'
|
||||
>
|
||||
<Button
|
||||
isIconOnly
|
||||
radius='full'
|
||||
size='sm'
|
||||
variant='light'
|
||||
className='w-4 h-4 min-w-0'
|
||||
>
|
||||
<FaRegCircleQuestion />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key='webhookClients'
|
||||
textValue='webhookClients'
|
||||
startContent={<LuWebhook className='w-5 h-5' />}
|
||||
>
|
||||
<div className='flex gap-1 items-center'>
|
||||
WebHook 客户端
|
||||
<Tooltip
|
||||
content='配置一个 WebHook 上报地址,NapCat 会将事件通过 HTTP POST 请求发送到指定的 URL。'
|
||||
showArrow
|
||||
className='max-w-64'
|
||||
>
|
||||
<Button
|
||||
isIconOnly
|
||||
radius='full'
|
||||
size='sm'
|
||||
variant='light'
|
||||
className='w-4 h-4 min-w-0'
|
||||
>
|
||||
<FaRegCircleQuestion />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default SatoriAddButton;
|
||||
@ -0,0 +1,84 @@
|
||||
import { Card, CardBody, CardFooter, CardHeader } from '@heroui/card';
|
||||
import { Chip } from '@heroui/chip';
|
||||
import { Button } from '@heroui/button';
|
||||
import { Switch } from '@heroui/switch';
|
||||
import { LuPencil, LuTrash2, LuGlobe } from 'react-icons/lu';
|
||||
|
||||
interface Props {
|
||||
data: SatoriHttpServerConfig;
|
||||
onDelete: () => Promise<void>;
|
||||
onEdit: () => void;
|
||||
onEnable: () => Promise<void>;
|
||||
onEnableDebug: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function SatoriHttpServerCard ({
|
||||
data,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onEnable,
|
||||
onEnableDebug,
|
||||
}: Props) {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<LuGlobe className="w-4 h-4" />
|
||||
<span className="font-semibold truncate">{data.name}</span>
|
||||
</div>
|
||||
<Chip
|
||||
color={data.enable ? 'success' : 'default'}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
{data.enable ? '已启用' : '未启用'}
|
||||
</Chip>
|
||||
</CardHeader>
|
||||
<CardBody className="gap-2">
|
||||
<div className="text-sm">
|
||||
<span className="text-default-500">地址: </span>
|
||||
<span>{data.host}:{data.port}{data.path}</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-default-500">Token: </span>
|
||||
<span>{data.token ? '******' : '未设置'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={data.enable}
|
||||
onValueChange={onEnable}
|
||||
>
|
||||
启用
|
||||
</Switch>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={data.debug}
|
||||
onValueChange={onEnableDebug}
|
||||
>
|
||||
调试
|
||||
</Switch>
|
||||
</div>
|
||||
</CardBody>
|
||||
<CardFooter className="gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
startContent={<LuPencil className="w-4 h-4" />}
|
||||
onPress={onEdit}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
startContent={<LuTrash2 className="w-4 h-4" />}
|
||||
onPress={onDelete}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,202 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from '@heroui/modal';
|
||||
import { Button } from '@heroui/button';
|
||||
import { Input } from '@heroui/input';
|
||||
import { Switch } from '@heroui/switch';
|
||||
import { useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import useProtocolConfig from '@/hooks/use-protocol-config';
|
||||
|
||||
interface Props {
|
||||
data?: SatoriWebSocketServerConfig | SatoriHttpServerConfig | SatoriWebHookClientConfig;
|
||||
field: SatoriNetworkConfigKey;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const defaultWSServer: SatoriWebSocketServerConfig = {
|
||||
name: '',
|
||||
enable: true,
|
||||
host: '127.0.0.1',
|
||||
port: 5500,
|
||||
path: '/v1/events',
|
||||
token: '',
|
||||
debug: false,
|
||||
heartInterval: 10000,
|
||||
};
|
||||
|
||||
const defaultHttpServer: SatoriHttpServerConfig = {
|
||||
name: '',
|
||||
enable: true,
|
||||
host: '127.0.0.1',
|
||||
port: 5501,
|
||||
path: '/v1',
|
||||
token: '',
|
||||
debug: false,
|
||||
};
|
||||
|
||||
const defaultWebhookClient: SatoriWebHookClientConfig = {
|
||||
name: '',
|
||||
enable: true,
|
||||
url: 'http://localhost:8080/webhook',
|
||||
token: '',
|
||||
debug: false,
|
||||
};
|
||||
|
||||
export default function SatoriNetworkFormModal ({
|
||||
data,
|
||||
field,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
}: Props) {
|
||||
const { createSatoriNetworkConfig, updateSatoriNetworkConfig } = useProtocolConfig();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState<any>(null);
|
||||
|
||||
const isEdit = !!data;
|
||||
const title = isEdit ? '编辑配置' : '新建配置';
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (data) {
|
||||
setFormData({ ...data });
|
||||
} else {
|
||||
switch (field) {
|
||||
case 'websocketServers':
|
||||
setFormData({ ...defaultWSServer });
|
||||
break;
|
||||
case 'httpServers':
|
||||
setFormData({ ...defaultHttpServer });
|
||||
break;
|
||||
case 'webhookClients':
|
||||
setFormData({ ...defaultWebhookClient });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isOpen, data, field]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.name) {
|
||||
toast.error('请输入配置名称');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isEdit) {
|
||||
await updateSatoriNetworkConfig(field, formData);
|
||||
} else {
|
||||
await createSatoriNetworkConfig(field, formData);
|
||||
}
|
||||
toast.success(isEdit ? '更新成功' : '创建成功');
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
toast.error((error as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateField = (key: string, value: any) => {
|
||||
setFormData((prev: any) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
if (!formData) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="lg">
|
||||
<ModalContent>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
<ModalBody className="gap-4">
|
||||
<Input
|
||||
label="配置名称"
|
||||
placeholder="请输入配置名称"
|
||||
value={formData.name}
|
||||
onValueChange={(v) => updateField('name', v)}
|
||||
isDisabled={isEdit}
|
||||
/>
|
||||
|
||||
{(field === 'websocketServers' || field === 'httpServers') && (
|
||||
<>
|
||||
<Input
|
||||
label="主机地址"
|
||||
placeholder="127.0.0.1"
|
||||
value={formData.host}
|
||||
onValueChange={(v) => updateField('host', v)}
|
||||
/>
|
||||
<Input
|
||||
label="端口"
|
||||
type="number"
|
||||
placeholder="5500"
|
||||
value={String(formData.port)}
|
||||
onValueChange={(v) => updateField('port', parseInt(v) || 0)}
|
||||
/>
|
||||
<Input
|
||||
label="路径"
|
||||
placeholder="/v1/events"
|
||||
value={formData.path}
|
||||
onValueChange={(v) => updateField('path', v)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{field === 'webhookClients' && (
|
||||
<Input
|
||||
label="WebHook URL"
|
||||
placeholder="http://localhost:8080/webhook"
|
||||
value={formData.url}
|
||||
onValueChange={(v) => updateField('url', v)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Input
|
||||
label="Token"
|
||||
placeholder="可选,用于鉴权"
|
||||
value={formData.token}
|
||||
onValueChange={(v) => updateField('token', v)}
|
||||
/>
|
||||
|
||||
{field === 'websocketServers' && (
|
||||
<Input
|
||||
label="心跳间隔 (ms)"
|
||||
type="number"
|
||||
placeholder="10000"
|
||||
value={String(formData.heartInterval)}
|
||||
onValueChange={(v) => updateField('heartInterval', parseInt(v) || 10000)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Switch
|
||||
isSelected={formData.enable}
|
||||
onValueChange={(v) => updateField('enable', v)}
|
||||
>
|
||||
启用
|
||||
</Switch>
|
||||
<Switch
|
||||
isSelected={formData.debug}
|
||||
onValueChange={(v) => updateField('debug', v)}
|
||||
>
|
||||
调试模式
|
||||
</Switch>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="flat" onPress={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="primary" isLoading={loading} onPress={handleSubmit}>
|
||||
{isEdit ? '保存' : '创建'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
import { Card, CardBody, CardFooter, CardHeader } from '@heroui/card';
|
||||
import { Chip } from '@heroui/chip';
|
||||
import { Button } from '@heroui/button';
|
||||
import { Switch } from '@heroui/switch';
|
||||
import { LuPencil, LuTrash2, LuWebhook } from 'react-icons/lu';
|
||||
|
||||
interface Props {
|
||||
data: SatoriWebHookClientConfig;
|
||||
onDelete: () => Promise<void>;
|
||||
onEdit: () => void;
|
||||
onEnable: () => Promise<void>;
|
||||
onEnableDebug: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function SatoriWebhookClientCard ({
|
||||
data,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onEnable,
|
||||
onEnableDebug,
|
||||
}: Props) {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<LuWebhook className="w-4 h-4" />
|
||||
<span className="font-semibold truncate">{data.name}</span>
|
||||
</div>
|
||||
<Chip
|
||||
color={data.enable ? 'success' : 'default'}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
{data.enable ? '已启用' : '未启用'}
|
||||
</Chip>
|
||||
</CardHeader>
|
||||
<CardBody className="gap-2">
|
||||
<div className="text-sm">
|
||||
<span className="text-default-500">URL: </span>
|
||||
<span className="break-all">{data.url}</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-default-500">Token: </span>
|
||||
<span>{data.token ? '******' : '未设置'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={data.enable}
|
||||
onValueChange={onEnable}
|
||||
>
|
||||
启用
|
||||
</Switch>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={data.debug}
|
||||
onValueChange={onEnableDebug}
|
||||
>
|
||||
调试
|
||||
</Switch>
|
||||
</div>
|
||||
</CardBody>
|
||||
<CardFooter className="gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
startContent={<LuPencil className="w-4 h-4" />}
|
||||
onPress={onEdit}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
startContent={<LuTrash2 className="w-4 h-4" />}
|
||||
onPress={onDelete}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
import { Card, CardBody, CardFooter, CardHeader } from '@heroui/card';
|
||||
import { Chip } from '@heroui/chip';
|
||||
import { Button } from '@heroui/button';
|
||||
import { Switch } from '@heroui/switch';
|
||||
import { LuPencil, LuTrash2, LuServer } from 'react-icons/lu';
|
||||
|
||||
interface Props {
|
||||
data: SatoriWebSocketServerConfig;
|
||||
onDelete: () => Promise<void>;
|
||||
onEdit: () => void;
|
||||
onEnable: () => Promise<void>;
|
||||
onEnableDebug: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function SatoriWSServerCard ({
|
||||
data,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onEnable,
|
||||
onEnableDebug,
|
||||
}: Props) {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<LuServer className="w-4 h-4" />
|
||||
<span className="font-semibold truncate">{data.name}</span>
|
||||
</div>
|
||||
<Chip
|
||||
color={data.enable ? 'success' : 'default'}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
{data.enable ? '已启用' : '未启用'}
|
||||
</Chip>
|
||||
</CardHeader>
|
||||
<CardBody className="gap-2">
|
||||
<div className="text-sm">
|
||||
<span className="text-default-500">地址: </span>
|
||||
<span>{data.host}:{data.port}{data.path}</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-default-500">心跳间隔: </span>
|
||||
<span>{data.heartInterval}ms</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-default-500">Token: </span>
|
||||
<span>{data.token ? '******' : '未设置'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={data.enable}
|
||||
onValueChange={onEnable}
|
||||
>
|
||||
启用
|
||||
</Switch>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={data.debug}
|
||||
onValueChange={onEnableDebug}
|
||||
>
|
||||
调试
|
||||
</Switch>
|
||||
</div>
|
||||
</CardBody>
|
||||
<CardFooter className="gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
startContent={<LuPencil className="w-4 h-4" />}
|
||||
onPress={onEdit}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
startContent={<LuTrash2 className="w-4 h-4" />}
|
||||
onPress={onDelete}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -4,6 +4,7 @@ import {
|
||||
LuFolderOpen,
|
||||
LuInfo,
|
||||
LuLayoutDashboard,
|
||||
LuPlug,
|
||||
LuSettings,
|
||||
LuSignal,
|
||||
LuTerminal,
|
||||
@ -34,6 +35,11 @@ export const siteConfig = {
|
||||
icon: <LuSignal className='w-5 h-5' />,
|
||||
href: '/network',
|
||||
},
|
||||
{
|
||||
label: '协议配置',
|
||||
icon: <LuPlug className='w-5 h-5' />,
|
||||
href: '/protocol',
|
||||
},
|
||||
{
|
||||
label: '其他配置',
|
||||
icon: <LuSettings className='w-5 h-5' />,
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
import { serverRequest } from '@/utils/request';
|
||||
|
||||
const ProtocolManager = {
|
||||
async getSupportedProtocols (): Promise<ProtocolInfo[]> {
|
||||
const res = await serverRequest.get<ServerResponse<ProtocolInfo[]>>(
|
||||
'/ProtocolConfig/protocols'
|
||||
);
|
||||
if (res.data.code !== 0) {
|
||||
throw new Error(res.data.message);
|
||||
}
|
||||
return res.data.data;
|
||||
},
|
||||
|
||||
async getProtocolStatus (): Promise<Record<string, boolean>> {
|
||||
const res = await serverRequest.get<ServerResponse<Record<string, boolean>>>(
|
||||
'/ProtocolConfig/status'
|
||||
);
|
||||
if (res.data.code !== 0) {
|
||||
throw new Error(res.data.message);
|
||||
}
|
||||
return res.data.data;
|
||||
},
|
||||
|
||||
async getSatoriConfig (): Promise<SatoriConfig> {
|
||||
const res = await serverRequest.get<ServerResponse<SatoriConfig>>(
|
||||
'/ProtocolConfig/satori'
|
||||
);
|
||||
if (res.data.code !== 0) {
|
||||
throw new Error(res.data.message);
|
||||
}
|
||||
return res.data.data;
|
||||
},
|
||||
|
||||
async setSatoriConfig (config: SatoriConfig): Promise<void> {
|
||||
const res = await serverRequest.post<ServerResponse<null>>(
|
||||
'/ProtocolConfig/satori',
|
||||
{ config: JSON.stringify(config) }
|
||||
);
|
||||
if (res.data.code !== 0) {
|
||||
throw new Error(res.data.message);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default ProtocolManager;
|
||||
141
packages/napcat-webui-frontend/src/hooks/use-protocol-config.ts
Normal file
141
packages/napcat-webui-frontend/src/hooks/use-protocol-config.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import ProtocolManager from '@/controllers/protocol_manager';
|
||||
import { deepClone } from '@/utils/object';
|
||||
|
||||
const useProtocolConfig = () => {
|
||||
const [protocols, setProtocols] = useState<ProtocolInfo[]>([]);
|
||||
const [protocolStatus, setProtocolStatus] = useState<Record<string, boolean>>({});
|
||||
const [satoriConfig, setSatoriConfig] = useState<SatoriConfig | null>(null);
|
||||
|
||||
const refreshProtocols = useCallback(async () => {
|
||||
const [protocolList, status] = await Promise.all([
|
||||
ProtocolManager.getSupportedProtocols(),
|
||||
ProtocolManager.getProtocolStatus(),
|
||||
]);
|
||||
setProtocols(protocolList);
|
||||
setProtocolStatus(status);
|
||||
}, []);
|
||||
|
||||
const refreshSatoriConfig = useCallback(async () => {
|
||||
const config = await ProtocolManager.getSatoriConfig();
|
||||
setSatoriConfig(config);
|
||||
}, []);
|
||||
|
||||
const createSatoriNetworkConfig = async <T extends SatoriNetworkConfigKey> (
|
||||
key: T,
|
||||
value: SatoriNetworkConfig[T][0]
|
||||
) => {
|
||||
if (!satoriConfig) throw new Error('配置未加载');
|
||||
|
||||
const allNames = Object.keys(satoriConfig.network).reduce((acc, k) => {
|
||||
const _key = k as SatoriNetworkConfigKey;
|
||||
return acc.concat(satoriConfig.network[_key].map((item) => item.name));
|
||||
}, [] as string[]);
|
||||
|
||||
if (value.name && allNames.includes(value.name)) {
|
||||
throw new Error('已存在相同名称的配置项');
|
||||
}
|
||||
|
||||
const newConfig = deepClone(satoriConfig);
|
||||
(newConfig.network[key] as (typeof value)[]).push(value);
|
||||
|
||||
await ProtocolManager.setSatoriConfig(newConfig);
|
||||
setSatoriConfig(newConfig);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const updateSatoriNetworkConfig = async <T extends SatoriNetworkConfigKey> (
|
||||
key: T,
|
||||
value: SatoriNetworkConfig[T][0]
|
||||
) => {
|
||||
if (!satoriConfig) throw new Error('配置未加载');
|
||||
|
||||
const newConfig = deepClone(satoriConfig);
|
||||
const index = newConfig.network[key].findIndex((item) => item.name === value.name);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('找不到对应的配置项');
|
||||
}
|
||||
|
||||
newConfig.network[key][index] = value;
|
||||
|
||||
await ProtocolManager.setSatoriConfig(newConfig);
|
||||
setSatoriConfig(newConfig);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const deleteSatoriNetworkConfig = async <T extends SatoriNetworkConfigKey> (
|
||||
key: T,
|
||||
name: string
|
||||
) => {
|
||||
if (!satoriConfig) throw new Error('配置未加载');
|
||||
|
||||
const newConfig = deepClone(satoriConfig);
|
||||
const index = newConfig.network[key].findIndex((item) => item.name === name);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('找不到对应的配置项');
|
||||
}
|
||||
|
||||
newConfig.network[key].splice(index, 1);
|
||||
|
||||
await ProtocolManager.setSatoriConfig(newConfig);
|
||||
setSatoriConfig(newConfig);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const enableSatoriNetworkConfig = async <T extends SatoriNetworkConfigKey> (
|
||||
key: T,
|
||||
name: string
|
||||
) => {
|
||||
if (!satoriConfig) throw new Error('配置未加载');
|
||||
|
||||
const newConfig = deepClone(satoriConfig);
|
||||
const index = newConfig.network[key].findIndex((item) => item.name === name);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('找不到对应的配置项');
|
||||
}
|
||||
|
||||
newConfig.network[key][index].enable = !newConfig.network[key][index].enable;
|
||||
|
||||
await ProtocolManager.setSatoriConfig(newConfig);
|
||||
setSatoriConfig(newConfig);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const enableSatoriDebugConfig = async <T extends SatoriNetworkConfigKey> (
|
||||
key: T,
|
||||
name: string
|
||||
) => {
|
||||
if (!satoriConfig) throw new Error('配置未加载');
|
||||
|
||||
const newConfig = deepClone(satoriConfig);
|
||||
const index = newConfig.network[key].findIndex((item) => item.name === name);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('找不到对应的配置项');
|
||||
}
|
||||
|
||||
newConfig.network[key][index].debug = !newConfig.network[key][index].debug;
|
||||
|
||||
await ProtocolManager.setSatoriConfig(newConfig);
|
||||
setSatoriConfig(newConfig);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
return {
|
||||
protocols,
|
||||
protocolStatus,
|
||||
satoriConfig,
|
||||
refreshProtocols,
|
||||
refreshSatoriConfig,
|
||||
createSatoriNetworkConfig,
|
||||
updateSatoriNetworkConfig,
|
||||
deleteSatoriNetworkConfig,
|
||||
enableSatoriNetworkConfig,
|
||||
enableSatoriDebugConfig,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProtocolConfig;
|
||||
268
packages/napcat-webui-frontend/src/pages/dashboard/protocol.tsx
Normal file
268
packages/napcat-webui-frontend/src/pages/dashboard/protocol.tsx
Normal file
@ -0,0 +1,268 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import { Card, CardBody, CardHeader } from '@heroui/card';
|
||||
import { Chip } from '@heroui/chip';
|
||||
import { useDisclosure } from '@heroui/modal';
|
||||
import { Tab, Tabs } from '@heroui/tabs';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IoMdRefresh } from 'react-icons/io';
|
||||
import { LuPlug, LuSettings } from 'react-icons/lu';
|
||||
|
||||
import SatoriAddButton from '@/components/button/satori_add_button';
|
||||
import PageLoading from '@/components/page_loading';
|
||||
import SatoriNetworkFormModal from '@/components/protocol_edit/satori_modal';
|
||||
import SatoriWSServerCard from '@/components/protocol_edit/satori_ws_server_card';
|
||||
import SatoriHttpServerCard from '@/components/protocol_edit/satori_http_server_card';
|
||||
import SatoriWebhookClientCard from '@/components/protocol_edit/satori_webhook_client_card';
|
||||
|
||||
import useProtocolConfig from '@/hooks/use-protocol-config';
|
||||
import useDialog from '@/hooks/use-dialog';
|
||||
|
||||
export default function ProtocolPage () {
|
||||
const {
|
||||
protocols,
|
||||
protocolStatus,
|
||||
satoriConfig,
|
||||
refreshProtocols,
|
||||
refreshSatoriConfig,
|
||||
deleteSatoriNetworkConfig,
|
||||
enableSatoriNetworkConfig,
|
||||
enableSatoriDebugConfig,
|
||||
} = useProtocolConfig();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeProtocol, setActiveProtocol] = useState<string>('onebot11');
|
||||
const [activeSatoriField, setActiveSatoriField] = useState<SatoriNetworkConfigKey>('websocketServers');
|
||||
const [activeSatoriName, setActiveSatoriName] = useState<string>('');
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
const dialog = useDialog();
|
||||
|
||||
const refresh = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await refreshProtocols();
|
||||
await refreshSatoriConfig();
|
||||
} catch (error) {
|
||||
toast.error(`刷新失败: ${(error as Error).message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickCreate = (key: SatoriNetworkConfigKey) => {
|
||||
setActiveSatoriField(key);
|
||||
setActiveSatoriName('');
|
||||
onOpen();
|
||||
};
|
||||
|
||||
const onDelete = async (field: SatoriNetworkConfigKey, name: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
dialog.confirm({
|
||||
title: '删除配置',
|
||||
content: `确定要删除配置「${name}」吗?`,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await deleteSatoriNetworkConfig(field, name);
|
||||
toast.success('删除配置成功');
|
||||
resolve();
|
||||
} catch (error) {
|
||||
toast.error(`删除配置失败: ${(error as Error).message}`);
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
onCancel: () => resolve(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onEnable = async (field: SatoriNetworkConfigKey, name: string) => {
|
||||
try {
|
||||
await enableSatoriNetworkConfig(field, name);
|
||||
toast.success('更新配置成功');
|
||||
} catch (error) {
|
||||
toast.error(`更新配置失败: ${(error as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const onEnableDebug = async (field: SatoriNetworkConfigKey, name: string) => {
|
||||
try {
|
||||
await enableSatoriDebugConfig(field, name);
|
||||
toast.success('更新配置成功');
|
||||
} catch (error) {
|
||||
toast.error(`更新配置失败: ${(error as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const onEdit = (field: SatoriNetworkConfigKey, name: string) => {
|
||||
setActiveSatoriField(field);
|
||||
setActiveSatoriName(name);
|
||||
onOpen();
|
||||
};
|
||||
|
||||
const activeSatoriData = satoriConfig?.network[activeSatoriField]?.find(
|
||||
(item: SatoriAdapterConfig) => item.name === activeSatoriName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>协议配置 - NapCat WebUI</title>
|
||||
<div className="p-2 md:p-4 relative">
|
||||
<PageLoading loading={loading} />
|
||||
<SatoriNetworkFormModal
|
||||
data={activeSatoriData}
|
||||
field={activeSatoriField}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
/>
|
||||
|
||||
<div className="flex mb-6 items-center gap-4">
|
||||
<Button
|
||||
isIconOnly
|
||||
className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md"
|
||||
radius="full"
|
||||
onPress={refresh}
|
||||
>
|
||||
<IoMdRefresh size={24} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 协议列表 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||
{protocols.map((protocol: ProtocolInfo) => (
|
||||
<Card
|
||||
key={protocol.id}
|
||||
className={clsx(
|
||||
'cursor-pointer transition-all',
|
||||
activeProtocol === protocol.id
|
||||
? 'ring-2 ring-primary'
|
||||
: 'hover:shadow-lg'
|
||||
)}
|
||||
isPressable
|
||||
onPress={() => setActiveProtocol(protocol.id)}
|
||||
>
|
||||
<CardHeader className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<LuPlug className="w-5 h-5" />
|
||||
<span className="font-semibold">{protocol.name}</span>
|
||||
</div>
|
||||
<Chip
|
||||
color={protocolStatus[protocol.id] ? 'success' : 'default'}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
{protocolStatus[protocol.id] ? '已启用' : '未启用'}
|
||||
</Chip>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<p className="text-sm text-default-500">{protocol.description}</p>
|
||||
<p className="text-xs text-default-400 mt-2">版本: {protocol.version}</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 协议配置详情 */}
|
||||
{activeProtocol === 'onebot11' && (
|
||||
<Card>
|
||||
<CardHeader className="flex items-center gap-2">
|
||||
<LuSettings className="w-5 h-5" />
|
||||
<span className="font-semibold">OneBot 11 配置</span>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<p className="text-default-500">
|
||||
OneBot 11 协议配置请前往
|
||||
<a href="/network" className="text-primary ml-1">
|
||||
网络配置
|
||||
</a>
|
||||
页面进行管理。
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeProtocol === 'satori' && satoriConfig && (
|
||||
<Card>
|
||||
<CardHeader className="flex items-center gap-2">
|
||||
<LuSettings className="w-5 h-5" />
|
||||
<span className="font-semibold">Satori 协议配置</span>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div className="flex mb-4 items-center gap-4">
|
||||
<SatoriAddButton onOpen={handleClickCreate} />
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
aria-label="Satori Network Configs"
|
||||
className="max-w-full"
|
||||
classNames={{
|
||||
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md',
|
||||
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
|
||||
}}
|
||||
>
|
||||
<Tab key="websocketServers" title="WebSocket 服务器">
|
||||
{satoriConfig.network.websocketServers.length === 0 ? (
|
||||
<p className="text-default-400">暂无配置</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{satoriConfig.network.websocketServers.map((item: SatoriWebSocketServerConfig) => (
|
||||
<SatoriWSServerCard
|
||||
key={item.name}
|
||||
data={item}
|
||||
onDelete={() => onDelete('websocketServers', item.name)}
|
||||
onEdit={() => onEdit('websocketServers', item.name)}
|
||||
onEnable={() => onEnable('websocketServers', item.name)}
|
||||
onEnableDebug={() => onEnableDebug('websocketServers', item.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab key="httpServers" title="HTTP 服务器">
|
||||
{satoriConfig.network.httpServers.length === 0 ? (
|
||||
<p className="text-default-400">暂无配置</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{satoriConfig.network.httpServers.map((item: SatoriHttpServerConfig) => (
|
||||
<SatoriHttpServerCard
|
||||
key={item.name}
|
||||
data={item}
|
||||
onDelete={() => onDelete('httpServers', item.name)}
|
||||
onEdit={() => onEdit('httpServers', item.name)}
|
||||
onEnable={() => onEnable('httpServers', item.name)}
|
||||
onEnableDebug={() => onEnableDebug('httpServers', item.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab key="webhookClients" title="WebHook 客户端">
|
||||
{satoriConfig.network.webhookClients.length === 0 ? (
|
||||
<p className="text-default-400">暂无配置</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{satoriConfig.network.webhookClients.map((item: SatoriWebHookClientConfig) => (
|
||||
<SatoriWebhookClientCard
|
||||
key={item.name}
|
||||
data={item}
|
||||
onDelete={() => onDelete('webhookClients', item.name)}
|
||||
onEdit={() => onEdit('webhookClients', item.name)}
|
||||
onEnable={() => onEnable('webhookClients', item.name)}
|
||||
onEnableDebug={() => onEnableDebug('webhookClients', item.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
48
packages/napcat-webui-frontend/src/types/protocol.d.ts
vendored
Normal file
48
packages/napcat-webui-frontend/src/types/protocol.d.ts
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
// 协议相关类型定义
|
||||
|
||||
interface ProtocolInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// Satori 配置类型
|
||||
interface SatoriAdapterConfig {
|
||||
name: string;
|
||||
enable: boolean;
|
||||
debug: boolean;
|
||||
token: string;
|
||||
}
|
||||
|
||||
interface SatoriWebSocketServerConfig extends SatoriAdapterConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
path: string;
|
||||
heartInterval: number;
|
||||
}
|
||||
|
||||
interface SatoriHttpServerConfig extends SatoriAdapterConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface SatoriWebHookClientConfig extends SatoriAdapterConfig {
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface SatoriNetworkConfig {
|
||||
websocketServers: SatoriWebSocketServerConfig[];
|
||||
httpServers: SatoriHttpServerConfig[];
|
||||
webhookClients: SatoriWebHookClientConfig[];
|
||||
}
|
||||
|
||||
interface SatoriConfig {
|
||||
network: SatoriNetworkConfig;
|
||||
platform: string;
|
||||
selfId: string;
|
||||
}
|
||||
|
||||
type SatoriNetworkConfigKey = keyof SatoriNetworkConfig;
|
||||
284
pnpm-lock.yaml
284
pnpm-lock.yaml
@ -117,9 +117,9 @@ importers:
|
||||
napcat-core:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-core
|
||||
napcat-onebot:
|
||||
napcat-protocol:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-onebot
|
||||
version: link:../napcat-protocol
|
||||
napcat-qrcode:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-qrcode
|
||||
@ -230,6 +230,25 @@ importers:
|
||||
specifier: ^5.6
|
||||
version: 5.9.3
|
||||
|
||||
packages/napcat-protocol:
|
||||
dependencies:
|
||||
napcat-common:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-common
|
||||
napcat-core:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-core
|
||||
napcat-onebot:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-onebot
|
||||
napcat-satori:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-satori
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.7.2
|
||||
version: 5.9.3
|
||||
|
||||
packages/napcat-pty:
|
||||
dependencies:
|
||||
'@homebridge/node-pty-prebuilt-multiarch':
|
||||
@ -246,6 +265,46 @@ importers:
|
||||
specifier: ^22.0.1
|
||||
version: 22.19.1
|
||||
|
||||
packages/napcat-satori:
|
||||
dependencies:
|
||||
'@sinclair/typebox':
|
||||
specifier: ^0.34.33
|
||||
version: 0.34.41
|
||||
ajv:
|
||||
specifier: ^8.17.1
|
||||
version: 8.17.1
|
||||
express:
|
||||
specifier: ^4.21.2
|
||||
version: 4.22.1
|
||||
json5:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
napcat-common:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-common
|
||||
napcat-core:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-core
|
||||
napcat-webui-backend:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-webui-backend
|
||||
ws:
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.3
|
||||
devDependencies:
|
||||
'@types/express':
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.5
|
||||
'@types/node':
|
||||
specifier: ^22.10.5
|
||||
version: 22.19.1
|
||||
'@types/ws':
|
||||
specifier: ^8.5.13
|
||||
version: 8.18.1
|
||||
typescript:
|
||||
specifier: ^5.7.2
|
||||
version: 5.9.3
|
||||
|
||||
packages/napcat-shell:
|
||||
dependencies:
|
||||
napcat-common:
|
||||
@ -254,9 +313,9 @@ importers:
|
||||
napcat-core:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-core
|
||||
napcat-onebot:
|
||||
napcat-protocol:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-onebot
|
||||
version: link:../napcat-protocol
|
||||
napcat-qrcode:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-qrcode
|
||||
@ -3143,6 +3202,10 @@ packages:
|
||||
abbrev@1.1.1:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
|
||||
accepts@1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
accepts@2.0.0:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -3237,6 +3300,9 @@ packages:
|
||||
resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
array-flatten@1.1.1:
|
||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||
|
||||
array-includes@3.1.9:
|
||||
resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -3357,6 +3423,10 @@ packages:
|
||||
blf-debug-appender@1.0.5:
|
||||
resolution: {integrity: sha512-ftNcGiVW2nxzG0WJ8Hcs58CbygPl2vMuLDDSxsfhGZmVl2NFGmZOuXhDDeJifvWmP0FHzP/L8dblxn65eQjgEA==}
|
||||
|
||||
body-parser@1.20.4:
|
||||
resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
|
||||
body-parser@2.2.0:
|
||||
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -3606,6 +3676,10 @@ packages:
|
||||
console-control-strings@1.1.0:
|
||||
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
||||
|
||||
content-disposition@0.5.4:
|
||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
content-disposition@1.0.0:
|
||||
resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -3620,6 +3694,9 @@ packages:
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
cookie-signature@1.0.7:
|
||||
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
|
||||
|
||||
cookie-signature@1.2.2:
|
||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||
engines: {node: '>=6.6.0'}
|
||||
@ -3759,6 +3836,10 @@ packages:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
destroy@1.2.0:
|
||||
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
@ -4109,6 +4190,10 @@ packages:
|
||||
peerDependencies:
|
||||
express: '>= 4.11'
|
||||
|
||||
express@4.22.1:
|
||||
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
||||
express@5.1.0:
|
||||
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
|
||||
engines: {node: '>= 18'}
|
||||
@ -4188,6 +4273,10 @@ packages:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
finalhandler@1.3.2:
|
||||
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
finalhandler@2.1.0:
|
||||
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -4266,6 +4355,10 @@ packages:
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fresh@0.5.2:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
fresh@2.0.0:
|
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -4457,6 +4550,10 @@ packages:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
http-proxy-agent@5.0.0:
|
||||
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
|
||||
engines: {node: '>= 6'}
|
||||
@ -4468,6 +4565,10 @@ packages:
|
||||
humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
iconv-lite@0.5.2:
|
||||
resolution: {integrity: sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -4983,6 +5084,9 @@ packages:
|
||||
resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
merge-descriptors@1.0.3:
|
||||
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
|
||||
|
||||
merge-descriptors@2.0.0:
|
||||
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
|
||||
engines: {node: '>=18'}
|
||||
@ -4991,6 +5095,10 @@ packages:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
methods@1.1.2:
|
||||
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
micromark-core-commonmark@2.0.3:
|
||||
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
|
||||
|
||||
@ -5095,6 +5203,11 @@ packages:
|
||||
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime@1.6.0:
|
||||
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -5228,6 +5341,10 @@ packages:
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
negotiator@0.6.3:
|
||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
negotiator@0.6.4:
|
||||
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -5414,6 +5531,9 @@ packages:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-to-regexp@0.1.12:
|
||||
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
|
||||
|
||||
path-to-regexp@8.3.0:
|
||||
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
|
||||
|
||||
@ -5594,6 +5714,10 @@ packages:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
raw-body@2.5.3:
|
||||
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
raw-body@3.0.1:
|
||||
resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@ -5900,10 +6024,18 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
send@0.19.2:
|
||||
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
send@1.2.0:
|
||||
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
serve-static@1.16.3:
|
||||
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
serve-static@2.2.0:
|
||||
resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
|
||||
engines: {node: '>= 18'}
|
||||
@ -6474,6 +6606,10 @@ packages:
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
utils-merge@1.0.1:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
uuid@2.0.3:
|
||||
resolution: {integrity: sha512-FULf7fayPdpASncVy4DLh3xydlXEJJpvIELjYjNeQWYUZ9pclcpvCZSr2gkmN2FrrGcI7G/cJsIEwk5/8vfXpg==}
|
||||
deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
|
||||
@ -9875,6 +10011,11 @@ snapshots:
|
||||
|
||||
abbrev@1.1.1: {}
|
||||
|
||||
accepts@1.3.8:
|
||||
dependencies:
|
||||
mime-types: 2.1.35
|
||||
negotiator: 0.6.3
|
||||
|
||||
accepts@2.0.0:
|
||||
dependencies:
|
||||
mime-types: 3.0.1
|
||||
@ -9973,6 +10114,8 @@ snapshots:
|
||||
call-bound: 1.0.4
|
||||
is-array-buffer: 3.0.5
|
||||
|
||||
array-flatten@1.1.1: {}
|
||||
|
||||
array-includes@3.1.9:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@ -10132,6 +10275,23 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
body-parser@1.20.4:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
content-type: 1.0.5
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
destroy: 1.2.0
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.4.24
|
||||
on-finished: 2.4.1
|
||||
qs: 6.14.0
|
||||
raw-body: 2.5.3
|
||||
type-is: 1.6.18
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
body-parser@2.2.0:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@ -10418,6 +10578,10 @@ snapshots:
|
||||
|
||||
console-control-strings@1.1.0: {}
|
||||
|
||||
content-disposition@0.5.4:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
content-disposition@1.0.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@ -10428,6 +10592,8 @@ snapshots:
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie-signature@1.0.7: {}
|
||||
|
||||
cookie-signature@1.2.2: {}
|
||||
|
||||
cookie@0.7.2: {}
|
||||
@ -10536,6 +10702,8 @@ snapshots:
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
destroy@1.2.0: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
devlop@1.1.0:
|
||||
@ -11029,6 +11197,42 @@ snapshots:
|
||||
dependencies:
|
||||
express: 5.1.0
|
||||
|
||||
express@4.22.1:
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
array-flatten: 1.1.1
|
||||
body-parser: 1.20.4
|
||||
content-disposition: 0.5.4
|
||||
content-type: 1.0.5
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.0.7
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
finalhandler: 1.3.2
|
||||
fresh: 0.5.2
|
||||
http-errors: 2.0.0
|
||||
merge-descriptors: 1.0.3
|
||||
methods: 1.1.2
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
path-to-regexp: 0.1.12
|
||||
proxy-addr: 2.0.7
|
||||
qs: 6.14.0
|
||||
range-parser: 1.2.1
|
||||
safe-buffer: 5.2.1
|
||||
send: 0.19.2
|
||||
serve-static: 1.16.3
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.2
|
||||
type-is: 1.6.18
|
||||
utils-merge: 1.0.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
express@5.1.0:
|
||||
dependencies:
|
||||
accepts: 2.0.0
|
||||
@ -11130,6 +11334,18 @@ snapshots:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
finalhandler@1.3.2:
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
statuses: 2.0.2
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
finalhandler@2.1.0:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@ -11223,6 +11439,8 @@ snapshots:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
fresh@0.5.2: {}
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
@ -11454,6 +11672,14 @@ snapshots:
|
||||
statuses: 2.0.1
|
||||
toidentifier: 1.0.1
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
inherits: 2.0.4
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.2
|
||||
toidentifier: 1.0.1
|
||||
|
||||
http-proxy-agent@5.0.0:
|
||||
dependencies:
|
||||
'@tootallnate/once': 2.0.0
|
||||
@ -11473,6 +11699,10 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
iconv-lite@0.5.2:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@ -12076,10 +12306,14 @@ snapshots:
|
||||
type-fest: 1.4.0
|
||||
yargs-parser: 20.2.9
|
||||
|
||||
merge-descriptors@1.0.3: {}
|
||||
|
||||
merge-descriptors@2.0.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
methods@1.1.2: {}
|
||||
|
||||
micromark-core-commonmark@2.0.3:
|
||||
dependencies:
|
||||
decode-named-character-reference: 1.2.0
|
||||
@ -12288,6 +12522,8 @@ snapshots:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
|
||||
mime@1.6.0: {}
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
|
||||
minimatch@10.1.1:
|
||||
@ -12409,6 +12645,8 @@ snapshots:
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
negotiator@0.6.3: {}
|
||||
|
||||
negotiator@0.6.4: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
@ -12626,6 +12864,8 @@ snapshots:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-to-regexp@0.1.12: {}
|
||||
|
||||
path-to-regexp@8.3.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
@ -12783,6 +13023,13 @@ snapshots:
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
raw-body@2.5.3:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.4.24
|
||||
unpipe: 1.0.0
|
||||
|
||||
raw-body@3.0.1:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@ -13157,6 +13404,24 @@ snapshots:
|
||||
|
||||
semver@7.7.3: {}
|
||||
|
||||
send@0.19.2:
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
destroy: 1.2.0
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
fresh: 0.5.2
|
||||
http-errors: 2.0.1
|
||||
mime: 1.6.0
|
||||
ms: 2.1.3
|
||||
on-finished: 2.4.1
|
||||
range-parser: 1.2.1
|
||||
statuses: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
send@1.2.0:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@ -13173,6 +13438,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
serve-static@1.16.3:
|
||||
dependencies:
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 0.19.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
serve-static@2.2.0:
|
||||
dependencies:
|
||||
encodeurl: 2.0.0
|
||||
@ -13883,6 +14157,8 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
utils-merge@1.0.1: {}
|
||||
|
||||
uuid@2.0.3: {}
|
||||
|
||||
validate-npm-package-license@3.0.4:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user