mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-12 16:00:27 +00:00
Compare commits
9 Commits
feat/secur
...
v4.14.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17322bb5a4 | ||
|
|
c0bcced5fb | ||
|
|
805c1d5ea2 | ||
|
|
b3399b07ad | ||
|
|
71f8504849 | ||
|
|
3b7ca1a08f | ||
|
|
57f3c4dd31 | ||
|
|
5b20ebb7b0 | ||
|
|
3a3eaeec7c |
8
packages/napcat-core/external/appid.json
vendored
8
packages/napcat-core/external/appid.json
vendored
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
packages/napcat-core/external/napi2native.json
vendored
12
packages/napcat-core/external/napi2native.json
vendored
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
packages/napcat-core/external/packet.json
vendored
12
packages/napcat-core/external/packet.json
vendored
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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';
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ const GenericForm = <T extends keyof NetworkConfigType> ({
|
|||||||
export default GenericForm;
|
export default GenericForm;
|
||||||
export function random_token (length: number) {
|
export function random_token (length: number) {
|
||||||
const chars =
|
const chars =
|
||||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()-_=+[]{}|;:,.<>?';
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~';
|
||||||
let result = '';
|
let result = '';
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function ExtensionPage () {
|
|||||||
pluginName: page.pluginName,
|
pluginName: page.pluginName,
|
||||||
path: page.path,
|
path: page.path,
|
||||||
icon: page.icon,
|
icon: page.icon,
|
||||||
description: page.description
|
description: page.description,
|
||||||
}));
|
}));
|
||||||
}, [extensionPages]);
|
}, [extensionPages]);
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export default function ExtensionPage () {
|
|||||||
const path = pathParts.join(':').replace(/^\//, '');
|
const path = pathParts.join(':').replace(/^\//, '');
|
||||||
// 获取认证 token
|
// 获取认证 token
|
||||||
const token = localStorage.getItem('token') || '';
|
const token = localStorage.getItem('token') || '';
|
||||||
return `/api/Plugin/page/${pluginId}/${path}?webui_token=${encodeURIComponent(token)}`;
|
return `/api/Plugin/page/${pluginId}/${path}?webui_token=${token}`;
|
||||||
}, [selectedTab]);
|
}, [selectedTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -89,72 +89,73 @@ 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'>
|
||||||
|
<div className='flex items-center gap-2 text-default-600'>
|
||||||
<MdExtension size={24} />
|
<MdExtension size={24} />
|
||||||
<span className="text-lg font-medium">插件扩展页面</span>
|
<span className='text-lg font-medium'>插件扩展页面</span>
|
||||||
</div>
|
</div>
|
||||||
<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'
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={refresh}
|
onPress={refresh}
|
||||||
>
|
>
|
||||||
<IoMdRefresh size={24} />
|
<IoMdRefresh size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{extensionPages.length > 0 && (
|
||||||
{extensionPages.length === 0 && !loading ? (
|
|
||||||
<div className="flex-1 flex flex-col items-center justify-center text-default-400">
|
|
||||||
<MdExtension size={64} className="mb-4 opacity-50" />
|
|
||||||
<p className="text-lg">暂无插件扩展页面</p>
|
|
||||||
<p className="text-sm mt-2">插件可以通过注册页面来扩展 WebUI 功能</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex-1 flex flex-col min-h-0">
|
|
||||||
<Tabs
|
<Tabs
|
||||||
aria-label="Extension Pages"
|
aria-label='Extension Pages'
|
||||||
className="max-w-full"
|
className='max-w-full'
|
||||||
selectedKey={selectedTab}
|
selectedKey={selectedTab}
|
||||||
onSelectionChange={(key) => setSelectedTab(key as string)}
|
onSelectionChange={(key) => setSelectedTab(key as string)}
|
||||||
classNames={{
|
classNames={{
|
||||||
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md flex-wrap',
|
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md',
|
||||||
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
|
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
|
||||||
panel: 'flex-1 min-h-0 p-0'
|
panel: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
{tab.icon && <span>{tab.icon}</span>}
|
{tab.icon && <span>{tab.icon}</span>}
|
||||||
<span>{tab.title}</span>
|
<span>{tab.title}</span>
|
||||||
<span className="text-xs text-default-400">({tab.pluginName})</span>
|
<span className='text-xs text-default-400'>({tab.pluginName})</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<div className="relative w-full h-[calc(100vh-220px)] bg-white/40 dark:bg-black/20 backdrop-blur-md rounded-lg overflow-hidden">
|
))}
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{extensionPages.length === 0 && !loading ? (
|
||||||
|
<div className='flex-1 flex flex-col items-center justify-center text-default-400'>
|
||||||
|
<MdExtension size={64} className='mb-4 opacity-50' />
|
||||||
|
<p className='text-lg'>暂无插件扩展页面</p>
|
||||||
|
<p className='text-sm mt-2'>插件可以通过注册页面来扩展 WebUI 功能</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='flex-1 min-h-0 bg-white/40 dark:bg-black/20 backdrop-blur-md rounded-lg overflow-hidden relative'>
|
||||||
{iframeLoading && (
|
{iframeLoading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-default-100/50 z-10">
|
<div className='absolute inset-0 flex items-center justify-center bg-default-100/50 z-10'>
|
||||||
<Spinner size="lg" />
|
<Spinner size='lg' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<iframe
|
<iframe
|
||||||
src={currentPageUrl}
|
src={currentPageUrl}
|
||||||
className="w-full h-full border-0"
|
className='w-full h-full border-0'
|
||||||
onLoad={handleIframeLoad}
|
onLoad={handleIframeLoad}
|
||||||
title={tab.title}
|
title='extension-page'
|
||||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
sandbox='allow-scripts allow-same-origin allow-forms allow-popups'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user