Compare commits

..

39 Commits

Author SHA1 Message Date
手瓜一十雪
fa12865924 fix: error 2025-02-06 17:10:30 +08:00
Mlikiowa
ecdd717742 release: v4.5.12 2025-02-06 08:23:07 +00:00
bietiaop
6851334af9 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-06 15:29:04 +08:00
bietiaop
9051b29565 feat: 字体修改#771 2025-02-06 15:28:42 +08:00
手瓜一十雪
95c7d3dfbd fix: remove __dirname 2025-02-06 15:28:24 +08:00
手瓜一十雪
bc1148c00a fix: require_dlopen 2025-02-06 15:25:47 +08:00
Mlikiowa
d4556d9299 release: v4.5.11 2025-02-06 03:13:17 +00:00
pk5ls20
5d389a2359 fix: fake forwardMsg construct 2025-02-06 01:09:23 +08:00
Mlikiowa
305116874b release: v4.5.10 2025-02-05 11:49:14 +00:00
bietiaop
b08a29897f fix: #769 2025-02-05 19:45:30 +08:00
Mlikiowa
b59c1d9122 release: v4.5.9 2025-02-05 11:14:25 +00:00
手瓜一十雪
adb9cea701 Merge pull request #765 from NapNeko/fix/multi-forward-protocol-fetch
fix: #721
2025-02-05 19:08:08 +08:00
Mlikiowa
5e148d2e82 release: v4.5.8 2025-02-05 11:02:28 +00:00
手瓜一十雪
a0d780558e fix 2025-02-05 19:01:14 +08:00
Mlikiowa
ad56065a4e release: v4.5.7 2025-02-05 07:10:27 +00:00
手瓜一十雪
f5dee80b6e Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-05 15:09:27 +08:00
手瓜一十雪
9cc75881b8 fix: arm64 2025-02-05 14:51:12 +08:00
bietiaop
593fb13b61 style: 语义化样式 2025-02-05 10:38:12 +08:00
pk5ls20
fca90592d6 try fix: #755 2025-02-05 08:29:37 +08:00
pk5ls20
d6848e2855 fix: #721 2025-02-05 08:07:58 +08:00
bietiaop
7539a4129f fix: 获取歌单 2025-02-04 22:14:23 +08:00
bietiaop
5402574266 feat: AI更新总结 2025-02-04 22:03:37 +08:00
Mlikiowa
853175aa1a release: v4.5.6 2025-02-04 13:24:46 +00:00
手瓜一十雪
feb84809ec fix: #761 2025-02-04 21:22:36 +08:00
bietiaop
a812c568e4 fix: 文件预览 2025-02-04 21:12:13 +08:00
bietiaop
11db25e355 fix: 文件预览 2025-02-04 21:08:28 +08:00
手瓜一十雪
ecd2fba629 fix: #762 2025-02-04 20:42:13 +08:00
Mlikiowa
a6763cf5a1 release: v4.5.5 2025-02-04 11:50:37 +00:00
手瓜一十雪
c9e91a9b94 fix: defalut config 2025-02-04 19:49:56 +08:00
Mlikiowa
43fb62c5bd release: v4.5.4 2025-02-04 11:35:51 +00:00
手瓜一十雪
cb8727d487 fix: reload and parse msg 2025-02-04 19:34:51 +08:00
Mlikiowa
a94e03e2fd release: v4.5.3 2025-02-04 10:16:07 +00:00
手瓜一十雪
425c3c6432 fix: 避免重复reload 2025-02-04 18:14:13 +08:00
手瓜一十雪
89b9610016 fix: 避免read异常 2025-02-04 18:13:42 +08:00
手瓜一十雪
62fe88f868 Merge pull request #760 from NapNeko/config-refactor
refactor
2025-02-04 18:09:57 +08:00
手瓜一十雪
11a7f5fade refactor 2025-02-04 18:09:30 +08:00
bietiaop
fbde997f7c style: 调整样式 2025-02-04 17:58:38 +08:00
bietiaop
26734a35ef fix: 文件下载 2025-02-04 15:31:10 +08:00
Mlikiowa
715c4ac534 release: v4.5.2 2025-02-04 06:52:11 +00:00
103 changed files with 1880 additions and 511 deletions

View File

