Compare commits

...

113 Commits

Author SHA1 Message Date
手瓜一十雪
0e49e17f68 feat: 32690 2025-02-27 16:34:09 +08:00
手瓜一十雪
524de45f6b Merge pull request #827 from NapNeko/dependabot/npm_and_yarn/globals-16.0.0
chore(deps-dev): bump globals from 15.15.0 to 16.0.0
2025-02-27 16:18:17 +08:00
手瓜一十雪
85741a4b60 feat: 32690 2025-02-27 16:14:39 +08:00
dependabot[bot]
f9ccb8c978 chore(deps-dev): bump globals from 15.15.0 to 16.0.0
Bumps [globals](https://github.com/sindresorhus/globals) from 15.15.0 to 16.0.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v15.15.0...v16.0.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 09:06:38 +00:00
手瓜一十雪
ea3d069e49 feat: vsc build dev体验增强 2025-02-23 17:54:19 +08:00
Mlikiowa
3e6024f183 release: v4.6.0 2025-02-23 09:31:55 +00:00
Mlikiowa
337871693a release: v4.5.24 2025-02-23 09:31:19 +00:00
手瓜一十雪
2d921c4577 feat: sisi的妙妙rkey 2025-02-23 17:30:01 +08:00
手瓜一十雪
9accff7323 fix: ts warning 2025-02-23 17:28:30 +08:00
手瓜一十雪
88b1ee8c31 docs: todo #819 2025-02-23 17:17:52 +08:00
手瓜一十雪
3ac618bb4e fix: #822 2025-02-23 17:01:00 +08:00
手瓜一十雪
0051df3741 fix: #824 2025-02-23 16:57:55 +08:00
手瓜一十雪
7eb4e010b0 Merge pull request #823 from NapNeko/refactor-worker
refactor: 即刻起逐出piscina
2025-02-23 14:31:33 +08:00
手瓜一十雪
33cc23ada3 refactor: 即刻起逐出piscina 2025-02-23 14:29:26 +08:00
手瓜一十雪
e5aee372e3 fix: 调整依赖 2025-02-23 13:40:47 +08:00
手瓜一十雪
6b6ce4a761 fix: 依赖迁移到dev 2025-02-22 12:59:37 +08:00
手瓜一十雪
8c4ea7f8f2 fix: 异常代码 2025-02-22 11:57:48 +08:00
手瓜一十雪
c8b268b806 fix: #791 2025-02-22 11:50:54 +08:00
手瓜一十雪
cf5e0e0f14 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-18 17:08:26 +08:00
手瓜一十雪
7b79f9cc17 fix: 日志显示 2025-02-18 17:08:24 +08:00
Mlikiowa
708d599966 release: v4.5.23 2025-02-18 08:56:25 +00:00
手瓜一十雪
1ecd5b78e6 feat: 文件移除path字段增强部分能力 2025-02-18 16:55:43 +08:00
手瓜一十雪
fca2e3c51a style: remove debug 2025-02-18 16:52:30 +08:00
手瓜一十雪
95ea761b2d feat: get_private_file_url 2025-02-18 16:51:51 +08:00
手瓜一十雪
6b3bfa1ee9 fix #810 2025-02-18 13:24:37 +08:00
bietiaop
df3e302a9d fix: #802 2025-02-14 21:26:16 +08:00
pk5ls20
c88a68c9a8 fix: typo x2 2025-02-14 20:52:31 +08:00
Mlikiowa
92d01b9cdd release: v4.5.22 2025-02-14 10:36:03 +00:00
手瓜一十雪
fe04fa5986 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-14 17:41:40 +08:00
手瓜一十雪
c382f541b4 fix: 优化文件处理错误信息并简化下载逻辑 2025-02-14 17:41:25 +08:00
手瓜一十雪
f420527207 Update msg.ts 2025-02-14 17:41:03 +08:00
手瓜一十雪
e0c83ebf79 fix: #793 2025-02-14 17:15:19 +08:00
手瓜一十雪
c7fb18fc08 feat: 补全一些type 2025-02-14 15:39:06 +08:00
手瓜一十雪
2db8ab937d feat: GetUnidirectionalFriendList router 2025-02-14 15:06:36 +08:00
手瓜一十雪
819f5dd8e5 fix: #785 2025-02-14 14:50:00 +08:00
手瓜一十雪
d4a8ed735e fix: #789 2025-02-14 14:48:36 +08:00
手瓜一十雪
f07e3bb4d5 fix: type 2025-02-14 14:44:10 +08:00
手瓜一十雪
fa5ef0c221 fix: #797 2025-02-14 14:41:16 +08:00
手瓜一十雪
da7499ec0b Merge pull request #790 from NapNeko/dependabot/npm_and_yarn/esbuild-0.25.0
chore(deps-dev): bump esbuild from 0.24.0 to 0.25.0
2025-02-14 13:51:47 +08:00
Mlikiowa
d2f4327e44 release: v4.5.21 2025-02-12 18:57:14 +00:00
pk5ls20
2eba640180 fix: typo 2025-02-13 02:56:07 +08:00
dependabot[bot]
29ae55f340 chore(deps-dev): bump esbuild from 0.24.0 to 0.25.0
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 08:16:17 +00:00
Mlikiowa
3d2bca3f9f release: v4.5.20 2025-02-09 05:05:52 +00:00
手瓜一十雪
7fd8c0c822 style:lint 2025-02-09 13:00:54 +08:00
手瓜一十雪
a9e9c81505 refactor: data recv 2025-02-09 13:00:17 +08:00
手瓜一十雪
e8cc68bdea style:lint 2025-02-09 12:53:42 +08:00
手瓜一十雪
9e51a661a4 fix: #761 2025-02-09 12:53:10 +08:00
bietiaop
a167aaf55f style: 修改首页卡片色适配主题 2025-02-09 12:28:57 +08:00
bietiaop
a54ecbcaa0 style: 修改侧边栏标题色适配主题 2025-02-09 12:21:34 +08:00
bietiaop
788462cdfa fix: 修复heroui primary色 2025-02-09 12:13:43 +08:00
bietiaop
45c5965b99 style: 增加heroui主题色 2025-02-09 12:11:27 +08:00
bietiaop
ce7614de46 fix: 缺少default 2025-02-09 12:00:02 +08:00
bietiaop
9f78e1ce1e feat: 预定义主题 2025-02-09 11:58:46 +08:00
pk5ls20
2c7b0625e8 chore: format 2025-02-09 01:35:37 +08:00
pk5ls20
c3a5da9be1 feat: #768 2025-02-09 01:33:56 +08:00
bietiaop
ca796e1920 feat: 设置快速登录QQ & 自定义webui主题色
feat: 设置快速登录QQ & 自定义webui主题色
2025-02-09 00:54:27 +08:00
bietiaop
7ce04cf781 final 2025-02-09 00:47:00 +08:00
bietiaop
024a3eb760 fix 2025-02-09 00:18:14 +08:00
bietiaop
1702f429b4 fix 2025-02-09 00:17:49 +08:00
bietiaop
96d79cf495 fix 2025-02-08 23:45:33 +08:00
bietiaop
a6a11a7026 fix 2025-02-08 23:38:30 +08:00
bietiaop
970a49e2a5 fix: 猪咪 2025-02-08 23:05:48 +08:00
bietiaop
2e013ed4f5 fix 2025-02-08 22:43:53 +08:00
bietiaop
f8c396b1fe feat(webui): 快速登录config 2025-02-08 21:16:49 +08:00
手瓜一十雪
b54870cb60 fix 2025-02-08 21:03:59 +08:00
bietiaop
84318acb18 feat(webui): theme 2025-02-08 21:01:29 +08:00
手瓜一十雪
a11a042b93 docs: update 2025-02-08 20:22:51 +08:00
Mlikiowa
8a8aa8f62c release: v4.5.18 2025-02-08 09:43:06 +00:00
手瓜一十雪
93f78f4db5 feat: #780 2025-02-08 17:34:31 +08:00
手瓜一十雪
404bfdd5e6 fix: #783 2025-02-08 17:00:11 +08:00
Mlikiowa
e4577dc2f1 release: v4.5.17 2025-02-07 12:40:47 +00:00
pk5ls20
5c932e5a27 fix: native rkey 2025-02-07 19:20:35 +08:00
Mlikiowa
4bd63c6267 release: v4.5.16 2025-02-07 10:02:35 +00:00
bietiaop
aabe24f903 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-07 18:00:31 +08:00
bietiaop
69cebd7fbc feat: 提示修改默认密码 2025-02-07 18:00:22 +08:00
Mlikiowa
8da371176a release: v4.5.15 2025-02-07 09:52:51 +00:00
手瓜一十雪
dd08adf1d1 fix 2025-02-07 17:43:08 +08:00
手瓜一十雪
2f67bef139 fix: #775 2025-02-07 17:25:48 +08:00
手瓜一十雪
8968c51cdc fix: 砍掉mac pty 沙盒权限不足 2025-02-07 17:11:10 +08:00
手瓜一十雪
f2fdcc9289 feat: webui体验优化 2025-02-07 13:56:48 +08:00
手瓜一十雪
aa3a575cbe feat: 优化初始化步骤 2025-02-07 13:26:48 +08:00
bietiaop
11816d038d fix: #776 2025-02-06 20:10:11 +08:00
Mlikiowa
6a990edb38 release: v4.5.14 2025-02-06 09:17:22 +00:00
手瓜一十雪
fa12865924 fix: error 2025-02-06 17:10:30 +08:00
Mlikiowa
ecdd717742 release: v4.5.12 2025-02-06 08:23:07 +00:00
bietiaop
6851334af9 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-06 15:29:04 +08:00
bietiaop
9051b29565 feat: 字体修改#771 2025-02-06 15:28:42 +08:00
手瓜一十雪
95c7d3dfbd fix: remove __dirname 2025-02-06 15:28:24 +08:00
手瓜一十雪
bc1148c00a fix: require_dlopen 2025-02-06 15:25:47 +08:00
Mlikiowa
d4556d9299 release: v4.5.11 2025-02-06 03:13:17 +00:00
pk5ls20
5d389a2359 fix: fake forwardMsg construct 2025-02-06 01:09:23 +08:00
Mlikiowa
305116874b release: v4.5.10 2025-02-05 11:49:14 +00:00
bietiaop
b08a29897f fix: #769 2025-02-05 19:45:30 +08:00
Mlikiowa
b59c1d9122 release: v4.5.9 2025-02-05 11:14:25 +00:00
手瓜一十雪
adb9cea701 Merge pull request #765 from NapNeko/fix/multi-forward-protocol-fetch
fix: #721
2025-02-05 19:08:08 +08:00
Mlikiowa
5e148d2e82 release: v4.5.8 2025-02-05 11:02:28 +00:00
手瓜一十雪
a0d780558e fix 2025-02-05 19:01:14 +08:00
Mlikiowa
ad56065a4e release: v4.5.7 2025-02-05 07:10:27 +00:00
手瓜一十雪
f5dee80b6e Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-05 15:09:27 +08:00
手瓜一十雪
9cc75881b8 fix: arm64 2025-02-05 14:51:12 +08:00
bietiaop
593fb13b61 style: 语义化样式 2025-02-05 10:38:12 +08:00
pk5ls20
fca90592d6 try fix: #755 2025-02-05 08:29:37 +08:00
pk5ls20
d6848e2855 fix: #721 2025-02-05 08:07:58 +08:00
bietiaop
7539a4129f fix: 获取歌单 2025-02-04 22:14:23 +08:00
bietiaop
5402574266 feat: AI更新总结 2025-02-04 22:03:37 +08:00
Mlikiowa
853175aa1a release: v4.5.6 2025-02-04 13:24:46 +00:00
手瓜一十雪
feb84809ec fix: #761 2025-02-04 21:22:36 +08:00
bietiaop
a812c568e4 fix: 文件预览 2025-02-04 21:12:13 +08:00
bietiaop
11db25e355 fix: 文件预览 2025-02-04 21:08:28 +08:00
手瓜一十雪
ecd2fba629 fix: #762 2025-02-04 20:42:13 +08:00
Mlikiowa
a6763cf5a1 release: v4.5.5 2025-02-04 11:50:37 +00:00
手瓜一十雪
c9e91a9b94 fix: defalut config 2025-02-04 19:49:56 +08:00
Mlikiowa
43fb62c5bd release: v4.5.4 2025-02-04 11:35:51 +00:00
155 changed files with 3692 additions and 682 deletions

115
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,115 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "dev:shell",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:shell"
]
},
{
"type": "node",
"request": "launch",
"name": "build:shell",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:shell"
]
},
{
"type": "node",
"request": "launch",
"name": "build:universal",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:universal"
]
},
{
"type": "node",
"request": "launch",
"name": "build:framework",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:framework"
]
},
{
"type": "node",
"request": "launch",
"name": "build:webui",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:webui"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:universal",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:universal"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:framework",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:framework"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:webui",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:webui"
]
},
{
"type": "node",
"request": "launch",
"name": "lint",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"lint"
]
},
{
"type": "node",
"request": "launch",
"name": "depend",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"depend"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:depend",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:depend"
]
}
]
}

