Compare commits

..

1 Commits

Author SHA1 Message Date
手瓜一十雪
5e1e3a6df7 Add convertToNTSilkTct to FFmpeg adapters and update usage
Introduces the convertToNTSilkTct method to FFmpeg adapter interfaces and implementations, updating audio conversion logic to use this new method for Silk format conversion. Refactors FFmpegService to rename convertFile to convertAudioFmt and updates related usages. Removes 'audio-worker' entry from vite configs in napcat-framework and napcat-shell. Also fixes a typo in appid.json.

Remove silk-wasm dependency and refactor audio handling

Eliminated the silk-wasm package and related code, including audio-worker and direct Silk encoding/decoding logic. Audio format conversion and Silk detection are now handled via FFmpeg adapters. Updated related OneBot actions and configuration files to remove all references to silk-wasm and streamline audio processing.
2026-01-13 16:12:30 +08:00
59 changed files with 227 additions and 1386 deletions

View File

@@ -15,11 +15,11 @@ NapCat.Shell.Windows.OneKey.zip (无头)
**注意QQ版本推荐使用 40768+ 版本 最低可以使用40768版本**
**默认WebUi密钥为随机密码 控制台查看**
**[9.9.26-44343 X64 Win](https://dldir1.qq.com/qqfile/qq/QQNT/40d6045a/QQ9.9.26.44343_x64.exe)**
[LinuxX64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_amd64.deb)
[LinuxX64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.rpm)
[LinuxArm64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.deb)
[LinuxArm64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_aarch64.rpm)
**[9.9.22-40990 X64 Win](https://dldir1v6.qq.com/qqfile/qq/QQNT/2c9d3f6c/QQ9.9.22.40990_x64.exe)**
[LinuxX64 DEB 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_amd64.deb)
[LinuxX64 RPM 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_x86_64.rpm)
[LinuxArm64 DEB 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_arm64.deb)
[LinuxArm64 RPM 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_aarch64.rpm)
[MAC DMG 40990 ](https://dldir1v6.qq.com/qqfile/qq/QQNT/c6cb0f5d/QQ_v6.9.82.40990.dmg)
## 如果WinX64缺少运行库或者xxx.dll
[安装运行库](https://aka.ms/vs/17/release/vc_redist.x64.exe)

View File

@@ -44,11 +44,11 @@ NapCat.Shell.Windows.OneKey.zip (无头)
**注意QQ版本推荐使用 40768+ 版本 最低可以使用40768版本**
**默认WebUi密钥为随机密码 控制台查看**
**[9.9.26-44343 X64 Win](https://dldir1.qq.com/qqfile/qq/QQNT/40d6045a/QQ9.9.26.44343_x64.exe)**
[LinuxX64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_amd64.deb)
[LinuxX64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.rpm)
[LinuxArm64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.deb)
[LinuxArm64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_aarch64.rpm)
**[9.9.22-40990 X64 Win](https://dldir1v6.qq.com/qqfile/qq/QQNT/2c9d3f6c/QQ9.9.22.40990_x64.exe)**
[LinuxX64 DEB 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_amd64.deb)
[LinuxX64 RPM 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_x86_64.rpm)
[LinuxArm64 DEB 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_arm64.deb)
[LinuxArm64 RPM 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_aarch64.rpm)
[MAC DMG 40990 ](https://dldir1v6.qq.com/qqfile/qq/QQNT/c6cb0f5d/QQ_v6.9.82.40990.dmg)
## 如果WinX64缺少运行库或者xxx.dll
[安装运行库](https://aka.ms/vs/17/release/vc_redist.x64.exe)

View File

@@ -46,8 +46,8 @@ jobs:
env:
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
NAPCAT_VERSION: ${{ env.latest_tag }}
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/8015ff90/linuxqq_3.2.21-42086_x86_64.AppImage' # 写死 QQ 版本
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/8015ff90/linuxqq_3.2.21-42086_arm64.AppImage' # 写死 QQ 版本
run: |
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_version_x86_64=${QQ_VERSION_X86_64}, qq_version_arm64=${QQ_VERSION_ARM64}"
curl -X POST \
@@ -72,8 +72,8 @@ jobs:
env:
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
NAPCAT_VERSION: ${{ env.latest_tag }}
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/8015ff90/linuxqq_3.2.21-42086_x86_64.AppImage' # 写死 QQ 版本
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/8015ff90/linuxqq_3.2.21-42086_arm64.AppImage' # 写死 QQ 版本
run: |
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_url_amd64=${QQ_VERSION_X86_64}, qq_url_arm64=${QQ_VERSION_ARM64}"
curl -X POST \

View File

@@ -10,7 +10,7 @@ permissions: write-all
env:
OPENROUTER_API_URL: https://91vip.futureppo.top/v1/chat/completions
OPENROUTER_MODEL: "copilot/ant/gemini-3-flash-preview"
OPENROUTER_MODEL: "Antigravity/gemini-3-flash-preview"
RELEASE_NAME: "NapCat"
jobs:

View File

@@ -21,4 +21,4 @@ export interface IStatusHelperSubscription {
on (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
off (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
emit (event: 'statusUpdate', status: SystemStatus): boolean;
}
}

View File

@@ -513,10 +513,7 @@
},
"9.9.26-44498": {
"appid": 537337416,
"offset": "0x1809C2810",
"qua": "V1_WIN_NQ_9.9.26_44498_GW_B"
},
"9.9.26-44725": {
"appid": 537337569,
"qua": "V1_WIN_NQ_9.9.26_44725_GW_B"
}
}

View File

@@ -87,10 +87,6 @@
"send": "23B0330",
"recv": "0957648"
},
"3.2.21-42086-arm64": {
"send": "3D6D98C",
"recv": "14797C8"
},
"3.2.21-42086-x64": {
"send": "5B42CF0",
"recv": "2FDA6F0"
@@ -150,9 +146,5 @@
"9.9.26-44498-x64": {
"send": "0A1051C",
"recv": "1D3BC0D"
},
"9.9.26-44725-x64": {
"send": "0A18D0C",
"recv": "1D4BF0D"
}
}

View File

@@ -658,9 +658,5 @@
"9.9.26-44498-x64": {
"send": "2CDAE40",
"recv": "2CDE3C0"
},
"9.9.26-44725-x64": {
"send": "2CEBB20",
"recv": "2CEF0A0"
}
}

View File

@@ -5,7 +5,7 @@
import { platform, arch } from 'node:os';
import path from 'node:path';
import { existsSync, openSync, readSync, closeSync } from 'node:fs';
import { existsSync, readFileSync, openSync, readSync, closeSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import type { FFmpeg } from './ffmpeg-addon';
import type { IFFmpegAdapter, VideoInfoResult } from './ffmpeg-adapter-interface';

View File

@@ -258,7 +258,7 @@ export class FFmpegExecAdapter implements IFFmpegAdapter {
}
}
async convertToNTSilkTct (_inputFile: string, _outputFile: string): Promise<void> {
async convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void> {
throw new Error('convertToNTSilkTct is not implemented in FFmpegExecAdapter');
}
}

View File

@@ -125,7 +125,7 @@ export class NapCatCore {
container.bind(TypedEventEmitter).toConstantValue(this.event);
ReceiverServiceRegistry.forEach((ServiceClass, serviceName) => {
container.bind(ServiceClass).toSelf();
// console.log(`Registering service handler for: ${serviceName}`);
//console.log(`Registering service handler for: ${serviceName}`);
this.context.packetHandler.onCmd(serviceName, ({ seq, hex_data }) => {
const serviceInstance = container.get(ServiceClass);
return serviceInstance.handler(seq, hex_data);
@@ -177,10 +177,8 @@ export class NapCatCore {
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知
const tips = `[KickedOffLine] [${Info.tipsTitle}] ${Info.tipsDesc}`;
this.context.logger.logError(tips);
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
this.selfInfo.online = false;
this.event.emit('KickedOffLine', tips);
};
msgListener.onRecvMsg = (msgs) => {
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));

View File

@@ -53,8 +53,6 @@ export class NodeIKernelLoginListener {
onLoginState (..._args: any[]): any {
}
onLoginRecordUpdate (..._args: any[]): any {
}
}
export interface QRCodeLoginSucceedResult {

View File

@@ -1,7 +1,6 @@
import { TypedEventEmitter } from './typeEvent';
export interface AppEvents {
'event:emoji_like': { groupId: string; senderUin: string; emojiId: string, msgSeq: string, isAdd: boolean, count: number; };
KickedOffLine: string;
'event:emoji_like': { groupId: string; senderUin: string; emojiId: string, msgSeq: string, isAdd: boolean, count: number };
}
export const appEvent = new TypedEventEmitter<AppEvents>();

View File

@@ -181,7 +181,7 @@ export interface MessageElement {
tofuRecordElement?: TofuRecordElement,
taskTopMsgElement?: TaskTopMsgElement,
recommendedMsgElement?: RecommendedMsgElement,
actionBarElement?: ActionBarElement;
actionBarElement?: ActionBarElement
}
/**
@@ -337,7 +337,7 @@ export interface InlineKeyboardElementRowButton {
*/
export interface InlineKeyboardElement {
rows: [{
buttons: InlineKeyboardElementRowButton[];
buttons: InlineKeyboardElementRowButton[]
}],
botAppid: string;
}
@@ -441,14 +441,14 @@ export interface TipGroupElement {
uid: string;
card: string;
name: string;
role: NTGroupMemberRole;
role: NTGroupMemberRole
};
member: {
uid: string;
uid: string
card: string;
name: string;
role: NTGroupMemberRole;
};
role: NTGroupMemberRole
}
};
}
@@ -498,7 +498,6 @@ export interface RawMessage {
sendStatus?: SendStatusType;// 消息状态
recallTime: string;// 撤回时间,"0" 是没有撤回
records: RawMessage[];// 消息记录
emojiLikesList?: Array<{ emojiId: string; emojiType: string; likesCnt: string; isClicked: string; }>;
elements: MessageElement[];// 消息元素
sourceType: MsgSourceType;// 消息来源类型
isOnlineMsg: boolean;// 是否为在线消息
@@ -509,9 +508,9 @@ export interface RawMessage {
* 查询消息参数接口
*/
export interface QueryMsgsParams {
chatInfo: Peer & { privilegeFlag?: number; };
chatInfo: Peer & { privilegeFlag?: number };
// searchFields: number;
filterMsgType: Array<{ type: NTMsgType, subType: Array<number>; }>;
filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>;
filterSendersUid: string[];
filterMsgFromTime: string;
filterMsgToTime: string;
@@ -555,7 +554,7 @@ export interface MsgReqType {
queryOrder: boolean,
includeSelf: boolean,
includeDeleteMsg: boolean,
extraCnt: number;
extraCnt: number
}
/**

View File

@@ -73,7 +73,7 @@ export interface WebApiGroupNoticeFeed {
fn: number;
cn: number;
vn: number;
settings?: {
settings: {
is_show_edit_card: number
remind_ts: number
tip_window_type: number

View File

@@ -73,8 +73,6 @@ async function copyAll () {
process.env.NAPCAT_QQ_PACKAGE_INFO_PATH = path.join(TARGET_DIR, 'package.json');
process.env.NAPCAT_QQ_VERSION_CONFIG_PATH = path.join(TARGET_DIR, 'config.json');
process.env.NAPCAT_DISABLE_PIPE = '1';
// 禁用重启和多进程功能
process.env.NAPCAT_DISABLE_MULTI_PROCESS = '1';
process.env.NAPCAT_WORKDIR = TARGET_DIR;
// 开发环境使用固定密钥
process.env.NAPCAT_WEBUI_JWT_SECRET_KEY = 'napcat_dev_secret_key';

View File

@@ -7,26 +7,19 @@ interface GroupNotice {
publish_time: number;
notice_id: string;
message: {
text: string;
text: string
// 保持一段时间兼容性 防止以往版本出现问题 后续版本可考虑移除
image: Array<{
height: string;
width: string;
id: string;
height: string
width: string
id: string
}>,
images: Array<{
height: string;
width: string;
id: string;
}>;
height: string
width: string
id: string
}>
};
settings?: {
is_show_edit_card: number,
remind_ts: number,
tip_window_type: number,
confirm_required: number;
};
read_num?: number;
}
const SchemaData = Type.Object({
@@ -66,8 +59,6 @@ export class GetGroupNotice extends OneBotAction<Payload, GroupNotice[]> {
image,
images: image,
},
settings: retApiNotice.settings,
read_num: retApiNotice.read_num
};
retNotices.push(retNotice);
}

View File

@@ -86,7 +86,6 @@ import { GetGroupMemberList } from './group/GetGroupMemberList';
import { GetGroupFileUrl } from '@/napcat-onebot/action/file/GetGroupFileUrl';
import { GetPacketStatus } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { GetCredentials } from './system/GetCredentials';
import { SetRestart } from './system/SetRestart';
import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign';
import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain';
import { GoCQHTTPCheckUrlSafely } from './go-cqhttp/GoCQHTTPCheckUrlSafely';
@@ -267,7 +266,6 @@ export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatC
new GetGroupFileSystemInfo(obContext, core),
new GetGroupFilesByFolder(obContext, core),
new GetPacketStatus(obContext, core),
new SetRestart(obContext, core),
new GroupPoke(obContext, core),
new FriendPoke(obContext, core),
new GetUserStatus(obContext, core),

View File

@@ -36,15 +36,6 @@ class GetMsg extends OneBotAction<Payload, OB11Message> {
if (!msg) throw Error('消息不存在');
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat);
if (!retMsg) throw Error('消息为空');
retMsg.emoji_likes_list = [];
msg.emojiLikesList?.map(emoji => {
retMsg.emoji_likes_list?.push({
emoji_id: emoji.emojiId,
emoji_type: emoji.emojiType,
likes_cnt: emoji.likesCnt,
});
});
// 烘焙emoji_likes_list 仅此处烘焙
try {
retMsg.message_id = MessageUnique.createUniqueMsgId(peer, msg.msgId)!;
retMsg.message_seq = retMsg.message_id;

View File

@@ -81,7 +81,7 @@ export const ActionName = {
CanSendRecord: 'can_send_record',
GetStatus: 'get_status',
GetVersionInfo: 'get_version_info',
Reboot: 'set_restart',
// Reboot : 'set_restart',
CleanCache: 'clean_cache',
Exit: 'bot_exit',
// go-cqhttp

View File

@@ -1,14 +0,0 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { OneBotAction } from '../OneBotAction';
import { WebUiDataRuntime } from 'napcat-webui-backend/src/helper/Data';
export class SetRestart extends OneBotAction<void, void> {
override actionName = ActionName.Reboot;
async _handle () {
const result = await WebUiDataRuntime.requestRestartProcess();
if (!result.result) {
throw new Error(result.message || '进程重启失败');
}
}
}

View File

@@ -6,7 +6,7 @@ const HttpServerConfigSchema = Type.Object({
port: Type.Number({ default: 3000 }),
host: Type.String({ default: '127.0.0.1' }),
enableCors: Type.Boolean({ default: true }),
enableWebsocket: Type.Boolean({ default: false }),
enableWebsocket: Type.Boolean({ default: true }),
messagePostFormat: Type.String({ default: 'array' }),
token: Type.String({ default: '' }),
debug: Type.Boolean({ default: false }),
@@ -18,7 +18,7 @@ const HttpSseServerConfigSchema = Type.Object({
port: Type.Number({ default: 3000 }),
host: Type.String({ default: '127.0.0.1' }),
enableCors: Type.Boolean({ default: true }),
enableWebsocket: Type.Boolean({ default: false }),
enableWebsocket: Type.Boolean({ default: true }),
messagePostFormat: Type.String({ default: 'array' }),
token: Type.String({ default: '' }),
debug: Type.Boolean({ default: false }),

View File

@@ -246,7 +246,7 @@ export class NapCatOneBot11Adapter {
await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11WebSocketClientAdapter);
}
private async handleConfigChange<CT extends NetworkAdapterConfig> (
private async handleConfigChange<CT extends NetworkAdapterConfig>(
prevConfig: NetworkAdapterConfig[],
nowConfig: NetworkAdapterConfig[],
adapterClass: new (
@@ -305,9 +305,6 @@ export class NapCatOneBot11Adapter {
};
msgListener.onRecvMsg = async (msg) => {
if (!this.networkManager.hasActiveAdapters()) {
return;
}
for (const m of msg) {
if (this.bootTime > parseInt(m.msgTime)) {
this.context.logger.logDebug(`消息时间${m.msgTime}早于启动时间${this.bootTime},忽略上报`);
@@ -387,7 +384,6 @@ export class NapCatOneBot11Adapter {
}
};
msgListener.onKickedOffLine = async (kick) => {
WebUiDataRuntime.setQQLoginStatus(false);
const event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc);
this.networkManager
.emitEvent(event)
@@ -521,14 +517,15 @@ export class NapCatOneBot11Adapter {
}
private async emitMsg (message: RawMessage) {
const network = await this.networkManager.getAllConfig();
this.context.logger.logDebug('收到新消息 RawMessage', message);
await Promise.allSettled([
this.handleMsg(message),
this.handleMsg(message, network),
message.chatType === ChatType.KCHATTYPEGROUP ? this.handleGroupEvent(message) : this.handlePrivateMsgEvent(message),
]);
}
private async handleMsg (message: RawMessage) {
private async handleMsg (message: RawMessage, network: Array<NetworkAdapterConfig>) {
// 过滤无效消息
if (message.msgType === NTMsgType.KMSGTYPENULL) {
return;
@@ -538,36 +535,10 @@ export class NapCatOneBot11Adapter {
if (ob11Msg) {
const isSelfMsg = this.isSelfMessage(ob11Msg);
this.context.logger.logDebug('转化为 OB11Message', ob11Msg);
if (isSelfMsg || message.chatType !== ChatType.KCHATTYPEGROUP) {
const targetId = parseInt(message.peerUin);
ob11Msg.stringMsg.target_id = targetId;
ob11Msg.arrayMsg.target_id = targetId;
}
const msgMap = new Map<string, OB11Message>();
for (const adapter of this.networkManager.adapters.values()) {
if (!adapter.isActive) continue;
const config = adapter.config;
if (isSelfMsg) {
if (!('reportSelfMessage' in config) || !config.reportSelfMessage) {
continue;
}
}
const msgData = config.messagePostFormat === 'string' ? ob11Msg.stringMsg : ob11Msg.arrayMsg;
if (config.debug) {
const clone = structuredClone(msgData);
clone.raw = message;
msgMap.set(adapter.name, clone);
} else {
msgMap.set(adapter.name, msgData);
}
}
if (msgMap.size > 0) {
this.networkManager.emitEventByNames(msgMap);
} else if (this.networkManager.hasActiveAdapters()) {
this.context.logger.logDebug('没有可用的网络适配器发送消息,消息内容:', message);
}
const msgMap = this.createMsgMap(network, ob11Msg, isSelfMsg, message);
this.handleDebugNetwork(network, msgMap, message);
this.handleNotReportSelfNetwork(network, msgMap, isSelfMsg);
this.networkManager.emitEventByNames(msgMap);
}
} catch (e) {
this.context.logger.logError('constructMessage error: ', e);
@@ -582,6 +553,48 @@ export class NapCatOneBot11Adapter {
ob11Msg.arrayMsg.user_id.toString() === this.core.selfInfo.uin;
}
private createMsgMap (network: Array<NetworkAdapterConfig>, ob11Msg: {
stringMsg: OB11Message;
arrayMsg: OB11Message;
}, isSelfMsg: boolean, message: RawMessage): Map<string, OB11Message> {
const msgMap: Map<string, OB11Message> = new Map();
network.filter(e => e.enable).forEach(e => {
if (isSelfMsg || message.chatType !== ChatType.KCHATTYPEGROUP) {
ob11Msg.stringMsg.target_id = parseInt(message.peerUin);
ob11Msg.arrayMsg.target_id = parseInt(message.peerUin);
}
if ('messagePostFormat' in e && e.messagePostFormat === 'string') {
msgMap.set(e.name, structuredClone(ob11Msg.stringMsg));
} else {
msgMap.set(e.name, structuredClone(ob11Msg.arrayMsg));
}
});
return msgMap;
}
private handleDebugNetwork (network: Array<NetworkAdapterConfig>, msgMap: Map<string, OB11Message>, message: RawMessage) {
const debugNetwork = network.filter(e => e.enable && e.debug);
if (debugNetwork.length > 0) {
debugNetwork.forEach(adapter => {
const msg = msgMap.get(adapter.name);
if (msg) {
msg.raw = message;
}
});
} else if (msgMap.size === 0) {
this.context.logger.logDebug('没有可用的网络适配器发送消息,消息内容:', message);
}
}
private handleNotReportSelfNetwork (network: Array<NetworkAdapterConfig>, msgMap: Map<string, OB11Message>, isSelfMsg: boolean) {
if (isSelfMsg) {
const notReportSelfNetwork = network.filter(e => e.enable && (('reportSelfMessage' in e && !e.reportSelfMessage) || !('reportSelfMessage' in e)));
notReportSelfNetwork.forEach(adapter => {
msgMap.delete(adapter.name);
});
}
}
private async handleGroupEvent (message: RawMessage) {
try {
// 群名片修改事件解析 任何都该判断

View File

@@ -23,15 +23,11 @@ export abstract class IOB11NetworkAdapter<CT extends NetworkAdapterConfig> {
this.logger = core.context.logger;
}
abstract onEvent<T extends OB11EmitEventContent> (event: T): Promise<void>;
abstract onEvent<T extends OB11EmitEventContent>(event: T): Promise<void>;
abstract open (): void | Promise<void>;
abstract close (): void | Promise<void>;
abstract reload (config: unknown): OB11NetworkReloadType | Promise<OB11NetworkReloadType>;
get isActive (): boolean {
return this.isEnable;
}
}

View File

@@ -5,10 +5,6 @@ import { OB11HttpServerAdapter } from './http-server';
export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter {
private sseClients: Response[] = [];
override get isActive (): boolean {
return this.isEnable && (this.sseClients.length > 0 || super.isActive);
}
override async handleRequest (req: Request, res: Response) {
if (req.path === '/_events') {
this.createSseSupport(req, res);
@@ -29,8 +25,7 @@ export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter {
});
}
override async onEvent<T extends OB11EmitEventContent> (event: T) {
super.onEvent(event);
override async onEvent<T extends OB11EmitEventContent>(event: T) {
const promises: Promise<void>[] = [];
this.sseClients.forEach((res) => {
promises.push(new Promise<void>((resolve, reject) => {

View File

@@ -1,6 +1,6 @@
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
import express, { Express, NextFunction, Request, Response } from 'express';
import http, { IncomingMessage } from 'http';
import http from 'http';
import { OB11Response } from '@/napcat-onebot/action/OneBotAction';
import cors from 'cors';
import { HttpServerConfig } from '@/napcat-onebot/config/config';
@@ -8,41 +8,13 @@ import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter';
import json5 from 'json5';
import { isFinished } from 'on-finished';
import typeis from 'type-is';
import { WebSocket, WebSocketServer, RawData } from 'ws';
import { URL } from 'url';
import { ActionName } from '@/napcat-onebot/action/router';
import { OB11HeartbeatEvent } from '@/napcat-onebot/event/meta/OB11HeartbeatEvent';
import { OB11LifeCycleEvent, LifeCycleSubType } from '@/napcat-onebot/event/meta/OB11LifeCycleEvent';
import { Mutex } from 'async-mutex';
export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig> {
private app: Express | undefined;
private server: http.Server | undefined;
private wsServer?: WebSocketServer;
private wsClients: WebSocket[] = [];
private wsClientsMutex = new Mutex();
private heartbeatIntervalId: NodeJS.Timeout | null = null;
private wsClientWithEvent: WebSocket[] = [];
override get isActive (): boolean {
return this.isEnable && this.wsClientWithEvent.length > 0;
}
override async onEvent<T extends OB11EmitEventContent> (event: T) {
override async onEvent<T extends OB11EmitEventContent> (_event: T) {
// http server is passive, no need to emit event
this.wsClientsMutex.runExclusive(async () => {
const promises = this.wsClientWithEvent.map((wsClient) => {
return new Promise<void>((resolve, reject) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(event));
resolve();
} else {
reject(new Error('WebSocket is not open'));
}
});
});
await Promise.allSettled(promises);
});
}
open () {
@@ -64,24 +36,11 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
this.isEnable = false;
this.server?.close();
this.app = undefined;
this.stopHeartbeat();
await this.wsClientsMutex.runExclusive(async () => {
this.wsClients.forEach((wsClient) => {
wsClient.close();
});
this.wsClients = [];
this.wsClientWithEvent = [];
});
this.wsServer?.close();
}
private initializeServer () {
this.app = express();
this.server = http.createServer(this.app);
if (this.config.enableWebsocket) {
this.wsServer = new WebSocketServer({ server: this.server });
this.createWSServer(this.wsServer);
}
this.app.use(cors());
this.app.use(express.urlencoded({ extended: true, limit: '5000mb' }));
@@ -134,137 +93,6 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
}
}
createWSServer (newServer: WebSocketServer) {
newServer.on('connection', async (wsClient, wsReq) => {
if (!this.isEnable) {
wsClient.close();
return;
}
if (!this.authorizeWS(this.config.token, wsClient, wsReq)) {
return;
}
const paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url;
const isApiConnect = paramUrl === '/api' || paramUrl === '/api/';
if (!isApiConnect) {
this.connectEvent(this.core, wsClient);
}
wsClient.on('error', (err) => this.logger.log('[OneBot] [HTTP WebSocket] Client Error:', err.message));
wsClient.on('message', (message) => {
this.handleWSMessage(wsClient, message).then().catch(e => this.logger.logError(e));
});
wsClient.on('ping', () => {
wsClient.pong();
});
wsClient.on('pong', () => {
// this.logger.logDebug('[OneBot] [HTTP WebSocket] Pong received');
});
wsClient.once('close', () => {
this.wsClientsMutex.runExclusive(async () => {
const NormolIndex = this.wsClients.indexOf(wsClient);
if (NormolIndex !== -1) {
this.wsClients.splice(NormolIndex, 1);
}
const EventIndex = this.wsClientWithEvent.indexOf(wsClient);
if (EventIndex !== -1) {
this.wsClientWithEvent.splice(EventIndex, 1);
}
if (this.wsClientWithEvent.length === 0) {
this.stopHeartbeat();
}
});
});
await this.wsClientsMutex.runExclusive(async () => {
if (!isApiConnect) {
this.wsClientWithEvent.push(wsClient);
}
this.wsClients.push(wsClient);
if (this.wsClientWithEvent.length > 0) {
this.startHeartbeat();
}
});
}).on('error', (err) => this.logger.log('[OneBot] [HTTP WebSocket] Server Error:', err.message));
}
connectEvent (core: any, wsClient: WebSocket) {
try {
this.checkStateAndReply<unknown>(new OB11LifeCycleEvent(core, LifeCycleSubType.CONNECT), wsClient).catch(e => this.logger.logError('[OneBot] [HTTP WebSocket] 发送生命周期失败', e));
} catch (e) {
this.logger.logError('[OneBot] [HTTP WebSocket] 发送生命周期失败', e);
}
}
private startHeartbeat () {
if (this.heartbeatIntervalId) return;
this.heartbeatIntervalId = setInterval(() => {
this.wsClientsMutex.runExclusive(async () => {
this.wsClientWithEvent.forEach((wsClient) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, 30000, this.core.selfInfo.online ?? true, true)));
}
});
});
}, 30000);
}
private stopHeartbeat () {
if (this.heartbeatIntervalId) {
clearInterval(this.heartbeatIntervalId);
this.heartbeatIntervalId = null;
}
}
private authorizeWS (token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
if (!token || token.length === 0) return true;
const url = new URL(wsReq?.url || '', `http://${wsReq.headers.host}`);
const QueryClientToken = url.searchParams.get('access_token');
const HeaderClientToken = wsReq.headers.authorization?.split('Bearer ').pop() || '';
const ClientToken = typeof (QueryClientToken) === 'string' && QueryClientToken !== '' ? QueryClientToken : HeaderClientToken;
if (ClientToken === token) {
return true;
}
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')));
wsClient.close();
return false;
}
private async checkStateAndReply<T> (data: T, wsClient: WebSocket) {
return await new Promise<void>((resolve, reject) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(data));
resolve();
} else {
reject(new Error('WebSocket is not open'));
}
});
}
private async handleWSMessage (wsClient: WebSocket, message: RawData) {
let receiveData: { action: typeof ActionName[keyof typeof ActionName], params?: any, echo?: any; } = { action: ActionName.Unknown, params: {} };
let echo;
try {
receiveData = json5.parse(message.toString());
echo = receiveData.echo;
} catch {
await this.checkStateAndReply<unknown>(OB11Response.error('json解析失败,请检查数据格式', 1400, echo), wsClient);
return;
}
receiveData.params = (receiveData?.params) ? receiveData.params : {};
const action = this.actions.get(receiveData.action as any);
if (!action) {
this.logger.logError('[OneBot] [HTTP WebSocket] 发生错误', '不支持的API ' + receiveData.action);
await this.checkStateAndReply<unknown>(OB11Response.error('不支持的API ' + receiveData.action, 1404, echo), wsClient);
return;
}
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config, {
send: async (data: object) => {
await this.checkStateAndReply<unknown>({ ...OB11Response.ok(data, echo ?? '', true) }, wsClient);
},
});
await this.checkStateAndReply<unknown>({ ...retdata }, wsClient);
}
async httpApiRequest (req: Request, res: Response, request_sse: boolean = false) {
let payload = req.body;
if (req.method === 'get') {
@@ -324,7 +152,6 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
async reload (newConfig: HttpServerConfig) {
const wasEnabled = this.isEnable;
const oldPort = this.config.port;
const oldEnableWebsocket = this.config.enableWebsocket;
this.config = newConfig;
if (newConfig.enable && !wasEnabled) {
@@ -335,7 +162,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
return OB11NetworkReloadType.NetWorkClose;
}
if (oldPort !== newConfig.port || oldEnableWebsocket !== newConfig.enableWebsocket) {
if (oldPort !== newConfig.port) {
this.close();
if (newConfig.enable) {
this.open();

View File

@@ -21,7 +21,7 @@ export class OB11NetworkManager {
async emitEvent (event: OB11EmitEventContent) {
return Promise.all(Array.from(this.adapters.values()).map(async adapter => {
if (adapter.isActive) {
if (adapter.isEnable) {
return await adapter.onEvent(event);
}
}));
@@ -34,7 +34,7 @@ export class OB11NetworkManager {
async emitEventByName (names: string[], event: OB11EmitEventContent) {
return Promise.all(names.map(async name => {
const adapter = this.adapters.get(name);
if (adapter && adapter.isActive) {
if (adapter && adapter.isEnable) {
return await adapter.onEvent(event);
}
}));
@@ -43,29 +43,29 @@ export class OB11NetworkManager {
async emitEventByNames (map: Map<string, OB11EmitEventContent>) {
return Promise.all(Array.from(map.entries()).map(async ([name, event]) => {
const adapter = this.adapters.get(name);
if (adapter && adapter.isActive) {
if (adapter && adapter.isEnable) {
return await adapter.onEvent(event);
}
}));
}
registerAdapter<CT extends NetworkAdapterConfig> (adapter: IOB11NetworkAdapter<CT>) {
registerAdapter<CT extends NetworkAdapterConfig>(adapter: IOB11NetworkAdapter<CT>) {
this.adapters.set(adapter.name, adapter);
}
async registerAdapterAndOpen<CT extends NetworkAdapterConfig> (adapter: IOB11NetworkAdapter<CT>) {
async registerAdapterAndOpen<CT extends NetworkAdapterConfig>(adapter: IOB11NetworkAdapter<CT>) {
this.registerAdapter(adapter);
await adapter.open();
}
async closeSomeAdapters<CT extends NetworkAdapterConfig> (adaptersToClose: IOB11NetworkAdapter<CT>[]) {
async closeSomeAdapters<CT extends NetworkAdapterConfig>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
for (const adapter of adaptersToClose) {
this.adapters.delete(adapter.name);
await adapter.close();
}
}
async closeSomeAdaterWhenOpen<CT extends NetworkAdapterConfig> (adaptersToClose: IOB11NetworkAdapter<CT>[]) {
async closeSomeAdaterWhenOpen<CT extends NetworkAdapterConfig>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
for (const adapter of adaptersToClose) {
this.adapters.delete(adapter.name);
if (adapter.isEnable) {
@@ -88,21 +88,17 @@ export class OB11NetworkManager {
this.adapters.clear();
}
async readloadAdapter<T> (name: string, config: T) {
async readloadAdapter<T>(name: string, config: T) {
const adapter = this.adapters.get(name);
if (adapter) {
await adapter.reload(config);
}
}
async readloadSomeAdapters<T> (configMap: Map<string, T>) {
async readloadSomeAdapters<T>(configMap: Map<string, T>) {
await Promise.all(Array.from(configMap.entries()).map(([name, config]) => this.readloadAdapter(name, config)));
}
hasActiveAdapters (): boolean {
return Array.from(this.adapters.values()).some(adapter => adapter.isActive);
}
async getAllConfig () {
return Array.from(this.adapters.values()).map(adapter => adapter.config);
}

View File

@@ -33,10 +33,6 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
private readonly pluginPath: string;
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
declare config: PluginConfig;
override get isActive (): boolean {
return this.isEnable && this.loadedPlugins.size > 0;
}
constructor (
name: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
) {
@@ -255,7 +251,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
this.logger.log(`[Plugin Adapter] Unloaded plugin: ${pluginName}`);
}
async onEvent<T extends OB11EmitEventContent> (event: T) {
async onEvent<T extends OB11EmitEventContent>(event: T) {
if (!this.isEnable) {
return;
}
@@ -363,7 +359,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
// 重新加载插件
const isDirectory = fs.statSync(plugin.pluginPath).isDirectory() &&
plugin.pluginPath !== this.pluginPath;
plugin.pluginPath !== this.pluginPath;
if (isDirectory) {
const dirname = path.basename(plugin.pluginPath);

View File

@@ -33,10 +33,6 @@ export class OB11PluginAdapter extends IOB11NetworkAdapter<PluginConfig> {
private readonly pluginPath: string;
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
declare config: PluginConfig;
override get isActive (): boolean {
return this.isEnable && this.loadedPlugins.size > 0;
}
constructor (
name: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
) {

View File

@@ -13,10 +13,6 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
private connection: WebSocket | null = null;
private heartbeatRef: NodeJS.Timeout | null = null;
override get isActive (): boolean {
return this.isEnable && !!this.connection && this.connection.readyState === WebSocket.OPEN;
}
async onEvent<T extends OB11EmitEventContent> (event: T) {
if (this.connection && this.connection.readyState === WebSocket.OPEN) {
this.connection.send(JSON.stringify(event));

View File

@@ -21,10 +21,6 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
private heartbeatIntervalId: NodeJS.Timeout | null = null;
wsClientWithEvent: WebSocket[] = [];
override get isActive (): boolean {
return this.isEnable && this.wsClientWithEvent.length > 0;
}
constructor (
name: string, config: WebsocketServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
) {
@@ -74,9 +70,6 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
if (EventIndex !== -1) {
this.wsClientWithEvent.splice(EventIndex, 1);
}
if (this.wsClientWithEvent.length === 0) {
this.stopHeartbeat();
}
});
});
await this.wsClientsMutex.runExclusive(async () => {
@@ -84,9 +77,6 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
this.wsClientWithEvent.push(wsClient);
}
this.wsClients.push(wsClient);
if (this.wsClientWithEvent.length > 0) {
this.startHeartbeat();
}
});
}).on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Server Error:', err.message));
}
@@ -124,6 +114,9 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
this.logger.log('[OneBot] [WebSocket Server] Server Started', typeof (addressInfo) === 'string' ? addressInfo : addressInfo?.address + ':' + addressInfo?.port);
this.isEnable = true;
if (this.config.heartInterval > 0) {
this.registerHeartBeat();
}
}
async close () {
@@ -135,7 +128,10 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
this.logger.log('[OneBot] [WebSocket Server] Server Closed');
}
});
this.stopHeartbeat();
if (this.heartbeatIntervalId) {
clearInterval(this.heartbeatIntervalId);
this.heartbeatIntervalId = null;
}
await this.wsClientsMutex.runExclusive(async () => {
this.wsClients.forEach((wsClient) => {
wsClient.close();
@@ -145,8 +141,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
});
}
private startHeartbeat () {
if (this.heartbeatIntervalId || this.config.heartInterval <= 0) return;
private registerHeartBeat () {
this.heartbeatIntervalId = setInterval(() => {
this.wsClientsMutex.runExclusive(async () => {
this.wsClientWithEvent.forEach((wsClient) => {
@@ -158,13 +153,6 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
}, this.config.heartInterval);
}
private stopHeartbeat () {
if (this.heartbeatIntervalId) {
clearInterval(this.heartbeatIntervalId);
this.heartbeatIntervalId = null;
}
}
private authorize (token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
if (!token || token.length === 0) return true;// 客户端未设置密钥
const url = new URL(wsReq?.url || '', `http://${wsReq.headers.host}`);
@@ -247,9 +235,12 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
}
if (oldHeartbeatInterval !== newConfig.heartInterval) {
this.stopHeartbeat();
if (newConfig.heartInterval > 0 && this.isEnable && this.wsClientWithEvent.length > 0) {
this.startHeartbeat();
if (this.heartbeatIntervalId) {
clearInterval(this.heartbeatIntervalId);
this.heartbeatIntervalId = null;
}
if (newConfig.heartInterval > 0 && this.isEnable) {
this.registerHeartBeat();
}
return OB11NetworkReloadType.NetWorkReload;
}

View File

@@ -31,7 +31,6 @@ export interface OB11Message {
font: number;
post_type?: EventType;
raw?: RawMessage;
emoji_likes_list?: Array<{ emoji_id: string; emoji_type: string; likes_cnt: string; }>;// 仅get_msg生效
}
// 合并转发消息接口定义
@@ -47,7 +46,7 @@ export interface OB11Return<DataType> {
message: string;
echo?: unknown; // ws调用api才有此字段
wording?: string; // go-cqhttp字段错误信息
stream?: 'stream-action' | 'normal-action'; // 流式返回标记
stream?: 'stream-action' | 'normal-action' ; // 流式返回标记
}
// 消息数据类型枚举
@@ -187,7 +186,7 @@ export interface OB11MessageNode {
name?: string; // compatible with go-cqhttp
content: OB11MessageMixType;
source?: string;
news?: { text: string; }[];
news?: { text: string }[];
summary?: string;
prompt?: string;
time?: string;
@@ -211,13 +210,13 @@ export interface OB11MessageIdMusic {
// 自定义音乐消息接口定义
export interface OB11MessageCustomMusic {
type: OB11MessageDataType.music;
data: Omit<CustomMusicSignPostData, 'singer'> & { content?: string; };
data: Omit<CustomMusicSignPostData, 'singer'> & { content?: string };
}
// JSON消息接口定义
export interface OB11MessageJson {
type: OB11MessageDataType.json;
data: { config?: { token: string; }, data: string | object; };
data: { config?: { token: string }, data: string | object };
}
// 骰子消息接口定义
@@ -255,12 +254,12 @@ export interface OB11MessageForward {
// 消息数据类型定义
export type OB11MessageData =
OB11MessageText |
OB11MessageFace | OB11MessageMFace |
OB11MessageAt | OB11MessageReply |
OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo |
OB11MessageNode | OB11MessageIdMusic | OB11MessageCustomMusic | OB11MessageJson |
OB11MessageDice | OB11MessageRPS | OB11MessageMarkdown | OB11MessageForward | OB11MessageContext | OB11MessagePoke;
OB11MessageText |
OB11MessageFace | OB11MessageMFace |
OB11MessageAt | OB11MessageReply |
OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo |
OB11MessageNode | OB11MessageIdMusic | OB11MessageCustomMusic | OB11MessageJson |
OB11MessageDice | OB11MessageRPS | OB11MessageMarkdown | OB11MessageForward | OB11MessageContext | OB11MessagePoke;
// 发送消息接口定义
export interface OB11PostSendMsg {
@@ -271,7 +270,7 @@ export interface OB11PostSendMsg {
messages?: OB11MessageMixType;
auto_escape?: boolean | string;
source?: string;
news?: { text: string; }[];
news?: { text: string }[];
summary?: string;
prompt?: string;
time?: string;

View File

@@ -29,6 +29,7 @@ import { napCatVersion } from 'napcat-common/src/version';
import { NodeIO3MiscListener } from 'napcat-core/listeners/NodeIO3MiscListener';
import { sleep } from 'napcat-common/src/helper';
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
import { connectToNamedPipe } from './pipe';
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
import { logSubscription, LogWrapper } from '@/napcat-core/helper/log';
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
@@ -127,13 +128,10 @@ async function handleLogin (
const loginListener = new NodeIKernelLoginListener();
loginListener.onUserLoggedIn = (userid: string) => {
const tips = `当前账号(${userid})已登录,无法重复登录`;
logger.logError(tips);
WebUiDataRuntime.setQQLoginError(tips);
logger.logError(`当前账号(${userid})已登录,无法重复登录`);
};
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
context.isLogined = true;
WebUiDataRuntime.setQQLoginStatus(true);
inner_resolve({
uid: loginResult.uid,
uin: loginResult.uin,
@@ -172,16 +170,13 @@ async function handleLogin (
logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode);
if (errType === 1 && errCode === 3) {
// 二维码过期刷新
WebUiDataRuntime.setQQLoginError('二维码已过期,请刷新');
}
loginService.getQRCodePicture();
}
};
loginListener.onLoginFailed = (...args) => {
const errInfo = JSON.stringify(args);
logger.logError('[Core] [Login] Login Error , ErrInfo: ', errInfo);
WebUiDataRuntime.setQQLoginError(`登录失败: ${errInfo}`);
logger.logError('[Core] [Login] Login Error , ErrInfo: ', JSON.stringify(args));
};
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
@@ -189,29 +184,17 @@ async function handleLogin (
return await selfInfo;
}
async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWrapper, loginService: NodeIKernelLoginService, quickLoginUin: string | undefined, historyLoginList: LoginListItem[]) {
// 注册刷新二维码回调
WebUiDataRuntime.setRefreshQRCodeCallback(async () => {
loginService.getQRCodePicture();
});
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
return await new Promise((resolve) => {
if (uin) {
logger.log('正在快速登录 ', uin);
loginService.quickLoginWithUin(uin).then(res => {
if (res.loginErrorInfo.errMsg) {
WebUiDataRuntime.setQQLoginError(res.loginErrorInfo.errMsg);
loginService.getQRCodePicture();
resolve({ result: false, message: res.loginErrorInfo.errMsg });
} else {
WebUiDataRuntime.setQQLoginStatus(true);
WebUiDataRuntime.setQQLoginError('');
resolve({ result: true, message: '' });
}
resolve({ result: true, message: '' });
}).catch((e) => {
logger.logError(e);
WebUiDataRuntime.setQQLoginError('快速登录发生错误');
loginService.getQRCodePicture();
resolve({ result: false, message: '快速登录发生错误' });
});
} else {
@@ -226,7 +209,6 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr
.then(result => {
if (result.loginErrorInfo.errMsg) {
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
WebUiDataRuntime.setQQLoginError(result.loginErrorInfo.errMsg);
if (!context.isLogined) loginService.getQRCodePicture();
}
})
@@ -342,9 +324,9 @@ export async function NCoreInitShell () {
// 初始化 FFmpeg 服务
await FFmpegService.init(pathWrapper.binaryPath, logger);
// if (process.env['NAPCAT_DISABLE_PIPE'] !== '1') {
// await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e));
// }
if (process.env['NAPCAT_DISABLE_PIPE'] !== '1') {
await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e));
}
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用
@@ -470,10 +452,6 @@ export class NapCatShell {
async InitNapCat () {
await this.core.initCore();
// 监听下线通知并同步到 WebUI
this.core.event.on('KickedOffLine', (tips: string) => {
WebUiDataRuntime.setQQLoginError(tips);
});
const oneBotAdapter = new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper);
// 注册到 WebUiDataRuntime供调试功能使用
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
@@ -481,3 +459,4 @@ export class NapCatShell {
.catch(e => this.context.logger.logError('初始化OneBot失败', e));
}
}

View File

@@ -1,319 +1,2 @@
import { NCoreInitShell } from './base';
import { NapCatPathWrapper } from '@/napcat-common/src/path';
import { LogWrapper } from '@/napcat-core/helper/log';
import { connectToNamedPipe } from './pipe';
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
import { createProcessManager, type IProcessManager, type IWorkerProcess } from './process-api';
import path from 'path';
import { fileURLToPath } from 'url';
// ES 模块中获取 __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 环境变量配置
const ENV = {
isWorkerProcess: process.env['NAPCAT_WORKER_PROCESS'] === '1',
isMultiProcessDisabled: process.env['NAPCAT_DISABLE_MULTI_PROCESS'] === '1',
isPipeDisabled: process.env['NAPCAT_DISABLE_PIPE'] === '1',
} as const;
// 初始化日志
const pathWrapper = new NapCatPathWrapper();
const logger = new LogWrapper(pathWrapper.logsPath);
// 进程管理器和当前 Worker 进程引用
let processManager: IProcessManager | null = null;
let currentWorker: IWorkerProcess | null = null;
let isElectron = false;
/**
* 获取进程类型名称(用于日志)
*/
function getProcessTypeName (): string {
return isElectron ? 'UtilityProcess' : 'Fork';
}
/**
* 获取 Worker 脚本路径
*/
function getWorkerScriptPath (): string {
return __filename.endsWith('.mjs')
? path.join(__dirname, 'napcat.mjs')
: path.join(__dirname, 'napcat.js');
}
/**
* 检查进程是否存在
*/
function isProcessAlive (pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
/**
* 强制终止进程
*/
function forceKillProcess (pid: number): void {
try {
process.kill(pid, 'SIGKILL');
logger.log(`[NapCat] [Process] 已强制终止进程 ${pid}`);
} catch (error) {
logger.logError(`[NapCat] [Process] 强制终止进程失败:`, error);
}
}
/**
* 重启 Worker 进程
*/
export async function restartWorker (): Promise<void> {
logger.log('[NapCat] [Process] 正在重启Worker进程...');
if (!currentWorker) {
logger.logWarn('[NapCat] [Process] 没有运行中的Worker进程');
await startWorker();
return;
}
const workerPid = currentWorker.pid;
logger.log(`[NapCat] [Process] 准备关闭Worker进程PID: ${workerPid}`);
// 1. 通知旧进程准备重启(旧进程会自行退出)
currentWorker.postMessage({ type: 'restart-prepare' });
// 2. 等待进程退出(最多 5 秒,给更多时间让进程自行清理)
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
logger.logWarn('[NapCat] [Process] Worker进程未在 5 秒内退出,尝试发送强制关闭信号');
currentWorker?.postMessage({ type: 'shutdown' });
// 再等待 2 秒
setTimeout(() => {
logger.logWarn('[NapCat] [Process] Worker进程仍未退出尝试 kill');
currentWorker?.kill();
resolve();
}, 2000);
}, 5000);
currentWorker?.once('exit', () => {
clearTimeout(timeout);
logger.log('[NapCat] [Process] Worker进程已正常退出');
resolve();
});
});
// 3. 二次确认进程是否真的被终止(兜底检查)
if (workerPid) {
logger.log(`[NapCat] [Process] 检查进程 ${workerPid} 是否已终止...`);
if (isProcessAlive(workerPid)) {
logger.logWarn(`[NapCat] [Process] 进程 ${workerPid} 仍在运行,尝试强制杀掉(兜底)`);
forceKillProcess(workerPid);
// 等待 1 秒后再次检查
await new Promise(resolve => setTimeout(resolve, 1000));
if (isProcessAlive(workerPid)) {
logger.logError(`[NapCat] [Process] 进程 ${workerPid} 无法终止,可能需要手动处理`);
} else {
logger.log(`[NapCat] [Process] 进程 ${workerPid} 已被强制终止`);
}
} else {
logger.log(`[NapCat] [Process] 进程 ${workerPid} 已确认终止`);
}
}
// 4. 等待 3 秒后启动新进程
logger.log('[NapCat] [Process] Worker进程已关闭等待 3 秒后启动新进程...');
await new Promise(resolve => setTimeout(resolve, 3000));
// 5. 启动新进程
await startWorker();
logger.log('[NapCat] [Process] Worker进程重启完成');
}
/**
* 启动 Worker 进程
*/
async function startWorker (): Promise<void> {
if (!processManager) {
throw new Error('进程管理器未初始化');
}
const workerScript = getWorkerScriptPath();
const processType = getProcessTypeName();
const child = processManager.createWorker(workerScript, [], {
env: {
...process.env,
NAPCAT_WORKER_PROCESS: '1',
},
stdio: isElectron ? 'pipe' : ['inherit', 'pipe', 'pipe', 'ipc'],
});
currentWorker = child;
// 监听标准输出(直接转发)
if (child.stdout) {
child.stdout.on('data', (data: Buffer) => {
process.stdout.write(data);
});
}
// 监听标准错误(直接转发)
if (child.stderr) {
child.stderr.on('data', (data: Buffer) => {
process.stderr.write(data);
});
}
// 监听子进程消息
child.on('message', (msg: unknown) => {
logger.log(`[NapCat] [${processType}] 收到Worker消息:`, msg);
// 处理重启请求
if (typeof msg === 'object' && msg !== null && 'type' in msg && msg.type === 'restart') {
logger.log(`[NapCat] [${processType}] 收到重启请求正在重启Worker进程...`);
restartWorker().catch(e => {
logger.logError(`[NapCat] [${processType}] 重启Worker进程失败:`, e);
});
}
});
// 监听子进程退出
child.on('exit', (code: unknown) => {
const exitCode = typeof code === 'number' ? code : 0;
if (exitCode !== 0) {
logger.logError(`[NapCat] [${processType}] Worker进程退出退出码: ${exitCode}`);
} else {
logger.log(`[NapCat] [${processType}] Worker进程正常退出`);
}
});
child.on('spawn', () => {
logger.log(`[NapCat] [${processType}] Worker进程已生成`);
});
}
/**
* 启动 Master 进程
*/
async function startMasterProcess (): Promise<void> {
const processType = getProcessTypeName();
logger.log(`[NapCat] [${processType}] Master进程启动PID: ${process.pid}`);
// 连接命名管道(可通过环境变量禁用)
if (!ENV.isPipeDisabled) {
await connectToNamedPipe(logger).catch(e =>
logger.logError('命名管道连接失败', e)
);
} else {
logger.log(`[NapCat] [${processType}] 命名管道已禁用 (NAPCAT_DISABLE_PIPE=1)`);
}
// 启动 Worker 进程
await startWorker();
// 优雅关闭处理
const shutdown = (signal: string) => {
logger.log(`[NapCat] [Process] 收到${signal}信号,正在关闭...`);
if (currentWorker) {
currentWorker.postMessage({ type: 'shutdown' });
setTimeout(() => {
currentWorker?.kill();
process.exit(0);
}, 1000);
} else {
process.exit(0);
}
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
}
/**
* 启动 Worker 进程(子进程入口)
*/
async function startWorkerProcess (): Promise<void> {
if (!processManager) {
throw new Error('进程管理器未初始化');
}
const processType = getProcessTypeName();
logger.log(`[NapCat] [${processType}] Worker进程启动PID: ${process.pid}`);
// 监听来自父进程的消息
processManager.onParentMessage((msg: unknown) => {
if (typeof msg === 'object' && msg !== null && 'type' in msg) {
if (msg.type === 'restart-prepare') {
logger.log(`[NapCat] [${processType}] 收到重启准备信号,正在主动退出...`);
setTimeout(() => {
process.exit(0);
}, 100);
} else if (msg.type === 'shutdown') {
logger.log(`[NapCat] [${processType}] 收到关闭信号,正在退出...`);
process.exit(0);
}
}
});
// 注册重启进程函数到 WebUI
WebUiDataRuntime.setRestartProcessCall(async () => {
try {
const success = processManager!.sendToParent({ type: 'restart' });
if (success) {
return { result: true, message: '进程重启请求已发送' };
} else {
return { result: false, message: '无法与主进程通信' };
}
} catch (e) {
logger.logError('[NapCat] [Process] 发送重启请求失败:', e);
return {
result: false,
message: '发送重启请求失败: ' + (e as Error).message
};
}
});
// 启动 NapCat 核心
await NCoreInitShell();
}
/**
* 主入口
*/
async function main (): Promise<void> {
// 单进程模式:直接启动核心
if (ENV.isMultiProcessDisabled) {
logger.log('[NapCat] [SingleProcess] 多进程模式已禁用,直接启动核心');
await NCoreInitShell();
return;
}
// 多进程模式:初始化进程管理器
const result = await createProcessManager();
processManager = result.manager;
isElectron = result.isElectron;
logger.log(`[NapCat] [Process] 检测到 ${isElectron ? 'Electron' : 'Node.js'} 环境`);
// 根据进程类型启动
if (ENV.isWorkerProcess) {
await startWorkerProcess();
} else {
await startMasterProcess();
}
}
// 启动应用
main().catch((e: Error) => {
logger.logError('[NapCat] [Process] 启动失败:', e);
process.exit(1);
});
NCoreInitShell();

View File

@@ -1,178 +0,0 @@
import type { Readable } from 'stream';
import type { fork as forkType } from 'child_process';
// 扩展 Process 类型以支持 parentPort
declare global {
namespace NodeJS {
interface Process {
parentPort?: {
on (event: 'message', listener: (e: { data: unknown; }) => void): void;
postMessage (message: unknown): void;
};
}
}
}
/**
* 统一的进程接口
*/
export interface IWorkerProcess {
readonly pid: number | undefined;
readonly stdout: Readable | null;
readonly stderr: Readable | null;
postMessage (message: unknown): void;
kill (): boolean;
on (event: string, listener: (...args: unknown[]) => void): void;
once (event: string, listener: (...args: unknown[]) => void): void;
}
/**
* 进程创建选项
*/
export interface ProcessOptions {
env: NodeJS.ProcessEnv;
stdio: 'pipe' | 'ignore' | 'inherit' | Array<'pipe' | 'ignore' | 'inherit' | 'ipc'>;
}
/**
* 进程管理器接口
*/
export interface IProcessManager {
createWorker (modulePath: string, args: string[], options: ProcessOptions): IWorkerProcess;
onParentMessage (handler: (message: unknown) => void): void;
sendToParent (message: unknown): boolean;
}
/**
* Electron utilityProcess 包装器
*/
class ElectronProcessManager implements IProcessManager {
private utilityProcess: {
fork (modulePath: string, args: string[], options: unknown): unknown;
};
constructor (utilityProcess: { fork (modulePath: string, args: string[], options: unknown): unknown; }) {
this.utilityProcess = utilityProcess;
}
createWorker (modulePath: string, args: string[], options: ProcessOptions): IWorkerProcess {
const child: any = this.utilityProcess.fork(modulePath, args, options);
return {
pid: child.pid as number | undefined,
stdout: child.stdout as Readable | null,
stderr: child.stderr as Readable | null,
postMessage (message: unknown): void {
child.postMessage(message);
},
kill (): boolean {
return child.kill() as boolean;
},
on (event: string, listener: (...args: unknown[]) => void): void {
child.on(event, listener);
},
once (event: string, listener: (...args: unknown[]) => void): void {
child.once(event, listener);
},
};
}
onParentMessage (handler: (message: unknown) => void): void {
if (process.parentPort) {
process.parentPort.on('message', (e: { data: unknown; }) => {
handler(e.data);
});
}
}
sendToParent (message: unknown): boolean {
if (process.parentPort) {
process.parentPort.postMessage(message);
return true;
}
return false;
}
}
/**
* Node.js child_process 包装器
*/
class NodeProcessManager implements IProcessManager {
private forkFn: typeof forkType;
constructor (forkFn: typeof forkType) {
this.forkFn = forkFn;
}
createWorker (modulePath: string, args: string[], options: ProcessOptions): IWorkerProcess {
const child = this.forkFn(modulePath, args, options as any);
return {
pid: child.pid,
stdout: child.stdout,
stderr: child.stderr,
postMessage (message: unknown): void {
if (child.send) {
child.send(message as any);
}
},
kill (): boolean {
return child.kill();
},
on (event: string, listener: (...args: unknown[]) => void): void {
child.on(event, listener);
},
once (event: string, listener: (...args: unknown[]) => void): void {
child.once(event, listener);
},
};
}
onParentMessage (handler: (message: unknown) => void): void {
process.on('message', (message: unknown) => {
handler(message);
});
}
sendToParent (message: unknown): boolean {
if (process.send) {
process.send(message as any);
return true;
}
return false;
}
}
/**
* 检测运行环境并创建对应的进程管理器
*/
export async function createProcessManager (): Promise<{
manager: IProcessManager;
isElectron: boolean;
}> {
const isElectron = typeof process.versions['electron'] !== 'undefined';
if (isElectron) {
// @ts-ignore - electron 运行时存在但类型声明可能缺失
const electron = await import('electron');
return {
manager: new ElectronProcessManager(electron.utilityProcess),
isElectron: true,
};
} else {
const { fork } = await import('child_process');
return {
manager: new NodeProcessManager(fork),
isElectron: false,
};
}
}

View File

@@ -11,7 +11,6 @@ import react from '@vitejs/plugin-react-swc';
const external = [
'ws',
'express',
'electron'
];
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();

View File

@@ -1,24 +1,24 @@
{
"name": "napcat-vite",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.ts",
"scripts": {
"_build": "vite build"
},
"exports": {
".": {
"import": "./index.ts"
"name": "napcat-vite",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.ts",
"scripts": {
"build": "vite build"
},
"./*": {
"import": "./*"
"exports": {
".": {
"import": "./index.ts"
},
"./*": {
"import": "./*"
}
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@@ -1,21 +0,0 @@
import type { Request, Response } from 'express';
import { WebUiDataRuntime } from '../helper/Data';
import { sendError, sendSuccess } from '../utils/response';
/**
* 重启进程处理器
* POST /api/Process/Restart
*/
export async function RestartProcessHandler (_req: Request, res: Response) {
try {
const result = await WebUiDataRuntime.requestRestartProcess();
if (result.result) {
return sendSuccess(res, { message: result.message || '进程重启请求已发送' });
} else {
return sendError(res, result.message || '进程重启失败');
}
} catch (e) {
return sendError(res, '重启进程时发生错误: ' + (e as Error).message);
}
}

View File

@@ -1,9 +1,9 @@
import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
import { WebUiConfig } from '@/napcat-webui-backend/index';
import { isEmpty } from '@/napcat-webui-backend/src/utils/check';
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
import { WebUiConfig } from '@/napcat-webui-backend/index';
// 获取QQ登录二维码
export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
@@ -27,17 +27,9 @@ export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
// 获取QQ登录状态
export const QQCheckLoginStatusHandler: RequestHandler = async (_, res) => {
// 从 OneBot 上下文获取实时的 selfInfo.online 状态
const oneBotContext = WebUiDataRuntime.getOneBotContext();
const selfInfo = oneBotContext?.core?.selfInfo;
const isOnline = selfInfo?.online;
const qqLoginStatus = WebUiDataRuntime.getQQLoginStatus();
// 必须同时满足已登录且在线online 必须明确为 true
const isLogin = qqLoginStatus && isOnline === true;
const data = {
isLogin,
isLogin: WebUiDataRuntime.getQQLoginStatus(),
qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(),
loginError: WebUiDataRuntime.getQQLoginError(),
};
return sendSuccess(res, data);
};
@@ -96,15 +88,3 @@ export const setAutoLoginAccountHandler: RequestHandler = async (req, res) => {
await WebUiConfig.UpdateAutoLoginAccount(uin);
return sendSuccess(res, null);
};
// 刷新QQ登录二维码
export const QQRefreshQRcodeHandler: RequestHandler = async (_, res) => {
// 判断是否已经登录
if (WebUiDataRuntime.getQQLoginStatus()) {
// 已经登录
return sendError(res, 'QQ Is Logined');
}
// 刷新二维码
await WebUiDataRuntime.refreshQRCode();
return sendSuccess(res, null);
};

View File

@@ -14,7 +14,6 @@ const LoginRuntime: LoginRuntimeType = {
uin: '',
nick: '',
},
QQLoginError: '',
QQVersion: 'unknown',
OneBotContext: null,
onQQLoginStatusChange: async (status: boolean) => {
@@ -22,9 +21,6 @@ const LoginRuntime: LoginRuntimeType = {
},
onWebUiTokenChange: async (_token: string) => {
},
onRefreshQRCode: async () => {
// 默认空实现,由 shell 注册真实回调
},
NapCatHelper: {
onOB11ConfigChanged: async () => {
@@ -33,9 +29,6 @@ const LoginRuntime: LoginRuntimeType = {
onQuickLoginRequested: async () => {
return { result: false, message: '' };
},
onRestartProcessRequested: async () => {
return { result: false, message: '重启功能未初始化' };
},
QQLoginList: [],
NewQQLoginList: [],
},
@@ -170,33 +163,4 @@ export const WebUiDataRuntime = {
getOneBotContext (): any | null {
return LoginRuntime.OneBotContext;
},
setRestartProcessCall (func: () => Promise<{ result: boolean; message: string; }>): void {
LoginRuntime.NapCatHelper.onRestartProcessRequested = func;
},
requestRestartProcess: async function () {
return await LoginRuntime.NapCatHelper.onRestartProcessRequested();
},
setQQLoginError (error: string): void {
LoginRuntime.QQLoginError = error;
},
getQQLoginError (): string {
return LoginRuntime.QQLoginError;
},
setRefreshQRCodeCallback (func: () => Promise<void>): void {
LoginRuntime.onRefreshQRCode = func;
},
getRefreshQRCodeCallback (): () => Promise<void> {
return LoginRuntime.onRefreshQRCode;
},
refreshQRCode: async function () {
LoginRuntime.QQLoginError = '';
await LoginRuntime.onRefreshQRCode();
},
};

View File

@@ -1,9 +0,0 @@
import { Router } from 'express';
import { RestartProcessHandler } from '../api/Process';
const router = Router();
// POST /api/Process/Restart - 重启进程
router.post('/Restart', RestartProcessHandler);
export { router as ProcessRouter };

View File

@@ -9,7 +9,6 @@ import {
getQQLoginInfoHandler,
getAutoLoginAccountHandler,
setAutoLoginAccountHandler,
QQRefreshQRcodeHandler,
} from '@/napcat-webui-backend/src/api/QQLogin';
const router = Router();
@@ -29,7 +28,5 @@ router.post('/GetQQLoginInfo', getQQLoginInfoHandler);
router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler);
// router:设置自动登录QQ账号
router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
// router:刷新QQ登录二维码
router.post('/RefreshQRcode', QQRefreshQRcodeHandler);
export { router as QQLoginRouter };

View File

@@ -16,7 +16,6 @@ import { FileRouter } from './File';
import { WebUIConfigRouter } from './WebUIConfig';
import { UpdateNapCatRouter } from './UpdateNapCat';
import DebugRouter from '@/napcat-webui-backend/src/api/Debug';
import { ProcessRouter } from './Process';
const router = Router();
@@ -45,7 +44,5 @@ router.use('/WebUIConfig', WebUIConfigRouter);
router.use('/UpdateNapCat', UpdateNapCatRouter);
// router:调试相关路由
router.use('/Debug', DebugRouter);
// router:进程管理相关路由
router.use('/Process', ProcessRouter);
export { router as ALLRouter };

View File

@@ -43,17 +43,14 @@ export interface LoginRuntimeType {
QQQRCodeURL: string;
QQLoginUin: string;
QQLoginInfo: SelfInfo;
QQLoginError: string;
QQVersion: string;
onQQLoginStatusChange: (status: boolean) => Promise<void>;
onWebUiTokenChange: (token: string) => Promise<void>;
onRefreshQRCode: () => Promise<void>;
WebUiConfigQuickFunction: () => Promise<void>;
OneBotContext: any | null; // OneBot 上下文,用于调试功能
NapCatHelper: {
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>;
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
onRestartProcessRequested: () => Promise<{ result: boolean; message: string; }>;
QQLoginList: string[];
NewQQLoginList: LoginListItem[];
};

View File

@@ -52,7 +52,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
onNativeClose();
}}
classNames={{
backdrop: 'z-[99] backdrop-blur-sm',
backdrop: 'z-[99]',
wrapper: 'z-[99]',
}}
{...rest}

View File

@@ -2,9 +2,9 @@ import GenericForm, { random_token } from './generic_form';
import type { Field } from './generic_form';
export interface HTTPServerFormProps {
data?: OneBotConfig['network']['httpServers'][0];
onClose: () => void;
onSubmit: (data: OneBotConfig['network']['httpServers'][0]) => Promise<void>;
data?: OneBotConfig['network']['httpServers'][0]
onClose: () => void
onSubmit: (data: OneBotConfig['network']['httpServers'][0]) => Promise<void>
}
type HTTPServerFormType = OneBotConfig['network']['httpServers'];
@@ -20,7 +20,7 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
host: '127.0.0.1',
port: 3000,
enableCors: true,
enableWebsocket: false,
enableWebsocket: true,
messagePostFormat: 'array',
token: random_token(16),
debug: false,

View File

@@ -2,11 +2,11 @@ import GenericForm, { random_token } from './generic_form';
import type { Field } from './generic_form';
export interface HTTPServerSSEFormProps {
data?: OneBotConfig['network']['httpSseServers'][0];
onClose: () => void;
data?: OneBotConfig['network']['httpSseServers'][0]
onClose: () => void
onSubmit: (
data: OneBotConfig['network']['httpSseServers'][0]
) => Promise<void>;
) => Promise<void>
}
type HTTPServerSSEFormType = OneBotConfig['network']['httpSseServers'];
@@ -22,7 +22,7 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
host: '127.0.0.1',
port: 3000,
enableCors: true,
enableWebsocket: false,
enableWebsocket: true,
messagePostFormat: 'array',
token: random_token(16),
debug: false,

View File

@@ -2,7 +2,6 @@ import { Modal, ModalContent, ModalHeader } from '@heroui/modal';
import toast from 'react-hot-toast';
import useConfig from '@/hooks/use-config';
import useDialog from '@/hooks/use-dialog';
import HTTPClientForm from './http_client';
import HTTPServerForm from './http_server';
@@ -32,57 +31,23 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']> (
) => {
const { isOpen, onOpenChange, field, data } = props;
const { createNetworkConfig, updateNetworkConfig } = useConfig();
const dialog = useDialog();
const isCreate = !data;
const onSubmit = async (data: OneBotConfig['network'][typeof field][0]) => {
const saveData = async (dataToSave: OneBotConfig['network'][typeof field][0]) => {
try {
if (isCreate) {
await createNetworkConfig(field, dataToSave);
} else {
await updateNetworkConfig(field, dataToSave);
}
toast.success('保存配置成功');
} catch (error) {
const msg = (error as Error).message;
toast.error(`保存配置失败: ${msg}`);
throw error;
try {
if (isCreate) {
await createNetworkConfig(field, data);
} else {
await updateNetworkConfig(field, data);
}
};
toast.success('保存配置成功');
} catch (error) {
const msg = (error as Error).message;
if (['httpServers', 'httpSseServers', 'websocketServers'].includes(field)) {
const serverData = data as any;
if (!serverData.token) {
await new Promise<void>((resolve, reject) => {
dialog.confirm({
title: '安全警告',
content: (
<div>
<p>Token</p>
<p className='text-sm text-gray-500 mt-2'>(Token时Host将被强制限制为 127.0.0.1)</p>
</div>
),
onConfirm: async () => {
serverData.host = '127.0.0.1';
try {
await saveData(serverData);
resolve();
} catch (e) {
reject(e);
}
},
onCancel: () => {
reject(new Error('Cancelled'));
},
});
});
return;
}
toast.error(`保存配置失败: ${msg}`);
throw error;
}
await saveData(data);
};
const renderFormComponent = (onClose: () => void) => {

View File

@@ -1,70 +1,22 @@
import { Button } from '@heroui/button';
import { Spinner } from '@heroui/spinner';
import { QRCodeSVG } from 'qrcode.react';
import { IoAlertCircle, IoRefresh } from 'react-icons/io5';
interface QrCodeLoginProps {
qrcode: string;
loginError?: string;
onRefresh?: () => void;
qrcode: string
}
const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode, loginError, onRefresh }) => {
const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode }) => {
return (
<div className='flex flex-col items-center'>
{loginError
? (
<div className='flex flex-col items-center py-4'>
<div className='w-full flex justify-center mb-6'>
<div className='p-4 bg-danger-50 rounded-full'>
<IoAlertCircle className='text-danger' size={64} />
</div>
</div>
<div className='text-center space-y-2 px-4'>
<div className='text-xl font-bold text-danger'></div>
<div className='text-default-600 text-sm leading-relaxed max-w-[300px]'>
{loginError}
</div>
</div>
{onRefresh && (
<Button
className='mt-8 min-w-[160px]'
variant='solid'
color='primary'
size='lg'
startContent={<IoRefresh />}
onPress={onRefresh}
>
</Button>
)}
<div className='bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden'>
{!qrcode && (
<div className='absolute left-2 top-2 right-2 bottom-2 bg-white bg-opacity-50 backdrop-blur flex items-center justify-center'>
<Spinner color='primary' />
</div>
)
: (
<>
<div className='bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden'>
{!qrcode && (
<div className='absolute left-0 top-0 right-0 bottom-0 bg-white dark:bg-zinc-900 bg-opacity-90 backdrop-blur-sm flex items-center justify-center z-10'>
<Spinner color='primary' />
</div>
)}
<QRCodeSVG size={180} value={qrcode || ' '} />
</div>
<div className='mt-5 text-center text-default-500 text-sm'>使QQ或者TIM扫描上方二维码</div>
{onRefresh && qrcode && (
<Button
className='mt-4'
variant='flat'
color='primary'
size='sm'
startContent={<IoRefresh />}
onPress={onRefresh}
>
</Button>
)}
</>
)}
<QRCodeSVG size={180} value={qrcode} />
</div>
<div className='mt-5 text-center'>使QQ或者TIM扫描上方二维码</div>
</div>
);
};

View File

@@ -54,7 +54,7 @@ const renderItems = (items: MenuItem[], children = false) => {
isActive
? 'bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-400 shadow-none font-semibold translate-x-1'
: 'hover:bg-default-100 hover:translate-x-1',
b64img && 'backdrop-blur-md text-white dark:text-white'
b64img && 'backdrop-blur-md text-white'
)}
color={isActive ? 'primary' : 'default'}
endContent={

View File

@@ -81,25 +81,25 @@ const UpdateDialogContent: React.FC<{
return (
<div className='space-y-6'>
{/* 版本对比 */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 px-6 py-8 bg-default-50 dark:bg-default-100/5 rounded-xl border border-default-100 dark:border-default-100/10">
<div className="flex flex-col items-center gap-2 min-w-0 w-full sm:w-auto">
<div className="flex items-center justify-between px-6 py-8 bg-default-50 dark:bg-default-100/5 rounded-xl border border-default-100 dark:border-default-100/10">
<div className="flex flex-col items-center gap-2">
<span className="text-xs text-default-500 font-medium uppercase tracking-wider"></span>
<Chip size="lg" variant="flat" color="default" classNames={{ content: "font-mono font-bold text-base sm:text-lg break-all whitespace-normal text-center h-auto py-1" }}>
<Chip size="lg" variant="flat" color="default" classNames={{ content: "font-mono font-bold text-lg" }}>
v{currentVersion}
</Chip>
</div>
<div className="flex flex-col items-center text-primary-500 px-4 shrink-0">
<div className="flex flex-col items-center text-primary-500 px-4">
<div className="p-2 rounded-full bg-primary-50 dark:bg-primary-900/20">
<svg className="w-6 h-6 animate-pulse rotate-90 sm:rotate-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg className="w-6 h-6 animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</div>
</div>
<div className="flex flex-col items-center gap-2 min-w-0 w-full sm:w-auto">
<div className="flex flex-col items-center gap-2">
<span className="text-xs text-primary-500 font-medium uppercase tracking-wider"></span>
<Chip size="lg" color="primary" variant="shadow" classNames={{ content: "font-mono font-bold text-base sm:text-lg break-all whitespace-normal text-center h-auto py-1" }}>
<Chip size="lg" color="primary" variant="shadow" classNames={{ content: "font-mono font-bold text-lg" }}>
v{latestVersion}
</Chip>
</div>

View File

@@ -1,14 +0,0 @@
import { serverRequest } from '@/utils/request';
export default class ProcessManager {
/**
* 重启进程
*/
public static async restartProcess () {
const data = await serverRequest.post<ServerResponse<{ message: string; }>>(
'/Process/Restart'
);
return data.data.data;
}
}

View File

@@ -1,4 +1,3 @@
import { AxiosRequestConfig } from 'axios';
import { serverRequest } from '@/utils/request';
import { SelfInfo } from '@/types/user';
@@ -21,8 +20,8 @@ export default class QQManager {
public static async checkQQLoginStatus () {
const data = await serverRequest.post<
ServerResponse<{
isLogin: string;
qrcodeurl: string;
isLogin: string
qrcodeurl: string
}>
>('/QQLogin/CheckLoginStatus');
@@ -31,20 +30,16 @@ export default class QQManager {
public static async checkQQLoginStatusWithQrcode () {
const data = await serverRequest.post<
ServerResponse<{ qrcodeurl: string; isLogin: string; loginError?: string; }>
ServerResponse<{ qrcodeurl: string; isLogin: string }>
>('/QQLogin/CheckLoginStatus');
return data.data.data;
}
public static async refreshQRCode () {
await serverRequest.post<ServerResponse<null>>('/QQLogin/RefreshQRcode');
}
public static async getQQLoginQrcode () {
const data = await serverRequest.post<
ServerResponse<{
qrcode: string;
qrcode: string
}>
>('/QQLogin/GetQQLoginQrcode');
@@ -72,11 +67,9 @@ export default class QQManager {
});
}
public static async getQQLoginInfo (config?: AxiosRequestConfig) {
public static async getQQLoginInfo () {
const data = await serverRequest.post<ServerResponse<SelfInfo>>(
'/QQLogin/GetQQLoginInfo',
{},
config
'/QQLogin/GetQQLoginInfo'
);
return data.data.data;
}

View File

@@ -3,7 +3,7 @@ import { Button } from '@heroui/button';
import { useLocalStorage } from '@uidotdev/usehooks';
import clsx from 'clsx';
import { AnimatePresence, motion } from 'motion/react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { MdMenu, MdMenuOpen } from 'react-icons/md';
import { useLocation, useNavigate } from 'react-router-dom';
@@ -11,17 +11,14 @@ import { useLocation, useNavigate } from 'react-router-dom';
import key from '@/const/key';
import errorFallbackRender from '@/components/error_fallback';
import PageLoading from '@/components/page_loading';
// import PageLoading from "@/components/Loading/PageLoading";
import SideBar from '@/components/sidebar';
import useAuth from '@/hooks/auth';
import useDialog from '@/hooks/use-dialog';
import type { MenuItem } from '@/config/site';
import { siteConfig } from '@/config/site';
import QQManager from '@/controllers/qq_manager';
import ProcessManager from '@/controllers/process_manager';
import { waitForBackendReady } from '@/utils/process_utils';
const menus: MenuItem[] = siteConfig.navItems;
@@ -51,67 +48,7 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
const [openSideBar, setOpenSideBar] = useLocalStorage(key.sideBarOpen, true);
const [b64img] = useLocalStorage(key.backgroundImage, '');
const navigate = useNavigate();
const { isAuth, revokeAuth } = useAuth();
const dialog = useDialog();
const isOnlineRef = useRef(true);
const [isRestarting, setIsRestarting] = useState(false);
// 定期检查 QQ 在线状态,掉线时弹窗提示
useEffect(() => {
if (!isAuth) return;
const checkOnlineStatus = async () => {
const currentPath = location.pathname;
if (currentPath === '/qq_login' || currentPath === '/web_login') return;
try {
const info = await QQManager.getQQLoginInfo();
if (info?.online === false && isOnlineRef.current === true) {
isOnlineRef.current = false;
dialog.confirm({
title: '账号已离线',
content: '您的 QQ 账号已下线,请重新登录。',
confirmText: '重新登陆',
cancelText: '退出账户',
onConfirm: async () => {
setIsRestarting(true);
try {
await ProcessManager.restartProcess();
} catch (_e) {
// 忽略错误,因为后端正在重启关闭连接
}
// 轮询探测后端是否恢复
await waitForBackendReady(
15000, // 15秒超时
() => {
setIsRestarting(false);
window.location.reload();
},
() => {
setIsRestarting(false);
dialog.alert({
title: '启动超时',
content: '后端在 15 秒内未响应,请检查 NapCat 运行日志或手动重启。',
});
}
);
},
onCancel: () => {
revokeAuth();
navigate('/web_login');
},
});
} else if (info?.online === true) {
isOnlineRef.current = true;
}
} catch (_e) {
// 忽略请求错误
}
};
const timer = setInterval(checkOnlineStatus, 5000);
checkOnlineStatus();
return () => clearInterval(timer);
}, [isAuth, location.pathname]);
const { isAuth } = useAuth();
const checkIsQQLogin = async () => {
try {
const result = await QQManager.checkQQLoginStatus();
@@ -149,7 +86,6 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
backgroundPosition: 'center',
}}
>
<PageLoading loading={isRestarting} />
<SideBar
items={menus}
open={openSideBar}

View File

@@ -1,7 +1,6 @@
import { Input } from '@heroui/input';
import { Button } from '@heroui/button';
import { useRequest } from 'ahooks';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
@@ -9,11 +8,8 @@ import SaveButtons from '@/components/button/save_buttons';
import PageLoading from '@/components/page_loading';
import QQManager from '@/controllers/qq_manager';
import ProcessManager from '@/controllers/process_manager';
import { waitForBackendReady } from '@/utils/process_utils';
const LoginConfigCard = () => {
const [isRestarting, setIsRestarting] = useState(false);
const {
data: quickLoginData,
loading: quickLoginLoading,
@@ -57,35 +53,6 @@ const LoginConfigCard = () => {
}
};
const onRestartProcess = async () => {
setIsRestarting(true);
try {
const result = await ProcessManager.restartProcess();
toast.success(result.message || '进程重启请求已发送');
// 轮询探测后端是否恢复
const isReady = await waitForBackendReady(
30000, // 30秒超时
() => {
setIsRestarting(false);
toast.success('进程重启完成');
},
() => {
setIsRestarting(false);
toast.error('后端在 30 秒内未响应,请检查 NapCat 运行日志');
}
);
if (!isReady) {
setIsRestarting(false);
}
} catch (error) {
const msg = (error as Error).message;
toast.error(`进程重启失败: ${msg}`);
setIsRestarting(false);
}
};
useEffect(() => {
reset();
}, [quickLoginData]);
@@ -115,22 +82,6 @@ const LoginConfigCard = () => {
isSubmitting={isSubmitting || quickLoginLoading}
refresh={onRefresh}
/>
<div className='flex-shrink-0 w-full mt-6 pt-6 border-t border-divider'>
<div className='mb-3 text-sm text-default-600'></div>
<Button
color='warning'
variant='flat'
onPress={onRestartProcess}
isLoading={isRestarting}
isDisabled={isRestarting}
fullWidth
>
{isRestarting ? '正在重启进程...' : '重启进程'}
</Button>
<div className='mt-2 text-xs text-default-500'>
Worker 3
</div>
</div>
</>
);
};

View File

@@ -16,39 +16,14 @@ import type { QQItem } from '@/components/quick_login';
import { ThemeSwitch } from '@/components/theme-switch';
import QQManager from '@/controllers/qq_manager';
import useDialog from '@/hooks/use-dialog';
import PureLayout from '@/layouts/pure';
import { motion } from 'motion/react';
const parseLoginError = (errorStr: string) => {
if (errorStr.startsWith('登录失败: ')) {
const jsonPart = errorStr.substring('登录失败: '.length);
try {
const parsed = JSON.parse(jsonPart);
if (Array.isArray(parsed) && parsed[1]) {
const info = parsed[1];
const codeStr = info.serverErrorCode ? ` (错误码: ${info.serverErrorCode})` : '';
return `${info.message || errorStr}${codeStr}`;
}
} catch (e) {
// 忽略解析错误
}
}
return errorStr;
};
export default function QQLoginPage () {
const navigate = useNavigate();
const dialog = useDialog();
const [uinValue, setUinValue] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [qrcode, setQrcode] = useState<string>('');
const [loginError, setLoginError] = useState<string>('');
const lastErrorRef = useRef<string>('');
const [qqList, setQQList] = useState<(QQItem | LoginListItem)[]>([]);
const [refresh, setRefresh] = useState<boolean>(false);
const firstLoad = useRef<boolean>(true);
@@ -86,20 +61,6 @@ export default function QQLoginPage () {
navigate('/', { replace: true });
} else {
setQrcode(data.qrcodeurl);
if (data.loginError && data.loginError !== lastErrorRef.current) {
lastErrorRef.current = data.loginError;
setLoginError(data.loginError);
const friendlyMsg = parseLoginError(data.loginError);
dialog.alert({
title: '登录失败',
content: friendlyMsg,
confirmText: '确定',
});
} else if (!data.loginError) {
lastErrorRef.current = '';
setLoginError('');
}
}
} catch (error) {
const msg = (error as Error).message;
@@ -138,18 +99,6 @@ export default function QQLoginPage () {
setUinValue(e.target.value);
};
const onRefreshQRCode = async () => {
try {
lastErrorRef.current = '';
setLoginError('');
await QQManager.refreshQRCode();
toast.success('已发送刷新请求');
} catch (error) {
const msg = (error as Error).message;
toast.error(`刷新二维码失败: ${msg}`);
}
};
useEffect(() => {
const timer = setInterval(() => {
onUpdateQrCode();
@@ -210,11 +159,7 @@ export default function QQLoginPage () {
/>
</Tab>
<Tab key='qrcode' title='扫码登录'>
<QrCodeLogin
loginError={parseLoginError(loginError)}
qrcode={qrcode}
onRefresh={onRefreshQRCode}
/>
<QrCodeLogin qrcode={qrcode} />
</Tab>
</Tabs>
<Button

View File

@@ -1,35 +0,0 @@
import QQManager from '@/controllers/qq_manager';
/**
* 轮询等待后端进程恢复
* @param maxWaitTime 最大等待时间,单位毫秒
* @param onSuccess 成功回调
* @param onTimeout 超时回调
*/
export async function waitForBackendReady (
maxWaitTime: number = 15000,
onSuccess?: () => void,
onTimeout?: () => void
): Promise<boolean> {
const startTime = Date.now();
return new Promise<boolean>((resolve) => {
const timer = setInterval(async () => {
try {
// 尝试请求后端,设置一个较短的请求超时避免挂起
await QQManager.getQQLoginInfo({ timeout: 500 });
// 如果能走到这一步说明请求成功了
clearInterval(timer);
onSuccess?.();
resolve(true);
} catch (_e) {
// 如果请求失败(后端没起来),检查是否超时
if (Date.now() - startTime > maxWaitTime) {
clearInterval(timer);
onTimeout?.();
resolve(false);
}
}
}, 500); // 每 500ms 探测一次
});
}

View File

@@ -1,6 +1,6 @@
import react from '@vitejs/plugin-react';
import { defineConfig, loadEnv } from 'vite';
// import viteCompression from 'vite-plugin-compression';
import viteCompression from 'vite-plugin-compression';
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
import tsconfigPaths from 'vite-tsconfig-paths';
@@ -13,7 +13,7 @@ export default defineConfig(({ mode }) => {
plugins: [
react(),
tsconfigPaths(),
ViteImageOptimizer({}),
ViteImageOptimizer({})
],
base: '/webui/',
server: {

12
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
express:
specifier: ^5.0.0
version: 5.1.0
silk-wasm:
specifier: ^3.6.1
version: 3.7.1
ws:
specifier: ^8.18.3
version: 8.18.3
@@ -51,6 +54,9 @@ importers:
file-type:
specifier: ^21.0.0
version: 21.1.0
silk-wasm:
specifier: ^3.6.1
version: 3.7.1
devDependencies:
'@types/node':
specifier: ^22.0.1
@@ -5967,6 +5973,10 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
silk-wasm@3.7.1:
resolution: {integrity: sha512-mXPwLRtZxrYV3TZx41jMAeKc80wvmyrcXIcs8HctFxK15Ahz2OJQENYhNgEPeCEOdI6Mbx1NxQsqxzwc3DKerw==}
engines: {node: '>=16.11.0'}
simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
@@ -13281,6 +13291,8 @@ snapshots:
signal-exit@4.1.0: {}
silk-wasm@3.7.1: {}
simple-concat@1.0.1: {}
simple-get@4.0.1: