Compare commits

..

9 Commits

Author SHA1 Message Date
手瓜一十雪
17322bb5a4 Add mappings for 9.9.26-45627 and 6.9.88-44725
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
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.
2026-01-31 15:55:36 +08:00
冷曦
c0bcced5fb 增加个网络配置导出导入 (#1567)
* 增加个网络配置导出导入

重装容器时可以直接导出导入

* Remove unused import for useRef in network.tsx
2026-01-31 15:28:18 +08:00
手瓜一十雪
805c1d5ea2 Default plugins disabled; skip loading disabled
Change plugin loader to treat plugins as disabled by default (unless the id/dir is 'napcat-plugin-builtin') by using nullish coalescing when reading statusConfig. Add an early-return guard in the plugin manager/adapter that logs and skips loading when entry.enable is false. This prevents disabled plugins from being loaded automatically and provides a clear log message when skipped.
2026-01-31 15:26:56 +08:00
手瓜一十雪
b3399b07ad Silence update log; change update UI colors
Comment out the noisy '[NapCat Update] No pending updates found' log in UpdateNapCat.ts. Update frontend color choices: switch the plugin store action color from 'success' to 'default', and change the NewVersion chip and spinner from 'danger' to 'primary' in system_info.tsx. These tweaks reduce alarming red styling and quiet an unnecessary backend log.
2026-01-31 15:15:01 +08:00
手瓜一十雪
71f8504849 Refactor extension page layout and tab handling
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Improves the layout of the extension page by adjusting container heights and restructuring the header to better support responsive design. Moves the tab navigation to the header and displays only the selected extension page in the main content area, simplifying the rendering logic and improving user experience.
2026-01-30 19:41:27 +08:00
手瓜一十雪
3b7ca1a08f Remove flex-wrap from tabList class in ExtensionPage
The 'flex-wrap' class was removed from the tabList classNames in the ExtensionPage component, likely to prevent tab items from wrapping onto multiple lines and to maintain a single-line tab layout.
2026-01-30 19:35:46 +08:00
手瓜一十雪
57f3c4dd31 Support nested innerPacketMsg in SendMsgBase
Adds handling for innerPacketMsg arrays within uploadReturnData, allowing nested packet messages to be included in the result. This change ensures that all relevant inner messages are processed and returned.
2026-01-30 19:25:01 +08:00
时瑾
5b20ebb7b0 fix: webui 随机token仅生成不会被url编码的随机字符 (#1565)
* fix: webui 随机token仅生成不会被url编码的随机字符

* fix: 移除调试模块中的encodeURIComponent
2026-01-30 18:51:13 +08:00
手瓜一十雪
3a3eaeec7c Add UploadForwardMsgV2 support for multi-message forwarding
Introduces UploadForwardMsgV2 transformer and integrates it into the message sending flow to support forwarding multiple messages with custom action commands. Updates related interfaces and logic to handle UUIDs and nested forwarded messages, improving flexibility and extensibility for message forwarding operations.
2026-01-30 18:47:45 +08:00
16 changed files with 277 additions and 86 deletions

View File

@@ -518,5 +518,13 @@
"9.9.26-44725": { "9.9.26-44725": {
"appid": 537337569, "appid": 537337569,
"qua": "V1_WIN_NQ_9.9.26_44725_GW_B" "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"
} }
} }

View File

@@ -154,5 +154,17 @@
"9.9.26-44725-x64": { "9.9.26-44725-x64": {
"send": "0A18D0C", "send": "0A18D0C",
"recv": "1D4BF0D" "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"
} }
} }

View File

@@ -662,5 +662,17 @@
"9.9.26-44725-x64": { "9.9.26-44725-x64": {
"send": "2CEBB20", "send": "2CEBB20",
"recv": "2CEF0A0" "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"
} }
} }

View File

@@ -2,14 +2,14 @@ import * as crypto from 'node:crypto';
import { PacketMsg } from '@/napcat-core/packet/message/message'; import { PacketMsg } from '@/napcat-core/packet/message/message';
interface ForwardMsgJson { interface ForwardMsgJson {
app: string app: string;
config: ForwardMsgJsonConfig, config: ForwardMsgJsonConfig,
desc: string, desc: string,
extra: ForwardMsgJsonExtra, extra: ForwardMsgJsonExtra,
meta: ForwardMsgJsonMeta, meta: ForwardMsgJsonMeta,
prompt: string, prompt: string,
ver: string, ver: string,
view: string view: string;
} }
interface ForwardMsgJsonConfig { interface ForwardMsgJsonConfig {
@@ -17,7 +17,7 @@ interface ForwardMsgJsonConfig {
forward: number, forward: number,
round: number, round: number,
type: string, type: string,
width: number width: number;
} }
interface ForwardMsgJsonExtra { interface ForwardMsgJsonExtra {
@@ -26,17 +26,17 @@ interface ForwardMsgJsonExtra {
} }
interface ForwardMsgJsonMeta { interface ForwardMsgJsonMeta {
detail: ForwardMsgJsonMetaDetail detail: ForwardMsgJsonMetaDetail;
} }
interface ForwardMsgJsonMetaDetail { interface ForwardMsgJsonMetaDetail {
news: { news: {
text: string text: string;
}[], }[],
resid: string, resid: string,
source: string, source: string,
summary: string, summary: string,
uniseq: string uniseq: string;
} }
interface ForwardAdaptMsg { interface ForwardAdaptMsg {
@@ -50,8 +50,8 @@ interface ForwardAdaptMsgElement {
} }
export class ForwardMsgBuilder { export class ForwardMsgBuilder {
private static build (resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson { private static build (resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string, uuid?: string): ForwardMsgJson {
const id = crypto.randomUUID(); const id = uuid ?? crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg); const isGroupMsg = msg.some(m => m.isGroupMsg);
if (!source) { if (!source) {
source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录'); source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录');
@@ -104,13 +104,19 @@ export class ForwardMsgBuilder {
return this.build(resId, []); return this.build(resId, []);
} }
static fromPacketMsg (resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson { static fromPacketMsg (resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string, uuid?: string): ForwardMsgJson {
return this.build(resId, packetMsg.map(msg => ({ return this.build(resId, packetMsg.map(msg => ({
senderName: msg.senderName, senderName: msg.senderName,
isGroupMsg: msg.groupId !== undefined, isGroupMsg: msg.groupId !== undefined,
msg: msg.msg.map(m => ({ msg: msg.msg.map(m => ({
preview: m.valid ? m.toPreview() : '[该消息类型暂不支持查看]', preview: m.valid ? m.toPreview() : '[该消息类型暂不支持查看]',
})), })),
})), source, news, summary, prompt); })),
source,
news,
summary,
prompt,
uuid,
);
} }
} }

View File

@@ -18,6 +18,7 @@ import { OidbPacket } from '@/napcat-core/packet/transformer/base';
import { ImageOcrResult } from '@/napcat-core/packet/entities/ocrResult'; import { ImageOcrResult } from '@/napcat-core/packet/entities/ocrResult';
import { gunzipSync } from 'zlib'; import { gunzipSync } from 'zlib';
import { PacketMsgConverter } from '@/napcat-core/packet/message/converter'; import { PacketMsgConverter } from '@/napcat-core/packet/message/converter';
import { UploadForwardMsgParams } from '@/napcat-core/packet/transformer/message/UploadForwardMsgV2';
export class PacketOperationContext { export class PacketOperationContext {
private readonly context: PacketContext; private readonly context: PacketContext;
@@ -26,7 +27,7 @@ export class PacketOperationContext {
this.context = context; this.context = context;
} }
async sendPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> { async sendPacket<T extends boolean = false> (pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
return await this.context.client.sendOidbPacket(pkt, rsp); return await this.context.client.sendOidbPacket(pkt, rsp);
} }
@@ -224,7 +225,15 @@ export class PacketOperationContext {
const res = trans.UploadForwardMsg.parse(resp); const res = trans.UploadForwardMsg.parse(resp);
return res.result.resId; return res.result.resId;
} }
async UploadForwardMsgV2 (msg: UploadForwardMsgParams[], groupUin: number = 0) {
//await this.SendPreprocess(msg, groupUin);
// 遍历上传资源
await Promise.allSettled(msg.map(async (item) => { return await this.SendPreprocess(item.actionMsg, groupUin); }));
const req = trans.UploadForwardMsgV2.build(this.context.napcore.basicInfo.uid, msg, groupUin);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.UploadForwardMsg.parse(resp);
return res.result.resId;
}
async MoveGroupFile ( async MoveGroupFile (
groupUin: number, groupUin: number,
fileUUID: string, fileUUID: string,

View File

@@ -0,0 +1,51 @@
import zlib from 'node:zlib';
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketBufBuilder, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import { PacketMsg } from '@/napcat-core/packet/message/message';
export interface UploadForwardMsgParams {
actionCommand: string;
actionMsg: PacketMsg[];
}
class UploadForwardMsgV2 extends PacketTransformer<typeof proto.SendLongMsgResp> {
build (selfUid: string, msg: UploadForwardMsgParams[], groupUin: number = 0): OidbPacket {
const reqdata = msg.map((item) => ({
actionCommand: item.actionCommand,
actionData: {
msgBody: this.msgBuilder.buildFakeMsg(selfUid, item.actionMsg),
}
}));
const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode(
{
action: reqdata,
}
);
const payload = zlib.gzipSync(Buffer.from(longMsgResultData));
const req = new NapProtoMsg(proto.SendLongMsgReq).encode(
{
info: {
type: groupUin === 0 ? 1 : 3,
uid: {
uid: groupUin === 0 ? selfUid : groupUin.toString(),
},
groupUin,
payload,
},
settings: {
field1: 4, field2: 1, field3: 7, field4: 0,
},
}
);
return {
cmd: 'trpc.group.long_msg_interface.MsgService.SsoSendLongMsg',
data: PacketBufBuilder(req),
};
}
parse (data: Buffer) {
return new NapProtoMsg(proto.SendLongMsgResp).decode(data);
}
}
export default new UploadForwardMsgV2();

View File

@@ -2,3 +2,4 @@ export { default as UploadForwardMsg } from './UploadForwardMsg';
export { default as FetchGroupMessage } from './FetchGroupMessage'; export { default as FetchGroupMessage } from './FetchGroupMessage';
export { default as FetchC2CMessage } from './FetchC2CMessage'; export { default as FetchC2CMessage } from './FetchC2CMessage';
export { default as DownloadForwardMsg } from './DownloadForwardMsg'; export { default as DownloadForwardMsg } from './DownloadForwardMsg';
export { default as UploadForwardMsgV2 } from './UploadForwardMsgV2';

View File

@@ -17,6 +17,7 @@ import { rawMsgWithSendMsg } from 'napcat-core/packet/message/converter';
import { Static, Type } from '@sinclair/typebox'; import { Static, Type } from '@sinclair/typebox';
import { MsgActionsExamples } from '@/napcat-onebot/action/msg/examples'; import { MsgActionsExamples } from '@/napcat-onebot/action/msg/examples';
import { OB11MessageMixTypeSchema } from '@/napcat-onebot/types/message'; import { OB11MessageMixTypeSchema } from '@/napcat-onebot/types/message';
import { UploadForwardMsgParams } from '@/napcat-core/packet/transformer/message/UploadForwardMsgV2';
export const SendMsgPayloadSchema = Type.Object({ export const SendMsgPayloadSchema = Type.Object({
message_type: Type.Optional(Type.Union([Type.Literal('private'), Type.Literal('group')], { description: '消息类型 (private/group)' })), message_type: Type.Optional(Type.Union([Type.Literal('private'), Type.Literal('group')], { description: '消息类型 (private/group)' })),
@@ -211,10 +212,14 @@ export class SendMsgBase extends OneBotAction<SendMsgPayload, ReturnDataType> {
}, dp: number = 0): Promise<{ }, dp: number = 0): Promise<{
finallySendElements: SendArkElement, finallySendElements: SendArkElement,
res_id?: string, res_id?: string,
uuid?: string,
packetMsg: PacketMsg[],
deleteAfterSentFiles: string[], deleteAfterSentFiles: string[],
innerPacketMsg?: Array<{ uuid: string, packetMsg: PacketMsg[]; }>;
} | null> { } | null> {
const packetMsg: PacketMsg[] = []; const packetMsg: PacketMsg[] = [];
const delFiles: string[] = []; const delFiles: string[] = [];
const innerMsg: Array<{ uuid: string, packetMsg: PacketMsg[]; }> = new Array();
for (const node of messageNodes) { for (const node of messageNodes) {
if (dp >= 3) { if (dp >= 3) {
this.core.context.logger.logWarn('转发消息深度超过3层将停止解析'); this.core.context.logger.logWarn('转发消息深度超过3层将停止解析');
@@ -232,6 +237,13 @@ export class SendMsgBase extends OneBotAction<SendMsgPayload, ReturnDataType> {
}, dp + 1); }, dp + 1);
sendElements = uploadReturnData?.finallySendElements ? [uploadReturnData.finallySendElements] : []; sendElements = uploadReturnData?.finallySendElements ? [uploadReturnData.finallySendElements] : [];
delFiles.push(...(uploadReturnData?.deleteAfterSentFiles || [])); delFiles.push(...(uploadReturnData?.deleteAfterSentFiles || []));
if (uploadReturnData?.uuid) {
innerMsg.push({ uuid: uploadReturnData.uuid, packetMsg: uploadReturnData.packetMsg });
uploadReturnData.innerPacketMsg?.forEach(m => {
innerMsg.push({ uuid: m.uuid, packetMsg: m.packetMsg });
});
}
} else { } else {
const sendElementsCreateReturn = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer); const sendElementsCreateReturn = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer);
sendElements = sendElementsCreateReturn.sendElements; sendElements = sendElementsCreateReturn.sendElements;
@@ -273,8 +285,19 @@ export class SendMsgBase extends OneBotAction<SendMsgPayload, ReturnDataType> {
this.core.context.logger.logWarn('handleForwardedNodesPacket 元素为空!'); this.core.context.logger.logWarn('handleForwardedNodesPacket 元素为空!');
return null; return null;
} }
const resid = await this.core.apis.PacketApi.pkt.operation.UploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0); const uploadMsgData: UploadForwardMsgParams[] = [{
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt); actionCommand: 'MultiMsg',
actionMsg: packetMsg,
}];
innerMsg.forEach(({ uuid, packetMsg: msg }) => {
uploadMsgData.push({
actionCommand: uuid,
actionMsg: msg,
});
});
const resid = await this.core.apis.PacketApi.pkt.operation.UploadForwardMsgV2(uploadMsgData, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0);
const uuid = crypto.randomUUID();
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt, uuid);
return { return {
deleteAfterSentFiles: delFiles, deleteAfterSentFiles: delFiles,
finallySendElements: { finallySendElements: {
@@ -285,6 +308,9 @@ export class SendMsgBase extends OneBotAction<SendMsgPayload, ReturnDataType> {
}, },
} as SendArkElement, } as SendArkElement,
res_id: resid, res_id: resid,
uuid: uuid,
packetMsg: packetMsg,
innerPacketMsg: innerMsg,
}; };
} }

View File

@@ -316,6 +316,11 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> i
entry = newEntry; entry = newEntry;
} }
if (!entry.enable) {
this.logger.log(`[PluginManager] Skipping loading disabled plugin: ${pluginId}`);
return false;
}
return await this.loadPlugin(entry); return await this.loadPlugin(entry);
} }