View File

@@ -64,4 +64,4 @@ NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进
## 开源附加
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**本仓库仅用于提高IM易用性,实现类似Hook推送,此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**

View File

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

View File

@@ -13,6 +13,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@heroui/accordion": "^2.2.8",
"@heroui/avatar": "2.2.7",
"@heroui/breadcrumbs": "2.2.7",
"@heroui/button": "2.2.10",
@@ -32,6 +33,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",
@@ -63,6 +65,7 @@
"qrcode.react": "^4.2.0",
"quill": "^2.0.3",
"react": "^19.0.0",
"react-color": "^2.19.3",
"react-dom": "^19.0.0",
"react-dropzone": "^14.3.5",
"react-error-boundary": "^5.0.0",

View File

@@ -0,0 +1,36 @@
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
import React from 'react'
import { ColorResult, SketchPicker } from 'react-color'
// 假定 heroui 提供的 Popover组件
interface ColorPickerProps {
color: string
onChange: (color: ColorResult) => void
}
const ColorPicker: React.FC<ColorPickerProps> = ({ color, onChange }) => {
const handleChange = (colorResult: ColorResult) => {
onChange(colorResult)
}
return (
<Popover triggerScaleOnOpen={false}>
<PopoverTrigger>
<div
className="w-36 h-8 rounded-md cursor-pointer border border-content4"
style={{ background: color }}
/>
</PopoverTrigger>
<PopoverContent>
<SketchPicker
color={color}
onChange={handleChange}
className="!bg-transparent !shadow-none"
/>
</PopoverContent>
</Popover>
)
}
export default ColorPicker

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { Button } from '@heroui/button'
import clsx from 'clsx'
import toast from 'react-hot-toast'
import { IoMdRefresh } from 'react-icons/io'
@@ -7,15 +8,22 @@ export interface SaveButtonsProps {
reset: () => void
refresh?: () => void
isSubmitting: boolean
className?: string
}
const SaveButtons: React.FC<SaveButtonsProps> = ({
onSubmit,
reset,
isSubmitting,
refresh
refresh,
className
}) => (
<div className="max-w-full mx-3 w-96 flex flex-col justify-center gap-3">
<div
className={clsx(
'max-w-full mx-3 w-96 flex flex-col justify-center gap-3',
className
)}
>
<div className="flex items-center justify-center gap-2 mt-5">
<Button
color="default"
@@ -27,7 +35,7 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
</Button>
<Button
color="danger"
color="primary"
isLoading={isSubmitting}
onPress={() => onSubmit()}
>

View File

@@ -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>

View File

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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

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

View File

@@ -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"

View File

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

View File

@@ -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>

View File

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

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)}
>

View File

@@ -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"
/>
)
}
>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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>
),

View File

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

View File

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

View File

@@ -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

View File

@@ -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()

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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'

View File

@@ -33,11 +33,11 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
>
<div className="w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0">
<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}

View File

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

View File

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

View File

@@ -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)}
>

View File

@@ -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" />

View File

@@ -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>
)}

View File

@@ -11,7 +11,7 @@ const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode }) => {
<div className="bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden">
{!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} />

View File

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

View File

@@ -55,15 +55,16 @@ const renderItems = (items: MenuItem[], children = false) => {
isActive && 'bg-opacity-60',
b64img && 'backdrop-blur-md text-white'
)}
color="danger"
color="primary"
endContent={
canOpen ? (
// div实现箭头V效果
<div
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-primary-200 dark:text-white',
'before:rounded-full',
'before:content-[""]',
'before:block',
@@ -95,8 +96,8 @@ 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-red-300 dark:bg-white'
? 'bg-primary-500 animate-spinner-ease-spin'
: 'bg-primary-200 dark:bg-white'
)}
/>
)

View File

@@ -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-100 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-100 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>

View File

@@ -24,7 +24,7 @@ const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
return (
<div
className={clsx(
'shadow-sm p-2 rounded-md text-sm bg-content1 bg-opacity-30',
'shadow-sm shadow-primary-100 p-2 rounded-md text-sm bg-content1 bg-opacity-30',
size === 'lg' ? 'col-span-2' : 'col-span-1 flex justify-between'
)}
>
@@ -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-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>

View File

@@ -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
)}

View File

@@ -0,0 +1,6 @@
import heroui from './themes/heroui'
import nc_pink from './themes/nc_pink'
const themes: ThemeInfo[] = [nc_pink, heroui]
export default themes

View File

