Compare commits

...

100 Commits

Author SHA1 Message Date
手瓜一十雪
03dafe727e fix: win 2025-03-06 18:51:05 +08:00
Mlikiowa
744921c45e release: v4.6.5 2025-03-06 10:09:45 +00:00
手瓜一十雪
abc4a4dcba feat: 32793 2025-03-06 18:09:14 +08:00
Mlikiowa
7e0da2f929 release: v4.6.4 2025-03-05 13:15:11 +00:00
手瓜一十雪
a3b70d0f1f fix 2025-03-05 21:14:52 +08:00
Mlikiowa
d291724f06 release: v4.6.3 2025-03-03 09:17:03 +00:00
手瓜一十雪
122a9ca2cc feat: o3拦截 2025-03-03 17:16:36 +08:00
手瓜一十雪
48aaddd32b feat:rkey 2025-03-03 12:28:55 +08:00
手瓜一十雪
47401af856 feat: searchMsgWithKeywords 2025-03-02 16:07:27 +08:00
Mlikiowa
709adfd812 release: v4.6.2 2025-03-02 07:11:16 +00:00
手瓜一十雪
038d0c5412 fix: #785 2025-03-02 14:55:47 +08:00
手瓜一十雪
6bb4362ed4 feat: 32721 2025-03-02 14:36:11 +08:00
手瓜一十雪
e617f9452d fix: #841 2025-03-02 14:32:21 +08:00
手瓜一十雪
6d8bb49a37 fix: #837 2025-03-02 14:27:09 +08:00
手瓜一十雪
4f6073ee86 fix: 837 2025-03-02 14:26:28 +08:00
手瓜一十雪
2e7176304b fix: #843 2025-03-02 14:24:51 +08:00
Mlikiowa
e36cf11004 release: v4.6.1 2025-02-27 08:35:02 +00:00
手瓜一十雪
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
97 changed files with 2702 additions and 465 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 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**

Binary file not shown.

View File

@@ -1,10 +1,9 @@
{ {
"name": "qq-chat", "name": "qq-chat",
"version": "9.9.17-30899", "version": "9.9.18-32793",
"verHash": "ececf273", "verHash": "d43f097e",
"linuxVersion": "3.2.15-30899", "linuxVersion": "3.2.16-32793",
"linuxVerHash": "63c751e8", "linuxVerHash": "ee4bd910",
"type": "module",
"private": true, "private": true,
"description": "QQ", "description": "QQ",
"productName": "QQ", "productName": "QQ",
@@ -17,10 +16,27 @@
"bin": { "bin": {
"qd": "externals/devtools/cli/index.js" "qd": "externals/devtools/cli/index.js"
}, },
"appid": {
"win32": "537258389",
"darwin": "537258412",
"linux": "537258424"
},
"main": "./loadNapCat.js", "main": "./loadNapCat.js",
"buildVersion": "30899", "peerDependenciesMeta": {
"*": {
"optional": true
}
},
"pnpm": {
"patchedDependencies": {
"@vue/runtime-dom@3.5.12": "patches/@vue__runtime-dom@3.5.12.patch",
"@swc/helpers@0.5.3": "patches/@swc__helpers@0.5.3.patch",
"vuex@4.1.0": "patches/vuex@4.1.0.patch"
}
},
"buildVersion": "32793",
"isPureShell": true, "isPureShell": true,
"isByteCodeShell": true, "isByteCodeShell": true,
"platform": "win32", "platform": "win32",
"eleArch": "x64" "eleArch": "x64"
} }

View File

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

View File

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

