Compare commits

...

9 Commits

Author SHA1 Message Date
手瓜一十雪
fa12865924 fix: error 2025-02-06 17:10:30 +08:00
Mlikiowa
ecdd717742 release: v4.5.12 2025-02-06 08:23:07 +00:00
bietiaop
6851334af9 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-06 15:29:04 +08:00
bietiaop
9051b29565 feat: 字体修改#771 2025-02-06 15:28:42 +08:00
手瓜一十雪
95c7d3dfbd fix: remove __dirname 2025-02-06 15:28:24 +08:00
手瓜一十雪
bc1148c00a fix: require_dlopen 2025-02-06 15:25:47 +08:00
Mlikiowa
d4556d9299 release: v4.5.11 2025-02-06 03:13:17 +00:00
pk5ls20
5d389a2359 fix: fake forwardMsg construct 2025-02-06 01:09:23 +08:00
Mlikiowa
305116874b release: v4.5.10 2025-02-05 11:49:14 +00:00
18 changed files with 454 additions and 109 deletions

View File

@@ -4,7 +4,7 @@
"name": "NapCatQQ",
"slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现",
"version": "4.5.9",
"version": "4.5.12",
"icon": "./logo.png",
"authors": [
{

View File

@@ -0,0 +1,69 @@
import { Button } from '@heroui/button'
import { Input } from '@heroui/input'
import { useRef, useState } from 'react'
export interface FileInputProps {
onChange: (file: File) => Promise<void> | void
onDelete?: () => Promise<void> | void
label?: string
accept?: string
}
const FileInput: React.FC<FileInputProps> = ({
onChange,
onDelete,
label,
accept
}) => {
const inputRef = useRef<HTMLInputElement>(null)
const [isLoading, setIsLoading] = useState(false)
return (
<div className="flex items-end gap-2">
<div className="flex-grow">
<Input
isDisabled={isLoading}
ref={inputRef}
label={label}
type="file"
placeholder="选择文件"
accept={accept}
onChange={async (e) => {
try {
setIsLoading(true)
const file = e.target.files?.[0]
if (file) {
await onChange(file)
}
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
if (inputRef.current) inputRef.current.value = ''
}
}}
/>
</div>
<Button
isDisabled={isLoading}
onPress={async () => {
try {
setIsLoading(true)
if (onDelete) await onDelete()
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
if (inputRef.current) inputRef.current.value = ''
}
}}
color="primary"
variant="flat"
size="sm"
>
</Button>
</div>
)
}
export default FileInput

View File

@@ -196,4 +196,26 @@ export default class FileManager {
)
return data.data
}
public static async uploadWebUIFont(file: File) {
const formData = new FormData()
formData.append('file', file)
const { data } = await serverRequest.post<ServerResponse<boolean>>(
'/File/font/upload/webui',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
)
return data.data
}
public static async deleteWebUIFont() {
const { data } = await serverRequest.post<ServerResponse<boolean>>(
'/File/font/delete/webui'
)
return data.data
}
}

View File