@@ -0,0 +1,256 @@
const theme: ThemeConfig = {
dark: {
'--heroui-background': '0 0% 0%',
'--heroui-foreground-50': '240 5.88% 10%',
'--heroui-foreground-100': '240 3.7% 15.88%',
'--heroui-foreground-200': '240 5.26% 26.08%',
'--heroui-foreground-300': '240 5.2% 33.92%',
'--heroui-foreground-400': '240 3.83% 46.08%',
'--heroui-foreground-500': '240 5.03% 64.9%',
'--heroui-foreground-600': '240 4.88% 83.92%',
'--heroui-foreground-700': '240 5.88% 90%',
'--heroui-foreground-800': '240 4.76% 95.88%',
'--heroui-foreground-900': '0 0% 98.04%',
'--heroui-foreground': '210 5.56% 92.94%',
'--heroui-focus': '212.01999999999998 100% 46.67%',
'--heroui-overlay': '0 0% 0%',
'--heroui-divider': '0 0% 100%',
'--heroui-divider-opacity': '0.15',
'--heroui-content1': '240 5.88% 10%',
'--heroui-content1-foreground': '0 0% 98.04%',
'--heroui-content2': '240 3.7% 15.88%',
'--heroui-content2-foreground': '240 4.76% 95.88%',
'--heroui-content3': '240 5.26% 26.08%',
'--heroui-content3-foreground': '240 5.88% 90%',
'--heroui-content4': '240 5.2% 33.92%',
'--heroui-content4-foreground': '240 4.88% 83.92%',
'--heroui-default-50': '240 5.88% 10%',
'--heroui-default-100': '240 3.7% 15.88%',
'--heroui-default-200': '240 5.26% 26.08%',
'--heroui-default-300': '240 5.2% 33.92%',
'--heroui-default-400': '240 3.83% 46.08%',
'--heroui-default-500': '240 5.03% 64.9%',
'--heroui-default-600': '240 4.88% 83.92%',
'--heroui-default-700': '240 5.88% 90%',
'--heroui-default-800': '240 4.76% 95.88%',
'--heroui-default-900': '0 0% 98.04%',
'--heroui-default-foreground': '0 0% 100%',
'--heroui-default': '240 5.26% 26.08%',
'--heroui-danger-50': '340 84.91% 10.39%',
'--heroui-danger-100': '339.33 86.54% 20.39%',
'--heroui-danger-200': '339.11 85.99% 30.78%',
'--heroui-danger-300': '339 86.54% 40.78%',
'--heroui-danger-400': '339.2 90.36% 51.18%',
'--heroui-danger-500': '339 90% 60.78%',
'--heroui-danger-600': '339.11 90.6% 70.78%',
'--heroui-danger-700': '339.33 90% 80.39%',
'--heroui-danger-800': '340 91.84% 90.39%',
'--heroui-danger-900': '339.13 92% 95.1%',
'--heroui-danger-foreground': '0 0% 100%',
'--heroui-danger': '339.2 90.36% 51.18%',
'--heroui-primary-50': '211.84 100% 9.61%',
'--heroui-primary-100': '211.84 100% 19.22%',
'--heroui-primary-200': '212.24 100% 28.82%',
'--heroui-primary-300': '212.14 100% 38.43%',
'--heroui-primary-400': '212.02 100% 46.67%',
'--heroui-primary-500': '212.14 92.45% 58.43%',
'--heroui-primary-600': '212.24 92.45% 68.82%',
'--heroui-primary-700': '211.84 92.45% 79.22%',
'--heroui-primary-800': '211.84 92.45% 89.61%',
'--heroui-primary-900': '212.5 92.31% 94.9%',
'--heroui-primary-foreground': '0 0% 100%',
'--heroui-primary': '212.02 100% 46.67%',
'--heroui-secondary-50': '270 66.67% 9.41%',
'--heroui-secondary-100': '270 66.67% 18.82%',
'--heroui-secondary-200': '270 66.67% 28.24%',
'--heroui-secondary-300': '270 66.67% 37.65%',
'--heroui-secondary-400': '270 66.67% 47.06%',
'--heroui-secondary-500': '270 59.26% 57.65%',
'--heroui-secondary-600': '270 59.26% 68.24%',
'--heroui-secondary-700': '270 59.26% 78.82%',
'--heroui-secondary-800': '270 59.26% 89.41%',
'--heroui-secondary-900': '270 61.54% 94.9%',
'--heroui-secondary-foreground': '0 0% 100%',
'--heroui-secondary': '270 59.26% 57.65%',
'--heroui-success-50': '145.71 77.78% 8.82%',
'--heroui-success-100': '146.2 79.78% 17.45%',
'--heroui-success-200': '145.79 79.26% 26.47%',
'--heroui-success-300': '146.01 79.89% 35.1%',
'--heroui-success-400': '145.96 79.46% 43.92%',
'--heroui-success-500': '146.01 62.45% 55.1%',
'--heroui-success-600': '145.79 62.57% 66.47%',
'--heroui-success-700': '146.2 61.74% 77.45%',
'--heroui-success-800': '145.71 61.4% 88.82%',
'--heroui-success-900': '146.67 64.29% 94.51%',
'--heroui-success-foreground': '0 0% 0%',
'--heroui-success': '145.96 79.46% 43.92%',
'--heroui-warning-50': '37.14 75% 10.98%',
'--heroui-warning-100': '37.14 75% 21.96%',
'--heroui-warning-200': '36.96 73.96% 33.14%',
'--heroui-warning-300': '37.01 74.22% 44.12%',
'--heroui-warning-400': '37.03 91.27% 55.1%',
'--heroui-warning-500': '37.01 91.26% 64.12%',
'--heroui-warning-600': '36.96 91.24% 73.14%',
'--heroui-warning-700': '37.14 91.3% 81.96%',
'--heroui-warning-800': '37.14 91.3% 90.98%',
'--heroui-warning-900': '54.55 91.67% 95.29%',
'--heroui-warning-foreground': '0 0% 0%',
'--heroui-warning': '37.03 91.27% 55.1%',
'--heroui-code-background': '240 5.56% 7.06%',
'--heroui-strong': '190.14 94.67% 44.12%',
'--heroui-code-mdx': '190.14 94.67% 44.12%',
'--heroui-divider-weight': '1px',
'--heroui-disabled-opacity': '.5',
'--heroui-font-size-tiny': '0.75rem',
'--heroui-font-size-small': '0.875rem',
'--heroui-font-size-medium': '1rem',
'--heroui-font-size-large': '1.125rem',
'--heroui-line-height-tiny': '1rem',
'--heroui-line-height-small': '1.25rem',
'--heroui-line-height-medium': '1.5rem',
'--heroui-line-height-large': '1.75rem',
'--heroui-radius-small': '8px',
'--heroui-radius-medium': '12px',
'--heroui-radius-large': '14px',
'--heroui-border-width-small': '1px',
'--heroui-border-width-medium': '2px',
'--heroui-border-width-large': '3px',
'--heroui-box-shadow-small':
'0px 0px 5px 0px rgba(0, 0, 0, .05), 0px 2px 10px 0px rgba(0, 0, 0, .2), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-box-shadow-medium':
'0px 0px 15px 0px rgba(0, 0, 0, .06), 0px 2px 30px 0px rgba(0, 0, 0, .22), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-box-shadow-large':
'0px 0px 30px 0px rgba(0, 0, 0, .07), 0px 30px 60px 0px rgba(0, 0, 0, .26), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-hover-opacity': '.9'
},
light: {
'--heroui-background': '0 0% 100%',
'--heroui-foreground-50': '240 5.88% 95%',
'--heroui-foreground-100': '240 3.7% 90%',
'--heroui-foreground-200': '240 5.26% 80%',
'--heroui-foreground-300': '240 5.2% 70%',
'--heroui-foreground-400': '240 3.83% 60%',
'--heroui-foreground-500': '240 5.03% 50%',
'--heroui-foreground-600': '240 4.88% 40%',
'--heroui-foreground-700': '240 5.88% 30%',
'--heroui-foreground-800': '240 4.76% 20%',
'--heroui-foreground-900': '0 0% 10%',
'--heroui-foreground': '210 5.56% 7.06%',
'--heroui-focus': '212.01999999999998 100% 53.33%',
'--heroui-overlay': '0 0% 100%',
'--heroui-divider': '0 0% 0%',
'--heroui-divider-opacity': '0.85',
'--heroui-content1': '240 5.88% 95%',
'--heroui-content1-foreground': '0 0% 10%',
'--heroui-content2': '240 3.7% 90%',
'--heroui-content2-foreground': '240 4.76% 20%',
'--heroui-content3': '240 5.26% 80%',
'--heroui-content3-foreground': '240 5.88% 30%',
'--heroui-content4': '240 5.2% 70%',
'--heroui-content4-foreground': '240 4.88% 40%',
'--heroui-default-50': '240 5.88% 95%',
'--heroui-default-100': '240 3.7% 90%',
'--heroui-default-200': '240 5.26% 80%',
'--heroui-default-300': '240 5.2% 70%',
'--heroui-default-400': '240 3.83% 60%',
'--heroui-default-500': '240 5.03% 50%',
'--heroui-default-600': '240 4.88% 40%',
'--heroui-default-700': '240 5.88% 30%',
'--heroui-default-800': '240 4.76% 20%',
'--heroui-default-900': '0 0% 10%',
'--heroui-default-foreground': '0 0% 0%',
'--heroui-default': '240 5.26% 80%',
'--heroui-danger-50': '339.13 92% 95.1%',
'--heroui-danger-100': '340 91.84% 90.39%',
'--heroui-danger-200': '339.33 90% 80.39%',
'--heroui-danger-300': '339.11 90.6% 70.78%',
'--heroui-danger-400': '339 90% 60.78%',
'--heroui-danger-500': '339.2 90.36% 51.18%',
'--heroui-danger-600': '339 86.54% 40.78%',
'--heroui-danger-700': '339.11 85.99% 30.78%',
'--heroui-danger-800': '339.33 86.54% 20.39%',
'--heroui-danger-900': '340 84.91% 10.39%',
'--heroui-danger-foreground': '0 0% 100%',
'--heroui-danger': '339.2 90.36% 51.18%',
'--heroui-primary-50': '212.5 92.31% 94.9%',
'--heroui-primary-100': '211.84 92.45% 89.61%',
'--heroui-primary-200': '211.84 92.45% 79.22%',
'--heroui-primary-300': '212.24 92.45% 68.82%',
'--heroui-primary-400': '212.14 92.45% 58.43%',
'--heroui-primary-500': '212.02 100% 46.67%',
'--heroui-primary-600': '212.14 100% 38.43%',
'--heroui-primary-700': '212.24 100% 28.82%',
'--heroui-primary-800': '211.84 100% 19.22%',
'--heroui-primary-900': '211.84 100% 9.61%',
'--heroui-primary-foreground': '0 0% 100%',
'--heroui-primary': '212.02 100% 46.67%',
'--heroui-secondary-50': '270 61.54% 94.9%',
'--heroui-secondary-100': '270 59.26% 89.41%',
'--heroui-secondary-200': '270 59.26% 78.82%',
'--heroui-secondary-300': '270 59.26% 68.24%',
'--heroui-secondary-400': '270 59.26% 57.65%',
'--heroui-secondary-500': '270 66.67% 47.06%',
'--heroui-secondary-600': '270 66.67% 37.65%',
'--heroui-secondary-700': '270 66.67% 28.24%',
'--heroui-secondary-800': '270 66.67% 18.82%',
'--heroui-secondary-900': '270 66.67% 9.41%',
'--heroui-secondary-foreground': '0 0% 100%',
'--heroui-secondary': '270 66.67% 47.06%',
'--heroui-success-50': '146.67 64.29% 94.51%',
'--heroui-success-100': '145.71 61.4% 88.82%',
'--heroui-success-200': '146.2 61.74% 77.45%',
'--heroui-success-300': '145.79 62.57% 66.47%',
'--heroui-success-400': '146.01 62.45% 55.1%',
'--heroui-success-500': '145.96 79.46% 43.92%',
'--heroui-success-600': '146.01 79.89% 35.1%',
'--heroui-success-700': '145.79 79.26% 26.47%',
'--heroui-success-800': '146.2 79.78% 17.45%',
'--heroui-success-900': '145.71 77.78% 8.82%',
'--heroui-success-foreground': '0 0% 0%',
'--heroui-success': '145.96 79.46% 43.92%',
'--heroui-warning-50': '54.55 91.67% 95.29%',
'--heroui-warning-100': '37.14 91.3% 90.98%',
'--heroui-warning-200': '37.14 91.3% 81.96%',
'--heroui-warning-300': '36.96 91.24% 73.14%',
'--heroui-warning-400': '37.01 91.26% 64.12%',
'--heroui-warning-500': '37.03 91.27% 55.1%',
'--heroui-warning-600': '37.01 74.22% 44.12%',
'--heroui-warning-700': '36.96 73.96% 33.14%',
'--heroui-warning-800': '37.14 75% 21.96%',
'--heroui-warning-900': '37.14 75% 10.98%',
'--heroui-warning-foreground': '0 0% 0%',
'--heroui-warning': '37.03 91.27% 55.1%',
'--heroui-code-background': '221.25 17.39% 18.04%',
'--heroui-strong': '316.95 100% 65.29%',
'--heroui-code-mdx': '316.95 100% 65.29%',
'--heroui-divider-weight': '1px',
'--heroui-disabled-opacity': '.5',
'--heroui-font-size-tiny': '0.75rem',
'--heroui-font-size-small': '0.875rem',
'--heroui-font-size-medium': '1rem',
'--heroui-font-size-large': '1.125rem',
'--heroui-line-height-tiny': '1rem',
'--heroui-line-height-small': '1.25rem',
'--heroui-line-height-medium': '1.5rem',
'--heroui-line-height-large': '1.75rem',
'--heroui-radius-small': '8px',
'--heroui-radius-medium': '12px',
'--heroui-radius-large': '14px',
'--heroui-border-width-small': '1px',
'--heroui-border-width-medium': '2px',
'--heroui-border-width-large': '3px',
'--heroui-box-shadow-small':
'0px 0px 5px 0px rgba(0, 0, 0, .02), 0px 2px 10px 0px rgba(0, 0, 0, .06), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-box-shadow-medium':
'0px 0px 15px 0px rgba(0, 0, 0, .03), 0px 2px 30px 0px rgba(0, 0, 0, .08), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-box-shadow-large':
'0px 0px 30px 0px rgba(0, 0, 0, .04), 0px 30px 60px 0px rgba(0, 0, 0, .12), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-hover-opacity': '.8'
}
}
export default {
theme,
author: 'HeroUI',
name: 'heroui',
description: 'HeroUI Default Theme'
} satisfies ThemeInfo

View File

