mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a0d780558e | ||
![]() |
ad56065a4e | ||
![]() |
f5dee80b6e | ||
![]() |
9cc75881b8 | ||
![]() |
593fb13b61 | ||
![]() |
fca90592d6 | ||
![]() |
7539a4129f | ||
![]() |
5402574266 | ||
![]() |
853175aa1a | ||
![]() |
feb84809ec | ||
![]() |
a812c568e4 | ||
![]() |
11db25e355 | ||
![]() |
ecd2fba629 | ||
![]() |
a6763cf5a1 | ||
![]() |
c9e91a9b94 | ||
![]() |
43fb62c5bd | ||
![]() |
cb8727d487 | ||
![]() |
a94e03e2fd | ||
![]() |
425c3c6432 | ||
![]() |
89b9610016 | ||
![]() |
62fe88f868 | ||
![]() |
11a7f5fade | ||
![]() |
fbde997f7c | ||
![]() |
26734a35ef | ||
![]() |
715c4ac534 |
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.5.1",
|
||||
"version": "4.5.7",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -32,6 +32,7 @@
|
||||
"@heroui/pagination": "^2.2.9",
|
||||
"@heroui/popover": "2.3.10",
|
||||
"@heroui/select": "2.4.10",
|
||||
"@heroui/skeleton": "^2.2.6",
|
||||
"@heroui/slider": "2.4.8",
|
||||
"@heroui/snippet": "2.2.11",
|
||||
"@heroui/spinner": "2.2.7",
|
||||
|
@@ -16,6 +16,16 @@ import store from '@/store'
|
||||
const WebLoginPage = lazy(() => import('@/pages/web_login'))
|
||||
const IndexPage = lazy(() => import('@/pages/index'))
|
||||
const QQLoginPage = lazy(() => import('@/pages/qq_login'))
|
||||
const DashboardIndexPage = lazy(() => import('@/pages/dashboard'))
|
||||
const AboutPage = lazy(() => import('@/pages/dashboard/about'))
|
||||
const ConfigPage = lazy(() => import('@/pages/dashboard/config'))
|
||||
const DebugPage = lazy(() => import('@/pages/dashboard/debug'))
|
||||
const HttpDebug = lazy(() => import('@/pages/dashboard/debug/http'))
|
||||
const WSDebug = lazy(() => import('@/pages/dashboard/debug/websocket'))
|
||||
const FileManagerPage = lazy(() => import('@/pages/dashboard/file_manager'))
|
||||
const LogsPage = lazy(() => import('@/pages/dashboard/logs'))
|
||||
const NetworkPage = lazy(() => import('@/pages/dashboard/network'))
|
||||
const TerminalPage = lazy(() => import('@/pages/dashboard/terminal'))
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -58,9 +68,21 @@ function AuthChecker({ children }: { children: React.ReactNode }) {
|
||||
function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<IndexPage />} path="/*" />
|
||||
<Route element={<QQLoginPage />} path="/qq_login" />
|
||||
<Route element={<WebLoginPage />} path="/web_login" />
|
||||
<Route path="/" element={<IndexPage />}>
|
||||
<Route index element={<DashboardIndexPage />} />
|
||||
<Route path="network" element={<NetworkPage />} />
|
||||
<Route path="config" element={<ConfigPage />} />
|
||||
<Route path="logs" element={<LogsPage />} />
|
||||
<Route path="debug" element={<DebugPage />}>
|
||||
<Route path="ws" element={<WSDebug />} />
|
||||
<Route path="http" element={<HttpDebug />} />
|
||||
</Route>
|
||||
<Route path="file_manager" element={<FileManagerPage />} />
|
||||
<Route path="terminal" element={<TerminalPage />} />
|
||||
<Route path="about" element={<AboutPage />} />
|
||||
</Route>
|
||||
<Route path="/qq_login" element={<QQLoginPage />} />
|
||||
<Route path="/web_login" element={<WebLoginPage />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
@@ -231,7 +231,7 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
||||
: 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md'
|
||||
)}
|
||||
variant="solid"
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
onPress={() => setIsCollapsed(!isCollapsed)}
|
||||
>
|
||||
|
@@ -33,7 +33,7 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
|
||||
>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
startContent={<IoAddCircleOutline className="text-2xl" />}
|
||||
>
|
||||
新建
|
||||
|
@@ -27,7 +27,7 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
|
||||
取消更改
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
isLoading={isSubmitting}
|
||||
onPress={() => onSubmit()}
|
||||
>
|
||||
|
@@ -110,7 +110,7 @@ const AudioInsert = () => {
|
||||
<Tooltip content="发送音频">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<IoMic className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -120,7 +120,7 @@ const AudioInsert = () => {
|
||||
<Tooltip content="上传音频">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -137,7 +137,7 @@ const AudioInsert = () => {
|
||||
<PopoverTrigger tooltip="输入音频地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -154,7 +154,7 @@ const AudioInsert = () => {
|
||||
placeholder="请输入音频地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
@@ -177,7 +177,7 @@ const AudioInsert = () => {
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -190,7 +190,7 @@ const AudioInsert = () => {
|
||||
<PopoverContent className="flex-col gap-2 p-4">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color={isRecording ? 'danger' : 'danger'}
|
||||
color={isRecording ? 'primary' : 'primary'}
|
||||
variant="flat"
|
||||
onPress={isRecording ? stopRecording : startRecording}
|
||||
>
|
||||
@@ -198,7 +198,7 @@ const AudioInsert = () => {
|
||||
</Button>
|
||||
{showPreview && audioPreview && (
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={handleShowPreview}
|
||||
>
|
||||
@@ -212,7 +212,7 @@ const AudioInsert = () => {
|
||||
className={clsx(
|
||||
'w-4 h-4 rounded-full',
|
||||
isRecording
|
||||
? 'animate-pulse bg-danger-400'
|
||||
? 'animate-pulse bg-primary-400'
|
||||
: 'bg-success-400'
|
||||
)}
|
||||
></span>
|
||||
|
@@ -10,7 +10,7 @@ const DiceInsert = () => {
|
||||
return (
|
||||
<Tooltip content="发送骰子">
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -55,7 +55,7 @@ const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
|
||||
<Tooltip content="插入表情">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<MdEmojiEmotions className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -65,7 +65,7 @@ const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
|
||||
{visibleEmojis.map((emoji) => (
|
||||
<Button
|
||||
key={emoji.id}
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -35,7 +35,7 @@ const FileInsert = () => {
|
||||
<Tooltip content="发送文件">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<FaFolder className="text-lg" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -45,7 +45,7 @@ const FileInsert = () => {
|
||||
<Tooltip content="上传文件">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -62,7 +62,7 @@ const FileInsert = () => {
|
||||
<PopoverTrigger tooltip="输入文件地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -79,7 +79,7 @@ const FileInsert = () => {
|
||||
placeholder="请输入文件地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -23,7 +23,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
<Tooltip content="插入图片">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<MdImage className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -33,7 +33,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
<Tooltip content="上传图片">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -50,7 +50,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
<PopoverTrigger tooltip="输入图片地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -67,7 +67,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
placeholder="请输入图片地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -80,7 +80,7 @@ const MusicInsert = () => {
|
||||
<Tooltip content="发送音乐">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<IoMusicalNotes className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -132,7 +132,7 @@ const MusicInsert = () => {
|
||||
<Button
|
||||
fullWidth
|
||||
size="lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
onPress={() => {
|
||||
@@ -236,7 +236,7 @@ const MusicInsert = () => {
|
||||
<Button
|
||||
fullWidth
|
||||
size="lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
type="submit"
|
||||
|
@@ -19,7 +19,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
|
||||
<Tooltip content="回复消息">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<BsChatQuoteFill className="text-lg" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -38,7 +38,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
|
@@ -10,7 +10,7 @@ const RPSInsert = () => {
|
||||
return (
|
||||
<Tooltip content="发送猜拳">
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -35,7 +35,7 @@ const VideoInsert = () => {
|
||||
<Tooltip content="发送视频">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<IoVideocam className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -45,7 +45,7 @@ const VideoInsert = () => {
|
||||
<Tooltip content="上传视频">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -62,7 +62,7 @@ const VideoInsert = () => {
|
||||
<PopoverTrigger tooltip="输入视频地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -79,7 +79,7 @@ const VideoInsert = () => {
|
||||
placeholder="请输入视频地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -190,7 +190,7 @@ const ChatInput = () => {
|
||||
<DiceInsert />
|
||||
<RPSInsert />
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
onPress={() => {
|
||||
const messages = getChatMessage()
|
||||
showStructuredMessage(messages)
|
||||
|
@@ -15,7 +15,7 @@ export default function ChatInputModal() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onPress={onOpen} color="danger" radius="full" variant="flat">
|
||||
<Button onPress={onOpen} color="primary" radius="full" variant="flat">
|
||||
构造聊天消息
|
||||
</Button>
|
||||
<Modal
|
||||
@@ -36,7 +36,7 @@ export default function ChatInputModal() {
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" onPress={onClose} variant="flat">
|
||||
<Button color="primary" onPress={onClose} variant="flat">
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -78,7 +78,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
|
||||
{debug ? '关闭调试' : '开启调试'}
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
startContent={<MdDeleteForever />}
|
||||
onPress={handleDelete}
|
||||
>
|
||||
|
@@ -19,7 +19,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
||||
className={clsx(
|
||||
'bg-opacity-60 shadow-sm md:rounded-3xl',
|
||||
size === 'md'
|
||||
? 'col-span-8 md:col-span-2 bg-danger-50 shadow-danger-100'
|
||||
? 'col-span-8 md:col-span-2 bg-primary-50 shadow-primary-100'
|
||||
: 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200'
|
||||
)}
|
||||
shadow="sm"
|
||||
|
@@ -33,7 +33,7 @@ export default function CreateFileModal({
|
||||
<ModalHeader>新建</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex flex-col gap-4">
|
||||
<ButtonGroup color="danger">
|
||||
<ButtonGroup color="primary">
|
||||
<Button
|
||||
variant={fileType === 'file' ? 'solid' : 'flat'}
|
||||
onPress={() => onTypeChange('file')}
|
||||
@@ -51,10 +51,10 @@ export default function CreateFileModal({
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onCreate}>
|
||||
<Button color="primary" onPress={onCreate}>
|
||||
创建
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -81,10 +81,10 @@ export default function FileEditModal({
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onSave}>
|
||||
<Button color="primary" onPress={onSave}>
|
||||
保存
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { useRequest } from 'ahooks'
|
||||
import path from 'path-browserify'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import FileManager from '@/controllers/file_manager'
|
||||
|
||||
@@ -33,6 +34,7 @@ export default function FilePreviewModal({
|
||||
async () => FileManager.downloadToURL(filePath),
|
||||
{
|
||||
refreshDeps: [filePath],
|
||||
manual: true,
|
||||
refreshDepsAction: () => {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
if (!filePath || !supportedPreviewExts.includes(ext)) {
|
||||
@@ -43,6 +45,12 @@ export default function FilePreviewModal({
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (filePath) {
|
||||
run()
|
||||
}
|
||||
}, [filePath])
|
||||
|
||||
let contentElement = null
|
||||
if (!supportedPreviewExts.includes(ext)) {
|
||||
contentElement = <div>暂不支持预览此文件类型</div>
|
||||
@@ -74,7 +82,7 @@ export default function FilePreviewModal({
|
||||
{contentElement}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -116,7 +116,7 @@ export default function FileTable({
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="danger"
|
||||
color="primary"
|
||||
page={page}
|
||||
total={pages}
|
||||
onChange={(page) => setPage(page)}
|
||||
@@ -195,7 +195,7 @@ export default function FileTable({
|
||||
<ButtonGroup size="sm">
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={() => onRenameRequest(file.name)}
|
||||
>
|
||||
@@ -203,7 +203,7 @@ export default function FileTable({
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={() => onMoveRequest(file.name)}
|
||||
>
|
||||
@@ -211,7 +211,7 @@ export default function FileTable({
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={() => onCopyPath(file.name)}
|
||||
>
|
||||
@@ -219,7 +219,7 @@ export default function FileTable({
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={() => onDownload(filePath)}
|
||||
>
|
||||
@@ -227,7 +227,7 @@ export default function FileTable({
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={() => onDelete(filePath)}
|
||||
>
|
||||
|
@@ -33,6 +33,7 @@ export default function ImageNameButton({
|
||||
async () => FileManager.downloadToURL(filePath),
|
||||
{
|
||||
refreshDeps: [filePath],
|
||||
manual: true,
|
||||
refreshDepsAction: () => {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
if (!filePath || !imageExts.includes(ext)) {
|
||||
@@ -52,6 +53,12 @@ export default function ImageNameButton({
|
||||
}
|
||||
}, [data, name, onAddPreview])
|
||||
|
||||
useEffect(() => {
|
||||
if (filePath) {
|
||||
run()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="light"
|
||||
@@ -63,7 +70,12 @@ export default function ImageNameButton({
|
||||
) : loading || !data ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
<Image src={data} alt={name} className="w-8 h-8" radius="sm" />
|
||||
<Image
|
||||
src={data}
|
||||
alt={name}
|
||||
className="w-8 h-8 flex-shrink-0"
|
||||
radius="sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@@ -86,13 +86,13 @@ function DirectoryTree({
|
||||
onPress={handleClick}
|
||||
className="py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md"
|
||||
size="sm"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant={variant}
|
||||
startContent={
|
||||
<div
|
||||
className={clsx(
|
||||
'rounded-md',
|
||||
isSeleted ? 'bg-danger-600' : 'bg-danger-50'
|
||||
isSeleted ? 'bg-primary-600' : 'bg-primary-50'
|
||||
)}
|
||||
>
|
||||
{expanded ? <IoRemove /> : <IoAdd />}
|
||||
@@ -105,7 +105,7 @@ function DirectoryTree({
|
||||
<div>
|
||||
{loading ? (
|
||||
<div className="flex py-1 px-8">
|
||||
<Spinner size="sm" color="danger" />
|
||||
<Spinner size="sm" color="primary" />
|
||||
</div>
|
||||
) : (
|
||||
dirs.map((dirName) => {
|
||||
@@ -155,10 +155,10 @@ export default function MoveModal({
|
||||
<p className="text-sm text-default-500">移动项:{selectionInfo}</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onMove}>
|
||||
<Button color="primary" onPress={onMove}>
|
||||
确定
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -31,10 +31,10 @@ export default function RenameModal({
|
||||
<Input label="新名称" value={newFileName} onChange={onNameChange} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onRename}>
|
||||
<Button color="primary" onPress={onRename}>
|
||||
确定
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -33,7 +33,7 @@ export default function Hitokoto() {
|
||||
<div className="relative">
|
||||
{loading && <PageLoading />}
|
||||
{error ? (
|
||||
<div className="text-danger-400">一言加载失败:{error.message}</div>
|
||||
<div className="text-primary-400">一言加载失败:{error.message}</div>
|
||||
) : (
|
||||
<>
|
||||
<div>{data?.hitokoto}</div>
|
||||
@@ -52,7 +52,7 @@ export default function Hitokoto() {
|
||||
isLoading={loading}
|
||||
isIconOnly
|
||||
radius="full"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
>
|
||||
<IoRefresh />
|
||||
|
@@ -34,7 +34,7 @@ export default function HoverTiltedCard({
|
||||
rotateAmplitude = 14,
|
||||
showTooltip = false,
|
||||
overlayContent = (
|
||||
<div className="text-center mt-6 px-4 py-0.5 shadow-lg rounded-full bg-danger-600 text-default-100 bg-opacity-80">
|
||||
<div className="text-center mt-6 px-4 py-0.5 shadow-lg rounded-full bg-primary-600 text-default-100 bg-opacity-80">
|
||||
NapCat
|
||||
</div>
|
||||
),
|
||||
|
@@ -43,7 +43,7 @@ const ImageInput: React.FC<ImageInputProps> = ({ onChange, value, label }) => {
|
||||
onChange('')
|
||||
if (inputRef.current) inputRef.current.value = ''
|
||||
}}
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
>
|
||||
|
@@ -16,13 +16,13 @@ const logLevelColor: {
|
||||
| 'secondary'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'primary'
|
||||
} = {
|
||||
[LogLevel.DEBUG]: 'default',
|
||||
[LogLevel.INFO]: 'primary',
|
||||
[LogLevel.WARN]: 'warning',
|
||||
[LogLevel.ERROR]: 'danger',
|
||||
[LogLevel.FATAL]: 'danger'
|
||||
[LogLevel.ERROR]: 'primary',
|
||||
[LogLevel.FATAL]: 'primary'
|
||||
}
|
||||
const LogLevelSelect = (props: LogLevelSelectProps) => {
|
||||
const { selectedKeys, onSelectionChange } = props
|
||||
|
@@ -65,7 +65,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
||||
<ModalFooter>
|
||||
{showCancel && (
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
onCancel?.()
|
||||
@@ -76,7 +76,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
onPress={() => {
|
||||
onConfirm?.()
|
||||
nativeClose()
|
||||
|
@@ -28,7 +28,7 @@ import type {
|
||||
|
||||
function displayData(data: number, loading: boolean, error?: Error) {
|
||||
if (error) {
|
||||
return <MdError className="text-danger-400" />
|
||||
return <MdError className="text-primary-400" />
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
@@ -175,7 +175,7 @@ export default function NapCatRepoInfo() {
|
||||
className="group h-auto py-3"
|
||||
endContent={
|
||||
releaseError ? (
|
||||
<MdError className="text-danger-400" />
|
||||
<MdError className="text-primary-400" />
|
||||
) : releaseLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
@@ -229,7 +229,7 @@ export default function NapCatRepoInfo() {
|
||||
</span>
|
||||
}
|
||||
startContent={
|
||||
<IconWrapper className="bg-danger/10 text-danger dark:text-danger-500">
|
||||
<IconWrapper className="bg-primary/10 text-primary dark:text-primary-500">
|
||||
<BookIcon />
|
||||
</IconWrapper>
|
||||
}
|
||||
|
@@ -150,7 +150,7 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
isDisabled={formState.isSubmitting}
|
||||
variant="light"
|
||||
onPress={onClose}
|
||||
|
@@ -20,7 +20,7 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
|
||||
enable: false,
|
||||
name: '',
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
port: 3001,
|
||||
reportSelfMessage: false,
|
||||
enableForcePushEvent: true,
|
||||
messagePostFormat: 'array',
|
||||
|
@@ -91,7 +91,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
|
||||
return (
|
||||
<section className="p-4 pt-14 rounded-lg shadow-md">
|
||||
<h1 className="text-2xl font-bold mb-4 flex items-center gap-1 text-danger-400">
|
||||
<h1 className="text-2xl font-bold mb-4 flex items-center gap-1 text-primary-400">
|
||||
<PiCatDuotone />
|
||||
{data.description}
|
||||
</h1>
|
||||
@@ -125,7 +125,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
/>
|
||||
<Button
|
||||
onPress={sendRequest}
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="lg"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
|
@@ -27,7 +27,7 @@ const SchemaType = ({
|
||||
name = '固定值'
|
||||
break
|
||||
}
|
||||
let chipColor: 'primary' | 'success' | 'danger' | 'warning' | 'secondary' =
|
||||
let chipColor: 'primary' | 'success' | 'primary' | 'warning' | 'secondary' =
|
||||
'primary'
|
||||
switch (type) {
|
||||
case 'enum':
|
||||
@@ -37,7 +37,7 @@ const SchemaType = ({
|
||||
chipColor = 'secondary'
|
||||
break
|
||||
case 'array':
|
||||
chipColor = 'danger'
|
||||
chipColor = 'primary'
|
||||
break
|
||||
case 'object':
|
||||
chipColor = 'success'
|
||||
|
@@ -33,11 +33,11 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
>
|
||||
<div className="w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0">
|
||||
<Input
|
||||
className="sticky top-0 z-10 text-danger-600"
|
||||
className="sticky top-0 z-10 text-primary-600"
|
||||
classNames={{
|
||||
inputWrapper:
|
||||
'bg-opacity-30 bg-danger-50 backdrop-blur-sm border border-danger-300 mb-2',
|
||||
input: 'bg-transparent !text-danger-400 !placeholder-danger-400'
|
||||
'bg-opacity-30 bg-primary-50 backdrop-blur-sm border border-primary-300 mb-2',
|
||||
input: 'bg-transparent !text-primary-400 !placeholder-primary-400'
|
||||
}}
|
||||
radius="full"
|
||||
placeholder="搜索 API"
|
||||
@@ -51,7 +51,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
key={apiName}
|
||||
shadow="none"
|
||||
className={clsx(
|
||||
'w-full border border-danger-100 rounded-lg mb-1 bg-opacity-30 backdrop-blur-sm text-danger-400',
|
||||
'w-full border border-primary-100 rounded-lg mb-1 bg-opacity-30 backdrop-blur-sm text-primary-400',
|
||||
{
|
||||
hidden: !(
|
||||
apiName.includes(searchValue) ||
|
||||
@@ -59,7 +59,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
)
|
||||
},
|
||||
{
|
||||
'!bg-opacity-40 border border-danger-400 bg-danger-50 text-danger-600':
|
||||
'!bg-opacity-40 border border-primary-400 bg-primary-50 text-primary-600':
|
||||
apiName === selectedApi
|
||||
}
|
||||
)}
|
||||
@@ -69,8 +69,8 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
<CardBody>
|
||||
<h2 className="font-bold">{api.description}</h2>
|
||||
<div
|
||||
className={clsx('text-sm text-danger-200', {
|
||||
'!text-danger-400': apiName === selectedApi
|
||||
className={clsx('text-sm text-primary-200', {
|
||||
'!text-primary-400': apiName === selectedApi
|
||||
})}
|
||||
>
|
||||
{apiName}
|
||||
|
@@ -109,7 +109,7 @@ const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
|
@@ -30,7 +30,7 @@ const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
className="text-medium"
|
||||
|
@@ -43,7 +43,7 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onPress={onOpen} color="danger" radius="full" variant="flat">
|
||||
<Button onPress={onOpen} color="primary" radius="full" variant="flat">
|
||||
构造请求
|
||||
</Button>
|
||||
<Modal
|
||||
@@ -75,11 +75,11 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
||||
<ModalFooter>
|
||||
<ChatInputModal />
|
||||
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
onPress={() => handleSendMessage(onClose)}
|
||||
>
|
||||
发送
|
||||
|
@@ -10,7 +10,7 @@ function StatusTag({
|
||||
color
|
||||
}: {
|
||||
title: string
|
||||
color: 'success' | 'danger' | 'warning'
|
||||
color: 'success' | 'primary' | 'warning'
|
||||
}) {
|
||||
const textClassName = `text-${color} text-sm`
|
||||
const bgClassName = `bg-${color}`
|
||||
@@ -27,7 +27,7 @@ export default function WSStatus({ state }: WSStatusProps) {
|
||||
return <StatusTag title="已连接" color="success" />
|
||||
}
|
||||
if (state === ReadyState.CLOSED) {
|
||||
return <StatusTag title="已关闭" color="danger" />
|
||||
return <StatusTag title="已关闭" color="primary" />
|
||||
}
|
||||
if (state === ReadyState.CONNECTING) {
|
||||
return <StatusTag title="连接中" color="warning" />
|
||||
|
@@ -16,7 +16,7 @@ export interface QQInfoCardProps {
|
||||
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||
return (
|
||||
<Card
|
||||
className="relative bg-danger-100 bg-opacity-60 overflow-hidden flex-shrink-0 shadow-md shadow-danger-300 dark:shadow-danger-50"
|
||||
className="relative bg-primary-100 bg-opacity-60 overflow-hidden flex-shrink-0 shadow-md shadow-primary-300 dark:shadow-primary-50"
|
||||
shadow="none"
|
||||
radius="lg"
|
||||
>
|
||||
@@ -30,7 +30,7 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||
</CardBody>
|
||||
) : (
|
||||
<CardBody className="flex-row items-center gap-2 overflow-hidden relative">
|
||||
<div className="absolute right-0 bottom-0 text-5xl text-danger-400">
|
||||
<div className="absolute right-0 bottom-0 text-5xl text-primary-400">
|
||||
<BsTencentQq />
|
||||
</div>
|
||||
<div className="relative flex-shrink-0 z-10">
|
||||
@@ -43,14 +43,14 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'w-4 h-4 rounded-full absolute right-0.5 bottom-0 border-2 border-danger-100 z-10',
|
||||
'w-4 h-4 rounded-full absolute right-0.5 bottom-0 border-2 border-primary-100 z-10',
|
||||
data?.online ? 'bg-green-500' : 'bg-gray-500'
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex-col justify-center">
|
||||
<div className="text-lg truncate">{data?.nick}</div>
|
||||
<div className="text-danger-500 text-sm">{data?.uin}</div>
|
||||
<div className="text-primary-500 text-sm">{data?.uin}</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
)}
|
||||
|
@@ -11,7 +11,7 @@ const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode }) => {
|
||||
<div className="bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden">
|
||||
{!qrcode && (
|
||||
<div className="absolute left-2 top-2 right-2 bottom-2 bg-white bg-opacity-50 backdrop-blur flex items-center justify-center">
|
||||
<Spinner color="danger" />
|
||||
<Spinner color="primary" />
|
||||
</div>
|
||||
)}
|
||||
<QRCodeSVG size={180} value={qrcode} />
|
||||
|
265
napcat.webui/src/components/rotating_text.tsx
Normal file
265
napcat.webui/src/components/rotating_text.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
import {
|
||||
AnimatePresence,
|
||||
HTMLMotionProps,
|
||||
TargetAndTransition,
|
||||
Transition,
|
||||
motion
|
||||
} from 'motion/react'
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react'
|
||||
|
||||
function cn(...classes: (string | undefined | null | boolean)[]): string {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export interface RotatingTextRef {
|
||||
next: () => void
|
||||
previous: () => void
|
||||
jumpTo: (index: number) => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export interface RotatingTextProps
|
||||
extends Omit<
|
||||
HTMLMotionProps<'span'>,
|
||||
'children' | 'transition' | 'initial' | 'animate' | 'exit'
|
||||
> {
|
||||
texts: string[]
|
||||
transition?: Transition
|
||||
initial?: TargetAndTransition
|
||||
animate?: TargetAndTransition
|
||||
exit?: TargetAndTransition
|
||||
animatePresenceMode?: 'sync' | 'wait'
|
||||
animatePresenceInitial?: boolean
|
||||
rotationInterval?: number
|
||||
staggerDuration?: number
|
||||
staggerFrom?: 'first' | 'last' | 'center' | 'random' | number
|
||||
loop?: boolean
|
||||
auto?: boolean
|
||||
splitBy?: string
|
||||
onNext?: (index: number) => void
|
||||
mainClassName?: string
|
||||
splitLevelClassName?: string
|
||||
elementLevelClassName?: string
|
||||
}
|
||||
|
||||
const RotatingText = forwardRef<RotatingTextRef, RotatingTextProps>(
|
||||
(
|
||||
{
|
||||
texts,
|
||||
transition = { type: 'spring', damping: 25, stiffness: 300 },
|
||||
initial = { y: '100%', opacity: 0 },
|
||||
animate = { y: 0, opacity: 1 },
|
||||
exit = { y: '-120%', opacity: 0 },
|
||||
animatePresenceMode = 'wait',
|
||||
animatePresenceInitial = false,
|
||||
rotationInterval = 2000,
|
||||
staggerDuration = 0,
|
||||
staggerFrom = 'first',
|
||||
loop = true,
|
||||
auto = true,
|
||||
splitBy = 'characters',
|
||||
onNext,
|
||||
mainClassName,
|
||||
splitLevelClassName,
|
||||
elementLevelClassName,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [currentTextIndex, setCurrentTextIndex] = useState<number>(0)
|
||||
|
||||
const splitIntoCharacters = (text: string): string[] => {
|
||||
return Array.from(text)
|
||||
}
|
||||
|
||||
const elements = useMemo(() => {
|
||||
const currentText: string = texts[currentTextIndex]
|
||||
if (splitBy === 'characters') {
|
||||
const words = currentText.split(' ')
|
||||
return words.map((word, i) => ({
|
||||
characters: splitIntoCharacters(word),
|
||||
needsSpace: i !== words.length - 1
|
||||
}))
|
||||
}
|
||||
if (splitBy === 'words') {
|
||||
return currentText.split(' ').map((word, i, arr) => ({
|
||||
characters: [word],
|
||||
needsSpace: i !== arr.length - 1
|
||||
}))
|
||||
}
|
||||
if (splitBy === 'lines') {
|
||||
return currentText.split('\n').map((line, i, arr) => ({
|
||||
characters: [line],
|
||||
needsSpace: i !== arr.length - 1
|
||||
}))
|
||||
}
|
||||
|
||||
return currentText.split(splitBy).map((part, i, arr) => ({
|
||||
characters: [part],
|
||||
needsSpace: i !== arr.length - 1
|
||||
}))
|
||||
}, [texts, currentTextIndex, splitBy])
|
||||
|
||||
const getStaggerDelay = useCallback(
|
||||
(index: number, totalChars: number): number => {
|
||||
const total = totalChars
|
||||
if (staggerFrom === 'first') return index * staggerDuration
|
||||
if (staggerFrom === 'last') return (total - 1 - index) * staggerDuration
|
||||
if (staggerFrom === 'center') {
|
||||
const center = Math.floor(total / 2)
|
||||
return Math.abs(center - index) * staggerDuration
|
||||
}
|
||||
if (staggerFrom === 'random') {
|
||||
const randomIndex = Math.floor(Math.random() * total)
|
||||
return Math.abs(randomIndex - index) * staggerDuration
|
||||
}
|
||||
return Math.abs((staggerFrom as number) - index) * staggerDuration
|
||||
},
|
||||
[staggerFrom, staggerDuration]
|
||||
)
|
||||
|
||||
const handleIndexChange = useCallback(
|
||||
(newIndex: number) => {
|
||||
setCurrentTextIndex(newIndex)
|
||||
if (onNext) onNext(newIndex)
|
||||
},
|
||||
[onNext]
|
||||
)
|
||||
|
||||
const next = useCallback(() => {
|
||||
const nextIndex =
|
||||
currentTextIndex === texts.length - 1
|
||||
? loop
|
||||
? 0
|
||||
: currentTextIndex
|
||||
: currentTextIndex + 1
|
||||
if (nextIndex !== currentTextIndex) {
|
||||
handleIndexChange(nextIndex)
|
||||
}
|
||||
}, [currentTextIndex, texts.length, loop, handleIndexChange])
|
||||
|
||||
const previous = useCallback(() => {
|
||||
const prevIndex =
|
||||
currentTextIndex === 0
|
||||
? loop
|
||||
? texts.length - 1
|
||||
: currentTextIndex
|
||||
: currentTextIndex - 1
|
||||
if (prevIndex !== currentTextIndex) {
|
||||
handleIndexChange(prevIndex)
|
||||
}
|
||||
}, [currentTextIndex, texts.length, loop, handleIndexChange])
|
||||
|
||||
const jumpTo = useCallback(
|
||||
(index: number) => {
|
||||
const validIndex = Math.max(0, Math.min(index, texts.length - 1))
|
||||
if (validIndex !== currentTextIndex) {
|
||||
handleIndexChange(validIndex)
|
||||
}
|
||||
},
|
||||
[texts.length, currentTextIndex, handleIndexChange]
|
||||
)
|
||||
|
||||
const reset = useCallback(() => {
|
||||
if (currentTextIndex !== 0) {
|
||||
handleIndexChange(0)
|
||||
}
|
||||
}, [currentTextIndex, handleIndexChange])
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
next,
|
||||
previous,
|
||||
jumpTo,
|
||||
reset
|
||||
}),
|
||||
[next, previous, jumpTo, reset]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!auto) return
|
||||
const intervalId = setInterval(next, rotationInterval)
|
||||
return () => clearInterval(intervalId)
|
||||
}, [next, rotationInterval, auto])
|
||||
|
||||
return (
|
||||
<motion.span
|
||||
className={cn(
|
||||
'flex flex-wrap whitespace-pre-wrap relative',
|
||||
mainClassName
|
||||
)}
|
||||
{...rest}
|
||||
layout
|
||||
transition={transition}
|
||||
>
|
||||
<span className="sr-only">{texts[currentTextIndex]}</span>
|
||||
<AnimatePresence
|
||||
mode={animatePresenceMode}
|
||||
initial={animatePresenceInitial}
|
||||
>
|
||||
<motion.div
|
||||
key={currentTextIndex}
|
||||
className={cn(
|
||||
splitBy === 'lines'
|
||||
? 'flex flex-col w-full'
|
||||
: 'flex flex-wrap whitespace-pre-wrap relative'
|
||||
)}
|
||||
layout
|
||||
aria-hidden="true"
|
||||
initial={initial as HTMLMotionProps<'div'>['initial']}
|
||||
animate={animate as HTMLMotionProps<'div'>['animate']}
|
||||
exit={exit as HTMLMotionProps<'div'>['exit']}
|
||||
>
|
||||
{elements.map((wordObj, wordIndex, array) => {
|
||||
const previousCharsCount = array
|
||||
.slice(0, wordIndex)
|
||||
.reduce((sum, word) => sum + word.characters.length, 0)
|
||||
return (
|
||||
<span
|
||||
key={wordIndex}
|
||||
className={cn('inline-flex', splitLevelClassName)}
|
||||
>
|
||||
{wordObj.characters.map((char, charIndex) => (
|
||||
<motion.span
|
||||
key={charIndex}
|
||||
initial={initial as HTMLMotionProps<'span'>['initial']}
|
||||
animate={animate as HTMLMotionProps<'span'>['animate']}
|
||||
exit={exit as HTMLMotionProps<'span'>['exit']}
|
||||
transition={{
|
||||
...transition,
|
||||
delay: getStaggerDelay(
|
||||
previousCharsCount + charIndex,
|
||||
array.reduce(
|
||||
(sum, word) => sum + word.characters.length,
|
||||
0
|
||||
)
|
||||
)
|
||||
}}
|
||||
className={cn('inline-block', elementLevelClassName)}
|
||||
>
|
||||
{char}
|
||||
</motion.span>
|
||||
))}
|
||||
{wordObj.needsSpace && (
|
||||
<span className="whitespace-pre"> </span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.span>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
RotatingText.displayName = 'RotatingText'
|
||||
export default RotatingText
|
@@ -63,7 +63,7 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
||||
<div className="mt-auto mb-10 md:mb-0">
|
||||
<Button
|
||||
className="w-full"
|
||||
color="danger"
|
||||
color="primary"
|
||||
radius="full"
|
||||
variant="light"
|
||||
onPress={toggleTheme}
|
||||
@@ -75,7 +75,7 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full mb-2"
|
||||
color="danger"
|
||||
color="primary"
|
||||
radius="full"
|
||||
variant="light"
|
||||
onPress={onRevokeAuth}
|
||||
|
@@ -55,7 +55,7 @@ const renderItems = (items: MenuItem[], children = false) => {
|
||||
isActive && 'bg-opacity-60',
|
||||
b64img && 'backdrop-blur-md text-white'
|
||||
)}
|
||||
color="danger"
|
||||
color="primary"
|
||||
endContent={
|
||||
canOpen ? (
|
||||
// div实现箭头V效果
|
||||
@@ -63,7 +63,9 @@ const renderItems = (items: MenuItem[], children = false) => {
|
||||
className={clsx(
|
||||
'ml-auto relative w-3 h-3 transition-transform',
|
||||
open && 'transform rotate-180',
|
||||
isActive ? 'text-danger-500' : 'text-red-300 dark:text-white',
|
||||
isActive
|
||||
? 'text-primary-500'
|
||||
: 'text-red-300 dark:text-white',
|
||||
'before:rounded-full',
|
||||
'before:content-[""]',
|
||||
'before:block',
|
||||
@@ -95,7 +97,7 @@ const renderItems = (items: MenuItem[], children = false) => {
|
||||
className={clsx(
|
||||
'w-3 h-1.5 rounded-full ml-auto shadow-lg',
|
||||
isActive
|
||||
? 'bg-danger-500 animate-spinner-ease-spin'
|
||||
? 'bg-primary-500 animate-spinner-ease-spin'
|
||||
: 'bg-red-300 dark:bg-white'
|
||||
)}
|
||||
/>
|
||||
|
@@ -4,6 +4,8 @@ import { Chip } from '@heroui/chip'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { Tooltip } from '@heroui/tooltip'
|
||||
import { useRequest } from 'ahooks'
|
||||
import { useEffect } from 'react'
|
||||
import { BsStars } from 'react-icons/bs'
|
||||
import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6'
|
||||
import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io'
|
||||
import { RiMacFill } from 'react-icons/ri'
|
||||
@@ -16,7 +18,6 @@ import { compareVersion } from '@/utils/version'
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
import { GithubRelease } from '@/types/github'
|
||||
|
||||
import packageJson from '../../package.json'
|
||||
import TailwindMarkdown from './tailwind_markdown'
|
||||
|
||||
export interface SystemInfoItemProps {
|
||||
@@ -33,10 +34,10 @@ const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
|
||||
endContent
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex text-sm gap-1 p-2 items-center shadow-sm shadow-danger-50 dark:shadow-danger-100 rounded text-danger-400">
|
||||
<div className="flex text-sm gap-1 p-2 items-center shadow-sm shadow-primary-50 dark:shadow-primary-100 rounded text-primary-400">
|
||||
{icon}
|
||||
<div className="w-24">{title}</div>
|
||||
<div className="text-danger-200">{value}</div>
|
||||
<div className="text-primary-200">{value}</div>
|
||||
<div className="ml-auto">{endContent}</div>
|
||||
</div>
|
||||
)
|
||||
@@ -61,7 +62,7 @@ const NewVersionTip = (props: NewVersionTipProps) => {
|
||||
<Button
|
||||
isIconOnly
|
||||
radius="full"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="shadow"
|
||||
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
||||
onPress={() => {
|
||||
@@ -98,12 +99,48 @@ const NewVersionTip = (props: NewVersionTipProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const AISummaryComponent = () => {
|
||||
const {
|
||||
data: aiSummaryData,
|
||||
loading: aiSummaryLoading,
|
||||
error: aiSummaryError,
|
||||
run: runAiSummary
|
||||
} = useRequest(
|
||||
(version) =>
|
||||
request.get<ServerResponse<string | null>>(
|
||||
`https://release.nc.152710.xyz/?version=${version}`,
|
||||
{
|
||||
timeout: 30000
|
||||
}
|
||||
),
|
||||
{
|
||||
manual: true
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
runAiSummary(currentVersion)
|
||||
}, [currentVersion, runAiSummary])
|
||||
|
||||
if (aiSummaryLoading) {
|
||||
return (
|
||||
<div className="flex justify-center py-1">
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (aiSummaryError) {
|
||||
return <div className="text-center text-primary-500">AI 摘要获取失败</div>
|
||||
}
|
||||
return <span className="text-default-700">{aiSummaryData?.data.data}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content="有新版本可用">
|
||||
<Button
|
||||
isIconOnly
|
||||
radius="full"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="shadow"
|
||||
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
||||
onPress={() => {
|
||||
@@ -121,6 +158,13 @@ const NewVersionTip = (props: NewVersionTipProps) => {
|
||||
<span>最新版本</span>
|
||||
<Chip color="primary">{latestVersion}</Chip>
|
||||
</div>
|
||||
<div className="p-2 rounded-md bg-content2 text-sm">
|
||||
<div className="text-primary-400 font-bold flex items-center gap-1 mb-1">
|
||||
<BsStars />
|
||||
<span>AI总结</span>
|
||||
</div>
|
||||
{<AISummaryComponent />}
|
||||
</div>
|
||||
<div className="text-sm space-y-2 !mt-4">
|
||||
{middleVersions.map((versionInfo) => (
|
||||
<div
|
||||
@@ -190,19 +234,14 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
|
||||
error: qqVersionError
|
||||
} = useRequest(WebUIManager.getQQVersion)
|
||||
return (
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-danger-50 dark:shadow-danger-100 overflow-visible flex-1">
|
||||
<CardHeader className="pb-0 items-center gap-1 text-danger-500 font-extrabold">
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-primary-50 dark:shadow-primary-100 overflow-visible flex-1">
|
||||
<CardHeader className="pb-0 items-center gap-1 text-primary-500 font-extrabold">
|
||||
<FaCircleInfo className="text-lg" />
|
||||
<span>系统信息</span>
|
||||
</CardHeader>
|
||||
<CardBody className="flex-1">
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<NapCatVersion />
|
||||
<SystemInfoItem
|
||||
title="WebUI 版本"
|
||||
icon={<IoLogoChrome className="text-xl" />}
|
||||
value={packageJson.version}
|
||||
/>
|
||||
<SystemInfoItem
|
||||
title="QQ 版本"
|
||||
icon={<FaQq className="text-lg" />}
|
||||
@@ -216,6 +255,11 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<SystemInfoItem
|
||||
title="WebUI 版本"
|
||||
icon={<IoLogoChrome className="text-xl" />}
|
||||
value="Next"
|
||||
/>
|
||||
<SystemInfoItem
|
||||
title="系统版本"
|
||||
icon={<RiMacFill className="text-xl" />}
|
||||
|
@@ -55,7 +55,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-danger-50 dark:shadow-danger-100 col-span-1 lg:col-span-2 relative overflow-hidden">
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-primary-50 dark:shadow-primary-100 col-span-1 lg:col-span-2 relative overflow-hidden">
|
||||
<div className="absolute h-full right-0 top-0">
|
||||
<Image
|
||||
src={bkg}
|
||||
@@ -69,7 +69,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
</div>
|
||||
<CardBody className="overflow-visible md:flex-row gap-4 items-center justify-stretch z-10">
|
||||
<div className="flex-1 w-full md:max-w-96">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-danger-400">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-primary-400">
|
||||
<GiCpu className="text-xl" />
|
||||
<span>CPU</span>
|
||||
</h2>
|
||||
@@ -88,7 +88,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
unit="%"
|
||||
/>
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-danger-400 mt-2">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-primary-400 mt-2">
|
||||
<BiSolidMemoryCard className="text-xl" />
|
||||
<span>内存</span>
|
||||
</h2>
|
||||
|
@@ -62,7 +62,7 @@ export const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||
className={clsx(
|
||||
'px-2 py-1 flex items-center gap-1 text-sm font-medium border-b-2 transition-colors',
|
||||
isSelected
|
||||
? 'border-danger text-danger'
|
||||
? 'border-primary text-primary'
|
||||
: 'border-transparent hover:border-default',
|
||||
className
|
||||
)}
|
||||
|
@@ -99,7 +99,7 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
||||
if (theme === 'dark') {
|
||||
terminalRef.current.options.theme = {
|
||||
background: '#00000000',
|
||||
black: '#000000',
|
||||
black: '#ffffff',
|
||||
red: '#cd3131',
|
||||
green: '#0dbc79',
|
||||
yellow: '#e5e510',
|
||||
|
@@ -35,6 +35,7 @@ const AudioProvider: React.FC<MusicProviderProps> = ({ children }) => {
|
||||
const [musicId, setMusicId] = useState<number>(0)
|
||||
const [playMode, setPlayMode] = useState<PlayMode>(PlayMode.Loop)
|
||||
const music = musicList.find((music) => music.id === musicId)
|
||||
const [token] = useLocalStorage(key.token, '')
|
||||
const onNext = () => {
|
||||
const nextID = getNextMusic(musicList, musicId, playMode)
|
||||
setMusicId(nextID)
|
||||
@@ -60,8 +61,8 @@ const AudioProvider: React.FC<MusicProviderProps> = ({ children }) => {
|
||||
setMusicId(res[0].id)
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchMusicList(listId)
|
||||
}, [listId])
|
||||
if (listId && token) fetchMusicList(listId)
|
||||
}, [listId, token])
|
||||
return (
|
||||
<AudioContext.Provider
|
||||
value={{
|
||||
|
69
napcat.webui/src/hooks/use-preload-images.ts
Normal file
69
napcat.webui/src/hooks/use-preload-images.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
// 全局图片缓存
|
||||
const imageCache = new Map<string, HTMLImageElement>()
|
||||
|
||||
export function usePreloadImages(urls: string[]) {
|
||||
const [loadedUrls, setLoadedUrls] = useState<Record<string, boolean>>({})
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const isMounted = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true
|
||||
|
||||
// 检查是否所有图片都已缓存
|
||||
const allCached = urls.every((url) => imageCache.has(url))
|
||||
if (allCached) {
|
||||
setLoadedUrls(urls.reduce((acc, url) => ({ ...acc, [url]: true }), {}))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
const loadedImages: Record<string, boolean> = {}
|
||||
let pendingCount = urls.length
|
||||
|
||||
urls.forEach((url) => {
|
||||
// 如果已经缓存,直接标记为已加载
|
||||
if (imageCache.has(url)) {
|
||||
loadedImages[url] = true
|
||||
pendingCount--
|
||||
if (pendingCount === 0) {
|
||||
setLoadedUrls(loadedImages)
|
||||
setIsLoading(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
if (!isMounted.current) return
|
||||
loadedImages[url] = true
|
||||
imageCache.set(url, img)
|
||||
pendingCount--
|
||||
|
||||
if (pendingCount === 0) {
|
||||
setLoadedUrls(loadedImages)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
img.onerror = () => {
|
||||
if (!isMounted.current) return
|
||||
loadedImages[url] = false
|
||||
pendingCount--
|
||||
|
||||
if (pendingCount === 0) {
|
||||
setLoadedUrls(loadedImages)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
img.src = url
|
||||
})
|
||||
|
||||
return () => {
|
||||
isMounted.current = false
|
||||
}
|
||||
}, [urls])
|
||||
|
||||
return { loadedUrls, isLoading }
|
||||
}
|
@@ -79,7 +79,7 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
}, [location.pathname])
|
||||
return (
|
||||
<div
|
||||
className="h-screen relative flex bg-danger-50 dark:bg-black items-stretch"
|
||||
className="h-screen relative flex bg-primary-50 dark:bg-black items-stretch"
|
||||
style={{
|
||||
backgroundImage: `url(${b64img})`,
|
||||
backgroundSize: 'cover'
|
||||
@@ -99,9 +99,9 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
<div
|
||||
className={clsx(
|
||||
'h-10 flex items-center font-bold text-xl backdrop-blur-lg rounded-full',
|
||||
'dark:bg-background dark:shadow-danger-100',
|
||||
'dark:bg-background dark:shadow-primary-100',
|
||||
'bg-background !bg-opacity-50',
|
||||
'shadow-sm shadow-danger-50',
|
||||
'shadow-sm shadow-primary-50',
|
||||
'z-30 m-2 mb-0 sticky top-2 left-0'
|
||||
)}
|
||||
>
|
||||
|
@@ -1,12 +1,19 @@
|
||||
import { Chip } from '@heroui/chip'
|
||||
import { Card, CardBody } from '@heroui/card'
|
||||
import { Image } from '@heroui/image'
|
||||
import { Link } from '@heroui/link'
|
||||
import { Skeleton } from '@heroui/skeleton'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { useRequest } from 'ahooks'
|
||||
import clsx from 'clsx'
|
||||
import { useMemo } from 'react'
|
||||
import { BsTelegram, BsTencentQq } from 'react-icons/bs'
|
||||
import { IoDocument } from 'react-icons/io5'
|
||||
|
||||
import HoverTiltedCard from '@/components/hover_titled_card'
|
||||
import NapCatRepoInfo from '@/components/napcat_repo_info'
|
||||
import { title } from '@/components/primitives'
|
||||
import RotatingText from '@/components/rotating_text'
|
||||
|
||||
import { usePreloadImages } from '@/hooks/use-preload-images'
|
||||
import { useTheme } from '@/hooks/use-theme'
|
||||
|
||||
import logo from '@/assets/images/logo.png'
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
@@ -14,54 +21,177 @@ import WebUIManager from '@/controllers/webui_manager'
|
||||
function VersionInfo() {
|
||||
const { data, loading, error } = useRequest(WebUIManager.getPackageInfo)
|
||||
return (
|
||||
<div className="flex items-center gap-2 mb-5">
|
||||
<Chip
|
||||
startContent={
|
||||
<Chip color="warning" size="sm" className="-ml-0.5 select-none">
|
||||
NapCat
|
||||
</Chip>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-2xl font-bold">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-primary-500 drop-shadow-md">NapCat</div>
|
||||
{error ? (
|
||||
error.message
|
||||
) : loading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
data?.version
|
||||
<RotatingText
|
||||
texts={['WebUI', data?.version ?? '']}
|
||||
mainClassName="overflow-hidden flex items-center bg-primary-500 px-2 rounded-lg text-default-50 shadow-md"
|
||||
staggerFrom={'last'}
|
||||
initial={{ y: '100%' }}
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: '-120%' }}
|
||||
staggerDuration={0.025}
|
||||
splitLevelClassName="overflow-hidden"
|
||||
transition={{ type: 'spring', damping: 30, stiffness: 400 }}
|
||||
rotationInterval={2000}
|
||||
/>
|
||||
)}
|
||||
</Chip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AboutPage() {
|
||||
const { isDark } = useTheme()
|
||||
|
||||
const imageUrls = useMemo(
|
||||
() => [
|
||||
'https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=777721566&limit=30&image_size=auto&color_scheme=light',
|
||||
'https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=777721566&limit=30&image_size=auto&color_scheme=dark',
|
||||
'https://next.ossinsight.io/widgets/official/compose-activity-trends/thumbnail.png?repo_id=41986369&image_size=auto&color_scheme=light',
|
||||
'https://next.ossinsight.io/widgets/official/compose-activity-trends/thumbnail.png?repo_id=41986369&image_size=auto&color_scheme=dark'
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const { loadedUrls, isLoading } = usePreloadImages(imageUrls)
|
||||
|
||||
const getImageUrl = useMemo(
|
||||
() => (baseUrl: string) => {
|
||||
const theme = isDark ? 'dark' : 'light'
|
||||
const fullUrl = baseUrl.replace(
|
||||
/color_scheme=(?:light|dark)/,
|
||||
`color_scheme=${theme}`
|
||||
)
|
||||
return isLoading ? null : loadedUrls[fullUrl] ? fullUrl : null
|
||||
},
|
||||
[isDark, isLoading, loadedUrls]
|
||||
)
|
||||
|
||||
const renderImage = useMemo(
|
||||
() => (baseUrl: string, alt: string) => {
|
||||
const imageUrl = getImageUrl(baseUrl)
|
||||
|
||||
if (!imageUrl) {
|
||||
return <Skeleton className="h-16 rounded-lg" />
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
className="flex-1 pointer-events-none select-none rounded-none"
|
||||
src={imageUrl}
|
||||
alt={alt}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[getImageUrl]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>关于 NapCat WebUI</title>
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
|
||||
<div className="max-w-full w-[1000px] px-5 flex flex-col items-center">
|
||||
<div className="flex flex-col md:flex-row items-center mb-6">
|
||||
<HoverTiltedCard imageSrc={logo} />
|
||||
<section className="max-w-7xl py-8 md:py-10 px-5 mx-auto space-y-10">
|
||||
<div className="w-full flex flex-col md:flex-row gap-4">
|
||||
<div className="flex flex-col md:flex-row items-center">
|
||||
<HoverTiltedCard imageSrc={logo} overlayContent="" />
|
||||
</div>
|
||||
<VersionInfo />
|
||||
<div className="mb-6 flex flex-col items-center gap-4">
|
||||
<p
|
||||
className={clsx(
|
||||
title({
|
||||
color: 'cyan',
|
||||
shadow: true
|
||||
}),
|
||||
'!text-3xl'
|
||||
)}
|
||||
>
|
||||
NapCat Contributors
|
||||
</p>
|
||||
<Image
|
||||
className="w-[600px] max-w-full pointer-events-none select-none"
|
||||
src="https://contrib.rocks/image?repo=bietiaop/NapCatQQ"
|
||||
alt="Contributors"
|
||||
/>
|
||||
<div className="flex-1 flex flex-col gap-2 py-2">
|
||||
<VersionInfo />
|
||||
<div className="space-y-1">
|
||||
<p className="font-bold text-primary-400">NapCat 是什么?</p>
|
||||
<p className="text-default-800">
|
||||
基于TypeScript构建的Bot框架,通过相应的启动器或者框架,主动调用QQ
|
||||
Node模块提供给客户端的接口,实现Bot的功能.
|
||||
</p>
|
||||
<p className="font-bold text-primary-400">魔法版介绍</p>
|
||||
<p className="text-default-800">
|
||||
猫猫框架通过魔法的手段获得了 QQ 的发送消息、接收消息等接口。
|
||||
为了方便使用,猫猫框架将通过一种名为 OneBot 的约定将你的 HTTP /
|
||||
WebSocket 请求按照规范读取,
|
||||
再去调用猫猫框架所获得的QQ发送接口之类的接口。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 flex-wrap justify-around">
|
||||
<Card
|
||||
as={Link}
|
||||
shadow="sm"
|
||||
isPressable
|
||||
isExternal
|
||||
href="https://qm.qq.com/q/F9cgs1N3Mc"
|
||||
>
|
||||
<CardBody className="flex-row items-center gap-2">
|
||||
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||
<BsTencentQq size={16} />
|
||||
</span>
|
||||
<span>官方社群1</span>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
as={Link}
|
||||
shadow="sm"
|
||||
isPressable
|
||||
isExternal
|
||||
href="https://qm.qq.com/q/hSt0u9PVn"
|
||||
>
|
||||
<CardBody className="flex-row items-center gap-2">
|
||||
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||
<BsTencentQq size={16} />
|
||||
</span>
|
||||
<span>官方社群2</span>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
as={Link}
|
||||
shadow="sm"
|
||||
isPressable
|
||||
isExternal
|
||||
href="https://t.me/MelodicMoonlight"
|
||||
>
|
||||
<CardBody className="flex-row items-center gap-2">
|
||||
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||
<BsTelegram size={16} />
|
||||
</span>
|
||||
<span>Telegram</span>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
as={Link}
|
||||
shadow="sm"
|
||||
isPressable
|
||||
isExternal
|
||||
href="https://napcat.napneko.icu/"
|
||||
>
|
||||
<CardBody className="flex-row items-center gap-2">
|
||||
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||
<IoDocument size={16} />
|
||||
</span>
|
||||
<span>使用文档</span>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-4">
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
{renderImage(
|
||||
'https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=777721566&limit=30&image_size=auto&color_scheme=light',
|
||||
'Contributors'
|
||||
)}
|
||||
{renderImage(
|
||||
'https://next.ossinsight.io/widgets/official/compose-activity-trends/thumbnail.png?repo_id=41986369&image_size=auto&color_scheme=light',
|
||||
'Activity Trends'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<NapCatRepoInfo />
|
||||
</div>
|
||||
</section>
|
||||
|
@@ -41,7 +41,7 @@ export default function HttpDebug() {
|
||||
>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
radius="md"
|
||||
variant="shadow"
|
||||
size="sm"
|
||||
|
@@ -15,7 +15,7 @@ import { useWebSocketDebug } from '@/hooks/use-websocket-debug'
|
||||
|
||||
export default function WSDebug() {
|
||||
const url = new URL(window.location.origin)
|
||||
url.port = '3000'
|
||||
url.port = '3001'
|
||||
url.protocol = 'ws:'
|
||||
const defaultWsUrl = url.href
|
||||
const [socketConfig, setSocketConfig] = useLocalStorage(key.wsDebugConfig, {
|
||||
@@ -64,7 +64,7 @@ export default function WSDebug() {
|
||||
/>
|
||||
<div className="flex-shrink-0 flex gap-2 col-span-2 md:col-span-1">
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
onPress={handleConnect}
|
||||
size="lg"
|
||||
radius="full"
|
||||
|
@@ -332,7 +332,7 @@ export default function FileManagerPage() {
|
||||
<div className="p-4">
|
||||
<div className="mb-4 flex items-center gap-4 sticky top-14 z-10 bg-content1 py-1">
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
@@ -343,7 +343,7 @@ export default function FileManagerPage() {
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
@@ -354,7 +354,7 @@ export default function FileManagerPage() {
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
isLoading={loading}
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@@ -365,7 +365,7 @@ export default function FileManagerPage() {
|
||||
<MdRefresh />
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
@@ -379,7 +379,7 @@ export default function FileManagerPage() {
|
||||
selectedFiles === 'all') && (
|
||||
<>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={handleBatchDelete}
|
||||
@@ -391,7 +391,7 @@ export default function FileManagerPage() {
|
||||
)
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => {
|
||||
@@ -406,7 +406,7 @@ export default function FileManagerPage() {
|
||||
)
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={handleBatchDownload}
|
||||
|
@@ -105,7 +105,7 @@ const DashboardIndexPage: React.FC = () => {
|
||||
<SystemStatusCard setArchInfo={setArchInfo} />
|
||||
</div>
|
||||
<Networks />
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-danger-50">
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-primary-50">
|
||||
<CardBody>
|
||||
<Hitokoto />
|
||||
</CardBody>
|
||||
|
@@ -133,7 +133,7 @@ export default function TerminalPage() {
|
||||
size="sm"
|
||||
className="min-w-0 w-4 h-4 flex-shrink-0"
|
||||
onPress={() => closeTerminal(tab.id)}
|
||||
color={selectedTab === tab.id ? 'danger' : 'default'}
|
||||
color={selectedTab === tab.id ? 'primary' : 'default'}
|
||||
>
|
||||
<IoClose />
|
||||
</Button>
|
||||
@@ -143,7 +143,7 @@ export default function TerminalPage() {
|
||||
</TabList>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={createNewTerminal}
|
||||
|
@@ -1,46 +1,35 @@
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import { Route, Routes, useLocation } from 'react-router-dom'
|
||||
import { Suspense } from 'react'
|
||||
import { Outlet, useLocation } from 'react-router-dom'
|
||||
|
||||
import DefaultLayout from '@/layouts/default'
|
||||
|
||||
import DashboardIndexPage from './dashboard'
|
||||
import AboutPage from './dashboard/about'
|
||||
import ConfigPage from './dashboard/config'
|
||||
import DebugPage from './dashboard/debug'
|
||||
import HttpDebug from './dashboard/debug/http'
|
||||
import WSDebug from './dashboard/debug/websocket'
|
||||
import FileManagerPage from './dashboard/file_manager'
|
||||
import LogsPage from './dashboard/logs'
|
||||
import NetworkPage from './dashboard/network'
|
||||
import TerminalPage from './dashboard/terminal'
|
||||
|
||||
export default function IndexPage() {
|
||||
const location = useLocation()
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={location.pathname}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -50 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Routes location={location} key={location.pathname}>
|
||||
<Route element={<DashboardIndexPage />} path="/" />
|
||||
<Route element={<NetworkPage />} path="/network" />
|
||||
<Route element={<ConfigPage />} path="/config" />
|
||||
<Route element={<LogsPage />} path="/logs" />
|
||||
<Route element={<DebugPage />} path="/debug">
|
||||
<Route path="ws" element={<WSDebug />} />
|
||||
<Route path="http" element={<HttpDebug />} />
|
||||
</Route>
|
||||
<Route element={<FileManagerPage />} path="/file_manager" />
|
||||
<Route element={<TerminalPage />} path="/terminal" />
|
||||
<Route element={<AboutPage />} path="/about" />
|
||||
</Routes>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex justify-center px-10">
|
||||
<Spinner />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={location.pathname}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
type: 'tween',
|
||||
ease: 'easeInOut'
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Suspense>
|
||||
</DefaultLayout>
|
||||
)
|
||||
}
|
||||
|
@@ -13,5 +13,74 @@ export default {
|
||||
extend: {}
|
||||
},
|
||||
darkMode: 'class',
|
||||
plugins: [heroui()]
|
||||
plugins: [
|
||||
heroui({
|
||||
themes: {
|
||||
light: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#f31260',
|
||||
foreground: '#fff',
|
||||
50: '#fee7ef',
|
||||
100: '#fdd0df',
|
||||
200: '#faa0bf',
|
||||
300: '#f871a0',
|
||||
400: '#f54180',
|
||||
500: '#f31260',
|
||||
600: '#c20e4d',
|
||||
700: '#920b3a',
|
||||
800: '#610726',
|
||||
900: '#310413'
|
||||
},
|
||||
danger: {
|
||||
DEFAULT: '#DB3694',
|
||||
foreground: '#fff',
|
||||
50: '#FEEAF6',
|
||||
100: '#FDD7DD',
|
||||
200: '#FBAFC4',
|
||||
300: '#F485AE',
|
||||
400: '#E965A3',
|
||||
500: '#DB3694',
|
||||
600: '#BC278B',
|
||||
700: '#9D1B7F',
|
||||
800: '#7F1170',
|
||||
900: '#690A66'
|
||||
}
|
||||
}
|
||||
},
|
||||
dark: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#f31260',
|
||||
foreground: '#fff',
|
||||
50: '#310413',
|
||||
100: '#610726',
|
||||
200: '#920b3a',
|
||||
300: '#c20e4d',
|
||||
400: '#f31260',
|
||||
500: '#f54180',
|
||||
600: '#f871a0',
|
||||
700: '#faa0bf',
|
||||
800: '#fdd0df',
|
||||
900: '#fee7ef'
|
||||
},
|
||||
danger: {
|
||||
DEFAULT: '#DB3694',
|
||||
foreground: '#fff',
|
||||
50: '#690A66',
|
||||
100: '#7F1170',
|
||||
200: '#9D1B7F',
|
||||
300: '#BC278B',
|
||||
400: '#DB3694',
|
||||
500: '#E965A3',
|
||||
600: '#F485AE',
|
||||
700: '#FBAFC4',
|
||||
800: '#FDD7DD',
|
||||
900: '#FEEAF6'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.5.1",
|
||||
"version": "4.5.7",
|
||||
"scripts": {
|
||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||
|
@@ -2,73 +2,73 @@ import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import type { NapCatCore } from '@/core';
|
||||
import json5 from 'json5';
|
||||
import Ajv, { AnySchema, ValidateFunction } from 'ajv';
|
||||
|
||||
export abstract class ConfigBase<T> {
|
||||
name: string;
|
||||
core: NapCatCore;
|
||||
configPath: string;
|
||||
configData: T = {} as T;
|
||||
ajv: Ajv;
|
||||
validate: ValidateFunction<T>;
|
||||
|
||||
protected constructor(name: string, core: NapCatCore, configPath: string, copy_default: boolean = true) {
|
||||
protected constructor(name: string, core: NapCatCore, configPath: string, ConfigSchema: AnySchema) {
|
||||
this.name = name;
|
||||
this.core = core;
|
||||
this.configPath = configPath;
|
||||
this.ajv = new Ajv({ useDefaults: true, coerceTypes: true });
|
||||
this.validate = this.ajv.compile<T>(ConfigSchema);
|
||||
fs.mkdirSync(this.configPath, { recursive: true });
|
||||
this.read(copy_default);
|
||||
this.read();
|
||||
}
|
||||
|
||||
protected getKeys(): string[] | null {
|
||||
// 决定 key 在json配置文件中的顺序
|
||||
return null;
|
||||
getConfigPath(pathName?: string): string {
|
||||
const filename = pathName ? `${this.name}_${pathName}.json` : `${this.name}.json`;
|
||||
return path.join(this.configPath, filename);
|
||||
}
|
||||
|
||||
getConfigPath(pathName: string | undefined): string {
|
||||
if (!pathName) {
|
||||
const filename = `${this.name}.json`;
|
||||
const mainPath = this.core.context.pathWrapper.binaryPath;
|
||||
return path.join(mainPath, 'config', filename);
|
||||
} else {
|
||||
const filename = `${this.name}_${pathName}.json`;
|
||||
return path.join(this.configPath, filename);
|
||||
}
|
||||
}
|
||||
|
||||
read(copy_default: boolean = true): T {
|
||||
|
||||
read(): T {
|
||||
const configPath = this.getConfigPath(this.core.selfInfo.uin);
|
||||
if (!fs.existsSync(configPath) && copy_default) {
|
||||
try {
|
||||
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
|
||||
this.core.context.logger.log('[Core] [Config] 配置文件创建成功!\n');
|
||||
} catch (e: unknown) {
|
||||
this.core.context.logger.logError('[Core] [Config] 创建配置文件时发生错误:', (e as Error).message);
|
||||
const defaultConfigPath = this.getConfigPath();
|
||||
if (!fs.existsSync(configPath)) {
|
||||
if (fs.existsSync(defaultConfigPath)) {
|
||||
this.configData = this.loadConfig(defaultConfigPath);
|
||||
}
|
||||
} else if (!fs.existsSync(configPath) && !copy_default) {
|
||||
fs.writeFileSync(configPath, '{}');
|
||||
this.save();
|
||||
return this.configData;
|
||||
}
|
||||
return this.loadConfig(configPath);
|
||||
}
|
||||
|
||||
private loadConfig(configPath: string): T {
|
||||
try {
|
||||
this.configData = json5.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
let newConfigData = json5.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
this.validate(newConfigData);
|
||||
this.configData = newConfigData;
|
||||
this.core.context.logger.logDebug(`[Core] [Config] 配置文件${configPath}加载`, this.configData);
|
||||
return this.configData;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof SyntaxError) {
|
||||
this.core.context.logger.logError('[Core] [Config] 配置文件格式错误,请检查配置文件:', e.message);
|
||||
} else {
|
||||
this.core.context.logger.logError('[Core] [Config] 读取配置文件时发生错误:', (e as Error).message);
|
||||
}
|
||||
this.handleError(e, '读取配置文件时发生错误');
|
||||
return {} as T;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
save(newConfigData: T = this.configData) {
|
||||
const selfInfo = this.core.selfInfo;
|
||||
save(newConfigData: T = this.configData): void {
|
||||
const configPath = this.getConfigPath(this.core.selfInfo.uin);
|
||||
this.validate(newConfigData);
|
||||
this.configData = newConfigData;
|
||||
const configPath = this.getConfigPath(selfInfo.uin);
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(newConfigData, this.getKeys(), 2));
|
||||
fs.writeFileSync(configPath, JSON.stringify(this.configData, null, 2));
|
||||
} catch (e: unknown) {
|
||||
this.core.context.logger.logError(`保存配置文件 ${configPath} 时发生错误:`, (e as Error).message);
|
||||
this.handleError(e, `保存配置文件 ${configPath} 时发生错误:`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(e: unknown, message: string): void {
|
||||
if (e instanceof SyntaxError) {
|
||||
this.core.context.logger.logError(`[Core] [Config] 操作配置文件格式错误,请检查配置文件:`, e.message);
|
||||
} else {
|
||||
this.core.context.logger.logError(`[Core] [Config] ${message}:`, (e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.5.1';
|
||||
export const napCatVersion = '4.5.7';
|
||||
|
@@ -1,11 +1,21 @@
|
||||
import { ConfigBase } from '@/common/config-base';
|
||||
import napCatDefaultConfig from '@/core/external/napcat.json';
|
||||
import { NapCatCore } from '@/core';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import { AnySchema } from 'ajv';
|
||||
|
||||
export type NapCatConfig = typeof napCatDefaultConfig;
|
||||
export const NapcatConfigSchema = Type.Object({
|
||||
fileLog: Type.Boolean({ default: false }),
|
||||
consoleLog: Type.Boolean({ default: true }),
|
||||
fileLogLevel: Type.String({ default: 'debug' }),
|
||||
consoleLogLevel: Type.String({ default: 'info' }),
|
||||
packetBackend: Type.String({ default: 'auto' }),
|
||||
packetServer: Type.String({ default: '' })
|
||||
});
|
||||
|
||||
export class NapCatConfigLoader extends ConfigBase<NapCatConfig> {
|
||||
constructor(core: NapCatCore, configPath: string) {
|
||||
super('napcat', core, configPath);
|
||||
export type NapcatConfig = Static<typeof NapcatConfigSchema>;
|
||||
|
||||
export class NapCatConfigLoader extends ConfigBase<NapcatConfig> {
|
||||
constructor(core: NapCatCore, configPath: string, schema: AnySchema) {
|
||||
super('napcat', core, configPath, schema);
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ import fs from 'node:fs';
|
||||
import { hostname, systemName, systemVersion } from '@/common/system';
|
||||
import { NTEventWrapper } from '@/common/event';
|
||||
import { KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types';
|
||||
import { NapCatConfigLoader } from '@/core/helper/config';
|
||||
import { NapCatConfigLoader, NapcatConfigSchema } from '@/core/helper/config';
|
||||
import os from 'node:os';
|
||||
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
||||
import { proxiedListenerOf } from '@/common/proxy-handler';
|
||||
@@ -99,7 +99,7 @@ export class NapCatCore {
|
||||
this.context = context;
|
||||
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
|
||||
this.eventWrapper = new NTEventWrapper(context.session);
|
||||
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath);
|
||||
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath,NapcatConfigSchema);
|
||||
this.apis = {
|
||||
FileApi: new NTQQFileApi(this.context, this),
|
||||
SystemApi: new NTQQSystemApi(this.context, this),
|
||||
|
@@ -137,18 +137,9 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
pbReserve: {
|
||||
messageId: this.messageId,
|
||||
},
|
||||
toUin: BigInt(0),
|
||||
toUin: BigInt(this.targetUin),
|
||||
type: 1,
|
||||
}
|
||||
}, {
|
||||
text: this.isGroupReply ? {
|
||||
str: 'nya~',
|
||||
pbReserve: new NapProtoMsg(MentionExtra).encode({
|
||||
type: this.targetUin === 0 ? 1 : 2,
|
||||
uin: 0,
|
||||
field5: 0,
|
||||
uid: String(this.targetUid),
|
||||
}),
|
||||
} : undefined,
|
||||
}];
|
||||
}
|
||||
|
||||
|
@@ -15,7 +15,6 @@ let napCatInitialized = false; // 添加一个标志
|
||||
function createServiceProxy(ServiceName) {
|
||||
return new Proxy(() => { }, {
|
||||
get: (target, FunctionName) => {
|
||||
console.log(ServiceName, FunctionName);
|
||||
if (ServiceName === 'NodeIQQNTWrapperSession' && FunctionName === 'create') {
|
||||
return () => new Proxy({}, {
|
||||
get: function (target, ClassFunName, receiver) {
|
||||
|
@@ -857,7 +857,7 @@ export class OneBotMsgApi {
|
||||
return parsedElement;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
return;
|
||||
},
|
||||
));
|
||||
|
||||
|
@@ -78,7 +78,7 @@ const NetworkConfigSchema = Type.Object({
|
||||
plugins: Type.Array(PluginConfigSchema, { default: [] })
|
||||
}, { default: {} });
|
||||
|
||||
const OneBotConfigSchema = Type.Object({
|
||||
export const OneBotConfigSchema = Type.Object({
|
||||
network: NetworkConfigSchema,
|
||||
musicSignUrl: Type.String({ default: '' }),
|
||||
enableLocalFile2Url: Type.Boolean({ default: false }),
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { ConfigBase } from '@/common/config-base';
|
||||
import { NapCatCore } from '@/core';
|
||||
import type { NapCatCore } from '@/core';
|
||||
import { OneBotConfig } from './config';
|
||||
import { AnySchema } from 'ajv';
|
||||
|
||||
export class OB11ConfigLoader extends ConfigBase<OneBotConfig> {
|
||||
constructor(core: NapCatCore, configPath: string) {
|
||||
super('onebot11', core, configPath, false);
|
||||
constructor(core: NapCatCore, configPath: string, schema: AnySchema) {
|
||||
super('onebot11', core, configPath, schema);
|
||||
}
|
||||
}
|
||||
|
@@ -44,8 +44,8 @@ import { LRUCache } from '@/common/lru-cache';
|
||||
import { BotOfflineEvent } from './event/notice/BotOfflineEvent';
|
||||
import {
|
||||
NetworkAdapterConfig,
|
||||
loadConfig,
|
||||
OneBotConfig,
|
||||
OneBotConfigSchema,
|
||||
} from './config/config';
|
||||
import { OB11Message } from './types';
|
||||
import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
|
||||
@@ -66,9 +66,7 @@ export class NapCatOneBot11Adapter {
|
||||
constructor(core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) {
|
||||
this.core = core;
|
||||
this.context = context;
|
||||
this.configLoader = new OB11ConfigLoader(core, pathWrapper.configPath);
|
||||
this.configLoader.save(this.configLoader.configData);
|
||||
this.configLoader.save(loadConfig(this.configLoader.configData));
|
||||
this.configLoader = new OB11ConfigLoader(core, pathWrapper.configPath, OneBotConfigSchema);
|
||||
this.apis = {
|
||||
GroupApi: new OneBotGroupApi(this, core),
|
||||
UserApi: new OneBotUserApi(this, core),
|
||||
@@ -176,9 +174,6 @@ export class NapCatOneBot11Adapter {
|
||||
WebUiDataRuntime.setQQLoginStatus(true);
|
||||
WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => {
|
||||
const prev = this.configLoader.configData;
|
||||
// 保证默认配置
|
||||
newConfig = loadConfig(newConfig);
|
||||
|
||||
this.configLoader.save(newConfig);
|
||||
//this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`);
|
||||
await this.reloadNetwork(prev, newConfig);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
|
||||
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
|
||||
import express, { Express, NextFunction, Request, Response } from 'express';
|
||||
import http from 'http';
|
||||
import { NapCatCore } from '@/core';
|
||||
@@ -60,7 +60,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
req.body = json5.parse(rawData || '{}');
|
||||
req.body = { ...json5.parse(rawData || '{}'), ...req.body };
|
||||
next();
|
||||
} catch {
|
||||
return res.status(400).send('Invalid JSON');
|
||||
|
@@ -15,7 +15,7 @@ import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
|
||||
import json5 from 'json5';
|
||||
|
||||
export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> {
|
||||
wsServer: WebSocketServer;
|
||||
wsServer?: WebSocketServer;
|
||||
wsClients: WebSocket[] = [];
|
||||
wsClientsMutex = new Mutex();
|
||||
private heartbeatIntervalId: NodeJS.Timeout | null = null;
|
||||
@@ -30,7 +30,11 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
host: this.config.host === '0.0.0.0' ? '' : this.config.host,
|
||||
maxPayload: 1024 * 1024 * 1024,
|
||||
});
|
||||
this.wsServer.on('connection', async (wsClient, wsReq) => {
|
||||
this.createServer(this.wsServer);
|
||||
|
||||
}
|
||||
createServer(newServer: WebSocketServer) {
|
||||
newServer.on('connection', async (wsClient, wsReq) => {
|
||||
if (!this.isEnable) {
|
||||
wsClient.close();
|
||||
return;
|
||||
@@ -40,7 +44,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
const paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url;
|
||||
const isApiConnect = paramUrl === '/api' || paramUrl === '/api/';
|
||||
if (!isApiConnect) {
|
||||
this.connectEvent(core, wsClient);
|
||||
this.connectEvent(this.core, wsClient);
|
||||
}
|
||||
|
||||
wsClient.on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Client Error:', err.message));
|
||||
@@ -74,7 +78,6 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
});
|
||||
}).on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Server Error:', err.message));
|
||||
}
|
||||
|
||||
connectEvent(core: NapCatCore, wsClient: WebSocket) {
|
||||
try {
|
||||
this.checkStateAndReply<unknown>(new OB11LifeCycleEvent(core, LifeCycleSubType.CONNECT), wsClient);
|
||||
@@ -96,7 +99,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
this.logger.logError('[OneBot] [WebSocket Server] Cannot open a opened WebSocket server');
|
||||
return;
|
||||
}
|
||||
const addressInfo = this.wsServer.address();
|
||||
const addressInfo = this.wsServer?.address();
|
||||
this.logger.log('[OneBot] [WebSocket Server] Server Started', typeof (addressInfo) === 'string' ? addressInfo : addressInfo?.address + ':' + addressInfo?.port);
|
||||
|
||||
this.isEnable = true;
|
||||
@@ -108,7 +111,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
|
||||
async close() {
|
||||
this.isEnable = false;
|
||||
this.wsServer.close((err) => {
|
||||
this.wsServer?.close((err) => {
|
||||
if (err) {
|
||||
this.logger.logError('[OneBot] [WebSocket Server] Error closing server:', err.message);
|
||||
} else {
|
||||
@@ -205,6 +208,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
||||
host: newConfig.host === '0.0.0.0' ? '' : newConfig.host,
|
||||
maxPayload: 1024 * 1024 * 1024,
|
||||
});
|
||||
this.createServer(this.wsServer);
|
||||
if (newConfig.enable) {
|
||||
this.open();
|
||||
}
|
||||
|
@@ -268,7 +268,11 @@ export const BatchMoveHandler: RequestHandler = async (req, res) => {
|
||||
// 新增:文件下载处理方法(注意流式传输,不将整个文件读入内存)
|
||||
export const DownloadHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const filePath = normalizePath(req.query['path'] as string);
|
||||
const filePath = normalizePath( req.query[ 'path' ] as string );
|
||||
if (!filePath) {
|
||||
return sendError( res, '参数错误' );
|
||||
}
|
||||
|
||||
const stat = await fsProm.stat(filePath);
|
||||
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
|
@@ -1,13 +1,12 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { OneBotConfig } from '@/onebot/config/config';
|
||||
|
||||
import { loadConfig, OneBotConfig } from '@/onebot/config/config';
|
||||
import { webUiPathWrapper } from '@/webui';
|
||||
import { WebUiDataRuntime } from '@webapi/helper/Data';
|
||||
import { sendError, sendSuccess } from '@webapi/utils/response';
|
||||
import { isEmpty } from '@webapi/utils/check';
|
||||
import json5 from 'json5';
|
||||
|
||||
// 获取OneBot11配置
|
||||
export const OB11GetConfigHandler: RequestHandler = (_, res) => {
|
||||
@@ -19,16 +18,16 @@ export const OB11GetConfigHandler: RequestHandler = (_, res) => {
|
||||
}
|
||||
// 获取登录的QQ号
|
||||
const uin = WebUiDataRuntime.getQQLoginUin();
|
||||
// 读取配置文件
|
||||
// 读取配置文件路径
|
||||
const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`);
|
||||
// 尝试解析配置文件
|
||||
try {
|
||||
// 读取配置文件
|
||||
const data = JSON.parse(
|
||||
existsSync(configFilePath)
|
||||
? readFileSync(configFilePath).toString()
|
||||
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString()
|
||||
) as OneBotConfig;
|
||||
// 读取配置文件内容
|
||||
const configFileContent = existsSync(configFilePath)
|
||||
? readFileSync(configFilePath).toString()
|
||||
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString();
|
||||
// 解析配置文件并加载配置
|
||||
const data = loadConfig(json5.parse(configFileContent)) as OneBotConfig;
|
||||
// 返回配置文件
|
||||
return sendSuccess(res, data);
|
||||
} catch (e) {
|
||||
@@ -50,9 +49,12 @@ export const OB11SetConfigHandler: RequestHandler = async (req, res) => {
|
||||
}
|
||||
// 写入配置
|
||||
try {
|
||||
await WebUiDataRuntime.setOB11Config(JSON.parse(req.body.config));
|
||||
// 解析并加载配置
|
||||
const config = loadConfig(json5.parse(req.body.config)) as OneBotConfig;
|
||||
// 写入配置
|
||||
await WebUiDataRuntime.setOB11Config(config);
|
||||
return sendSuccess(res, null);
|
||||
} catch (e) {
|
||||
return sendError(res, 'Error: ' + e);
|
||||
}
|
||||
};
|
||||
};
|
@@ -7,8 +7,8 @@ export const GetProxyHandler: RequestHandler = async (req, res) => {
|
||||
if (url && typeof url === 'string') {
|
||||
url = decodeURIComponent(url);
|
||||
const responseText = await RequestUtil.HttpGetText(url);
|
||||
res.send(sendSuccess(res, responseText));
|
||||
return sendSuccess(res, responseText);
|
||||
} else {
|
||||
res.send(sendError(res, 'url参数不合法'));
|
||||
return sendError(res, 'url参数不合法');
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user