mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-02 00:30:25 +00:00
Merge branch 'main' into feat-plugin-npm
This commit is contained in:
@@ -54,7 +54,7 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
|
||||
textValue='title'
|
||||
>
|
||||
<div className='flex items-center gap-2 justify-center'>
|
||||
<div className='w-5 h-5 -ml-3'>
|
||||
<div className='w-5 h-5 -ml-3 flex items-center justify-center'>
|
||||
<PlusIcon />
|
||||
</div>
|
||||
<div className='text-primary-400'>新建网络配置</div>
|
||||
|
||||
714
packages/napcat-webui-frontend/src/components/guid_manager.tsx
Normal file
714
packages/napcat-webui-frontend/src/components/guid_manager.tsx
Normal file
@@ -0,0 +1,714 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import { Input } from '@heroui/input';
|
||||
import { Divider } from '@heroui/divider';
|
||||
import { Chip } from '@heroui/chip';
|
||||
import { Listbox, ListboxItem } from '@heroui/listbox';
|
||||
import { Spinner } from '@heroui/spinner';
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { MdContentCopy, MdDelete, MdRefresh, MdSave, MdRestorePage, MdBackup } from 'react-icons/md';
|
||||
import MD5 from 'crypto-js/md5';
|
||||
|
||||
import QQManager from '@/controllers/qq_manager';
|
||||
import useDialog from '@/hooks/use-dialog';
|
||||
|
||||
interface GUIDManagerProps {
|
||||
/** 是否显示重启按钮 */
|
||||
showRestart?: boolean;
|
||||
/** 紧凑模式(用于弹窗场景) */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const GUIDManager: React.FC<GUIDManagerProps> = ({ showRestart = true, compact = false }) => {
|
||||
const dialog = useDialog();
|
||||
|
||||
// 平台检测
|
||||
const [platform, setPlatform] = useState<string>('');
|
||||
const isWindows = platform === 'win32';
|
||||
const isMac = platform === 'darwin';
|
||||
const isLinux = platform !== '' && platform !== 'win32' && platform !== 'darwin';
|
||||
const platformDetected = platform !== '';
|
||||
|
||||
// Windows 状态
|
||||
const [currentGUID, setCurrentGUID] = useState<string>('');
|
||||
const [inputGUID, setInputGUID] = useState<string>('');
|
||||
const [backups, setBackups] = useState<string[]>([]);
|
||||
|
||||
// Linux 状态
|
||||
const [currentMAC, setCurrentMAC] = useState<string>('');
|
||||
const [inputMAC, setInputMAC] = useState<string>('');
|
||||
const [machineId, setMachineId] = useState<string>('');
|
||||
const [linuxBackups, setLinuxBackups] = useState<string[]>([]);
|
||||
|
||||
// 通用状态
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
|
||||
const isValidGUID = (guid: string) => /^[0-9a-fA-F]{32}$/.test(guid);
|
||||
const isValidMAC = (mac: string) => /^[0-9a-fA-F]{2}(-[0-9a-fA-F]{2}){5}$/.test(mac.trim().toLowerCase());
|
||||
|
||||
// 前端实时计算 Linux GUID = MD5(machine-id + MAC)
|
||||
const computedLinuxGUID = useMemo(() => {
|
||||
if (!isLinux) return '';
|
||||
const mac = inputMAC.trim().toLowerCase();
|
||||
if (!machineId && !mac) return '';
|
||||
return MD5(machineId + mac).toString();
|
||||
}, [isLinux, machineId, inputMAC]);
|
||||
|
||||
// 当前生效的 GUID (基于已保存的 MAC)
|
||||
const currentLinuxGUID = useMemo(() => {
|
||||
if (!isLinux || !currentMAC) return '';
|
||||
return MD5(machineId + currentMAC).toString();
|
||||
}, [isLinux, machineId, currentMAC]);
|
||||
|
||||
// 检测平台
|
||||
const fetchPlatform = useCallback(async () => {
|
||||
try {
|
||||
const data = await QQManager.getPlatformInfo();
|
||||
setPlatform(data.platform);
|
||||
} catch {
|
||||
// 如果获取失败,默认 win32 向后兼容
|
||||
setPlatform('win32');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Windows: 获取 GUID
|
||||
const fetchGUID = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await QQManager.getDeviceGUID();
|
||||
setCurrentGUID(data.guid);
|
||||
setInputGUID(data.guid);
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
setCurrentGUID('');
|
||||
setInputGUID('');
|
||||
if (!msg.includes('not found')) {
|
||||
toast.error(`获取 GUID 失败: ${msg}`);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Windows: 获取备份
|
||||
const fetchBackups = useCallback(async () => {
|
||||
try {
|
||||
const data = await QQManager.getGUIDBackups();
|
||||
setBackups(data);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Linux: 获取 MAC + machine-id
|
||||
const fetchLinuxInfo = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [macData, midData] = await Promise.all([
|
||||
QQManager.getLinuxMAC().catch(() => ({ mac: '' })),
|
||||
QQManager.getLinuxMachineId().catch(() => ({ machineId: '' })),
|
||||
]);
|
||||
setCurrentMAC(macData.mac);
|
||||
setInputMAC(macData.mac);
|
||||
setMachineId(midData.machineId);
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`获取设备信息失败: ${msg}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Linux: 获取备份
|
||||
const fetchLinuxBackups = useCallback(async () => {
|
||||
try {
|
||||
const data = await QQManager.getLinuxMachineInfoBackups();
|
||||
setLinuxBackups(data);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlatform();
|
||||
}, [fetchPlatform]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!platformDetected) return;
|
||||
if (isWindows) {
|
||||
fetchGUID();
|
||||
fetchBackups();
|
||||
} else {
|
||||
fetchLinuxInfo();
|
||||
fetchLinuxBackups();
|
||||
}
|
||||
}, [platformDetected, isWindows, fetchGUID, fetchBackups, fetchLinuxInfo, fetchLinuxBackups]);
|
||||
|
||||
// ========== Windows 操作 ==========
|
||||
|
||||
const handleCopy = () => {
|
||||
const guid = isLinux ? currentLinuxGUID : currentGUID;
|
||||
if (guid) {
|
||||
navigator.clipboard.writeText(guid);
|
||||
toast.success('已复制到剪贴板');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!isValidGUID(inputGUID)) {
|
||||
toast.error('GUID 格式无效,需要 32 位十六进制字符');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await QQManager.setDeviceGUID(inputGUID);
|
||||
setCurrentGUID(inputGUID);
|
||||
toast.success('GUID 已设置,重启后生效');
|
||||
await fetchBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`设置 GUID 失败: ${msg}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
dialog.confirm({
|
||||
title: '确认删除',
|
||||
content: '删除 Registry20 后,QQ 将在下次启动时生成新的设备标识。确定要删除吗?',
|
||||
confirmText: '删除',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await QQManager.resetDeviceID();
|
||||
setCurrentGUID('');
|
||||
setInputGUID('');
|
||||
toast.success('已删除,重启后生效');
|
||||
await fetchBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`删除失败: ${msg}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBackup = async () => {
|
||||
try {
|
||||
await QQManager.createGUIDBackup();
|
||||
toast.success('备份已创建');
|
||||
await fetchBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`备份失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestore = (backupName: string) => {
|
||||
dialog.confirm({
|
||||
title: '确认恢复',
|
||||
content: `确定要从备份 "${backupName}" 恢复吗?当前的 Registry20 将被覆盖。`,
|
||||
confirmText: '恢复',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await QQManager.restoreGUIDBackup(backupName);
|
||||
toast.success('已恢复,重启后生效');
|
||||
await fetchGUID();
|
||||
await fetchBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`恢复失败: ${msg}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ========== Linux 操作 ==========
|
||||
|
||||
const handleLinuxSaveMAC = async () => {
|
||||
const mac = inputMAC.trim().toLowerCase();
|
||||
if (!isValidMAC(mac)) {
|
||||
toast.error('MAC 格式无效,需要 xx-xx-xx-xx-xx-xx 格式');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await QQManager.setLinuxMAC(mac);
|
||||
setCurrentMAC(mac);
|
||||
toast.success('MAC 已设置,重启后生效');
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`设置 MAC 失败: ${msg}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinuxCopyMAC = () => {
|
||||
if (currentMAC) {
|
||||
navigator.clipboard.writeText(currentMAC);
|
||||
toast.success('MAC 已复制到剪贴板');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinuxDelete = () => {
|
||||
dialog.confirm({
|
||||
title: '确认删除',
|
||||
content: '删除 machine-info 后,QQ 将在下次启动时生成新的设备标识。确定要删除吗?',
|
||||
confirmText: '删除',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await QQManager.resetLinuxDeviceID();
|
||||
setCurrentMAC('');
|
||||
setInputMAC('');
|
||||
toast.success('已删除,重启后生效');
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`删除失败: ${msg}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleLinuxBackup = async () => {
|
||||
try {
|
||||
await QQManager.createLinuxMachineInfoBackup();
|
||||
toast.success('备份已创建');
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`备份失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinuxRestore = (backupName: string) => {
|
||||
dialog.confirm({
|
||||
title: '确认恢复',
|
||||
content: `确定要从备份 "${backupName}" 恢复吗?当前的 machine-info 将被覆盖。`,
|
||||
confirmText: '恢复',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await QQManager.restoreLinuxMachineInfoBackup(backupName);
|
||||
toast.success('已恢复,重启后生效');
|
||||
await fetchLinuxInfo();
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`恢复失败: ${msg}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ========== 重启 ==========
|
||||
|
||||
const handleRestart = () => {
|
||||
dialog.confirm({
|
||||
title: '确认重启',
|
||||
content: '确定要重启 NapCat 吗?这将导致当前连接断开。',
|
||||
confirmText: '重启',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
setRestarting(true);
|
||||
try {
|
||||
await QQManager.restartNapCat();
|
||||
toast.success('重启指令已发送');
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`重启失败: ${msg}`);
|
||||
} finally {
|
||||
setRestarting(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (loading || !platformDetected) {
|
||||
return (
|
||||
<div className='flex items-center justify-center py-8'>
|
||||
<Spinner label='加载中...' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== macOS 不支持 ==========
|
||||
if (isMac) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-${compact ? '3' : '4'}`}>
|
||||
<div className='flex flex-col items-center justify-center py-8 gap-2'>
|
||||
<Chip variant='flat' color='warning' className='text-xs'>
|
||||
macOS 平台暂不支持 GUID 管理
|
||||
</Chip>
|
||||
<div className='text-xs text-default-400'>
|
||||
该功能仅适用于 Windows 和 Linux 平台
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Linux 渲染 ==========
|
||||
if (isLinux) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-${compact ? '3' : '4'}`}>
|
||||
{/* 当前设备信息 */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>当前设备 GUID</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
{currentLinuxGUID ? (
|
||||
<Chip variant='flat' color='primary' className='font-mono text-xs max-w-full'>
|
||||
{currentLinuxGUID}
|
||||
</Chip>
|
||||
) : (
|
||||
<Chip variant='flat' color='warning' className='text-xs'>
|
||||
未设置 / 不存在
|
||||
</Chip>
|
||||
)}
|
||||
{currentLinuxGUID && (
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={handleCopy}
|
||||
aria-label='复制GUID'
|
||||
>
|
||||
<MdContentCopy size={16} />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={fetchLinuxInfo}
|
||||
aria-label='刷新'
|
||||
>
|
||||
<MdRefresh size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='text-xs text-default-400'>
|
||||
GUID = MD5(machine-id + MAC),修改 MAC 即可改变 GUID
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* machine-id 显示 */}
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='text-sm font-medium text-default-700'>Machine ID</div>
|
||||
<Chip variant='flat' color='default' className='font-mono text-xs max-w-full'>
|
||||
{machineId || '未知'}
|
||||
</Chip>
|
||||
<div className='text-xs text-default-400'>
|
||||
来自 /etc/machine-id,不可修改
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 当前 MAC 显示 */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>当前 MAC 地址</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
{currentMAC ? (
|
||||
<Chip variant='flat' color='secondary' className='font-mono text-xs max-w-full'>
|
||||
{currentMAC}
|
||||
</Chip>
|
||||
) : (
|
||||
<Chip variant='flat' color='warning' className='text-xs'>
|
||||
未设置 / 不存在
|
||||
</Chip>
|
||||
)}
|
||||
{currentMAC && (
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={handleLinuxCopyMAC}
|
||||
aria-label='复制MAC'
|
||||
>
|
||||
<MdContentCopy size={16} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 编辑 MAC 地址 */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>设置 MAC 地址</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='bordered'
|
||||
placeholder='xx-xx-xx-xx-xx-xx'
|
||||
value={inputMAC}
|
||||
onValueChange={setInputMAC}
|
||||
isInvalid={inputMAC.length > 0 && !isValidMAC(inputMAC)}
|
||||
errorMessage={inputMAC.length > 0 && !isValidMAC(inputMAC) ? '格式: xx-xx-xx-xx-xx-xx' : undefined}
|
||||
classNames={{
|
||||
input: 'font-mono text-sm',
|
||||
}}
|
||||
maxLength={17}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 实时 GUID 预览 */}
|
||||
{inputMAC && isValidMAC(inputMAC) && (
|
||||
<div className='flex flex-col gap-1 p-2 rounded-lg bg-default-100'>
|
||||
<div className='text-xs font-medium text-default-500'>预览 GUID</div>
|
||||
<div className='font-mono text-xs text-primary break-all'>
|
||||
{computedLinuxGUID}
|
||||
</div>
|
||||
{computedLinuxGUID !== currentLinuxGUID && (
|
||||
<div className='text-xs text-warning-500'>
|
||||
与当前 GUID 不同,保存后重启生效
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
color='primary'
|
||||
variant='flat'
|
||||
isLoading={saving}
|
||||
isDisabled={!isValidMAC(inputMAC) || inputMAC.trim().toLowerCase() === currentMAC}
|
||||
onPress={handleLinuxSaveMAC}
|
||||
startContent={<MdSave size={16} />}
|
||||
>
|
||||
保存 MAC
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
color='danger'
|
||||
variant='flat'
|
||||
isDisabled={!currentMAC}
|
||||
onPress={handleLinuxDelete}
|
||||
startContent={<MdDelete size={16} />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
color='secondary'
|
||||
variant='flat'
|
||||
isDisabled={!currentMAC}
|
||||
onPress={handleLinuxBackup}
|
||||
startContent={<MdBackup size={16} />}
|
||||
>
|
||||
手动备份
|
||||
</Button>
|
||||
</div>
|
||||
<div className='text-xs text-default-400'>
|
||||
修改 MAC 后 GUID 将变化,需重启 NapCat 才能生效,操作前会自动备份
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 备份恢复 */}
|
||||
{linuxBackups.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>
|
||||
备份列表
|
||||
<span className='text-xs text-default-400 ml-2'>(点击恢复)</span>
|
||||
</div>
|
||||
<div className='max-h-[160px] overflow-y-auto rounded-lg border border-default-200'>
|
||||
<Listbox
|
||||
aria-label='备份列表'
|
||||
selectionMode='none'
|
||||
onAction={(key) => handleLinuxRestore(key as string)}
|
||||
>
|
||||
{linuxBackups.map((name) => (
|
||||
<ListboxItem
|
||||
key={name}
|
||||
startContent={<MdRestorePage size={16} className='text-default-400' />}
|
||||
className='font-mono text-xs'
|
||||
>
|
||||
{name}
|
||||
</ListboxItem>
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 重启 */}
|
||||
{showRestart && (
|
||||
<>
|
||||
<Divider />
|
||||
<Button
|
||||
size='sm'
|
||||
color='warning'
|
||||
variant='flat'
|
||||
isLoading={restarting}
|
||||
onPress={handleRestart}
|
||||
startContent={<MdRefresh size={16} />}
|
||||
>
|
||||
重启 NapCat
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Windows 渲染 ==========
|
||||
return (
|
||||
<div className={`flex flex-col gap-${compact ? '3' : '4'}`}>
|
||||
{/* 当前 GUID 显示 */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>当前设备 GUID</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
{currentGUID ? (
|
||||
<Chip variant='flat' color='primary' className='font-mono text-xs max-w-full'>
|
||||
{currentGUID}
|
||||
</Chip>
|
||||
) : (
|
||||
<Chip variant='flat' color='warning' className='text-xs'>
|
||||
未设置 / 不存在
|
||||
</Chip>
|
||||
)}
|
||||
{currentGUID && (
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={handleCopy}
|
||||
aria-label='复制GUID'
|
||||
>
|
||||
<MdContentCopy size={16} />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={fetchGUID}
|
||||
aria-label='刷新'
|
||||
>
|
||||
<MdRefresh size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 设置 GUID */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>设置 GUID</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='bordered'
|
||||
placeholder='输入32位十六进制 GUID'
|
||||
value={inputGUID}
|
||||
onValueChange={setInputGUID}
|
||||
isInvalid={inputGUID.length > 0 && !isValidGUID(inputGUID)}
|
||||
errorMessage={inputGUID.length > 0 && !isValidGUID(inputGUID) ? '需要32位十六进制字符' : undefined}
|
||||
classNames={{
|
||||
input: 'font-mono text-sm',
|
||||
}}
|
||||
maxLength={32}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
color='primary'
|
||||
variant='flat'
|
||||
isLoading={saving}
|
||||
isDisabled={!isValidGUID(inputGUID) || inputGUID === currentGUID}
|
||||
onPress={handleSave}
|
||||
startContent={<MdSave size={16} />}
|
||||
>
|
||||
保存 GUID
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
color='danger'
|
||||
variant='flat'
|
||||
isDisabled={!currentGUID}
|
||||
onPress={handleDelete}
|
||||
startContent={<MdDelete size={16} />}
|
||||
>
|
||||
删除 GUID
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
color='secondary'
|
||||
variant='flat'
|
||||
isDisabled={!currentGUID}
|
||||
onPress={handleBackup}
|
||||
startContent={<MdBackup size={16} />}
|
||||
>
|
||||
手动备份
|
||||
</Button>
|
||||
</div>
|
||||
<div className='text-xs text-default-400'>
|
||||
修改或删除 GUID 后需重启 NapCat 才能生效,操作前会自动备份
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 备份恢复 */}
|
||||
{backups.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>
|
||||
备份列表
|
||||
<span className='text-xs text-default-400 ml-2'>(点击恢复)</span>
|
||||
</div>
|
||||
<div className='max-h-[160px] overflow-y-auto rounded-lg border border-default-200'>
|
||||
<Listbox
|
||||
aria-label='备份列表'
|
||||
selectionMode='none'
|
||||
onAction={(key) => handleRestore(key as string)}
|
||||
>
|
||||
{backups.map((name) => (
|
||||
<ListboxItem
|
||||
key={name}
|
||||
startContent={<MdRestorePage size={16} className='text-default-400' />}
|
||||
className='font-mono text-xs'
|
||||
>
|
||||
{name}
|
||||
</ListboxItem>
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 重启 */}
|
||||
{showRestart && (
|
||||
<>
|
||||
<Divider />
|
||||
<Button
|
||||
size='sm'
|
||||
color='warning'
|
||||
variant='flat'
|
||||
isLoading={restarting}
|
||||
onPress={handleRestart}
|
||||
startContent={<MdRefresh size={16} />}
|
||||
>
|
||||
重启 NapCat
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GUIDManager;
|
||||
@@ -101,4 +101,100 @@ export default class QQManager {
|
||||
passwordMd5,
|
||||
});
|
||||
}
|
||||
|
||||
public static async resetDeviceID () {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/ResetDeviceID');
|
||||
}
|
||||
|
||||
public static async restartNapCat () {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/RestartNapCat');
|
||||
}
|
||||
|
||||
public static async getDeviceGUID () {
|
||||
const data = await serverRequest.post<ServerResponse<{ guid: string; }>>('/QQLogin/GetDeviceGUID');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async setDeviceGUID (guid: string) {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/SetDeviceGUID', { guid });
|
||||
}
|
||||
|
||||
public static async getGUIDBackups () {
|
||||
const data = await serverRequest.post<ServerResponse<string[]>>('/QQLogin/GetGUIDBackups');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async restoreGUIDBackup (backupName: string) {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/RestoreGUIDBackup', { backupName });
|
||||
}
|
||||
|
||||
public static async createGUIDBackup () {
|
||||
const data = await serverRequest.post<ServerResponse<{ path: string; }>>('/QQLogin/CreateGUIDBackup');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 平台信息 & Linux GUID 管理
|
||||
// ============================================================
|
||||
|
||||
public static async getPlatformInfo () {
|
||||
const data = await serverRequest.post<ServerResponse<{ platform: string; }>>('/QQLogin/GetPlatformInfo');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async getLinuxMAC () {
|
||||
const data = await serverRequest.post<ServerResponse<{ mac: string; }>>('/QQLogin/GetLinuxMAC');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async setLinuxMAC (mac: string) {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/SetLinuxMAC', { mac });
|
||||
}
|
||||
|
||||
public static async getLinuxMachineId () {
|
||||
const data = await serverRequest.post<ServerResponse<{ machineId: string; }>>('/QQLogin/GetLinuxMachineId');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async computeLinuxGUID (mac?: string, machineId?: string) {
|
||||
const data = await serverRequest.post<ServerResponse<{ guid: string; machineId: string; mac: string; }>>('/QQLogin/ComputeLinuxGUID', { mac, machineId });
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async getLinuxMachineInfoBackups () {
|
||||
const data = await serverRequest.post<ServerResponse<string[]>>('/QQLogin/GetLinuxMachineInfoBackups');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async createLinuxMachineInfoBackup () {
|
||||
const data = await serverRequest.post<ServerResponse<{ path: string; }>>('/QQLogin/CreateLinuxMachineInfoBackup');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async restoreLinuxMachineInfoBackup (backupName: string) {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/RestoreLinuxMachineInfoBackup', { backupName });
|
||||
}
|
||||
|
||||
public static async resetLinuxDeviceID () {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/ResetLinuxDeviceID');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NapCat 配置管理
|
||||
// ============================================================
|
||||
|
||||
public static async getNapCatConfig () {
|
||||
const { data } = await serverRequest.get<ServerResponse<NapCatConfig>>(
|
||||
'/NapCatConfig/GetConfig'
|
||||
);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
public static async setNapCatConfig (config: Partial<NapCatConfig>) {
|
||||
await serverRequest.post<ServerResponse<null>>(
|
||||
'/NapCatConfig/SetConfig',
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import SaveButtons from '@/components/button/save_buttons';
|
||||
import PageLoading from '@/components/page_loading';
|
||||
import SwitchCard from '@/components/switch_card';
|
||||
|
||||
import QQManager from '@/controllers/qq_manager';
|
||||
|
||||
interface BypassFormData {
|
||||
hook: boolean;
|
||||
window: boolean;
|
||||
module: boolean;
|
||||
process: boolean;
|
||||
container: boolean;
|
||||
js: boolean;
|
||||
o3HookMode: boolean;
|
||||
}
|
||||
|
||||
|
||||
const BypassConfigCard = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
setValue,
|
||||
} = useForm<BypassFormData>();
|
||||
|
||||
const loadConfig = async (showTip = false) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const config = await QQManager.getNapCatConfig();
|
||||
const bypass = config.bypass ?? {} as Partial<BypassOptions>;
|
||||
setValue('hook', bypass.hook ?? false);
|
||||
setValue('window', bypass.window ?? false);
|
||||
setValue('module', bypass.module ?? false);
|
||||
setValue('process', bypass.process ?? false);
|
||||
setValue('container', bypass.container ?? false);
|
||||
setValue('js', bypass.js ?? false);
|
||||
setValue('o3HookMode', config.o3HookMode === 1);
|
||||
if (showTip) toast.success('刷新成功');
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`获取配置失败: ${msg}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
const { o3HookMode, ...bypass } = data;
|
||||
await QQManager.setNapCatConfig({ bypass, o3HookMode: o3HookMode ? 1 : 0 });
|
||||
toast.success('保存成功,重启后生效');
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`保存失败: ${msg}`);
|
||||
}
|
||||
});
|
||||
|
||||
const onReset = () => {
|
||||
loadConfig();
|
||||
};
|
||||
|
||||
const onRefresh = async () => {
|
||||
await loadConfig(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
if (loading) return <PageLoading loading />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>反检测配置 - NapCat WebUI</title>
|
||||
<div className='flex flex-col gap-1 mb-2'>
|
||||
<h3 className='text-lg font-semibold text-default-700'>反检测开关配置</h3>
|
||||
<p className='text-sm text-default-500'>
|
||||
控制 Napi2Native 模块的各项反检测功能,修改后需重启生效。
|
||||
</p>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='hook'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='Hook'
|
||||
description='hook特征隐藏'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='window'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='Window'
|
||||
description='窗口伪造'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='module'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='Module'
|
||||
description='加载模块隐藏'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='process'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='Process'
|
||||
description='进程反检测'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='container'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='Container'
|
||||
description='容器反检测'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='js'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='JS'
|
||||
description='JS反检测'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='o3HookMode'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='o3HookMode'
|
||||
description='O3 Hook 模式'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<SaveButtons
|
||||
onSubmit={onSubmit}
|
||||
reset={onReset}
|
||||
isSubmitting={isSubmitting}
|
||||
refresh={onRefresh}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BypassConfigCard;
|
||||
@@ -14,6 +14,7 @@ import SSLConfigCard from './ssl';
|
||||
import ThemeConfigCard from './theme';
|
||||
import WebUIConfigCard from './webui';
|
||||
import BackupConfigCard from './backup';
|
||||
import BypassConfigCard from './bypass';
|
||||
|
||||
export interface ConfigPageProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -114,6 +115,11 @@ export default function ConfigPage () {
|
||||
<BackupConfigCard />
|
||||
</ConfigPageItem>
|
||||
</Tab>
|
||||
<Tab title='反检测' key='bypass'>
|
||||
<ConfigPageItem>
|
||||
<BypassConfigCard />
|
||||
</ConfigPageItem>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Input } from '@heroui/input';
|
||||
import { Button } from '@heroui/button';
|
||||
import { Divider } from '@heroui/divider';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import SaveButtons from '@/components/button/save_buttons';
|
||||
import GUIDManager from '@/components/guid_manager';
|
||||
import PageLoading from '@/components/page_loading';
|
||||
|
||||
import QQManager from '@/controllers/qq_manager';
|
||||
@@ -131,6 +133,14 @@ const LoginConfigCard = () => {
|
||||
重启进程将关闭当前 Worker 进程,等待 3 秒后启动新进程
|
||||
</div>
|
||||
</div>
|
||||
<Divider className='mt-6' />
|
||||
<div className='flex-shrink-0 w-full mt-4'>
|
||||
<div className='mb-3 text-sm text-default-600'>设备 GUID 管理</div>
|
||||
<div className='text-xs text-default-400 mb-3'>
|
||||
GUID 是设备登录唯一识别码,存储在 Registry20 文件中。修改后需重启生效。
|
||||
</div>
|
||||
<GUIDManager showRestart={false} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -131,7 +131,7 @@ const WebUIConfigCard = () => {
|
||||
isLoading={isLoadingOptions}
|
||||
className='w-fit'
|
||||
>
|
||||
{!isLoadingOptions && '📥'}
|
||||
{!isLoadingOptions}
|
||||
准备选项
|
||||
</Button>
|
||||
<Button
|
||||
@@ -225,12 +225,12 @@ const WebUIConfigCard = () => {
|
||||
disabled={!registrationOptions}
|
||||
className='w-fit'
|
||||
>
|
||||
🔐 注册Passkey
|
||||
注册Passkey
|
||||
</Button>
|
||||
</div>
|
||||
{registrationOptions && (
|
||||
<div className='text-xs text-green-600'>
|
||||
✅ 注册选项已准备就绪,可以开始注册
|
||||
注册选项已准备就绪,可以开始注册
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -115,39 +115,42 @@ export default function ExtensionPage () {
|
||||
</Button>
|
||||
</div>
|
||||
{extensionPages.length > 0 && (
|
||||
<Tabs
|
||||
aria-label='Extension Pages'
|
||||
className='max-w-full'
|
||||
selectedKey={selectedTab}
|
||||
onSelectionChange={(key) => setSelectedTab(key as string)}
|
||||
classNames={{
|
||||
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
|
||||
className='cursor-pointer hover:underline truncate max-w-[6rem] md:max-w-none'
|
||||
title={`插件:${tab.pluginName}\n点击在新窗口打开`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openInNewWindow(tab.pluginId, tab.path);
|
||||
}}
|
||||
>
|
||||
{tab.title}
|
||||
</span>
|
||||
<span className='text-xs text-default-400 hidden md:inline'>({tab.pluginName})</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<div className='max-w-full overflow-x-auto overflow-y-hidden pb-1 -mb-1'>
|
||||
<Tabs
|
||||
aria-label='Extension Pages'
|
||||
className='min-w-max'
|
||||
selectedKey={selectedTab}
|
||||
onSelectionChange={(key) => setSelectedTab(key as string)}
|
||||
classNames={{
|
||||
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md flex-nowrap',
|
||||
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
|
||||
panel: 'hidden',
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className='shrink-0'
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
{tab.icon && <span>{tab.icon}</span>}
|
||||
<span
|
||||
className='cursor-pointer hover:underline truncate max-w-[6rem] md:max-w-none shrink-0'
|
||||
title={`插件:${tab.pluginName}\n点击在新窗口打开`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openInNewWindow(tab.pluginId, tab.path);
|
||||
}}
|
||||
>
|
||||
{tab.title}
|
||||
</span>
|
||||
<span className='text-xs text-default-400 hidden md:inline shrink-0'>({tab.pluginName})</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -61,30 +61,28 @@ export default function PluginPage () {
|
||||
|
||||
const handleUninstall = async (plugin: PluginItem) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let cleanData = false;
|
||||
dialog.confirm({
|
||||
title: '卸载插件',
|
||||
content: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>确定要卸载插件「{plugin.name}」吗? 此操作不可恢复。</p>
|
||||
<p className="text-small text-default-500">如果插件创建了数据文件,是否一并删除?</p>
|
||||
<p className="text-base text-default-800">确定要卸载插件「<span className="font-semibold text-danger">{plugin.name}</span>」吗? 此操作不可恢复。</p>
|
||||
<div className="mt-2 bg-default-100 dark:bg-default-50/10 p-3 rounded-lg flex flex-col gap-1">
|
||||
<label className="flex items-center gap-2 cursor-pointer w-fit">
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={(e) => { cleanData = e.target.checked; }}
|
||||
className="w-4 h-4 cursor-pointer accent-danger"
|
||||
/>
|
||||
<span className="text-small font-medium text-default-700">同时删除其配置文件</span>
|
||||
</label>
|
||||
<p className="text-xs text-default-500 pl-6 break-all w-full">配置目录: config/plugins/{plugin.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
// This 'dialog' utility might not support returning a value from UI interacting.
|
||||
// We might need to implement a custom confirmation flow if we want a checkbox.
|
||||
// Alternatively, use two buttons? "Uninstall & Clean", "Uninstall Only"?
|
||||
// Standard dialog usually has Confirm/Cancel.
|
||||
// Let's stick to a simpler "Uninstall" and then maybe a second prompt? Or just clean data?
|
||||
// User requested: "Uninstall prompts whether to clean data".
|
||||
// Let's use `window.confirm` for the second step or assume `dialog.confirm` is flexible enough?
|
||||
// I will implement a two-step confirmation or try to modify the dialog hook if visible (not visible here).
|
||||
// Let's use a standard `window.confirm` for the data cleanup question if the custom dialog doesn't support complex return.
|
||||
// Better: Inside onConfirm, ask again?
|
||||
confirmText: '确定卸载',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
// Ask for data cleanup
|
||||
// Since we are in an async callback, we can use another dialog or confirm.
|
||||
// Native confirm is ugly but works reliably for logic:
|
||||
const cleanData = window.confirm(`是否同时清理插件「${plugin.name}」的数据文件?\n点击“确定”清理数据,点击“取消”仅卸载插件。`);
|
||||
|
||||
const loadingToast = toast.loading('卸载中...');
|
||||
try {
|
||||
await PluginManager.uninstallPlugin(plugin.id, cleanData);
|
||||
|
||||
@@ -374,10 +374,10 @@ export default function PluginConfigModal ({ isOpen, onOpenChange, pluginId }: P
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
Close
|
||||
关闭
|
||||
</Button>
|
||||
<Button color="primary" onPress={handleSave} isLoading={saving}>
|
||||
Save
|
||||
保存
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
|
||||
@@ -6,8 +6,11 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { MdSettings } from 'react-icons/md';
|
||||
|
||||
import logo from '@/assets/images/logo.png';
|
||||
import GUIDManager from '@/components/guid_manager';
|
||||
import Modal from '@/components/modal';
|
||||
|
||||
import HoverEffectCard from '@/components/effect_card';
|
||||
import { title } from '@/components/primitives';
|
||||
@@ -174,6 +177,8 @@ export default function QQLoginPage () {
|
||||
}
|
||||
};
|
||||
|
||||
const [showGUIDManager, setShowGUIDManager] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
onUpdateQrCode();
|
||||
@@ -210,7 +215,12 @@ export default function QQLoginPage () {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ThemeSwitch className='absolute right-4 top-4' />
|
||||
<div className='absolute right-4 top-4 flex items-center gap-2'>
|
||||
<Button isIconOnly variant="light" aria-label="Settings" onPress={() => setShowGUIDManager(true)}>
|
||||
<MdSettings size={22} />
|
||||
</Button>
|
||||
<ThemeSwitch />
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody className='flex gap-5 p-10 pt-0'>
|
||||
@@ -266,6 +276,15 @@ export default function QQLoginPage () {
|
||||
</HoverEffectCard>
|
||||
</motion.div>
|
||||
</PureLayout>
|
||||
{showGUIDManager && (
|
||||
<Modal
|
||||
title='设备 GUID 管理'
|
||||
content={<GUIDManager compact showRestart />}
|
||||
size='lg'
|
||||
hideFooter
|
||||
onClose={() => setShowGUIDManager(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
19
packages/napcat-webui-frontend/src/types/napcat_conf.d.ts
vendored
Normal file
19
packages/napcat-webui-frontend/src/types/napcat_conf.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
interface BypassOptions {
|
||||
hook: boolean;
|
||||
window: boolean;
|
||||
module: boolean;
|
||||
process: boolean;
|
||||
container: boolean;
|
||||
js: boolean;
|
||||
}
|
||||
|
||||
interface NapCatConfig {
|
||||
fileLog: boolean;
|
||||
consoleLog: boolean;
|
||||
fileLogLevel: string;
|
||||
consoleLogLevel: string;
|
||||
packetBackend: string;
|
||||
packetServer: string;
|
||||
o3HookMode: number;
|
||||
bypass?: BypassOptions;
|
||||
}
|
||||
Reference in New Issue
Block a user