@@ -7,11 +7,13 @@ import toast from 'react-hot-toast'
import key from '@/const/key'
import SaveButtons from '@/components/button/save_buttons'
import FileInput from '@/components/input/file_input'
import ImageInput from '@/components/input/image_input'
import useMusic from '@/hooks/use-music'
import { siteConfig } from '@/config/site'
import FileManager from '@/controllers/file_manager'
const WebUIConfigCard = () => {
const {
@@ -59,17 +61,47 @@ const WebUIConfigCard = () => {
return (
<>
<title>WebUI配置 - NapCat WebUI</title>
<Controller
control={control}
name="musicListID"
render={({ field }) => (
<Input
{...field}
label="网易云音乐歌单ID网页内音乐播放器"
placeholder="请输入歌单ID"
<div className="flex flex-col gap-2">
<div className="flex-shrink-0 w-full">WebUI字体</div>
<div className="text-sm text-default-400">
<FileInput
label="中文字体"
onChange={async (file) => {
try {
await FileManager.uploadWebUIFont(file)
toast.success('上传成功')
setTimeout(() => {
window.location.reload()
}, 1000)
} catch (error) {
toast.error('上传失败: ' + (error as Error).message)
}
}}
onDelete={async () => {
try {
await FileManager.deleteWebUIFont()
} catch (error) {
toast.error('删除失败: ' + (error as Error).message)
}
}}
/>
)}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="flex-shrink-0 w-full">WebUI音乐播放器</div>
<Controller
control={control}
name="musicListID"
render={({ field }) => (
<Input
{...field}
label="网易云音乐歌单ID网页内音乐播放器"
placeholder="请输入歌单ID"
/>
)}
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex-shrink-0 w-full"></div>
<Controller

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.5.9",
"version": "4.5.12",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",

View File

@@ -1 +1 @@
export const napCatVersion = '4.5.9';
export const napCatVersion = '4.5.12';

View File

@@ -13,12 +13,12 @@ class UploadForwardMsg extends PacketTransformer<typeof proto.SendLongMsgResp> {
const msgBody = this.msgBuilder.buildFakeMsg(selfUid, msg);
const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode(
{
action: {
action: [{
actionCommand: 'MultiMsg',
actionData: {
msgBody: msgBody
}
}
}]
}
);
const payload = zlib.gzipSync(Buffer.from(longMsgResultData));

View File

@@ -26,9 +26,4 @@ export function require_dlopen(modulename: string) {
process.dlopen(module, path.join(import__dirname, modulename));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return module.exports as any;
}
/**
* Expose the native API when not Windows, note that this is not public API and
* could be removed at any time.
*/
export const native = (process.platform !== 'win32' ? require_dlopen('./pty/' + process.platform + '.' + process.arch + '/pty.node') : null);
}

View File

@@ -13,12 +13,13 @@ import { IProcessEnv, IPtyForkOptions, IPtyOpenOptions } from '@homebridge/node-
import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/types';
import { assign } from '@homebridge/node-pty-prebuilt-multiarch/src/utils';
import { pty_loader } from './prebuild-loader';
import { fileURLToPath } from 'url';
export const pty = pty_loader();
let helperPath: string;
helperPath = '../build/Release/spawn-helper';
helperPath = path.resolve(__dirname, helperPath);
const import__dirname = path.dirname(fileURLToPath(import.meta.url));
helperPath = path.resolve(import__dirname, helperPath);
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');

View File

@@ -14,6 +14,8 @@ import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/t
import { fork } from 'child_process';
import { ConoutConnection } from './windowsConoutConnection';
import { require_dlopen } from '.';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
let conptyNative: IConptyNative;
let winptyNative: IWinptyNative;
@@ -149,7 +151,7 @@ export class WindowsPtyAgent {
consoleProcessList.forEach((pid: number) => {
try {
process.kill(pid);
} catch{
} catch {
// Ignore if process cannot be found (kill ESRCH error)
}
});
@@ -176,8 +178,9 @@ export class WindowsPtyAgent {
}
private _getConsoleProcessList(): Promise<number[]> {
const import__dirname = dirname(fileURLToPath(import.meta.url));
return new Promise<number[]>(resolve => {
const agent = fork(path.join(__dirname, 'conpty_console_list_agent'), [this._innerPid.toString()]);
const agent = fork(path.join(import__dirname, 'conpty_console_list_agent'), [this._innerPid.toString()]);
agent.on('message', message => {
clearTimeout(timeout);
// @ts-expect-error no need to check if it is null

View File

@@ -223,7 +223,7 @@ async function handleLogin(
logger.log(`可用于快速登录的 QQ\n${historyLoginList
.map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`)
.join('\n')
}`);
}`);
}
loginService.getQRCodePicture();
}
@@ -314,7 +314,15 @@ export async function NCoreInitShell() {
await initializeSession(session, sessionConfig);
const accountDataPath = path.resolve(dataPath, './NapCat/data');
fs.mkdirSync(dataPath, { recursive: true });
//判断dataPath是否为根目录 或者 D:/ 之类的盘目录
if (dataPath !== '/' && /^[a-zA-Z]:\\$/.test(dataPath) === false) {
try {
fs.mkdirSync(accountDataPath, { recursive: true });
} catch (error) {
logger.logError('创建accountDataPath失败', error);
}
}
logger.logDebug('本账号数据/缓存目录:', accountDataPath);
await new NapCatShell(

View File

@@ -10,9 +10,10 @@ import { WebUiConfigWrapper } from '@webapi/helper/config';
import { ALLRouter } from '@webapi/router';
import { cors } from '@webapi/middleware/cors';
import { createUrl } from '@webapi/utils/url';
import { sendSuccess } from '@webapi/utils/response';
import { sendError, sendSuccess } from '@webapi/utils/response';
import { join } from 'node:path';
import { terminalManager } from '@webapi/terminal/terminal_manager';
import multer from 'multer'; // 新增引入multer用于错误捕获
// 实例化Express
const app = express();
@@ -42,10 +43,22 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// CORS中间件
// TODO:
app.use(cors);
// 如果是webui字体文件挂载字体文件
app.use('/webui/fonts/AaCute.woff', async (_req, res, next) => {
const isFontExist = await WebUiConfigWrapper.CheckWebUIFontExist();
console.log(isFontExist, 'isFontExist');
if (isFontExist) {
res.sendFile(WebUiConfigWrapper.GetWebUIFontPath());
} else {
next();
}
});
// ------------中间件结束------------
// ------------挂载路由------------
// 挂载静态路由(前端),路径为 [/前缀]/webui
// 挂载静态路由(前端),路径为 /webui
app.use('/webui', express.static(pathWrapper.staticPath));
// 初始化WebSocket服务器
server.on('upgrade', (request, socket, head) => {
@@ -64,7 +77,19 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
app.all('/', (_req, res) => {
sendSuccess(res, null, 'NapCat WebAPI is now running!');
});
// ------------路由挂载结束------------
// 错误处理中间件捕获multer的错误
app.use((err: Error, _: express.Request, res: express.Response, next: express.NextFunction) => {
if (err instanceof multer.MulterError) {
return sendError(res, err.message, true);
}
next(err);
});
// 全局错误处理中间件非multer错误
app.use((_: Error, __: express.Request, res: express.Response, ___: express.NextFunction) => {
sendError(res, 'An unknown error occurred.', true);
});
// ------------启动服务------------
server.listen(config.port, config.host, async () => {

View File

@@ -1,4 +1,4 @@
import type { RequestHandler, Request } from 'express';
import type { RequestHandler } from 'express';
import { sendError, sendSuccess } from '../utils/response';
import fsProm from 'fs/promises';
import fs from 'fs';
@@ -7,7 +7,9 @@ import os from 'os';
import compressing from 'compressing';
import { PassThrough } from 'stream';
import multer from 'multer';
import { randomUUID } from 'crypto';
import { WebUiConfigWrapper } from '../helper/config';
import webUIFontUploader from '../uploader/webui_font';
import diskUploader from '../uploader/disk';
const isWindows = os.platform() === 'win32';
@@ -268,11 +270,11 @@ export const BatchMoveHandler: RequestHandler = async (req, res) => {
// 新增:文件下载处理方法(注意流式传输,不将整个文件读入内存)
export const DownloadHandler: RequestHandler = async (req, res) => {
try {
const filePath = normalizePath( req.query[ 'path' ] as string );
const filePath = normalizePath(req.query['path'] as string);
if (!filePath) {
return sendError( res, '参数错误' );
return sendError(res, '参数错误');
}
const stat = await fsProm.stat(filePath);
res.setHeader('Content-Type', 'application/octet-stream');
@@ -327,74 +329,71 @@ export const BatchDownloadHandler: RequestHandler = async (req, res) => {
}
};
// 修改:使用 Buffer 转码文件名,解决文件上传时乱码问题
const decodeFileName = (fileName: string): string => {
// 修改上传处理方法
export const UploadHandler: RequestHandler = async (req, res) => {
try {
return Buffer.from(fileName, 'binary').toString('utf8');
} catch {
return fileName;
await diskUploader(req, res);
return sendSuccess(res, true, '文件上传成功', true);
} catch (error) {
let errorMessage = '文件上传失败';
if (error instanceof multer.MulterError) {
switch (error.code) {
case 'LIMIT_FILE_SIZE':
errorMessage = '文件大小超过限制40MB';
break;
case 'LIMIT_UNEXPECTED_FILE':
errorMessage = '无效的文件上传字段';
break;
default:
errorMessage = `上传错误: ${error.message}`;
}
} else if (error instanceof Error) {
errorMessage = error.message;
}
return sendError(res, errorMessage, true);
}
};
// 修改上传处理方法
export const UploadHandler: RequestHandler = (req, res) => {
const uploadPath = (req.query['path'] || '') as string;
// 上传WebUI字体文件处理方法
export const UploadWebUIFontHandler: RequestHandler = async (req, res) => {
try {
await webUIFontUploader(req, res);
return sendSuccess(res, true, '字体文件上传成功', true);
} catch (error) {
let errorMessage = '字体文件上传失败';
const storage = multer.diskStorage({
destination: (
_: Request,
file: Express.Multer.File,
cb: (error: Error | null, destination: string) => void
) => {
try {
const decodedName = decodeFileName(file.originalname);
if (!uploadPath) {
return cb(new Error('上传路径不能为空'), '');
}
if (isWindows && uploadPath === '\\') {
return cb(new Error('根目录不允许上传文件'), '');
}
// 处理文件夹上传的情况
if (decodedName.includes('/') || decodedName.includes('\\')) {
const fullPath = path.join(uploadPath, path.dirname(decodedName));
fs.mkdirSync(fullPath, { recursive: true });
cb(null, fullPath);
} else {
cb(null, uploadPath);
}
} catch (error) {
cb(error as Error, '');
if (error instanceof multer.MulterError) {
switch (error.code) {
case 'LIMIT_FILE_SIZE':
errorMessage = '字体文件大小超过限制40MB';
break;
case 'LIMIT_UNEXPECTED_FILE':
errorMessage = '无效的文件上传字段';
break;
default:
errorMessage = `上传错误: ${error.message}`;
}
},
filename: (_: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => {
try {
const decodedName = decodeFileName(file.originalname);
const fileName = path.basename(decodedName);
// 检查文件是否存在
const fullPath = path.join(uploadPath, decodedName);
if (fs.existsSync(fullPath)) {
const ext = path.extname(fileName);
const name = path.basename(fileName, ext);
cb(null, `${name}-${randomUUID()}${ext}`);
} else {
cb(null, fileName);
}
} catch (error) {
cb(error as Error, '');
}
},
});
const upload = multer({ storage }).array('files');
upload(req, res, (err: any) => {
if (err) {
return sendError(res, err.message || '文件上传失败');
} else if (error instanceof Error) {
errorMessage = error.message;
}
return sendSuccess(res, true);
});
return sendError(res, errorMessage, true);
}
};
// 删除WebUI字体文件处理方法
export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => {
try {
const fontPath = WebUiConfigWrapper.GetWebUIFontPath();
const exists = await WebUiConfigWrapper.CheckWebUIFontExist();
if (!exists) {
return sendSuccess(res, true);
}
await fsProm.unlink(fontPath);
return sendSuccess(res, true);
} catch (error) {
return sendError(res, '删除字体文件失败');
}
};

View File

@@ -203,4 +203,32 @@ export class WebUiConfigWrapper {
}
return '';
}
// 获取字体文件夹内的字体列表
public static async GetFontList(): Promise<string[]> {
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
if (
await fs
.access(fontsPath, constants.F_OK)
.then(() => true)
.catch(() => false)
) {
return (await fs.readdir(fontsPath)).filter((file) => file.endsWith('.ttf'));
}
return [];
}
// 判断字体是否存在webui.woff
public static async CheckWebUIFontExist(): Promise<boolean> {
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
return await fs
.access(resolve(fontsPath, './webui.woff'), constants.F_OK)
.then(() => true)
.catch(() => false);
}
// 获取webui字体文件路径
public static GetWebUIFontPath(): string {
return resolve(webUiPathWrapper.configPath, './fonts/webui.woff');
}
}

View File

@@ -13,7 +13,9 @@ import {
BatchMoveHandler,
DownloadHandler,
BatchDownloadHandler, // 新增下载处理方法
UploadHandler, // 添加上传处理器
UploadHandler,
UploadWebUIFontHandler,
DeleteWebUIFontHandler, // 添加上传处理器
} from '../api/File';
const router = Router();
@@ -37,5 +39,8 @@ router.post('/move', MoveHandler);
router.post('/batchMove', BatchMoveHandler);
router.post('/download', DownloadHandler);
router.post('/batchDownload', BatchDownloadHandler);
router.post('/upload', UploadHandler); // 添加上传处理路由
router.post('/upload', UploadHandler);
router.post('/font/upload/webui', UploadWebUIFontHandler);
router.post('/font/delete/webui', DeleteWebUIFontHandler);
export { router as FileRouter };

View File

@@ -0,0 +1,85 @@
import multer from 'multer';
import { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import { randomUUID } from 'crypto';
const isWindows = process.platform === 'win32';
// 修改:使用 Buffer 转码文件名,解决文件上传时乱码问题
const decodeFileName = (fileName: string): string => {
try {
return Buffer.from(fileName, 'binary').toString('utf8');
} catch {
return fileName;
}
};
export const createDiskStorage = (uploadPath: string) => {
return multer.diskStorage({
destination: (
_: Request,
file: Express.Multer.File,
cb: (error: Error | null, destination: string) => void
) => {
try {
const decodedName = decodeFileName(file.originalname);
if (!uploadPath) {
return cb(new Error('上传路径不能为空'), '');
}
if (isWindows && uploadPath === '\\') {
return cb(new Error('根目录不允许上传文件'), '');
}
// 处理文件夹上传的情况
if (decodedName.includes('/') || decodedName.includes('\\')) {
const fullPath = path.join(uploadPath, path.dirname(decodedName));
fs.mkdirSync(fullPath, { recursive: true });
cb(null, fullPath);
} else {
cb(null, uploadPath);
}
} catch (error) {
cb(error as Error, '');
}
},
filename: (_: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => {
try {
const decodedName = decodeFileName(file.originalname);
const fileName = path.basename(decodedName);
// 检查文件是否存在
const fullPath = path.join(uploadPath, decodedName);
if (fs.existsSync(fullPath)) {
const ext = path.extname(fileName);
const name = path.basename(fileName, ext);
cb(null, `${name}-${randomUUID()}${ext}`);
} else {
cb(null, fileName);
}
} catch (error) {
cb(error as Error, '');
}
},
});
};
export const createDiskUpload = (uploadPath: string) => {
const upload = multer({ storage: createDiskStorage(uploadPath) }).array('files');
return upload;
};
const diskUploader = (req: Request, res: Response) => {
const uploadPath = (req.query['path'] || '') as string;
return new Promise((resolve, reject) => {
createDiskUpload(uploadPath)(req, res, (error) => {
if (error) {
// 错误处理
return reject(error);
}
return resolve(true);
});
});
};
export default diskUploader;

View File

@@ -0,0 +1,52 @@
import multer from 'multer';
import { WebUiConfigWrapper } from '../helper/config';
import path from 'path';
import fs from 'fs';
import type { Request, Response } from 'express';
export const webUIFontStorage = multer.diskStorage({
destination: (_, __, cb) => {
try {
const fontsPath = path.dirname(WebUiConfigWrapper.GetWebUIFontPath());
// 确保字体目录存在
fs.mkdirSync(fontsPath, { recursive: true });
cb(null, fontsPath);
} catch (error) {
// 确保错误信息被正确传递
cb(new Error(`创建字体目录失败:${(error as Error).message}`), '');
}
},
filename: (_, __, cb) => {
// 统一保存为webui.woff
cb(null, 'webui.woff');
},
});
export const webUIFontUpload = multer({
storage: webUIFontStorage,
fileFilter: (_, file, cb) => {
// 再次验证文件类型
if (!file.originalname.toLowerCase().endsWith('.woff')) {
cb(new Error('只支持WOFF格式的字体文件'));
return;
}
cb(null, true);
},
limits: {
fileSize: 40 * 1024 * 1024, // 限制40MB
},
}).single('file');
const webUIFontUploader = (req: Request, res: Response) => {
return new Promise((resolve, reject) => {
webUIFontUpload(req, res, (error) => {
if (error) {
// 错误处理
// sendError(res, error.message, true);
return reject(error);
}
return resolve(true);
});
});
};
export default webUIFontUploader;

View File

@@ -2,25 +2,46 @@ import type { Response } from 'express';
import { ResponseCode, HttpStatusCode } from '@webapi/const/status';
export const sendResponse = <T>(res: Response, data?: T, code: ResponseCode = 0, message = 'success') => {
res.status(HttpStatusCode.OK).json({
export const sendResponse = <T>(
res: Response,
data?: T,
code: ResponseCode = 0,
message = 'success',
useSend: boolean = false
) => {
const result = {
code,
message,
data,
});
};
if (useSend) {
res.status(HttpStatusCode.OK).send(JSON.stringify(result));
return;
}
res.status(HttpStatusCode.OK).json(result);
};
export const sendError = (res: Response, message = 'error') => {
res.status(HttpStatusCode.OK).json({
export const sendError = (res: Response, message = 'error', useSend: boolean = false) => {
const result = {
code: ResponseCode.Error,
message,
});
};
if (useSend) {
res.status(HttpStatusCode.OK).send(JSON.stringify(result));
return;
}
res.status(HttpStatusCode.OK).json(result);
};
export const sendSuccess = <T>(res: Response, data?: T, message = 'success') => {
res.status(HttpStatusCode.OK).json({
export const sendSuccess = <T>(res: Response, data?: T, message = 'success', useSend: boolean = false) => {
const result = {
code: ResponseCode.Success,
data,
message,
});
};
if (useSend) {
res.status(HttpStatusCode.OK).send(JSON.stringify(result));
return;
}
res.status(HttpStatusCode.OK).json(result);
};