View File

@@ -123,8 +123,8 @@ export class PluginLoader {
const entryFile = this.findEntryFile(pluginDir, packageJson); const entryFile = this.findEntryFile(pluginDir, packageJson);
const entryPath = entryFile ? path.join(pluginDir, entryFile) : undefined; const entryPath = entryFile ? path.join(pluginDir, entryFile) : undefined;
// 获取启用状态(默认启用 // 获取启用状态(默认禁用,内置插件除外
const enable = statusConfig[pluginId] !== false; const enable = statusConfig[pluginId] ?? (pluginId === 'napcat-plugin-builtin');
// 创建插件条目 // 创建插件条目
const entry: PluginEntry = { const entry: PluginEntry = {
@@ -159,7 +159,7 @@ export class PluginLoader {
id: dirname, // 使用目录名作为 ID id: dirname, // 使用目录名作为 ID
fileId: dirname, fileId: dirname,
pluginPath: path.join(this.pluginPath, dirname), pluginPath: path.join(this.pluginPath, dirname),
enable: statusConfig[dirname] !== false, enable: statusConfig[dirname] ?? (dirname === 'napcat-plugin-builtin'),
loaded: false, loaded: false,
runtime: { runtime: {
status: 'error', status: 'error',

View File

@@ -285,6 +285,11 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
entry = newEntry; entry = newEntry;
} }
if (!entry.enable) {
this.logger.log(`[PluginManager] Skipping loading disabled plugin: ${pluginId}`);
return false;
}
return await this.loadPlugin(entry); return await this.loadPlugin(entry);
} }

View File

@@ -340,7 +340,7 @@ export async function applyPendingUpdates (webUiPathWrapper: NapCatPathWrapper,
const configPath = path.join(webUiPathWrapper.configPath, 'napcat-update.json'); const configPath = path.join(webUiPathWrapper.configPath, 'napcat-update.json');
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
logger.log('[NapCat Update] No pending updates found'); //logger.log('[NapCat Update] No pending updates found');
return; return;
} }

View File

@@ -41,7 +41,7 @@ const PluginStoreCard: React.FC<PluginStoreCardProps> = ({
return { return {
text: '更新', text: '更新',
icon: <IoMdDownload size={16} />, icon: <IoMdDownload size={16} />,
color: 'success' as const, color: 'default' as const,
}; };
default: default:
return { return {

View File

@@ -260,14 +260,14 @@ const NewVersionTip = (props: NewVersionTipProps) => {
<div className="cursor-pointer flex items-center justify-center" onClick={updateStatus === 'updating' ? undefined : showUpdateDialog}> <div className="cursor-pointer flex items-center justify-center" onClick={updateStatus === 'updating' ? undefined : showUpdateDialog}>
<Chip <Chip
size="sm" size="sm"
color="danger" color="primary"
variant="flat" variant="flat"
classNames={{ classNames={{
content: "font-bold text-[10px] px-1 flex items-center justify-center", content: "font-bold text-[10px] px-1 flex items-center justify-center",
base: "h-5 min-h-5 min-w-[42px]" base: "h-5 min-h-5 min-w-[42px]"
}} }}
> >
{updateStatus === 'updating' ? <Spinner size="sm" color="danger" classNames={{ wrapper: "w-3 h-3" }} /> : 'New'} {updateStatus === 'updating' ? <Spinner size="sm" color="primary" classNames={{ wrapper: "w-3 h-3" }} /> : 'New'}
</Chip> </Chip>
</div> </div>
</Tooltip> </Tooltip>

View File

@@ -89,75 +89,74 @@ export default function ExtensionPage () {
return ( return (
<> <>
<title> - NapCat WebUI</title> <title> - NapCat WebUI</title>
<div className='p-2 md:p-4 relative h-full flex flex-col'> <div className='p-2 md:p-4 relative h-[calc(100vh-6rem)] md:h-[calc(100vh-4rem)] flex flex-col'>
<PageLoading loading={loading} /> <PageLoading loading={loading} />
<div className='flex mb-4 items-center gap-4'> <div className='flex mb-4 items-center justify-between gap-4 flex-wrap'>
<div className='flex items-center gap-2 text-default-600'> <div className='flex items-center gap-4'>
<MdExtension size={24} /> <div className='flex items-center gap-2 text-default-600'>
<span className='text-lg font-medium'></span> <MdExtension size={24} />
<span className='text-lg font-medium'></span>
</div>
<Button
isIconOnly
className='bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md'
radius='full'
onPress={refresh}
>
<IoMdRefresh size={24} />
</Button>
</div> </div>
<Button {extensionPages.length > 0 && (
isIconOnly <Tabs
className='bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md' aria-label='Extension Pages'
radius='full' className='max-w-full'
onPress={refresh} selectedKey={selectedTab}
> onSelectionChange={(key) => setSelectedTab(key as string)}
<IoMdRefresh size={24} /> classNames={{
</Button> tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md',
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
panel: 'hidden',
}}
>
{tabs.map((tab) => (
<Tab
key={tab.key}
title={
<div className='flex items-center gap-2'>
{tab.icon && <span>{tab.icon}</span>}
<span>{tab.title}</span>
<span className='text-xs text-default-400'>({tab.pluginName})</span>
</div>
}
/>
))}
</Tabs>
)}
</div> </div>
{extensionPages.length === 0 && !loading {extensionPages.length === 0 && !loading ? (
? ( <div className='flex-1 flex flex-col items-center justify-center text-default-400'>
<div className='flex-1 flex flex-col items-center justify-center text-default-400'> <MdExtension size={64} className='mb-4 opacity-50' />
<MdExtension size={64} className='mb-4 opacity-50' /> <p className='text-lg'></p>
<p className='text-lg'></p> <p className='text-sm mt-2'> WebUI </p>
<p className='text-sm mt-2'> WebUI </p> </div>
</div> ) : (
) <div className='flex-1 min-h-0 bg-white/40 dark:bg-black/20 backdrop-blur-md rounded-lg overflow-hidden relative'>
: ( {iframeLoading && (
<div className='flex-1 flex flex-col min-h-0'> <div className='absolute inset-0 flex items-center justify-center bg-default-100/50 z-10'>
<Tabs <Spinner size='lg' />
aria-label='Extension Pages' </div>
className='max-w-full' )}
selectedKey={selectedTab} <iframe
onSelectionChange={(key) => setSelectedTab(key as string)} src={currentPageUrl}
classNames={{ className='w-full h-full border-0'
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md flex-wrap', onLoad={handleIframeLoad}
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm', title='extension-page'
panel: 'flex-1 min-h-0 p-0', sandbox='allow-scripts allow-same-origin allow-forms allow-popups'
}} />
> </div>
{tabs.map((tab) => ( )}
<Tab
key={tab.key}
title={
<div className='flex items-center gap-2'>
{tab.icon && <span>{tab.icon}</span>}
<span>{tab.title}</span>
<span className='text-xs text-default-400'>({tab.pluginName})</span>
</div>
}
>
<div className='relative w-full h-[calc(100vh-220px)] bg-white/40 dark:bg-black/20 backdrop-blur-md rounded-lg overflow-hidden'>
{iframeLoading && (
<div className='absolute inset-0 flex items-center justify-center bg-default-100/50 z-10'>
<Spinner size='lg' />
</div>
)}
<iframe
src={currentPageUrl}
className='w-full h-full border-0'
onLoad={handleIframeLoad}
title={tab.title}
sandbox='allow-scripts allow-same-origin allow-forms allow-popups'
/>
</div>
</Tab>
))}
</Tabs>
</div>
)}
</div> </div>
</> </>
); );

View File

@@ -2,9 +2,10 @@ import { Button } from '@heroui/button';
import { useDisclosure } from '@heroui/modal'; import { useDisclosure } from '@heroui/modal';
import { Tab, Tabs } from '@heroui/tabs'; import { Tab, Tabs } from '@heroui/tabs';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { IoMdRefresh } from 'react-icons/io'; import { IoMdRefresh } from 'react-icons/io';
import { FiDownload, FiUpload } from 'react-icons/fi';
import AddButton from '@/components/button/add_button'; import AddButton from '@/components/button/add_button';
import HTTPClientDisplayCard from '@/components/display_card/http_client'; import HTTPClientDisplayCard from '@/components/display_card/http_client';
@@ -55,7 +56,9 @@ export default function NetworkPage () {
deleteNetworkConfig, deleteNetworkConfig,
enableNetworkConfig, enableNetworkConfig,
enableDebugNetworkConfig, enableDebugNetworkConfig,
updateSingleConfig,
} = useConfig(); } = useConfig();
const fileInputRef = useRef<HTMLInputElement>(null);
const [activeField, setActiveField] = const [activeField, setActiveField] =
useState<keyof OneBotConfig['network']>('httpServers'); useState<keyof OneBotConfig['network']>('httpServers');
const [activeName, setActiveName] = useState<string>(''); const [activeName, setActiveName] = useState<string>('');
@@ -99,6 +102,45 @@ export default function NetworkPage () {
onOpen(); 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<HTMLInputElement>) => {
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 ( const onDelete = async (
field: keyof OneBotConfig['network'], field: keyof OneBotConfig['network'],
name: string name: string
@@ -373,6 +415,21 @@ export default function NetworkPage () {
<PageLoading loading={loading} /> <PageLoading loading={loading} />
<div className='flex mb-6 items-center gap-4'> <div className='flex mb-6 items-center gap-4'>
<AddButton onOpen={handleClickCreate} /> <AddButton onOpen={handleClickCreate} />
<Button
className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md"
startContent={<FiUpload size={18} />}
onPress={handleExport}
>
</Button>
<Button
className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md"
startContent={<FiDownload size={18} />}
onPress={() => fileInputRef.current?.click()}
>
</Button>
<input ref={fileInputRef} type="file" accept=".json" className="hidden" onChange={handleFileChange} />
<Button <Button
isIconOnly isIconOnly
className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md" className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md"