mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
43 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
93f78f4db5 | ||
![]() |
404bfdd5e6 | ||
![]() |
e4577dc2f1 | ||
![]() |
5c932e5a27 | ||
![]() |
4bd63c6267 | ||
![]() |
aabe24f903 | ||
![]() |
69cebd7fbc | ||
![]() |
8da371176a | ||
![]() |
dd08adf1d1 | ||
![]() |
2f67bef139 | ||
![]() |
8968c51cdc | ||
![]() |
f2fdcc9289 | ||
![]() |
aa3a575cbe | ||
![]() |
11816d038d | ||
![]() |
6a990edb38 | ||
![]() |
fa12865924 | ||
![]() |
ecdd717742 | ||
![]() |
6851334af9 | ||
![]() |
9051b29565 | ||
![]() |
95c7d3dfbd | ||
![]() |
bc1148c00a | ||
![]() |
d4556d9299 | ||
![]() |
5d389a2359 | ||
![]() |
305116874b | ||
![]() |
b08a29897f | ||
![]() |
b59c1d9122 | ||
![]() |
adb9cea701 | ||
![]() |
5e148d2e82 | ||
![]() |
a0d780558e | ||
![]() |
ad56065a4e | ||
![]() |
f5dee80b6e | ||
![]() |
9cc75881b8 | ||
![]() |
593fb13b61 | ||
![]() |
fca90592d6 | ||
![]() |
d6848e2855 | ||
![]() |
7539a4129f | ||
![]() |
5402574266 | ||
![]() |
853175aa1a | ||
![]() |
feb84809ec | ||
![]() |
a812c568e4 | ||
![]() |
11db25e355 | ||
![]() |
ecd2fba629 | ||
![]() |
a6763cf5a1 |
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.5.4",
|
||||
"version": "4.5.17",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -32,6 +32,7 @@
|
||||
"@heroui/pagination": "^2.2.9",
|
||||
"@heroui/popover": "2.3.10",
|
||||
"@heroui/select": "2.4.10",
|
||||
"@heroui/skeleton": "^2.2.6",
|
||||
"@heroui/slider": "2.4.8",
|
||||
"@heroui/snippet": "2.2.11",
|
||||
"@heroui/spinner": "2.2.7",
|
||||
|
@@ -231,7 +231,7 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
||||
: 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md'
|
||||
)}
|
||||
variant="solid"
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
onPress={() => setIsCollapsed(!isCollapsed)}
|
||||
>
|
||||
|
@@ -33,7 +33,7 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
|
||||
>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
startContent={<IoAddCircleOutline className="text-2xl" />}
|
||||
>
|
||||
新建
|
||||
|
@@ -27,7 +27,7 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
|
||||
取消更改
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
isLoading={isSubmitting}
|
||||
onPress={() => onSubmit()}
|
||||
>
|
||||
|
@@ -110,7 +110,7 @@ const AudioInsert = () => {
|
||||
<Tooltip content="发送音频">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<IoMic className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -120,7 +120,7 @@ const AudioInsert = () => {
|
||||
<Tooltip content="上传音频">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -137,7 +137,7 @@ const AudioInsert = () => {
|
||||
<PopoverTrigger tooltip="输入音频地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -154,7 +154,7 @@ const AudioInsert = () => {
|
||||
placeholder="请输入音频地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
@@ -177,7 +177,7 @@ const AudioInsert = () => {
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -190,7 +190,7 @@ const AudioInsert = () => {
|
||||
<PopoverContent className="flex-col gap-2 p-4">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color={isRecording ? 'danger' : 'danger'}
|
||||
color={isRecording ? 'primary' : 'primary'}
|
||||
variant="flat"
|
||||
onPress={isRecording ? stopRecording : startRecording}
|
||||
>
|
||||
@@ -198,7 +198,7 @@ const AudioInsert = () => {
|
||||
</Button>
|
||||
{showPreview && audioPreview && (
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={handleShowPreview}
|
||||
>
|
||||
@@ -212,7 +212,7 @@ const AudioInsert = () => {
|
||||
className={clsx(
|
||||
'w-4 h-4 rounded-full',
|
||||
isRecording
|
||||
? 'animate-pulse bg-danger-400'
|
||||
? 'animate-pulse bg-primary-400'
|
||||
: 'bg-success-400'
|
||||
)}
|
||||
></span>
|
||||
|
@@ -10,7 +10,7 @@ const DiceInsert = () => {
|
||||
return (
|
||||
<Tooltip content="发送骰子">
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -55,7 +55,7 @@ const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
|
||||
<Tooltip content="插入表情">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<MdEmojiEmotions className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -65,7 +65,7 @@ const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
|
||||
{visibleEmojis.map((emoji) => (
|
||||
<Button
|
||||
key={emoji.id}
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -35,7 +35,7 @@ const FileInsert = () => {
|
||||
<Tooltip content="发送文件">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<FaFolder className="text-lg" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -45,7 +45,7 @@ const FileInsert = () => {
|
||||
<Tooltip content="上传文件">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -62,7 +62,7 @@ const FileInsert = () => {
|
||||
<PopoverTrigger tooltip="输入文件地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -79,7 +79,7 @@ const FileInsert = () => {
|
||||
placeholder="请输入文件地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -23,7 +23,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
<Tooltip content="插入图片">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<MdImage className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -33,7 +33,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
<Tooltip content="上传图片">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -50,7 +50,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
<PopoverTrigger tooltip="输入图片地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -67,7 +67,7 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||
placeholder="请输入图片地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -80,7 +80,7 @@ const MusicInsert = () => {
|
||||
<Tooltip content="发送音乐">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<IoMusicalNotes className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -132,7 +132,7 @@ const MusicInsert = () => {
|
||||
<Button
|
||||
fullWidth
|
||||
size="lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
onPress={() => {
|
||||
@@ -236,7 +236,7 @@ const MusicInsert = () => {
|
||||
<Button
|
||||
fullWidth
|
||||
size="lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
type="submit"
|
||||
|
@@ -19,7 +19,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
|
||||
<Tooltip content="回复消息">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<BsChatQuoteFill className="text-lg" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -38,7 +38,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
|
@@ -10,7 +10,7 @@ const RPSInsert = () => {
|
||||
return (
|
||||
<Tooltip content="发送猜拳">
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -35,7 +35,7 @@ const VideoInsert = () => {
|
||||
<Tooltip content="发送视频">
|
||||
<div className="max-w-fit">
|
||||
<PopoverTrigger>
|
||||
<Button color="danger" variant="flat" isIconOnly radius="full">
|
||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
||||
<IoVideocam className="text-xl" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -45,7 +45,7 @@ const VideoInsert = () => {
|
||||
<Tooltip content="上传视频">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -62,7 +62,7 @@ const VideoInsert = () => {
|
||||
<PopoverTrigger tooltip="输入视频地址">
|
||||
<Button
|
||||
className="text-lg"
|
||||
color="danger"
|
||||
color="primary"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
radius="full"
|
||||
@@ -79,7 +79,7 @@ const VideoInsert = () => {
|
||||
placeholder="请输入视频地址"
|
||||
/>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
|
@@ -190,7 +190,7 @@ const ChatInput = () => {
|
||||
<DiceInsert />
|
||||
<RPSInsert />
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
onPress={() => {
|
||||
const messages = getChatMessage()
|
||||
showStructuredMessage(messages)
|
||||
|
@@ -15,7 +15,7 @@ export default function ChatInputModal() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onPress={onOpen} color="danger" radius="full" variant="flat">
|
||||
<Button onPress={onOpen} color="primary" radius="full" variant="flat">
|
||||
构造聊天消息
|
||||
</Button>
|
||||
<Modal
|
||||
@@ -36,7 +36,7 @@ export default function ChatInputModal() {
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" onPress={onClose} variant="flat">
|
||||
<Button color="primary" onPress={onClose} variant="flat">
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -78,7 +78,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
|
||||
{debug ? '关闭调试' : '开启调试'}
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
startContent={<MdDeleteForever />}
|
||||
onPress={handleDelete}
|
||||
>
|
||||
|
@@ -19,7 +19,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
||||
className={clsx(
|
||||
'bg-opacity-60 shadow-sm md:rounded-3xl',
|
||||
size === 'md'
|
||||
? 'col-span-8 md:col-span-2 bg-danger-50 shadow-danger-100'
|
||||
? 'col-span-8 md:col-span-2 bg-primary-50 shadow-primary-100'
|
||||
: 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200'
|
||||
)}
|
||||
shadow="sm"
|
||||
|
@@ -33,7 +33,7 @@ export default function CreateFileModal({
|
||||
<ModalHeader>新建</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex flex-col gap-4">
|
||||
<ButtonGroup color="danger">
|
||||
<ButtonGroup color="primary">
|
||||
<Button
|
||||
variant={fileType === 'file' ? 'solid' : 'flat'}
|
||||
onPress={() => onTypeChange('file')}
|
||||
@@ -51,10 +51,10 @@ export default function CreateFileModal({
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onCreate}>
|
||||
<Button color="primary" onPress={onCreate}>
|
||||
创建
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -81,10 +81,10 @@ export default function FileEditModal({
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onSave}>
|
||||
<Button color="primary" onPress={onSave}>
|
||||
保存
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -45,6 +45,12 @@ export default function FilePreviewModal({
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (filePath) {
|
||||
run()
|
||||
}
|
||||
}, [filePath])
|
||||
|
||||
let contentElement = null
|
||||
if (!supportedPreviewExts.includes(ext)) {
|
||||
contentElement = <div>暂不支持预览此文件类型</div>
|
||||
@@ -68,12 +74,6 @@ export default function FilePreviewModal({
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (filePath) {
|
||||
run()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} scrollBehavior="inside" size="3xl">
|
||||
<ModalContent>
|
||||
@@ -82,7 +82,7 @@ export default function FilePreviewModal({
|
||||
{contentElement}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -82,7 +82,7 @@ export default function FileTable({
|
||||
setPreviewImages([])
|
||||
setPreviewIndex(0)
|
||||
setShowImage(false)
|
||||
}, [files])
|
||||
}, [currentPath])
|
||||
|
||||
const onPreviewImage = (name: string, images: PreviewImage[]) => {
|
||||
const index = images.findIndex((image) => image.key === name)
|
||||
@@ -116,7 +116,7 @@ export default function FileTable({
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="danger"
|
||||
color="primary"
|
||||
page={page}
|
||||
total={pages}
|
||||
onChange={(page) => setPage(page)}
|
||||
@@ -195,7 +195,7 @@ export default function FileTable({
|
||||
<ButtonGroup size="sm">
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={() => onRenameRequest(file.name)}
|
||||
>
|
||||
@@ -203,7 +203,7 @@ export default function FileTable({
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={() => onMoveRequest(file.name)}
|
||||
>
|
||||
@@ -211,7 +211,7 @@ export default function FileTable({
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={() => onCopyPath(file.name)}
|
||||
>
|
||||
@@ -219,7 +219,7 @@ export default function FileTable({
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={() => onDownload(filePath)}
|
||||
>
|
||||
@@ -227,7 +227,7 @@ export default function FileTable({
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={() => onDelete(filePath)}
|
||||
>
|
||||
|
@@ -58,6 +58,7 @@ export default function ImageNameButton({
|
||||
run()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="light"
|
||||
@@ -69,7 +70,15 @@ export default function ImageNameButton({
|
||||
) : loading || !data ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
<Image src={data} alt={name} className="w-8 h-8" radius="sm" />
|
||||
<Image
|
||||
src={data}
|
||||
alt={name}
|
||||
className="w-8 h-8 flex-shrink-0"
|
||||
classNames={{
|
||||
wrapper: 'w-8 h-8 flex-shrink-0'
|
||||
}}
|
||||
radius="sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@@ -86,13 +86,13 @@ function DirectoryTree({
|
||||
onPress={handleClick}
|
||||
className="py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md"
|
||||
size="sm"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant={variant}
|
||||
startContent={
|
||||
<div
|
||||
className={clsx(
|
||||
'rounded-md',
|
||||
isSeleted ? 'bg-danger-600' : 'bg-danger-50'
|
||||
isSeleted ? 'bg-primary-600' : 'bg-primary-50'
|
||||
)}
|
||||
>
|
||||
{expanded ? <IoRemove /> : <IoAdd />}
|
||||
@@ -105,7 +105,7 @@ function DirectoryTree({
|
||||
<div>
|
||||
{loading ? (
|
||||
<div className="flex py-1 px-8">
|
||||
<Spinner size="sm" color="danger" />
|
||||
<Spinner size="sm" color="primary" />
|
||||
</div>
|
||||
) : (
|
||||
dirs.map((dirName) => {
|
||||
@@ -155,10 +155,10 @@ export default function MoveModal({
|
||||
<p className="text-sm text-default-500">移动项:{selectionInfo}</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onMove}>
|
||||
<Button color="primary" onPress={onMove}>
|
||||
确定
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -31,10 +31,10 @@ export default function RenameModal({
|
||||
<Input label="新名称" value={newFileName} onChange={onNameChange} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onRename}>
|
||||
<Button color="primary" onPress={onRename}>
|
||||
确定
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@@ -33,7 +33,7 @@ export default function Hitokoto() {
|
||||
<div className="relative">
|
||||
{loading && <PageLoading />}
|
||||
{error ? (
|
||||
<div className="text-danger-400">一言加载失败:{error.message}</div>
|
||||
<div className="text-primary-400">一言加载失败:{error.message}</div>
|
||||
) : (
|
||||
<>
|
||||
<div>{data?.hitokoto}</div>
|
||||
@@ -52,7 +52,7 @@ export default function Hitokoto() {
|
||||
isLoading={loading}
|
||||
isIconOnly
|
||||
radius="full"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
>
|
||||
<IoRefresh />
|
||||
|
@@ -34,7 +34,7 @@ export default function HoverTiltedCard({
|
||||
rotateAmplitude = 14,
|
||||
showTooltip = false,
|
||||
overlayContent = (
|
||||
<div className="text-center mt-6 px-4 py-0.5 shadow-lg rounded-full bg-danger-600 text-default-100 bg-opacity-80">
|
||||
<div className="text-center mt-6 px-4 py-0.5 shadow-lg rounded-full bg-primary-600 text-default-100 bg-opacity-80">
|
||||
NapCat
|
||||
</div>
|
||||
),
|
||||
|
69
napcat.webui/src/components/input/file_input.tsx
Normal file
69
napcat.webui/src/components/input/file_input.tsx
Normal 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
|
@@ -43,7 +43,7 @@ const ImageInput: React.FC<ImageInputProps> = ({ onChange, value, label }) => {
|
||||
onChange('')
|
||||
if (inputRef.current) inputRef.current.value = ''
|
||||
}}
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
>
|
||||
|
@@ -16,13 +16,13 @@ const logLevelColor: {
|
||||
| 'secondary'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'primary'
|
||||
} = {
|
||||
[LogLevel.DEBUG]: 'default',
|
||||
[LogLevel.INFO]: 'primary',
|
||||
[LogLevel.WARN]: 'warning',
|
||||
[LogLevel.ERROR]: 'danger',
|
||||
[LogLevel.FATAL]: 'danger'
|
||||
[LogLevel.ERROR]: 'primary',
|
||||
[LogLevel.FATAL]: 'primary'
|
||||
}
|
||||
const LogLevelSelect = (props: LogLevelSelectProps) => {
|
||||
const { selectedKeys, onSelectionChange } = props
|
||||
|
@@ -65,7 +65,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
||||
<ModalFooter>
|
||||
{showCancel && (
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
onCancel?.()
|
||||
@@ -76,7 +76,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
onPress={() => {
|
||||
onConfirm?.()
|
||||
nativeClose()
|
||||
|
@@ -28,7 +28,7 @@ import type {
|
||||
|
||||
function displayData(data: number, loading: boolean, error?: Error) {
|
||||
if (error) {
|
||||
return <MdError className="text-danger-400" />
|
||||
return <MdError className="text-primary-400" />
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
@@ -175,7 +175,7 @@ export default function NapCatRepoInfo() {
|
||||
className="group h-auto py-3"
|
||||
endContent={
|
||||
releaseError ? (
|
||||
<MdError className="text-danger-400" />
|
||||
<MdError className="text-primary-400" />
|
||||
) : releaseLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
@@ -229,7 +229,7 @@ export default function NapCatRepoInfo() {
|
||||
</span>
|
||||
}
|
||||
startContent={
|
||||
<IconWrapper className="bg-danger/10 text-danger dark:text-danger-500">
|
||||
<IconWrapper className="bg-primary/10 text-primary dark:text-primary-500">
|
||||
<BookIcon />
|
||||
</IconWrapper>
|
||||
}
|
||||
|
@@ -150,7 +150,7 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
isDisabled={formState.isSubmitting}
|
||||
variant="light"
|
||||
onPress={onClose}
|
||||
|
@@ -91,7 +91,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
|
||||
return (
|
||||
<section className="p-4 pt-14 rounded-lg shadow-md">
|
||||
<h1 className="text-2xl font-bold mb-4 flex items-center gap-1 text-danger-400">
|
||||
<h1 className="text-2xl font-bold mb-4 flex items-center gap-1 text-primary-400">
|
||||
<PiCatDuotone />
|
||||
{data.description}
|
||||
</h1>
|
||||
@@ -125,7 +125,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
/>
|
||||
<Button
|
||||
onPress={sendRequest}
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="lg"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
|
@@ -27,7 +27,7 @@ const SchemaType = ({
|
||||
name = '固定值'
|
||||
break
|
||||
}
|
||||
let chipColor: 'primary' | 'success' | 'danger' | 'warning' | 'secondary' =
|
||||
let chipColor: 'primary' | 'success' | 'primary' | 'warning' | 'secondary' =
|
||||
'primary'
|
||||
switch (type) {
|
||||
case 'enum':
|
||||
@@ -37,7 +37,7 @@ const SchemaType = ({
|
||||
chipColor = 'secondary'
|
||||
break
|
||||
case 'array':
|
||||
chipColor = 'danger'
|
||||
chipColor = 'primary'
|
||||
break
|
||||
case 'object':
|
||||
chipColor = 'success'
|
||||
|
@@ -33,11 +33,11 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
>
|
||||
<div className="w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0">
|
||||
<Input
|
||||
className="sticky top-0 z-10 text-danger-600"
|
||||
className="sticky top-0 z-10 text-primary-600"
|
||||
classNames={{
|
||||
inputWrapper:
|
||||
'bg-opacity-30 bg-danger-50 backdrop-blur-sm border border-danger-300 mb-2',
|
||||
input: 'bg-transparent !text-danger-400 !placeholder-danger-400'
|
||||
'bg-opacity-30 bg-primary-50 backdrop-blur-sm border border-primary-300 mb-2',
|
||||
input: 'bg-transparent !text-primary-400 !placeholder-primary-400'
|
||||
}}
|
||||
radius="full"
|
||||
placeholder="搜索 API"
|
||||
@@ -51,7 +51,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
key={apiName}
|
||||
shadow="none"
|
||||
className={clsx(
|
||||
'w-full border border-danger-100 rounded-lg mb-1 bg-opacity-30 backdrop-blur-sm text-danger-400',
|
||||
'w-full border border-primary-100 rounded-lg mb-1 bg-opacity-30 backdrop-blur-sm text-primary-400',
|
||||
{
|
||||
hidden: !(
|
||||
apiName.includes(searchValue) ||
|
||||
@@ -59,7 +59,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
)
|
||||
},
|
||||
{
|
||||
'!bg-opacity-40 border border-danger-400 bg-danger-50 text-danger-600':
|
||||
'!bg-opacity-40 border border-primary-400 bg-primary-50 text-primary-600':
|
||||
apiName === selectedApi
|
||||
}
|
||||
)}
|
||||
@@ -69,8 +69,8 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
<CardBody>
|
||||
<h2 className="font-bold">{api.description}</h2>
|
||||
<div
|
||||
className={clsx('text-sm text-danger-200', {
|
||||
'!text-danger-400': apiName === selectedApi
|
||||
className={clsx('text-sm text-primary-200', {
|
||||
'!text-primary-400': apiName === selectedApi
|
||||
})}
|
||||
>
|
||||
{apiName}
|
||||
|
@@ -109,7 +109,7 @@ const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
|
@@ -30,7 +30,7 @@ const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
radius="full"
|
||||
className="text-medium"
|
||||
|
@@ -43,7 +43,7 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onPress={onOpen} color="danger" radius="full" variant="flat">
|
||||
<Button onPress={onOpen} color="primary" radius="full" variant="flat">
|
||||
构造请求
|
||||
</Button>
|
||||
<Modal
|
||||
@@ -75,11 +75,11 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
||||
<ModalFooter>
|
||||
<ChatInputModal />
|
||||
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
onPress={() => handleSendMessage(onClose)}
|
||||
>
|
||||
发送
|
||||
|
@@ -10,7 +10,7 @@ function StatusTag({
|
||||
color
|
||||
}: {
|
||||
title: string
|
||||
color: 'success' | 'danger' | 'warning'
|
||||
color: 'success' | 'primary' | 'warning'
|
||||
}) {
|
||||
const textClassName = `text-${color} text-sm`
|
||||
const bgClassName = `bg-${color}`
|
||||
@@ -27,7 +27,7 @@ export default function WSStatus({ state }: WSStatusProps) {
|
||||
return <StatusTag title="已连接" color="success" />
|
||||
}
|
||||
if (state === ReadyState.CLOSED) {
|
||||
return <StatusTag title="已关闭" color="danger" />
|
||||
return <StatusTag title="已关闭" color="primary" />
|
||||
}
|
||||
if (state === ReadyState.CONNECTING) {
|
||||
return <StatusTag title="连接中" color="warning" />
|
||||
|
@@ -16,7 +16,7 @@ export interface QQInfoCardProps {
|
||||
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||
return (
|
||||
<Card
|
||||
className="relative bg-danger-100 bg-opacity-60 overflow-hidden flex-shrink-0 shadow-md shadow-danger-300 dark:shadow-danger-50"
|
||||
className="relative bg-primary-100 bg-opacity-60 overflow-hidden flex-shrink-0 shadow-md shadow-primary-300 dark:shadow-primary-50"
|
||||
shadow="none"
|
||||
radius="lg"
|
||||
>
|
||||
@@ -30,7 +30,7 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||
</CardBody>
|
||||
) : (
|
||||
<CardBody className="flex-row items-center gap-2 overflow-hidden relative">
|
||||
<div className="absolute right-0 bottom-0 text-5xl text-danger-400">
|
||||
<div className="absolute right-0 bottom-0 text-5xl text-primary-400">
|
||||
<BsTencentQq />
|
||||
</div>
|
||||
<div className="relative flex-shrink-0 z-10">
|
||||
@@ -43,14 +43,14 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'w-4 h-4 rounded-full absolute right-0.5 bottom-0 border-2 border-danger-100 z-10',
|
||||
'w-4 h-4 rounded-full absolute right-0.5 bottom-0 border-2 border-primary-100 z-10',
|
||||
data?.online ? 'bg-green-500' : 'bg-gray-500'
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex-col justify-center">
|
||||
<div className="text-lg truncate">{data?.nick}</div>
|
||||
<div className="text-danger-500 text-sm">{data?.uin}</div>
|
||||
<div className="text-primary-500 text-sm">{data?.uin}</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
)}
|
||||
|
@@ -11,7 +11,7 @@ const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode }) => {
|
||||
<div className="bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden">
|
||||
{!qrcode && (
|
||||
<div className="absolute left-2 top-2 right-2 bottom-2 bg-white bg-opacity-50 backdrop-blur flex items-center justify-center">
|
||||
<Spinner color="danger" />
|
||||
<Spinner color="primary" />
|
||||
</div>
|
||||
)}
|
||||
<QRCodeSVG size={180} value={qrcode} />
|
||||
|
@@ -63,7 +63,7 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
||||
<div className="mt-auto mb-10 md:mb-0">
|
||||
<Button
|
||||
className="w-full"
|
||||
color="danger"
|
||||
color="primary"
|
||||
radius="full"
|
||||
variant="light"
|
||||
onPress={toggleTheme}
|
||||
@@ -75,7 +75,7 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full mb-2"
|
||||
color="danger"
|
||||
color="primary"
|
||||
radius="full"
|
||||
variant="light"
|
||||
onPress={onRevokeAuth}
|
||||
|
@@ -55,7 +55,7 @@ const renderItems = (items: MenuItem[], children = false) => {
|
||||
isActive && 'bg-opacity-60',
|
||||
b64img && 'backdrop-blur-md text-white'
|
||||
)}
|
||||
color="danger"
|
||||
color="primary"
|
||||
endContent={
|
||||
canOpen ? (
|
||||
// div实现箭头V效果
|
||||
@@ -63,7 +63,9 @@ const renderItems = (items: MenuItem[], children = false) => {
|
||||
className={clsx(
|
||||
'ml-auto relative w-3 h-3 transition-transform',
|
||||
open && 'transform rotate-180',
|
||||
isActive ? 'text-danger-500' : 'text-red-300 dark:text-white',
|
||||
isActive
|
||||
? 'text-primary-500'
|
||||
: 'text-red-300 dark:text-white',
|
||||
'before:rounded-full',
|
||||
'before:content-[""]',
|
||||
'before:block',
|
||||
@@ -95,7 +97,7 @@ const renderItems = (items: MenuItem[], children = false) => {
|
||||
className={clsx(
|
||||
'w-3 h-1.5 rounded-full ml-auto shadow-lg',
|
||||
isActive
|
||||
? 'bg-danger-500 animate-spinner-ease-spin'
|
||||
? 'bg-primary-500 animate-spinner-ease-spin'
|
||||
: 'bg-red-300 dark:bg-white'
|
||||
)}
|
||||
/>
|
||||
|
@@ -4,6 +4,8 @@ import { Chip } from '@heroui/chip'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { Tooltip } from '@heroui/tooltip'
|
||||
import { useRequest } from 'ahooks'
|
||||
import { useEffect } from 'react'
|
||||
import { BsStars } from 'react-icons/bs'
|
||||
import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6'
|
||||
import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io'
|
||||
import { RiMacFill } from 'react-icons/ri'
|
||||
@@ -32,10 +34,10 @@ const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
|
||||
endContent
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex text-sm gap-1 p-2 items-center shadow-sm shadow-danger-50 dark:shadow-danger-100 rounded text-danger-400">
|
||||
<div className="flex text-sm gap-1 p-2 items-center shadow-sm shadow-primary-50 dark:shadow-primary-100 rounded text-primary-400">
|
||||
{icon}
|
||||
<div className="w-24">{title}</div>
|
||||
<div className="text-danger-200">{value}</div>
|
||||
<div className="text-primary-200">{value}</div>
|
||||
<div className="ml-auto">{endContent}</div>
|
||||
</div>
|
||||
)
|
||||
@@ -60,7 +62,7 @@ const NewVersionTip = (props: NewVersionTipProps) => {
|
||||
<Button
|
||||
isIconOnly
|
||||
radius="full"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="shadow"
|
||||
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
||||
onPress={() => {
|
||||
@@ -97,12 +99,48 @@ const NewVersionTip = (props: NewVersionTipProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const AISummaryComponent = () => {
|
||||
const {
|
||||
data: aiSummaryData,
|
||||
loading: aiSummaryLoading,
|
||||
error: aiSummaryError,
|
||||
run: runAiSummary
|
||||
} = useRequest(
|
||||
(version) =>
|
||||
request.get<ServerResponse<string | null>>(
|
||||
`https://release.nc.152710.xyz/?version=${version}`,
|
||||
{
|
||||
timeout: 30000
|
||||
}
|
||||
),
|
||||
{
|
||||
manual: true
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
runAiSummary(currentVersion)
|
||||
}, [currentVersion, runAiSummary])
|
||||
|
||||
if (aiSummaryLoading) {
|
||||
return (
|
||||
<div className="flex justify-center py-1">
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (aiSummaryError) {
|
||||
return <div className="text-center text-primary-500">AI 摘要获取失败</div>
|
||||
}
|
||||
return <span className="text-default-700">{aiSummaryData?.data.data}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content="有新版本可用">
|
||||
<Button
|
||||
isIconOnly
|
||||
radius="full"
|
||||
color="danger"
|
||||
color="primary"
|
||||
variant="shadow"
|
||||
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
||||
onPress={() => {
|
||||
@@ -120,6 +158,13 @@ const NewVersionTip = (props: NewVersionTipProps) => {
|
||||
<span>最新版本</span>
|
||||
<Chip color="primary">{latestVersion}</Chip>
|
||||
</div>
|
||||
<div className="p-2 rounded-md bg-content2 text-sm">
|
||||
<div className="text-primary-400 font-bold flex items-center gap-1 mb-1">
|
||||
<BsStars />
|
||||
<span>AI总结</span>
|
||||
</div>
|
||||
{<AISummaryComponent />}
|
||||
</div>
|
||||
<div className="text-sm space-y-2 !mt-4">
|
||||
{middleVersions.map((versionInfo) => (
|
||||
<div
|
||||
@@ -189,8 +234,8 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
|
||||
error: qqVersionError
|
||||
} = useRequest(WebUIManager.getQQVersion)
|
||||
return (
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-danger-50 dark:shadow-danger-100 overflow-visible flex-1">
|
||||
<CardHeader className="pb-0 items-center gap-1 text-danger-500 font-extrabold">
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-primary-50 dark:shadow-primary-100 overflow-visible flex-1">
|
||||
<CardHeader className="pb-0 items-center gap-1 text-primary-500 font-extrabold">
|
||||
<FaCircleInfo className="text-lg" />
|
||||
<span>系统信息</span>
|
||||
</CardHeader>
|
||||
|
@@ -55,7 +55,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-danger-50 dark:shadow-danger-100 col-span-1 lg:col-span-2 relative overflow-hidden">
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-primary-50 dark:shadow-primary-100 col-span-1 lg:col-span-2 relative overflow-hidden">
|
||||
<div className="absolute h-full right-0 top-0">
|
||||
<Image
|
||||
src={bkg}
|
||||
@@ -69,7 +69,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
</div>
|
||||
<CardBody className="overflow-visible md:flex-row gap-4 items-center justify-stretch z-10">
|
||||
<div className="flex-1 w-full md:max-w-96">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-danger-400">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-primary-400">
|
||||
<GiCpu className="text-xl" />
|
||||
<span>CPU</span>
|
||||
</h2>
|
||||
@@ -88,7 +88,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
unit="%"
|
||||
/>
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-danger-400 mt-2">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-primary-400 mt-2">
|
||||
<BiSolidMemoryCard className="text-xl" />
|
||||
<span>内存</span>
|
||||
</h2>
|
||||
|
@@ -62,7 +62,7 @@ export const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||
className={clsx(
|
||||
'px-2 py-1 flex items-center gap-1 text-sm font-medium border-b-2 transition-colors',
|
||||
isSelected
|
||||
? 'border-danger text-danger'
|
||||
? 'border-primary text-primary'
|
||||
: 'border-transparent hover:border-default',
|
||||
className
|
||||
)}
|
||||
|
@@ -35,6 +35,7 @@ const AudioProvider: React.FC<MusicProviderProps> = ({ children }) => {
|
||||
const [musicId, setMusicId] = useState<number>(0)
|
||||
const [playMode, setPlayMode] = useState<PlayMode>(PlayMode.Loop)
|
||||
const music = musicList.find((music) => music.id === musicId)
|
||||
const [token] = useLocalStorage(key.token, '')
|
||||
const onNext = () => {
|
||||
const nextID = getNextMusic(musicList, musicId, playMode)
|
||||
setMusicId(nextID)
|
||||
@@ -60,8 +61,8 @@ const AudioProvider: React.FC<MusicProviderProps> = ({ children }) => {
|
||||
setMusicId(res[0].id)
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchMusicList(listId)
|
||||
}, [listId])
|
||||
if (listId && token) fetchMusicList(listId)
|
||||
}, [listId, token])
|
||||
return (
|
||||
<AudioContext.Provider
|
||||
value={{
|
||||
|
@@ -196,4 +196,26 @@ export default class FileManager {
|
||||
)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@@ -9,14 +9,6 @@ export interface Log {
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface TerminalSession {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface TerminalInfo {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default class WebUIManager {
|
||||
public static async checkWebUiLogined() {
|
||||
const { data } =
|
||||
@@ -40,6 +32,13 @@ export default class WebUIManager {
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async checkUsingDefaultToken() {
|
||||
const { data } = await serverRequest.get<ServerResponse<boolean>>(
|
||||
'/auth/check_using_default_token'
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async proxy<T>(url = '') {
|
||||
const data = await serverRequest.get<ServerResponse<string>>(
|
||||
'/base/proxy?url=' + encodeURIComponent(url)
|
||||
|
@@ -79,7 +79,7 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
}, [location.pathname])
|
||||
return (
|
||||
<div
|
||||
className="h-screen relative flex bg-danger-50 dark:bg-black items-stretch"
|
||||
className="h-screen relative flex bg-primary-50 dark:bg-black items-stretch"
|
||||
style={{
|
||||
backgroundImage: `url(${b64img})`,
|
||||
backgroundSize: 'cover'
|
||||
@@ -99,9 +99,9 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
<div
|
||||
className={clsx(
|
||||
'h-10 flex items-center font-bold text-xl backdrop-blur-lg rounded-full',
|
||||
'dark:bg-background dark:shadow-danger-100',
|
||||
'dark:bg-background dark:shadow-primary-100',
|
||||
'bg-background !bg-opacity-50',
|
||||
'shadow-sm shadow-danger-50',
|
||||
'shadow-sm shadow-primary-50',
|
||||
'z-30 m-2 mb-0 sticky top-2 left-0'
|
||||
)}
|
||||
>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Card, CardBody } from '@heroui/card'
|
||||
import { Image } from '@heroui/image'
|
||||
import { Link } from '@heroui/link'
|
||||
import { Skeleton } from '@heroui/skeleton'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { useRequest } from 'ahooks'
|
||||
import { useMemo } from 'react'
|
||||
@@ -22,7 +23,7 @@ function VersionInfo() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-2xl font-bold">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-danger-500 drop-shadow-md">NapCat</div>
|
||||
<div className="text-primary-500 drop-shadow-md">NapCat</div>
|
||||
{error ? (
|
||||
error.message
|
||||
) : loading ? (
|
||||
@@ -30,7 +31,7 @@ function VersionInfo() {
|
||||
) : (
|
||||
<RotatingText
|
||||
texts={['WebUI', data?.version ?? '']}
|
||||
mainClassName="overflow-hidden flex items-center bg-danger-500 px-2 rounded-lg text-default-50 shadow-md"
|
||||
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 }}
|
||||
@@ -78,16 +79,12 @@ export default function AboutPage() {
|
||||
const imageUrl = getImageUrl(baseUrl)
|
||||
|
||||
if (!imageUrl) {
|
||||
return (
|
||||
<div className="flex-1 h-32 flex items-center justify-center bg-default-100 rounded-lg">
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
return <Skeleton className="h-16 rounded-lg" />
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
className="flex-1 pointer-events-none select-none"
|
||||
className="flex-1 pointer-events-none select-none rounded-none"
|
||||
src={imageUrl}
|
||||
alt={alt}
|
||||
/>
|
||||
@@ -107,12 +104,12 @@ export default function AboutPage() {
|
||||
<div className="flex-1 flex flex-col gap-2 py-2">
|
||||
<VersionInfo />
|
||||
<div className="space-y-1">
|
||||
<p className="font-bold text-danger-400">NapCat 是什么?</p>
|
||||
<p className="font-bold text-primary-400">NapCat 是什么?</p>
|
||||
<p className="text-default-800">
|
||||
基于TypeScript构建的Bot框架,通过相应的启动器或者框架,主动调用QQ
|
||||
Node模块提供给客户端的接口,实现Bot的功能.
|
||||
</p>
|
||||
<p className="font-bold text-danger-400">魔法版介绍</p>
|
||||
<p className="font-bold text-primary-400">魔法版介绍</p>
|
||||
<p className="text-default-800">
|
||||
猫猫框架通过魔法的手段获得了 QQ 的发送消息、接收消息等接口。
|
||||
为了方便使用,猫猫框架将通过一种名为 OneBot 的约定将你的 HTTP /
|
||||
@@ -131,7 +128,7 @@ export default function AboutPage() {
|
||||
href="https://qm.qq.com/q/F9cgs1N3Mc"
|
||||
>
|
||||
<CardBody className="flex-row items-center gap-2">
|
||||
<span className="p-2 rounded-small bg-primary-50">
|
||||
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||
<BsTencentQq size={16} />
|
||||
</span>
|
||||
<span>官方社群1</span>
|
||||
@@ -146,7 +143,7 @@ export default function AboutPage() {
|
||||
href="https://qm.qq.com/q/hSt0u9PVn"
|
||||
>
|
||||
<CardBody className="flex-row items-center gap-2">
|
||||
<span className="p-2 rounded-small bg-primary-50">
|
||||
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||
<BsTencentQq size={16} />
|
||||
</span>
|
||||
<span>官方社群2</span>
|
||||
@@ -161,7 +158,7 @@ export default function AboutPage() {
|
||||
href="https://t.me/MelodicMoonlight"
|
||||
>
|
||||
<CardBody className="flex-row items-center gap-2">
|
||||
<span className="p-2 rounded-small bg-primary-50">
|
||||
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||
<BsTelegram size={16} />
|
||||
</span>
|
||||
<span>Telegram</span>
|
||||
@@ -176,7 +173,7 @@ export default function AboutPage() {
|
||||
href="https://napcat.napneko.icu/"
|
||||
>
|
||||
<CardBody className="flex-row items-center gap-2">
|
||||
<span className="p-2 rounded-small bg-primary-50">
|
||||
<span className="p-2 rounded-small bg-primary-50 text-primary-500">
|
||||
<IoDocument size={16} />
|
||||
</span>
|
||||
<span>使用文档</span>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Card, CardBody } from '@heroui/card'
|
||||
import { Tab, Tabs } from '@heroui/tabs'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
|
||||
import ChangePasswordCard from './change_password'
|
||||
import OneBotConfigCard from './onebot'
|
||||
@@ -22,6 +23,11 @@ const ConfingPageItem: React.FC<ConfigPageProps> = ({ children }) => {
|
||||
|
||||
export default function ConfigPage() {
|
||||
const isMediumUp = useMediaQuery({ minWidth: 768 })
|
||||
const navigate = useNavigate()
|
||||
const search = useSearchParams({
|
||||
tab: 'onebot'
|
||||
})[0]
|
||||
const tab = search.get('tab') ?? 'onebot'
|
||||
|
||||
return (
|
||||
<section className="w-[1000px] max-w-full md:mx-auto gap-4 py-8 px-2 md:py-10">
|
||||
@@ -30,6 +36,10 @@ export default function ConfigPage() {
|
||||
fullWidth
|
||||
className="w-full"
|
||||
isVertical={isMediumUp}
|
||||
selectedKey={tab}
|
||||
onSelectionChange={(key) => {
|
||||
navigate(`/config?tab=${key}`)
|
||||
}}
|
||||
classNames={{
|
||||
tabList: 'sticky flex top-14 bg-opacity-50 backdrop-blur-sm',
|
||||
panel: 'w-full relative',
|
||||
|
@@ -7,11 +7,13 @@ import toast from 'react-hot-toast'
|
||||
import key from '@/const/key'
|
||||
|
||||
import SaveButtons from '@/components/button/save_buttons'
|
||||
import FileInput from '@/components/input/file_input'
|
||||
import ImageInput from '@/components/input/image_input'
|
||||
|
||||
import useMusic from '@/hooks/use-music'
|
||||
|
||||
import { siteConfig } from '@/config/site'
|
||||
import FileManager from '@/controllers/file_manager'
|
||||
|
||||
const WebUIConfigCard = () => {
|
||||
const {
|
||||
@@ -59,17 +61,51 @@ const WebUIConfigCard = () => {
|
||||
return (
|
||||
<>
|
||||
<title>WebUI配置 - NapCat WebUI</title>
|
||||
<Controller
|
||||
control={control}
|
||||
name="musicListID"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="网易云音乐歌单ID(网页内音乐播放器)"
|
||||
placeholder="请输入歌单ID"
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex-shrink-0 w-full">WebUI字体</div>
|
||||
<div className="text-sm text-default-400">
|
||||
此项不需要手动保存,上传成功后需清空网页缓存并刷新
|
||||
<FileInput
|
||||
label="中文字体"
|
||||
onChange={async (file) => {
|
||||
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()
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 1000)
|
||||
} 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-shrink-0 w-full">背景图</div>
|
||||
<Controller
|
||||
|
@@ -41,7 +41,7 @@ export default function HttpDebug() {
|
||||
>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
radius="md"
|
||||
variant="shadow"
|
||||
size="sm"
|
||||
|
@@ -64,7 +64,7 @@ export default function WSDebug() {
|
||||
/>
|
||||
<div className="flex-shrink-0 flex gap-2 col-span-2 md:col-span-1">
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
onPress={handleConnect}
|
||||
size="lg"
|
||||
radius="full"
|
||||
|
@@ -332,7 +332,7 @@ export default function FileManagerPage() {
|
||||
<div className="p-4">
|
||||
<div className="mb-4 flex items-center gap-4 sticky top-14 z-10 bg-content1 py-1">
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
@@ -343,7 +343,7 @@ export default function FileManagerPage() {
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
@@ -354,7 +354,7 @@ export default function FileManagerPage() {
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
isLoading={loading}
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@@ -365,7 +365,7 @@ export default function FileManagerPage() {
|
||||
<MdRefresh />
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
@@ -379,7 +379,7 @@ export default function FileManagerPage() {
|
||||
selectedFiles === 'all') && (
|
||||
<>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={handleBatchDelete}
|
||||
@@ -391,7 +391,7 @@ export default function FileManagerPage() {
|
||||
)
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => {
|
||||
@@ -406,7 +406,7 @@ export default function FileManagerPage() {
|
||||
)
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={handleBatchDownload}
|
||||
|
@@ -105,7 +105,7 @@ const DashboardIndexPage: React.FC = () => {
|
||||
<SystemStatusCard setArchInfo={setArchInfo} />
|
||||
</div>
|
||||
<Networks />
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-danger-50">
|
||||
<Card className="bg-opacity-60 shadow-sm shadow-primary-50">
|
||||
<CardBody>
|
||||
<Hitokoto />
|
||||
</CardBody>
|
||||
|
@@ -133,7 +133,7 @@ export default function TerminalPage() {
|
||||
size="sm"
|
||||
className="min-w-0 w-4 h-4 flex-shrink-0"
|
||||
onPress={() => closeTerminal(tab.id)}
|
||||
color={selectedTab === tab.id ? 'danger' : 'default'}
|
||||
color={selectedTab === tab.id ? 'primary' : 'default'}
|
||||
>
|
||||
<IoClose />
|
||||
</Button>
|
||||
@@ -143,7 +143,7 @@ export default function TerminalPage() {
|
||||
</TabList>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
color="primary"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={createNewTerminal}
|
||||
|
@@ -1,14 +1,46 @@
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import { Suspense } from 'react'
|
||||
import { Outlet, useLocation } from 'react-router-dom'
|
||||
import { Suspense, useEffect } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import useAuth from '@/hooks/auth'
|
||||
import useDialog from '@/hooks/use-dialog'
|
||||
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
import DefaultLayout from '@/layouts/default'
|
||||
|
||||
const CheckDefaultPassword = () => {
|
||||
const { isAuth } = useAuth()
|
||||
const dialog = useDialog()
|
||||
const navigate = useNavigate()
|
||||
const checkDefaultPassword = async () => {
|
||||
const data = await WebUIManager.checkUsingDefaultToken()
|
||||
if (data) {
|
||||
dialog.confirm({
|
||||
title: '修改默认密码',
|
||||
content: '检测到当前密码为默认密码,请尽快修改密码。',
|
||||
confirmText: '前往修改',
|
||||
onConfirm: () => {
|
||||
navigate('/config?tab=token')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuth) {
|
||||
checkDefaultPassword()
|
||||
}
|
||||
}, [isAuth])
|
||||
return null
|
||||
}
|
||||
|
||||
export default function IndexPage() {
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<CheckDefaultPassword />
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex justify-center px-10">
|
||||
|
@@ -13,5 +13,74 @@ export default {
|
||||
extend: {}
|
||||
},
|
||||
darkMode: 'class',
|
||||
plugins: [heroui()]
|
||||
plugins: [
|
||||
heroui({
|
||||
themes: {
|
||||
light: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#f31260',
|
||||
foreground: '#fff',
|
||||
50: '#fee7ef',
|
||||
100: '#fdd0df',
|
||||
200: '#faa0bf',
|
||||
300: '#f871a0',
|
||||
400: '#f54180',
|
||||
500: '#f31260',
|
||||
600: '#c20e4d',
|
||||
700: '#920b3a',
|
||||
800: '#610726',
|
||||
900: '#310413'
|
||||
},
|
||||
danger: {
|
||||
DEFAULT: '#DB3694',
|
||||
foreground: '#fff',
|
||||
50: '#FEEAF6',
|
||||
100: '#FDD7DD',
|
||||
200: '#FBAFC4',
|
||||
300: '#F485AE',
|
||||
400: '#E965A3',
|
||||
500: '#DB3694',
|
||||
600: '#BC278B',
|
||||
700: '#9D1B7F',
|
||||
800: '#7F1170',
|
||||
900: '#690A66'
|
||||
}
|
||||
}
|
||||
},
|
||||
dark: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#f31260',
|
||||
foreground: '#fff',
|
||||
50: '#310413',
|
||||
100: '#610726',
|
||||
200: '#920b3a',
|
||||
300: '#c20e4d',
|
||||
400: '#f31260',
|
||||
500: '#f54180',
|
||||
600: '#f871a0',
|
||||
700: '#faa0bf',
|
||||
800: '#fdd0df',
|
||||
900: '#fee7ef'
|
||||
},
|
||||
danger: {
|
||||
DEFAULT: '#DB3694',
|
||||
foreground: '#fff',
|
||||
50: '#690A66',
|
||||
100: '#7F1170',
|
||||
200: '#9D1B7F',
|
||||
300: '#BC278B',
|
||||
400: '#DB3694',
|
||||
500: '#E965A3',
|
||||
600: '#F485AE',
|
||||
700: '#FBAFC4',
|
||||
800: '#FDD7DD',
|
||||
900: '#FEEAF6'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.5.4",
|
||||
"version": "4.5.17",
|
||||
"scripts": {
|
||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||
|
@@ -66,7 +66,7 @@ export abstract class ConfigBase<T> {
|
||||
|
||||
private handleError(e: unknown, message: string): void {
|
||||
if (e instanceof SyntaxError) {
|
||||
this.core.context.logger.logError(`[Core] [Config] 操作配置文件格式错误,请检查配置文件:`, e.message);
|
||||
this.core.context.logger.logError('[Core] [Config] 操作配置文件格式错误,请检查配置文件:', e.message);
|
||||
} else {
|
||||
this.core.context.logger.logError(`[Core] [Config] ${message}:`, (e as Error).message);
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.5.4';
|
||||
export const napCatVersion = '4.5.17';
|
||||
|
@@ -43,7 +43,7 @@ export class NTQQFileApi {
|
||||
this.rkeyManager = new RkeyManager([
|
||||
'https://rkey.napneko.icu/rkeys'
|
||||
],
|
||||
this.context.logger
|
||||
this.context.logger
|
||||
);
|
||||
}
|
||||
|
||||
@@ -300,18 +300,18 @@ export class NTQQFileApi {
|
||||
element.elementType === ElementType.FILE
|
||||
) {
|
||||
switch (element.elementType) {
|
||||
case ElementType.PIC:
|
||||
case ElementType.PIC:
|
||||
element.picElement!.sourcePath = elementResults?.[elementIndex] ?? '';
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
element.videoElement!.filePath = elementResults?.[elementIndex] ?? '';
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
element.pttElement!.filePath = elementResults?.[elementIndex] ?? '';
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
element.fileElement!.filePath = elementResults?.[elementIndex] ?? '';
|
||||
break;
|
||||
break;
|
||||
}
|
||||
elementIndex++;
|
||||
}
|
||||
@@ -434,9 +434,9 @@ export class NTQQFileApi {
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.core.apis.PacketApi.available && this.packetRkey?.[0] && this.packetRkey?.[1]) {
|
||||
const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
|
||||
const rkey_expired_group = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
|
||||
if (this.core.apis.PacketApi.available) {
|
||||
const rkey_expired_private = !this.packetRkey || (this.packetRkey[0] && this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000);
|
||||
const rkey_expired_group = !this.packetRkey || (this.packetRkey[0] && this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000);
|
||||
if (rkey_expired_private || rkey_expired_group) {
|
||||
this.packetRkey = await this.fetchRkeyWithRetry();
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ export const NapcatConfigSchema = Type.Object({
|
||||
fileLogLevel: Type.String({ default: 'debug' }),
|
||||
consoleLogLevel: Type.String({ default: 'info' }),
|
||||
packetBackend: Type.String({ default: 'auto' }),
|
||||
packetServer: Type.String({ default: '' })
|
||||
packetServer: Type.String({ default: '' }),
|
||||
});
|
||||
|
||||
export type NapcatConfig = Static<typeof NapcatConfigSchema>;
|
||||
|
@@ -11,7 +11,7 @@ export class NodeIKernelSessionListener {
|
||||
|
||||
}
|
||||
|
||||
onOpentelemetryInit(args: unknown): any {
|
||||
onOpentelemetryInit(info: { is_init: boolean, is_report: boolean }): any {
|
||||
|
||||
}
|
||||
|
||||
|
@@ -1,20 +1,22 @@
|
||||
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 { PacketMsg } from '@/core/packet/message/message';
|
||||
import {PacketMsg} from '@/core/packet/message/message';
|
||||
import {
|
||||
PacketMsgFileElement,
|
||||
PacketMsgPicElement,
|
||||
PacketMsgPttElement,
|
||||
PacketMsgVideoElement
|
||||
} from '@/core/packet/message/element';
|
||||
import { ChatType } from '@/core';
|
||||
import { MiniAppRawData, MiniAppReqParams } from '@/core/packet/entities/miniApp';
|
||||
import { AIVoiceChatType } from '@/core/packet/entities/aiChat';
|
||||
import { NapProtoDecodeStructType, NapProtoEncodeStructType } from '@napneko/nap-proto-core';
|
||||
import { IndexNode, MsgInfo } from '@/core/packet/transformer/proto';
|
||||
import { OidbPacket } from '@/core/packet/transformer/base';
|
||||
import { ImageOcrResult } from '@/core/packet/entities/ocrResult';
|
||||
import {ChatType, MsgSourceType, NTMsgType, RawMessage} from '@/core';
|
||||
import {MiniAppRawData, MiniAppReqParams} from '@/core/packet/entities/miniApp';
|
||||
import {AIVoiceChatType} from '@/core/packet/entities/aiChat';
|
||||
import {NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg} from '@napneko/nap-proto-core';
|
||||
import {IndexNode, LongMsgResult, MsgInfo} from '@/core/packet/transformer/proto';
|
||||
import {OidbPacket} from '@/core/packet/transformer/base';
|
||||
import {ImageOcrResult} from '@/core/packet/entities/ocrResult';
|
||||
import {gunzipSync} from 'zlib';
|
||||
import {PacketMsgConverter} from '@/core/packet/message/converter';
|
||||
|
||||
export class PacketOperationContext {
|
||||
private readonly context: PacketContext;
|
||||
@@ -57,10 +59,10 @@ export class PacketOperationContext {
|
||||
const res = trans.GetStrangerInfo.parse(resp);
|
||||
const extBigInt = BigInt(res.data.status.value);
|
||||
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));
|
||||
return { status: 10, ext_status: status };
|
||||
return {status: 10, ext_status: status};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
@@ -77,13 +79,13 @@ export class PacketOperationContext {
|
||||
const reqList = msg.flatMap(m =>
|
||||
m.msg.map(e => {
|
||||
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) {
|
||||
return this.context.highway.uploadVideo({ chatType, peerUid }, e);
|
||||
return this.context.highway.uploadVideo({chatType, peerUid}, e);
|
||||
} 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) {
|
||||
return this.context.highway.uploadFile({ chatType, peerUid }, e);
|
||||
return this.context.highway.uploadFile({chatType, peerUid}, e);
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean)
|
||||
@@ -116,6 +118,13 @@ export class PacketOperationContext {
|
||||
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) {
|
||||
const req = trans.ImageOCR.build(imgUrl);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
@@ -195,4 +204,74 @@ export class PacketOperationContext {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
Peer,
|
||||
ChatType,
|
||||
ElementType,
|
||||
MessageElement,
|
||||
Peer,
|
||||
RawMessage,
|
||||
SendArkElement,
|
||||
SendFaceElement,
|
||||
@@ -31,7 +31,9 @@ import {
|
||||
PacketMsgVideoElement,
|
||||
PacketMultiMsgElement
|
||||
} 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 = [
|
||||
ElementType.TEXT,
|
||||
@@ -154,4 +156,16 @@ export class PacketMsgConverter {
|
||||
}).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);
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,22 @@
|
||||
import * as zlib from 'node:zlib';
|
||||
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import {NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg} from '@napneko/nap-proto-core';
|
||||
import {
|
||||
CustomFace,
|
||||
Elem,
|
||||
FileExtra,
|
||||
GroupFileExtra,
|
||||
MarkdownData,
|
||||
MentionExtra,
|
||||
MsgInfo,
|
||||
NotOnlineImage,
|
||||
OidbSvcTrpcTcp0XE37_800Response,
|
||||
QBigFaceExtra,
|
||||
QSmallFaceExtra,
|
||||
MsgInfo,
|
||||
OidbSvcTrpcTcp0XE37_800Response,
|
||||
FileExtra,
|
||||
GroupFileExtra
|
||||
} from '@/core/packet/transformer/proto';
|
||||
import {
|
||||
ElementType,
|
||||
FaceType,
|
||||
MessageElement,
|
||||
NTMsgAtType,
|
||||
PicType,
|
||||
SendArkElement,
|
||||
@@ -29,8 +31,11 @@ import {
|
||||
SendTextElement,
|
||||
SendVideoElement
|
||||
} from '@/core';
|
||||
import { ForwardMsgBuilder } from '@/common/forward-msg-builder';
|
||||
import { PacketMsg, PacketSendMsgElement } from '@/core/packet/message/message';
|
||||
import {ForwardMsgBuilder} from '@/common/forward-msg-builder';
|
||||
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
|
||||
// TODO: SendStructLongMsgElement
|
||||
@@ -51,6 +56,8 @@ export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
|
||||
return [];
|
||||
}
|
||||
|
||||
static parseElement: ParseElementFn;
|
||||
|
||||
toPreview(): string {
|
||||
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 {
|
||||
return this.text;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class PacketMsgAtElement extends PacketMsgTextElement {
|
||||
targetUid: string;
|
||||
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> {
|
||||
@@ -137,21 +179,28 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
pbReserve: {
|
||||
messageId: this.messageId,
|
||||
},
|
||||
toUin: BigInt(0),
|
||||
toUin: BigInt(this.targetUin),
|
||||
type: 1,
|
||||
}
|
||||
}, {
|
||||
text: this.isGroupReply ? {
|
||||
str: 'nya~',
|
||||
pbReserve: new NapProtoMsg(MentionExtra).encode({
|
||||
type: this.targetUin === 0 ? 1 : 2,
|
||||
uin: 0,
|
||||
field5: 0,
|
||||
uid: String(this.targetUid),
|
||||
}),
|
||||
} : undefined,
|
||||
}];
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return this.summary;
|
||||
}
|
||||
|
50
src/core/packet/transformer/highway/DownloadGroupImage.ts
Normal file
50
src/core/packet/transformer/highway/DownloadGroupImage.ts
Normal 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();
|
@@ -14,7 +14,7 @@ class DownloadImage extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 1,
|
||||
command: 100
|
||||
command: 200
|
||||
},
|
||||
scene: {
|
||||
requestType: 2,
|
||||
|
@@ -12,3 +12,4 @@ export { default as UploadPrivateImage } from './UploadPrivateImage';
|
||||
export { default as UploadPrivatePtt } from './UploadPrivatePtt';
|
||||
export { default as UploadPrivateVideo } from './UploadPrivateVideo';
|
||||
export { default as DownloadImage } from './DownloadImage';
|
||||
export { default as DownloadGroupImage } from './DownloadGroupImage';
|
||||
|
37
src/core/packet/transformer/message/DownloadForwardMsg.ts
Normal file
37
src/core/packet/transformer/message/DownloadForwardMsg.ts
Normal 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();
|
@@ -13,12 +13,12 @@ class UploadForwardMsg extends PacketTransformer<typeof proto.SendLongMsgResp> {
|
||||
const msgBody = this.msgBuilder.buildFakeMsg(selfUid, msg);
|
||||
const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode(
|
||||
{
|
||||
action: {
|
||||
action: [{
|
||||
actionCommand: 'MultiMsg',
|
||||
actionData: {
|
||||
msgBody: msgBody
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
);
|
||||
const payload = zlib.gzipSync(Buffer.from(longMsgResultData));
|
||||
|
@@ -1 +1,2 @@
|
||||
export { default as UploadForwardMsg } from './UploadForwardMsg';
|
||||
export { default as DownloadForwardMsg } from './DownloadForwardMsg';
|
@@ -2,7 +2,7 @@ import { ProtoField, ScalarType } from '@napneko/nap-proto-core';
|
||||
import { PushMsgBody } from '@/core/packet/transformer/proto';
|
||||
|
||||
export const LongMsgResult = {
|
||||
action: ProtoField(2, () => LongMsgAction)
|
||||
action: ProtoField(2, () => LongMsgAction, false, true)
|
||||
};
|
||||
|
||||
export const LongMsgAction = {
|
||||
|
@@ -1,5 +1,9 @@
|
||||
import { GeneralCallResult } from './common';
|
||||
|
||||
enum ProxyType {
|
||||
CLOSE = 0,
|
||||
HTTP = 1,
|
||||
SOCKET = 2
|
||||
}
|
||||
export interface NodeIKernelMSFService {
|
||||
getServerTime(): string;
|
||||
setNetworkProxy(param: {
|
||||
@@ -7,10 +11,19 @@ export interface NodeIKernelMSFService {
|
||||
userPwd: string,
|
||||
address: string,
|
||||
port: number,
|
||||
proxyType: number,
|
||||
proxyType: ProxyType,
|
||||
domain: string,
|
||||
isSocket: boolean
|
||||
}): Promise<GeneralCallResult>;
|
||||
getNetworkProxy(): Promise<{
|
||||
userName: string,
|
||||
userPwd: string,
|
||||
address: string,
|
||||
port: number,
|
||||
proxyType: ProxyType,
|
||||
domain: string,
|
||||
isSocket: boolean
|
||||
}>;
|
||||
//http
|
||||
// userName: '',
|
||||
// userPwd: '',
|
||||
|
@@ -15,7 +15,6 @@ let napCatInitialized = false; // 添加一个标志
|
||||
function createServiceProxy(ServiceName) {
|
||||
return new Proxy(() => { }, {
|
||||
get: (target, FunctionName) => {
|
||||
console.log(ServiceName, FunctionName);
|
||||
if (ServiceName === 'NodeIQQNTWrapperSession' && FunctionName === 'create') {
|
||||
return () => new Proxy({}, {
|
||||
get: function (target, ClassFunName, receiver) {
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,7 @@ import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward,
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { MessageUnique } from '@/common/message-unique';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ChatType, ElementType, MsgSourceType, NTMsgType, RawMessage } from '@/core';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
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');
|
||||
}
|
||||
|
||||
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 rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId);
|
||||
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]);
|
||||
|
||||
if (!data || data.result !== 0) {
|
||||
throw new Error('找不到相关的聊天记录' + data?.errMsg);
|
||||
return await protocolFallbackLogic(msgId.toString());
|
||||
}
|
||||
|
||||
const singleMsg = data.msgList[0];
|
||||
if (!singleMsg) {
|
||||
throw new Error('找不到相关的聊天记录');
|
||||
return await protocolFallbackLogic(msgId.toString());
|
||||
}
|
||||
const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;//强制array 以便处理
|
||||
if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) {
|
||||
throw new Error('找不到相关的聊天记录');
|
||||
return await protocolFallbackLogic(msgId.toString());
|
||||
}
|
||||
return {
|
||||
messages: (resMsg?.message?.[0] as OB11MessageForward)?.data?.content
|
||||
|
@@ -1,25 +1,34 @@
|
||||
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
|
||||
import { MessageUnique } from '@/common/message-unique';
|
||||
import {
|
||||
NTMsgAtType,
|
||||
ChatType,
|
||||
CustomMusicSignPostData,
|
||||
ElementType,
|
||||
FaceIndex,
|
||||
FaceType,
|
||||
GrayTipElement,
|
||||
GroupNotify,
|
||||
IdMusicSignPostData,
|
||||
MessageElement,
|
||||
NapCatCore,
|
||||
NTGrayTipElementSubTypeV2,
|
||||
NTMsgAtType,
|
||||
Peer,
|
||||
RawMessage,
|
||||
SendMessageElement,
|
||||
SendTextElement,
|
||||
FaceType,
|
||||
GrayTipElement,
|
||||
GroupNotify,
|
||||
} from '@/core';
|
||||
import faceConfig from '@/core/external/face_config.json';
|
||||
import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, OB11MessageForward, OB11MessageImage, OB11MessageVideo, } from '@/onebot';
|
||||
import {
|
||||
NapCatOneBot11Adapter,
|
||||
OB11Message,
|
||||
OB11MessageData,
|
||||
OB11MessageDataType,
|
||||
OB11MessageFileBase,
|
||||
OB11MessageForward,
|
||||
OB11MessageImage,
|
||||
OB11MessageVideo,
|
||||
} from '@/onebot';
|
||||
import { OB11Construct } from '@/onebot/helper/data';
|
||||
import { EventType } from '@/onebot/event/OneBotEvent';
|
||||
import { encodeCQCode } from '@/onebot/helper/cqcode';
|
||||
@@ -30,7 +39,7 @@ import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNot
|
||||
import { ForwardMsgBuilder } from '@/common/forward-msg-builder';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
|
||||
import { OB11GroupDecreaseEvent, GroupDecreaseSubType } from '../event/notice/OB11GroupDecreaseEvent';
|
||||
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';
|
||||
@@ -264,7 +273,6 @@ export class OneBotMsgApi {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 丢弃该消息段
|
||||
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
|
||||
this.core.context.logger.logError(
|
||||
@@ -355,15 +363,22 @@ export class OneBotMsgApi {
|
||||
};
|
||||
},
|
||||
|
||||
multiForwardMsgElement: async (_, msg, _wrapper, context) => {
|
||||
multiForwardMsgElement: async (element, msg, _wrapper, context) => {
|
||||
const parentMsgPeer = msg.parentMsgPeer ?? {
|
||||
chatType: msg.chatType,
|
||||
guildId: '',
|
||||
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 = {
|
||||
type: OB11MessageDataType.forward,
|
||||
data: { id: msg.msgId }
|
||||
@@ -692,7 +707,10 @@ export class OneBotMsgApi {
|
||||
if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
|
||||
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
|
||||
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 !== '') {
|
||||
return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
|
||||
}
|
||||
@@ -919,16 +937,16 @@ export class OneBotMsgApi {
|
||||
const calculateTotalSize = async (elements: SendMessageElement[]): Promise<number> => {
|
||||
const sizePromises = elements.map(async element => {
|
||||
switch (element.elementType) {
|
||||
case ElementType.PTT:
|
||||
return (await fsPromise.stat(element.pttElement.filePath)).size;
|
||||
case ElementType.FILE:
|
||||
return (await fsPromise.stat(element.fileElement.filePath)).size;
|
||||
case ElementType.VIDEO:
|
||||
return (await fsPromise.stat(element.videoElement.filePath)).size;
|
||||
case ElementType.PIC:
|
||||
return (await fsPromise.stat(element.picElement.sourcePath)).size;
|
||||
default:
|
||||
return 0;
|
||||
case ElementType.PTT:
|
||||
return (await fsPromise.stat(element.pttElement.filePath)).size;
|
||||
case ElementType.FILE:
|
||||
return (await fsPromise.stat(element.fileElement.filePath)).size;
|
||||
case ElementType.VIDEO:
|
||||
return (await fsPromise.stat(element.videoElement.filePath)).size;
|
||||
case ElementType.PIC:
|
||||
return (await fsPromise.stat(element.picElement.sourcePath)).size;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
const sizes = await Promise.all(sizePromises);
|
||||
@@ -998,6 +1016,7 @@ export class OneBotMsgApi {
|
||||
return { path, fileName: inputdata.name ?? fileName };
|
||||
}
|
||||
}
|
||||
|
||||
async handleObfuckName(name: string) {
|
||||
const contextMsgFile = FileNapCatOneBotUUID.decode(name);
|
||||
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
|
||||
@@ -1021,16 +1040,17 @@ export class OneBotMsgApi {
|
||||
}
|
||||
throw new Error('文件名解析失败');
|
||||
}
|
||||
|
||||
groupChangDecreseType2String(type: number): GroupDecreaseSubType {
|
||||
switch (type) {
|
||||
case 130:
|
||||
return 'leave';
|
||||
case 131:
|
||||
return 'kick';
|
||||
case 3:
|
||||
return 'kick_me';
|
||||
default:
|
||||
return 'kick';
|
||||
case 130:
|
||||
return 'leave';
|
||||
case 131:
|
||||
return 'kick';
|
||||
case 3:
|
||||
return 'kick_me';
|
||||
default:
|
||||
return 'kick';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1049,7 +1069,7 @@ export class OneBotMsgApi {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, 1, 1000).catch(undefined);
|
||||
}, 1, 1000).catch(() => undefined);
|
||||
if (dataNotify) {
|
||||
return !dataNotify.actionUser.uid ? dataNotify.user2.uid : dataNotify.actionUser.uid;
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ConfigBase } from '@/common/config-base';
|
||||
import { NapCatCore } from '@/core';
|
||||
import type { NapCatCore } from '@/core';
|
||||
import { OneBotConfig } from './config';
|
||||
import { AnySchema } from 'ajv';
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
|
||||
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
|
||||
import express, { Express, NextFunction, Request, Response } from 'express';
|
||||
import http from 'http';
|
||||
import { NapCatCore } from '@/core';
|
||||
@@ -60,7 +60,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
req.body = json5.parse(rawData || '{}');
|
||||
req.body = { ...json5.parse(rawData || '{}'), ...req.body };
|
||||
next();
|
||||
} catch {
|
||||
return res.status(400).send('Invalid JSON');
|
||||
|
@@ -26,9 +26,4 @@ export function require_dlopen(modulename: string) {
|
||||
process.dlopen(module, path.join(import__dirname, modulename));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-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);
|
||||
}
|
@@ -13,12 +13,13 @@ import { IProcessEnv, IPtyForkOptions, IPtyOpenOptions } from '@homebridge/node-
|
||||
import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/types';
|
||||
import { assign } from '@homebridge/node-pty-prebuilt-multiarch/src/utils';
|
||||
import { pty_loader } from './prebuild-loader';
|
||||
import { fileURLToPath } from 'url';
|
||||
export const pty = pty_loader();
|
||||
|
||||
let helperPath: string;
|
||||
helperPath = '../build/Release/spawn-helper';
|
||||
|
||||
helperPath = path.resolve(__dirname, helperPath);
|
||||
const import__dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
helperPath = path.resolve(import__dirname, helperPath);
|
||||
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
|
||||
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
|
||||
|
||||
|
@@ -14,6 +14,8 @@ import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/t
|
||||
import { fork } from 'child_process';
|
||||
import { ConoutConnection } from './windowsConoutConnection';
|
||||
import { require_dlopen } from '.';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
let conptyNative: IConptyNative;
|
||||
let winptyNative: IWinptyNative;
|
||||
@@ -149,7 +151,7 @@ export class WindowsPtyAgent {
|
||||
consoleProcessList.forEach((pid: number) => {
|
||||
try {
|
||||
process.kill(pid);
|
||||
} catch{
|
||||
} catch {
|
||||
// Ignore if process cannot be found (kill ESRCH error)
|
||||
}
|
||||
});
|
||||
@@ -176,8 +178,9 @@ export class WindowsPtyAgent {
|
||||
}
|
||||
|
||||
private _getConsoleProcessList(): Promise<number[]> {
|
||||
const import__dirname = dirname(fileURLToPath(import.meta.url));
|
||||
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 => {
|
||||
clearTimeout(timeout);
|
||||
// @ts-expect-error no need to check if it is null
|
||||
|
@@ -223,7 +223,7 @@ async function handleLogin(
|
||||
logger.log(`可用于快速登录的 QQ:\n${historyLoginList
|
||||
.map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`)
|
||||
.join('\n')
|
||||
}`);
|
||||
}`);
|
||||
}
|
||||
loginService.getQRCodePicture();
|
||||
}
|
||||
@@ -236,11 +236,11 @@ async function initializeSession(
|
||||
) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const sessionListener = new NodeIKernelSessionListener();
|
||||
sessionListener.onSessionInitComplete = (r: unknown) => {
|
||||
if (r === 0) {
|
||||
sessionListener.onOpentelemetryInit = (info) => {
|
||||
if (info.is_init) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('登录异常' + r?.toString()));
|
||||
reject(new Error('opentelemetry init failed'));
|
||||
}
|
||||
};
|
||||
session.init(
|
||||
@@ -260,7 +260,30 @@ async function initializeSession(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleProxy(session: NodeIQQNTWrapperSession, logger: LogWrapper) {
|
||||
if (process.env['NAPCAT_PROXY_PORT']) {
|
||||
session.getMSFService().setNetworkProxy({
|
||||
userName: '',
|
||||
userPwd: '',
|
||||
address: process.env['NAPCAT_PROXY_ADDRESS'] || '127.0.0.1',
|
||||
port: +process.env['NAPCAT_PROXY_PORT'],
|
||||
proxyType: 2,
|
||||
domain: '',
|
||||
isSocket: true
|
||||
});
|
||||
logger.logWarn('已设置代理', process.env['NAPCAT_PROXY_ADDRESS'], process.env['NAPCAT_PROXY_PORT']);
|
||||
} else if (process.env['NAPCAT_PROXY_CLOSE']) {
|
||||
session.getMSFService().setNetworkProxy({
|
||||
userName: '',
|
||||
userPwd: '',
|
||||
address: '',
|
||||
port: 0,
|
||||
proxyType: 0,
|
||||
domain: '',
|
||||
isSocket: false
|
||||
});
|
||||
}
|
||||
}
|
||||
export async function NCoreInitShell() {
|
||||
console.log('NapCat Shell App Loading...');
|
||||
const pathWrapper = new NapCatPathWrapper();
|
||||
@@ -286,7 +309,7 @@ export async function NCoreInitShell() {
|
||||
|
||||
await initializeEngine(engine, basicInfoWrapper, dataPathGlobal, systemPlatform, systemVersion);
|
||||
await initializeLoginService(loginService, basicInfoWrapper, dataPathGlobal, systemVersion, hostname);
|
||||
|
||||
handleProxy(session, logger);
|
||||
program.option('-q, --qq [number]', 'QQ号').parse(process.argv);
|
||||
const cmdOptions = program.opts();
|
||||
const quickLoginUin = cmdOptions['qq'];
|
||||
@@ -294,6 +317,7 @@ export async function NCoreInitShell() {
|
||||
|
||||
const dataTimestape = new Date().getTime().toString();
|
||||
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
|
||||
|
||||
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
|
||||
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
|
||||
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));
|
||||
@@ -314,7 +338,15 @@ export async function NCoreInitShell() {
|
||||
await initializeSession(session, sessionConfig);
|
||||
|
||||
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);
|
||||
|
||||
await new NapCatShell(
|
||||
|
@@ -10,9 +10,10 @@ import { WebUiConfigWrapper } from '@webapi/helper/config';
|
||||
import { ALLRouter } from '@webapi/router';
|
||||
import { cors } from '@webapi/middleware/cors';
|
||||
import { createUrl } from '@webapi/utils/url';
|
||||
import { sendSuccess } from '@webapi/utils/response';
|
||||
import { sendError } from '@webapi/utils/response';
|
||||
import { join } from 'node:path';
|
||||
import { terminalManager } from '@webapi/terminal/terminal_manager';
|
||||
import multer from 'multer'; // 新增:引入multer用于错误捕获
|
||||
|
||||
// 实例化Express
|
||||
const app = express();
|
||||
@@ -25,16 +26,43 @@ const server = createServer(app);
|
||||
*/
|
||||
export let WebUiConfig: WebUiConfigWrapper;
|
||||
export let webUiPathWrapper: NapCatPathWrapper;
|
||||
const MAX_PORT_TRY = 100;
|
||||
import * as net from 'node:net';
|
||||
import { WebUiDataRuntime } from './src/helper/Data';
|
||||
|
||||
export async function InitPort(parsedConfig: WebUiConfigType): Promise<[string, number, string]> {
|
||||
try {
|
||||
await tryUseHost(parsedConfig.host);
|
||||
const port = await tryUsePort(parsedConfig.port, parsedConfig.host);
|
||||
return [parsedConfig.host, port, parsedConfig.token];
|
||||
} catch (error) {
|
||||
console.log('host或port不可用', error);
|
||||
return ['', 0, ''];
|
||||
}
|
||||
}
|
||||
|
||||
export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapper) {
|
||||
webUiPathWrapper = pathWrapper;
|
||||
WebUiConfig = new WebUiConfigWrapper();
|
||||
const config = await WebUiConfig.GetWebUIConfig();
|
||||
if (config.port == 0) {
|
||||
const [host, port, token] = await InitPort(await WebUiConfig.GetWebUIConfig());
|
||||
if (port == 0) {
|
||||
logger.log('[NapCat] [WebUi] Current WebUi is not run.');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
let autoLoginAccount = process.env['NAPCAT_QUICK_ACCOUNT'] || WebUiConfig.getAutoLoginAccount();
|
||||
if (autoLoginAccount) {
|
||||
try {
|
||||
const { result, message } = await WebUiDataRuntime.requestQuickLogin(autoLoginAccount);
|
||||
if (!result) {
|
||||
throw new Error(message);
|
||||
}
|
||||
console.log(`[NapCat] [WebUi] Auto login account: ${autoLoginAccount}`);
|
||||
} catch (error) {
|
||||
console.log(`[NapCat] [WebUi] Auto login account failed.` + error);
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
// ------------注册中间件------------
|
||||
// 使用express的json中间件
|
||||
app.use(express.json());
|
||||
@@ -42,10 +70,21 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
||||
// CORS中间件
|
||||
// TODO:
|
||||
app.use(cors);
|
||||
|
||||
// 如果是webui字体文件,挂载字体文件
|
||||
app.use('/webui/fonts/AaCute.woff', async (_req, res, next) => {
|
||||
const isFontExist = await WebUiConfigWrapper.CheckWebUIFontExist();
|
||||
if (isFontExist) {
|
||||
res.sendFile(WebUiConfigWrapper.GetWebUIFontPath());
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// ------------中间件结束------------
|
||||
|
||||
// ------------挂载路由------------
|
||||
// 挂载静态路由(前端),路径为 [/前缀]/webui
|
||||
// 挂载静态路由(前端),路径为 /webui
|
||||
app.use('/webui', express.static(pathWrapper.staticPath));
|
||||
// 初始化WebSocket服务器
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
@@ -62,21 +101,91 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
||||
|
||||
// 初始服务(先放个首页)
|
||||
app.all('/', (_req, res) => {
|
||||
sendSuccess(res, null, 'NapCat WebAPI is now running!');
|
||||
res.status(301).header('Location', '/webui').send();
|
||||
});
|
||||
|
||||
// 错误处理中间件,捕获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(port, host, async () => {
|
||||
// 启动后打印出相关地址
|
||||
const port = config.port.toString(),
|
||||
searchParams = { token: config.token };
|
||||
if (config.host !== '' && config.host !== '0.0.0.0') {
|
||||
let searchParams = { token: token };
|
||||
if (host !== '' && host !== '0.0.0.0') {
|
||||
logger.log(
|
||||
`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(config.host, port, '/webui', searchParams)}`
|
||||
`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(host, port.toString(), '/webui', searchParams)}`
|
||||
);
|
||||
}
|
||||
logger.log(`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', port, '/webui', searchParams)}`);
|
||||
logger.log(
|
||||
`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', port.toString(), '/webui', searchParams)}`
|
||||
);
|
||||
});
|
||||
// ------------Over!------------
|
||||
}
|
||||
|
||||
async function tryUseHost(host: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const server = net.createServer();
|
||||
server.on('listening', () => {
|
||||
server.close();
|
||||
resolve(host);
|
||||
});
|
||||
|
||||
server.on('error', (err: any) => {
|
||||
if (err.code === 'EADDRNOTAVAIL') {
|
||||
reject(new Error('主机地址验证失败,可能为非本机地址'));
|
||||
} else {
|
||||
reject(new Error(`遇到错误: ${err.code}`));
|
||||
}
|
||||
});
|
||||
|
||||
// 尝试监听 让系统随机分配一个端口
|
||||
server.listen(0, host);
|
||||
} catch (error) {
|
||||
// 这里捕获到的错误应该是启动服务器时的同步错误
|
||||
reject(new Error(`服务器启动时发生错误: ${error}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function tryUsePort(port: number, host: string, tryCount: number = 0): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const server = net.createServer();
|
||||
server.on('listening', () => {
|
||||
server.close();
|
||||
resolve(port);
|
||||
});
|
||||
|
||||
server.on('error', (err: any) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
if (tryCount < MAX_PORT_TRY) {
|
||||
// 使用循环代替递归
|
||||
resolve(tryUsePort(port + 1, host, tryCount + 1));
|
||||
} else {
|
||||
reject(new Error(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`));
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`遇到错误: ${err.code}`));
|
||||
}
|
||||
});
|
||||
|
||||
// 尝试监听端口
|
||||
server.listen(port, host);
|
||||
} catch (error) {
|
||||
// 这里捕获到的错误应该是启动服务器时的同步错误
|
||||
reject(new Error(`服务器启动时发生错误: ${error}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -7,6 +7,15 @@ import { WebUiDataRuntime } from '@webapi/helper/Data';
|
||||
import { sendSuccess, sendError } from '@webapi/utils/response';
|
||||
import { isEmpty } from '@webapi/utils/check';
|
||||
|
||||
// 检查是否使用默认Token
|
||||
export const CheckDefaultTokenHandler: RequestHandler = async (_, res) => {
|
||||
const webuiToken = await WebUiConfig.GetWebUIConfig();
|
||||
if (webuiToken.token === 'napcat') {
|
||||
return sendSuccess(res, true);
|
||||
}
|
||||
return sendSuccess(res, false);
|
||||
};
|
||||
|
||||
// 登录
|
||||
export const LoginHandler: RequestHandler = async (req, res) => {
|
||||
// 获取WebUI配置
|
||||
@@ -93,7 +102,7 @@ export const UpdateTokenHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
// 注销当前的Token
|
||||
if (authorization) {
|
||||
const CredentialBase64: string = authorization.split(' ')[1];
|
||||
const CredentialBase64: string = authorization.split(' ')[1] as string;
|
||||
const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString());
|
||||
AuthHelper.revokeCredential(Credential);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { RequestHandler, Request } from 'express';
|
||||
import type { RequestHandler } from 'express';
|
||||
import { sendError, sendSuccess } from '../utils/response';
|
||||
import fsProm from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
@@ -7,7 +7,9 @@ import os from 'os';
|
||||
import compressing from 'compressing';
|
||||
import { PassThrough } from 'stream';
|
||||
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';
|
||||
|
||||
@@ -268,11 +270,11 @@ export const BatchMoveHandler: RequestHandler = async (req, res) => {
|
||||
// 新增:文件下载处理方法(注意流式传输,不将整个文件读入内存)
|
||||
export const DownloadHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const filePath = normalizePath( req.query[ 'path' ] as string );
|
||||
const filePath = normalizePath(req.query['path'] as string);
|
||||
if (!filePath) {
|
||||
return sendError( res, '参数错误' );
|
||||
return sendError(res, '参数错误');
|
||||
}
|
||||
|
||||
|
||||
const stat = await fsProm.stat(filePath);
|
||||
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
@@ -327,74 +329,71 @@ export const BatchDownloadHandler: RequestHandler = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 修改:使用 Buffer 转码文件名,解决文件上传时乱码问题
|
||||
const decodeFileName = (fileName: string): string => {
|
||||
// 修改上传处理方法
|
||||
export const UploadHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
return Buffer.from(fileName, 'binary').toString('utf8');
|
||||
} catch {
|
||||
return fileName;
|
||||
await diskUploader(req, res);
|
||||
return sendSuccess(res, true, '文件上传成功', true);
|
||||
} 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);
|
||||
}
|
||||
};
|
||||
|
||||
// 修改上传处理方法
|
||||
export const UploadHandler: RequestHandler = (req, res) => {
|
||||
const uploadPath = (req.query['path'] || '') as string;
|
||||
// 上传WebUI字体文件处理方法
|
||||
export const UploadWebUIFontHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
await webUIFontUploader(req, res);
|
||||
return sendSuccess(res, true, '字体文件上传成功', true);
|
||||
} catch (error) {
|
||||
let errorMessage = '字体文件上传失败';
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (
|
||||
_: Request,
|
||||
file: Express.Multer.File,
|
||||
cb: (error: Error | null, destination: string) => void
|
||||
) => {
|
||||
try {
|
||||
const decodedName = decodeFileName(file.originalname);
|
||||
|
||||
if (!uploadPath) {
|
||||
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, '');
|
||||
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}`;
|
||||
}
|
||||
},
|
||||
filename: (_: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => {
|
||||
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 || '文件上传失败');
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.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, '删除字体文件失败');
|
||||
}
|
||||
};
|
||||
|
@@ -3,7 +3,8 @@ import { sendError, sendSuccess } from '../utils/response';
|
||||
import { WebUiConfigWrapper } from '../helper/config';
|
||||
import { logSubscription } from '@/common/log';
|
||||
import { terminalManager } from '../terminal/terminal_manager';
|
||||
|
||||
// 判断是否是 macos
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
// 日志记录
|
||||
export const LogHandler: RequestHandler = async (req, res) => {
|
||||
const filename = req.query['id'];
|
||||
@@ -43,6 +44,9 @@ export const LogRealTimeHandler: RequestHandler = async (req, res) => {
|
||||
|
||||
// 终端相关处理器
|
||||
export const CreateTerminalHandler: RequestHandler = async (req, res) => {
|
||||
if (isMacOS) {
|
||||
return sendError(res, 'MacOS不支持终端');
|
||||
}
|
||||
try {
|
||||
const { cols, rows } = req.body;
|
||||
const { id } = terminalManager.createTerminal(cols, rows);
|
||||
|
@@ -1,13 +1,12 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { OneBotConfig } from '@/onebot/config/config';
|
||||
|
||||
import { loadConfig, OneBotConfig } from '@/onebot/config/config';
|
||||
import { webUiPathWrapper } from '@/webui';
|
||||
import { WebUiDataRuntime } from '@webapi/helper/Data';
|
||||
import { sendError, sendSuccess } from '@webapi/utils/response';
|
||||
import { isEmpty } from '@webapi/utils/check';
|
||||
import json5 from 'json5';
|
||||
|
||||
// 获取OneBot11配置
|
||||
export const OB11GetConfigHandler: RequestHandler = (_, res) => {
|
||||
@@ -19,16 +18,16 @@ export const OB11GetConfigHandler: RequestHandler = (_, res) => {
|
||||
}
|
||||
// 获取登录的QQ号
|
||||
const uin = WebUiDataRuntime.getQQLoginUin();
|
||||
// 读取配置文件
|
||||
// 读取配置文件路径
|
||||
const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`);
|
||||
// 尝试解析配置文件
|
||||
try {
|
||||
// 读取配置文件
|
||||
const data = JSON.parse(
|
||||
existsSync(configFilePath)
|
||||
? readFileSync(configFilePath).toString()
|
||||
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString()
|
||||
) as OneBotConfig;
|
||||
// 读取配置文件内容
|
||||
const configFileContent = existsSync(configFilePath)
|
||||
? readFileSync(configFilePath).toString()
|
||||
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString();
|
||||
// 解析配置文件并加载配置
|
||||
const data = loadConfig(json5.parse(configFileContent)) as OneBotConfig;
|
||||
// 返回配置文件
|
||||
return sendSuccess(res, data);
|
||||
} catch (e) {
|
||||
@@ -50,9 +49,12 @@ export const OB11SetConfigHandler: RequestHandler = async (req, res) => {
|
||||
}
|
||||
// 写入配置
|
||||
try {
|
||||
await WebUiDataRuntime.setOB11Config(JSON.parse(req.body.config));
|
||||
// 解析并加载配置
|
||||
const config = loadConfig(json5.parse(req.body.config)) as OneBotConfig;
|
||||
// 写入配置
|
||||
await WebUiDataRuntime.setOB11Config(config);
|
||||
return sendSuccess(res, null);
|
||||
} catch (e) {
|
||||
return sendError(res, 'Error: ' + e);
|
||||
}
|
||||
};
|
||||
};
|
@@ -7,8 +7,8 @@ export const GetProxyHandler: RequestHandler = async (req, res) => {
|
||||
if (url && typeof url === 'string') {
|
||||
url = decodeURIComponent(url);
|
||||
const responseText = await RequestUtil.HttpGetText(url);
|
||||
res.send(sendSuccess(res, responseText));
|
||||
return sendSuccess(res, responseText);
|
||||
} else {
|
||||
res.send(sendError(res, 'url参数不合法'));
|
||||
return sendError(res, 'url参数不合法');
|
||||
}
|
||||
};
|
@@ -1,167 +1,76 @@
|
||||
import { webUiPathWrapper } from '@/webui';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import Ajv from 'ajv';
|
||||
import fs, { constants } from 'node:fs/promises';
|
||||
import * as net from 'node:net';
|
||||
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
// 限制尝试端口的次数,避免死循环
|
||||
const MAX_PORT_TRY = 100;
|
||||
|
||||
async function tryUseHost(host: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const server = net.createServer();
|
||||
server.on('listening', () => {
|
||||
server.close();
|
||||
resolve(host);
|
||||
});
|
||||
// 定义配置的类型
|
||||
const WebUiConfigSchema = Type.Object({
|
||||
host: Type.String({ default: '0.0.0.0' }),
|
||||
port: Type.Number({ default: 6099 }),
|
||||
token: Type.String({ default: 'napcat' }),
|
||||
loginRate: Type.Number({ default: 10 }),
|
||||
autoLoginAccount: Type.String({ default: '' }),
|
||||
});
|
||||
|
||||
server.on('error', (err: any) => {
|
||||
if (err.code === 'EADDRNOTAVAIL') {
|
||||
reject(new Error('主机地址验证失败,可能为非本机地址'));
|
||||
} else {
|
||||
reject(new Error(`遇到错误: ${err.code}`));
|
||||
}
|
||||
});
|
||||
export type WebUiConfigType = Static<typeof WebUiConfigSchema>;
|
||||
|
||||
// 尝试监听 让系统随机分配一个端口
|
||||
server.listen(0, host);
|
||||
} catch (error) {
|
||||
// 这里捕获到的错误应该是启动服务器时的同步错误
|
||||
reject(new Error(`服务器启动时发生错误: ${error}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function tryUsePort(port: number, host: string, tryCount: number = 0): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const server = net.createServer();
|
||||
server.on('listening', () => {
|
||||
server.close();
|
||||
resolve(port);
|
||||
});
|
||||
|
||||
server.on('error', (err: any) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
if (tryCount < MAX_PORT_TRY) {
|
||||
// 使用循环代替递归
|
||||
resolve(tryUsePort(port + 1, host, tryCount + 1));
|
||||
} else {
|
||||
reject(new Error(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`));
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`遇到错误: ${err.code}`));
|
||||
}
|
||||
});
|
||||
|
||||
// 尝试监听端口
|
||||
server.listen(port, host);
|
||||
} catch (error) {
|
||||
// 这里捕获到的错误应该是启动服务器时的同步错误
|
||||
reject(new Error(`服务器启动时发生错误: ${error}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件
|
||||
export class WebUiConfigWrapper {
|
||||
WebUiConfigData: WebUiConfigType | undefined = undefined;
|
||||
|
||||
private applyDefaults<T>(obj: Partial<T>, defaults: T): T {
|
||||
const result = { ...defaults } as T;
|
||||
for (const key in obj) {
|
||||
if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
||||
result[key] = this.applyDefaults(obj[key], defaults[key]);
|
||||
} else if (obj[key] !== undefined) {
|
||||
result[key] = obj[key] as T[Extract<keyof T, string>];
|
||||
}
|
||||
private validateAndApplyDefaults(config: Partial<WebUiConfigType>): WebUiConfigType {
|
||||
new Ajv({ coerceTypes: true, useDefaults: true }).compile(WebUiConfigSchema)(config);
|
||||
return config as WebUiConfigType;
|
||||
}
|
||||
|
||||
private async ensureConfigFileExists(configPath: string): Promise<void> {
|
||||
const configExists = await fs.access(configPath, constants.F_OK).then(() => true).catch(() => false);
|
||||
if (!configExists) {
|
||||
await fs.writeFile(configPath, JSON.stringify(this.validateAndApplyDefaults({}), null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
private async readAndValidateConfig(configPath: string): Promise<WebUiConfigType> {
|
||||
const fileContent = await fs.readFile(configPath, 'utf-8');
|
||||
return this.validateAndApplyDefaults(JSON.parse(fileContent));
|
||||
}
|
||||
|
||||
private async writeConfig(configPath: string, config: WebUiConfigType): Promise<void> {
|
||||
const hasWritePermission = await fs.access(configPath, constants.W_OK).then(() => true).catch(() => false);
|
||||
if (hasWritePermission) {
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 4));
|
||||
} else {
|
||||
console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async GetWebUIConfig(): Promise<WebUiConfigType> {
|
||||
if (this.WebUiConfigData) {
|
||||
return this.WebUiConfigData;
|
||||
}
|
||||
const defaultconfig: WebUiConfigType = {
|
||||
host: '0.0.0.0',
|
||||
port: 6099,
|
||||
token: '', // 默认先填空,空密码无法登录
|
||||
loginRate: 3,
|
||||
};
|
||||
try {
|
||||
defaultconfig.token = Math.random().toString(36).slice(2); //生成随机密码
|
||||
} catch (e) {
|
||||
console.log('随机密码生成失败', e);
|
||||
}
|
||||
try {
|
||||
const configPath = resolve(webUiPathWrapper.configPath, './webui.json');
|
||||
|
||||
if (
|
||||
!(await fs
|
||||
.access(configPath, constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
) {
|
||||
await fs.writeFile(configPath, JSON.stringify(defaultconfig, null, 4));
|
||||
}
|
||||
|
||||
const fileContent = await fs.readFile(configPath, 'utf-8');
|
||||
const parsedConfig = this.applyDefaults(JSON.parse(fileContent) as Partial<WebUiConfigType>, defaultconfig);
|
||||
|
||||
if (
|
||||
await fs
|
||||
.access(configPath, constants.W_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
) {
|
||||
await fs.writeFile(configPath, JSON.stringify(parsedConfig, null, 4));
|
||||
} else {
|
||||
console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`);
|
||||
}
|
||||
|
||||
const [host_err, host] = await tryUseHost(parsedConfig.host)
|
||||
.then((data) => [null, data])
|
||||
.catch((err) => [err, null]);
|
||||
if (host_err) {
|
||||
console.log('host不可用', host_err);
|
||||
parsedConfig.port = 0; // 设置为0,禁用WebUI
|
||||
} else {
|
||||
parsedConfig.host = host;
|
||||
const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host)
|
||||
.then((data) => [null, data])
|
||||
.catch((err) => [err, null]);
|
||||
if (port_err) {
|
||||
console.log('port不可用', port_err);
|
||||
parsedConfig.port = 0; // 设置为0,禁用WebUI
|
||||
} else {
|
||||
parsedConfig.port = port;
|
||||
}
|
||||
}
|
||||
await this.ensureConfigFileExists(configPath);
|
||||
const parsedConfig = await this.readAndValidateConfig(configPath);
|
||||
this.WebUiConfigData = parsedConfig;
|
||||
return this.WebUiConfigData;
|
||||
} catch (e) {
|
||||
console.log('读取配置文件失败', e);
|
||||
return this.validateAndApplyDefaults({});
|
||||
}
|
||||
return defaultconfig; // 理论上这行代码到不了,到了只能返回默认配置了
|
||||
}
|
||||
|
||||
async UpdateWebUIConfig(newConfig: Partial<WebUiConfigType>): Promise<void> {
|
||||
const configPath = resolve(webUiPathWrapper.configPath, './webui.json');
|
||||
const currentConfig = await this.GetWebUIConfig();
|
||||
const updatedConfig = this.applyDefaults(newConfig, currentConfig);
|
||||
|
||||
if (
|
||||
await fs
|
||||
.access(configPath, constants.W_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
) {
|
||||
await fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 4));
|
||||
this.WebUiConfigData = updatedConfig;
|
||||
} else {
|
||||
console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`);
|
||||
}
|
||||
const updatedConfig = this.validateAndApplyDefaults({ ...currentConfig, ...newConfig });
|
||||
await this.writeConfig(configPath, updatedConfig);
|
||||
this.WebUiConfigData = updatedConfig;
|
||||
}
|
||||
|
||||
async UpdateToken(oldToken: string, newToken: string): Promise<void> {
|
||||
@@ -176,31 +85,49 @@ export class WebUiConfigWrapper {
|
||||
public static async GetLogsPath(): Promise<string> {
|
||||
return resolve(webUiPathWrapper.logsPath);
|
||||
}
|
||||
|
||||
// 获取日志列表
|
||||
public static async GetLogsList(): Promise<string[]> {
|
||||
if (
|
||||
await fs
|
||||
.access(webUiPathWrapper.logsPath, constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
) {
|
||||
return (await fs.readdir(webUiPathWrapper.logsPath))
|
||||
.filter((file) => file.endsWith('.log'))
|
||||
.map((file) => file.replace('.log', ''));
|
||||
const logsPath = resolve(webUiPathWrapper.logsPath);
|
||||
const logsExist = await fs.access(logsPath, constants.F_OK).then(() => true).catch(() => false);
|
||||
if (logsExist) {
|
||||
return (await fs.readdir(logsPath)).filter(file => file.endsWith('.log')).map(file => file.replace('.log', ''));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取指定日志文件内容
|
||||
public static async GetLogContent(filename: string): Promise<string> {
|
||||
const logPath = resolve(webUiPathWrapper.logsPath, `${filename}.log`);
|
||||
if (
|
||||
await fs
|
||||
.access(logPath, constants.R_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
) {
|
||||
const logExists = await fs.access(logPath, constants.R_OK).then(() => true).catch(() => false);
|
||||
if (logExists) {
|
||||
return await fs.readFile(logPath, 'utf-8');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取字体文件夹内的字体列表
|
||||
public static async GetFontList(): Promise<string[]> {
|
||||
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
|
||||
const fontsExist = await fs.access(fontsPath, constants.F_OK).then(() => true).catch(() => false);
|
||||
if (fontsExist) {
|
||||
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');
|
||||
}
|
||||
|
||||
public getAutoLoginAccount(): string | undefined {
|
||||
return this.WebUiConfigData?.autoLoginAccount;
|
||||
}
|
||||
}
|
@@ -13,7 +13,9 @@ import {
|
||||
BatchMoveHandler,
|
||||
DownloadHandler,
|
||||
BatchDownloadHandler, // 新增下载处理方法
|
||||
UploadHandler, // 添加上传处理器
|
||||
UploadHandler,
|
||||
UploadWebUIFontHandler,
|
||||
DeleteWebUIFontHandler, // 添加上传处理器
|
||||
} from '../api/File';
|
||||
|
||||
const router = Router();
|
||||
@@ -21,6 +23,9 @@ const router = Router();
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1分钟内
|
||||
max: 60, // 最大60个请求
|
||||
validate: {
|
||||
xForwardedForHeader: false,
|
||||
},
|
||||
});
|
||||
|
||||
router.use(apiLimiter);
|
||||
@@ -37,5 +42,8 @@ router.post('/move', MoveHandler);
|
||||
router.post('/batchMove', BatchMoveHandler);
|
||||
router.post('/download', DownloadHandler);
|
||||
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 };
|
||||
|
@@ -1,6 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
import { checkHandler, LoginHandler, LogoutHandler, UpdateTokenHandler } from '@webapi/api/Auth';
|
||||
import {
|
||||
CheckDefaultTokenHandler,
|
||||
checkHandler,
|
||||
LoginHandler,
|
||||
LogoutHandler,
|
||||
UpdateTokenHandler,
|
||||
} from '@webapi/api/Auth';
|
||||
|
||||
const router = Router();
|
||||
// router:登录
|
||||
@@ -11,5 +17,7 @@ router.post('/check', checkHandler);
|
||||
router.post('/logout', LogoutHandler);
|
||||
// router:更新token
|
||||
router.post('/update_token', UpdateTokenHandler);
|
||||
// router:检查默认token
|
||||
router.get('/check_using_default_token', CheckDefaultTokenHandler);
|
||||
|
||||
export { router as AuthRouter };
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user