@@ -4,7 +4,7 @@
"name": "NapCatQQ", "name": "NapCatQQ",
"slug": "NapCat.Framework", "slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现", "description": "高性能的 OneBot 11 协议实现",
"version": "4.5.1", "version": "4.5.12",
"icon": "./logo.png", "icon": "./logo.png",
"authors": [ "authors": [
{ {

View File

@@ -32,6 +32,7 @@
"@heroui/pagination": "^2.2.9", "@heroui/pagination": "^2.2.9",
"@heroui/popover": "2.3.10", "@heroui/popover": "2.3.10",
"@heroui/select": "2.4.10", "@heroui/select": "2.4.10",
"@heroui/skeleton": "^2.2.6",
"@heroui/slider": "2.4.8", "@heroui/slider": "2.4.8",
"@heroui/snippet": "2.2.11", "@heroui/snippet": "2.2.11",
"@heroui/spinner": "2.2.7", "@heroui/spinner": "2.2.7",

View File

@@ -16,6 +16,16 @@ import store from '@/store'
const WebLoginPage = lazy(() => import('@/pages/web_login')) const WebLoginPage = lazy(() => import('@/pages/web_login'))
const IndexPage = lazy(() => import('@/pages/index')) const IndexPage = lazy(() => import('@/pages/index'))
const QQLoginPage = lazy(() => import('@/pages/qq_login')) 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() { function App() {
return ( return (
@@ -58,9 +68,21 @@ function AuthChecker({ children }: { children: React.ReactNode }) {
function AppRoutes() { function AppRoutes() {
return ( return (
<Routes> <Routes>
<Route element={<IndexPage />} path="/*" /> <Route path="/" element={<IndexPage />}>
<Route element={<QQLoginPage />} path="/qq_login" /> <Route index element={<DashboardIndexPage />} />
<Route element={<WebLoginPage />} path="/web_login" /> <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> </Routes>
) )
} }

View File

@@ -231,7 +231,7 @@ export default function AudioPlayer(props: AudioPlayerProps) {
: 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md' : 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md'
)} )}
variant="solid" variant="solid"
color="danger" color="primary"
size="sm" size="sm"
onPress={() => setIsCollapsed(!isCollapsed)} onPress={() => setIsCollapsed(!isCollapsed)}
> >

View File

@@ -33,7 +33,7 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
> >
<DropdownTrigger> <DropdownTrigger>
<Button <Button
color="danger" color="primary"
startContent={<IoAddCircleOutline className="text-2xl" />} startContent={<IoAddCircleOutline className="text-2xl" />}
> >

View File

@@ -27,7 +27,7 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
</Button> </Button>
<Button <Button
color="danger" color="primary"
isLoading={isSubmitting} isLoading={isSubmitting}
onPress={() => onSubmit()} onPress={() => onSubmit()}
> >

View File

@@ -110,7 +110,7 @@ const AudioInsert = () => {
<Tooltip content="发送音频"> <Tooltip content="发送音频">
<div className="max-w-fit"> <div className="max-w-fit">
<PopoverTrigger> <PopoverTrigger>
<Button color="danger" variant="flat" isIconOnly radius="full"> <Button color="primary" variant="flat" isIconOnly radius="full">
<IoMic className="text-xl" /> <IoMic className="text-xl" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -120,7 +120,7 @@ const AudioInsert = () => {
<Tooltip content="上传音频"> <Tooltip content="上传音频">
<Button <Button
className="text-lg" className="text-lg"
color="danger" color="primary"
isIconOnly isIconOnly
variant="flat" variant="flat"
radius="full" radius="full"
@@ -137,7 +137,7 @@ const AudioInsert = () => {
<PopoverTrigger tooltip="输入音频地址"> <PopoverTrigger tooltip="输入音频地址">
<Button <Button
className="text-lg" className="text-lg"
color="danger" color="primary"
isIconOnly isIconOnly
variant="flat" variant="flat"
radius="full" radius="full"
@@ -154,7 +154,7 @@ const AudioInsert = () => {
placeholder="请输入音频地址" placeholder="请输入音频地址"
/> />
<Button <Button
color="danger" color="primary"
variant="flat" variant="flat"
isIconOnly isIconOnly
radius="full" radius="full"
@@ -177,7 +177,7 @@ const AudioInsert = () => {
<PopoverTrigger> <PopoverTrigger>
<Button <Button
className="text-lg" className="text-lg"
color="danger" color="primary"
isIconOnly isIconOnly
variant="flat" variant="flat"
radius="full" radius="full"
@@ -190,7 +190,7 @@ const AudioInsert = () => {
<PopoverContent className="flex-col gap-2 p-4"> <PopoverContent className="flex-col gap-2 p-4">
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
color={isRecording ? 'danger' : 'danger'} color={isRecording ? 'primary' : 'primary'}
variant="flat" variant="flat"
onPress={isRecording ? stopRecording : startRecording} onPress={isRecording ? stopRecording : startRecording}
> >
@@ -198,7 +198,7 @@ const AudioInsert = () => {
</Button> </Button>
{showPreview && audioPreview && ( {showPreview && audioPreview && (
<Button <Button
color="danger" color="primary"
variant="flat" variant="flat"
onPress={handleShowPreview} onPress={handleShowPreview}
> >
@@ -212,7 +212,7 @@ const AudioInsert = () => {
className={clsx( className={clsx(
'w-4 h-4 rounded-full', 'w-4 h-4 rounded-full',
isRecording isRecording
? 'animate-pulse bg-danger-400' ? 'animate-pulse bg-primary-400'
: 'bg-success-400' : 'bg-success-400'
)} )}
></span> ></span>

View File

@@ -10,7 +10,7 @@ const DiceInsert = () => {
return ( return (
<Tooltip content="发送骰子"> <Tooltip content="发送骰子">
<Button <Button
color="danger" color="primary"
variant="flat" variant="flat"
isIconOnly isIconOnly
radius="full" radius="full"

View File

@@ -55,7 +55,7 @@ const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
<Tooltip content="插入表情"> <Tooltip content="插入表情">
<div className="max-w-fit"> <div className="max-w-fit">
<PopoverTrigger> <PopoverTrigger>
<Button color="danger" variant="flat" isIconOnly radius="full"> <Button color="primary" variant="flat" isIconOnly radius="full">
<MdEmojiEmotions className="text-xl" /> <MdEmojiEmotions className="text-xl" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -65,7 +65,7 @@ const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
{visibleEmojis.map((emoji) => ( {visibleEmojis.map((emoji) => (
<Button <Button
key={emoji.id} key={emoji.id}
color="danger" color="primary"
variant="flat" variant="flat"
isIconOnly isIconOnly
radius="full" radius="full"

View File

@@ -35,7 +35,7 @@ const FileInsert = () => {
<Tooltip content="发送文件"> <Tooltip content="发送文件">
<div className="max-w-fit"> <div className="max-w-fit">
<PopoverTrigger> <PopoverTrigger>
<Button color="danger" variant="flat" isIconOnly radius="full"> <Button color="primary" variant="flat" isIconOnly radius="full">
<FaFolder className="text-lg" /> <FaFolder className="text-lg" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -45,7 +45,7 @@ const FileInsert = () => {
<Tooltip content="上传文件"> <Tooltip content="上传文件">
<Button <Button
className="text-lg" className="text-lg"
color="danger" color="primary"
isIconOnly isIconOnly
variant="flat" variant="flat"
radius="full" radius="full"
@@ -62,7 +62,7 @@ const FileInsert = () => {
<PopoverTrigger tooltip="输入文件地址"> <PopoverTrigger tooltip="输入文件地址">
<Button <Button
className="text-lg" className="text-lg"
color="danger" color="primary"
isIconOnly isIconOnly
variant="flat" variant="flat"
radius="full" radius="full"
@@ -79,7 +79,7 @@ const FileInsert = () => {
placeholder="请输入文件地址" placeholder="请输入文件地址"
/> />
<Button <Button
color="danger" color="primary"
variant="flat" variant="flat"
isIconOnly isIconOnly
radius="full" radius="full"

View File

@@ -23,7 +23,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
<Tooltip content="插入图片"> <Tooltip content="插入图片">
<div className="max-w-fit"> <div className="max-w-fit">
<PopoverTrigger> <PopoverTrigger>
<Button color="danger" variant="flat" isIconOnly radius="full"> <Button color="primary" variant="flat" isIconOnly radius="full">
<MdImage className="text-xl" /> <MdImage className="text-xl" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -33,7 +33,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
<Tooltip content="上传图片"> <Tooltip content="上传图片">
<Button <Button
className="text-lg" className="text-lg"
color="danger" color="primary"
isIconOnly isIconOnly
variant="flat" variant="flat"
radius="full" radius="full"
@@ -50,7 +50,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
<PopoverTrigger tooltip="输入图片地址"> <PopoverTrigger tooltip="输入图片地址">
<Button <Button
className="text-lg" className="text-lg"
color="danger" color="primary"
isIconOnly isIconOnly
variant="flat" variant="flat"
radius="full" radius="full"
@@ -67,7 +67,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
placeholder="请输入图片地址" placeholder="请输入图片地址"
/> />
<Button <Button
color="danger" color="primary"
variant="flat" variant="flat"
isIconOnly isIconOnly
radius="full" radius="full"

View File

@@ -80,7 +80,7 @@ const MusicInsert = () => {
<Tooltip content="发送音乐"> <Tooltip content="发送音乐">
<div className="max-w-fit"> <div className="max-w-fit">
<PopoverTrigger> <PopoverTrigger>
<Button color="danger" variant="flat" isIconOnly radius="full"> <Button color="primary" variant="flat" isIconOnly radius="full">
<IoMusicalNotes className="text-xl" /> <IoMusicalNotes className="text-xl" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -132,7 +132,7 @@ const MusicInsert = () => {
<Button <Button
fullWidth fullWidth
size="lg" size="lg"
color="danger" color="primary"
variant="flat" variant="flat"
radius="full" radius="full"
onPress={() => { onPress={() => {
@@ -236,7 +236,7 @@ const MusicInsert = () => {
<Button <Button
fullWidth fullWidth
size="lg" size="lg"
color="danger" color="primary"
variant="flat" variant="flat"
radius="full" radius="full"
type="submit" type="submit"

View File

@@ -19,7 +19,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
<Tooltip content="回复消息"> <Tooltip content="回复消息">
<div className="max-w-fit"> <div className="max-w-fit">
<PopoverTrigger> <PopoverTrigger>
<Button color="danger" variant="flat" isIconOnly radius="full"> <Button color="primary" variant="flat" isIconOnly radius="full">
<BsChatQuoteFill className="text-lg" /> <BsChatQuoteFill className="text-lg" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -38,7 +38,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
}} }}
/> />
<Button <Button
color="danger" color="primary"
variant="flat" variant="flat"
radius="full" radius="full"
isIconOnly isIconOnly

View File

@@ -10,7 +10,7 @@ const RPSInsert = () => {
return ( return (
<Tooltip content="发送猜拳"> <Tooltip content="发送猜拳">
<Button <Button
color="danger" color="primary"
variant="flat" variant="flat"
isIconOnly isIconOnly
radius="full" radius="full"

View File

@@ -35,7 +35,7 @@ const VideoInsert = () => {
<Tooltip content="发送视频"> <Tooltip content="发送视频">
<div className="max-w-fit"> <div className="max-w-fit">
<PopoverTrigger> <PopoverTrigger>
<Button color="danger" variant="flat" isIconOnly radius="full"> <Button color="primary" variant="flat" isIconOnly radius="full">
<IoVideocam className="text-xl" /> <IoVideocam className="text-xl" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -45,7 +45,7 @@ const VideoInsert = () => {
<Tooltip content="上传视频"> <Tooltip content="上传视频">
<Button <Button
className="text-lg" className="text-lg"
color="danger" color="primary"
isIconOnly isIconOnly
variant="flat" variant="flat"
radius="full" radius="full"
@@ -62,7 +62,7 @@ const VideoInsert = () => {
<PopoverTrigger tooltip="输入视频地址"> <PopoverTrigger tooltip="输入视频地址">
<Button <Button
className="text-lg" className="text-lg"
color="danger" color="primary"
isIconOnly isIconOnly
variant="flat" variant="flat"
radius="full" radius="full"
@@ -79,7 +79,7 @@ const VideoInsert = () => {
placeholder="请输入视频地址" placeholder="请输入视频地址"
/> />
<Button <Button
color="danger" color="primary"
variant="flat" variant="flat"
isIconOnly isIconOnly
radius="full" radius="full"

View File

@@ -190,7 +190,7 @@ const ChatInput = () => {
<DiceInsert /> <DiceInsert />
<RPSInsert /> <RPSInsert />
<Button <Button
color="danger" color="primary"
onPress={() => { onPress={() => {
const messages = getChatMessage() const messages = getChatMessage()
showStructuredMessage(messages) showStructuredMessage(messages)

View File

@@ -15,7 +15,7 @@ export default function ChatInputModal() {
return ( return (
<> <>
<Button onPress={onOpen} color="danger" radius="full" variant="flat"> <Button onPress={onOpen} color="primary" radius="full" variant="flat">
</Button> </Button>
<Modal <Modal
@@ -36,7 +36,7 @@ export default function ChatInputModal() {
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="danger" onPress={onClose} variant="flat"> <Button color="primary" onPress={onClose} variant="flat">
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -78,7 +78,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
{debug ? '关闭调试' : '开启调试'} {debug ? '关闭调试' : '开启调试'}
</Button> </Button>
<Button <Button
color="danger" color="primary"
startContent={<MdDeleteForever />} startContent={<MdDeleteForever />}
onPress={handleDelete} onPress={handleDelete}
> >

View File

@@ -19,7 +19,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
className={clsx( className={clsx(
'bg-opacity-60 shadow-sm md:rounded-3xl', 'bg-opacity-60 shadow-sm md:rounded-3xl',
size === 'md' 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' : 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200'
)} )}
shadow="sm" shadow="sm"

View File

@@ -33,7 +33,7 @@ export default function CreateFileModal({
<ModalHeader></ModalHeader> <ModalHeader></ModalHeader>
<ModalBody> <ModalBody>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<ButtonGroup color="danger"> <ButtonGroup color="primary">
<Button <Button
variant={fileType === 'file' ? 'solid' : 'flat'} variant={fileType === 'file' ? 'solid' : 'flat'}
onPress={() => onTypeChange('file')} onPress={() => onTypeChange('file')}
@@ -51,10 +51,10 @@ export default function CreateFileModal({
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="danger" variant="flat" onPress={onClose}> <Button color="primary" variant="flat" onPress={onClose}>
</Button> </Button>
<Button color="danger" onPress={onCreate}> <Button color="primary" onPress={onCreate}>
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -81,10 +81,10 @@ export default function FileEditModal({
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="danger" variant="flat" onPress={onClose}> <Button color="primary" variant="flat" onPress={onClose}>
</Button> </Button>
<Button color="danger" onPress={onSave}> <Button color="primary" onPress={onSave}>
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -9,6 +9,7 @@ import {
import { Spinner } from '@heroui/spinner' import { Spinner } from '@heroui/spinner'
import { useRequest } from 'ahooks' import { useRequest } from 'ahooks'
import path from 'path-browserify' import path from 'path-browserify'
import { useEffect } from 'react'
import FileManager from '@/controllers/file_manager' import FileManager from '@/controllers/file_manager'
@@ -33,6 +34,7 @@ export default function FilePreviewModal({
async () => FileManager.downloadToURL(filePath), async () => FileManager.downloadToURL(filePath),
{ {
refreshDeps: [filePath], refreshDeps: [filePath],
manual: true,
refreshDepsAction: () => { refreshDepsAction: () => {
const ext = path.extname(filePath).toLowerCase() const ext = path.extname(filePath).toLowerCase()
if (!filePath || !supportedPreviewExts.includes(ext)) { if (!filePath || !supportedPreviewExts.includes(ext)) {
@@ -43,6 +45,12 @@ export default function FilePreviewModal({
} }
) )
useEffect(() => {
if (filePath) {
run()
}
}, [filePath])
let contentElement = null let contentElement = null
if (!supportedPreviewExts.includes(ext)) { if (!supportedPreviewExts.includes(ext)) {
contentElement = <div></div> contentElement = <div></div>
@@ -74,7 +82,7 @@ export default function FilePreviewModal({
{contentElement} {contentElement}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="danger" variant="flat" onPress={onClose}> <Button color="primary" variant="flat" onPress={onClose}>
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -82,7 +82,7 @@ export default function FileTable({
setPreviewImages([]) setPreviewImages([])
setPreviewIndex(0) setPreviewIndex(0)
setShowImage(false) setShowImage(false)
}, [files]) }, [currentPath])
const onPreviewImage = (name: string, images: PreviewImage[]) => { const onPreviewImage = (name: string, images: PreviewImage[]) => {
const index = images.findIndex((image) => image.key === name) const index = images.findIndex((image) => image.key === name)
@@ -116,7 +116,7 @@ export default function FileTable({
isCompact isCompact
showControls showControls
showShadow showShadow
color="danger" color="primary"
page={page} page={page}
total={pages} total={pages}
onChange={(page) => setPage(page)} onChange={(page) => setPage(page)}
@@ -195,7 +195,7 @@ export default function FileTable({
<ButtonGroup size="sm"> <ButtonGroup size="sm">
<Button <Button
isIconOnly isIconOnly
color="danger" color="primary"
variant="flat" variant="flat"
onPress={() => onRenameRequest(file.name)} onPress={() => onRenameRequest(file.name)}
> >
@@ -203,7 +203,7 @@ export default function FileTable({
</Button> </Button>
<Button <Button
isIconOnly isIconOnly
color="danger" color="primary"
variant="flat" variant="flat"
onPress={() => onMoveRequest(file.name)} onPress={() => onMoveRequest(file.name)}
> >
@@ -211,7 +211,7 @@ export default function FileTable({
</Button> </Button>
<Button <Button
isIconOnly isIconOnly
color="danger" color="primary"
variant="flat" variant="flat"
onPress={() => onCopyPath(file.name)} onPress={() => onCopyPath(file.name)}
> >
@@ -219,7 +219,7 @@ export default function FileTable({
</Button> </Button>
<Button <Button
isIconOnly isIconOnly
color="danger" color="primary"
variant="flat" variant="flat"
onPress={() => onDownload(filePath)} onPress={() => onDownload(filePath)}
> >
@@ -227,7 +227,7 @@ export default function FileTable({
</Button> </Button>
<Button <Button
isIconOnly isIconOnly
color="danger" color="primary"
variant="flat" variant="flat"
onPress={() => onDelete(filePath)} onPress={() => onDelete(filePath)}
> >

View File

@@ -33,6 +33,7 @@ export default function ImageNameButton({
async () => FileManager.downloadToURL(filePath), async () => FileManager.downloadToURL(filePath),
{ {
refreshDeps: [filePath], refreshDeps: [filePath],
manual: true,
refreshDepsAction: () => { refreshDepsAction: () => {
const ext = path.extname(filePath).toLowerCase() const ext = path.extname(filePath).toLowerCase()
if (!filePath || !imageExts.includes(ext)) { if (!filePath || !imageExts.includes(ext)) {
@@ -52,6 +53,12 @@ export default function ImageNameButton({
} }
}, [data, name, onAddPreview]) }, [data, name, onAddPreview])
useEffect(() => {
if (filePath) {
run()
}
}, [])
return ( return (
<Button <Button
variant="light" variant="light"
@@ -63,7 +70,15 @@ export default function ImageNameButton({
) : loading || !data ? ( ) : loading || !data ? (
<Spinner size="sm" /> <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"
classNames={{
wrapper: 'w-8 h-8 flex-shrink-0'
}}
radius="sm"
/>
) )
} }
> >

View File

@@ -86,13 +86,13 @@ function DirectoryTree({
onPress={handleClick} onPress={handleClick}
className="py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md" className="py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md"
size="sm" size="sm"
color="danger" color="primary"
variant={variant} variant={variant}
startContent={ startContent={
<div <div
className={clsx( className={clsx(
'rounded-md', 'rounded-md',
isSeleted ? 'bg-danger-600' : 'bg-danger-50' isSeleted ? 'bg-primary-600' : 'bg-primary-50'
)} )}
> >
{expanded ? <IoRemove /> : <IoAdd />} {expanded ? <IoRemove /> : <IoAdd />}
@@ -105,7 +105,7 @@ function DirectoryTree({
<div> <div>
{loading ? ( {loading ? (
<div className="flex py-1 px-8"> <div className="flex py-1 px-8">
<Spinner size="sm" color="danger" /> <Spinner size="sm" color="primary" />
</div> </div>
) : ( ) : (
dirs.map((dirName) => { dirs.map((dirName) => {
@@ -155,10 +155,10 @@ export default function MoveModal({
<p className="text-sm text-default-500">{selectionInfo}</p> <p className="text-sm text-default-500">{selectionInfo}</p>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="danger" variant="flat" onPress={onClose}> <Button color="primary" variant="flat" onPress={onClose}>
</Button> </Button>
<Button color="danger" onPress={onMove}> <Button color="primary" onPress={onMove}>
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -31,10 +31,10 @@ export default function RenameModal({
<Input label="新名称" value={newFileName} onChange={onNameChange} /> <Input label="新名称" value={newFileName} onChange={onNameChange} />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="danger" variant="flat" onPress={onClose}> <Button color="primary" variant="flat" onPress={onClose}>
</Button> </Button>
<Button color="danger" onPress={onRename}> <Button color="primary" onPress={onRename}>
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -33,7 +33,7 @@ export default function Hitokoto() {
<div className="relative"> <div className="relative">
{loading && <PageLoading />} {loading && <PageLoading />}
{error ? ( {error ? (
<div className="text-danger-400">{error.message}</div> <div className="text-primary-400">{error.message}</div>
) : ( ) : (
<> <>
<div>{data?.hitokoto}</div> <div>{data?.hitokoto}</div>
@@ -52,7 +52,7 @@ export default function Hitokoto() {
isLoading={loading} isLoading={loading}
isIconOnly isIconOnly
radius="full" radius="full"
color="danger" color="primary"
variant="flat" variant="flat"
> >
<IoRefresh /> <IoRefresh />

View File

@@ -34,7 +34,7 @@ export default function HoverTiltedCard({
rotateAmplitude = 14, rotateAmplitude = 14,
showTooltip = false, showTooltip = false,
overlayContent = ( 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 NapCat
</div> </div>
), ),

View File

@@ -0,0 +1,69 @@
import { Button } from '@heroui/button'
import { Input } from '@heroui/input'
import { useRef, useState } from 'react'
export interface FileInputProps {
onChange: (file: File) => Promise<void> | void
onDelete?: () => Promise<void> | void
label?: string
accept?: string
}
const FileInput: React.FC<FileInputProps> = ({
onChange,
onDelete,
label,
accept
}) => {
const inputRef = useRef<HTMLInputElement>(null)
const [isLoading, setIsLoading] = useState(false)
return (
<div className="flex items-end gap-2">
<div className="flex-grow">
<Input
isDisabled={isLoading}
ref={inputRef}
label={label}
type="file"
placeholder="选择文件"
accept={accept}
onChange={async (e) => {
try {
setIsLoading(true)
const file = e.target.files?.[0]
if (file) {
await onChange(file)
}
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
if (inputRef.current) inputRef.current.value = ''
}
}}
/>
</div>
<Button
isDisabled={isLoading}
onPress={async () => {
try {
setIsLoading(true)
if (onDelete) await onDelete()
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
if (inputRef.current) inputRef.current.value = ''
}
}}
color="primary"
variant="flat"
size="sm"
>
</Button>
</div>
)
}
export default FileInput

View File

@@ -43,7 +43,7 @@ const ImageInput: React.FC<ImageInputProps> = ({ onChange, value, label }) => {
onChange('') onChange('')
if (inputRef.current) inputRef.current.value = '' if (inputRef.current) inputRef.current.value = ''
}} }}
color="danger" color="primary"
variant="flat" variant="flat"
size="sm" size="sm"
> >

View File

@@ -16,13 +16,13 @@ const logLevelColor: {
| 'secondary' | 'secondary'
| 'success' | 'success'
| 'warning' | 'warning'
| 'danger' | 'primary'
} = { } = {
[LogLevel.DEBUG]: 'default', [LogLevel.DEBUG]: 'default',
[LogLevel.INFO]: 'primary', [LogLevel.INFO]: 'primary',
[LogLevel.WARN]: 'warning', [LogLevel.WARN]: 'warning',
[LogLevel.ERROR]: 'danger', [LogLevel.ERROR]: 'primary',
[LogLevel.FATAL]: 'danger' [LogLevel.FATAL]: 'primary'
} }
const LogLevelSelect = (props: LogLevelSelectProps) => { const LogLevelSelect = (props: LogLevelSelectProps) => {
const { selectedKeys, onSelectionChange } = props const { selectedKeys, onSelectionChange } = props

View File

@@ -65,7 +65,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
<ModalFooter> <ModalFooter>
{showCancel && ( {showCancel && (
<Button <Button
color="danger" color="primary"
variant="light" variant="light"
onPress={() => { onPress={() => {
onCancel?.() onCancel?.()
@@ -76,7 +76,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
</Button> </Button>
)} )}
<Button <Button
color="danger" color="primary"
onPress={() => { onPress={() => {
onConfirm?.() onConfirm?.()
nativeClose() nativeClose()

View File

@@ -28,7 +28,7 @@ import type {
function displayData(data: number, loading: boolean, error?: Error) { function displayData(data: number, loading: boolean, error?: Error) {
if (error) { if (error) {
return <MdError className="text-danger-400" /> return <MdError className="text-primary-400" />
} }
if (loading) { if (loading) {
@@ -175,7 +175,7 @@ export default function NapCatRepoInfo() {
className="group h-auto py-3" className="group h-auto py-3"
endContent={ endContent={
releaseError ? ( releaseError ? (
<MdError className="text-danger-400" /> <MdError className="text-primary-400" />
) : releaseLoading ? ( ) : releaseLoading ? (
<Spinner size="sm" /> <Spinner size="sm" />
) : ( ) : (
@@ -229,7 +229,7 @@ export default function NapCatRepoInfo() {
</span> </span>
} }
startContent={ startContent={
<IconWrapper className="bg-danger/10 text-danger dark:text-danger-500"> <IconWrapper className="bg-primary/10 text-primary dark:text-primary-500">
<BookIcon /> <BookIcon />
</IconWrapper> </IconWrapper>
} }

View File

@@ -150,7 +150,7 @@ const GenericForm = <T extends keyof NetworkConfigType>({
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
color="danger" color="primary"
isDisabled={formState.isSubmitting} isDisabled={formState.isSubmitting}
variant="light" variant="light"
onPress={onClose} onPress={onClose}

View File

@@ -20,7 +20,7 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
enable: false, enable: false,
name: '', name: '',
host: '0.0.0.0', host: '0.0.0.0',
port: 3000, port: 3001,
reportSelfMessage: false, reportSelfMessage: false,
enableForcePushEvent: true, enableForcePushEvent: true,
messagePostFormat: 'array', messagePostFormat: 'array',

View File

@@ -91,7 +91,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
return ( return (
<section className="p-4 pt-14 rounded-lg shadow-md"> <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 /> <PiCatDuotone />
{data.description} {data.description}
</h1> </h1>
@@ -125,7 +125,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
/> />
<Button <Button
onPress={sendRequest} onPress={sendRequest}
color="danger" color="primary"
size="lg" size="lg"
radius="full" radius="full"
isIconOnly isIconOnly

View File

@@ -27,7 +27,7 @@ const SchemaType = ({
name = '固定值' name = '固定值'
break break
} }
let chipColor: 'primary' | 'success' | 'danger' | 'warning' | 'secondary' = let chipColor: 'primary' | 'success' | 'primary' | 'warning' | 'secondary' =
'primary' 'primary'
switch (type) { switch (type) {
case 'enum': case 'enum':
@@ -37,7 +37,7 @@ const SchemaType = ({
chipColor = 'secondary' chipColor = 'secondary'
break break
case 'array': case 'array':
chipColor = 'danger' chipColor = 'primary'
break break
case 'object': case 'object':
chipColor = 'success' chipColor = 'success'

View File

@@ -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"> <div className="w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0">
<Input <Input
className="sticky top-0 z-10 text-danger-600" className="sticky top-0 z-10 text-primary-600"
classNames={{ classNames={{
inputWrapper: inputWrapper:
'bg-opacity-30 bg-danger-50 backdrop-blur-sm border border-danger-300 mb-2', 'bg-opacity-30 bg-primary-50 backdrop-blur-sm border border-primary-300 mb-2',
input: 'bg-transparent !text-danger-400 !placeholder-danger-400' input: 'bg-transparent !text-primary-400 !placeholder-primary-400'
}} }}
radius="full" radius="full"
placeholder="搜索 API" placeholder="搜索 API"
@@ -51,7 +51,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
key={apiName} key={apiName}
shadow="none" shadow="none"
className={clsx( 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: !( hidden: !(
apiName.includes(searchValue) || 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 apiName === selectedApi
} }
)} )}
@@ -69,8 +69,8 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
<CardBody> <CardBody>
<h2 className="font-bold">{api.description}</h2> <h2 className="font-bold">{api.description}</h2>
<div <div
className={clsx('text-sm text-danger-200', { className={clsx('text-sm text-primary-200', {
'!text-danger-400': apiName === selectedApi '!text-primary-400': apiName === selectedApi
})} })}
> >
{apiName} {apiName}

View File

@@ -109,7 +109,7 @@ const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
<PopoverTrigger> <PopoverTrigger>
<Button <Button
size="sm" size="sm"
color="danger" color="primary"
variant="flat" variant="flat"
radius="full" radius="full"
isIconOnly isIconOnly

View File

@@ -30,7 +30,7 @@ const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
<PopoverTrigger> <PopoverTrigger>
<Button <Button
size="sm" size="sm"
color="danger" color="primary"
variant="flat" variant="flat"
radius="full" radius="full"
className="text-medium" className="text-medium"

View File

@@ -43,7 +43,7 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
return ( return (
<> <>
<Button onPress={onOpen} color="danger" radius="full" variant="flat"> <Button onPress={onOpen} color="primary" radius="full" variant="flat">
</Button> </Button>
<Modal <Modal
@@ -75,11 +75,11 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
<ModalFooter> <ModalFooter>
<ChatInputModal /> <ChatInputModal />
<Button color="danger" variant="flat" onPress={onClose}> <Button color="primary" variant="flat" onPress={onClose}>
</Button> </Button>
<Button <Button
color="danger" color="primary"
onPress={() => handleSendMessage(onClose)} onPress={() => handleSendMessage(onClose)}
> >

View File

@@ -10,7 +10,7 @@ function StatusTag({
color color
}: { }: {
title: string title: string
color: 'success' | 'danger' | 'warning' color: 'success' | 'primary' | 'warning'
}) { }) {
const textClassName = `text-${color} text-sm` const textClassName = `text-${color} text-sm`
const bgClassName = `bg-${color}` const bgClassName = `bg-${color}`
@@ -27,7 +27,7 @@ export default function WSStatus({ state }: WSStatusProps) {
return <StatusTag title="已连接" color="success" /> return <StatusTag title="已连接" color="success" />
} }
if (state === ReadyState.CLOSED) { if (state === ReadyState.CLOSED) {
return <StatusTag title="已关闭" color="danger" /> return <StatusTag title="已关闭" color="primary" />
} }
if (state === ReadyState.CONNECTING) { if (state === ReadyState.CONNECTING) {
return <StatusTag title="连接中" color="warning" /> return <StatusTag title="连接中" color="warning" />

View File

@@ -16,7 +16,7 @@ export interface QQInfoCardProps {
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => { const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
return ( return (
<Card <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" shadow="none"
radius="lg" radius="lg"
> >
@@ -30,7 +30,7 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
</CardBody> </CardBody>
) : ( ) : (
<CardBody className="flex-row items-center gap-2 overflow-hidden relative"> <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 /> <BsTencentQq />
</div> </div>
<div className="relative flex-shrink-0 z-10"> <div className="relative flex-shrink-0 z-10">
@@ -43,14 +43,14 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
/> />
<div <div
className={clsx( 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' data?.online ? 'bg-green-500' : 'bg-gray-500'
)} )}
></div> ></div>
</div> </div>
<div className="flex-col justify-center"> <div className="flex-col justify-center">
<div className="text-lg truncate">{data?.nick}</div> <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> </div>
</CardBody> </CardBody>
)} )}

View File

@@ -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"> <div className="bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden">
{!qrcode && ( {!qrcode && (
<div className="absolute left-2 top-2 right-2 bottom-2 bg-white bg-opacity-50 backdrop-blur flex items-center justify-center"> <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> </div>
)} )}
<QRCodeSVG size={180} value={qrcode} /> <QRCodeSVG size={180} value={qrcode} />

View 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

View File

@@ -63,7 +63,7 @@ const SideBar: React.FC<SideBarProps> = (props) => {
<div className="mt-auto mb-10 md:mb-0"> <div className="mt-auto mb-10 md:mb-0">
<Button <Button
className="w-full" className="w-full"
color="danger" color="primary"
radius="full" radius="full"
variant="light" variant="light"
onPress={toggleTheme} onPress={toggleTheme}
@@ -75,7 +75,7 @@ const SideBar: React.FC<SideBarProps> = (props) => {
</Button> </Button>
<Button <Button
className="w-full mb-2" className="w-full mb-2"
color="danger" color="primary"
radius="full" radius="full"
variant="light" variant="light"
onPress={onRevokeAuth} onPress={onRevokeAuth}

View File

@@ -55,7 +55,7 @@ const renderItems = (items: MenuItem[], children = false) => {
isActive && 'bg-opacity-60', isActive && 'bg-opacity-60',
b64img && 'backdrop-blur-md text-white' b64img && 'backdrop-blur-md text-white'
)} )}
color="danger" color="primary"
endContent={ endContent={
canOpen ? ( canOpen ? (
// div实现箭头V效果 // div实现箭头V效果
@@ -63,7 +63,9 @@ const renderItems = (items: MenuItem[], children = false) => {
className={clsx( className={clsx(
'ml-auto relative w-3 h-3 transition-transform', 'ml-auto relative w-3 h-3 transition-transform',
open && 'transform rotate-180', 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:rounded-full',
'before:content-[""]', 'before:content-[""]',
'before:block', 'before:block',
@@ -95,7 +97,7 @@ const renderItems = (items: MenuItem[], children = false) => {
className={clsx( className={clsx(
'w-3 h-1.5 rounded-full ml-auto shadow-lg', 'w-3 h-1.5 rounded-full ml-auto shadow-lg',
isActive isActive
? 'bg-danger-500 animate-spinner-ease-spin' ? 'bg-primary-500 animate-spinner-ease-spin'
: 'bg-red-300 dark:bg-white' : 'bg-red-300 dark:bg-white'
)} )}
/> />

View File

@@ -4,6 +4,8 @@ import { Chip } from '@heroui/chip'
import { Spinner } from '@heroui/spinner' import { Spinner } from '@heroui/spinner'
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip'
import { useRequest } from 'ahooks' import { useRequest } from 'ahooks'
import { useEffect } from 'react'
import { BsStars } from 'react-icons/bs'
import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6' import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6'
import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io' import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io'
import { RiMacFill } from 'react-icons/ri' import { RiMacFill } from 'react-icons/ri'
@@ -16,7 +18,6 @@ import { compareVersion } from '@/utils/version'
import WebUIManager from '@/controllers/webui_manager' import WebUIManager from '@/controllers/webui_manager'
import { GithubRelease } from '@/types/github' import { GithubRelease } from '@/types/github'
import packageJson from '../../package.json'
import TailwindMarkdown from './tailwind_markdown' import TailwindMarkdown from './tailwind_markdown'
export interface SystemInfoItemProps { export interface SystemInfoItemProps {
@@ -33,10 +34,10 @@ const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
endContent endContent
}) => { }) => {
return ( 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} {icon}
<div className="w-24">{title}</div> <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 className="ml-auto">{endContent}</div>
</div> </div>
) )
@@ -61,7 +62,7 @@ const NewVersionTip = (props: NewVersionTipProps) => {
<Button <Button
isIconOnly isIconOnly
radius="full" radius="full"
color="danger" color="primary"
variant="shadow" variant="shadow"
className="!w-5 !h-5 !min-w-0 text-small shadow-md" className="!w-5 !h-5 !min-w-0 text-small shadow-md"
onPress={() => { 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 ( return (
<Tooltip content="有新版本可用"> <Tooltip content="有新版本可用">
<Button <Button
isIconOnly isIconOnly
radius="full" radius="full"
color="danger" color="primary"
variant="shadow" variant="shadow"
className="!w-5 !h-5 !min-w-0 text-small shadow-md" className="!w-5 !h-5 !min-w-0 text-small shadow-md"
onPress={() => { onPress={() => {
@@ -121,6 +158,13 @@ const NewVersionTip = (props: NewVersionTipProps) => {
<span></span> <span></span>
<Chip color="primary">{latestVersion}</Chip> <Chip color="primary">{latestVersion}</Chip>
</div> </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"> <div className="text-sm space-y-2 !mt-4">
{middleVersions.map((versionInfo) => ( {middleVersions.map((versionInfo) => (
<div <div
@@ -190,19 +234,14 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
error: qqVersionError error: qqVersionError
} = useRequest(WebUIManager.getQQVersion) } = useRequest(WebUIManager.getQQVersion)
return ( return (
<Card className="bg-opacity-60 shadow-sm shadow-danger-50 dark:shadow-danger-100 overflow-visible flex-1"> <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-danger-500 font-extrabold"> <CardHeader className="pb-0 items-center gap-1 text-primary-500 font-extrabold">
<FaCircleInfo className="text-lg" /> <FaCircleInfo className="text-lg" />
<span></span> <span></span>
</CardHeader> </CardHeader>
<CardBody className="flex-1"> <CardBody className="flex-1">
<div className="flex flex-col justify-between h-full"> <div className="flex flex-col justify-between h-full">
<NapCatVersion /> <NapCatVersion />
<SystemInfoItem
title="WebUI 版本"
icon={<IoLogoChrome className="text-xl" />}
value={packageJson.version}
/>
<SystemInfoItem <SystemInfoItem
title="QQ 版本" title="QQ 版本"
icon={<FaQq className="text-lg" />} 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 <SystemInfoItem
title="系统版本" title="系统版本"
icon={<RiMacFill className="text-xl" />} icon={<RiMacFill className="text-xl" />}

View File

@@ -55,7 +55,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
} }
return ( 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"> <div className="absolute h-full right-0 top-0">
<Image <Image
src={bkg} src={bkg}
@@ -69,7 +69,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
</div> </div>
<CardBody className="overflow-visible md:flex-row gap-4 items-center justify-stretch z-10"> <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"> <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" /> <GiCpu className="text-xl" />
<span>CPU</span> <span>CPU</span>
</h2> </h2>
@@ -88,7 +88,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
unit="%" unit="%"
/> />
</div> </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" /> <BiSolidMemoryCard className="text-xl" />
<span></span> <span></span>
</h2> </h2>

View File

@@ -62,7 +62,7 @@ export const Tab = forwardRef<HTMLDivElement, TabProps>(
className={clsx( className={clsx(
'px-2 py-1 flex items-center gap-1 text-sm font-medium border-b-2 transition-colors', 'px-2 py-1 flex items-center gap-1 text-sm font-medium border-b-2 transition-colors',
isSelected isSelected
? 'border-danger text-danger' ? 'border-primary text-primary'
: 'border-transparent hover:border-default', : 'border-transparent hover:border-default',
className className
)} )}

View File

@@ -99,7 +99,7 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
if (theme === 'dark') { if (theme === 'dark') {
terminalRef.current.options.theme = { terminalRef.current.options.theme = {
background: '#00000000', background: '#00000000',
black: '#000000', black: '#ffffff',
red: '#cd3131', red: '#cd3131',
green: '#0dbc79', green: '#0dbc79',
yellow: '#e5e510', yellow: '#e5e510',

View File

@@ -35,6 +35,7 @@ const AudioProvider: React.FC<MusicProviderProps> = ({ children }) => {
const [musicId, setMusicId] = useState<number>(0) const [musicId, setMusicId] = useState<number>(0)
const [playMode, setPlayMode] = useState<PlayMode>(PlayMode.Loop) const [playMode, setPlayMode] = useState<PlayMode>(PlayMode.Loop)
const music = musicList.find((music) => music.id === musicId) const music = musicList.find((music) => music.id === musicId)
const [token] = useLocalStorage(key.token, '')
const onNext = () => { const onNext = () => {
const nextID = getNextMusic(musicList, musicId, playMode) const nextID = getNextMusic(musicList, musicId, playMode)
setMusicId(nextID) setMusicId(nextID)
@@ -60,8 +61,8 @@ const AudioProvider: React.FC<MusicProviderProps> = ({ children }) => {
setMusicId(res[0].id) setMusicId(res[0].id)
} }
useEffect(() => { useEffect(() => {
fetchMusicList(listId) if (listId && token) fetchMusicList(listId)
}, [listId]) }, [listId, token])
return ( return (
<AudioContext.Provider <AudioContext.Provider
value={{ value={{

View File

@@ -196,4 +196,26 @@ export default class FileManager {
) )
return data.data return data.data
} }
public static async uploadWebUIFont(file: File) {
const formData = new FormData()
formData.append('file', file)
const { data } = await serverRequest.post<ServerResponse<boolean>>(
'/File/font/upload/webui',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
)
return data.data
}
public static async deleteWebUIFont() {
const { data } = await serverRequest.post<ServerResponse<boolean>>(
'/File/font/delete/webui'
)
return data.data
}
} }

View 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 }
}

View File

@@ -79,7 +79,7 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
}, [location.pathname]) }, [location.pathname])
return ( return (
<div <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={{ style={{
backgroundImage: `url(${b64img})`, backgroundImage: `url(${b64img})`,
backgroundSize: 'cover' backgroundSize: 'cover'
@@ -99,9 +99,9 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
<div <div
className={clsx( className={clsx(
'h-10 flex items-center font-bold text-xl backdrop-blur-lg rounded-full', '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', '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' 'z-30 m-2 mb-0 sticky top-2 left-0'
)} )}
> >

View File

@@ -1,12 +1,19 @@
import { Chip } from '@heroui/chip' import { Card, CardBody } from '@heroui/card'
import { Image } from '@heroui/image' import { Image } from '@heroui/image'
import { Link } from '@heroui/link'
import { Skeleton } from '@heroui/skeleton'
import { Spinner } from '@heroui/spinner' import { Spinner } from '@heroui/spinner'
import { useRequest } from 'ahooks' 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 HoverTiltedCard from '@/components/hover_titled_card'
import NapCatRepoInfo from '@/components/napcat_repo_info' 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 logo from '@/assets/images/logo.png'
import WebUIManager from '@/controllers/webui_manager' import WebUIManager from '@/controllers/webui_manager'
@@ -14,54 +21,177 @@ import WebUIManager from '@/controllers/webui_manager'
function VersionInfo() { function VersionInfo() {
const { data, loading, error } = useRequest(WebUIManager.getPackageInfo) const { data, loading, error } = useRequest(WebUIManager.getPackageInfo)
return ( return (
<div className="flex items-center gap-2 mb-5"> <div className="flex items-center gap-2 text-2xl font-bold">
<Chip <div className="flex items-center gap-2">
startContent={ <div className="text-primary-500 drop-shadow-md">NapCat</div>
<Chip color="warning" size="sm" className="-ml-0.5 select-none">
NapCat
</Chip>
}
>
{error ? ( {error ? (
error.message error.message
) : loading ? ( ) : loading ? (
<Spinner size="sm" /> <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> </div>
) )
} }
export default function AboutPage() { 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 ( return (
<> <>
<title> NapCat WebUI</title> <title> NapCat WebUI</title>
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10"> <section className="max-w-7xl py-8 md:py-10 px-5 mx-auto space-y-10">
<div className="max-w-full w-[1000px] px-5 flex flex-col items-center"> <div className="w-full flex flex-col md:flex-row gap-4">
<div className="flex flex-col md:flex-row items-center mb-6"> <div className="flex flex-col md:flex-row items-center">
<HoverTiltedCard imageSrc={logo} /> <HoverTiltedCard imageSrc={logo} overlayContent="" />
</div> </div>
<VersionInfo /> <div className="flex-1 flex flex-col gap-2 py-2">
<div className="mb-6 flex flex-col items-center gap-4"> <VersionInfo />
<p <div className="space-y-1">
className={clsx( <p className="font-bold text-primary-400">NapCat ?</p>
title({ <p className="text-default-800">
color: 'cyan', TypeScript构建的Bot框架,,QQ
shadow: true Node模块提供给客户端的接口,Bot的功能.
}), </p>
'!text-3xl' <p className="font-bold text-primary-400"></p>
)} <p className="text-default-800">
> QQ
NapCat Contributors 便使 OneBot HTTP /
</p> WebSocket
<Image QQ发送接口之类的接口
className="w-[600px] max-w-full pointer-events-none select-none" </p>
src="https://contrib.rocks/image?repo=bietiaop/NapCatQQ" </div>
alt="Contributors"
/>
</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 /> <NapCatRepoInfo />
</div> </div>
</section> </section>

View File

@@ -7,11 +7,13 @@ import toast from 'react-hot-toast'
import key from '@/const/key' import key from '@/const/key'
import SaveButtons from '@/components/button/save_buttons' import SaveButtons from '@/components/button/save_buttons'
import FileInput from '@/components/input/file_input'
import ImageInput from '@/components/input/image_input' import ImageInput from '@/components/input/image_input'
import useMusic from '@/hooks/use-music' import useMusic from '@/hooks/use-music'
import { siteConfig } from '@/config/site' import { siteConfig } from '@/config/site'
import FileManager from '@/controllers/file_manager'
const WebUIConfigCard = () => { const WebUIConfigCard = () => {
const { const {
@@ -59,17 +61,47 @@ const WebUIConfigCard = () => {
return ( return (
<> <>
<title>WebUI配置 - NapCat WebUI</title> <title>WebUI配置 - NapCat WebUI</title>
<Controller <div className="flex flex-col gap-2">
control={control} <div className="flex-shrink-0 w-full">WebUI字体</div>
name="musicListID" <div className="text-sm text-default-400">
render={({ field }) => (
<Input <FileInput
{...field} label="中文字体"
label="网易云音乐歌单ID网页内音乐播放器" onChange={async (file) => {
placeholder="请输入歌单ID" try {
await FileManager.uploadWebUIFont(file)
toast.success('上传成功')
setTimeout(() => {
window.location.reload()
}, 1000)
} catch (error) {
toast.error('上传失败: ' + (error as Error).message)
}
}}
onDelete={async () => {
try {
await FileManager.deleteWebUIFont()
} catch (error) {
toast.error('删除失败: ' + (error as Error).message)
}
}}
/> />
)} </div>
/> </div>
<div className="flex flex-col gap-2">
<div className="flex-shrink-0 w-full">WebUI音乐播放器</div>
<Controller
control={control}
name="musicListID"
render={({ field }) => (
<Input
{...field}
label="网易云音乐歌单ID网页内音乐播放器"
placeholder="请输入歌单ID"
/>
)}
/>
</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex-shrink-0 w-full"></div> <div className="flex-shrink-0 w-full"></div>
<Controller <Controller

View File

@@ -41,7 +41,7 @@ export default function HttpDebug() {
> >
<Button <Button
isIconOnly isIconOnly
color="danger" color="primary"
radius="md" radius="md"
variant="shadow" variant="shadow"
size="sm" size="sm"

View File

@@ -15,7 +15,7 @@ import { useWebSocketDebug } from '@/hooks/use-websocket-debug'
export default function WSDebug() { export default function WSDebug() {
const url = new URL(window.location.origin) const url = new URL(window.location.origin)
url.port = '3000' url.port = '3001'
url.protocol = 'ws:' url.protocol = 'ws:'
const defaultWsUrl = url.href const defaultWsUrl = url.href
const [socketConfig, setSocketConfig] = useLocalStorage(key.wsDebugConfig, { 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"> <div className="flex-shrink-0 flex gap-2 col-span-2 md:col-span-1">
<Button <Button
color="danger" color="primary"
onPress={handleConnect} onPress={handleConnect}
size="lg" size="lg"
radius="full" radius="full"

View File

@@ -332,7 +332,7 @@ export default function FileManagerPage() {
<div className="p-4"> <div className="p-4">
<div className="mb-4 flex items-center gap-4 sticky top-14 z-10 bg-content1 py-1"> <div className="mb-4 flex items-center gap-4 sticky top-14 z-10 bg-content1 py-1">
<Button <Button
color="danger" color="primary"
size="sm" size="sm"
isIconOnly isIconOnly
variant="flat" variant="flat"
@@ -343,7 +343,7 @@ export default function FileManagerPage() {
</Button> </Button>
<Button <Button
color="danger" color="primary"
size="sm" size="sm"
isIconOnly isIconOnly
variant="flat" variant="flat"
@@ -354,7 +354,7 @@ export default function FileManagerPage() {
</Button> </Button>
<Button <Button
color="danger" color="primary"
isLoading={loading} isLoading={loading}
size="sm" size="sm"
isIconOnly isIconOnly
@@ -365,7 +365,7 @@ export default function FileManagerPage() {
<MdRefresh /> <MdRefresh />
</Button> </Button>
<Button <Button
color="danger" color="primary"
size="sm" size="sm"
isIconOnly isIconOnly
variant="flat" variant="flat"
@@ -379,7 +379,7 @@ export default function FileManagerPage() {
selectedFiles === 'all') && ( selectedFiles === 'all') && (
<> <>
<Button <Button
color="danger" color="primary"
size="sm" size="sm"
variant="flat" variant="flat"
onPress={handleBatchDelete} onPress={handleBatchDelete}
@@ -391,7 +391,7 @@ export default function FileManagerPage() {
) )
</Button> </Button>
<Button <Button
color="danger" color="primary"
size="sm" size="sm"
variant="flat" variant="flat"
onPress={() => { onPress={() => {
@@ -406,7 +406,7 @@ export default function FileManagerPage() {
) )
</Button> </Button>
<Button <Button
color="danger" color="primary"
size="sm" size="sm"
variant="flat" variant="flat"
onPress={handleBatchDownload} onPress={handleBatchDownload}

View File

@@ -105,7 +105,7 @@ const DashboardIndexPage: React.FC = () => {
<SystemStatusCard setArchInfo={setArchInfo} /> <SystemStatusCard setArchInfo={setArchInfo} />
</div> </div>
<Networks /> <Networks />
<Card className="bg-opacity-60 shadow-sm shadow-danger-50"> <Card className="bg-opacity-60 shadow-sm shadow-primary-50">
<CardBody> <CardBody>
<Hitokoto /> <Hitokoto />
</CardBody> </CardBody>

View File

@@ -133,7 +133,7 @@ export default function TerminalPage() {
size="sm" size="sm"
className="min-w-0 w-4 h-4 flex-shrink-0" className="min-w-0 w-4 h-4 flex-shrink-0"
onPress={() => closeTerminal(tab.id)} onPress={() => closeTerminal(tab.id)}
color={selectedTab === tab.id ? 'danger' : 'default'} color={selectedTab === tab.id ? 'primary' : 'default'}
> >
<IoClose /> <IoClose />
</Button> </Button>
@@ -143,7 +143,7 @@ export default function TerminalPage() {
</TabList> </TabList>
<Button <Button
isIconOnly isIconOnly
color="danger" color="primary"
size="sm" size="sm"
variant="flat" variant="flat"
onPress={createNewTerminal} onPress={createNewTerminal}

View File

@@ -1,46 +1,35 @@
import { Spinner } from '@heroui/spinner'
import { AnimatePresence, motion } from 'motion/react' 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 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() { export default function IndexPage() {
const location = useLocation() const location = useLocation()
return ( return (
<DefaultLayout> <DefaultLayout>
<AnimatePresence mode="wait"> <Suspense
<motion.div fallback={
key={location.pathname} <div className="flex justify-center px-10">
initial={{ opacity: 0, y: 50 }} <Spinner />
animate={{ opacity: 1, y: 0 }} </div>
exit={{ opacity: 0, y: -50 }} }
transition={{ duration: 0.3 }} >
> <AnimatePresence mode="wait">
<Routes location={location} key={location.pathname}> <motion.div
<Route element={<DashboardIndexPage />} path="/" /> key={location.pathname}
<Route element={<NetworkPage />} path="/network" /> initial={{ opacity: 0, y: 20 }}
<Route element={<ConfigPage />} path="/config" /> animate={{ opacity: 1, y: 0 }}
<Route element={<LogsPage />} path="/logs" /> transition={{
<Route element={<DebugPage />} path="/debug"> type: 'tween',
<Route path="ws" element={<WSDebug />} /> ease: 'easeInOut'
<Route path="http" element={<HttpDebug />} /> }}
</Route> >
<Route element={<FileManagerPage />} path="/file_manager" /> <Outlet />
<Route element={<TerminalPage />} path="/terminal" /> </motion.div>
<Route element={<AboutPage />} path="/about" /> </AnimatePresence>
</Routes> </Suspense>
</motion.div>
</AnimatePresence>
</DefaultLayout> </DefaultLayout>
) )
} }

View File

@@ -13,5 +13,74 @@ export default {
extend: {} extend: {}
}, },
darkMode: 'class', 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'
}
}
}
}
})
]
} }

View File

@@ -2,7 +2,7 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "4.5.1", "version": "4.5.12",
"scripts": { "scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1", "build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1", "build:framework": "npm run build:webui && vite build --mode framework || exit 1",

View File

@@ -2,73 +2,73 @@ import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
import type { NapCatCore } from '@/core'; import type { NapCatCore } from '@/core';
import json5 from 'json5'; import json5 from 'json5';
import Ajv, { AnySchema, ValidateFunction } from 'ajv';
export abstract class ConfigBase<T> { export abstract class ConfigBase<T> {
name: string; name: string;
core: NapCatCore; core: NapCatCore;
configPath: string; configPath: string;
configData: T = {} as T; 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.name = name;
this.core = core; this.core = core;
this.configPath = configPath; this.configPath = configPath;
this.ajv = new Ajv({ useDefaults: true, coerceTypes: true });
this.validate = this.ajv.compile<T>(ConfigSchema);
fs.mkdirSync(this.configPath, { recursive: true }); fs.mkdirSync(this.configPath, { recursive: true });
this.read(copy_default); this.read();
} }
protected getKeys(): string[] | null { getConfigPath(pathName?: string): string {
// 决定 key 在json配置文件中的顺序 const filename = pathName ? `${this.name}_${pathName}.json` : `${this.name}.json`;
return null; return path.join(this.configPath, filename);
} }
getConfigPath(pathName: string | undefined): string { read(): T {
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 {
const configPath = this.getConfigPath(this.core.selfInfo.uin); const configPath = this.getConfigPath(this.core.selfInfo.uin);
if (!fs.existsSync(configPath) && copy_default) { const defaultConfigPath = this.getConfigPath();
try { if (!fs.existsSync(configPath)) {
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8')); if (fs.existsSync(defaultConfigPath)) {
this.core.context.logger.log('[Core] [Config] 配置文件创建成功!\n'); this.configData = this.loadConfig(defaultConfigPath);
} catch (e: unknown) {
this.core.context.logger.logError('[Core] [Config] 创建配置文件时发生错误:', (e as Error).message);
} }
} else if (!fs.existsSync(configPath) && !copy_default) { this.save();
fs.writeFileSync(configPath, '{}'); return this.configData;
} }
return this.loadConfig(configPath);
}
private loadConfig(configPath: string): T {
try { 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); this.core.context.logger.logDebug(`[Core] [Config] 配置文件${configPath}加载`, this.configData);
return this.configData; return this.configData;
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof SyntaxError) { this.handleError(e, '读取配置文件时发生错误');
this.core.context.logger.logError('[Core] [Config] 配置文件格式错误,请检查配置文件:', e.message);
} else {
this.core.context.logger.logError('[Core] [Config] 读取配置文件时发生错误:', (e as Error).message);
}
return {} as T; return {} as T;
} }
} }
save(newConfigData: T = this.configData): void {
save(newConfigData: T = this.configData) { const configPath = this.getConfigPath(this.core.selfInfo.uin);
const selfInfo = this.core.selfInfo; this.validate(newConfigData);
this.configData = newConfigData; this.configData = newConfigData;
const configPath = this.getConfigPath(selfInfo.uin);
try { try {
fs.writeFileSync(configPath, JSON.stringify(newConfigData, this.getKeys(), 2)); fs.writeFileSync(configPath, JSON.stringify(this.configData, null, 2));
} catch (e: unknown) { } 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);
}
}
}

View File

@@ -1 +1 @@
export const napCatVersion = '4.5.1'; export const napCatVersion = '4.5.12';

View File

@@ -43,7 +43,7 @@ export class NTQQFileApi {
this.rkeyManager = new RkeyManager([ this.rkeyManager = new RkeyManager([
'https://rkey.napneko.icu/rkeys' 'https://rkey.napneko.icu/rkeys'
], ],
this.context.logger this.context.logger
); );
} }
@@ -300,18 +300,18 @@ export class NTQQFileApi {
element.elementType === ElementType.FILE element.elementType === ElementType.FILE
) { ) {
switch (element.elementType) { switch (element.elementType) {
case ElementType.PIC: case ElementType.PIC:
element.picElement!.sourcePath = elementResults?.[elementIndex] ?? ''; element.picElement!.sourcePath = elementResults?.[elementIndex] ?? '';
break; break;
case ElementType.VIDEO: case ElementType.VIDEO:
element.videoElement!.filePath = elementResults?.[elementIndex] ?? ''; element.videoElement!.filePath = elementResults?.[elementIndex] ?? '';
break; break;
case ElementType.PTT: case ElementType.PTT:
element.pttElement!.filePath = elementResults?.[elementIndex] ?? ''; element.pttElement!.filePath = elementResults?.[elementIndex] ?? '';
break; break;
case ElementType.FILE: case ElementType.FILE:
element.fileElement!.filePath = elementResults?.[elementIndex] ?? ''; element.fileElement!.filePath = elementResults?.[elementIndex] ?? '';
break; break;
} }
elementIndex++; elementIndex++;
} }

View File

@@ -1,11 +1,21 @@
import { ConfigBase } from '@/common/config-base'; import { ConfigBase } from '@/common/config-base';
import napCatDefaultConfig from '@/core/external/napcat.json';
import { NapCatCore } from '@/core'; 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> { export type NapcatConfig = Static<typeof NapcatConfigSchema>;
constructor(core: NapCatCore, configPath: string) {
super('napcat', core, configPath); export class NapCatConfigLoader extends ConfigBase<NapcatConfig> {
constructor(core: NapCatCore, configPath: string, schema: AnySchema) {
super('napcat', core, configPath, schema);
} }
} }

View File

@@ -25,7 +25,7 @@ import fs from 'node:fs';
import { hostname, systemName, systemVersion } from '@/common/system'; import { hostname, systemName, systemVersion } from '@/common/system';
import { NTEventWrapper } from '@/common/event'; import { NTEventWrapper } from '@/common/event';
import { KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types'; 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 os from 'node:os';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners'; import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { proxiedListenerOf } from '@/common/proxy-handler'; import { proxiedListenerOf } from '@/common/proxy-handler';
@@ -99,7 +99,7 @@ export class NapCatCore {
this.context = context; this.context = context;
this.util = this.context.wrapper.NodeQQNTWrapperUtil; this.util = this.context.wrapper.NodeQQNTWrapperUtil;
this.eventWrapper = new NTEventWrapper(context.session); 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 = { this.apis = {
FileApi: new NTQQFileApi(this.context, this), FileApi: new NTQQFileApi(this.context, this),
SystemApi: new NTQQSystemApi(this.context, this), SystemApi: new NTQQSystemApi(this.context, this),

View File

@@ -1,20 +1,22 @@
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { PacketContext } from '@/core/packet/context/packetContext'; import {PacketContext} from '@/core/packet/context/packetContext';
import * as trans from '@/core/packet/transformer'; import * as trans from '@/core/packet/transformer';
import { PacketMsg } from '@/core/packet/message/message'; import {PacketMsg} from '@/core/packet/message/message';
import { import {
PacketMsgFileElement, PacketMsgFileElement,
PacketMsgPicElement, PacketMsgPicElement,
PacketMsgPttElement, PacketMsgPttElement,
PacketMsgVideoElement PacketMsgVideoElement
} from '@/core/packet/message/element'; } from '@/core/packet/message/element';
import { ChatType } from '@/core'; import {ChatType, MsgSourceType, NTMsgType, RawMessage} from '@/core';
import { MiniAppRawData, MiniAppReqParams } from '@/core/packet/entities/miniApp'; import {MiniAppRawData, MiniAppReqParams} from '@/core/packet/entities/miniApp';
import { AIVoiceChatType } from '@/core/packet/entities/aiChat'; import {AIVoiceChatType} from '@/core/packet/entities/aiChat';
import { NapProtoDecodeStructType, NapProtoEncodeStructType } from '@napneko/nap-proto-core'; import {NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg} from '@napneko/nap-proto-core';
import { IndexNode, MsgInfo } from '@/core/packet/transformer/proto'; import {IndexNode, LongMsgResult, MsgInfo} from '@/core/packet/transformer/proto';
import { OidbPacket } from '@/core/packet/transformer/base'; import {OidbPacket} from '@/core/packet/transformer/base';
import { ImageOcrResult } from '@/core/packet/entities/ocrResult'; import {ImageOcrResult} from '@/core/packet/entities/ocrResult';
import {gunzipSync} from 'zlib';
import {PacketMsgConverter} from '@/core/packet/message/converter';
export class PacketOperationContext { export class PacketOperationContext {
private readonly context: PacketContext; private readonly context: PacketContext;
@@ -57,10 +59,10 @@ export class PacketOperationContext {
const res = trans.GetStrangerInfo.parse(resp); const res = trans.GetStrangerInfo.parse(resp);
const extBigInt = BigInt(res.data.status.value); const extBigInt = BigInt(res.data.status.value);
if (extBigInt <= 10n) { if (extBigInt <= 10n) {
return { status: Number(extBigInt) * 10, ext_status: 0 }; return {status: Number(extBigInt) * 10, ext_status: 0};
} }
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn));
return { status: 10, ext_status: status }; return {status: 10, ext_status: status};
} catch { } catch {
return undefined; return undefined;
} }
@@ -77,13 +79,13 @@ export class PacketOperationContext {
const reqList = msg.flatMap(m => const reqList = msg.flatMap(m =>
m.msg.map(e => { m.msg.map(e => {
if (e instanceof PacketMsgPicElement) { if (e instanceof PacketMsgPicElement) {
return this.context.highway.uploadImage({ chatType, peerUid }, e); return this.context.highway.uploadImage({chatType, peerUid}, e);
} else if (e instanceof PacketMsgVideoElement) { } else if (e instanceof PacketMsgVideoElement) {
return this.context.highway.uploadVideo({ chatType, peerUid }, e); return this.context.highway.uploadVideo({chatType, peerUid}, e);
} else if (e instanceof PacketMsgPttElement) { } else if (e instanceof PacketMsgPttElement) {
return this.context.highway.uploadPtt({ chatType, peerUid }, e); return this.context.highway.uploadPtt({chatType, peerUid}, e);
} else if (e instanceof PacketMsgFileElement) { } else if (e instanceof PacketMsgFileElement) {
return this.context.highway.uploadFile({ chatType, peerUid }, e); return this.context.highway.uploadFile({chatType, peerUid}, e);
} }
return null; return null;
}).filter(Boolean) }).filter(Boolean)
@@ -116,6 +118,13 @@ export class PacketOperationContext {
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
} }
async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupImage.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async ImageOCR(imgUrl: string) { async ImageOCR(imgUrl: string) {
const req = trans.ImageOCR.build(imgUrl); const req = trans.ImageOCR.build(imgUrl);
const resp = await this.context.client.sendOidbPacket(req, true); const resp = await this.context.client.sendOidbPacket(req, true);
@@ -195,4 +204,74 @@ export class PacketOperationContext {
return res.msgInfo; return res.msgInfo;
} }
} }
async FetchForwardMsg(res_id: string): Promise<RawMessage[]> {
const req = trans.DownloadForwardMsg.build(this.context.napcore.basicInfo.uid, res_id);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadForwardMsg.parse(resp);
const inflate = gunzipSync(res.result.payload);
const result = new NapProtoMsg(LongMsgResult).decode(inflate);
const main = result.action.find((r) => r.actionCommand === 'MultiMsg');
if (!main?.actionData.msgBody) {
throw new Error('msgBody is empty');
}
const messagesPromises = main.actionData.msgBody.map(async (msg) => {
if (!msg?.body?.richText?.elems) {
throw new Error('msg.body.richText.elems is empty');
}
const rawChains = new PacketMsgConverter().packetMsgToRaw(msg?.body?.richText?.elems);
const elements = await Promise.all(
rawChains.map(async ([element, rawElem]) => {
if (element.picElement && rawElem?.commonElem?.pbElem) {
const extra = new NapProtoMsg(MsgInfo).decode(rawElem.commonElem.pbElem);
const index = extra?.msgInfoBody[0]?.index;
if (msg?.responseHead.grp !== undefined) {
const groupUin = msg?.responseHead.grp?.groupUin ?? 0;
element.picElement = {
...element.picElement,
originImageUrl: await this.GetGroupImageUrl(groupUin, index!)
};
} else {
element.picElement = {
...element.picElement,
originImageUrl: await this.GetImageUrl(this.context.napcore.basicInfo.uid, index!)
};
}
return element;
}
return element;
})
);
return {
chatType: ChatType.KCHATTYPEGROUP,
elements: elements,
guildId: '',
isOnlineMsg: false,
msgId: '7467703692092974645', // TODO: no necessary
msgRandom: '0',
msgSeq: String(msg.contentHead.sequence ?? 0),
msgTime: String(msg.contentHead.timeStamp ?? 0),
msgType: NTMsgType.KMSGTYPEMIX,
parentMsgIdList: [],
parentMsgPeer: {
chatType: ChatType.KCHATTYPEGROUP,
peerUid: String(msg?.responseHead.grp?.groupUin ?? 0),
},
peerName: '',
peerUid: '1094950020',
peerUin: '1094950020',
recallTime: '0',
records: [],
sendNickName: msg?.responseHead.grp?.memberName ?? '',
sendRemarkName: msg?.responseHead.grp?.memberName ?? '',
senderUid: '',
senderUin: '1094950020',
sourceType: MsgSourceType.K_DOWN_SOURCETYPE_UNKNOWN,
subMsgType: 1,
};
});
return await Promise.all(messagesPromises);
}
} }

View File

@@ -1,8 +1,8 @@
import { import {
Peer,
ChatType, ChatType,
ElementType, ElementType,
MessageElement, MessageElement,
Peer,
RawMessage, RawMessage,
SendArkElement, SendArkElement,
SendFaceElement, SendFaceElement,
@@ -31,7 +31,9 @@ import {
PacketMsgVideoElement, PacketMsgVideoElement,
PacketMultiMsgElement PacketMultiMsgElement
} from '@/core/packet/message/element'; } from '@/core/packet/message/element';
import { PacketMsg, PacketSendMsgElement } from '@/core/packet/message/message'; import {PacketMsg, PacketSendMsgElement} from '@/core/packet/message/message';
import {NapProtoDecodeStructType} from '@napneko/nap-proto-core';
import {Elem} from '@/core/packet/transformer/proto';
const SupportedElementTypes = [ const SupportedElementTypes = [
ElementType.TEXT, ElementType.TEXT,
@@ -154,4 +156,16 @@ export class PacketMsgConverter {
}).filter((e) => e !== null) }).filter((e) => e !== null)
}; };
} }
packetMsgToRaw(msg: NapProtoDecodeStructType<typeof Elem>[]): [MessageElement, NapProtoDecodeStructType<typeof Elem> | null][] {
const converters = [PacketMsgTextElement.parseElement,
PacketMsgAtElement.parseElement, PacketMsgReplyElement.parseElement, PacketMsgPicElement.parseElement];
return msg.map((element) => {
for (const converter of converters) {
const result = converter(element);
if (result) return result;
}
return null;
}).filter((e) => e !== null);
}
} }

View File

@@ -1,20 +1,22 @@
import * as zlib from 'node:zlib'; import * as zlib from 'node:zlib';
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core'; import {NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg} from '@napneko/nap-proto-core';
import { import {
CustomFace, CustomFace,
Elem, Elem,
FileExtra,
GroupFileExtra,
MarkdownData, MarkdownData,
MentionExtra, MentionExtra,
MsgInfo,
NotOnlineImage, NotOnlineImage,
OidbSvcTrpcTcp0XE37_800Response,
QBigFaceExtra, QBigFaceExtra,
QSmallFaceExtra, QSmallFaceExtra,
MsgInfo,
OidbSvcTrpcTcp0XE37_800Response,
FileExtra,
GroupFileExtra
} from '@/core/packet/transformer/proto'; } from '@/core/packet/transformer/proto';
import { import {
ElementType,
FaceType, FaceType,
MessageElement,
NTMsgAtType, NTMsgAtType,
PicType, PicType,
SendArkElement, SendArkElement,
@@ -29,8 +31,11 @@ import {
SendTextElement, SendTextElement,
SendVideoElement SendVideoElement
} from '@/core'; } from '@/core';
import { ForwardMsgBuilder } from '@/common/forward-msg-builder'; import {ForwardMsgBuilder} from '@/common/forward-msg-builder';
import { PacketMsg, PacketSendMsgElement } from '@/core/packet/message/message'; import {PacketMsg, PacketSendMsgElement} from '@/core/packet/message/message';
export type ParseElementFnR = [MessageElement, NapProtoDecodeStructType<typeof Elem> | null] | undefined;
type ParseElementFn = (elem: NapProtoDecodeStructType<typeof Elem>) => ParseElementFnR;
// raw <-> packet // raw <-> packet
// TODO: SendStructLongMsgElement // TODO: SendStructLongMsgElement
@@ -51,6 +56,8 @@ export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
return []; return [];
} }
static parseElement: ParseElementFn;
toPreview(): string { toPreview(): string {
return '[暂不支持该消息类型喵~]'; return '[暂不支持该消息类型喵~]';
} }
@@ -72,11 +79,30 @@ export class PacketMsgTextElement extends IPacketMsgElement<SendTextElement> {
}]; }];
} }
static override parseElement = (elem: NapProtoDecodeStructType<typeof Elem>): ParseElementFnR => {
if (elem.text?.str && (elem.text?.attr6Buf === undefined || elem.text?.attr6Buf?.length === 0)) {
return [{
textElement: {
content: elem.text?.str,
atType: NTMsgAtType.ATTYPEUNKNOWN,
atUid: '',
atTinyId: '',
atNtUid: '',
},
elementType: ElementType.UNKNOWN,
elementId: '',
}, null];
}
return undefined;
};
override toPreview(): string { override toPreview(): string {
return this.text; return this.text;
} };
} }
export class PacketMsgAtElement extends PacketMsgTextElement { export class PacketMsgAtElement extends PacketMsgTextElement {
targetUid: string; targetUid: string;
atAll: boolean; atAll: boolean;
@@ -101,6 +127,22 @@ export class PacketMsgAtElement extends PacketMsgTextElement {
} }
}]; }];
} }
static override parseElement = (elem: NapProtoDecodeStructType<typeof Elem>): ParseElementFnR => {
if (elem.text?.str && (elem.text?.attr6Buf?.length ?? 100) >= 11) {
return [{
textElement: {
content: elem.text?.str,
atType: NTMsgAtType.ATTYPEONE,
atUid: String(Buffer.from(elem.text!.attr6Buf!).readUInt32BE(7)), // FIXME: hack
atTinyId: '',
atNtUid: '',
},
elementType: ElementType.UNKNOWN,
elementId: '',
}, null];
}
return undefined;
};
} }
export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> { export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
@@ -137,21 +179,28 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
pbReserve: { pbReserve: {
messageId: this.messageId, 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,
}]; }];
} }
static override parseElement = (elem: NapProtoDecodeStructType<typeof Elem>): ParseElementFnR => {
if (elem.srcMsg && elem.srcMsg.pbReserve) {
const reserve = elem.srcMsg.pbReserve;
return [{
replyElement: {
replayMsgSeq: String(reserve.friendSeq ?? elem.srcMsg?.origSeqs?.[0] ?? 0),
replayMsgId: String(reserve.messageId ?? 0),
senderUin: String(elem?.srcMsg ?? 0)
},
elementType: ElementType.UNKNOWN,
elementId: '',
}, null];
}
return undefined;
};
override toPreview(): string { override toPreview(): string {
return '[回复消息]'; return '[回复消息]';
} }
@@ -207,6 +256,46 @@ export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
} }
} }
static override parseElement = (elem: NapProtoDecodeStructType<typeof Elem>): ParseElementFnR => {
if (elem.face?.index) {
return [{
faceElement: {
faceIndex: elem.face.index,
faceType: FaceType.Normal
},
elementType: ElementType.UNKNOWN,
elementId: '',
}, null];
}
if (elem?.commonElem?.serviceType === 37 && elem?.commonElem?.pbElem) {
const qface = new NapProtoMsg(QBigFaceExtra).decode(elem?.commonElem?.pbElem);
if (qface?.faceId) {
return [{
faceElement: {
faceIndex: qface.faceId,
faceType: FaceType.Normal
},
elementType: ElementType.UNKNOWN,
elementId: '',
}, null];
}
}
if (elem?.commonElem?.serviceType === 33 && elem?.commonElem?.pbElem) {
const qface = new NapProtoMsg(QSmallFaceExtra).decode(elem?.commonElem?.pbElem);
if (qface?.faceId) {
return [{
faceElement: {
faceIndex: qface.faceId,
faceType: FaceType.Normal
},
elementType: ElementType.UNKNOWN,
elementId: '',
}, null];
}
}
return undefined;
};
override toPreview(): string { override toPreview(): string {
return '[表情]'; return '[表情]';
} }
@@ -295,6 +384,60 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
}]; }];
} }
static override parseElement = (elem: NapProtoDecodeStructType<typeof Elem>): ParseElementFnR => {
if (elem?.commonElem?.serviceType === 48 || [10, 20].includes(elem?.commonElem?.businessType ?? 0)) {
const extra = new NapProtoMsg(MsgInfo).decode(elem.commonElem!.pbElem!);
const msgInfoBody = extra.msgInfoBody[0];
const index = msgInfoBody?.index;
return [{
picElement: {
fileSize: index?.info.fileSize ?? 0,
picWidth: index?.info?.width ?? 0,
picHeight: index?.info?.height ?? 0,
fileName: index?.info?.fileHash ?? '',
sourcePath: '',
original: false,
picType: PicType.NEWPIC_APNG,
fileUuid: '',
fileSubId: '',
thumbFileSize: 0,
summary: '[图片]',
thumbPath: new Map(),
},
elementType: ElementType.UNKNOWN,
elementId: '',
}, elem];
}
if (elem?.notOnlineImage) {
const img = elem?.notOnlineImage; // url in originImageUrl
const preImg: MessageElement = {
picElement: {
fileSize: img.fileLen ?? 0,
picWidth: img.picWidth ?? 0,
picHeight: img.picHeight ?? 0,
fileName: Buffer.from(img.picMd5!).toString('hex') ?? '',
sourcePath: '',
original: false,
picType: PicType.NEWPIC_APNG,
fileUuid: '',
fileSubId: '',
thumbFileSize: 0,
summary: '[图片]',
thumbPath: new Map(),
},
elementType: ElementType.UNKNOWN,
elementId: '',
};
if (img.origUrl?.includes('&fileid=')) {
preImg.picElement!.originImageUrl = `https://multimedia.nt.qq.com.cn${img.origUrl}`;
} else {
preImg.picElement!.originImageUrl = `https://gchat.qpic.cn${img.origUrl}`;
}
return [preImg, elem];
}
return undefined;
};
override toPreview(): string { override toPreview(): string {
return this.summary; return this.summary;
} }

View File

@@ -0,0 +1,50 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
import { IndexNode } from '@/core/packet/transformer/proto';
class DownloadGroupImage extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(group_uin: number, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 200
},
scene: {
requestType: 2,
businessType: 1,
sceneType: 2,
group: {
groupUin: group_uin
}
},
client: {
agentType: 2,
}
},
download: {
node: node,
download: {
video: {
busiType: 0,
sceneType: 0
}
}
}
});
return OidbBase.build(0x11C4, 200, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new DownloadGroupImage();

View File

@@ -14,7 +14,7 @@ class DownloadImage extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
reqHead: { reqHead: {
common: { common: {
requestId: 1, requestId: 1,
command: 100 command: 200
}, },
scene: { scene: {
requestType: 2, requestType: 2,

View File

@@ -12,3 +12,4 @@ export { default as UploadPrivateImage } from './UploadPrivateImage';
export { default as UploadPrivatePtt } from './UploadPrivatePtt'; export { default as UploadPrivatePtt } from './UploadPrivatePtt';
export { default as UploadPrivateVideo } from './UploadPrivateVideo'; export { default as UploadPrivateVideo } from './UploadPrivateVideo';
export { default as DownloadImage } from './DownloadImage'; export { default as DownloadImage } from './DownloadImage';
export { default as DownloadGroupImage } from './DownloadGroupImage';

View File

@@ -0,0 +1,37 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from '@/core/packet/transformer/base';
class DownloadForwardMsg extends PacketTransformer<typeof proto.RecvLongMsgResp> {
constructor() {
super();
}
build(uid: string, resId: string): OidbPacket {
const req = new NapProtoMsg(proto.RecvLongMsgReq).encode({
info: {
uid: {
uid: uid
},
resId: resId,
acquire: true
},
settings: {
field1: 2,
field2: 0,
field3: 0,
field4: 0
}
});
return {
cmd: 'trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg',
data: PacketHexStrBuilder(req)
};
}
parse(data: Buffer) {
return new NapProtoMsg(proto.RecvLongMsgResp).decode(data);
}
}
export default new DownloadForwardMsg();

View File

@@ -13,12 +13,12 @@ class UploadForwardMsg extends PacketTransformer<typeof proto.SendLongMsgResp> {
const msgBody = this.msgBuilder.buildFakeMsg(selfUid, msg); const msgBody = this.msgBuilder.buildFakeMsg(selfUid, msg);
const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode( const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode(
{ {
action: { action: [{
actionCommand: 'MultiMsg', actionCommand: 'MultiMsg',
actionData: { actionData: {
msgBody: msgBody msgBody: msgBody
} }
} }]
} }
); );
const payload = zlib.gzipSync(Buffer.from(longMsgResultData)); const payload = zlib.gzipSync(Buffer.from(longMsgResultData));

View File

@@ -1 +1,2 @@
export { default as UploadForwardMsg } from './UploadForwardMsg'; export { default as UploadForwardMsg } from './UploadForwardMsg';
export { default as DownloadForwardMsg } from './DownloadForwardMsg';

View File

@@ -2,7 +2,7 @@ import { ProtoField, ScalarType } from '@napneko/nap-proto-core';
import { PushMsgBody } from '@/core/packet/transformer/proto'; import { PushMsgBody } from '@/core/packet/transformer/proto';
export const LongMsgResult = { export const LongMsgResult = {
action: ProtoField(2, () => LongMsgAction) action: ProtoField(2, () => LongMsgAction, false, true)
}; };
export const LongMsgAction = { export const LongMsgAction = {

View File

@@ -15,7 +15,6 @@ let napCatInitialized = false; // 添加一个标志
function createServiceProxy(ServiceName) { function createServiceProxy(ServiceName) {
return new Proxy(() => { }, { return new Proxy(() => { }, {
get: (target, FunctionName) => { get: (target, FunctionName) => {
console.log(ServiceName, FunctionName);
if (ServiceName === 'NodeIQQNTWrapperSession' && FunctionName === 'create') { if (ServiceName === 'NodeIQQNTWrapperSession' && FunctionName === 'create') {
return () => new Proxy({}, { return () => new Proxy({}, {
get: function (target, ClassFunName, receiver) { get: function (target, ClassFunName, receiver) {

View File

@@ -3,6 +3,7 @@ import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward,
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { MessageUnique } from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
import { Static, Type } from '@sinclair/typebox'; import { Static, Type } from '@sinclair/typebox';
import { ChatType, ElementType, MsgSourceType, NTMsgType, RawMessage } from '@/core';
const SchemaData = Type.Object({ const SchemaData = Type.Object({
message_id: Type.Optional(Type.Union([Type.Number(), Type.String()])), message_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
@@ -57,24 +58,72 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, {
throw new Error('message_id is required'); throw new Error('message_id is required');
} }
const fakeForwardMsg = (res_id: string) => {
return {
chatType: ChatType.KCHATTYPEGROUP,
elements: [{
elementType: ElementType.MULTIFORWARD,
elementId: '',
multiForwardMsgElement: {
resId: res_id,
fileName: '',
xmlContent: '',
}
}],
guildId: '',
isOnlineMsg: false,
msgId: '', // TODO: no necessary
msgRandom: '0',
msgSeq: '',
msgTime: '',
msgType: NTMsgType.KMSGTYPEMIX,
parentMsgIdList: [],
parentMsgPeer: {
chatType: ChatType.KCHATTYPEGROUP,
peerUid: '',
},
peerName: '',
peerUid: '284840486',
peerUin: '284840486',
recallTime: '0',
records: [],
sendNickName: '',
sendRemarkName: '',
senderUid: '',
senderUin: '1094950020',
sourceType: MsgSourceType.K_DOWN_SOURCETYPE_UNKNOWN,
subMsgType: 1,
} as RawMessage;
};
const protocolFallbackLogic = async (res_id: string) => {
const ob = (await this.obContext.apis.MsgApi.parseMessageV2(fakeForwardMsg(res_id)))?.arrayMsg;
if (ob) {
return {
messages: (ob?.message?.[0] as OB11MessageForward)?.data?.content
};
}
throw new Error('protocolFallbackLogic: 找不到相关的聊天记录');
};
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId.toString()); const rootMsgId = MessageUnique.getShortIdByMsgId(msgId.toString());
const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId); const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId);
if (!rootMsg) { if (!rootMsg) {
throw new Error('msg not found'); return await protocolFallbackLogic(msgId.toString());
} }
const data = await this.core.apis.MsgApi.getMsgsByMsgId(rootMsg.Peer, [rootMsg.MsgId]); const data = await this.core.apis.MsgApi.getMsgsByMsgId(rootMsg.Peer, [rootMsg.MsgId]);
if (!data || data.result !== 0) { if (!data || data.result !== 0) {
throw new Error('找不到相关的聊天记录' + data?.errMsg); return await protocolFallbackLogic(msgId.toString());
} }
const singleMsg = data.msgList[0]; const singleMsg = data.msgList[0];
if (!singleMsg) { if (!singleMsg) {
throw new Error('找不到相关的聊天记录'); return await protocolFallbackLogic(msgId.toString());
} }
const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;//强制array 以便处理 const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;//强制array 以便处理
if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) { if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) {
throw new Error('找不到相关的聊天记录'); return await protocolFallbackLogic(msgId.toString());
} }
return { return {
messages: (resMsg?.message?.[0] as OB11MessageForward)?.data?.content messages: (resMsg?.message?.[0] as OB11MessageForward)?.data?.content

View File

@@ -1,41 +1,50 @@
import { FileNapCatOneBotUUID } from '@/common/file-uuid'; import {FileNapCatOneBotUUID} from '@/common/file-uuid';
import { MessageUnique } from '@/common/message-unique'; import {MessageUnique} from '@/common/message-unique';
import { import {
NTMsgAtType,
ChatType, ChatType,
CustomMusicSignPostData, CustomMusicSignPostData,
ElementType, ElementType,
FaceIndex, FaceIndex,
FaceType,
GrayTipElement,
GroupNotify,
IdMusicSignPostData, IdMusicSignPostData,
MessageElement, MessageElement,
NapCatCore, NapCatCore,
NTGrayTipElementSubTypeV2, NTGrayTipElementSubTypeV2,
NTMsgAtType,
Peer, Peer,
RawMessage, RawMessage,
SendMessageElement, SendMessageElement,
SendTextElement, SendTextElement,
FaceType,
GrayTipElement,
GroupNotify,
} from '@/core'; } from '@/core';
import faceConfig from '@/core/external/face_config.json'; import faceConfig from '@/core/external/face_config.json';
import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, OB11MessageForward, OB11MessageImage, OB11MessageVideo, } from '@/onebot'; import {
import { OB11Construct } from '@/onebot/helper/data'; NapCatOneBot11Adapter,
import { EventType } from '@/onebot/event/OneBotEvent'; OB11Message,
import { encodeCQCode } from '@/onebot/helper/cqcode'; OB11MessageData,
import { uriToLocalFile } from '@/common/file'; OB11MessageDataType,
import { RequestUtil } from '@/common/request'; OB11MessageFileBase,
import fsPromise, { constants } from 'node:fs/promises'; OB11MessageForward,
import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent'; OB11MessageImage,
import { ForwardMsgBuilder } from '@/common/forward-msg-builder'; OB11MessageVideo,
import { NapProtoMsg } from '@napneko/nap-proto-core'; } from '@/onebot';
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent'; import {OB11Construct} from '@/onebot/helper/data';
import { OB11GroupDecreaseEvent, GroupDecreaseSubType } from '../event/notice/OB11GroupDecreaseEvent'; import {EventType} from '@/onebot/event/OneBotEvent';
import { GroupAdmin } from '@/core/packet/transformer/proto/message/groupAdmin'; import {encodeCQCode} from '@/onebot/helper/cqcode';
import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent'; import {uriToLocalFile} from '@/common/file';
import { GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody } from '@/core/packet/transformer/proto'; import {RequestUtil} from '@/common/request';
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'; import fsPromise, {constants} from 'node:fs/promises';
import { LRUCache } from '@/common/lru-cache'; import {OB11FriendAddNoticeEvent} from '@/onebot/event/notice/OB11FriendAddNoticeEvent';
import {ForwardMsgBuilder} from '@/common/forward-msg-builder';
import {NapProtoMsg} from '@napneko/nap-proto-core';
import {OB11GroupIncreaseEvent} from '../event/notice/OB11GroupIncreaseEvent';
import {GroupDecreaseSubType, OB11GroupDecreaseEvent} from '../event/notice/OB11GroupDecreaseEvent';
import {GroupAdmin} from '@/core/packet/transformer/proto/message/groupAdmin';
import {OB11GroupAdminNoticeEvent} from '../event/notice/OB11GroupAdminNoticeEvent';
import {GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody} from '@/core/packet/transformer/proto';
import {OB11GroupRequestEvent} from '../event/request/OB11GroupRequest';
import {LRUCache} from '@/common/lru-cache';
type RawToOb11Converters = { type RawToOb11Converters = {
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: ( [Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
@@ -84,12 +93,12 @@ export class OneBotMsgApi {
} }
return { return {
type: OB11MessageDataType.text, type: OB11MessageDataType.text,
data: { text }, data: {text},
}; };
} else { } else {
let qq: string = 'all'; let qq: string = 'all';
if (element.atType !== NTMsgAtType.ATTYPEALL) { if (element.atType !== NTMsgAtType.ATTYPEALL) {
const { atNtUid, atUid } = element; const {atNtUid, atUid} = element;
qq = !atUid || atUid === '0' ? await this.core.apis.UserApi.getUinByUidV2(atNtUid) : atUid; qq = !atUid || atUid === '0' ? await this.core.apis.UserApi.getUinByUidV2(atNtUid) : atUid;
} }
return { return {
@@ -197,7 +206,7 @@ export class OneBotMsgApi {
peerUid: msg.peerUid, peerUid: msg.peerUid,
guildId: '', guildId: '',
}; };
const { emojiId } = _; const {emojiId} = _;
const dir = emojiId.substring(0, 2); const dir = emojiId.substring(0, 2);
const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`; const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`;
const filename = `${dir}-${emojiId}.gif`; const filename = `${dir}-${emojiId}.gif`;
@@ -264,7 +273,6 @@ export class OneBotMsgApi {
} }
// 丢弃该消息段 // 丢弃该消息段
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) { if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
this.core.context.logger.logError( this.core.context.logger.logError(
@@ -355,18 +363,25 @@ export class OneBotMsgApi {
}; };
}, },
multiForwardMsgElement: async (_, msg, _wrapper, context) => { multiForwardMsgElement: async (element, msg, _wrapper, context) => {
const parentMsgPeer = msg.parentMsgPeer ?? { const parentMsgPeer = msg.parentMsgPeer ?? {
chatType: msg.chatType, chatType: msg.chatType,
guildId: '', guildId: '',
peerUid: msg.peerUid, peerUid: msg.peerUid,
}; };
const multiMsgs = await this.getMultiMessages(msg, parentMsgPeer); let multiMsgs = await this.getMultiMessages(msg, parentMsgPeer);
// 拉取失败则跳过 // 拉取失败则跳过
if (!multiMsgs) return null; if (!multiMsgs || multiMsgs.length === 0) {
try {
multiMsgs = await this.core.apis.PacketApi.pkt.operation.FetchForwardMsg(element.resId);
} catch (e) {
this.core.context.logger.logError('Protocol FetchForwardMsg fallback failed!', e);
return null;
}
}
const forward: OB11MessageForward = { const forward: OB11MessageForward = {
type: OB11MessageDataType.forward, type: OB11MessageDataType.forward,
data: { id: msg.msgId } data: {id: msg.msgId}
}; };
if (!context.parseMultMsg) return forward; if (!context.parseMultMsg) return forward;
forward.data.content = await this.parseMultiMessageContent( forward.data.content = await this.parseMultiMessageContent(
@@ -397,7 +412,7 @@ export class OneBotMsgApi {
}; };
ob11ToRawConverters: Ob11ToRawConverters = { ob11ToRawConverters: Ob11ToRawConverters = {
[OB11MessageDataType.text]: async ({ data: { text } }) => ({ [OB11MessageDataType.text]: async ({data: {text}}) => ({
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
elementId: '', elementId: '',
textElement: { textElement: {
@@ -409,7 +424,7 @@ export class OneBotMsgApi {
}, },
}), }),
[OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => { [OB11MessageDataType.at]: async ({data: {qq: atQQ}}, context) => {
function at(atUid: string, atNtUid: string, atType: NTMsgAtType, atName: string): SendTextElement { function at(atUid: string, atNtUid: string, atType: NTMsgAtType, atName: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
@@ -436,7 +451,7 @@ export class OneBotMsgApi {
return at(atQQ, uid, NTMsgAtType.ATTYPEONE, info.nick || ''); return at(atQQ, uid, NTMsgAtType.ATTYPEONE, info.nick || '');
}, },
[OB11MessageDataType.reply]: async ({ data: { id } }) => { [OB11MessageDataType.reply]: async ({data: {id}}) => {
const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id)); const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
if (!replyMsgM) { if (!replyMsgM) {
this.core.context.logger.logWarn('回复消息不存在', id); this.core.context.logger.logWarn('回复消息不存在', id);
@@ -458,7 +473,7 @@ export class OneBotMsgApi {
undefined; undefined;
}, },
[OB11MessageDataType.face]: async ({ data: { id, resultId, chainCount } }) => { [OB11MessageDataType.face]: async ({data: {id, resultId, chainCount}}) => {
const parsedFaceId = +id; const parsedFaceId = +id;
// 从face_config.json中获取表情名称 // 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface; const sysFaces = faceConfig.sysface;
@@ -522,12 +537,12 @@ export class OneBotMsgApi {
}, },
[OB11MessageDataType.file]: async (sendMsg, context) => { [OB11MessageDataType.file]: async (sendMsg, context) => {
const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context); const {path, fileName} = await this.handleOb11FileLikeMessage(sendMsg, context);
return await this.core.apis.FileApi.createValidSendFileElement(context, path, fileName); return await this.core.apis.FileApi.createValidSendFileElement(context, path, fileName);
}, },
[OB11MessageDataType.video]: async (sendMsg, context) => { [OB11MessageDataType.video]: async (sendMsg, context) => {
const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context); const {path, fileName} = await this.handleOb11FileLikeMessage(sendMsg, context);
let thumb = sendMsg.data.thumb; let thumb = sendMsg.data.thumb;
if (thumb) { if (thumb) {
@@ -545,7 +560,7 @@ export class OneBotMsgApi {
this.core.apis.FileApi.createValidSendPttElement( this.core.apis.FileApi.createValidSendPttElement(
(await this.handleOb11FileLikeMessage(sendMsg, context)).path), (await this.handleOb11FileLikeMessage(sendMsg, context)).path),
[OB11MessageDataType.json]: async ({ data: { data } }) => ({ [OB11MessageDataType.json]: async ({data: {data}}) => ({
elementType: ElementType.ARK, elementType: ElementType.ARK,
elementId: '', elementId: '',
arkElement: { arkElement: {
@@ -588,13 +603,13 @@ export class OneBotMsgApi {
}), }),
// Need signing // Need signing
[OB11MessageDataType.markdown]: async ({ data: { content } }) => ({ [OB11MessageDataType.markdown]: async ({data: {content}}) => ({
elementType: ElementType.MARKDOWN, elementType: ElementType.MARKDOWN,
elementId: '', elementId: '',
markdownElement: { content }, markdownElement: {content},
}), }),
[OB11MessageDataType.music]: async ({ data }, context) => { [OB11MessageDataType.music]: async ({data}, context) => {
// 保留, 直到...找到更好的解决方案 // 保留, 直到...找到更好的解决方案
if (data.id !== undefined) { if (data.id !== undefined) {
if (!['qq', '163', 'kugou', 'kuwo', 'migu'].includes(data.type)) { if (!['qq', '163', 'kugou', 'kuwo', 'migu'].includes(data.type)) {
@@ -618,8 +633,8 @@ export class OneBotMsgApi {
let postData: IdMusicSignPostData | CustomMusicSignPostData; let postData: IdMusicSignPostData | CustomMusicSignPostData;
if (data.id === undefined && data.content) { if (data.id === undefined && data.content) {
const { content, ...others } = data; const {content, ...others} = data;
postData = { singer: content, ...others }; postData = {singer: content, ...others};
} else { } else {
postData = data; postData = data;
} }
@@ -631,7 +646,7 @@ export class OneBotMsgApi {
try { try {
const musicJson = await RequestUtil.HttpGetJson<string>(signUrl, 'POST', postData); const musicJson = await RequestUtil.HttpGetJson<string>(signUrl, 'POST', postData);
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: { data: musicJson }, data: {data: musicJson},
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
} catch (e) { } catch (e) {
@@ -642,10 +657,10 @@ export class OneBotMsgApi {
[OB11MessageDataType.node]: async () => undefined, [OB11MessageDataType.node]: async () => undefined,
[OB11MessageDataType.forward]: async ({ data }, context) => { [OB11MessageDataType.forward]: async ({data}, context) => {
const jsonData = ForwardMsgBuilder.fromResId(data.id); const jsonData = ForwardMsgBuilder.fromResId(data.id);
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: { data: JSON.stringify(jsonData) }, data: {data: JSON.stringify(jsonData)},
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
}, },
@@ -665,17 +680,17 @@ export class OneBotMsgApi {
[OB11MessageDataType.miniapp]: async () => undefined, [OB11MessageDataType.miniapp]: async () => undefined,
[OB11MessageDataType.contact]: async ({ data: { type = 'qq', id } }, context) => { [OB11MessageDataType.contact]: async ({data: {type = 'qq', id}}, context) => {
if (type === 'qq') { if (type === 'qq') {
const arkJson = await this.core.apis.UserApi.getBuddyRecommendContactArkJson(id.toString(), ''); const arkJson = await this.core.apis.UserApi.getBuddyRecommendContactArkJson(id.toString(), '');
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: { data: arkJson.arkMsg }, data: {data: arkJson.arkMsg},
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
} else if (type === 'group') { } else if (type === 'group') {
const arkJson = await this.core.apis.GroupApi.getGroupRecommendContactArkJson(id.toString()); const arkJson = await this.core.apis.GroupApi.getGroupRecommendContactArkJson(id.toString());
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: { data: arkJson.arkJson }, data: {data: arkJson.arkJson},
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
} }
@@ -692,7 +707,10 @@ export class OneBotMsgApi {
if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) { if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
if (grayTipElement.jsonGrayTipElement.busiId == 1061) { if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid))); const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
if (PokeEvent) { return PokeEvent; }; if (PokeEvent) {
return PokeEvent;
}
;
} else if (grayTipElement.jsonGrayTipElement.busiId == 19324 && msg.peerUid !== '') { } else if (grayTipElement.jsonGrayTipElement.busiId == 19324 && msg.peerUid !== '') {
return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid))); return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
} }
@@ -849,7 +867,7 @@ export class OneBotMsgApi {
element[key], element[key],
msg, msg,
element, element,
{ parseMultMsg } {parseMultMsg}
); );
if (key === 'faceElement' && !parsedElement) { if (key === 'faceElement' && !parsedElement) {
return null; return null;
@@ -857,7 +875,7 @@ export class OneBotMsgApi {
return parsedElement; return parsedElement;
} }
} }
return []; return;
}, },
)); ));
@@ -902,13 +920,13 @@ export class OneBotMsgApi {
) => Promise<SendMessageElement | undefined>; ) => Promise<SendMessageElement | undefined>;
const callResult = converter( const callResult = converter(
sendMsg, sendMsg,
{ peer, deleteAfterSentFiles }, {peer, deleteAfterSentFiles},
)?.catch(undefined); )?.catch(undefined);
callResultList.push(callResult); callResultList.push(callResult);
} }
const ret = await Promise.all(callResultList); const ret = await Promise.all(callResultList);
const sendElements: SendMessageElement[] = ret.filter(ele => !!ele); const sendElements: SendMessageElement[] = ret.filter(ele => !!ele);
return { sendElements, deleteAfterSentFiles }; return {sendElements, deleteAfterSentFiles};
} }
async sendMsgWithOb11UniqueId(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[]) { async sendMsgWithOb11UniqueId(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[]) {
@@ -970,8 +988,8 @@ export class OneBotMsgApi {
} }
private async handleOb11FileLikeMessage( private async handleOb11FileLikeMessage(
{ data: inputdata }: OB11MessageFileBase, {data: inputdata}: OB11MessageFileBase,
{ deleteAfterSentFiles }: SendMessageContext {deleteAfterSentFiles}: SendMessageContext
) { ) {
let realUri = [inputdata.url, inputdata.file, inputdata.path].find(uri => uri && uri.trim()) ?? ''; let realUri = [inputdata.url, inputdata.file, inputdata.path].find(uri => uri && uri.trim()) ?? '';
if (!realUri) { if (!realUri) {
@@ -980,28 +998,29 @@ export class OneBotMsgApi {
} }
const downloadFile = async (uri: string) => { const downloadFile = async (uri: string) => {
const { path, fileName, errMsg, success } = await uriToLocalFile(this.core.NapCatTempPath, uri); const {path, fileName, errMsg, success} = await uriToLocalFile(this.core.NapCatTempPath, uri);
if (!success) { if (!success) {
this.core.context.logger.logError('文件下载失败', errMsg); this.core.context.logger.logError('文件下载失败', errMsg);
throw new Error('文件下载失败: ' + errMsg); throw new Error('文件下载失败: ' + errMsg);
} }
return { path, fileName }; return {path, fileName};
}; };
try { try {
const { path, fileName } = await downloadFile(realUri); const {path, fileName} = await downloadFile(realUri);
deleteAfterSentFiles.push(path); deleteAfterSentFiles.push(path);
return { path, fileName: inputdata.name ?? fileName }; return {path, fileName: inputdata.name ?? fileName};
} catch { } catch {
realUri = await this.handleObfuckName(realUri); realUri = await this.handleObfuckName(realUri);
const { path, fileName } = await downloadFile(realUri); const {path, fileName} = await downloadFile(realUri);
deleteAfterSentFiles.push(path); deleteAfterSentFiles.push(path);
return { path, fileName: inputdata.name ?? fileName }; return {path, fileName: inputdata.name ?? fileName};
} }
} }
async handleObfuckName(name: string) { async handleObfuckName(name: string) {
const contextMsgFile = FileNapCatOneBotUUID.decode(name); const contextMsgFile = FileNapCatOneBotUUID.decode(name);
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) { if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
const { peer, msgId, elementId } = contextMsgFile; const {peer, msgId, elementId} = contextMsgFile;
const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList.find(msg => msg.msgId === msgId); const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList.find(msg => msg.msgId === msgId);
const mixElement = rawMessage?.elements.find(e => e.elementId === elementId); const mixElement = rawMessage?.elements.find(e => e.elementId === elementId);
const mixElementInner = mixElement?.videoElement ?? mixElement?.fileElement ?? mixElement?.pttElement ?? mixElement?.picElement; const mixElementInner = mixElement?.videoElement ?? mixElement?.fileElement ?? mixElement?.pttElement ?? mixElement?.picElement;
@@ -1009,18 +1028,19 @@ export class OneBotMsgApi {
let url = ''; let url = '';
if (mixElement?.picElement && rawMessage) { if (mixElement?.picElement && rawMessage) {
const tempData = const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageImage | undefined; await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, {parseMultMsg: false}) as OB11MessageImage | undefined;
url = tempData?.data.url ?? ''; url = tempData?.data.url ?? '';
} }
if (mixElement?.videoElement && rawMessage) { if (mixElement?.videoElement && rawMessage) {
const tempData = const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageVideo | undefined; await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, {parseMultMsg: false}) as OB11MessageVideo | undefined;
url = tempData?.data.url ?? ''; url = tempData?.data.url ?? '';
} }
return url !== '' ? url : await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', ''); return url !== '' ? url : await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', '');
} }
throw new Error('文件名解析失败'); throw new Error('文件名解析失败');
} }
groupChangDecreseType2String(type: number): GroupDecreaseSubType { groupChangDecreseType2String(type: number): GroupDecreaseSubType {
switch (type) { switch (type) {
case 130: case 130:

View File

@@ -78,7 +78,7 @@ const NetworkConfigSchema = Type.Object({
plugins: Type.Array(PluginConfigSchema, { default: [] }) plugins: Type.Array(PluginConfigSchema, { default: [] })
}, { default: {} }); }, { default: {} });
const OneBotConfigSchema = Type.Object({ export const OneBotConfigSchema = Type.Object({
network: NetworkConfigSchema, network: NetworkConfigSchema,
musicSignUrl: Type.String({ default: '' }), musicSignUrl: Type.String({ default: '' }),
enableLocalFile2Url: Type.Boolean({ default: false }), enableLocalFile2Url: Type.Boolean({ default: false }),

View File

@@ -1,9 +1,10 @@
import { ConfigBase } from '@/common/config-base'; import { ConfigBase } from '@/common/config-base';
import { NapCatCore } from '@/core'; import type { NapCatCore } from '@/core';
import { OneBotConfig } from './config'; import { OneBotConfig } from './config';
import { AnySchema } from 'ajv';
export class OB11ConfigLoader extends ConfigBase<OneBotConfig> { export class OB11ConfigLoader extends ConfigBase<OneBotConfig> {
constructor(core: NapCatCore, configPath: string) { constructor(core: NapCatCore, configPath: string, schema: AnySchema) {
super('onebot11', core, configPath, false); super('onebot11', core, configPath, schema);
} }
} }

View File

@@ -44,8 +44,8 @@ import { LRUCache } from '@/common/lru-cache';
import { BotOfflineEvent } from './event/notice/BotOfflineEvent'; import { BotOfflineEvent } from './event/notice/BotOfflineEvent';
import { import {
NetworkAdapterConfig, NetworkAdapterConfig,
loadConfig,
OneBotConfig, OneBotConfig,
OneBotConfigSchema,
} from './config/config'; } from './config/config';
import { OB11Message } from './types'; import { OB11Message } from './types';
import { IOB11NetworkAdapter } from '@/onebot/network/adapter'; import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
@@ -66,9 +66,7 @@ export class NapCatOneBot11Adapter {
constructor(core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) { constructor(core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) {
this.core = core; this.core = core;
this.context = context; this.context = context;
this.configLoader = new OB11ConfigLoader(core, pathWrapper.configPath); this.configLoader = new OB11ConfigLoader(core, pathWrapper.configPath, OneBotConfigSchema);
this.configLoader.save(this.configLoader.configData);
this.configLoader.save(loadConfig(this.configLoader.configData));
this.apis = { this.apis = {
GroupApi: new OneBotGroupApi(this, core), GroupApi: new OneBotGroupApi(this, core),
UserApi: new OneBotUserApi(this, core), UserApi: new OneBotUserApi(this, core),
@@ -176,9 +174,6 @@ export class NapCatOneBot11Adapter {
WebUiDataRuntime.setQQLoginStatus(true); WebUiDataRuntime.setQQLoginStatus(true);
WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => { WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => {
const prev = this.configLoader.configData; const prev = this.configLoader.configData;
// 保证默认配置
newConfig = loadConfig(newConfig);
this.configLoader.save(newConfig); this.configLoader.save(newConfig);
//this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`); //this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`);
await this.reloadNetwork(prev, newConfig); await this.reloadNetwork(prev, newConfig);

View File

@@ -1,4 +1,4 @@
import { OB11EmitEventContent, OB11NetworkReloadType } from './index'; import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
import express, { Express, NextFunction, Request, Response } from 'express'; import express, { Express, NextFunction, Request, Response } from 'express';
import http from 'http'; import http from 'http';
import { NapCatCore } from '@/core'; import { NapCatCore } from '@/core';
@@ -60,7 +60,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
}); });
req.on('end', () => { req.on('end', () => {
try { try {
req.body = json5.parse(rawData || '{}'); req.body = { ...json5.parse(rawData || '{}'), ...req.body };
next(); next();
} catch { } catch {
return res.status(400).send('Invalid JSON'); return res.status(400).send('Invalid JSON');

View File

@@ -15,7 +15,7 @@ import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
import json5 from 'json5'; import json5 from 'json5';
export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> { export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> {
wsServer: WebSocketServer; wsServer?: WebSocketServer;
wsClients: WebSocket[] = []; wsClients: WebSocket[] = [];
wsClientsMutex = new Mutex(); wsClientsMutex = new Mutex();
private heartbeatIntervalId: NodeJS.Timeout | null = null; 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, host: this.config.host === '0.0.0.0' ? '' : this.config.host,
maxPayload: 1024 * 1024 * 1024, 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) { if (!this.isEnable) {
wsClient.close(); wsClient.close();
return; 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 paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url;
const isApiConnect = paramUrl === '/api' || paramUrl === '/api/'; const isApiConnect = paramUrl === '/api' || paramUrl === '/api/';
if (!isApiConnect) { 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)); 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)); }).on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Server Error:', err.message));
} }
connectEvent(core: NapCatCore, wsClient: WebSocket) { connectEvent(core: NapCatCore, wsClient: WebSocket) {
try { try {
this.checkStateAndReply<unknown>(new OB11LifeCycleEvent(core, LifeCycleSubType.CONNECT), wsClient); 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'); this.logger.logError('[OneBot] [WebSocket Server] Cannot open a opened WebSocket server');
return; 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.logger.log('[OneBot] [WebSocket Server] Server Started', typeof (addressInfo) === 'string' ? addressInfo : addressInfo?.address + ':' + addressInfo?.port);
this.isEnable = true; this.isEnable = true;
@@ -108,7 +111,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
async close() { async close() {
this.isEnable = false; this.isEnable = false;
this.wsServer.close((err) => { this.wsServer?.close((err) => {
if (err) { if (err) {
this.logger.logError('[OneBot] [WebSocket Server] Error closing server:', err.message); this.logger.logError('[OneBot] [WebSocket Server] Error closing server:', err.message);
} else { } else {
@@ -205,6 +208,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
host: newConfig.host === '0.0.0.0' ? '' : newConfig.host, host: newConfig.host === '0.0.0.0' ? '' : newConfig.host,
maxPayload: 1024 * 1024 * 1024, maxPayload: 1024 * 1024 * 1024,
}); });
this.createServer(this.wsServer);
if (newConfig.enable) { if (newConfig.enable) {
this.open(); this.open();
} }

View File

@@ -26,9 +26,4 @@ export function require_dlopen(modulename: string) {
process.dlopen(module, path.join(import__dirname, modulename)); process.dlopen(module, path.join(import__dirname, modulename));
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
return module.exports as any; return module.exports as any;
} }
/**
* Expose the native API when not Windows, note that this is not public API and
* could be removed at any time.
*/
export const native = (process.platform !== 'win32' ? require_dlopen('./pty/' + process.platform + '.' + process.arch + '/pty.node') : null);

View File

@@ -13,12 +13,13 @@ import { IProcessEnv, IPtyForkOptions, IPtyOpenOptions } from '@homebridge/node-
import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/types'; import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/types';
import { assign } from '@homebridge/node-pty-prebuilt-multiarch/src/utils'; import { assign } from '@homebridge/node-pty-prebuilt-multiarch/src/utils';
import { pty_loader } from './prebuild-loader'; import { pty_loader } from './prebuild-loader';
import { fileURLToPath } from 'url';
export const pty = pty_loader(); export const pty = pty_loader();
let helperPath: string; let helperPath: string;
helperPath = '../build/Release/spawn-helper'; helperPath = '../build/Release/spawn-helper';
const import__dirname = path.dirname(fileURLToPath(import.meta.url));
helperPath = path.resolve(__dirname, helperPath); helperPath = path.resolve(import__dirname, helperPath);
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');

View File

@@ -14,6 +14,8 @@ import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/t
import { fork } from 'child_process'; import { fork } from 'child_process';
import { ConoutConnection } from './windowsConoutConnection'; import { ConoutConnection } from './windowsConoutConnection';
import { require_dlopen } from '.'; import { require_dlopen } from '.';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
let conptyNative: IConptyNative; let conptyNative: IConptyNative;
let winptyNative: IWinptyNative; let winptyNative: IWinptyNative;
@@ -149,7 +151,7 @@ export class WindowsPtyAgent {
consoleProcessList.forEach((pid: number) => { consoleProcessList.forEach((pid: number) => {
try { try {
process.kill(pid); process.kill(pid);
} catch{ } catch {
// Ignore if process cannot be found (kill ESRCH error) // Ignore if process cannot be found (kill ESRCH error)
} }
}); });
@@ -176,8 +178,9 @@ export class WindowsPtyAgent {
} }
private _getConsoleProcessList(): Promise<number[]> { private _getConsoleProcessList(): Promise<number[]> {
const import__dirname = dirname(fileURLToPath(import.meta.url));
return new Promise<number[]>(resolve => { return new Promise<number[]>(resolve => {
const agent = fork(path.join(__dirname, 'conpty_console_list_agent'), [this._innerPid.toString()]); const agent = fork(path.join(import__dirname, 'conpty_console_list_agent'), [this._innerPid.toString()]);
agent.on('message', message => { agent.on('message', message => {
clearTimeout(timeout); clearTimeout(timeout);
// @ts-expect-error no need to check if it is null // @ts-expect-error no need to check if it is null

View File

@@ -223,7 +223,7 @@ async function handleLogin(
logger.log(`可用于快速登录的 QQ\n${historyLoginList logger.log(`可用于快速登录的 QQ\n${historyLoginList
.map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`) .map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`)
.join('\n') .join('\n')
}`); }`);
} }
loginService.getQRCodePicture(); loginService.getQRCodePicture();
} }
@@ -314,7 +314,15 @@ export async function NCoreInitShell() {
await initializeSession(session, sessionConfig); await initializeSession(session, sessionConfig);
const accountDataPath = path.resolve(dataPath, './NapCat/data'); const accountDataPath = path.resolve(dataPath, './NapCat/data');
fs.mkdirSync(dataPath, { recursive: true }); //判断dataPath是否为根目录 或者 D:/ 之类的盘目录
if (dataPath !== '/' && /^[a-zA-Z]:\\$/.test(dataPath) === false) {
try {
fs.mkdirSync(accountDataPath, { recursive: true });
} catch (error) {
logger.logError('创建accountDataPath失败', error);
}
}
logger.logDebug('本账号数据/缓存目录:', accountDataPath); logger.logDebug('本账号数据/缓存目录:', accountDataPath);
await new NapCatShell( await new NapCatShell(

View File

@@ -10,9 +10,10 @@ import { WebUiConfigWrapper } from '@webapi/helper/config';
import { ALLRouter } from '@webapi/router'; import { ALLRouter } from '@webapi/router';
import { cors } from '@webapi/middleware/cors'; import { cors } from '@webapi/middleware/cors';
import { createUrl } from '@webapi/utils/url'; import { createUrl } from '@webapi/utils/url';
import { sendSuccess } from '@webapi/utils/response'; import { sendError, sendSuccess } from '@webapi/utils/response';
import { join } from 'node:path'; import { join } from 'node:path';
import { terminalManager } from '@webapi/terminal/terminal_manager'; import { terminalManager } from '@webapi/terminal/terminal_manager';
import multer from 'multer'; // 新增引入multer用于错误捕获
// 实例化Express // 实例化Express
const app = express(); const app = express();
@@ -42,10 +43,22 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// CORS中间件 // CORS中间件
// TODO: // TODO:
app.use(cors); app.use(cors);
// 如果是webui字体文件挂载字体文件
app.use('/webui/fonts/AaCute.woff', async (_req, res, next) => {
const isFontExist = await WebUiConfigWrapper.CheckWebUIFontExist();
console.log(isFontExist, 'isFontExist');
if (isFontExist) {
res.sendFile(WebUiConfigWrapper.GetWebUIFontPath());
} else {
next();
}
});
// ------------中间件结束------------ // ------------中间件结束------------
// ------------挂载路由------------ // ------------挂载路由------------
// 挂载静态路由(前端),路径为 [/前缀]/webui // 挂载静态路由(前端),路径为 /webui
app.use('/webui', express.static(pathWrapper.staticPath)); app.use('/webui', express.static(pathWrapper.staticPath));
// 初始化WebSocket服务器 // 初始化WebSocket服务器
server.on('upgrade', (request, socket, head) => { server.on('upgrade', (request, socket, head) => {
@@ -64,7 +77,19 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
app.all('/', (_req, res) => { app.all('/', (_req, res) => {
sendSuccess(res, null, 'NapCat WebAPI is now running!'); sendSuccess(res, null, 'NapCat WebAPI is now running!');
}); });
// ------------路由挂载结束------------
// 错误处理中间件捕获multer的错误
app.use((err: Error, _: express.Request, res: express.Response, next: express.NextFunction) => {
if (err instanceof multer.MulterError) {
return sendError(res, err.message, true);
}
next(err);
});
// 全局错误处理中间件非multer错误
app.use((_: Error, __: express.Request, res: express.Response, ___: express.NextFunction) => {
sendError(res, 'An unknown error occurred.', true);
});
// ------------启动服务------------ // ------------启动服务------------
server.listen(config.port, config.host, async () => { server.listen(config.port, config.host, async () => {

View File

@@ -1,4 +1,4 @@
import type { RequestHandler, Request } from 'express'; import type { RequestHandler } from 'express';
import { sendError, sendSuccess } from '../utils/response'; import { sendError, sendSuccess } from '../utils/response';
import fsProm from 'fs/promises'; import fsProm from 'fs/promises';
import fs from 'fs'; import fs from 'fs';
@@ -7,7 +7,9 @@ import os from 'os';
import compressing from 'compressing'; import compressing from 'compressing';
import { PassThrough } from 'stream'; import { PassThrough } from 'stream';
import multer from 'multer'; import multer from 'multer';
import { randomUUID } from 'crypto'; import { WebUiConfigWrapper } from '../helper/config';
import webUIFontUploader from '../uploader/webui_font';
import diskUploader from '../uploader/disk';
const isWindows = os.platform() === 'win32'; const isWindows = os.platform() === 'win32';
@@ -269,6 +271,10 @@ export const BatchMoveHandler: RequestHandler = async (req, res) => {
export const DownloadHandler: RequestHandler = async (req, res) => { export const DownloadHandler: RequestHandler = async (req, res) => {
try { 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); const stat = await fsProm.stat(filePath);
res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Type', 'application/octet-stream');
@@ -323,74 +329,71 @@ export const BatchDownloadHandler: RequestHandler = async (req, res) => {
} }
}; };
// 修改:使用 Buffer 转码文件名,解决文件上传时乱码问题 // 修改上传处理方法
const decodeFileName = (fileName: string): string => { export const UploadHandler: RequestHandler = async (req, res) => {
try { try {
return Buffer.from(fileName, 'binary').toString('utf8'); await diskUploader(req, res);
} catch { return sendSuccess(res, true, '文件上传成功', true);
return fileName; } catch (error) {
let errorMessage = '文件上传失败';
if (error instanceof multer.MulterError) {
switch (error.code) {
case 'LIMIT_FILE_SIZE':
errorMessage = '文件大小超过限制40MB';
break;
case 'LIMIT_UNEXPECTED_FILE':
errorMessage = '无效的文件上传字段';
break;
default:
errorMessage = `上传错误: ${error.message}`;
}
} else if (error instanceof Error) {
errorMessage = error.message;
}
return sendError(res, errorMessage, true);
} }
}; };
// 修改上传处理方法 // 上传WebUI字体文件处理方法
export const UploadHandler: RequestHandler = (req, res) => { export const UploadWebUIFontHandler: RequestHandler = async (req, res) => {
const uploadPath = (req.query['path'] || '') as string; try {
await webUIFontUploader(req, res);
return sendSuccess(res, true, '字体文件上传成功', true);
} catch (error) {
let errorMessage = '字体文件上传失败';
const storage = multer.diskStorage({ if (error instanceof multer.MulterError) {
destination: ( switch (error.code) {
_: Request, case 'LIMIT_FILE_SIZE':
file: Express.Multer.File, errorMessage = '字体文件大小超过限制40MB';
cb: (error: Error | null, destination: string) => void break;
) => { case 'LIMIT_UNEXPECTED_FILE':
try { errorMessage = '无效的文件上传字段';
const decodedName = decodeFileName(file.originalname); break;
default:
if (!uploadPath) { errorMessage = `上传错误: ${error.message}`;
return cb(new Error('上传路径不能为空'), '');
}
if (isWindows && uploadPath === '\\') {
return cb(new Error('根目录不允许上传文件'), '');
}
// 处理文件夹上传的情况
if (decodedName.includes('/') || decodedName.includes('\\')) {
const fullPath = path.join(uploadPath, path.dirname(decodedName));
fs.mkdirSync(fullPath, { recursive: true });
cb(null, fullPath);
} else {
cb(null, uploadPath);
}
} catch (error) {
cb(error as Error, '');
} }
}, } else if (error instanceof Error) {
filename: (_: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => { errorMessage = error.message;
try {
const decodedName = decodeFileName(file.originalname);
const fileName = path.basename(decodedName);
// 检查文件是否存在
const fullPath = path.join(uploadPath, decodedName);
if (fs.existsSync(fullPath)) {
const ext = path.extname(fileName);
const name = path.basename(fileName, ext);
cb(null, `${name}-${randomUUID()}${ext}`);
} else {
cb(null, fileName);
}
} catch (error) {
cb(error as Error, '');
}
},
});
const upload = multer({ storage }).array('files');
upload(req, res, (err: any) => {
if (err) {
return sendError(res, err.message || '文件上传失败');
} }
return sendSuccess(res, true); return sendError(res, errorMessage, true);
}); }
};
// 删除WebUI字体文件处理方法
export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => {
try {
const fontPath = WebUiConfigWrapper.GetWebUIFontPath();
const exists = await WebUiConfigWrapper.CheckWebUIFontExist();
if (!exists) {
return sendSuccess(res, true);
}
await fsProm.unlink(fontPath);
return sendSuccess(res, true);
} catch (error) {
return sendError(res, '删除字体文件失败');
}
}; };

View File

@@ -1,13 +1,12 @@
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { existsSync, readFileSync } from 'node:fs'; import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { loadConfig, OneBotConfig } from '@/onebot/config/config';
import { OneBotConfig } from '@/onebot/config/config';
import { webUiPathWrapper } from '@/webui'; import { webUiPathWrapper } from '@/webui';
import { WebUiDataRuntime } from '@webapi/helper/Data'; import { WebUiDataRuntime } from '@webapi/helper/Data';
import { sendError, sendSuccess } from '@webapi/utils/response'; import { sendError, sendSuccess } from '@webapi/utils/response';
import { isEmpty } from '@webapi/utils/check'; import { isEmpty } from '@webapi/utils/check';
import json5 from 'json5';
// 获取OneBot11配置 // 获取OneBot11配置
export const OB11GetConfigHandler: RequestHandler = (_, res) => { export const OB11GetConfigHandler: RequestHandler = (_, res) => {
@@ -19,16 +18,16 @@ export const OB11GetConfigHandler: RequestHandler = (_, res) => {
} }
// 获取登录的QQ号 // 获取登录的QQ号
const uin = WebUiDataRuntime.getQQLoginUin(); const uin = WebUiDataRuntime.getQQLoginUin();
// 读取配置文件 // 读取配置文件路径
const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`); const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`);
// 尝试解析配置文件 // 尝试解析配置文件
try { try {
// 读取配置文件 // 读取配置文件内容
const data = JSON.parse( const configFileContent = existsSync(configFilePath)
existsSync(configFilePath) ? readFileSync(configFilePath).toString()
? readFileSync(configFilePath).toString() : readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString();
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString() // 解析配置文件并加载配置
) as OneBotConfig; const data = loadConfig(json5.parse(configFileContent)) as OneBotConfig;
// 返回配置文件 // 返回配置文件
return sendSuccess(res, data); return sendSuccess(res, data);
} catch (e) { } catch (e) {
@@ -50,9 +49,12 @@ export const OB11SetConfigHandler: RequestHandler = async (req, res) => {
} }
// 写入配置 // 写入配置
try { 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); return sendSuccess(res, null);
} catch (e) { } catch (e) {
return sendError(res, 'Error: ' + e); return sendError(res, 'Error: ' + e);
} }
}; };

View File

@@ -7,8 +7,8 @@ export const GetProxyHandler: RequestHandler = async (req, res) => {
if (url && typeof url === 'string') { if (url && typeof url === 'string') {
url = decodeURIComponent(url); url = decodeURIComponent(url);
const responseText = await RequestUtil.HttpGetText(url); const responseText = await RequestUtil.HttpGetText(url);
res.send(sendSuccess(res, responseText)); return sendSuccess(res, responseText);
} else { } else {
res.send(sendError(res, 'url参数不合法')); return sendError(res, 'url参数不合法');
} }
}; };

View File

@@ -203,4 +203,32 @@ export class WebUiConfigWrapper {
} }
return ''; return '';
} }
// 获取字体文件夹内的字体列表
public static async GetFontList(): Promise<string[]> {
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
if (
await fs
.access(fontsPath, constants.F_OK)
.then(() => true)
.catch(() => false)
) {
return (await fs.readdir(fontsPath)).filter((file) => file.endsWith('.ttf'));
}
return [];
}
// 判断字体是否存在webui.woff
public static async CheckWebUIFontExist(): Promise<boolean> {
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
return await fs
.access(resolve(fontsPath, './webui.woff'), constants.F_OK)
.then(() => true)
.catch(() => false);
}
// 获取webui字体文件路径
public static GetWebUIFontPath(): string {
return resolve(webUiPathWrapper.configPath, './fonts/webui.woff');
}
} }

View File

@@ -13,7 +13,9 @@ import {
BatchMoveHandler, BatchMoveHandler,
DownloadHandler, DownloadHandler,
BatchDownloadHandler, // 新增下载处理方法 BatchDownloadHandler, // 新增下载处理方法
UploadHandler, // 添加上传处理器 UploadHandler,
UploadWebUIFontHandler,
DeleteWebUIFontHandler, // 添加上传处理器
} from '../api/File'; } from '../api/File';
const router = Router(); const router = Router();
@@ -37,5 +39,8 @@ router.post('/move', MoveHandler);
router.post('/batchMove', BatchMoveHandler); router.post('/batchMove', BatchMoveHandler);
router.post('/download', DownloadHandler); router.post('/download', DownloadHandler);
router.post('/batchDownload', BatchDownloadHandler); router.post('/batchDownload', BatchDownloadHandler);
router.post('/upload', UploadHandler); // 添加上传处理路由 router.post('/upload', UploadHandler);
router.post('/font/upload/webui', UploadWebUIFontHandler);
router.post('/font/delete/webui', DeleteWebUIFontHandler);
export { router as FileRouter }; export { router as FileRouter };

Some files were not shown because too many files have changed in this diff Show More