@@ -0,0 +1,256 @@
const theme: ThemeConfig = {
dark: {
'--heroui-background': '0 0% 0%',
'--heroui-foreground-50': '240 5.88% 10%',
'--heroui-foreground-100': '240 3.7% 15.88%',
'--heroui-foreground-200': '240 5.26% 26.08%',
'--heroui-foreground-300': '240 5.2% 33.92%',
'--heroui-foreground-400': '240 3.83% 46.08%',
'--heroui-foreground-500': '240 5.03% 64.9%',
'--heroui-foreground-600': '240 4.88% 83.92%',
'--heroui-foreground-700': '240 5.88% 90%',
'--heroui-foreground-800': '240 4.76% 95.88%',
'--heroui-foreground-900': '0 0% 98.04%',
'--heroui-foreground': '210 5.56% 92.94%',
'--heroui-focus': '212.01999999999998 100% 46.67%',
'--heroui-overlay': '0 0% 0%',
'--heroui-divider': '0 0% 100%',
'--heroui-divider-opacity': '0.15',
'--heroui-content1': '240 5.88% 10%',
'--heroui-content1-foreground': '0 0% 98.04%',
'--heroui-content2': '240 3.7% 15.88%',
'--heroui-content2-foreground': '240 4.76% 95.88%',
'--heroui-content3': '240 5.26% 26.08%',
'--heroui-content3-foreground': '240 5.88% 90%',
'--heroui-content4': '240 5.2% 33.92%',
'--heroui-content4-foreground': '240 4.88% 83.92%',
'--heroui-default-50': '240 5.88% 10%',
'--heroui-default-100': '240 3.7% 15.88%',
'--heroui-default-200': '240 5.26% 26.08%',
'--heroui-default-300': '240 5.2% 33.92%',
'--heroui-default-400': '240 3.83% 46.08%',
'--heroui-default-500': '240 5.03% 64.9%',
'--heroui-default-600': '240 4.88% 83.92%',
'--heroui-default-700': '240 5.88% 90%',
'--heroui-default-800': '240 4.76% 95.88%',
'--heroui-default-900': '0 0% 98.04%',
'--heroui-default-foreground': '0 0% 100%',
'--heroui-default': '240 5.26% 26.08%',
'--heroui-danger-50': '301.89 82.61% 22.55%',
'--heroui-danger-100': '308.18 76.39% 28.24%',
'--heroui-danger-200': '313.85 70.65% 36.08%',
'--heroui-danger-300': '319.73 65.64% 44.51%',
'--heroui-danger-400': '325.82 69.62% 53.53%',
'--heroui-danger-500': '331.82 75% 65.49%',
'--heroui-danger-600': '337.84 83.46% 73.92%',
'--heroui-danger-700': '343.42 90.48% 83.53%',
'--heroui-danger-800': '350.53 90.48% 91.76%',
'--heroui-danger-900': '324 90.91% 95.69%',
'--heroui-danger-foreground': '0 0% 100%',
'--heroui-danger': '325.82 69.62% 53.53%',
'--heroui-primary-50': '340 84.91% 10.39%',
'--heroui-primary-100': '339.33 86.54% 20.39%',
'--heroui-primary-200': '339.11 85.99% 30.78%',
'--heroui-primary-300': '339 86.54% 40.78%',
'--heroui-primary-400': '339.2 90.36% 51.18%',
'--heroui-primary-500': '339 90% 60.78%',
'--heroui-primary-600': '339.11 90.6% 70.78%',
'--heroui-primary-700': '339.33 90% 80.39%',
'--heroui-primary-800': '340 91.84% 90.39%',
'--heroui-primary-900': '339.13 92% 95.1%',
'--heroui-primary-foreground': '0 0% 100%',
'--heroui-primary': '339.2 90.36% 51.18%',
'--heroui-secondary-50': '270 66.67% 9.41%',
'--heroui-secondary-100': '270 66.67% 18.82%',
'--heroui-secondary-200': '270 66.67% 28.24%',
'--heroui-secondary-300': '270 66.67% 37.65%',
'--heroui-secondary-400': '270 66.67% 47.06%',
'--heroui-secondary-500': '270 59.26% 57.65%',
'--heroui-secondary-600': '270 59.26% 68.24%',
'--heroui-secondary-700': '270 59.26% 78.82%',
'--heroui-secondary-800': '270 59.26% 89.41%',
'--heroui-secondary-900': '270 61.54% 94.9%',
'--heroui-secondary-foreground': '0 0% 100%',
'--heroui-secondary': '270 59.26% 57.65%',
'--heroui-success-50': '145.71 77.78% 8.82%',
'--heroui-success-100': '146.2 79.78% 17.45%',
'--heroui-success-200': '145.79 79.26% 26.47%',
'--heroui-success-300': '146.01 79.89% 35.1%',
'--heroui-success-400': '145.96 79.46% 43.92%',
'--heroui-success-500': '146.01 62.45% 55.1%',
'--heroui-success-600': '145.79 62.57% 66.47%',
'--heroui-success-700': '146.2 61.74% 77.45%',
'--heroui-success-800': '145.71 61.4% 88.82%',
'--heroui-success-900': '146.67 64.29% 94.51%',
'--heroui-success-foreground': '0 0% 0%',
'--heroui-success': '145.96 79.46% 43.92%',
'--heroui-warning-50': '37.14 75% 10.98%',
'--heroui-warning-100': '37.14 75% 21.96%',
'--heroui-warning-200': '36.96 73.96% 33.14%',
'--heroui-warning-300': '37.01 74.22% 44.12%',
'--heroui-warning-400': '37.03 91.27% 55.1%',
'--heroui-warning-500': '37.01 91.26% 64.12%',
'--heroui-warning-600': '36.96 91.24% 73.14%',
'--heroui-warning-700': '37.14 91.3% 81.96%',
'--heroui-warning-800': '37.14 91.3% 90.98%',
'--heroui-warning-900': '54.55 91.67% 95.29%',
'--heroui-warning-foreground': '0 0% 0%',
'--heroui-warning': '37.03 91.27% 55.1%',
'--heroui-code-background': '240 5.56% 7.06%',
'--heroui-strong': '190.14 94.67% 44.12%',
'--heroui-code-mdx': '190.14 94.67% 44.12%',
'--heroui-divider-weight': '1px',
'--heroui-disabled-opacity': '.5',
'--heroui-font-size-tiny': '0.75rem',
'--heroui-font-size-small': '0.875rem',
'--heroui-font-size-medium': '1rem',
'--heroui-font-size-large': '1.125rem',
'--heroui-line-height-tiny': '1rem',
'--heroui-line-height-small': '1.25rem',
'--heroui-line-height-medium': '1.5rem',
'--heroui-line-height-large': '1.75rem',
'--heroui-radius-small': '8px',
'--heroui-radius-medium': '12px',
'--heroui-radius-large': '14px',
'--heroui-border-width-small': '1px',
'--heroui-border-width-medium': '2px',
'--heroui-border-width-large': '3px',
'--heroui-box-shadow-small':
'0px 0px 5px 0px rgba(0, 0, 0, .05), 0px 2px 10px 0px rgba(0, 0, 0, .2), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-box-shadow-medium':
'0px 0px 15px 0px rgba(0, 0, 0, .06), 0px 2px 30px 0px rgba(0, 0, 0, .22), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-box-shadow-large':
'0px 0px 30px 0px rgba(0, 0, 0, .07), 0px 30px 60px 0px rgba(0, 0, 0, .26), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-hover-opacity': '.9'
},
light: {
'--heroui-background': '0 0% 100%',
'--heroui-foreground-50': '240 5.88% 95%',
'--heroui-foreground-100': '240 3.7% 90%',
'--heroui-foreground-200': '240 5.26% 80%',
'--heroui-foreground-300': '240 5.2% 70%',
'--heroui-foreground-400': '240 3.83% 60%',
'--heroui-foreground-500': '240 5.03% 50%',
'--heroui-foreground-600': '240 4.88% 40%',
'--heroui-foreground-700': '240 5.88% 30%',
'--heroui-foreground-800': '240 4.76% 20%',
'--heroui-foreground-900': '0 0% 10%',
'--heroui-foreground': '210 5.56% 7.06%',
'--heroui-focus': '212.01999999999998 100% 53.33%',
'--heroui-overlay': '0 0% 100%',
'--heroui-divider': '0 0% 0%',
'--heroui-divider-opacity': '0.85',
'--heroui-content1': '240 5.88% 95%',
'--heroui-content1-foreground': '0 0% 10%',
'--heroui-content2': '240 3.7% 90%',
'--heroui-content2-foreground': '240 4.76% 20%',
'--heroui-content3': '240 5.26% 80%',
'--heroui-content3-foreground': '240 5.88% 30%',
'--heroui-content4': '240 5.2% 70%',
'--heroui-content4-foreground': '240 4.88% 40%',
'--heroui-default-50': '240 5.88% 95%',
'--heroui-default-100': '240 3.7% 90%',
'--heroui-default-200': '240 5.26% 80%',
'--heroui-default-300': '240 5.2% 70%',
'--heroui-default-400': '240 3.83% 60%',
'--heroui-default-500': '240 5.03% 50%',
'--heroui-default-600': '240 4.88% 40%',
'--heroui-default-700': '240 5.88% 30%',
'--heroui-default-800': '240 4.76% 20%',
'--heroui-default-900': '0 0% 10%',
'--heroui-default-foreground': '0 0% 0%',
'--heroui-default': '240 5.26% 80%',
'--heroui-danger-50': '324 90.91% 95.69%',
'--heroui-danger-100': '350.53 90.48% 91.76%',
'--heroui-danger-200': '343.42 90.48% 83.53%',
'--heroui-danger-300': '337.84 83.46% 73.92%',
'--heroui-danger-400': '331.82 75% 65.49%',
'--heroui-danger-500': '325.82 69.62% 53.53%',
'--heroui-danger-600': '319.73 65.64% 44.51%',
'--heroui-danger-700': '313.85 70.65% 36.08%',
'--heroui-danger-800': '308.18 76.39% 28.24%',
'--heroui-danger-900': '301.89 82.61% 22.55%',
'--heroui-danger-foreground': '0 0% 100%',
'--heroui-danger': '325.82 69.62% 53.53%',
'--heroui-primary-50': '339.13 92% 95.1%',
'--heroui-primary-100': '340 91.84% 90.39%',
'--heroui-primary-200': '339.33 90% 80.39%',
'--heroui-primary-300': '339.11 90.6% 70.78%',
'--heroui-primary-400': '339 90% 60.78%',
'--heroui-primary-500': '339.2 90.36% 51.18%',
'--heroui-primary-600': '339 86.54% 40.78%',
'--heroui-primary-700': '339.11 85.99% 30.78%',
'--heroui-primary-800': '339.33 86.54% 20.39%',
'--heroui-primary-900': '340 84.91% 10.39%',
'--heroui-primary-foreground': '0 0% 100%',
'--heroui-primary': '339.2 90.36% 51.18%',
'--heroui-secondary-50': '270 61.54% 94.9%',
'--heroui-secondary-100': '270 59.26% 89.41%',
'--heroui-secondary-200': '270 59.26% 78.82%',
'--heroui-secondary-300': '270 59.26% 68.24%',
'--heroui-secondary-400': '270 59.26% 57.65%',
'--heroui-secondary-500': '270 66.67% 47.06%',
'--heroui-secondary-600': '270 66.67% 37.65%',
'--heroui-secondary-700': '270 66.67% 28.24%',
'--heroui-secondary-800': '270 66.67% 18.82%',
'--heroui-secondary-900': '270 66.67% 9.41%',
'--heroui-secondary-foreground': '0 0% 100%',
'--heroui-secondary': '270 66.67% 47.06%',
'--heroui-success-50': '146.67 64.29% 94.51%',
'--heroui-success-100': '145.71 61.4% 88.82%',
'--heroui-success-200': '146.2 61.74% 77.45%',
'--heroui-success-300': '145.79 62.57% 66.47%',
'--heroui-success-400': '146.01 62.45% 55.1%',
'--heroui-success-500': '145.96 79.46% 43.92%',
'--heroui-success-600': '146.01 79.89% 35.1%',
'--heroui-success-700': '145.79 79.26% 26.47%',
'--heroui-success-800': '146.2 79.78% 17.45%',
'--heroui-success-900': '145.71 77.78% 8.82%',
'--heroui-success-foreground': '0 0% 0%',
'--heroui-success': '145.96 79.46% 43.92%',
'--heroui-warning-50': '54.55 91.67% 95.29%',
'--heroui-warning-100': '37.14 91.3% 90.98%',
'--heroui-warning-200': '37.14 91.3% 81.96%',
'--heroui-warning-300': '36.96 91.24% 73.14%',
'--heroui-warning-400': '37.01 91.26% 64.12%',
'--heroui-warning-500': '37.03 91.27% 55.1%',
'--heroui-warning-600': '37.01 74.22% 44.12%',
'--heroui-warning-700': '36.96 73.96% 33.14%',
'--heroui-warning-800': '37.14 75% 21.96%',
'--heroui-warning-900': '37.14 75% 10.98%',
'--heroui-warning-foreground': '0 0% 0%',
'--heroui-warning': '37.03 91.27% 55.1%',
'--heroui-code-background': '221.25 17.39% 18.04%',
'--heroui-strong': '316.95 100% 65.29%',
'--heroui-code-mdx': '316.95 100% 65.29%',
'--heroui-divider-weight': '1px',
'--heroui-disabled-opacity': '.5',
'--heroui-font-size-tiny': '0.75rem',
'--heroui-font-size-small': '0.875rem',
'--heroui-font-size-medium': '1rem',
'--heroui-font-size-large': '1.125rem',
'--heroui-line-height-tiny': '1rem',
'--heroui-line-height-small': '1.25rem',
'--heroui-line-height-medium': '1.5rem',
'--heroui-line-height-large': '1.75rem',
'--heroui-radius-small': '8px',
'--heroui-radius-medium': '12px',
'--heroui-radius-large': '14px',
'--heroui-border-width-small': '1px',
'--heroui-border-width-medium': '2px',
'--heroui-border-width-large': '3px',
'--heroui-box-shadow-small':
'0px 0px 5px 0px rgba(0, 0, 0, .02), 0px 2px 10px 0px rgba(0, 0, 0, .06), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-box-shadow-medium':
'0px 0px 15px 0px rgba(0, 0, 0, .03), 0px 2px 30px 0px rgba(0, 0, 0, .08), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-box-shadow-large':
'0px 0px 30px 0px rgba(0, 0, 0, .04), 0px 30px 60px 0px rgba(0, 0, 0, .12), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-hover-opacity': '.8'
}
}
export default {
theme,
author: 'NapCat',
name: 'nc_pink',
description: 'NapCat Pink Theme'
} satisfies ThemeInfo