@@ -1,4 +1,5 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button'
import clsx from 'clsx'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { IoMdRefresh } from 'react-icons/io' import { IoMdRefresh } from 'react-icons/io'
@@ -7,15 +8,22 @@ export interface SaveButtonsProps {
reset: () => void reset: () => void
refresh?: () => void refresh?: () => void
isSubmitting: boolean isSubmitting: boolean
className?: string
} }
const SaveButtons: React.FC<SaveButtonsProps> = ({ const SaveButtons: React.FC<SaveButtonsProps> = ({
onSubmit, onSubmit,
reset, reset,
isSubmitting, 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"> <div className="flex items-center justify-center gap-2 mt-5">
<Button <Button
color="default" color="default"

View File

@@ -58,14 +58,13 @@ const renderItems = (items: MenuItem[], children = false) => {
color="primary" color="primary"
endContent={ endContent={
canOpen ? ( canOpen ? (
// div实现箭头V效果
<div <div
className={clsx( className={clsx(
'ml-auto relative w-3 h-3 transition-transform', 'ml-auto relative w-3 h-3 transition-transform',
open && 'transform rotate-180', open && 'transform rotate-180',
isActive isActive
? 'text-primary-500' ? 'text-primary-500'
: 'text-red-300 dark:text-white', : 'text-primary-200 dark:text-white',
'before:rounded-full', 'before:rounded-full',
'before:content-[""]', 'before:content-[""]',
'before:block', 'before:block',
@@ -98,7 +97,7 @@ const renderItems = (items: MenuItem[], children = false) => {
'w-3 h-1.5 rounded-full ml-auto shadow-lg', 'w-3 h-1.5 rounded-full ml-auto shadow-lg',
isActive isActive
? 'bg-primary-500 animate-spinner-ease-spin' ? 'bg-primary-500 animate-spinner-ease-spin'
: 'bg-red-300 dark:bg-white' : 'bg-primary-200 dark:bg-white'
)} )}
/> />
) )

View File

@@ -34,7 +34,7 @@ const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
endContent endContent
}) => { }) => {
return ( return (
<div className="flex text-sm gap-1 p-2 items-center shadow-sm shadow-primary-50 dark:shadow-primary-100 rounded text-primary-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} {icon}
<div className="w-24">{title}</div> <div className="w-24">{title}</div>
<div className="text-primary-200">{value}</div> <div className="text-primary-200">{value}</div>
@@ -234,7 +234,7 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
error: qqVersionError error: qqVersionError
} = useRequest(WebUIManager.getQQVersion) } = useRequest(WebUIManager.getQQVersion)
return ( return (
<Card className="bg-opacity-60 shadow-sm shadow-primary-50 dark:shadow-primary-100 overflow-visible flex-1"> <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"> <CardHeader className="pb-0 items-center gap-1 text-primary-500 font-extrabold">
<FaCircleInfo className="text-lg" /> <FaCircleInfo className="text-lg" />
<span></span> <span></span>

View File

@@ -24,7 +24,7 @@ const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
return ( return (
<div <div
className={clsx( 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' size === 'lg' ? 'col-span-2' : 'col-span-1 flex justify-between'
)} )}
> >
@@ -55,7 +55,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
} }
return ( return (
<Card className="bg-opacity-60 shadow-sm shadow-primary-50 dark:shadow-primary-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"> <div className="absolute h-full right-0 top-0">
<Image <Image
src={bkg} src={bkg}

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

@@ -73,4 +73,17 @@ export default class QQManager {
) )
return data.data.data 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 message: string
} }
export interface TerminalSession {
id: string
}
export interface TerminalInfo {
id: string
}
export default class WebUIManager { export default class WebUIManager {
public static async checkWebUiLogined() { public static async checkWebUiLogined() {
const { data } = const { data } =
@@ -40,6 +32,13 @@ export default class WebUIManager {
return data.data 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 = '') { public static async proxy<T>(url = '') {
const data = await serverRequest.get<ServerResponse<string>>( const data = await serverRequest.get<ServerResponse<string>>(
'/base/proxy?url=' + encodeURIComponent(url) '/base/proxy?url=' + encodeURIComponent(url)
@@ -60,6 +59,20 @@ export default class WebUIManager {
return data.data 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() { public static async getLogList() {
const { data } = const { data } =
await serverRequest.get<ServerResponse<string[]>>('/Log/GetLogList') await serverRequest.get<ServerResponse<string[]>>('/Log/GetLogList')

View File

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

View File

@@ -1,20 +1,36 @@
import { Card, CardBody } from '@heroui/card' import { Card, CardBody } from '@heroui/card'
import { Tab, Tabs } from '@heroui/tabs' import { Tab, Tabs } from '@heroui/tabs'
import clsx from 'clsx'
import { useMediaQuery } from 'react-responsive' import { useMediaQuery } from 'react-responsive'
import { useNavigate, useSearchParams } from 'react-router-dom'
import ChangePasswordCard from './change_password' import ChangePasswordCard from './change_password'
import LoginConfigCard from './login'
import OneBotConfigCard from './onebot' import OneBotConfigCard from './onebot'
import ThemeConfigCard from './theme'
import WebUIConfigCard from './webui' import WebUIConfigCard from './webui'
export interface ConfigPageProps { export interface ConfigPageProps {
children?: React.ReactNode children?: React.ReactNode
size?: 'sm' | 'md' | 'lg'
} }
const ConfingPageItem: React.FC<ConfigPageProps> = ({ children }) => { const ConfingPageItem: React.FC<ConfigPageProps> = ({
children,
size = 'md'
}) => {
return ( return (
<Card className="bg-opacity-50 backdrop-blur-sm"> <Card className="bg-opacity-50 backdrop-blur-sm">
<CardBody className="items-center py-5"> <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> </CardBody>
</Card> </Card>
) )
@@ -22,6 +38,11 @@ const ConfingPageItem: React.FC<ConfigPageProps> = ({ children }) => {
export default function ConfigPage() { export default function ConfigPage() {
const isMediumUp = useMediaQuery({ minWidth: 768 }) const isMediumUp = useMediaQuery({ minWidth: 768 })
const navigate = useNavigate()
const search = useSearchParams({
tab: 'onebot'
})[0]
const tab = search.get('tab') ?? 'onebot'
return ( return (
<section className="w-[1000px] max-w-full md:mx-auto gap-4 py-8 px-2 md:py-10"> <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 fullWidth
className="w-full" className="w-full"
isVertical={isMediumUp} isVertical={isMediumUp}
selectedKey={tab}
onSelectionChange={(key) => {
navigate(`/config?tab=${key}`)
}}
classNames={{ classNames={{
tabList: 'sticky flex top-14 bg-opacity-50 backdrop-blur-sm', tabList: 'sticky flex top-14 bg-opacity-50 backdrop-blur-sm',
panel: 'w-full relative', panel: 'w-full relative',
@@ -47,12 +72,22 @@ export default function ConfigPage() {
<WebUIConfigCard /> <WebUIConfigCard />
</ConfingPageItem> </ConfingPageItem>
</Tab> </Tab>
<Tab title="登录配置" key="login">
<ConfingPageItem>
<LoginConfigCard />
</ConfingPageItem>
</Tab>
<Tab title="修改密码" key="token"> <Tab title="修改密码" key="token">
<ConfingPageItem> <ConfingPageItem>
<ChangePasswordCard /> <ChangePasswordCard />
</ConfingPageItem> </ConfingPageItem>
</Tab> </Tab>
<Tab title="主题配置" key="theme">
<ConfingPageItem size="lg">
<ThemeConfigCard />
</ConfingPageItem>
</Tab>
</Tabs> </Tabs>
</section> </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) setOnebotValue('parseMultMsg', config.parseMultMsg)
} }
const onSubmit = handleOnebotSubmit((data) => { const onSubmit = handleOnebotSubmit(async (data) => {
try { try {
saveConfigWithoutNetwork(data) await saveConfigWithoutNetwork(data)
toast.success('保存成功') toast.success('保存成功')
} catch (error) { } catch (error) {
const msg = (error as Error).message 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

@@ -81,6 +81,10 @@ const WebUIConfigCard = () => {
onDelete={async () => { onDelete={async () => {
try { try {
await FileManager.deleteWebUIFont() await FileManager.deleteWebUIFont()
toast.success('删除成功')
setTimeout(() => {
window.location.reload()
}, 1000)
} catch (error) { } catch (error) {
toast.error('删除失败: ' + (error as Error).message) toast.error('删除失败: ' + (error as Error).message)
} }

View File

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

View File

@@ -1,14 +1,46 @@
import { Spinner } from '@heroui/spinner' import { Spinner } from '@heroui/spinner'
import { AnimatePresence, motion } from 'motion/react' import { AnimatePresence, motion } from 'motion/react'
import { Suspense } from 'react' import { Suspense, useEffect } from 'react'
import { Outlet, useLocation } from 'react-router-dom' 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' 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() { export default function IndexPage() {
const location = useLocation() const location = useLocation()
return ( return (
<DefaultLayout> <DefaultLayout>
<CheckDefaultPassword />
<Suspense <Suspense
fallback={ fallback={
<div className="flex justify-center px-10"> <div className="flex justify-center px-10">

View File

@@ -1,6 +1,6 @@
@layer base { @layer base {
.shiny-text { .shiny-text {
@apply text-pink-400 text-opacity-60; @apply text-primary-400 text-opacity-60;
background-size: 200% 100%; background-size: 200% 100%;
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
@@ -10,7 +10,7 @@
background-image: linear-gradient( background-image: linear-gradient(
120deg, 120deg,
rgba(255, 50, 50, 0) 40%, 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% rgba(255, 50, 50, 0) 60%
); );
} }
@@ -18,11 +18,10 @@
background-image: linear-gradient( background-image: linear-gradient(
120deg, 120deg,
rgba(255, 255, 255, 0) 40%, 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% rgba(255, 255, 255, 0) 60%
); );
} }
@keyframes shine { @keyframes shine {
0% { 0% {
background-position: 100%; background-position: 100%;

View File

@@ -48,3 +48,136 @@ interface SystemStatus {
} }
arch: string 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 { PlayMode } from '@/const/enum'
import WebUIManager from '@/controllers/webui_manager'
import type { import type {
FinalMusic, FinalMusic,
Music163ListResponse, Music163ListResponse,
Music163URLResponse Music163URLResponse
} from '@/types/music' } from '@/types/music'
import WebUIManager from '@/controllers/webui_manager'
/** /**
* 获取网易云音乐歌单 * 获取网易云音乐歌单
* @param id 歌单id * @param id 歌单id
* @returns 歌单信息 * @returns 歌单信息
*/ */
export const get163MusicList = async (id: string) => { 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>( // const res = await request.get<Music163ListResponse>(
// `https://wavesgame.top/playlist/track/all?id=${id}` // `https://wavesgame.top/playlist/track/all?id=${id}`
// ) // )
@@ -71,7 +73,7 @@ export const get163MusicListSongs = async (id: string) => {
if (songURL) { if (songURL) {
finalMusic.push({ finalMusic.push({
id: song.id, id: song.id,
url: songURL, url: songURL.replace(/http:\/\//, '//').replace(/https:\/\//, '//'),
title: song.name, title: song.name,
artist: song.ar.map((p) => p.name).join('/'), artist: song.ar.map((p) => p.name).join('/'),
cover: song.al.picUrl 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,6 +9,12 @@ export default {
'./src/components/**/*.{js,ts,jsx,tsx,mdx}', './src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}' './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: { theme: {
extend: {} extend: {}
}, },

View File

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

View File

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

View File

@@ -1,9 +1,20 @@
import { encode } from 'silk-wasm'; import { encode } from 'silk-wasm';
import { parentPort } from 'worker_threads';
export interface EncodeArgs { export interface EncodeArgs {
input: ArrayBufferView | ArrayBuffer input: ArrayBufferView | ArrayBuffer
sampleRate: number 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); return await encode(input, sampleRate);
}; });

View File

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

View File

@@ -5,6 +5,17 @@ import { readFileSync, statSync, writeFileSync } from 'fs';
import type { VideoInfo } from './video'; import type { VideoInfo } from './video';
import { fileTypeFromFile } from 'file-type'; import { fileTypeFromFile } from 'file-type';
import imageSize from 'image-size'; 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 { class FFmpegService {
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> { public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }); 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> { export default async function handleFFmpegTask({ method, args }: FFmpegTask): Promise<any> {
switch (method) { switch (method) {
case 'extractThumbnail': case 'extractThumbnail':
return await FFmpegService.extractThumbnail(...args as [string, string]); return await FFmpegService.extractThumbnail(...args as [string, string]);
case 'convertFile': case 'convertFile':
return await FFmpegService.convertFile(...args as [string, string, string]); return await FFmpegService.convertFile(...args as [string, string, string]);
case 'convert': case 'convert':
return await FFmpegService.convert(...args as [string, string]); return await FFmpegService.convert(...args as [string, string]);
case 'getVideoInfo': case 'getVideoInfo':
return await FFmpegService.getVideoInfo(...args as [string, string]); return await FFmpegService.getVideoInfo(...args as [string, string]);
default: default:
throw new Error(`Unknown method: ${method}`); 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 */ /* eslint-disable @typescript-eslint/no-explicit-any */
import Piscina from 'piscina';
import { VideoInfo } from './video'; import { VideoInfo } from './video';
import path from 'path';
import { fileURLToPath } from 'url';
import { runTask } from './worker';
type EncodeArgs = { type EncodeArgs = {
method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo'; method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
@@ -9,42 +11,26 @@ type EncodeArgs = {
type EncodeResult = any; type EncodeResult = any;
async function getWorkerPath() { function getWorkerPath() {
return new URL(/* @vite-ignore */ './ffmpeg-worker.mjs', import.meta.url).href; return path.join(path.dirname(fileURLToPath(import.meta.url)), './ffmpeg-worker.mjs');
} }
export class FFmpegService { export class FFmpegService {
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> { public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({ await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'extractThumbnail', args: [videoPath, thumbnailPath] });
filename: await getWorkerPath(),
});
await piscina.run({ method: 'extractThumbnail', args: [videoPath, thumbnailPath] });
await piscina.destroy();
} }
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> { public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({ await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'convertFile', args: [inputFile, outputFile, format] });
filename: await getWorkerPath(),
});
await piscina.run({ method: 'convertFile', args: [inputFile, outputFile, format] });
await piscina.destroy();
} }
public static async convert(filePath: string, pcmPath: string): Promise<Buffer> { public static async convert(filePath: string, pcmPath: string): Promise<Buffer> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({ const result = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'convert', args: [filePath, pcmPath] });
filename: await getWorkerPath(),
});
const result = await piscina.run({ method: 'convert', args: [filePath, pcmPath] });
await piscina.destroy();
return result; return result;
} }
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> { public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({ const result = await await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
filename: await getWorkerPath(),
});
const result = await piscina.run({ method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
await piscina.destroy();
return result; return result;
} }
} }

View File

@@ -54,7 +54,7 @@ export class ForwardMsgBuilder {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg); const isGroupMsg = msg.some(m => m.isGroupMsg);
if (!source) { 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) { if (!news) {
news = msg.length === 0 ? [{ news = msg.length === 0 ? [{

View File

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

View File

@@ -163,7 +163,7 @@ class Store {
const current = this.get<StoreValueType>(key); const current = this.get<StoreValueType>(key);
if (current === null) { if (current === null) {
this.set(key, 1); this.set(key, 1, 60);
return 1; return 1;
} }
@@ -180,7 +180,7 @@ class Store {
} }
const newValue = numericValue + 1; const newValue = numericValue + 1;
this.set(key, newValue); this.set(key, newValue, 60);
return newValue; return newValue;
} }
} }

View File

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

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,7 +41,8 @@ export class NTQQFileApi {
this.context = context; this.context = context;
this.core = core; this.core = core;
this.rkeyManager = new RkeyManager([ this.rkeyManager = new RkeyManager([
'https://rkey.napneko.icu/rkeys' 'https://ss.xingzhige.com/music_card/rkey', // 国内
'https://secret-service.bietiaop.com/rkeys',//国内
], ],
this.context.logger this.context.logger
); );
@@ -434,9 +435,9 @@ export class NTQQFileApi {
}; };
try { try {
if (this.core.apis.PacketApi.available && this.packetRkey?.[0] && this.packetRkey?.[1]) { if (this.core.apis.PacketApi.available) {
const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000; 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].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) { if (rkey_expired_private || rkey_expired_group) {
this.packetRkey = await this.fetchRkeyWithRetry(); this.packetRkey = await this.fetchRkeyWithRetry();
} }

View File

@@ -165,7 +165,13 @@ export class NTQQGroupApi {
return this.groupMemberCache.get(groupCode); 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) { async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString(); const groupCodeStr = groupCode.toString();
const memberUinOrUidStr = memberUinOrUid.toString(); const memberUinOrUidStr = memberUinOrUid.toString();

View File

@@ -1,5 +1,5 @@
import { ChatType, GetFileListParam, Peer, RawMessage, SendMessageElement, SendStatusType } from '@/core/types'; 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'; import { GeneralCallResult } from '@/core/services/common';
export class NTQQMsgApi { export class NTQQMsgApi {
@@ -12,6 +12,11 @@ export class NTQQMsgApi {
this.context = context; this.context = context;
this.core = core; this.core = core;
} }
async clickInlineKeyboardButton(...params: Parameters<NodeIKernelMsgService['clickInlineKeyboardButton']>) {
return this.context.session.getMsgService().clickInlineKeyboardButton(...params);
}
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) { getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
// https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType 可以用过特殊方式拉取 // https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType 可以用过特殊方式拉取
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime); return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
@@ -131,6 +136,20 @@ export class NTQQMsgApi {
}); });
} }
async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) {
console.log(peer, SendersUid);
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: SendersUid,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: true,
isIncludeCurrent: true,
pageLimit: 20000,
});
}
async setMsgRead(peer: Peer) { async setMsgRead(peer: Peer) {
return this.context.session.getMsgService().setMsgRead(peer); return this.context.session.getMsgService().setMsgRead(peer);
} }

View File

@@ -186,5 +186,29 @@
"9.9.17-31363": { "9.9.17-31363": {
"appid": 537266500, "appid": 537266500,
"qua": "V1_WIN_NQ_9.9.17_31363_GW_B" "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"
},
"3.2.16-32721": {
"appid": 537271229,
"qua": "V1_LNX_NQ_3.2.16_32721_GW_B"
},
"9.9.18-32793": {
"appid": 537271244,
"qua": "V1_WIN_NQ_9.9.18_32793_GW_B"
},
"3.2.16-32793": {
"appid": 537271279,
"qua": "V1_LNX_NQ_3.2.16_32793_GW_B"
} }
} }

View File

@@ -4,5 +4,6 @@
"fileLogLevel": "debug", "fileLogLevel": "debug",
"consoleLogLevel": "info", "consoleLogLevel": "info",
"packetBackend": "auto", "packetBackend": "auto",
"packetServer": "" "packetServer": "",
} "o3HookMode": 1
}

View File

@@ -175,7 +175,7 @@
"send": "713A318", "send": "713A318",
"recv": "713DB50" "recv": "713DB50"
}, },
"6.9.63.30851-x64": { "6.9.63-30851-x64": {
"send": "46C8040", "send": "46C8040",
"recv": "46CA8AC" "recv": "46CA8AC"
}, },
@@ -195,7 +195,7 @@
"send": "713A318", "send": "713A318",
"recv": "713DB50" "recv": "713DB50"
}, },
"6.9.63.30899-x64": { "6.9.63-30899-x64": {
"send": "46C8040", "send": "46C8040",
"recv": "46CA8AC" "recv": "46CA8AC"
}, },
@@ -211,7 +211,7 @@
"send": "39C1350", "send": "39C1350",
"recv": "39C5784" "recv": "39C5784"
}, },
"6.9.63.31245-x64": { "6.9.63-31245-x64": {
"send": "4720A40", "send": "4720A40",
"recv": "47232AC" "recv": "47232AC"
}, },
@@ -239,12 +239,44 @@
"send": "71BFD48", "send": "71BFD48",
"recv": "71C3580" "recv": "71C3580"
}, },
"6.9.65.31363-x64": { "6.9.65-31363-x64": {
"send": "4720E80", "send": "4720E80",
"recv": "47236EC" "recv": "47236EC"
}, },
"6.9.65.31363-arm64": { "6.9.65-31363-arm64": {
"send": "422CEF8", "send": "422CEF8",
"recv": "422F710" "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"
},
"3.2.16-32721-x64": {
"send": "A5E24C0",
"recv": "A5E5EE0"
},
"3.2.16-32721-arm64": {
"send": "7226630",
"recv": "7229F60"
},
"9.9.18-32793-x64": {
"send": "39F9A30",
"recv": "39FE230"
},
"3.2.16-32793-x64": {
"send": "A5E24C0",
"recv": "A5E5EE0"
},
"3.2.16-32793-arm64": {
"send": "7226630",
"recv": "7229F60"
} }
} }

View File

@@ -9,7 +9,8 @@ export const NapcatConfigSchema = Type.Object({
fileLogLevel: Type.String({ default: 'debug' }), fileLogLevel: Type.String({ default: 'debug' }),
consoleLogLevel: Type.String({ default: 'info' }), consoleLogLevel: Type.String({ default: 'info' }),
packetBackend: Type.String({ default: 'auto' }), packetBackend: Type.String({ default: 'auto' }),
packetServer: Type.String({ default: '' }) packetServer: Type.String({ default: '' }),
o3HookMode: Type.Number({ default: 0 }),
}); });
export type NapcatConfig = Static<typeof NapcatConfigSchema>; export type NapcatConfig = Static<typeof NapcatConfigSchema>;

View File

@@ -1,4 +1,4 @@
import { ChatType } from '@/core'; import { ChatType, RawMessage } from '@/core';
export interface SearchGroupInfo { export interface SearchGroupInfo {
groupCode: string; groupCode: string;
ownerUid: string; ownerUid: string;
@@ -56,7 +56,7 @@ export interface GroupSearchResult {
nextPos: number; nextPos: number;
} }
export interface NodeIKernelSearchListener { export interface NodeIKernelSearchListener {
onSearchGroupResult(params: GroupSearchResult): any; onSearchGroupResult(params: GroupSearchResult): any;
onSearchFileKeywordsResult(params: { onSearchFileKeywordsResult(params: {
@@ -94,4 +94,27 @@ export interface NodeIKernelSearchListener {
}[] }[]
}[] }[]
}): any; }): any;
onSearchMsgKeywordsResult(params: {
searchId: string,
hasMore: boolean,
resultItems: Array<{
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderUin: string,
senderNick: string,
senderNickHits: unknown[],
senderRemark: string,
senderRemarkHits: unknown[],
senderCard: string,
senderCardHits: unknown[],
fieldType: number,
fieldText: string,
msgRecord: RawMessage;
hitsInfo: Array<unknown>,
msgAbstract: unknown,
}>
}): void | Promise<void>;
} }

View File

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

View File

@@ -11,7 +11,7 @@ import { PacketLogger } from '@/core/packet/context/loggerContext';
// 0 send 1 recv // 0 send 1 recv
export interface NativePacketExportType { export interface NativePacketExportType {
InitHook?: (send: string, recv: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void) => boolean; InitHook?: (send: string, recv: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void, o3_hook: boolean) => boolean;
SendPacket?: (cmd: string, data: string, trace_id: string) => void; SendPacket?: (cmd: string, data: string, trace_id: string) => void;
} }
@@ -42,6 +42,7 @@ export class NativePacketClient extends IPacketClient {
const platform = process.platform + '.' + process.arch; const platform = process.platform + '.' + process.arch;
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node'); const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node');
process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY); process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY);
this.MoeHooExport.exports.InitHook?.(send, recv, (type: number, uin: string, cmd: string, seq: number, hex_data: string) => { this.MoeHooExport.exports.InitHook?.(send, recv, (type: number, uin: string, cmd: string, seq: number, hex_data: string) => {
const trace_id = createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex'); const trace_id = createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex');
if (type === 0 && this.cb.get(trace_id + 'recv')) { if (type === 0 && this.cb.get(trace_id + 'recv')) {
@@ -55,7 +56,7 @@ export class NativePacketClient extends IPacketClient {
// console.log('callback:', callback, trace_id); // console.log('callback:', callback, trace_id);
callback?.({ seq, cmd, hex_data }); callback?.({ seq, cmd, hex_data });
} }
}); }, this.napcore.config.o3HookMode == 1);
this.available = true; this.available = true;
} }

View File

@@ -1,22 +1,22 @@
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import {PacketContext} from '@/core/packet/context/packetContext'; import { PacketContext } from '@/core/packet/context/packetContext';
import * as trans from '@/core/packet/transformer'; import * as trans from '@/core/packet/transformer';
import {PacketMsg} from '@/core/packet/message/message'; import { PacketMsg } from '@/core/packet/message/message';
import { import {
PacketMsgFileElement, PacketMsgFileElement,
PacketMsgPicElement, PacketMsgPicElement,
PacketMsgPttElement, PacketMsgPttElement,
PacketMsgVideoElement PacketMsgVideoElement
} from '@/core/packet/message/element'; } from '@/core/packet/message/element';
import {ChatType, MsgSourceType, NTMsgType, RawMessage} from '@/core'; import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/core';
import {MiniAppRawData, MiniAppReqParams} from '@/core/packet/entities/miniApp'; import { MiniAppRawData, MiniAppReqParams } from '@/core/packet/entities/miniApp';
import {AIVoiceChatType} from '@/core/packet/entities/aiChat'; import { AIVoiceChatType } from '@/core/packet/entities/aiChat';
import {NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg} from '@napneko/nap-proto-core'; import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import {IndexNode, LongMsgResult, MsgInfo} from '@/core/packet/transformer/proto'; import { IndexNode, LongMsgResult, MsgInfo } from '@/core/packet/transformer/proto';
import {OidbPacket} from '@/core/packet/transformer/base'; import { OidbPacket } from '@/core/packet/transformer/base';
import {ImageOcrResult} from '@/core/packet/entities/ocrResult'; import { ImageOcrResult } from '@/core/packet/entities/ocrResult';
import {gunzipSync} from 'zlib'; import { gunzipSync } from 'zlib';
import {PacketMsgConverter} from '@/core/packet/message/converter'; import { PacketMsgConverter } from '@/core/packet/message/converter';
export class PacketOperationContext { export class PacketOperationContext {
private readonly context: PacketContext; private readonly context: PacketContext;
@@ -59,10 +59,10 @@ export class PacketOperationContext {
const res = trans.GetStrangerInfo.parse(resp); const res = trans.GetStrangerInfo.parse(resp);
const extBigInt = BigInt(res.data.status.value); const extBigInt = BigInt(res.data.status.value);
if (extBigInt <= 10n) { if (extBigInt <= 10n) {
return {status: Number(extBigInt) * 10, ext_status: 0}; return { status: Number(extBigInt) * 10, ext_status: 0 };
} }
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn));
return {status: 10, ext_status: status}; return { status: 10, ext_status: status };
} catch { } catch {
return undefined; return undefined;
} }
@@ -79,13 +79,13 @@ export class PacketOperationContext {
const reqList = msg.flatMap(m => const reqList = msg.flatMap(m =>
m.msg.map(e => { m.msg.map(e => {
if (e instanceof PacketMsgPicElement) { if (e instanceof PacketMsgPicElement) {
return this.context.highway.uploadImage({chatType, peerUid}, e); return this.context.highway.uploadImage({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgVideoElement) { } else if (e instanceof PacketMsgVideoElement) {
return this.context.highway.uploadVideo({chatType, peerUid}, e); return this.context.highway.uploadVideo({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgPttElement) { } else if (e instanceof PacketMsgPttElement) {
return this.context.highway.uploadPtt({chatType, peerUid}, e); return this.context.highway.uploadPtt({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgFileElement) { } else if (e instanceof PacketMsgFileElement) {
return this.context.highway.uploadFile({chatType, peerUid}, e); return this.context.highway.uploadFile({ chatType, peerUid }, e);
} }
return null; return null;
}).filter(Boolean) }).filter(Boolean)
@@ -160,6 +160,12 @@ export class PacketOperationContext {
const res = trans.DownloadGroupFile.parse(resp); const res = trans.DownloadGroupFile.parse(resp);
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`; 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>) { async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupPtt.build(groupUin, node); const req = trans.DownloadGroupPtt.build(groupUin, node);

View File

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

View File

@@ -9,10 +9,10 @@ import {
SendFileElement, SendFileElement,
SendMarkdownElement, SendMarkdownElement,
SendMarketFaceElement, SendMarketFaceElement,
SendMultiForwardMsgElement,
SendPicElement, SendPicElement,
SendPttElement, SendPttElement,
SendReplyElement, SendReplyElement,
SendStructLongMsgElement,
SendTextElement, SendTextElement,
SendVideoElement SendVideoElement
} from '@/core'; } from '@/core';
@@ -46,7 +46,7 @@ const SupportedElementTypes = [
ElementType.PTT, ElementType.PTT,
ElementType.ARK, ElementType.ARK,
ElementType.MARKDOWN, ElementType.MARKDOWN,
ElementType.STRUCTLONGMSG ElementType.MULTIFORWARD
]; ];
type SendMessageTypeElementMap = { type SendMessageTypeElementMap = {
@@ -59,7 +59,7 @@ type SendMessageTypeElementMap = {
[ElementType.REPLY]: SendReplyElement, [ElementType.REPLY]: SendReplyElement,
[ElementType.ARK]: SendArkElement, [ElementType.ARK]: SendArkElement,
[ElementType.MFACE]: SendMarketFaceElement, [ElementType.MFACE]: SendMarketFaceElement,
[ElementType.STRUCTLONGMSG]: SendStructLongMsgElement, [ElementType.MULTIFORWARD]: SendMultiForwardMsgElement,
[ElementType.MARKDOWN]: SendMarkdownElement, [ElementType.MARKDOWN]: SendMarkdownElement,
}; };
@@ -118,9 +118,8 @@ export class PacketMsgConverter {
[ElementType.MARKDOWN]: (element) => { [ElementType.MARKDOWN]: (element) => {
return new PacketMsgMarkDownElement(element as SendMarkdownElement); return new PacketMsgMarkDownElement(element as SendMarkdownElement);
}, },
// TODO: check this logic, move it in arkElement? [ElementType.MULTIFORWARD]: (element) => {
[ElementType.STRUCTLONGMSG]: (element) => { return new PacketMultiMsgElement(element as SendMultiForwardMsgElement);
return new PacketMultiMsgElement(element as SendStructLongMsgElement);
} }
}; };

View File

@@ -27,7 +27,7 @@ import {
SendPicElement, SendPicElement,
SendPttElement, SendPttElement,
SendReplyElement, SendReplyElement,
SendStructLongMsgElement, SendMultiForwardMsgElement,
SendTextElement, SendTextElement,
SendVideoElement SendVideoElement
} from '@/core'; } from '@/core';
@@ -661,13 +661,13 @@ export class PacketMsgMarkDownElement extends IPacketMsgElement<SendMarkdownElem
} }
} }
export class PacketMultiMsgElement extends IPacketMsgElement<SendStructLongMsgElement> { export class PacketMultiMsgElement extends IPacketMsgElement<SendMultiForwardMsgElement> {
resid: string; resid: string;
message: PacketMsg[]; message: PacketMsg[];
constructor(rawElement: SendStructLongMsgElement, message?: PacketMsg[]) { constructor(rawElement: SendMultiForwardMsgElement, message?: PacketMsg[]) {
super(rawElement); super(rawElement);
this.resid = rawElement.structLongMsgElement.resId; this.resid = rawElement.multiForwardMsgElement.resId;
this.message = message ?? []; this.message = message ?? [];
} }

View File

@@ -1,7 +1,7 @@
import { IPacketMsgElement } from '@/core/packet/message/element'; import { IPacketMsgElement } from '@/core/packet/message/element';
import { SendMessageElement, SendStructLongMsgElement } from '@/core'; import {SendMessageElement, SendMultiForwardMsgElement} from '@/core';
export type PacketSendMsgElement = SendMessageElement | SendStructLongMsgElement export type PacketSendMsgElement = SendMessageElement | SendMultiForwardMsgElement
export interface PacketMsg { export interface PacketMsg {
seq?: number; seq?: number;

View File

@@ -1,4 +1,5 @@
import { DownloadBaseEmojiByIdReq, DownloadBaseEmojiByUrlReq, GetBaseEmojiPathReq, PullSysEmojisReq } from '../types'; import { DownloadBaseEmojiByIdReq, DownloadBaseEmojiByUrlReq, GetBaseEmojiPathReq, PullSysEmojisReq } from '../types';
import { GeneralCallResult } from './common';
export interface NodeIKernelBaseEmojiService { export interface NodeIKernelBaseEmojiService {
removeKernelBaseEmojiListener(listenerId: number): void; removeKernelBaseEmojiListener(listenerId: number): void;
@@ -7,7 +8,26 @@ export interface NodeIKernelBaseEmojiService {
isBaseEmojiPathExist(args: Array<string>): unknown; isBaseEmojiPathExist(args: Array<string>): unknown;
fetchFullSysEmojis(pullSysEmojisReq: PullSysEmojisReq): unknown; fetchFullSysEmojis(pullSysEmojisReq: PullSysEmojisReq): Promise<GeneralCallResult & {
rsp: {
otherPanelResult: {
SysEmojiGroupList: Array<unknown>,
downloadInfo: Array<unknown>
},
normalPanelResult: {
SysEmojiGroupList: Array<unknown>,
downloadInfo: Array<unknown>
},
superPanelResult: {
SysEmojiGroupList: Array<unknown>,
downloadInfo: Array<unknown>
},
redHeartPanelResult: {
SysEmojiGroupList: Array<unknown>,
downloadInfo: Array<unknown>
}
}
}>;
getBaseEmojiPathByIds(getBaseEmojiPathReqs: Array<GetBaseEmojiPathReq>): unknown; getBaseEmojiPathByIds(getBaseEmojiPathReqs: Array<GetBaseEmojiPathReq>): unknown;

View File

@@ -253,7 +253,7 @@ export interface NodeIKernelGroupService {
getGroupShutUpMemberList(groupCode: string): Promise<GeneralCallResult>; getGroupShutUpMemberList(groupCode: string): Promise<GeneralCallResult>;
setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void>; setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<GeneralCallResult>;
getGroupRecommendContactArkJson(groupCode: string): Promise<GeneralCallResult & { arkJson: string }>; getGroupRecommendContactArkJson(groupCode: string): Promise<GeneralCallResult & { arkJson: string }>;

View File

@@ -1,5 +1,9 @@
import { GeneralCallResult } from './common'; import { GeneralCallResult } from './common';
enum ProxyType {
CLOSE = 0,
HTTP = 1,
SOCKET = 2
}
export interface NodeIKernelMSFService { export interface NodeIKernelMSFService {
getServerTime(): string; getServerTime(): string;
setNetworkProxy(param: { setNetworkProxy(param: {
@@ -7,10 +11,19 @@ export interface NodeIKernelMSFService {
userPwd: string, userPwd: string,
address: string, address: string,
port: number, port: number,
proxyType: number, proxyType: ProxyType,
domain: string, domain: string,
isSocket: boolean isSocket: boolean
}): Promise<GeneralCallResult>; }): Promise<GeneralCallResult>;
getNetworkProxy(): Promise<{
userName: string,
userPwd: string,
address: string,
port: number,
proxyType: ProxyType,
domain: string,
isSocket: boolean
}>;
//http //http
// userName: '', // userName: '',
// userPwd: '', // userPwd: '',

View File

@@ -464,11 +464,20 @@ export interface NodeIKernelMsgService {
setMsgEmojiLikesForRole(...args: unknown[]): unknown; setMsgEmojiLikesForRole(...args: unknown[]): unknown;
clickInlineKeyboardButton(...args: unknown[]): unknown; clickInlineKeyboardButton(params: {
guildId?: string,
peerId: string,
botAppid: string,
msgSeq: string,
buttonId: string,
callback_data: string,
dmFlag: number,
chatType: number // 1私聊 2群
}): Promise<GeneralCallResult & { status: number, promptText: string, promptType: number, promptIcon: number }>;
setCurOnScreenMsg(...args: unknown[]): unknown; setCurOnScreenMsg(...args: unknown[]): unknown;
setCurOnScreenMsgForMsgEvent(...args: unknown[]): unknown; setCurOnScreenMsgForMsgEvent(peer: Peer, msgRegList: Map<string, Uint8Array>): void;
getMiscData(key: string): unknown; getMiscData(key: string): unknown;

View File

@@ -1,4 +1,5 @@
import { NodeIKernelRobotListener } from '@/core/listeners'; import { NodeIKernelRobotListener } from '@/core/listeners';
import { GeneralCallResult, Peer } from '..';
export interface NodeIKernelRobotService { export interface NodeIKernelRobotService {
fetchGroupRobotStoreDiscovery(arg: unknown): unknown; fetchGroupRobotStoreDiscovery(arg: unknown): unknown;
@@ -31,5 +32,17 @@ export interface NodeIKernelRobotService {
getRobotUinRange(data: unknown): Promise<{ response: { robotUinRanges: Array<unknown> } }>; getRobotUinRange(data: unknown): Promise<{ response: { robotUinRanges: Array<unknown> } }>;
getRobotFunctions(peer: Peer, params: {
uins: Array<string>,
num: 0,
client_info: { platform: 4, version: '', build_num: 9999 },
tinyids: [],
page: 0,
full_fetch: false,
scene: 4,
filter: 1,
bkn: ''
}): Promise<GeneralCallResult & { response: { bot_features: Array<unknown>, next_page: number } }>;
isNull(): boolean; isNull(): boolean;
} }

View File

@@ -1,4 +1,4 @@
import { ChatType } from '@/core/types'; import { ChatType, Peer } from '@/core/types';
import { GeneralCallResult } from './common'; import { GeneralCallResult } from './common';
export interface NodeIKernelSearchService { export interface NodeIKernelSearchService {
@@ -54,7 +54,7 @@ export interface NodeIKernelSearchService {
cancelSearchChatMsgs(...args: unknown[]): unknown;// needs 3 arguments cancelSearchChatMsgs(...args: unknown[]): unknown;// needs 3 arguments
searchMsgWithKeywords(...args: unknown[]): unknown;// needs 2 arguments searchMsgWithKeywords(keyWords: string[], param: Peer & { searchFields: number, pageLimit: number }): Promise<GeneralCallResult>;
searchMoreMsgWithKeywords(...args: unknown[]): unknown;// needs 1 arguments searchMoreMsgWithKeywords(...args: unknown[]): unknown;// needs 1 arguments

View File

@@ -347,6 +347,8 @@ export type SendMarkdownElement = SendElementBase<ElementType.MARKDOWN> & Elemen
export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION> & ElementBase<'shareLocationElement'>; export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION> & ElementBase<'shareLocationElement'>;
export type SendMultiForwardMsgElement = SendElementBase<ElementType.MULTIFORWARD> & ElementBase<'multiForwardMsgElement'>;
export type SendMessageElement = SendTextElement | SendPttElement | export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement | SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
SendVideoElement | SendArkElement | SendMarkdownElement | SendShareLocationElement; SendVideoElement | SendArkElement | SendMarkdownElement | SendShareLocationElement;

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,10 @@
import { ActionName } from '@/onebot/action/router';
import { OneBotAction } from '../OneBotAction';
export class BotExit extends OneBotAction<void, void> {
override actionName = ActionName.Exit;
async _handle() {
process.exit(0);
}
}

View File

@@ -0,0 +1,30 @@
import { ActionName } from '@/onebot/action/router';
import { OneBotAction } from '../OneBotAction';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
bot_appid: Type.String(),
button_id: Type.String({ default: '' }),
callback_data: Type.String({ default: '' }),
msg_seq: Type.String({ default: '10086' }),
});
type Payload = Static<typeof SchemaData>;
export class ClickInlineKeyboardButton extends OneBotAction<Payload, unknown> {
override actionName = ActionName.ClickInlineKeyboardButton;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
return await this.core.apis.MsgApi.clickInlineKeyboardButton({
buttonId: payload.button_id,
peerId: payload.group_id.toString(),
botAppid: payload.bot_appid,
msgSeq: payload.msg_seq,
callback_data: payload.callback_data,
dmFlag: 0,
chatType: 2
})
}
}

View File

@@ -0,0 +1,56 @@
import { PacketHexStr } from '@/core/packet/transformer/base';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { ProtoBuf, ProtoBufBase, PBUint32, PBString } from 'napcat.protobuf';
interface Friend {
uin: number;
uid: string;
nick_name: string;
age: number;
source: string;
}
interface Block {
str_uid: string;
bytes_source: string;
uint32_sex: number;
uint32_age: number;
bytes_nick: string;
uint64_uin: number;
}
export class GetUnidirectionalFriendList extends OneBotAction<void, Friend[]> {
override actionName = ActionName.GetUnidirectionalFriendList;
async pack_data(data: string): Promise<Uint8Array> {
return ProtoBuf(class extends ProtoBufBase {
type = PBUint32(2, false, 0);
data = PBString(3, false, data);
}).encode();
}
async _handle(): Promise<Friend[]> {
const self_id = this.core.selfInfo.uin;
const req_json = {
uint64_uin: self_id,
uint64_top: 0,
uint32_req_num: 99,
bytes_cookies: ""
};
const packed_data = await this.pack_data(JSON.stringify(req_json));
const data = Buffer.from(packed_data).toString('hex');
const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as PacketHexStr };
const rsp_data = await this.core.apis.PacketApi.pkt.operation.sendPacket(rsq, true);
const block_json = ProtoBuf(class extends ProtoBufBase { data = PBString(4); }).decode(rsp_data);
const block_list: Block[] = JSON.parse(block_json.data).rpt_block_list;
return block_list.map((block) => ({
uin: block.uint64_uin,
uid: block.str_uid,
nick_name: Buffer.from(block.bytes_nick, 'base64').toString(),
age: block.uint32_age,
source: Buffer.from(block.bytes_source, 'base64').toString()
}));
}
}

View File

@@ -5,7 +5,7 @@ import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({ const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]), group_id: Type.Union([Type.Number(), Type.String()]),
user_id: Type.Union([Type.Number(), Type.String()]), user_id: Type.Union([Type.Number(), Type.String()]),
special_title: Type.String(), special_title: Type.String({ default: '' }),
}); });
type Payload = Static<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
@@ -16,7 +16,7 @@ export class SetSpecialTittle extends GetPacketStatusDepends<Payload, void> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if(!uid) throw new Error('User not found'); if (!uid) throw new Error('User not found');
await this.core.apis.PacketApi.pkt.operation.SetGroupSpecialTitle(+payload.group_id, uid, payload.special_title); await this.core.apis.PacketApi.pkt.operation.SetGroupSpecialTitle(+payload.group_id, uid, payload.special_title);
} }
} }

View File

@@ -0,0 +1,36 @@
import { ActionName } from '@/onebot/action/router';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
file_id: Type.String(),
});
type Payload = Static<typeof SchemaData>;
interface GetPrivateFileUrlResponse {
url?: string;
}
export class GetPrivateFileUrl extends GetPacketStatusDepends<Payload, GetPrivateFileUrlResponse> {
override actionName = ActionName.NapCat_GetPrivateFileUrl;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id);
if (contextMsgFile?.fileUUID && contextMsgFile.msgId) {
let msg = await this.core.apis.MsgApi.getMsgsByMsgId(contextMsgFile.peer, [contextMsgFile.msgId]);
let self_id = this.core.selfInfo.uid;
let file_hash = msg.msgList[0]?.elements.map(ele => ele.fileElement?.file10MMd5)[0];
if (file_hash) {
return {
url: await this.core.apis.PacketApi.pkt.operation.GetPrivateFileUrl(self_id, contextMsgFile.fileUUID, file_hash)
};
}
}
throw new Error('real fileUUID not found!');
}
}

View File

@@ -13,12 +13,15 @@ type Payload = Static<typeof SchemaData>;
export default class SetGroupBan extends OneBotAction<Payload, null> { export default class SetGroupBan extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupBan; override actionName = ActionName.SetGroupBan;
override payloadSchema = SchemaData; override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> { async _handle(payload: Payload): Promise<null> {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error('uid error'); if (!uid) throw new Error('uid error');
await this.core.apis.GroupApi.banMember(payload.group_id.toString(), let member_role = (await this.core.apis.GroupApi.getGroupMemberEx(payload.group_id.toString(), uid, true))?.role;
if (member_role === 4) throw new Error('cannot ban owner');
// 例如无管理员权限时 result为 120101005 errMsg为 'ERR_NOT_GROUP_ADMIN'
let ret = await this.core.apis.GroupApi.banMember(payload.group_id.toString(),
[{ uid: uid, timeStamp: +payload.duration }]); [{ uid: uid, timeStamp: +payload.duration }]);
if (ret.result !== 0) throw new Error(ret.errMsg);
return null; return null;
} }
} }

View File

@@ -104,6 +104,10 @@ import { GetClientkey } from './extends/GetClientkey';
import { SendPacket } from './extends/SendPacket'; import { SendPacket } from './extends/SendPacket';
import { SendPoke } from '@/onebot/action/packet/SendPoke'; import { SendPoke } from '@/onebot/action/packet/SendPoke';
import { SetDiyOnlineStatus } from './extends/SetDiyOnlineStatus'; import { SetDiyOnlineStatus } from './extends/SetDiyOnlineStatus';
import { BotExit } from './extends/BotExit';
import { ClickInlineKeyboardButton } from './extends/ClickInlineKeyboardButton';
import { GetPrivateFileUrl } from './file/GetPrivateFileUrl';
import { GetUnidirectionalFriendList } from './extends/GetUnidirectionalFriendList';
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) { export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
@@ -221,6 +225,10 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new SendPacket(obContext, core), new SendPacket(obContext, core),
new SendPoke(obContext, core), new SendPoke(obContext, core),
new GetGroupSystemMsg(obContext, core), new GetGroupSystemMsg(obContext, core),
new BotExit(obContext, core),
new ClickInlineKeyboardButton(obContext, core),
new GetPrivateFileUrl(obContext,core),
new GetUnidirectionalFriendList(obContext,core),
]; ];
type HandlerUnion = typeof actionHandlers[number]; type HandlerUnion = typeof actionHandlers[number];

View File

@@ -10,6 +10,9 @@ export interface InvalidCheckResult {
} }
export const ActionName = { export const ActionName = {
NapCat_GetPrivateFileUrl: 'get_private_file_url',
ClickInlineKeyboardButton: 'click_inline_keyboard_button',
GetUnidirectionalFriendList: 'get_unidirectional_friend_list',
// onebot 11 // onebot 11
SendPrivateMsg: 'send_private_msg', SendPrivateMsg: 'send_private_msg',
SendGroupMsg: 'send_group_msg', SendGroupMsg: 'send_group_msg',
@@ -49,7 +52,7 @@ export const ActionName = {
GetVersionInfo: 'get_version_info', GetVersionInfo: 'get_version_info',
// Reboot : 'set_restart', // Reboot : 'set_restart',
// CleanCache : 'clean_cache', // CleanCache : 'clean_cache',
Exit: 'bot_exit',
// go-cqhttp // go-cqhttp
SetQQProfile: 'set_qq_profile', SetQQProfile: 'set_qq_profile',
// QidianGetAccountInfo : 'qidian_get_account_info', // QidianGetAccountInfo : 'qidian_get_account_info',
@@ -141,6 +144,6 @@ export const ActionName = {
SendGroupAiRecord: 'send_group_ai_record', SendGroupAiRecord: 'send_group_ai_record',
GetClientkey: 'get_clientkey', GetClientkey: 'get_clientkey',
SendPoke: 'send_poke', SendPoke: 'send_poke',
} as const; } as const;

View File

@@ -49,6 +49,7 @@ export class OneBotGroupApi {
duration = -1; duration = -1;
} }
} }
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(GroupCode, memberUid);
const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, adminUid))?.uin; const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, adminUid))?.uin;
if (memberUin && adminUin) { if (memberUin && adminUin) {
return new OB11GroupBanEvent( return new OB11GroupBanEvent(
@@ -113,12 +114,16 @@ export class OneBotGroupApi {
async parseCardChangedEvent(msg: RawMessage) { async parseCardChangedEvent(msg: RawMessage) {
if (msg.senderUin && msg.senderUin !== '0') { if (msg.senderUin && msg.senderUin !== '0') {
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin); const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin);
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
if (member && member.cardName !== msg.sendMemberName) { if (member && member.cardName !== msg.sendMemberName) {
const newCardName = msg.sendMemberName ?? ''; const newCardName = msg.sendMemberName ?? '';
const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName); const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName);
member.cardName = newCardName; member.cardName = newCardName;
return event; return event;
} }
if (member && member.nick !== msg.sendNickName) {
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
}
} }
return undefined; return undefined;
} }

View File

@@ -1,5 +1,5 @@
import {FileNapCatOneBotUUID} from '@/common/file-uuid'; import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import {MessageUnique} from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
import { import {
ChatType, ChatType,
CustomMusicSignPostData, CustomMusicSignPostData,
@@ -29,22 +29,22 @@ import {
OB11MessageImage, OB11MessageImage,
OB11MessageVideo, OB11MessageVideo,
} from '@/onebot'; } from '@/onebot';
import {OB11Construct} from '@/onebot/helper/data'; import { OB11Construct } from '@/onebot/helper/data';
import {EventType} from '@/onebot/event/OneBotEvent'; import { EventType } from '@/onebot/event/OneBotEvent';
import {encodeCQCode} from '@/onebot/helper/cqcode'; import { encodeCQCode } from '@/onebot/helper/cqcode';
import {uriToLocalFile} from '@/common/file'; import { uriToLocalFile } from '@/common/file';
import {RequestUtil} from '@/common/request'; import { RequestUtil } from '@/common/request';
import fsPromise, {constants} from 'node:fs/promises'; import fsPromise, { constants } from 'node:fs/promises';
import {OB11FriendAddNoticeEvent} from '@/onebot/event/notice/OB11FriendAddNoticeEvent'; import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent';
import {ForwardMsgBuilder} from '@/common/forward-msg-builder'; import { ForwardMsgBuilder } from '@/common/forward-msg-builder';
import {NapProtoMsg} from '@napneko/nap-proto-core'; import { NapProtoMsg } from '@napneko/nap-proto-core';
import {OB11GroupIncreaseEvent} from '../event/notice/OB11GroupIncreaseEvent'; import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
import {GroupDecreaseSubType, OB11GroupDecreaseEvent} from '../event/notice/OB11GroupDecreaseEvent'; import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../event/notice/OB11GroupDecreaseEvent';
import {GroupAdmin} from '@/core/packet/transformer/proto/message/groupAdmin'; import { GroupAdmin } from '@/core/packet/transformer/proto/message/groupAdmin';
import {OB11GroupAdminNoticeEvent} from '../event/notice/OB11GroupAdminNoticeEvent'; import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent';
import {GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody} from '@/core/packet/transformer/proto'; import { GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody } from '@/core/packet/transformer/proto';
import {OB11GroupRequestEvent} from '../event/request/OB11GroupRequest'; import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest';
import {LRUCache} from '@/common/lru-cache'; import { LRUCache } from '@/common/lru-cache';
type RawToOb11Converters = { type RawToOb11Converters = {
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: ( [Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
@@ -93,12 +93,12 @@ export class OneBotMsgApi {
} }
return { return {
type: OB11MessageDataType.text, type: OB11MessageDataType.text,
data: {text}, data: { text },
}; };
} else { } else {
let qq: string = 'all'; let qq: string = 'all';
if (element.atType !== NTMsgAtType.ATTYPEALL) { if (element.atType !== NTMsgAtType.ATTYPEALL) {
const {atNtUid, atUid} = element; const { atNtUid, atUid } = element;
qq = !atUid || atUid === '0' ? await this.core.apis.UserApi.getUinByUidV2(atNtUid) : atUid; qq = !atUid || atUid === '0' ? await this.core.apis.UserApi.getUinByUidV2(atNtUid) : atUid;
} }
return { return {
@@ -132,7 +132,6 @@ export class OneBotMsgApi {
file: element.fileName, file: element.fileName,
sub_type: element.picSubType, sub_type: element.picSubType,
url: await this.core.apis.FileApi.getImageUrl(element), url: await this.core.apis.FileApi.getImageUrl(element),
path: element.filePath,
file_size: element.fileSize, file_size: element.fileSize,
}, },
}; };
@@ -148,13 +147,13 @@ export class OneBotMsgApi {
peerUid: msg.peerUid, peerUid: msg.peerUid,
guildId: '', guildId: '',
}; };
const file = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName); FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileUuid);
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
return { return {
type: OB11MessageDataType.file, type: OB11MessageDataType.file,
data: { data: {
file: file, file: element.fileName,
path: element.filePath, file_id: element.fileUuid,
file_id: file,
file_size: element.fileSize, file_size: element.fileSize,
}, },
}; };
@@ -206,7 +205,7 @@ export class OneBotMsgApi {
peerUid: msg.peerUid, peerUid: msg.peerUid,
guildId: '', guildId: '',
}; };
const {emojiId} = _; const { emojiId } = _;
const dir = emojiId.substring(0, 2); const dir = emojiId.substring(0, 2);
const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`; const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`;
const filename = `${dir}-${emojiId}.gif`; const filename = `${dir}-${emojiId}.gif`;
@@ -216,7 +215,6 @@ export class OneBotMsgApi {
data: { data: {
summary: _.faceName, // 商城表情名称 summary: _.faceName, // 商城表情名称
file: filename, file: filename,
path: url,
url: url, url: url,
key: _.key, key: _.key,
emoji_id: _.emojiId, emoji_id: _.emojiId,
@@ -339,7 +337,6 @@ export class OneBotMsgApi {
type: OB11MessageDataType.video, type: OB11MessageDataType.video,
data: { data: {
file: fileCode, file: fileCode,
path: videoDownUrl,
url: videoDownUrl, url: videoDownUrl,
file_size: element.fileSize, file_size: element.fileSize,
}, },
@@ -357,8 +354,8 @@ export class OneBotMsgApi {
type: OB11MessageDataType.voice, type: OB11MessageDataType.voice,
data: { data: {
file: fileCode, file: fileCode,
path: element.filePath,
file_size: element.fileSize, file_size: element.fileSize,
path: element.filePath,
}, },
}; };
}, },
@@ -381,7 +378,7 @@ export class OneBotMsgApi {
} }
const forward: OB11MessageForward = { const forward: OB11MessageForward = {
type: OB11MessageDataType.forward, type: OB11MessageDataType.forward,
data: {id: msg.msgId} data: { id: msg.msgId }
}; };
if (!context.parseMultMsg) return forward; if (!context.parseMultMsg) return forward;
forward.data.content = await this.parseMultiMessageContent( forward.data.content = await this.parseMultiMessageContent(
@@ -412,7 +409,7 @@ export class OneBotMsgApi {
}; };
ob11ToRawConverters: Ob11ToRawConverters = { ob11ToRawConverters: Ob11ToRawConverters = {
[OB11MessageDataType.text]: async ({data: {text}}) => ({ [OB11MessageDataType.text]: async ({ data: { text } }) => ({
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
elementId: '', elementId: '',
textElement: { textElement: {
@@ -424,7 +421,7 @@ export class OneBotMsgApi {
}, },
}), }),
[OB11MessageDataType.at]: async ({data: {qq: atQQ}}, context) => { [OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => {
function at(atUid: string, atNtUid: string, atType: NTMsgAtType, atName: string): SendTextElement { function at(atUid: string, atNtUid: string, atType: NTMsgAtType, atName: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
@@ -451,7 +448,7 @@ export class OneBotMsgApi {
return at(atQQ, uid, NTMsgAtType.ATTYPEONE, info.nick || ''); return at(atQQ, uid, NTMsgAtType.ATTYPEONE, info.nick || '');
}, },
[OB11MessageDataType.reply]: async ({data: {id}}) => { [OB11MessageDataType.reply]: async ({ data: { id } }) => {
const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id)); const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
if (!replyMsgM) { if (!replyMsgM) {
this.core.context.logger.logWarn('回复消息不存在', id); this.core.context.logger.logWarn('回复消息不存在', id);
@@ -473,7 +470,7 @@ export class OneBotMsgApi {
undefined; undefined;
}, },
[OB11MessageDataType.face]: async ({data: {id, resultId, chainCount}}) => { [OB11MessageDataType.face]: async ({ data: { id, resultId, chainCount } }) => {
const parsedFaceId = +id; const parsedFaceId = +id;
// 从face_config.json中获取表情名称 // 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface; const sysFaces = faceConfig.sysface;
@@ -537,12 +534,12 @@ export class OneBotMsgApi {
}, },
[OB11MessageDataType.file]: async (sendMsg, context) => { [OB11MessageDataType.file]: async (sendMsg, context) => {
const {path, fileName} = await this.handleOb11FileLikeMessage(sendMsg, context); const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context);
return await this.core.apis.FileApi.createValidSendFileElement(context, path, fileName); return await this.core.apis.FileApi.createValidSendFileElement(context, path, fileName);
}, },
[OB11MessageDataType.video]: async (sendMsg, context) => { [OB11MessageDataType.video]: async (sendMsg, context) => {
const {path, fileName} = await this.handleOb11FileLikeMessage(sendMsg, context); const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context);
let thumb = sendMsg.data.thumb; let thumb = sendMsg.data.thumb;
if (thumb) { if (thumb) {
@@ -560,7 +557,7 @@ export class OneBotMsgApi {
this.core.apis.FileApi.createValidSendPttElement( this.core.apis.FileApi.createValidSendPttElement(
(await this.handleOb11FileLikeMessage(sendMsg, context)).path), (await this.handleOb11FileLikeMessage(sendMsg, context)).path),
[OB11MessageDataType.json]: async ({data: {data}}) => ({ [OB11MessageDataType.json]: async ({ data: { data } }) => ({
elementType: ElementType.ARK, elementType: ElementType.ARK,
elementId: '', elementId: '',
arkElement: { arkElement: {
@@ -603,13 +600,13 @@ export class OneBotMsgApi {
}), }),
// Need signing // Need signing
[OB11MessageDataType.markdown]: async ({data: {content}}) => ({ [OB11MessageDataType.markdown]: async ({ data: { content } }) => ({
elementType: ElementType.MARKDOWN, elementType: ElementType.MARKDOWN,
elementId: '', elementId: '',
markdownElement: {content}, markdownElement: { content },
}), }),
[OB11MessageDataType.music]: async ({data}, context) => { [OB11MessageDataType.music]: async ({ data }, context) => {
// 保留, 直到...找到更好的解决方案 // 保留, 直到...找到更好的解决方案
if (data.id !== undefined) { if (data.id !== undefined) {
if (!['qq', '163', 'kugou', 'kuwo', 'migu'].includes(data.type)) { if (!['qq', '163', 'kugou', 'kuwo', 'migu'].includes(data.type)) {
@@ -633,8 +630,8 @@ export class OneBotMsgApi {
let postData: IdMusicSignPostData | CustomMusicSignPostData; let postData: IdMusicSignPostData | CustomMusicSignPostData;
if (data.id === undefined && data.content) { if (data.id === undefined && data.content) {
const {content, ...others} = data; const { content, ...others } = data;
postData = {singer: content, ...others}; postData = { singer: content, ...others };
} else { } else {
postData = data; postData = data;
} }
@@ -646,7 +643,7 @@ export class OneBotMsgApi {
try { try {
const musicJson = await RequestUtil.HttpGetJson<string>(signUrl, 'POST', postData); const musicJson = await RequestUtil.HttpGetJson<string>(signUrl, 'POST', postData);
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: {data: musicJson}, data: { data: musicJson },
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
} catch (e) { } catch (e) {
@@ -657,10 +654,23 @@ export class OneBotMsgApi {
[OB11MessageDataType.node]: async () => undefined, [OB11MessageDataType.node]: async () => undefined,
[OB11MessageDataType.forward]: async ({data}, context) => { [OB11MessageDataType.forward]: async ({ data }, context) => {
// let id = data.id.toString();
// let peer: Peer | undefined = context.peer;
// if (isNumeric(id)) {
// let msgid = '';
// if (BigInt(data.id) > 2147483647n) {
// peer = MessageUnique.getPeerByMsgId(id)?.Peer;
// msgid = id;
// } else {
// let data = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
// msgid = data?.MsgId ?? '';
// peer = data?.Peer;
// }
// }
const jsonData = ForwardMsgBuilder.fromResId(data.id); const jsonData = ForwardMsgBuilder.fromResId(data.id);
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: {data: JSON.stringify(jsonData)}, data: { data: JSON.stringify(jsonData) },
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
}, },
@@ -680,17 +690,17 @@ export class OneBotMsgApi {
[OB11MessageDataType.miniapp]: async () => undefined, [OB11MessageDataType.miniapp]: async () => undefined,
[OB11MessageDataType.contact]: async ({data: {type = 'qq', id}}, context) => { [OB11MessageDataType.contact]: async ({ data: { type = 'qq', id } }, context) => {
if (type === 'qq') { if (type === 'qq') {
const arkJson = await this.core.apis.UserApi.getBuddyRecommendContactArkJson(id.toString(), ''); const arkJson = await this.core.apis.UserApi.getBuddyRecommendContactArkJson(id.toString(), '');
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: {data: arkJson.arkMsg}, data: { data: arkJson.arkMsg },
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
} else if (type === 'group') { } else if (type === 'group') {
const arkJson = await this.core.apis.GroupApi.getGroupRecommendContactArkJson(id.toString()); const arkJson = await this.core.apis.GroupApi.getGroupRecommendContactArkJson(id.toString());
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: {data: arkJson.arkJson}, data: { data: arkJson.arkJson },
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
} }
@@ -867,7 +877,7 @@ export class OneBotMsgApi {
element[key], element[key],
msg, msg,
element, element,
{parseMultMsg} { parseMultMsg }
); );
if (key === 'faceElement' && !parsedElement) { if (key === 'faceElement' && !parsedElement) {
return null; return null;
@@ -920,13 +930,13 @@ export class OneBotMsgApi {
) => Promise<SendMessageElement | undefined>; ) => Promise<SendMessageElement | undefined>;
const callResult = converter( const callResult = converter(
sendMsg, sendMsg,
{peer, deleteAfterSentFiles}, { peer, deleteAfterSentFiles },
)?.catch(undefined); )?.catch(undefined);
callResultList.push(callResult); callResultList.push(callResult);
} }
const ret = await Promise.all(callResultList); const ret = await Promise.all(callResultList);
const sendElements: SendMessageElement[] = ret.filter(ele => !!ele); const sendElements: SendMessageElement[] = ret.filter(ele => !!ele);
return {sendElements, deleteAfterSentFiles}; return { sendElements, deleteAfterSentFiles };
} }
async sendMsgWithOb11UniqueId(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[]) { async sendMsgWithOb11UniqueId(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[]) {
@@ -937,16 +947,16 @@ export class OneBotMsgApi {
const calculateTotalSize = async (elements: SendMessageElement[]): Promise<number> => { const calculateTotalSize = async (elements: SendMessageElement[]): Promise<number> => {
const sizePromises = elements.map(async element => { const sizePromises = elements.map(async element => {
switch (element.elementType) { switch (element.elementType) {
case ElementType.PTT: case ElementType.PTT:
return (await fsPromise.stat(element.pttElement.filePath)).size; return (await fsPromise.stat(element.pttElement.filePath)).size;
case ElementType.FILE: case ElementType.FILE:
return (await fsPromise.stat(element.fileElement.filePath)).size; return (await fsPromise.stat(element.fileElement.filePath)).size;
case ElementType.VIDEO: case ElementType.VIDEO:
return (await fsPromise.stat(element.videoElement.filePath)).size; return (await fsPromise.stat(element.videoElement.filePath)).size;
case ElementType.PIC: case ElementType.PIC:
return (await fsPromise.stat(element.picElement.sourcePath)).size; return (await fsPromise.stat(element.picElement.sourcePath)).size;
default: default:
return 0; return 0;
} }
}); });
const sizes = await Promise.all(sizePromises); const sizes = await Promise.all(sizePromises);
@@ -988,39 +998,32 @@ export class OneBotMsgApi {
} }
private async handleOb11FileLikeMessage( private async handleOb11FileLikeMessage(
{data: inputdata}: OB11MessageFileBase, { data: inputdata }: OB11MessageFileBase,
{deleteAfterSentFiles}: SendMessageContext { deleteAfterSentFiles }: SendMessageContext
) { ) {
let realUri = [inputdata.url, inputdata.file, inputdata.path].find(uri => uri && uri.trim()) ?? ''; let realUri = [inputdata.url, inputdata.file, inputdata.path].find(uri => uri && uri.trim()) ?? '';
if (!realUri) { if (!realUri) {
this.core.context.logger.logError('文件消息缺少参数', inputdata); this.core.context.logger.logError('文件消息缺少参数', inputdata);
throw new Error('文件消息缺少参数'); throw new Error('文件消息缺少参数');
} }
realUri = await this.handleObfuckName(realUri) ?? realUri;
const downloadFile = async (uri: string) => {
const {path, fileName, errMsg, success} = await uriToLocalFile(this.core.NapCatTempPath, uri);
if (!success) {
this.core.context.logger.logError('文件下载失败', errMsg);
throw new Error('文件下载失败: ' + errMsg);
}
return {path, fileName};
};
try { try {
const {path, fileName} = await downloadFile(realUri); const { path, fileName, errMsg, success } = await uriToLocalFile(this.core.NapCatTempPath, realUri);
if (!success) {
this.core.context.logger.logError('文件处理失败', errMsg);
throw new Error('文件处理失败: ' + errMsg);
}
deleteAfterSentFiles.push(path); deleteAfterSentFiles.push(path);
return {path, fileName: inputdata.name ?? fileName}; return { path, fileName: inputdata.name ?? fileName };
} catch { } catch (e: unknown) {
realUri = await this.handleObfuckName(realUri); throw new Error((e as Error).message);
const {path, fileName} = await downloadFile(realUri);
deleteAfterSentFiles.push(path);
return {path, fileName: inputdata.name ?? fileName};
} }
} }
async handleObfuckName(name: string) { async handleObfuckName(name: string) {
const contextMsgFile = FileNapCatOneBotUUID.decode(name); const contextMsgFile = FileNapCatOneBotUUID.decode(name);
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) { if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
const {peer, msgId, elementId} = contextMsgFile; const { peer, msgId, elementId } = contextMsgFile;
const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList.find(msg => msg.msgId === msgId); const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList.find(msg => msg.msgId === msgId);
const mixElement = rawMessage?.elements.find(e => e.elementId === elementId); const mixElement = rawMessage?.elements.find(e => e.elementId === elementId);
const mixElementInner = mixElement?.videoElement ?? mixElement?.fileElement ?? mixElement?.pttElement ?? mixElement?.picElement; const mixElementInner = mixElement?.videoElement ?? mixElement?.fileElement ?? mixElement?.pttElement ?? mixElement?.picElement;
@@ -1028,29 +1031,29 @@ export class OneBotMsgApi {
let url = ''; let url = '';
if (mixElement?.picElement && rawMessage) { if (mixElement?.picElement && rawMessage) {
const tempData = const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, {parseMultMsg: false}) as OB11MessageImage | undefined; await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageImage | undefined;
url = tempData?.data.url ?? ''; url = tempData?.data.url ?? '';
} }
if (mixElement?.videoElement && rawMessage) { if (mixElement?.videoElement && rawMessage) {
const tempData = const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, {parseMultMsg: false}) as OB11MessageVideo | undefined; await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageVideo | undefined;
url = tempData?.data.url ?? ''; url = tempData?.data.url ?? '';
} }
return url !== '' ? url : await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', ''); return url !== '' ? url : await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', '');
} }
throw new Error('文件名解析失败'); return undefined;
} }
groupChangDecreseType2String(type: number): GroupDecreaseSubType { groupChangDecreseType2String(type: number): GroupDecreaseSubType {
switch (type) { switch (type) {
case 130: case 130:
return 'leave'; return 'leave';
case 131: case 131:
return 'kick'; return 'kick';
case 3: case 3:
return 'kick_me'; return 'kick_me';
default: default:
return 'kick'; return 'kick';
} }
} }
@@ -1069,7 +1072,7 @@ export class OneBotMsgApi {
} }
} }
return false; return false;
}, 1, 1000).catch(undefined); }, 1, 1000).catch(() => undefined);
if (dataNotify) { if (dataNotify) {
return !dataNotify.actionUser.uid ? dataNotify.user2.uid : dataNotify.actionUser.uid; return !dataNotify.actionUser.uid ? dataNotify.user2.uid : dataNotify.actionUser.uid;
} }

View File

@@ -3,7 +3,7 @@ import { NapCatCore } from '@/core';
export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent { export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_admin'; notice_type = 'group_admin';
sub_type: 'set' | 'unset'; sub_type: 'set' | 'unset';
constructor(core: NapCatCore, group_id: number, user_id: number, sub_type: 'set' | 'unset') { constructor(core: NapCatCore, group_id: number, user_id: number, sub_type: 'set' | 'unset') {
super(core, group_id, user_id); super(core, group_id, user_id);

View File

@@ -9,7 +9,8 @@ import { HttpServerConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from '@/onebot'; import { NapCatOneBot11Adapter } from '@/onebot';
import { IOB11NetworkAdapter } from '@/onebot/network/adapter'; import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
import json5 from 'json5'; import json5 from 'json5';
import { isFinished } from 'on-finished';
import typeis from 'type-is';
export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig> { export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig> {
private app: Express | undefined; private app: Express | undefined;
private server: http.Server | undefined; private server: http.Server | undefined;
@@ -45,13 +46,23 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
this.app = undefined; this.app = undefined;
} }
private initializeServer() { private initializeServer() {
this.app = express(); this.app = express();
this.server = http.createServer(this.app); this.server = http.createServer(this.app);
this.app.use(cors()); this.app.use(cors());
this.app.use(express.urlencoded({ extended: true, limit: '5000mb' })); this.app.use(express.urlencoded({ extended: true, limit: '5000mb' }));
this.app.use((req, res, next) => { this.app.use((req, res, next) => {
if (isFinished(req)) {
next();
return;
}
if (!typeis.hasBody(req)) {
next();
return;
}
// 兼容处理没有带content-type的请求 // 兼容处理没有带content-type的请求
req.headers['content-type'] = 'application/json'; req.headers['content-type'] = 'application/json';
let rawData = ''; let rawData = '';
@@ -98,7 +109,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
if (req.method == 'get') { if (req.method == 'get') {
payload = req.query; payload = req.query;
} else if (req.query) { } else if (req.query) {
payload = { ...req.query, ...req.body }; payload = { ...req.body, ...req.query };
} }
if (req.path === '' || req.path === '/') { if (req.path === '' || req.path === '/') {
const hello = OB11Response.ok({}); const hello = OB11Response.ok({});

View File

@@ -236,11 +236,11 @@ async function initializeSession(
) { ) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const sessionListener = new NodeIKernelSessionListener(); const sessionListener = new NodeIKernelSessionListener();
sessionListener.onSessionInitComplete = (r: unknown) => { sessionListener.onOpentelemetryInit = (info) => {
if (r === 0) { if (info.is_init) {
resolve(); resolve();
} else { } else {
reject(new Error('登录异常' + r?.toString())); reject(new Error('opentelemetry init failed'));
} }
}; };
session.init( session.init(
@@ -260,7 +260,30 @@ async function initializeSession(
} }
}); });
} }
async function handleProxy(session: NodeIQQNTWrapperSession, logger: LogWrapper) {
if (process.env['NAPCAT_PROXY_PORT']) {
session.getMSFService().setNetworkProxy({
userName: '',
userPwd: '',
address: process.env['NAPCAT_PROXY_ADDRESS'] || '127.0.0.1',
port: +process.env['NAPCAT_PROXY_PORT'],
proxyType: 2,
domain: '',
isSocket: true
});
logger.logWarn('已设置代理', process.env['NAPCAT_PROXY_ADDRESS'], process.env['NAPCAT_PROXY_PORT']);
} else if (process.env['NAPCAT_PROXY_CLOSE']) {
session.getMSFService().setNetworkProxy({
userName: '',
userPwd: '',
address: '',
port: 0,
proxyType: 0,
domain: '',
isSocket: false
});
}
}
export async function NCoreInitShell() { export async function NCoreInitShell() {
console.log('NapCat Shell App Loading...'); console.log('NapCat Shell App Loading...');
const pathWrapper = new NapCatPathWrapper(); const pathWrapper = new NapCatPathWrapper();
@@ -286,7 +309,7 @@ export async function NCoreInitShell() {
await initializeEngine(engine, basicInfoWrapper, dataPathGlobal, systemPlatform, systemVersion); await initializeEngine(engine, basicInfoWrapper, dataPathGlobal, systemPlatform, systemVersion);
await initializeLoginService(loginService, basicInfoWrapper, dataPathGlobal, systemVersion, hostname); await initializeLoginService(loginService, basicInfoWrapper, dataPathGlobal, systemVersion, hostname);
handleProxy(session, logger);
program.option('-q, --qq [number]', 'QQ号').parse(process.argv); program.option('-q, --qq [number]', 'QQ号').parse(process.argv);
const cmdOptions = program.opts(); const cmdOptions = program.opts();
const quickLoginUin = cmdOptions['qq']; const quickLoginUin = cmdOptions['qq'];
@@ -294,6 +317,7 @@ export async function NCoreInitShell() {
const dataTimestape = new Date().getTime().toString(); const dataTimestape = new Date().getTime().toString();
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']); o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList); const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129'; const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex'))); o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));

View File

@@ -10,7 +10,7 @@ import { WebUiConfigWrapper } from '@webapi/helper/config';
import { ALLRouter } from '@webapi/router'; import { ALLRouter } from '@webapi/router';
import { cors } from '@webapi/middleware/cors'; import { cors } from '@webapi/middleware/cors';
import { createUrl } from '@webapi/utils/url'; import { createUrl } from '@webapi/utils/url';
import { sendError, sendSuccess } from '@webapi/utils/response'; import { sendError } from '@webapi/utils/response';
import { join } from 'node:path'; import { join } from 'node:path';
import { terminalManager } from '@webapi/terminal/terminal_manager'; import { terminalManager } from '@webapi/terminal/terminal_manager';
import multer from 'multer'; // 新增引入multer用于错误捕获 import multer from 'multer'; // 新增引入multer用于错误捕获
@@ -26,16 +26,43 @@ const server = createServer(app);
*/ */
export let WebUiConfig: WebUiConfigWrapper; export let WebUiConfig: WebUiConfigWrapper;
export let webUiPathWrapper: NapCatPathWrapper; export let webUiPathWrapper: NapCatPathWrapper;
const MAX_PORT_TRY = 100;
import * as net from 'node:net';
import { WebUiDataRuntime } from './src/helper/Data';
export async function InitPort(parsedConfig: WebUiConfigType): Promise<[string, number, string]> {
try {
await tryUseHost(parsedConfig.host);
const port = await tryUsePort(parsedConfig.port, parsedConfig.host);
return [parsedConfig.host, port, parsedConfig.token];
} catch (error) {
console.log('host或port不可用', error);
return ['', 0, ''];
}
}
export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapper) { export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapper) {
webUiPathWrapper = pathWrapper; webUiPathWrapper = pathWrapper;
WebUiConfig = new WebUiConfigWrapper(); WebUiConfig = new WebUiConfigWrapper();
const config = await WebUiConfig.GetWebUIConfig(); const [host, port, token] = await InitPort(await WebUiConfig.GetWebUIConfig());
if (config.port == 0) { if (port == 0) {
logger.log('[NapCat] [WebUi] Current WebUi is not run.'); logger.log('[NapCat] [WebUi] Current WebUi is not run.');
return; return;
} }
setTimeout(async () => {
let autoLoginAccount = process.env['NAPCAT_QUICK_ACCOUNT'] || WebUiConfig.getAutoLoginAccount();
if (autoLoginAccount) {
try {
const { result, message } = await WebUiDataRuntime.requestQuickLogin(autoLoginAccount);
if (!result) {
throw new Error(message);
}
console.log(`[NapCat] [WebUi] Auto login account: ${autoLoginAccount}`);
} catch (error) {
console.log(`[NapCat] [WebUi] Auto login account failed.` + error);
}
}
}, 30000);
// ------------注册中间件------------ // ------------注册中间件------------
// 使用express的json中间件 // 使用express的json中间件
app.use(express.json()); app.use(express.json());
@@ -46,15 +73,32 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// 如果是webui字体文件挂载字体文件 // 如果是webui字体文件挂载字体文件
app.use('/webui/fonts/AaCute.woff', async (_req, res, next) => { app.use('/webui/fonts/AaCute.woff', async (_req, res, next) => {
const isFontExist = await WebUiConfigWrapper.CheckWebUIFontExist(); const isFontExist = await WebUiConfig.CheckWebUIFontExist();
console.log(isFontExist, 'isFontExist');
if (isFontExist) { if (isFontExist) {
res.sendFile(WebUiConfigWrapper.GetWebUIFontPath()); res.sendFile(WebUiConfig.GetWebUIFontPath());
} else { } else {
next(); next();
} }
}); });
// 如果是自定义色彩构建一个css文件
app.use('/files/theme.css', async (_req, res) => {
const colors = await WebUiConfig.GetTheme();
let css = ':root, .light, [data-theme="light"] {';
for (const key in colors.light) {
css += `${key}: ${colors.light[key]};`;
}
css += '}';
css += '.dark, [data-theme="dark"] {';
for (const key in colors.dark) {
css += `${key}: ${colors.dark[key]};`;
}
css += '}';
res.send(css);
});
// ------------中间件结束------------ // ------------中间件结束------------
// ------------挂载路由------------ // ------------挂载路由------------
@@ -75,7 +119,7 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// 初始服务(先放个首页) // 初始服务(先放个首页)
app.all('/', (_req, res) => { app.all('/', (_req, res) => {
sendSuccess(res, null, 'NapCat WebAPI is now running!'); res.status(301).header('Location', '/webui').send();
}); });
// 错误处理中间件捕获multer的错误 // 错误处理中间件捕获multer的错误
@@ -92,16 +136,74 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
}); });
// ------------启动服务------------ // ------------启动服务------------
server.listen(config.port, config.host, async () => { server.listen(port, host, async () => {
// 启动后打印出相关地址 // 启动后打印出相关地址
const port = config.port.toString(), let searchParams = { token: token };
searchParams = { token: config.token }; if (host !== '' && host !== '0.0.0.0') {
if (config.host !== '' && config.host !== '0.0.0.0') {
logger.log( logger.log(
`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(config.host, port, '/webui', searchParams)}` `[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(host, port.toString(), '/webui', searchParams)}`
); );
} }
logger.log(`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', port, '/webui', searchParams)}`); logger.log(
`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', port.toString(), '/webui', searchParams)}`
);
}); });
// ------------Over------------ // ------------Over------------
} }
async function tryUseHost(host: string): Promise<string> {
return new Promise((resolve, reject) => {
try {
const server = net.createServer();
server.on('listening', () => {
server.close();
resolve(host);
});
server.on('error', (err: any) => {
if (err.code === 'EADDRNOTAVAIL') {
reject(new Error('主机地址验证失败,可能为非本机地址'));
} else {
reject(new Error(`遇到错误: ${err.code}`));
}
});
// 尝试监听 让系统随机分配一个端口
server.listen(0, host);
} catch (error) {
// 这里捕获到的错误应该是启动服务器时的同步错误
reject(new Error(`服务器启动时发生错误: ${error}`));
}
});
}
async function tryUsePort(port: number, host: string, tryCount: number = 0): Promise<number> {
return new Promise((resolve, reject) => {
try {
const server = net.createServer();
server.on('listening', () => {
server.close();
resolve(port);
});
server.on('error', (err: any) => {
if (err.code === 'EADDRINUSE') {
if (tryCount < MAX_PORT_TRY) {
// 使用循环代替递归
resolve(tryUsePort(port + 1, host, tryCount + 1));
} else {
reject(new Error(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`));
}
} else {
reject(new Error(`遇到错误: ${err.code}`));
}
});
// 尝试监听端口
server.listen(port, host);
} catch (error) {
// 这里捕获到的错误应该是启动服务器时的同步错误
reject(new Error(`服务器启动时发生错误: ${error}`));
}
});
}

View File

@@ -7,6 +7,15 @@ import { WebUiDataRuntime } from '@webapi/helper/Data';
import { sendSuccess, sendError } from '@webapi/utils/response'; import { sendSuccess, sendError } from '@webapi/utils/response';
import { isEmpty } from '@webapi/utils/check'; import { isEmpty } from '@webapi/utils/check';
// 检查是否使用默认Token
export const CheckDefaultTokenHandler: RequestHandler = async (_, res) => {
const webuiToken = await WebUiConfig.GetWebUIConfig();
if (webuiToken.token === 'napcat') {
return sendSuccess(res, true);
}
return sendSuccess(res, false);
};
// 登录 // 登录
export const LoginHandler: RequestHandler = async (req, res) => { export const LoginHandler: RequestHandler = async (req, res) => {
// 获取WebUI配置 // 获取WebUI配置
@@ -93,7 +102,7 @@ export const UpdateTokenHandler: RequestHandler = async (req, res) => {
try { try {
// 注销当前的Token // 注销当前的Token
if (authorization) { if (authorization) {
const CredentialBase64: string = authorization.split(' ')[1]; const CredentialBase64: string = authorization.split(' ')[1] as string;
const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString()); const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString());
AuthHelper.revokeCredential(Credential); AuthHelper.revokeCredential(Credential);
} }

View File

@@ -2,14 +2,25 @@ import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '@webapi/helper/Data'; import { WebUiDataRuntime } from '@webapi/helper/Data';
import { sendSuccess } from '@webapi/utils/response'; import { sendSuccess } from '@webapi/utils/response';
import { WebUiConfig } from '@/webui';
export const PackageInfoHandler: RequestHandler = (_, res) => { export const PackageInfoHandler: RequestHandler = (_, res) => {
const data = WebUiDataRuntime.getPackageJson(); const data = WebUiDataRuntime.getPackageJson();
sendSuccess(res, data); sendSuccess(res, data);
}; };
export const QQVersionHandler: RequestHandler = (_, res) => { export const QQVersionHandler: RequestHandler = (_, res) => {
const data = WebUiDataRuntime.getQQVersion(); const data = WebUiDataRuntime.getQQVersion();
sendSuccess(res, data); sendSuccess(res, data);
}; };
export const GetThemeConfigHandler: RequestHandler = async (_, res) => {
const data = await WebUiConfig.GetTheme();
sendSuccess(res, data);
};
export const SetThemeConfigHandler: RequestHandler = async (req, res) => {
const { theme } = req.body;
await WebUiConfig.UpdateTheme(theme);
sendSuccess(res, { message: '更新成功' });
};

View File

@@ -7,9 +7,9 @@ import os from 'os';
import compressing from 'compressing'; import compressing from 'compressing';
import { PassThrough } from 'stream'; import { PassThrough } from 'stream';
import multer from 'multer'; import multer from 'multer';
import { WebUiConfigWrapper } from '../helper/config';
import webUIFontUploader from '../uploader/webui_font'; import webUIFontUploader from '../uploader/webui_font';
import diskUploader from '../uploader/disk'; import diskUploader from '../uploader/disk';
import { WebUiConfig } from '@/webui';
const isWindows = os.platform() === 'win32'; const isWindows = os.platform() === 'win32';
@@ -384,8 +384,8 @@ export const UploadWebUIFontHandler: RequestHandler = async (req, res) => {
// 删除WebUI字体文件处理方法 // 删除WebUI字体文件处理方法
export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => { export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => {
try { try {
const fontPath = WebUiConfigWrapper.GetWebUIFontPath(); const fontPath = WebUiConfig.GetWebUIFontPath();
const exists = await WebUiConfigWrapper.CheckWebUIFontExist(); const exists = await WebUiConfig.CheckWebUIFontExist();
if (!exists) { if (!exists) {
return sendSuccess(res, true); return sendSuccess(res, true);

View File

@@ -1,9 +1,10 @@
import type { RequestHandler } from 'express'; import type { RequestHandler } from 'express';
import { sendError, sendSuccess } from '../utils/response'; import { sendError, sendSuccess } from '../utils/response';
import { WebUiConfigWrapper } from '../helper/config';
import { logSubscription } from '@/common/log'; import { logSubscription } from '@/common/log';
import { terminalManager } from '../terminal/terminal_manager'; import { terminalManager } from '../terminal/terminal_manager';
import { WebUiConfig } from '@/webui';
// 判断是否是 macos
const isMacOS = process.platform === 'darwin';
// 日志记录 // 日志记录
export const LogHandler: RequestHandler = async (req, res) => { export const LogHandler: RequestHandler = async (req, res) => {
const filename = req.query['id']; const filename = req.query['id'];
@@ -14,13 +15,13 @@ export const LogHandler: RequestHandler = async (req, res) => {
if (filename.includes('..')) { if (filename.includes('..')) {
return sendError(res, 'ID不合法'); return sendError(res, 'ID不合法');
} }
const logContent = await WebUiConfigWrapper.GetLogContent(filename); const logContent = await WebUiConfig.GetLogContent(filename);
return sendSuccess(res, logContent); return sendSuccess(res, logContent);
}; };
// 日志列表 // 日志列表
export const LogListHandler: RequestHandler = async (_, res) => { export const LogListHandler: RequestHandler = async (_, res) => {
const logList = await WebUiConfigWrapper.GetLogsList(); const logList = await WebUiConfig.GetLogsList();
return sendSuccess(res, logList); return sendSuccess(res, logList);
}; };
@@ -43,6 +44,9 @@ export const LogRealTimeHandler: RequestHandler = async (req, res) => {
// 终端相关处理器 // 终端相关处理器
export const CreateTerminalHandler: RequestHandler = async (req, res) => { export const CreateTerminalHandler: RequestHandler = async (req, res) => {
if (isMacOS) {
return sendError(res, 'MacOS不支持终端');
}
try { try {
const { cols, rows } = req.body; const { cols, rows } = req.body;
const { id } = terminalManager.createTerminal(cols, rows); const { id } = terminalManager.createTerminal(cols, rows);

View File

@@ -3,9 +3,10 @@ import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '@webapi/helper/Data'; import { WebUiDataRuntime } from '@webapi/helper/Data';
import { isEmpty } from '@webapi/utils/check'; import { isEmpty } from '@webapi/utils/check';
import { sendError, sendSuccess } from '@webapi/utils/response'; import { sendError, sendSuccess } from '@webapi/utils/response';
import { WebUiConfig } from '@/webui';
// 获取QQ登录二维码 // 获取QQ登录二维码
export const QQGetQRcodeHandler: RequestHandler = async (req, res) => { export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
// 判断是否已经登录 // 判断是否已经登录
if (WebUiDataRuntime.getQQLoginStatus()) { if (WebUiDataRuntime.getQQLoginStatus()) {
// 已经登录 // 已经登录
@@ -25,7 +26,7 @@ export const QQGetQRcodeHandler: RequestHandler = async (req, res) => {
}; };
// 获取QQ登录状态 // 获取QQ登录状态
export const QQCheckLoginStatusHandler: RequestHandler = async (req, res) => { export const QQCheckLoginStatusHandler: RequestHandler = async (_, res) => {
const data = { const data = {
isLogin: WebUiDataRuntime.getQQLoginStatus(), isLogin: WebUiDataRuntime.getQQLoginStatus(),
qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(), qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(),
@@ -74,3 +75,16 @@ export const getQQLoginInfoHandler: RequestHandler = async (_, res) => {
const data = WebUiDataRuntime.getQQLoginInfo(); const data = WebUiDataRuntime.getQQLoginInfo();
return sendSuccess(res, data); return sendSuccess(res, data);
}; };
// 获取自动登录QQ账号
export const getAutoLoginAccountHandler: RequestHandler = async (_, res) => {
const data = WebUiConfig.getAutoLoginAccount();
return sendSuccess(res, data);
};
// 设置自动登录QQ账号
export const setAutoLoginAccountHandler: RequestHandler = async (req, res) => {
const { uin } = req.body;
await WebUiConfig.UpdateAutoLoginAccount(uin);
return sendSuccess(res, null);
};

View File

@@ -1,167 +1,86 @@
import { webUiPathWrapper } from '@/webui'; import { webUiPathWrapper } from '@/webui';
import { Type, Static } from '@sinclair/typebox';
import Ajv from 'ajv';
import fs, { constants } from 'node:fs/promises'; import fs, { constants } from 'node:fs/promises';
import * as net from 'node:net';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { deepMerge } from '../utils/object';
import { themeType } from '../types/theme';
// 限制尝试端口的次数,避免死循环 // 限制尝试端口的次数,避免死循环
const MAX_PORT_TRY = 100;
async function tryUseHost(host: string): Promise<string> { // 定义配置的类型
return new Promise((resolve, reject) => { const WebUiConfigSchema = Type.Object({
try { host: Type.String({ default: '0.0.0.0' }),
const server = net.createServer(); port: Type.Number({ default: 6099 }),
server.on('listening', () => { token: Type.String({ default: 'napcat' }),
server.close(); loginRate: Type.Number({ default: 10 }),
resolve(host); autoLoginAccount: Type.String({ default: '' }),
}); theme: themeType,
});
server.on('error', (err: any) => { export type WebUiConfigType = Static<typeof WebUiConfigSchema>;
if (err.code === 'EADDRNOTAVAIL') {
reject(new Error('主机地址验证失败,可能为非本机地址'));
} else {
reject(new Error(`遇到错误: ${err.code}`));
}
});
// 尝试监听 让系统随机分配一个端口
server.listen(0, host);
} catch (error) {
// 这里捕获到的错误应该是启动服务器时的同步错误
reject(new Error(`服务器启动时发生错误: ${error}`));
}
});
}
async function tryUsePort(port: number, host: string, tryCount: number = 0): Promise<number> {
return new Promise((resolve, reject) => {
try {
const server = net.createServer();
server.on('listening', () => {
server.close();
resolve(port);
});
server.on('error', (err: any) => {
if (err.code === 'EADDRINUSE') {
if (tryCount < MAX_PORT_TRY) {
// 使用循环代替递归
resolve(tryUsePort(port + 1, host, tryCount + 1));
} else {
reject(new Error(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`));
}
} else {
reject(new Error(`遇到错误: ${err.code}`));
}
});
// 尝试监听端口
server.listen(port, host);
} catch (error) {
// 这里捕获到的错误应该是启动服务器时的同步错误
reject(new Error(`服务器启动时发生错误: ${error}`));
}
});
}
// 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件 // 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件
export class WebUiConfigWrapper { export class WebUiConfigWrapper {
WebUiConfigData: WebUiConfigType | undefined = undefined; WebUiConfigData: WebUiConfigType | undefined = undefined;
private applyDefaults<T>(obj: Partial<T>, defaults: T): T { private validateAndApplyDefaults(config: Partial<WebUiConfigType>): WebUiConfigType {
const result = { ...defaults } as T; new Ajv({ coerceTypes: true, useDefaults: true }).compile(WebUiConfigSchema)(config);
for (const key in obj) { return config as WebUiConfigType;
if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { }
result[key] = this.applyDefaults(obj[key], defaults[key]);
} else if (obj[key] !== undefined) { private async ensureConfigFileExists(configPath: string): Promise<void> {
result[key] = obj[key] as T[Extract<keyof T, string>]; const configExists = await fs
} .access(configPath, constants.F_OK)
.then(() => true)
.catch(() => false);
if (!configExists) {
await fs.writeFile(configPath, JSON.stringify(this.validateAndApplyDefaults({}), null, 4));
}
}
private async readAndValidateConfig(configPath: string): Promise<WebUiConfigType> {
const fileContent = await fs.readFile(configPath, 'utf-8');
return this.validateAndApplyDefaults(JSON.parse(fileContent));
}
private async writeConfig(configPath: string, config: WebUiConfigType): Promise<void> {
const hasWritePermission = await fs
.access(configPath, constants.W_OK)
.then(() => true)
.catch(() => false);
if (hasWritePermission) {
await fs.writeFile(configPath, JSON.stringify(config, null, 4));
} else {
console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`);
} }
return result;
} }
async GetWebUIConfig(): Promise<WebUiConfigType> { async GetWebUIConfig(): Promise<WebUiConfigType> {
if (this.WebUiConfigData) { if (this.WebUiConfigData) {
return this.WebUiConfigData; return this.WebUiConfigData;
} }
const defaultconfig: WebUiConfigType = {
host: '0.0.0.0',
port: 6099,
token: '', // 默认先填空,空密码无法登录
loginRate: 3,
};
try {
defaultconfig.token = Math.random().toString(36).slice(2); //生成随机密码
} catch (e) {
console.log('随机密码生成失败', e);
}
try { try {
const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); const configPath = resolve(webUiPathWrapper.configPath, './webui.json');
await this.ensureConfigFileExists(configPath);
if ( const parsedConfig = await this.readAndValidateConfig(configPath);
!(await fs
.access(configPath, constants.F_OK)
.then(() => true)
.catch(() => false))
) {
await fs.writeFile(configPath, JSON.stringify(defaultconfig, null, 4));
}
const fileContent = await fs.readFile(configPath, 'utf-8');
const parsedConfig = this.applyDefaults(JSON.parse(fileContent) as Partial<WebUiConfigType>, defaultconfig);
if (
await fs
.access(configPath, constants.W_OK)
.then(() => true)
.catch(() => false)
) {
await fs.writeFile(configPath, JSON.stringify(parsedConfig, null, 4));
} else {
console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`);
}
const [host_err, host] = await tryUseHost(parsedConfig.host)
.then((data) => [null, data])
.catch((err) => [err, null]);
if (host_err) {
console.log('host不可用', host_err);
parsedConfig.port = 0; // 设置为0禁用WebUI
} else {
parsedConfig.host = host;
const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host)
.then((data) => [null, data])
.catch((err) => [err, null]);
if (port_err) {
console.log('port不可用', port_err);
parsedConfig.port = 0; // 设置为0禁用WebUI
} else {
parsedConfig.port = port;
}
}
this.WebUiConfigData = parsedConfig; this.WebUiConfigData = parsedConfig;
return this.WebUiConfigData; return this.WebUiConfigData;
} catch (e) { } catch (e) {
console.log('读取配置文件失败', e); console.log('读取配置文件失败', e);
return this.validateAndApplyDefaults({});
} }
return defaultconfig; // 理论上这行代码到不了,到了只能返回默认配置了
} }
async UpdateWebUIConfig(newConfig: Partial<WebUiConfigType>): Promise<void> { async UpdateWebUIConfig(newConfig: Partial<WebUiConfigType>): Promise<void> {
const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); const configPath = resolve(webUiPathWrapper.configPath, './webui.json');
const currentConfig = await this.GetWebUIConfig(); const currentConfig = await this.GetWebUIConfig();
const updatedConfig = this.applyDefaults(newConfig, currentConfig); const mergedConfig = deepMerge({ ...currentConfig }, newConfig);
const updatedConfig = this.validateAndApplyDefaults(mergedConfig);
if ( await this.writeConfig(configPath, updatedConfig);
await fs this.WebUiConfigData = updatedConfig;
.access(configPath, constants.W_OK)
.then(() => true)
.catch(() => false)
) {
await fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 4));
this.WebUiConfigData = updatedConfig;
} else {
console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`);
}
} }
async UpdateToken(oldToken: string, newToken: string): Promise<void> { async UpdateToken(oldToken: string, newToken: string): Promise<void> {
@@ -173,53 +92,53 @@ export class WebUiConfigWrapper {
} }
// 获取日志文件夹路径 // 获取日志文件夹路径
public static async GetLogsPath(): Promise<string> { async GetLogsPath(): Promise<string> {
return resolve(webUiPathWrapper.logsPath); return resolve(webUiPathWrapper.logsPath);
} }
// 获取日志列表 // 获取日志列表
public static async GetLogsList(): Promise<string[]> { async GetLogsList(): Promise<string[]> {
if ( const logsPath = resolve(webUiPathWrapper.logsPath);
await fs const logsExist = await fs
.access(webUiPathWrapper.logsPath, constants.F_OK) .access(logsPath, constants.F_OK)
.then(() => true) .then(() => true)
.catch(() => false) .catch(() => false);
) { if (logsExist) {
return (await fs.readdir(webUiPathWrapper.logsPath)) return (await fs.readdir(logsPath))
.filter((file) => file.endsWith('.log')) .filter((file) => file.endsWith('.log'))
.map((file) => file.replace('.log', '')); .map((file) => file.replace('.log', ''));
} }
return []; return [];
} }
// 获取指定日志文件内容 // 获取指定日志文件内容
public static async GetLogContent(filename: string): Promise<string> { async GetLogContent(filename: string): Promise<string> {
const logPath = resolve(webUiPathWrapper.logsPath, `${filename}.log`); const logPath = resolve(webUiPathWrapper.logsPath, `${filename}.log`);
if ( const logExists = await fs
await fs .access(logPath, constants.R_OK)
.access(logPath, constants.R_OK) .then(() => true)
.then(() => true) .catch(() => false);
.catch(() => false) if (logExists) {
) {
return await fs.readFile(logPath, 'utf-8'); return await fs.readFile(logPath, 'utf-8');
} }
return ''; return '';
} }
// 获取字体文件夹内的字体列表 // 获取字体文件夹内的字体列表
public static async GetFontList(): Promise<string[]> { async GetFontList(): Promise<string[]> {
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts'); const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
if ( const fontsExist = await fs
await fs .access(fontsPath, constants.F_OK)
.access(fontsPath, constants.F_OK) .then(() => true)
.then(() => true) .catch(() => false);
.catch(() => false) if (fontsExist) {
) {
return (await fs.readdir(fontsPath)).filter((file) => file.endsWith('.ttf')); return (await fs.readdir(fontsPath)).filter((file) => file.endsWith('.ttf'));
} }
return []; return [];
} }
// 判断字体是否存在webui.woff // 判断字体是否存在webui.woff
public static async CheckWebUIFontExist(): Promise<boolean> { async CheckWebUIFontExist(): Promise<boolean> {
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts'); const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
return await fs return await fs
.access(resolve(fontsPath, './webui.woff'), constants.F_OK) .access(resolve(fontsPath, './webui.woff'), constants.F_OK)
@@ -228,7 +147,33 @@ export class WebUiConfigWrapper {
} }
// 获取webui字体文件路径 // 获取webui字体文件路径
public static GetWebUIFontPath(): string { GetWebUIFontPath(): string {
return resolve(webUiPathWrapper.configPath, './fonts/webui.woff'); return resolve(webUiPathWrapper.configPath, './fonts/webui.woff');
} }
getAutoLoginAccount(): string | undefined {
return this.WebUiConfigData?.autoLoginAccount;
}
// 获取自动登录账号
async GetAutoLoginAccount(): Promise<string> {
return (await this.GetWebUIConfig()).autoLoginAccount;
}
// 更新自动登录账号
async UpdateAutoLoginAccount(uin: string): Promise<void> {
await this.UpdateWebUIConfig({ autoLoginAccount: uin });
}
// 获取主题内容
async GetTheme(): Promise<WebUiConfigType['theme']> {
const config = await this.GetWebUIConfig();
return config.theme;
}
// 更新主题内容
async UpdateTheme(theme: WebUiConfigType['theme']): Promise<void> {
await this.UpdateWebUIConfig({ theme: theme });
}
} }

View File

@@ -1,5 +1,5 @@
import { Router } from 'express'; import { Router } from 'express';
import { PackageInfoHandler, QQVersionHandler } from '../api/BaseInfo'; import { GetThemeConfigHandler, PackageInfoHandler, QQVersionHandler, SetThemeConfigHandler } from '../api/BaseInfo';
import { StatusRealTimeHandler } from '@webapi/api/Status'; import { StatusRealTimeHandler } from '@webapi/api/Status';
import { GetProxyHandler } from '../api/Proxy'; import { GetProxyHandler } from '../api/Proxy';
@@ -9,4 +9,7 @@ router.get('/QQVersion', QQVersionHandler);
router.get('/PackageInfo', PackageInfoHandler); router.get('/PackageInfo', PackageInfoHandler);
router.get('/GetSysStatusRealTime', StatusRealTimeHandler); router.get('/GetSysStatusRealTime', StatusRealTimeHandler);
router.get('/proxy', GetProxyHandler); router.get('/proxy', GetProxyHandler);
router.get('/Theme', GetThemeConfigHandler);
router.post('/SetTheme', SetThemeConfigHandler);
export { router as BaseRouter }; export { router as BaseRouter };

View File

@@ -23,6 +23,9 @@ const router = Router();
const apiLimiter = rateLimit({ const apiLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1分钟内 windowMs: 1 * 60 * 1000, // 1分钟内
max: 60, // 最大60个请求 max: 60, // 最大60个请求
validate: {
xForwardedForHeader: false,
},
}); });
router.use(apiLimiter); router.use(apiLimiter);

View File

@@ -7,6 +7,8 @@ import {
QQSetQuickLoginHandler, QQSetQuickLoginHandler,
QQGetLoginListNewHandler, QQGetLoginListNewHandler,
getQQLoginInfoHandler, getQQLoginInfoHandler,
getAutoLoginAccountHandler,
setAutoLoginAccountHandler,
} from '@webapi/api/QQLogin'; } from '@webapi/api/QQLogin';
const router = Router(); const router = Router();
@@ -22,5 +24,9 @@ router.post('/GetQQLoginQrcode', QQGetQRcodeHandler);
router.post('/SetQuickLogin', QQSetQuickLoginHandler); router.post('/SetQuickLogin', QQSetQuickLoginHandler);
// router:获取QQ登录信息 // router:获取QQ登录信息
router.post('/GetQQLoginInfo', getQQLoginInfoHandler); router.post('/GetQQLoginInfo', getQQLoginInfoHandler);
// router:获取快速登录QQ账号
router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler);
// router:设置自动登录QQ账号
router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
export { router as QQLoginRouter }; export { router as QQLoginRouter };

View File

@@ -1,6 +1,12 @@
import { Router } from 'express'; import { Router } from 'express';
import { checkHandler, LoginHandler, LogoutHandler, UpdateTokenHandler } from '@webapi/api/Auth'; import {
CheckDefaultTokenHandler,
checkHandler,
LoginHandler,
LogoutHandler,
UpdateTokenHandler,
} from '@webapi/api/Auth';
const router = Router(); const router = Router();
// router:登录 // router:登录
@@ -11,5 +17,7 @@ router.post('/check', checkHandler);
router.post('/logout', LogoutHandler); router.post('/logout', LogoutHandler);
// router:更新token // router:更新token
router.post('/update_token', UpdateTokenHandler); router.post('/update_token', UpdateTokenHandler);
// router:检查默认token
router.get('/check_using_default_token', CheckDefaultTokenHandler);
export { router as AuthRouter }; export { router as AuthRouter };

View File

@@ -0,0 +1,260 @@
import { Type } from '@sinclair/typebox';
export const themeType = Type.Object(
{
dark: Type.Record(Type.String(), Type.String()),
light: Type.Record(Type.String(), Type.String()),
},
{
default: {
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',
},
},
}
);

View File

@@ -0,0 +1,22 @@
export function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
// 如果 source[key] 为 undefined则跳过保留 target[key]
if (source[key] === undefined) {
continue;
}
if (
target[key] !== undefined &&
typeof target[key] === 'object' &&
!Array.isArray(target[key]) &&
typeof source[key] === 'object' &&
!Array.isArray(source[key])
) {
target[key] = deepMerge({ ...target[key] }, source[key]!) as T[Extract<keyof T, string>];
} else {
target[key] = source[key]! as T[Extract<keyof T, string>];
}
}
}
return target;
}

View File

@@ -8,8 +8,7 @@ const external = [
'silk-wasm', 'silk-wasm',
'ws', 'ws',
'express', 'express',
'@ffmpeg.wasm/core-mt', '@ffmpeg.wasm/core-mt'
'piscina'
]; ];
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat(); const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();