From 17322bb5a46b06bdc7cb493ef267944e08d9d460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Sat, 31 Jan 2026 15:55:36 +0800 Subject: [PATCH 1/5] Add mappings for 9.9.26-45627 and 6.9.88-44725 Add support for new client builds by updating external mappings. appid.json: add 9.9.26-45627 (appid 537340060) and 6.9.88-44725 (appid 537337594) with QUA strings. napi2native.json: add send/recv entries for 9.9.26-45627-x64, 6.9.88-44725-x64 and 6.9.88-44725-arm64. packet.json: add corresponding send/recv offsets for the same builds/architectures. These additions enable handling of the new versions in napcat-core. --- packages/napcat-core/external/appid.json | 8 ++++++++ packages/napcat-core/external/napi2native.json | 12 ++++++++++++ packages/napcat-core/external/packet.json | 12 ++++++++++++ 3 files changed, 32 insertions(+) diff --git a/packages/napcat-core/external/appid.json b/packages/napcat-core/external/appid.json index a0d31ba9..187ea51d 100644 --- a/packages/napcat-core/external/appid.json +++ b/packages/napcat-core/external/appid.json @@ -518,5 +518,13 @@ "9.9.26-44725": { "appid": 537337569, "qua": "V1_WIN_NQ_9.9.26_44725_GW_B" + }, + "9.9.26-45627": { + "appid": 537340060, + "qua": "V1_WIN_NQ_9.9.26_45627_GW_B" + }, + "6.9.88-44725": { + "appid": 537337594, + "qua": "V1_MAC_NQ_6.9.88_44725_GW_B" } } \ No newline at end of file diff --git a/packages/napcat-core/external/napi2native.json b/packages/napcat-core/external/napi2native.json index 6349796d..f2fa9ee1 100644 --- a/packages/napcat-core/external/napi2native.json +++ b/packages/napcat-core/external/napi2native.json @@ -154,5 +154,17 @@ "9.9.26-44725-x64": { "send": "0A18D0C", "recv": "1D4BF0D" + }, + "9.9.26-45627-x64": { + "send": "0A697CC", + "recv": "1E86AC1" + }, + "6.9.88-44725-x64": { + "send": "2756EF6", + "recv": "0A36152" + }, + "6.9.88-44725-arm64": { + "send": "2313C68", + "recv": "09693E4" } } \ No newline at end of file diff --git a/packages/napcat-core/external/packet.json b/packages/napcat-core/external/packet.json index a409e15b..5e41a440 100644 --- a/packages/napcat-core/external/packet.json +++ b/packages/napcat-core/external/packet.json @@ -662,5 +662,17 @@ "9.9.26-44725-x64": { "send": "2CEBB20", "recv": "2CEF0A0" + }, + "9.9.26-45627-x64": { + "send": "2E59CC0", + "recv": "2E5D240" + }, + "6.9.88-44725-x64": { + "send": "451FE90", + "recv": "4522A40" + }, + "6.9.88-44725-arm64": { + "send": "3D79168", + "recv": "3D7BA78" } } \ No newline at end of file From 9377dc3d52f2bb4d5ab3e1d35c3ef33094d25476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Sat, 31 Jan 2026 22:04:08 +0800 Subject: [PATCH 2/5] Update version keys to 9.9.27-45627 Rename release keys for build 45627 to 9.9.27-45627 across external metadata. Updated keys in packages/napcat-core/external/appid.json, napi2native.json, and packet.json (including x64 entries). No other payload values were modified. --- packages/napcat-core/external/appid.json | 2 +- packages/napcat-core/external/napi2native.json | 2 +- packages/napcat-core/external/packet.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/napcat-core/external/appid.json b/packages/napcat-core/external/appid.json index 187ea51d..43ce009b 100644 --- a/packages/napcat-core/external/appid.json +++ b/packages/napcat-core/external/appid.json @@ -519,7 +519,7 @@ "appid": 537337569, "qua": "V1_WIN_NQ_9.9.26_44725_GW_B" }, - "9.9.26-45627": { + "9.9.27-45627": { "appid": 537340060, "qua": "V1_WIN_NQ_9.9.26_45627_GW_B" }, diff --git a/packages/napcat-core/external/napi2native.json b/packages/napcat-core/external/napi2native.json index f2fa9ee1..4173600d 100644 --- a/packages/napcat-core/external/napi2native.json +++ b/packages/napcat-core/external/napi2native.json @@ -155,7 +155,7 @@ "send": "0A18D0C", "recv": "1D4BF0D" }, - "9.9.26-45627-x64": { + "9.9.27-45627-x64": { "send": "0A697CC", "recv": "1E86AC1" }, diff --git a/packages/napcat-core/external/packet.json b/packages/napcat-core/external/packet.json index 5e41a440..93ae01d3 100644 --- a/packages/napcat-core/external/packet.json +++ b/packages/napcat-core/external/packet.json @@ -663,7 +663,7 @@ "send": "2CEBB20", "recv": "2CEF0A0" }, - "9.9.26-45627-x64": { + "9.9.27-45627-x64": { "send": "2E59CC0", "recv": "2E5D240" }, From d33a872c427c54f63302e052517f2c72f4f0e3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B7=E6=9B=A6?= <2218872014@qq.com> Date: Sun, 1 Feb 2026 09:53:40 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E4=B8=8A=E4=BC=A0=E8=B5=84=E6=BA=90=E6=97=A5?= =?UTF-8?q?=E5=BF=97=20(#1573)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当上传资源有失败时为warn 全部成功则不输出日志 --- .../packet/context/operationContext.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/napcat-core/packet/context/operationContext.ts b/packages/napcat-core/packet/context/operationContext.ts index df5e250a..34a2bde0 100644 --- a/packages/napcat-core/packet/context/operationContext.ts +++ b/packages/napcat-core/packet/context/operationContext.ts @@ -95,12 +95,15 @@ export class PacketOperationContext { .filter(Boolean) ); const res = await Promise.allSettled(reqList); - this.context.logger.info(`上传资源${res.length}个,失败${res.filter((r) => r.status === 'rejected').length}个`); - res.forEach((result, index) => { - if (result.status === 'rejected') { - this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`); - } - }); + const failedCount = res.filter((r) => r.status === 'rejected').length; + if (failedCount > 0) { + this.context.logger.warn(`上传资源${res.length}个,失败${failedCount}个`); + res.forEach((result, index) => { + if (result.status === 'rejected') { + this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`); + } + }); + } } async UploadImage (img: PacketMsgPicElement) { From ebe3e9c63c4b32a25c6660b732b191180905b3b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A6=99=E8=8D=89=E5=91=B3=E7=9A=84=E7=BA=B3=E8=A5=BF?= =?UTF-8?q?=E5=A6=B2=E5=96=B5?= <151599587+VanillaNahida@users.noreply.github.com> Date: Sun, 1 Feb 2026 10:21:19 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat(webui):=20=E6=96=B0=E5=A2=9E=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=85=A8=E9=87=8F=E5=A4=87=E4=BB=BD=E4=B8=8E=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E5=8A=9F=E8=83=BD=E3=80=82=20(#1571)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(webui): 新增配置全量备份与恢复功能。 * chore: Remove dependencies "archiver" * feat(webui): 增加上传文件大小限制配置并优化上传处理 * Use memory-based zip import/export and multer Replace disk-based zip handling with in-memory streaming to avoid temp files: remove unzipper/@types(unzipper) deps from package.json; update BackupConfig to stream-export configs with compressing.zip.Stream and to import by extracting uploaded zip buffer via compressing.zip.UncompressStream into in-memory Buffers. Backup of existing config is kept in-memory instead of copying to tmp, and imported files are written with path normalization checks. Router changed to use multer.memoryStorage() for uploads (remove dynamic tmp/disk upload logic and uploadSizeLimit usage). Also remove uploadSizeLimit from config schema. * Revert "chore: Remove dependencies "archiver"" This reverts commit 890736d3c72a154b4502f8399dba8c7fb5280574. * Regenerate pnpm-lock.yaml (prune entries) Regenerated pnpm-lock.yaml to reflect the current dependency resolution. This update prunes many removed/unused lock entries (notably archiver, unzipper and related @types, older/deprecated packages such as rimraf v2/fstream/bluebird, etc.) and removes platform 'libc' metadata from several platform-specific packages. There are no package.json changes; run `pnpm install` to sync your local node_modules with the updated lockfile. --------- Co-authored-by: 手瓜一十雪 --- .../src/api/BackupConfig.ts | 151 ++++++++++++++++++ .../src/router/OB11Config.ts | 13 ++ .../src/pages/dashboard/config/backup.tsx | 129 +++++++++++++++ .../src/pages/dashboard/config/index.tsx | 6 + 4 files changed, 299 insertions(+) create mode 100644 packages/napcat-webui-backend/src/api/BackupConfig.ts create mode 100644 packages/napcat-webui-frontend/src/pages/dashboard/config/backup.tsx diff --git a/packages/napcat-webui-backend/src/api/BackupConfig.ts b/packages/napcat-webui-backend/src/api/BackupConfig.ts new file mode 100644 index 00000000..075adb18 --- /dev/null +++ b/packages/napcat-webui-backend/src/api/BackupConfig.ts @@ -0,0 +1,151 @@ +import { RequestHandler } from 'express'; +import { existsSync, readdirSync, writeFileSync, readFileSync } from 'node:fs'; +import { join, normalize } from 'node:path'; +import { webUiPathWrapper } from '@/napcat-webui-backend/index'; +import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data'; +import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; +import compressing from 'compressing'; +import { Readable } from 'node:stream'; + +// 使用compressing库进行流式压缩导出 +export const BackupExportConfigHandler: RequestHandler = async (_req, res) => { + const isLogin = WebUiDataRuntime.getQQLoginStatus(); + if (!isLogin) { + return sendError(res, 'Not Login'); + } + + try { + const configPath = webUiPathWrapper.configPath; + + if (!existsSync(configPath)) { + return sendError(res, '配置目录不存在'); + } + + const formatDate = (date: Date) => { + return date.toISOString().replace(/[:.]/g, '-'); + }; + const zipFileName = `config_backup_${formatDate(new Date())}.zip`; + + // 设置响应头 + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Disposition', `attachment; filename="${zipFileName}"`); + + // 使用compressing的Stream API进行流式压缩 + const stream = new compressing.zip.Stream(); + + // 添加目录下的所有文件到压缩流(单层平坦结构) + const entries = readdirSync(configPath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isFile()) { + const entryPath = join(configPath, entry.name); + stream.addEntry(entryPath, { relativePath: entry.name }); + } + } + + // 管道传输到响应 + stream.pipe(res); + + // 处理流错误 + stream.on('error', (err) => { + console.error('压缩流错误:', err); + if (!res.headersSent) { + sendError(res, '流式压缩失败'); + } + }); + + } catch (error) { + const msg = (error as Error).message; + console.error('导出配置失败:', error); + if (!res.headersSent) { + return sendError(res, `导出配置失败: ${msg}`); + } + } +}; + +// 从内存Buffer流式解压,返回文件名和内容的映射 +async function extractZipToMemory (buffer: Buffer): Promise> { + return new Promise((resolve, reject) => { + const files = new Map(); + const bufferStream = Readable.from(buffer); + const uncompressStream = new compressing.zip.UncompressStream(); + + uncompressStream.on('entry', (header, stream, next) => { + // 只处理文件,忽略目录 + if (header.type === 'file') { + const chunks: Buffer[] = []; + stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); + stream.on('end', () => { + // 取文件名(忽略路径中的目录部分) + const fileName = header.name.split('/').pop() || header.name; + files.set(fileName, Buffer.concat(chunks)); + next(); + }); + stream.on('error', (err) => { + console.error(`读取文件失败: ${header.name}`, err); + next(); + }); + } else { + stream.resume(); + next(); + } + }); + + uncompressStream.on('finish', () => resolve(files)); + uncompressStream.on('error', reject); + + bufferStream.pipe(uncompressStream); + }); +} + +// 导入配置 - 流式处理,完全在内存中解压 +export const BackupImportConfigHandler: RequestHandler = async (req, res) => { + // 检查是否有文件上传(multer memoryStorage 模式下文件在 req.file.buffer) + if (!req.file || !req.file.buffer) { + return sendError(res, '请选择要导入的配置文件'); + } + + try { + const configPath = webUiPathWrapper.configPath; + + // 从内存中解压zip + const extractedFiles = await extractZipToMemory(req.file.buffer); + + if (extractedFiles.size === 0) { + return sendError(res, '配置文件为空或格式不正确'); + } + + // 备份当前配置到内存 + const backupFiles = new Map(); + if (existsSync(configPath)) { + const currentFiles = readdirSync(configPath, { withFileTypes: true }); + for (const entry of currentFiles) { + if (entry.isFile()) { + const filePath = join(configPath, entry.name); + backupFiles.set(entry.name, readFileSync(filePath)); + } + } + } + + // 写入新的配置文件 + for (const [fileName, content] of extractedFiles) { + // 防止路径穿越攻击 + const destPath = join(configPath, fileName); + const normalizedPath = normalize(destPath); + if (!normalizedPath.startsWith(normalize(configPath))) { + continue; + } + writeFileSync(destPath, content); + } + + return sendSuccess(res, { + message: '配置导入成功,重启后生效~', + filesImported: extractedFiles.size, + filesBackedUp: backupFiles.size + }); + + } catch (error) { + console.error('导入配置失败:', error); + const msg = (error as Error).message; + return sendError(res, `导入配置失败: ${msg}`); + } +}; diff --git a/packages/napcat-webui-backend/src/router/OB11Config.ts b/packages/napcat-webui-backend/src/router/OB11Config.ts index 99f96681..d4dab3f9 100644 --- a/packages/napcat-webui-backend/src/router/OB11Config.ts +++ b/packages/napcat-webui-backend/src/router/OB11Config.ts @@ -1,11 +1,24 @@ import { Router } from 'express'; +import multer from 'multer'; import { OB11GetConfigHandler, OB11SetConfigHandler } from '@/napcat-webui-backend/src/api/OB11Config'; +import { BackupExportConfigHandler, BackupImportConfigHandler } from '@/napcat-webui-backend/src/api/BackupConfig'; const router: Router = Router(); + +// 使用内存存储,配合流式处理 +const upload = multer({ + storage: multer.memoryStorage() +}); + // router:读取配置 router.post('/GetConfig', OB11GetConfigHandler); // router:写入配置 router.post('/SetConfig', OB11SetConfigHandler); +// router:导出配置 +router.get('/ExportConfig', BackupExportConfigHandler); +// router:导入配置 +router.post('/ImportConfig', upload.single('configFile'), BackupImportConfigHandler); export { router as OB11ConfigRouter }; + diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/config/backup.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/config/backup.tsx new file mode 100644 index 00000000..50cd417f --- /dev/null +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/backup.tsx @@ -0,0 +1,129 @@ +import { Button } from '@heroui/button'; +import toast from 'react-hot-toast'; +import { LuDownload, LuUpload } from 'react-icons/lu'; +import { requestServerWithFetch } from '@/utils/request'; + +// 导入配置 +const handleImportConfig = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + // 检查文件类型 + if (!file.name.endsWith('.zip')) { + toast.error('请选择zip格式的配置文件'); + return; + } + + try { + const formData = new FormData(); + formData.append('configFile', file); + + const response = await requestServerWithFetch('/OB11Config/ImportConfig', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || '导入配置失败'); + } + + const result = await response.json(); + // 检查是否成功导入 + if (result.code === 0) { + toast.success(result.data?.message || '配置导入成功。'); + } else { + toast.error(`配置导入失败: ${result.data?.message || '未知错误'}`); + } + + } catch (error) { + const msg = (error as Error).message; + toast.error(`导入配置失败: ${msg}`); + } finally { + // 重置文件输入 + event.target.value = ''; + } +}; + +// 导出配置 +const handleExportConfig = async () => { + try { + const response = await requestServerWithFetch('/OB11Config/ExportConfig', { + method: 'GET', + }); + + if (!response.ok) { + throw new Error('导出配置失败'); + } + + // 创建下载链接 + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + const fileName = response.headers.get('Content-Disposition')?.split('=')[1]?.replace(/"/g, '') || 'config_backup.zip'; + a.download = fileName; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + toast.success('配置导出成功'); + } catch (error) { + const msg = (error as Error).message; + + toast.error(`导出配置失败: ${msg}`); + } +}; + +const BackupConfigCard: React.FC = () => { + return ( +
+
+

备份与恢复

+

+ 您可以通过导入/导出配置文件来备份和恢复NapCat的所有设置 +

+ +
+ + +
+ +
+
+

+ 导入配置会覆盖当前所有设置,请谨慎操作。导入前建议先导出当前配置作为备份。 +

+
+
+
+
+ ); +}; + +export default BackupConfigCard; \ No newline at end of file diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/config/index.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/config/index.tsx index 711f58d8..1274b879 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/config/index.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/index.tsx @@ -13,6 +13,7 @@ import ServerConfigCard from './server'; import SSLConfigCard from './ssl'; import ThemeConfigCard from './theme'; import WebUIConfigCard from './webui'; +import BackupConfigCard from './backup'; export interface ConfigPageProps { children?: React.ReactNode; @@ -108,6 +109,11 @@ export default function ConfigPage () { + + + + + ); From 7c65b1eaf147ca59fcff1c44a4b943d8c1614289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Sun, 1 Feb 2026 10:22:15 +0800 Subject: [PATCH 5/5] =?UTF-8?q?Revert=20"=E5=A2=9E=E5=8A=A0=E4=B8=AA?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E9=85=8D=E7=BD=AE=E5=AF=BC=E5=87=BA=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=20(#1567)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit c0bcced5fb981343f8f83efc3dfb36170e745021. --- .../src/pages/dashboard/network.tsx | 59 +------------------ 1 file changed, 1 insertion(+), 58 deletions(-) diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/network.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/network.tsx index dc25a78c..f6d93253 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/network.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/network.tsx @@ -2,10 +2,9 @@ import { Button } from '@heroui/button'; import { useDisclosure } from '@heroui/modal'; import { Tab, Tabs } from '@heroui/tabs'; import clsx from 'clsx'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import { IoMdRefresh } from 'react-icons/io'; -import { FiDownload, FiUpload } from 'react-icons/fi'; import AddButton from '@/components/button/add_button'; import HTTPClientDisplayCard from '@/components/display_card/http_client'; @@ -56,9 +55,7 @@ export default function NetworkPage () { deleteNetworkConfig, enableNetworkConfig, enableDebugNetworkConfig, - updateSingleConfig, } = useConfig(); - const fileInputRef = useRef(null); const [activeField, setActiveField] = useState('httpServers'); const [activeName, setActiveName] = useState(''); @@ -102,45 +99,6 @@ export default function NetworkPage () { onOpen(); }; - // 导出网络配置 - const handleExport = () => { - const blob = new Blob([JSON.stringify(config.network, null, 2)], { type: 'application/json' }); - const link = document.createElement('a'); - link.href = URL.createObjectURL(blob); - link.download = `network-config-${Date.now()}.json`; - link.click(); - URL.revokeObjectURL(link.href); - toast.success('导出成功'); - }; - - // 导入网络配置 - const handleFileChange = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - e.target.value = ''; - - try { - const data = JSON.parse(await file.text()) as OneBotConfig['network']; - const keys: (keyof OneBotConfig['network'])[] = ['httpServers', 'httpClients', 'httpSseServers', 'websocketServers', 'websocketClients']; - if (keys.some(k => !Array.isArray(data[k]))) throw new Error('配置格式错误'); - - dialog.confirm({ - title: '导入配置', - content: '确定导入?这将覆盖现有网络配置。', - onConfirm: async () => { - try { - await updateSingleConfig('network', data); - toast.success('导入成功'); - } catch (err) { - toast.error(`导入失败: ${(err as Error).message}`); - } - }, - }); - } catch (err) { - toast.error(`解析失败: ${(err as Error).message}`); - } - }; - const onDelete = async ( field: keyof OneBotConfig['network'], name: string @@ -415,21 +373,6 @@ export default function NetworkPage () {
- - -