View File

@@ -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={{

View File

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

View File

@@ -73,4 +73,17 @@ export default class QQManager {
)
return data.data.data
}
public static async getQuickLoginQQ() {
const { data } = await serverRequest.post<ServerResponse<string>>(
'/QQLogin/GetQuickLoginQQ'
)
return data.data
}
public static async setQuickLoginQQ(uin: string) {
await serverRequest.post<ServerResponse<null>>('/QQLogin/SetQuickLoginQQ', {
uin
})
}
}

View File

@@ -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)
@@ -60,6 +59,20 @@ export default class WebUIManager {
return data.data
}
public static async getThemeConfig() {
const { data } =
await serverRequest.get<ServerResponse<ThemeConfig>>('/base/Theme')
return data.data
}
public static async setThemeConfig(theme: ThemeConfig) {
const { data } = await serverRequest.post<ServerResponse<boolean>>(
'/base/SetTheme',
{ theme }
)
return data.data
}
public static async getLogList() {
const { data } =
await serverRequest.get<ServerResponse<string[]>>('/Log/GetLogList')

View File

@@ -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'
)}
>

View File

@@ -8,6 +8,7 @@ import '@/styles/globals.css'
import key from './const/key'
import WebUIManager from './controllers/webui_manager'
import { loadTheme } from './utils/theme'
WebUIManager.checkWebUiLogined()
@@ -22,6 +23,8 @@ if (theme && !theme.startsWith('"')) {
localStorage.setItem(key.theme, JSON.stringify(theme))
}
loadTheme()
ReactDOM.createRoot(document.getElementById('root')!).render(
// <React.StrictMode>
<BrowserRouter basename="/webui/">

View File

@@ -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>

View File

@@ -1,20 +1,36 @@
import { Card, CardBody } from '@heroui/card'
import { Tab, Tabs } from '@heroui/tabs'
import clsx from 'clsx'
import { useMediaQuery } from 'react-responsive'
import { useNavigate, useSearchParams } from 'react-router-dom'
import ChangePasswordCard from './change_password'
import LoginConfigCard from './login'
import OneBotConfigCard from './onebot'
import ThemeConfigCard from './theme'
import WebUIConfigCard from './webui'
export interface ConfigPageProps {
children?: React.ReactNode
size?: 'sm' | 'md' | 'lg'
}
const ConfingPageItem: React.FC<ConfigPageProps> = ({ children }) => {
const ConfingPageItem: React.FC<ConfigPageProps> = ({
children,
size = 'md'
}) => {
return (
<Card className="bg-opacity-50 backdrop-blur-sm">
<CardBody className="items-center py-5">
<div className="w-96 max-w-full flex flex-col gap-2">{children}</div>
<div
className={clsx('max-w-full flex flex-col gap-2', {
'w-72': size === 'sm',
'w-96': size === 'md',
'w-[32rem]': size === 'lg'
})}
>
{children}
</div>
</CardBody>
</Card>
)
@@ -22,6 +38,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 +51,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',
@@ -47,12 +72,22 @@ export default function ConfigPage() {
<WebUIConfigCard />
</ConfingPageItem>
</Tab>
<Tab title="登录配置" key="login">
<ConfingPageItem>
<LoginConfigCard />
</ConfingPageItem>
</Tab>
<Tab title="修改密码" key="token">
<ConfingPageItem>
<ChangePasswordCard />
</ConfingPageItem>
</Tab>
<Tab title="主题配置" key="theme">
<ConfingPageItem size="lg">
<ThemeConfigCard />
</ConfingPageItem>
</Tab>
</Tabs>
</section>
)

View File

@@ -0,0 +1,89 @@
import { Input } from '@heroui/input'
import { useRequest } from 'ahooks'
import { useEffect } from 'react'
import { Controller, useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
import SaveButtons from '@/components/button/save_buttons'
import PageLoading from '@/components/page_loading'
import QQManager from '@/controllers/qq_manager'
const LoginConfigCard = () => {
const {
data: quickLoginData,
loading: quickLoginLoading,
error: quickLoginError,
refreshAsync: refreshQuickLogin
} = useRequest(QQManager.getQuickLoginQQ)
const {
control,
handleSubmit: handleOnebotSubmit,
formState: { isSubmitting },
setValue: setOnebotValue
} = useForm<{
quickLoginQQ: string
}>({
defaultValues: {
quickLoginQQ: ''
}
})
const reset = () => {
setOnebotValue('quickLoginQQ', quickLoginData ?? '')
}
const onSubmit = handleOnebotSubmit(async (data) => {
try {
await QQManager.setQuickLoginQQ(data.quickLoginQQ)
toast.success('保存成功')
} catch (error) {
const msg = (error as Error).message
toast.error(`保存失败: ${msg}`)
}
})
const onRefresh = async () => {
try {
await refreshQuickLogin()
toast.success('刷新成功')
} catch (error) {
const msg = (error as Error).message
toast.error(`刷新失败: ${msg}`)
}
}
useEffect(() => {
reset()
}, [quickLoginData])
if (quickLoginLoading) return <PageLoading loading={true} />
return (
<>
<title>OneBot配置 - NapCat WebUI</title>
<div className="flex-shrink-0 w-full">QQ</div>
<Controller
control={control}
name="quickLoginQQ"
render={({ field }) => (
<Input
{...field}
label="快速登录QQ"
placeholder="请输入QQ号"
isDisabled={!!quickLoginError}
errorMessage={quickLoginError ? '获取快速登录QQ失败' : undefined}
/>
)}
/>
<SaveButtons
onSubmit={onSubmit}
reset={reset}
isSubmitting={isSubmitting || quickLoginLoading}
refresh={onRefresh}
/>
</>
)
}
export default LoginConfigCard

View File

@@ -30,9 +30,9 @@ const OneBotConfigCard = () => {
setOnebotValue('parseMultMsg', config.parseMultMsg)
}
const onSubmit = handleOnebotSubmit((data) => {
const onSubmit = handleOnebotSubmit(async (data) => {
try {
saveConfigWithoutNetwork(data)
await saveConfigWithoutNetwork(data)
toast.success('保存成功')
} catch (error) {
const msg = (error as Error).message

View File

@@ -0,0 +1,279 @@
import { Accordion, AccordionItem } from '@heroui/accordion'
import { Card, CardBody, CardHeader } from '@heroui/card'
import { useRequest } from 'ahooks'
import clsx from 'clsx'
import { useEffect, useRef } from 'react'
import { Controller, useForm, useWatch } from 'react-hook-form'
import toast from 'react-hot-toast'
import { FaUserAstronaut } from 'react-icons/fa'
import { FaPaintbrush } from 'react-icons/fa6'
import { IoIosColorPalette } from 'react-icons/io'
import { MdDarkMode, MdLightMode } from 'react-icons/md'
import themes from '@/const/themes'
import ColorPicker from '@/components/ColorPicker'
import SaveButtons from '@/components/button/save_buttons'
import PageLoading from '@/components/page_loading'
import { colorKeys, generateTheme, loadTheme } from '@/utils/theme'
import WebUIManager from '@/controllers/webui_manager'
export type PreviewThemeCardProps = {
theme: ThemeInfo
onPreview: () => void
}
const values = [
'',
'-50',
'-100',
'-200',
'-300',
'-400',
'-500',
'-600',
'-700',
'-800',
'-900'
]
const colors = [
'primary',
'secondary',
'success',
'danger',
'warning',
'default'
]
function PreviewThemeCard({ theme, onPreview }: PreviewThemeCardProps) {
const style = document.createElement('style')
style.innerHTML = generateTheme(theme.theme, theme.name)
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
document.head.appendChild(style)
return () => {
document.head.removeChild(style)
}
}, [])
return (
<Card
ref={cardRef}
shadow="sm"
radius="sm"
isPressable
onPress={onPreview}
className={clsx('text-primary bg-primary-50', theme.name)}
>
<CardHeader className="pb-0 flex flex-col items-start gap-1">
<div className="px-1 rounded-md bg-primary text-primary-foreground">
{theme.name}
</div>
<div className="text-xs flex items-center gap-1 text-primary-300">
<FaUserAstronaut />
{theme.author ?? '未知'}
</div>
<div className="text-xs text-primary-200">{theme.description}</div>
</CardHeader>
<CardBody>
<div className="flex flex-col gap-1">
{colors.map((color) => (
<div className="flex gap-1 items-center flex-wrap" key={color}>
<div className="text-xs w-4 text-right">
{color[0].toUpperCase()}
</div>
{values.map((value) => (
<div
key={value}
className={clsx(
'w-2 h-2 rounded-full shadow-small',
`bg-${color}${value}`
)}
></div>
))}
</div>
))}
</div>
</CardBody>
</Card>
)
}
const ThemeConfigCard = () => {
const { data, loading, error, refreshAsync } = useRequest(
WebUIManager.getThemeConfig
)
const {
control,
handleSubmit: handleOnebotSubmit,
formState: { isSubmitting },
setValue: setOnebotValue
} = useForm<{
theme: ThemeConfig
}>({
defaultValues: {
theme: {
dark: {},
light: {}
}
}
})
// 使用 useRef 存储 style 标签引用
const styleTagRef = useRef<HTMLStyleElement | null>(null)
// 在组件挂载时创建 style 标签,并在卸载时清理
useEffect(() => {
const styleTag = document.createElement('style')
document.head.appendChild(styleTag)
styleTagRef.current = styleTag
return () => {
if (styleTagRef.current) {
document.head.removeChild(styleTagRef.current)
}
}
}, [])
const theme = useWatch({ control, name: 'theme' })
const reset = () => {
if (data) setOnebotValue('theme', data)
}
const onSubmit = handleOnebotSubmit(async (data) => {
try {
await WebUIManager.setThemeConfig(data.theme)
toast.success('保存成功')
loadTheme()
} catch (error) {
const msg = (error as Error).message
toast.error(`保存失败: ${msg}`)
}
})
const onRefresh = async () => {
try {
await refreshAsync()
toast.success('刷新成功')
} catch (error) {
const msg = (error as Error).message
toast.error(`刷新失败: ${msg}`)
}
}
useEffect(() => {
reset()
}, [data])
useEffect(() => {
if (theme && styleTagRef.current) {
const css = generateTheme(theme)
styleTagRef.current.innerHTML = css
}
}, [theme])
if (loading) return <PageLoading loading={true} />
if (error)
return (
<div className="py-24 text-danger-500 text-center">{error.message}</div>
)
return (
<>
<title> - NapCat WebUI</title>
<SaveButtons
onSubmit={onSubmit}
reset={reset}
isSubmitting={isSubmitting}
refresh={onRefresh}
className="items-end w-full p-4"
/>
<div className="px-4 text-sm text-default-600"></div>
<Accordion variant="splitted" defaultExpandedKeys={['select']}>
<AccordionItem
key="select"
aria-label="Pick Color"
title="选择主题"
subtitle="可以切换夜间/白昼模式查看对应颜色"
className="shadow-small"
startContent={<IoIosColorPalette />}
>
<div className="flex flex-wrap gap-2">
{themes.map((theme) => (
<PreviewThemeCard
key={theme.name}
theme={theme}
onPreview={() => {
setOnebotValue('theme', theme.theme)
}}
/>
))}
</div>
</AccordionItem>
<AccordionItem
key="pick"
aria-label="Pick Color"
title="自定义配色"
className="shadow-small"
startContent={<FaPaintbrush />}
>
<div className="space-y-2">
{(['dark', 'light'] as const).map((mode) => (
<div
key={mode}
className={clsx(
'p-2 rounded-md',
mode === 'dark' ? 'text-white' : 'text-black',
mode === 'dark'
? 'bg-content1-foreground dark:bg-content1'
: 'bg-content1 dark:bg-content1-foreground'
)}
>
<h3 className="text-center p-2 rounded-md bg-content2 mb-2 text-default-800 flex items-center justify-center">
{mode === 'dark' ? (
<MdDarkMode size={24} />
) : (
<MdLightMode size={24} />
)}
{mode === 'dark' ? '夜间模式主题' : '白昼模式主题'}
</h3>
{colorKeys.map((key) => (
<div
key={key}
className="grid grid-cols-2 items-center mb-2 gap-2"
>
<label className="text-right">{key}</label>
<Controller
control={control}
name={`theme.${mode}.${key}`}
render={({ field: { value, onChange } }) => {
const hslArray = value?.split(' ') ?? [0, 0, 0]
const color = `hsl(${hslArray[0]}, ${hslArray[1]}, ${hslArray[2]})`
return (
<ColorPicker
color={color}
onChange={(result) => {
onChange(
`${result.hsl.h} ${result.hsl.s * 100}% ${result.hsl.l * 100}%`
)
}}
/>
)
}}
/>
</div>
))}
</div>
))}
</div>
</AccordionItem>
</Accordion>
</>
)
}
export default ThemeConfigCard

View File

@@ -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

View File

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

View File

@@ -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"

View File

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

View File

@@ -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-100">
<CardBody>
<Hitokoto />
</CardBody>

View File

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

View File

@@ -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">

View File

@@ -1,6 +1,6 @@
@layer base {
.shiny-text {
@apply text-pink-400 text-opacity-60;
@apply text-primary-400 text-opacity-60;
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
@@ -10,7 +10,7 @@
background-image: linear-gradient(
120deg,
rgba(255, 50, 50, 0) 40%,
rgba(255, 76, 76, 0.8) 50%,
hsl(var(--heroui-primary-400) / 0.8) 50%,
rgba(255, 50, 50, 0) 60%
);
}
@@ -18,11 +18,10 @@
background-image: linear-gradient(
120deg,
rgba(255, 255, 255, 0) 40%,
rgba(206, 21, 21, 0.8) 50%,
hsl(var(--heroui-primary-600) / 0.8) 50%,
rgba(255, 255, 255, 0) 60%
);
}
@keyframes shine {
0% {
background-position: 100%;

View File

@@ -48,3 +48,136 @@ interface SystemStatus {
}
arch: string
}
interface ThemeConfigItem {
'--heroui-background': string
'--heroui-foreground-50': string
'--heroui-foreground-100': string
'--heroui-foreground-200': string
'--heroui-foreground-300': string
'--heroui-foreground-400': string
'--heroui-foreground-500': string
'--heroui-foreground-600': string
'--heroui-foreground-700': string
'--heroui-foreground-800': string
'--heroui-foreground-900': string
'--heroui-foreground': string
'--heroui-focus': string
'--heroui-overlay': string
'--heroui-divider': string
'--heroui-divider-opacity': string
'--heroui-content1': string
'--heroui-content1-foreground': string
'--heroui-content2': string
'--heroui-content2-foreground': string
'--heroui-content3': string
'--heroui-content3-foreground': string
'--heroui-content4': string
'--heroui-content4-foreground': string
'--heroui-default-50': string
'--heroui-default-100': string
'--heroui-default-200': string
'--heroui-default-300': string
'--heroui-default-400': string
'--heroui-default-500': string
'--heroui-default-600': string
'--heroui-default-700': string
'--heroui-default-800': string
'--heroui-default-900': string
'--heroui-default-foreground': string
'--heroui-default': string
// 新增 danger
'--heroui-danger-50': string
'--heroui-danger-100': string
'--heroui-danger-200': string
'--heroui-danger-300': string
'--heroui-danger-400': string
'--heroui-danger-500': string
'--heroui-danger-600': string
'--heroui-danger-700': string
'--heroui-danger-800': string
'--heroui-danger-900': string
'--heroui-danger-foreground': string
'--heroui-danger': string
// 新增 primary
'--heroui-primary-50': string
'--heroui-primary-100': string
'--heroui-primary-200': string
'--heroui-primary-300': string
'--heroui-primary-400': string
'--heroui-primary-500': string
'--heroui-primary-600': string
'--heroui-primary-700': string
'--heroui-primary-800': string
'--heroui-primary-900': string
'--heroui-primary-foreground': string
'--heroui-primary': string
// 新增 secondary
'--heroui-secondary-50': string
'--heroui-secondary-100': string
'--heroui-secondary-200': string
'--heroui-secondary-300': string
'--heroui-secondary-400': string
'--heroui-secondary-500': string
'--heroui-secondary-600': string
'--heroui-secondary-700': string
'--heroui-secondary-800': string
'--heroui-secondary-900': string
'--heroui-secondary-foreground': string
'--heroui-secondary': string
// 新增 success
'--heroui-success-50': string
'--heroui-success-100': string
'--heroui-success-200': string
'--heroui-success-300': string
'--heroui-success-400': string
'--heroui-success-500': string
'--heroui-success-600': string
'--heroui-success-700': string
'--heroui-success-800': string
'--heroui-success-900': string
'--heroui-success-foreground': string
'--heroui-success': string
// 新增 warning
'--heroui-warning-50': string
'--heroui-warning-100': string
'--heroui-warning-200': string
'--heroui-warning-300': string
'--heroui-warning-400': string
'--heroui-warning-500': string
'--heroui-warning-600': string
'--heroui-warning-700': string
'--heroui-warning-800': string
'--heroui-warning-900': string
'--heroui-warning-foreground': string
'--heroui-warning': string
// 其它配置
'--heroui-code-background': string
'--heroui-strong': string
'--heroui-code-mdx': string
'--heroui-divider-weight': string
'--heroui-disabled-opacity': string
'--heroui-font-size-tiny': string
'--heroui-font-size-small': string
'--heroui-font-size-medium': string
'--heroui-font-size-large': string
'--heroui-line-height-tiny': string
'--heroui-line-height-small': string
'--heroui-line-height-medium': string
'--heroui-line-height-large': string
'--heroui-radius-small': string
'--heroui-radius-medium': string
'--heroui-radius-large': string
'--heroui-border-width-small': string
'--heroui-border-width-medium': string
'--heroui-border-width-large': string
'--heroui-box-shadow-small': string
'--heroui-box-shadow-medium': string
'--heroui-box-shadow-large': string
'--heroui-hover-opacity': string
}
interface ThemeConfig {
dark: ThemeConfigItem
light: ThemeConfigItem
}

6
napcat.webui/src/types/theme.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
interface ThemeInfo {
theme: ThemeConfig
name: string
description?: string
author?: string
}

View File

@@ -1,19 +1,21 @@
import { PlayMode } from '@/const/enum'
import WebUIManager from '@/controllers/webui_manager'
import type {
FinalMusic,
Music163ListResponse,
Music163URLResponse
} from '@/types/music'
import WebUIManager from '@/controllers/webui_manager'
/**
* 获取网易云音乐歌单
* @param id 歌单id
* @returns 歌单信息
*/
export const get163MusicList = async (id: string) => {
let res = await WebUIManager.proxy<Music163ListResponse>('https://wavesgame.top/playlist/track/all?id=' + id);
let res = await WebUIManager.proxy<Music163ListResponse>(
'https://wavesgame.top/playlist/track/all?id=' + id
)
// const res = await request.get<Music163ListResponse>(
// `https://wavesgame.top/playlist/track/all?id=${id}`
// )
@@ -71,7 +73,7 @@ export const get163MusicListSongs = async (id: string) => {
if (songURL) {
finalMusic.push({
id: song.id,
url: songURL,
url: songURL.replace(/http:\/\//, '//').replace(/https:\/\//, '//'),
title: song.name,
artist: song.ar.map((p) => p.name).join('/'),
cover: song.al.picUrl

View File

@@ -0,0 +1,141 @@
import { request } from './request'
const style = document.createElement('style')
document.head.appendChild(style)
export function loadTheme() {
request('/files/theme.css?_t=' + Date.now())
.then((res) => res.data)
.then((css) => {
style.innerHTML = css
})
.catch(() => {
console.error('Failed to load theme.css')
})
}
export const colorKeys = [
'--heroui-background',
'--heroui-foreground-50',
'--heroui-foreground-100',
'--heroui-foreground-200',
'--heroui-foreground-300',
'--heroui-foreground-400',
'--heroui-foreground-500',
'--heroui-foreground-600',
'--heroui-foreground-700',
'--heroui-foreground-800',
'--heroui-foreground-900',
'--heroui-foreground',
'--heroui-content1',
'--heroui-content1-foreground',
'--heroui-content2',
'--heroui-content2-foreground',
'--heroui-content3',
'--heroui-content3-foreground',
'--heroui-content4',
'--heroui-content4-foreground',
'--heroui-default-50',
'--heroui-default-100',
'--heroui-default-200',
'--heroui-default-300',
'--heroui-default-400',
'--heroui-default-500',
'--heroui-default-600',
'--heroui-default-700',
'--heroui-default-800',
'--heroui-default-900',
'--heroui-default-foreground',
'--heroui-default',
'--heroui-danger-50',
'--heroui-danger-100',
'--heroui-danger-200',
'--heroui-danger-300',
'--heroui-danger-400',
'--heroui-danger-500',
'--heroui-danger-600',
'--heroui-danger-700',
'--heroui-danger-800',
'--heroui-danger-900',
'--heroui-danger-foreground',
'--heroui-danger',
'--heroui-primary-50',
'--heroui-primary-100',
'--heroui-primary-200',
'--heroui-primary-300',
'--heroui-primary-400',
'--heroui-primary-500',
'--heroui-primary-600',
'--heroui-primary-700',
'--heroui-primary-800',
'--heroui-primary-900',
'--heroui-primary-foreground',
'--heroui-primary',
'--heroui-secondary-50',
'--heroui-secondary-100',
'--heroui-secondary-200',
'--heroui-secondary-300',
'--heroui-secondary-400',
'--heroui-secondary-500',
'--heroui-secondary-600',
'--heroui-secondary-700',
'--heroui-secondary-800',
'--heroui-secondary-900',
'--heroui-secondary-foreground',
'--heroui-secondary',
'--heroui-success-50',
'--heroui-success-100',
'--heroui-success-200',
'--heroui-success-300',
'--heroui-success-400',
'--heroui-success-500',
'--heroui-success-600',
'--heroui-success-700',
'--heroui-success-800',
'--heroui-success-900',
'--heroui-success-foreground',
'--heroui-success',
'--heroui-warning-50',
'--heroui-warning-100',
'--heroui-warning-200',
'--heroui-warning-300',
'--heroui-warning-400',
'--heroui-warning-500',
'--heroui-warning-600',
'--heroui-warning-700',
'--heroui-warning-800',
'--heroui-warning-900',
'--heroui-warning-foreground',
'--heroui-warning',
'--heroui-focus',
'--heroui-overlay',
'--heroui-divider',
'--heroui-code-background',
'--heroui-strong',
'--heroui-code-mdx'
] as const
export const generateTheme = (theme: ThemeConfig, validField?: string) => {
let css = `:root ${validField ? `.${validField}` : ''}, .light ${validField ? `.${validField}` : ''}, [data-theme="light"] ${validField ? `.${validField}` : ''} {`
for (const key in theme.light) {
const _key = key as keyof ThemeConfigItem
css += `${_key}: ${theme.light[_key]};`
}
css += `}`
css += `.dark ${validField ? `.${validField}` : ''}, [data-theme="dark"] ${validField ? `.${validField}` : ''} {`
for (const key in theme.dark) {
const _key = key as keyof ThemeConfigItem
css += `${_key}: ${theme.dark[_key]};`
}
css += `}`
return css
}

View File

@@ -9,9 +9,84 @@ export default {
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'
],
safelist: [
{
pattern:
/bg-(primary|secondary|success|danger|warning|default)-(50|100|200|300|400|500|600|700|800|900)/
}
],
theme: {
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'
}
}
}
}
})
]
}

View File

@@ -34,7 +34,8 @@ export default defineConfig(({ mode }) => {
ws: true,
changeOrigin: true
},
'/api': backendDebugUrl
'/api': backendDebugUrl,
'/files': backendDebugUrl
}
},
build: {

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.5.3",
"version": "4.6.0",
"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",
@@ -32,7 +32,10 @@
"@types/express": "^5.0.0",
"@types/multer": "^1.4.12",
"@types/node": "^22.0.1",
"@types/on-finished": "^2.3.4",
"@types/qrcode-terminal": "^0.12.2",
"@types/react-color": "^3.0.13",
"@types/type-is": "^1.6.7",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
@@ -40,14 +43,14 @@
"async-mutex": "^0.5.0",
"commander": "^13.0.0",
"cors": "^2.8.5",
"esbuild": "0.24.0",
"esbuild": "0.25.0",
"eslint": "^9.14.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"express-rate-limit": "^7.5.0",
"fast-xml-parser": "^4.3.6",
"file-type": "^20.0.0",
"globals": "^15.12.0",
"globals": "^16.0.0",
"image-size": "^1.1.1",
"json5": "^2.2.3",
"multer": "^1.4.5-lts.1",
@@ -56,13 +59,13 @@
"vite": "^6.0.1",
"vite-plugin-cp": "^4.0.8",
"vite-tsconfig-paths": "^5.1.0",
"winston": "^3.17.0"
"napcat.protobuf": "^1.1.3",
"winston": "^3.17.0",
"compressing": "^1.10.1"
},
"dependencies": {
"@ffmpeg.wasm/core-mt": "^0.13.2",
"compressing": "^1.10.1",
"express": "^5.0.0",
"piscina": "^4.7.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
}

View File

@@ -1,9 +1,20 @@
import { encode } from 'silk-wasm';
import { parentPort } from 'worker_threads';
export interface EncodeArgs {
input: ArrayBufferView | ArrayBuffer
sampleRate: number
}
export default async ({ input, sampleRate }: EncodeArgs) => {
export function recvTask<T>(cb: (taskData: T) => Promise<unknown>) {
parentPort?.on('message', async (taskData: T) => {
try {
let ret = await cb(taskData);
parentPort?.postMessage(ret);
} catch (error: unknown) {
parentPort?.postMessage({ error: (error as Error).message });
}
});
}
recvTask<EncodeArgs>(async ({ input, sampleRate }) => {
return await encode(input, sampleRate);
};
});

View File

@@ -1,4 +1,3 @@
import Piscina from 'piscina';
import fsPromise from 'fs/promises';
import path from 'node:path';
import { randomUUID } from 'crypto';
@@ -6,16 +5,16 @@ import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-w
import { LogWrapper } from '@/common/log';
import { EncodeArgs } from '@/common/audio-worker';
import { FFmpegService } from '@/common/ffmpeg';
import { runTask } from './worker';
import { fileURLToPath } from 'node:url';
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
async function getWorkerPath() {
return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
function getWorkerPath() {
//return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'audio-worker.mjs');
}
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
async function guessDuration(pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath);
@@ -46,7 +45,7 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
const { input, sampleRate } = isWav(file)
? await handleWavFile(file, filePath, pcmPath)
: { input: await FFmpegService.convert(filePath, pcmPath), sampleRate: 24000 };
const silk = await piscina.run({ input: input, sampleRate: sampleRate });
const silk = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { input: input, sampleRate: sampleRate });
fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e));
await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);

View File

@@ -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);
}

View File

@@ -5,6 +5,17 @@ import { readFileSync, statSync, writeFileSync } from 'fs';
import type { VideoInfo } from './video';
import { fileTypeFromFile } from 'file-type';
import imageSize from 'image-size';
import { parentPort } from 'worker_threads';
export function recvTask<T>(cb: (taskData: T) => Promise<unknown>) {
parentPort?.on('message', async (taskData: T) => {
try {
let ret = await cb(taskData);
parentPort?.postMessage(ret);
} catch (error: unknown) {
parentPort?.postMessage({ error: (error as Error).message });
}
});
}
class FFmpegService {
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
@@ -137,15 +148,18 @@ interface FFmpegTask {
}
export default async function handleFFmpegTask({ method, args }: FFmpegTask): Promise<any> {
switch (method) {
case 'extractThumbnail':
return await FFmpegService.extractThumbnail(...args as [string, string]);
case 'convertFile':
return await FFmpegService.convertFile(...args as [string, string, string]);
case 'convert':
return await FFmpegService.convert(...args as [string, string]);
case 'getVideoInfo':
return await FFmpegService.getVideoInfo(...args as [string, string]);
default:
throw new Error(`Unknown method: ${method}`);
case 'extractThumbnail':
return await FFmpegService.extractThumbnail(...args as [string, string]);
case 'convertFile':
return await FFmpegService.convertFile(...args as [string, string, string]);
case 'convert':
return await FFmpegService.convert(...args as [string, string]);
case 'getVideoInfo':
return await FFmpegService.getVideoInfo(...args as [string, string]);
default:
throw new Error(`Unknown method: ${method}`);
}
}
}
recvTask<FFmpegTask>(async ({ method, args }: FFmpegTask) => {
return await handleFFmpegTask({ method, args });
});

View File

@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Piscina from 'piscina';
import { VideoInfo } from './video';
import path from 'path';
import { fileURLToPath } from 'url';
import { runTask } from './worker';
type EncodeArgs = {
method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
@@ -9,42 +11,26 @@ type EncodeArgs = {
type EncodeResult = any;
async function getWorkerPath() {
return new URL(/* @vite-ignore */ './ffmpeg-worker.mjs', import.meta.url).href;
function getWorkerPath() {
return path.join(path.dirname(fileURLToPath(import.meta.url)), './ffmpeg-worker.mjs');
}
export class FFmpegService {
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
await piscina.run({ method: 'extractThumbnail', args: [videoPath, thumbnailPath] });
await piscina.destroy();
await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'extractThumbnail', args: [videoPath, thumbnailPath] });
}
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
await piscina.run({ method: 'convertFile', args: [inputFile, outputFile, format] });
await piscina.destroy();
await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'convertFile', args: [inputFile, outputFile, format] });
}
public static async convert(filePath: string, pcmPath: string): Promise<Buffer> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
const result = await piscina.run({ method: 'convert', args: [filePath, pcmPath] });
await piscina.destroy();
const result = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'convert', args: [filePath, pcmPath] });
return result;
}
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
const result = await piscina.run({ method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
await piscina.destroy();
const result = await await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
return result;
}
}

View File

@@ -54,7 +54,7 @@ export class ForwardMsgBuilder {
const id = crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg);
if (!source) {
source = isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录';
source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录');
}
if (!news) {
news = msg.length === 0 ? [{

View File

@@ -232,7 +232,7 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
}
if (msg.senderUin !== '0') {
tokens.push(`[${msg.sendMemberName ?? msg.sendRemarkName ?? msg.sendNickName}(${msg.senderUin})]`);
tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
}
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
tokens.push('移动设备');

View File

@@ -1 +1 @@
export const napCatVersion = '4.5.3';
export const napCatVersion = '4.6.0';

29
src/common/worker.ts Normal file
View File

@@ -0,0 +1,29 @@
import { Worker } from 'worker_threads';
export async function runTask<T, R>(workerScript: string, taskData: T): Promise<R> {
let worker = new Worker(workerScript);
try {
return await new Promise<R>((resolve, reject) => {
worker.on('message', (result: R) => {
resolve(result);
});
worker.on('error', (error) => {
reject(new Error(`Worker error: ${error.message}`));
});
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
worker.postMessage(taskData);
});
} catch (error: unknown) {
throw new Error(`Failed to run task: ${(error as Error).message}`);
} finally {
// Ensure the worker is terminated after the promise is settled
worker.terminate();
}
}

View File

@@ -41,9 +41,10 @@ export class NTQQFileApi {
this.context = context;
this.core = core;
this.rkeyManager = new RkeyManager([
'https://rkey.napneko.icu/rkeys'
'https://ss.xingzhige.com/music_card/rkey', // 国内
'https://rkey.napneko.icu/rkeys' // Cloudflare
],
this.context.logger
this.context.logger
);
}
@@ -300,18 +301,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 +435,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();
}

View File

@@ -165,7 +165,13 @@ export class NTQQGroupApi {
return this.groupMemberCache.get(groupCode);
}
async refreshGroupMemberCachePartial(groupCode: string, uid: string) {
const member = await this.getGroupMemberEx(groupCode, uid, true);
if (member) {
this.groupMemberCache.get(groupCode)?.set(uid, member);
}
return member;
}
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString();
const memberUinOrUidStr = memberUinOrUid.toString();

View File

@@ -1,5 +1,5 @@
import { ChatType, GetFileListParam, Peer, RawMessage, SendMessageElement, SendStatusType } from '@/core/types';
import { GroupFileInfoUpdateItem, InstanceContext, NapCatCore } from '@/core';
import { GroupFileInfoUpdateItem, InstanceContext, NapCatCore, NodeIKernelMsgService } from '@/core';
import { GeneralCallResult } from '@/core/services/common';
export class NTQQMsgApi {
@@ -12,6 +12,11 @@ export class NTQQMsgApi {
this.context = context;
this.core = core;
}
async clickInlineKeyboardButton(...params: Parameters<NodeIKernelMsgService['clickInlineKeyboardButton']>) {
return this.context.session.getMsgService().clickInlineKeyboardButton(...params);
}
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
// https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType 可以用过特殊方式拉取
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);

View File

@@ -186,5 +186,17 @@
"9.9.17-31363": {
"appid": 537266500,
"qua": "V1_WIN_NQ_9.9.17_31363_GW_B"
},
"3.2.16-32690": {
"appid": 537271229,
"qua": "V1_LNX_NQ_3.2.16_32690_GW_B"
},
"9.9.18-32690": {
"appid": 537271194,
"qua": "V1_WIN_NQ_9.9.18_32690_GW_B"
},
"6.9.66-32690": {
"appid": 537271218,
"qua": "V1_MAC_NQ_6.9.66_32690_GW_B"
}
}
}

View File

@@ -175,7 +175,7 @@
"send": "713A318",
"recv": "713DB50"
},
"6.9.63.30851-x64": {
"6.9.63-30851-x64": {
"send": "46C8040",
"recv": "46CA8AC"
},
@@ -195,7 +195,7 @@
"send": "713A318",
"recv": "713DB50"
},
"6.9.63.30899-x64": {
"6.9.63-30899-x64": {
"send": "46C8040",
"recv": "46CA8AC"
},
@@ -211,7 +211,7 @@
"send": "39C1350",
"recv": "39C5784"
},
"6.9.63.31245-x64": {
"6.9.63-31245-x64": {
"send": "4720A40",
"recv": "47232AC"
},
@@ -239,12 +239,24 @@
"send": "71BFD48",
"recv": "71C3580"
},
"6.9.65.31363-x64": {
"6.9.65-31363-x64": {
"send": "4720E80",
"recv": "47236EC"
},
"6.9.65.31363-arm64": {
"6.9.65-31363-arm64": {
"send": "422CEF8",
"recv": "422F710"
},
"9.9.18-32690-x64": {
"send": "39F9630",
"recv": "39FDE30"
},
"3.2.16-32690-x64": {
"send": "A5E24C0",
"recv": "A5E5EE0"
},
"3.2.16-32690-arm64": {
"send": "7226630",
"recv": "7229F60"
}
}
}

View File

@@ -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>;

View File

@@ -11,7 +11,7 @@ export class NodeIKernelSessionListener {
}
onOpentelemetryInit(args: unknown): any {
onOpentelemetryInit(info: { is_init: boolean, is_report: boolean }): any {
}

View File

@@ -8,13 +8,15 @@ import {
PacketMsgPttElement,
PacketMsgVideoElement
} from '@/core/packet/message/element';
import { ChatType } from '@/core';
import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/core';
import { MiniAppRawData, MiniAppReqParams } from '@/core/packet/entities/miniApp';
import { AIVoiceChatType } from '@/core/packet/entities/aiChat';
import { NapProtoDecodeStructType, NapProtoEncodeStructType } from '@napneko/nap-proto-core';
import { IndexNode, MsgInfo } from '@/core/packet/transformer/proto';
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;
@@ -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);
@@ -151,6 +160,12 @@ export class PacketOperationContext {
const res = trans.DownloadGroupFile.parse(resp);
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
}
async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string) {
const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadPrivateFile.parse(resp);
return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`;
}
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupPtt.build(groupUin, node);
@@ -195,4 +210,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);
}
}

View File

@@ -144,7 +144,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
@@ -181,7 +181,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
@@ -219,7 +219,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
@@ -244,16 +244,16 @@ export class PacketHighwayContext {
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
}
const subFile = preRespData.upload.subFileInfos[0];
if (subFile.uKey && subFile.uKey != '') {
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
if (subFile!.uKey && subFile!.uKey != '') {
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile!.uKey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[1]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: subFile.uKey,
uKey: subFile!.uKey,
network: {
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S)
ipv4S: oidbIpv4s2HighwayIpv4s(subFile!.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
@@ -269,7 +269,7 @@ export class PacketHighwayContext {
extend
);
} else {
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile!.uKey}, don't need upload!`);
}
video.msgInfo = preRespData.upload.msgInfo;
}
@@ -284,7 +284,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
@@ -309,16 +309,16 @@ export class PacketHighwayContext {
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
}
const subFile = preRespData.upload.subFileInfos[0];
if (subFile.uKey && subFile.uKey != '') {
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
if (subFile!.uKey && subFile!.uKey != '') {
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile!.uKey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[1]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: subFile.uKey,
uKey: subFile!.uKey,
network: {
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S)
ipv4S: oidbIpv4s2HighwayIpv4s(subFile!.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
@@ -334,7 +334,7 @@ export class PacketHighwayContext {
extend
);
} else {
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile!.uKey}, don't need upload!`);
}
video.msgInfo = preRespData.upload.msgInfo;
}
@@ -347,7 +347,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadGroupPttReq get upload ptt ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
@@ -383,7 +383,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadC2CPttReq get upload ptt ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({

View File

@@ -1,18 +1,18 @@
import {
Peer,
ChatType,
ElementType,
MessageElement,
Peer,
RawMessage,
SendArkElement,
SendFaceElement,
SendFileElement,
SendMarkdownElement,
SendMarketFaceElement,
SendMultiForwardMsgElement,
SendPicElement,
SendPttElement,
SendReplyElement,
SendStructLongMsgElement,
SendTextElement,
SendVideoElement
} from '@/core';
@@ -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,
@@ -44,7 +46,7 @@ const SupportedElementTypes = [
ElementType.PTT,
ElementType.ARK,
ElementType.MARKDOWN,
ElementType.STRUCTLONGMSG
ElementType.MULTIFORWARD
];
type SendMessageTypeElementMap = {
@@ -57,7 +59,7 @@ type SendMessageTypeElementMap = {
[ElementType.REPLY]: SendReplyElement,
[ElementType.ARK]: SendArkElement,
[ElementType.MFACE]: SendMarketFaceElement,
[ElementType.STRUCTLONGMSG]: SendStructLongMsgElement,
[ElementType.MULTIFORWARD]: SendMultiForwardMsgElement,
[ElementType.MARKDOWN]: SendMarkdownElement,
};
@@ -116,9 +118,8 @@ export class PacketMsgConverter {
[ElementType.MARKDOWN]: (element) => {
return new PacketMsgMarkDownElement(element as SendMarkdownElement);
},
// TODO: check this logic, move it in arkElement?
[ElementType.STRUCTLONGMSG]: (element) => {
return new PacketMultiMsgElement(element as SendStructLongMsgElement);
[ElementType.MULTIFORWARD]: (element) => {
return new PacketMultiMsgElement(element as SendMultiForwardMsgElement);
}
};
@@ -154,4 +155,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);
}
}

View File

@@ -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,
@@ -25,12 +27,15 @@ import {
SendPicElement,
SendPttElement,
SendReplyElement,
SendStructLongMsgElement,
SendMultiForwardMsgElement,
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;
}
@@ -518,13 +661,13 @@ export class PacketMsgMarkDownElement extends IPacketMsgElement<SendMarkdownElem
}
}
export class PacketMultiMsgElement extends IPacketMsgElement<SendStructLongMsgElement> {
export class PacketMultiMsgElement extends IPacketMsgElement<SendMultiForwardMsgElement> {
resid: string;
message: PacketMsg[];
constructor(rawElement: SendStructLongMsgElement, message?: PacketMsg[]) {
constructor(rawElement: SendMultiForwardMsgElement, message?: PacketMsg[]) {
super(rawElement);
this.resid = rawElement.structLongMsgElement.resId;
this.resid = rawElement.multiForwardMsgElement.resId;
this.message = message ?? [];
}

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