mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
50 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
197eec40ad | ||
![]() |
b72156866d | ||
![]() |
59a7d12a8c | ||
![]() |
179351b23a | ||
![]() |
790809e8e5 | ||
![]() |
1414a8a8c9 | ||
![]() |
9ab41734a5 | ||
![]() |
03cace2ea1 | ||
![]() |
c7371ab869 | ||
![]() |
b32d4b618c | ||
![]() |
3a27f37686 | ||
![]() |
fe2d21979d | ||
![]() |
48b1f3d4f0 | ||
![]() |
93ed589ac7 | ||
![]() |
96de9e2c16 | ||
![]() |
b25f9d3bec | ||
![]() |
15854c605b | ||
![]() |
ac193cc94a | ||
![]() |
d626f872e6 | ||
![]() |
3eb66fa34a | ||
![]() |
0fdd0175b7 | ||
![]() |
dec9b477e0 | ||
![]() |
a0a4b0dd1d | ||
![]() |
8dc6da56a7 | ||
![]() |
b4e07aacfe | ||
![]() |
19b47f0f42 | ||
![]() |
f9ef3d63c7 | ||
![]() |
2b574d33b5 | ||
![]() |
6039e9bb46 | ||
![]() |
adfd4b043f | ||
![]() |
719189be55 | ||
![]() |
ef9907f4b6 | ||
![]() |
16b7447df1 | ||
![]() |
4157746478 | ||
![]() |
5120786708 | ||
![]() |
0176fa75ef | ||
![]() |
e6968f2d80 | ||
![]() |
c0dd8a53e8 | ||
![]() |
3cb3135235 | ||
![]() |
28182cac64 | ||
![]() |
73b80d2482 | ||
![]() |
f22eb22409 | ||
![]() |
4a95b17a47 | ||
![]() |
f4a71159fd | ||
![]() |
c0431e3dc2 | ||
![]() |
7f87cee282 | ||
![]() |
c24c704439 | ||
![]() |
232e5d55b8 | ||
![]() |
da24ae7e1c | ||
![]() |
8fc13f8a8f |
@@ -12,7 +12,7 @@ insert_final_newline = true
|
||||
# Set default charset
|
||||
charset = utf-8
|
||||
|
||||
# 2 space indentation
|
||||
# 4 space indentation
|
||||
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
@@ -21,4 +21,4 @@ indent_size = 4
|
||||
charset = latin1
|
||||
|
||||
# Unfortunately, EditorConfig doesn't support space configuration inside import braces directly.
|
||||
# You'll need to rely on your linter/formatter like ESLint or Prettier for that.
|
||||
# You'll need to rely on your linter/formatter like ESLint or Prettier for that.
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
# Develop
|
||||
node_modules/
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
out/
|
||||
dist/
|
||||
/src/core.lib/common/
|
||||
@@ -13,4 +14,4 @@ devconfig/*
|
||||
# Build
|
||||
*.db
|
||||
checkVersion.sh
|
||||
bun.lockb
|
||||
bun.lockb
|
||||
|
@@ -64,4 +64,4 @@ NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进
|
||||
|
||||
## 开源附加
|
||||
|
||||
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。**
|
||||
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**本仓库仅用于提高IM易用性,实现类似Hook推送,此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
|
||||
|
@@ -1,70 +1,32 @@
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import _import from "eslint-plugin-import";
|
||||
import { fixupPluginRules } from "@eslint/compat";
|
||||
import eslint from '@eslint/js';
|
||||
import tsEslintPlugin from '@typescript-eslint/eslint-plugin';
|
||||
import tsEslintParser from '@typescript-eslint/parser';
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
export default [{
|
||||
ignores: ["src/core/proto/"],
|
||||
}, ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), {
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
import: fixupPluginRules(_import),
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
|
||||
settings: {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts"],
|
||||
},
|
||||
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
const customTsFlatConfig = [
|
||||
{
|
||||
name: 'typescript-eslint/base',
|
||||
languageOptions: {
|
||||
parser: tsEslintParser,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
NodeJS: 'readonly', // 添加 NodeJS 全局变量
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
indent: ["error", 4],
|
||||
semi: ["error", "always"],
|
||||
"no-unused-vars": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
},
|
||||
}, {
|
||||
files: ["**/.eslintrc.{js,cjs}"],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
...tsEslintPlugin.configs.recommended.rules,
|
||||
'quotes': ['error', 'single'], // 使用单引号
|
||||
'semi': ['error', 'always'], // 强制使用分号
|
||||
'indent': ['error', 4], // 使用 4 空格缩进
|
||||
},
|
||||
ecmaVersion: 5,
|
||||
sourceType: "commonjs",
|
||||
plugins: {
|
||||
'@typescript-eslint': tsEslintPlugin,
|
||||
},
|
||||
ignores: ['src/webui/**'], // 忽略 src/webui/ 目录所有文件
|
||||
},
|
||||
}];
|
||||
];
|
||||
|
||||
export default [eslint.configs.recommended, ...customTsFlatConfig];
|
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.4.12",
|
||||
"version": "4.4.16",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -4,12 +4,15 @@
|
||||
"version": "0.0.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host=0.0.0.0",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint -c eslint.config.mjs ./src/**/**/*.{ts,tsx} --fix",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@heroui/avatar": "2.2.7",
|
||||
"@heroui/breadcrumbs": "2.2.7",
|
||||
"@heroui/button": "2.2.10",
|
||||
@@ -33,13 +36,14 @@
|
||||
"@heroui/spinner": "2.2.7",
|
||||
"@heroui/switch": "2.2.9",
|
||||
"@heroui/system": "2.4.7",
|
||||
"@heroui/table": "^2.2.9",
|
||||
"@heroui/tabs": "2.2.8",
|
||||
"@heroui/theme": "2.4.6",
|
||||
"@heroui/tooltip": "2.2.8",
|
||||
"@monaco-editor/loader": "^1.4.0",
|
||||
"@monaco-editor/react": "4.7.0-rc.0",
|
||||
"@react-aria/visually-hidden": "3.8.18",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"@react-aria/visually-hidden": "^3.8.19",
|
||||
"@reduxjs/toolkit": "^2.5.1",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
@@ -47,17 +51,18 @@
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.7.9",
|
||||
"clsx": "2.1.1",
|
||||
"clsx": "^2.1.1",
|
||||
"echarts": "^5.5.1",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"framer-motion": "^11.15.0",
|
||||
"framer-motion": "^12.0.6",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"motion": "^11.15.0",
|
||||
"motion": "^12.0.6",
|
||||
"path-browserify": "^1.0.1",
|
||||
"qface": "^1.4.1",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"quill": "^2.0.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
@@ -65,41 +70,42 @@
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-router-dom": "7.1.0",
|
||||
"react-router-dom": "^7.1.4",
|
||||
"react-use-websocket": "^4.11.1",
|
||||
"react-window": "^1.8.11",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"tailwind-variants": "0.3.0",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tailwind-variants": "^0.3.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@react-types/shared": "^3.26.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
"@types/fabric": "^5.3.9",
|
||||
"@types/node": "22.10.2",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.18.1",
|
||||
"@typescript-eslint/parser": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "10.4.20",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-prettier": "5.2.3",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"globals": "^15.14.0",
|
||||
"postcss": "8.4.49",
|
||||
"prettier": "3.4.2",
|
||||
"typescript": "5.7.2",
|
||||
"postcss": "^8.5.1",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.5",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
|
@@ -5,7 +5,7 @@ import { IoMdRefresh } from 'react-icons/io'
|
||||
export interface SaveButtonsProps {
|
||||
onSubmit: () => void
|
||||
reset: () => void
|
||||
refresh: () => void
|
||||
refresh?: () => void
|
||||
isSubmitting: boolean
|
||||
}
|
||||
|
||||
@@ -27,21 +27,23 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
|
||||
取消更改
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
color="danger"
|
||||
isLoading={isSubmitting}
|
||||
onPress={() => onSubmit()}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="secondary"
|
||||
radius="full"
|
||||
variant="flat"
|
||||
onPress={() => refresh()}
|
||||
>
|
||||
<IoMdRefresh size={24} />
|
||||
</Button>
|
||||
{refresh && (
|
||||
<Button
|
||||
isIconOnly
|
||||
color="secondary"
|
||||
radius="full"
|
||||
variant="flat"
|
||||
onPress={() => refresh()}
|
||||
>
|
||||
<IoMdRefresh size={24} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
166
napcat.webui/src/components/file_icon.tsx
Normal file
166
napcat.webui/src/components/file_icon.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
FaFile,
|
||||
FaFileAudio,
|
||||
FaFileCode,
|
||||
FaFileCsv,
|
||||
FaFileExcel,
|
||||
FaFileImage,
|
||||
FaFileLines,
|
||||
FaFilePdf,
|
||||
FaFilePowerpoint,
|
||||
FaFileVideo,
|
||||
FaFileWord,
|
||||
FaFileZipper,
|
||||
FaFolderClosed
|
||||
} from 'react-icons/fa6'
|
||||
|
||||
export interface FileIconProps {
|
||||
name?: string
|
||||
isDirectory?: boolean
|
||||
}
|
||||
|
||||
const FileIcon = (props: FileIconProps) => {
|
||||
const { name, isDirectory = false } = props
|
||||
if (isDirectory) {
|
||||
return <FaFolderClosed className="text-yellow-500" />
|
||||
}
|
||||
|
||||
const ext = name?.split('.').pop() || ''
|
||||
if (ext) {
|
||||
switch (ext.toLowerCase()) {
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'png':
|
||||
case 'gif':
|
||||
case 'svg':
|
||||
case 'bmp':
|
||||
case 'ico':
|
||||
case 'webp':
|
||||
case 'tiff':
|
||||
case 'tif':
|
||||
case 'heic':
|
||||
case 'heif':
|
||||
case 'avif':
|
||||
case 'apng':
|
||||
case 'flif':
|
||||
case 'ai':
|
||||
case 'psd':
|
||||
case 'xcf':
|
||||
case 'sketch':
|
||||
case 'fig':
|
||||
case 'xd':
|
||||
case 'svgz':
|
||||
return <FaFileImage className="text-green-500" />
|
||||
case 'pdf':
|
||||
return <FaFilePdf className="text-red-500" />
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return <FaFileWord className="text-blue-500" />
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
return <FaFileExcel className="text-green-500" />
|
||||
case 'csv':
|
||||
return <FaFileCsv className="text-green-500" />
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
return <FaFilePowerpoint className="text-red-500" />
|
||||
case 'zip':
|
||||
case 'rar':
|
||||
case '7z':
|
||||
case 'tar':
|
||||
case 'gz':
|
||||
case 'bz2':
|
||||
case 'xz':
|
||||
case 'lz':
|
||||
case 'lzma':
|
||||
case 'zst':
|
||||
case 'zstd':
|
||||
case 'z':
|
||||
case 'taz':
|
||||
case 'tz':
|
||||
case 'tzo':
|
||||
return <FaFileZipper className="text-green-500" />
|
||||
case 'txt':
|
||||
return <FaFileLines className="text-gray-500" />
|
||||
case 'mp3':
|
||||
case 'wav':
|
||||
case 'flac':
|
||||
return <FaFileAudio className="text-green-500" />
|
||||
case 'mp4':
|
||||
case 'avi':
|
||||
case 'mov':
|
||||
case 'wmv':
|
||||
return <FaFileVideo className="text-red-500" />
|
||||
case 'html':
|
||||
case 'css':
|
||||
case 'js':
|
||||
case 'ts':
|
||||
case 'jsx':
|
||||
case 'tsx':
|
||||
case 'json':
|
||||
case 'xml':
|
||||
case 'yaml':
|
||||
case 'yml':
|
||||
case 'md':
|
||||
case 'sh':
|
||||
case 'py':
|
||||
case 'java':
|
||||
case 'c':
|
||||
case 'cpp':
|
||||
case 'cs':
|
||||
case 'go':
|
||||
case 'php':
|
||||
case 'rb':
|
||||
case 'pl':
|
||||
case 'swift':
|
||||
case 'kt':
|
||||
case 'rs':
|
||||
case 'sql':
|
||||
case 'r':
|
||||
case 'scala':
|
||||
case 'groovy':
|
||||
case 'dart':
|
||||
case 'lua':
|
||||
case 'perl':
|
||||
case 'h':
|
||||
case 'm':
|
||||
case 'mm':
|
||||
case 'makefile':
|
||||
case 'cmake':
|
||||
case 'dockerfile':
|
||||
case 'gradle':
|
||||
case 'properties':
|
||||
case 'ini':
|
||||
case 'conf':
|
||||
case 'env':
|
||||
case 'bat':
|
||||
case 'cmd':
|
||||
case 'ps1':
|
||||
case 'psm1':
|
||||
case 'psd1':
|
||||
case 'ps1xml':
|
||||
case 'psc1':
|
||||
case 'pssc':
|
||||
case 'nuspec':
|
||||
case 'resx':
|
||||
case 'resw':
|
||||
case 'csproj':
|
||||
case 'vbproj':
|
||||
case 'vcxproj':
|
||||
case 'fsproj':
|
||||
case 'sln':
|
||||
case 'suo':
|
||||
case 'user':
|
||||
case 'userosscache':
|
||||
case 'sln.docstates':
|
||||
case 'dll':
|
||||
return <FaFileCode className="text-blue-500" />
|
||||
default:
|
||||
return <FaFile className="text-gray-500" />
|
||||
}
|
||||
}
|
||||
|
||||
return <FaFile className="text-gray-500" />
|
||||
}
|
||||
|
||||
export default FileIcon
|
@@ -0,0 +1,64 @@
|
||||
import { Button, ButtonGroup } from '@heroui/button'
|
||||
import { Input } from '@heroui/input'
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader
|
||||
} from '@heroui/modal'
|
||||
|
||||
interface CreateFileModalProps {
|
||||
isOpen: boolean
|
||||
fileType: 'file' | 'directory'
|
||||
newFileName: string
|
||||
onTypeChange: (type: 'file' | 'directory') => void
|
||||
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onClose: () => void
|
||||
onCreate: () => void
|
||||
}
|
||||
|
||||
export default function CreateFileModal({
|
||||
isOpen,
|
||||
fileType,
|
||||
newFileName,
|
||||
onTypeChange,
|
||||
onNameChange,
|
||||
onClose,
|
||||
onCreate
|
||||
}: CreateFileModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>新建</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex flex-col gap-4">
|
||||
<ButtonGroup color="danger">
|
||||
<Button
|
||||
variant={fileType === 'file' ? 'solid' : 'flat'}
|
||||
onPress={() => onTypeChange('file')}
|
||||
>
|
||||
文件
|
||||
</Button>
|
||||
<Button
|
||||
variant={fileType === 'directory' ? 'solid' : 'flat'}
|
||||
onPress={() => onTypeChange('directory')}
|
||||
>
|
||||
目录
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Input label="名称" value={newFileName} onChange={onNameChange} />
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onCreate}>
|
||||
创建
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
94
napcat.webui/src/components/file_manage/file_edit_modal.tsx
Normal file
94
napcat.webui/src/components/file_manage/file_edit_modal.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Code } from '@heroui/code'
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader
|
||||
} from '@heroui/modal'
|
||||
|
||||
import CodeEditor from '@/components/code_editor'
|
||||
|
||||
interface FileEditModalProps {
|
||||
isOpen: boolean
|
||||
file: { path: string; content: string } | null
|
||||
onClose: () => void
|
||||
onSave: () => void
|
||||
onContentChange: (newContent?: string) => void
|
||||
}
|
||||
|
||||
export default function FileEditModal({
|
||||
isOpen,
|
||||
file,
|
||||
onClose,
|
||||
onSave,
|
||||
onContentChange
|
||||
}: FileEditModalProps) {
|
||||
// 根据文件后缀返回对应语言
|
||||
const getLanguage = (filePath: string) => {
|
||||
if (filePath.endsWith('.js')) return 'javascript'
|
||||
if (filePath.endsWith('.ts')) return 'typescript'
|
||||
if (filePath.endsWith('.tsx')) return 'tsx'
|
||||
if (filePath.endsWith('.jsx')) return 'jsx'
|
||||
if (filePath.endsWith('.vue')) return 'vue'
|
||||
if (filePath.endsWith('.svelte')) return 'svelte'
|
||||
if (filePath.endsWith('.json')) return 'json'
|
||||
if (filePath.endsWith('.html')) return 'html'
|
||||
if (filePath.endsWith('.css')) return 'css'
|
||||
if (filePath.endsWith('.scss')) return 'scss'
|
||||
if (filePath.endsWith('.less')) return 'less'
|
||||
if (filePath.endsWith('.md')) return 'markdown'
|
||||
if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) return 'yaml'
|
||||
if (filePath.endsWith('.xml')) return 'xml'
|
||||
if (filePath.endsWith('.sql')) return 'sql'
|
||||
if (filePath.endsWith('.sh')) return 'shell'
|
||||
if (filePath.endsWith('.bat')) return 'bat'
|
||||
if (filePath.endsWith('.php')) return 'php'
|
||||
if (filePath.endsWith('.java')) return 'java'
|
||||
if (filePath.endsWith('.c')) return 'c'
|
||||
if (filePath.endsWith('.cpp')) return 'cpp'
|
||||
if (filePath.endsWith('.h')) return 'h'
|
||||
if (filePath.endsWith('.hpp')) return 'hpp'
|
||||
if (filePath.endsWith('.go')) return 'go'
|
||||
if (filePath.endsWith('.py')) return 'python'
|
||||
if (filePath.endsWith('.rb')) return 'ruby'
|
||||
if (filePath.endsWith('.cs')) return 'csharp'
|
||||
if (filePath.endsWith('.swift')) return 'swift'
|
||||
if (filePath.endsWith('.vb')) return 'vb'
|
||||
if (filePath.endsWith('.lua')) return 'lua'
|
||||
if (filePath.endsWith('.pl')) return 'perl'
|
||||
if (filePath.endsWith('.r')) return 'r'
|
||||
return 'plaintext'
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal size="full" isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex items-center gap-2 bg-content2 bg-opacity-50">
|
||||
<span>编辑文件</span>
|
||||
<Code className="text-xs">{file?.path}</Code>
|
||||
</ModalHeader>
|
||||
<ModalBody className="p-0">
|
||||
<div className="h-full">
|
||||
<CodeEditor
|
||||
height="100%"
|
||||
value={file?.content || ''}
|
||||
onChange={onContentChange}
|
||||
options={{ wordWrap: 'on' }}
|
||||
language={file?.path ? getLanguage(file.path) : 'plaintext'}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onSave}>
|
||||
保存
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
159
napcat.webui/src/components/file_manage/file_table.tsx
Normal file
159
napcat.webui/src/components/file_manage/file_table.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { Button, ButtonGroup } from '@heroui/button'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import {
|
||||
type Selection,
|
||||
type SortDescriptor,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '@heroui/table'
|
||||
import { Tooltip } from '@heroui/tooltip'
|
||||
import path from 'path-browserify'
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import { FiCopy, FiMove, FiTrash2 } from 'react-icons/fi'
|
||||
|
||||
import FileIcon from '@/components/file_icon'
|
||||
|
||||
import type { FileInfo } from '@/controllers/file_manager'
|
||||
|
||||
interface FileTableProps {
|
||||
files: FileInfo[]
|
||||
currentPath: string
|
||||
loading: boolean
|
||||
sortDescriptor: SortDescriptor
|
||||
onSortChange: (descriptor: SortDescriptor) => void
|
||||
selectedFiles: Selection
|
||||
onSelectionChange: (selected: Selection) => void
|
||||
onDirectoryClick: (dirPath: string) => void
|
||||
onEdit: (filePath: string) => void
|
||||
onRenameRequest: (name: string) => void
|
||||
onMoveRequest: (name: string) => void
|
||||
onCopyPath: (fileName: string) => void
|
||||
onDelete: (filePath: string) => void
|
||||
}
|
||||
|
||||
export default function FileTable({
|
||||
files,
|
||||
currentPath,
|
||||
loading,
|
||||
sortDescriptor,
|
||||
onSortChange,
|
||||
selectedFiles,
|
||||
onSelectionChange,
|
||||
onDirectoryClick,
|
||||
onEdit,
|
||||
onRenameRequest,
|
||||
onMoveRequest,
|
||||
onCopyPath,
|
||||
onDelete
|
||||
}: FileTableProps) {
|
||||
return (
|
||||
<Table
|
||||
aria-label="文件列表"
|
||||
sortDescriptor={sortDescriptor}
|
||||
onSortChange={onSortChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
defaultSelectedKeys={[]}
|
||||
selectedKeys={selectedFiles}
|
||||
selectionMode="multiple"
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key="name" allowsSorting>
|
||||
名称
|
||||
</TableColumn>
|
||||
<TableColumn key="type" allowsSorting>
|
||||
类型
|
||||
</TableColumn>
|
||||
<TableColumn key="size" allowsSorting>
|
||||
大小
|
||||
</TableColumn>
|
||||
<TableColumn key="mtime" allowsSorting>
|
||||
修改时间
|
||||
</TableColumn>
|
||||
<TableColumn key="actions">操作</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
isLoading={loading}
|
||||
loadingContent={
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
}
|
||||
items={files}
|
||||
>
|
||||
{(file: FileInfo) => (
|
||||
<TableRow key={file.name}>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={() =>
|
||||
file.isDirectory
|
||||
? onDirectoryClick(file.name)
|
||||
: onEdit(path.join(currentPath, file.name))
|
||||
}
|
||||
className="text-left justify-start"
|
||||
startContent={
|
||||
<FileIcon name={file.name} isDirectory={file.isDirectory} />
|
||||
}
|
||||
>
|
||||
{file.name}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>{file.isDirectory ? '目录' : '文件'}</TableCell>
|
||||
<TableCell>
|
||||
{isNaN(file.size) || file.isDirectory ? '-' : `${file.size} 字节`}
|
||||
</TableCell>
|
||||
<TableCell>{new Date(file.mtime).toLocaleString()}</TableCell>
|
||||
<TableCell>
|
||||
<ButtonGroup size="sm">
|
||||
<Tooltip content="重命名">
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
variant="flat"
|
||||
onPress={() => onRenameRequest(file.name)}
|
||||
>
|
||||
<BiRename />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content="移动">
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
variant="flat"
|
||||
onPress={() => onMoveRequest(file.name)}
|
||||
>
|
||||
<FiMove />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content="复制路径">
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
variant="flat"
|
||||
onPress={() => onCopyPath(file.name)}
|
||||
>
|
||||
<FiCopy />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content="删除">
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
variant="flat"
|
||||
onPress={() => onDelete(path.join(currentPath, file.name))}
|
||||
>
|
||||
<FiTrash2 />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
168
napcat.webui/src/components/file_manage/move_modal.tsx
Normal file
168
napcat.webui/src/components/file_manage/move_modal.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader
|
||||
} from '@heroui/modal'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import clsx from 'clsx'
|
||||
import path from 'path-browserify'
|
||||
import { useState } from 'react'
|
||||
import { IoAdd, IoRemove } from 'react-icons/io5'
|
||||
|
||||
import FileManager from '@/controllers/file_manager'
|
||||
|
||||
interface MoveModalProps {
|
||||
isOpen: boolean
|
||||
moveTargetPath: string
|
||||
selectionInfo: string
|
||||
onClose: () => void
|
||||
onMove: () => void
|
||||
onSelect: (dir: string) => void // 新增回调
|
||||
}
|
||||
|
||||
// 将 DirectoryTree 改为递归组件
|
||||
// 新增 selectedPath 属性,用于标识当前选中的目录
|
||||
function DirectoryTree({
|
||||
basePath,
|
||||
onSelect,
|
||||
selectedPath
|
||||
}: {
|
||||
basePath: string
|
||||
onSelect: (dir: string) => void
|
||||
selectedPath?: string
|
||||
}) {
|
||||
const [dirs, setDirs] = useState<string[]>([])
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
// 新增loading状态
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchDirectories = async () => {
|
||||
try {
|
||||
// 直接使用 basePath 调用接口,移除 process.platform 判断
|
||||
const list = await FileManager.listDirectories(basePath)
|
||||
setDirs(list.map((item) => item.name))
|
||||
} catch (error) {
|
||||
// ...error handling...
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggle = async () => {
|
||||
if (!expanded) {
|
||||
setExpanded(true)
|
||||
setLoading(true)
|
||||
await fetchDirectories()
|
||||
setLoading(false)
|
||||
} else {
|
||||
setExpanded(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
onSelect(basePath)
|
||||
handleToggle()
|
||||
}
|
||||
|
||||
// 计算显示的名称
|
||||
const getDisplayName = () => {
|
||||
if (basePath === '/') return '/'
|
||||
if (/^[A-Z]:$/i.test(basePath)) return basePath
|
||||
return path.basename(basePath)
|
||||
}
|
||||
|
||||
// 更新 Button 的 variant 逻辑
|
||||
const isSeleted = selectedPath === basePath
|
||||
const variant = isSeleted
|
||||
? 'solid'
|
||||
: selectedPath && path.dirname(selectedPath) === basePath
|
||||
? 'flat'
|
||||
: 'light'
|
||||
|
||||
return (
|
||||
<div className="ml-4">
|
||||
<Button
|
||||
onPress={handleClick}
|
||||
className="py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md"
|
||||
size="sm"
|
||||
color="danger"
|
||||
variant={variant}
|
||||
startContent={
|
||||
<div
|
||||
className={clsx(
|
||||
'rounded-md',
|
||||
isSeleted ? 'bg-danger-600' : 'bg-danger-50'
|
||||
)}
|
||||
>
|
||||
{expanded ? <IoRemove /> : <IoAdd />}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{getDisplayName()}
|
||||
</Button>
|
||||
{expanded && (
|
||||
<div>
|
||||
{loading ? (
|
||||
<div className="flex py-1 px-8">
|
||||
<Spinner size="sm" color="danger" />
|
||||
</div>
|
||||
) : (
|
||||
dirs.map((dirName) => {
|
||||
const childPath =
|
||||
basePath === '/' && /^[A-Z]:$/i.test(dirName)
|
||||
? dirName
|
||||
: path.join(basePath, dirName)
|
||||
return (
|
||||
<DirectoryTree
|
||||
key={childPath}
|
||||
basePath={childPath}
|
||||
onSelect={onSelect}
|
||||
selectedPath={selectedPath}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MoveModal({
|
||||
isOpen,
|
||||
moveTargetPath,
|
||||
selectionInfo,
|
||||
onClose,
|
||||
onMove,
|
||||
onSelect
|
||||
}: MoveModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>选择目标目录</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="rounded-md p-2 border border-default-300 overflow-auto max-h-60">
|
||||
<DirectoryTree
|
||||
basePath="/"
|
||||
onSelect={onSelect}
|
||||
selectedPath={moveTargetPath}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-default-500 mt-2">
|
||||
当前选择:{moveTargetPath || '未选择'}
|
||||
</p>
|
||||
<p className="text-sm text-default-500">移动项:{selectionInfo}</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onMove}>
|
||||
确定
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
44
napcat.webui/src/components/file_manage/rename_modal.tsx
Normal file
44
napcat.webui/src/components/file_manage/rename_modal.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Input } from '@heroui/input'
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader
|
||||
} from '@heroui/modal'
|
||||
|
||||
interface RenameModalProps {
|
||||
isOpen: boolean
|
||||
newFileName: string
|
||||
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onClose: () => void
|
||||
onRename: () => void
|
||||
}
|
||||
|
||||
export default function RenameModal({
|
||||
isOpen,
|
||||
newFileName,
|
||||
onNameChange,
|
||||
onClose,
|
||||
onRename
|
||||
}: RenameModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>重命名</ModalHeader>
|
||||
<ModalBody>
|
||||
<Input label="新名称" value={newFileName} onChange={onNameChange} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="danger" onPress={onRename}>
|
||||
确定
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
146
napcat.webui/src/components/hover_titled_card.tsx
Normal file
146
napcat.webui/src/components/hover_titled_card.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { motion, useMotionValue, useSpring } from 'motion/react'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
const springValues = {
|
||||
damping: 30,
|
||||
stiffness: 100,
|
||||
mass: 2
|
||||
}
|
||||
|
||||
export interface HoverTiltedCardProps {
|
||||
imageSrc: string
|
||||
altText?: string
|
||||
captionText?: string
|
||||
containerHeight?: string
|
||||
containerWidth?: string
|
||||
imageHeight?: string
|
||||
imageWidth?: string
|
||||
scaleOnHover?: number
|
||||
rotateAmplitude?: number
|
||||
showTooltip?: boolean
|
||||
overlayContent?: React.ReactNode
|
||||
displayOverlayContent?: boolean
|
||||
}
|
||||
|
||||
export default function HoverTiltedCard({
|
||||
imageSrc,
|
||||
altText = 'NapCat',
|
||||
captionText = 'NapCat',
|
||||
containerHeight = '200px',
|
||||
containerWidth = '100%',
|
||||
imageHeight = '200px',
|
||||
imageWidth = '200px',
|
||||
scaleOnHover = 1.1,
|
||||
rotateAmplitude = 14,
|
||||
showTooltip = false,
|
||||
overlayContent = (
|
||||
<div className="text-center font-ubuntu mt-6 px-4 py-0.5 shadow-lg rounded-full bg-danger-600 text-default-100 bg-opacity-80">
|
||||
NapCat
|
||||
</div>
|
||||
),
|
||||
displayOverlayContent = true
|
||||
}: HoverTiltedCardProps) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const x = useMotionValue(0)
|
||||
const y = useMotionValue(0)
|
||||
const rotateX = useSpring(useMotionValue(0), springValues)
|
||||
const rotateY = useSpring(useMotionValue(0), springValues)
|
||||
const scale = useSpring(1, springValues)
|
||||
const opacity = useSpring(0)
|
||||
const rotateFigcaption = useSpring(0, {
|
||||
stiffness: 350,
|
||||
damping: 30,
|
||||
mass: 1
|
||||
})
|
||||
|
||||
const [lastY, setLastY] = useState(0)
|
||||
|
||||
function handleMouse(e: React.MouseEvent) {
|
||||
if (!ref.current) return
|
||||
|
||||
const rect = ref.current.getBoundingClientRect()
|
||||
const offsetX = e.clientX - rect.left - rect.width / 2
|
||||
const offsetY = e.clientY - rect.top - rect.height / 2
|
||||
|
||||
const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude
|
||||
const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude
|
||||
|
||||
rotateX.set(rotationX)
|
||||
rotateY.set(rotationY)
|
||||
|
||||
x.set(e.clientX - rect.left)
|
||||
y.set(e.clientY - rect.top)
|
||||
|
||||
const velocityY = offsetY - lastY
|
||||
rotateFigcaption.set(-velocityY * 0.6)
|
||||
setLastY(offsetY)
|
||||
}
|
||||
|
||||
function handleMouseEnter() {
|
||||
scale.set(scaleOnHover)
|
||||
opacity.set(1)
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
opacity.set(0)
|
||||
scale.set(1)
|
||||
rotateX.set(0)
|
||||
rotateY.set(0)
|
||||
rotateFigcaption.set(0)
|
||||
}
|
||||
|
||||
return (
|
||||
<figure
|
||||
ref={ref}
|
||||
className="relative w-full h-full [perspective:800px] flex flex-col items-center justify-center"
|
||||
style={{
|
||||
height: containerHeight,
|
||||
width: containerWidth
|
||||
}}
|
||||
onMouseMove={handleMouse}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<motion.div
|
||||
className="relative [transform-style:preserve-3d]"
|
||||
style={{
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
rotateX,
|
||||
rotateY,
|
||||
scale
|
||||
}}
|
||||
>
|
||||
<motion.img
|
||||
src={imageSrc}
|
||||
alt={altText}
|
||||
className="absolute top-0 left-0 object-cover rounded-md will-change-transform [transform:translateZ(0)] pointer-events-none select-none"
|
||||
style={{
|
||||
width: imageWidth,
|
||||
height: imageHeight
|
||||
}}
|
||||
/>
|
||||
|
||||
{displayOverlayContent && overlayContent && (
|
||||
<motion.div className="absolute top-0 left-0 right-0 z-10 flex justify-center will-change-transform [transform:translateZ(30px)]">
|
||||
{overlayContent}
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{showTooltip && (
|
||||
<motion.figcaption
|
||||
className="pointer-events-none absolute left-0 top-0 rounded-md bg-white px-2 py-1 text-sm text-default-900 opacity-0 z-10 hidden sm:block"
|
||||
style={{
|
||||
x,
|
||||
y,
|
||||
opacity,
|
||||
rotate: rotateFigcaption
|
||||
}}
|
||||
>
|
||||
{captionText}
|
||||
</motion.figcaption>
|
||||
)}
|
||||
</figure>
|
||||
)
|
||||
}
|
@@ -1152,7 +1152,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="0ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1197,7 +1197,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="800ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1247,7 +1247,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="1600ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1297,7 +1297,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="2400ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1344,7 +1344,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="3200ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1399,7 +1399,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="0ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1446,7 +1446,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="600ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1496,7 +1496,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="1200ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1543,7 +1543,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="1800ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1590,7 +1590,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="2400ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1637,7 +1637,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="3000ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1684,7 +1684,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="3600ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1731,7 +1731,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="4200ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1744,3 +1744,224 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
</svg>
|
||||
</>
|
||||
)
|
||||
|
||||
export const FileIcon = (props: IconSvgProps) => (
|
||||
<svg
|
||||
version="1.1"
|
||||
id="_x36_"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512"
|
||||
xmlSpace="preserve"
|
||||
{...props}
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||
<g
|
||||
id="SVGRepo_tracerCarrier"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: '#D4B476' }}
|
||||
d="M441.853,393.794H70.147C31.566,393.794,0,362.228,0,323.647V106.969 c0-38.581,31.566-70.147,70.147-70.147h371.706c38.581,0,70.147,31.566,70.147,70.147v216.678 C512,362.228,480.434,393.794,441.853,393.794z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#D4B476' }}
|
||||
d="M199.884,249.574H70.147C31.566,249.574,0,218.008,0,179.427V70.147C0,31.566,31.566,0,70.147,0 h129.737c38.581,0,70.147,31.566,70.147,70.147v109.28C270.031,218.008,238.465,249.574,199.884,249.574z"
|
||||
></path>
|
||||
<polygon
|
||||
style={{ fill: '#F0EFEF' }}
|
||||
points="485.439,329.388 87.357,347.774 78.653,130.095 476.734,111.709 "
|
||||
></polygon>
|
||||
<defs>
|
||||
<filter
|
||||
id="Adobe_OpacityMaskFilter"
|
||||
filterUnits="userSpaceOnUse"
|
||||
x="34.381"
|
||||
y="60.216"
|
||||
width="416.68"
|
||||
height="259.557"
|
||||
>
|
||||
<feFlood
|
||||
style={{
|
||||
floodColor: 'white',
|
||||
floodOpacity: 1
|
||||
}}
|
||||
result="back"
|
||||
></feFlood>
|
||||
<feBlend in="SourceGraphic" in2="back" mode="normal"></feBlend>
|
||||
</filter>
|
||||
</defs>
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="34.381"
|
||||
y="60.216"
|
||||
width="416.68"
|
||||
height="259.557"
|
||||
id="SVGID_1_"
|
||||
>
|
||||
<g style={{ filter: 'url(#Adobe_OpacityMaskFilter)' }}>
|
||||
<defs>
|
||||
<filter
|
||||
id="Adobe_OpacityMaskFilter_1_"
|
||||
filterUnits="userSpaceOnUse"
|
||||
x="34.381"
|
||||
y="60.216"
|
||||
width="416.68"
|
||||
height="259.557"
|
||||
>
|
||||
<feFlood
|
||||
style={{ floodColor: 'white', floodOpacity: 1 }}
|
||||
result="back"
|
||||
></feFlood>
|
||||
<feBlend in="SourceGraphic" in2="back" mode="normal"></feBlend>
|
||||
</filter>
|
||||
</defs>
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="34.381"
|
||||
y="60.216"
|
||||
width="416.68"
|
||||
height="259.557"
|
||||
id="SVGID_1_"
|
||||
>
|
||||
<g style={{ filter: 'url(#Adobe_OpacityMaskFilter_1_)' }}> </g>
|
||||
</mask>
|
||||
<linearGradient
|
||||
id="SVGID_2_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="34.3814"
|
||||
y1="189.9944"
|
||||
x2="451.061"
|
||||
y2="189.9944"
|
||||
>
|
||||
<stop offset="0.57" style={{ stopColor: '#F6F6F6' }}></stop>
|
||||
<stop offset="0.6039" style={{ stopColor: '#F6F6F6' }}></stop>
|
||||
</linearGradient>
|
||||
<polygon
|
||||
style={{ mask: 'url(#SVGID_1_)', fill: 'url(#SVGID_2_)' }}
|
||||
points="451.061,277.073 54.598,319.773 34.381,102.916 430.845,60.216 "
|
||||
></polygon>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient
|
||||
id="SVGID_3_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="34.3814"
|
||||
y1="189.9944"
|
||||
x2="451.061"
|
||||
y2="189.9944"
|
||||
>
|
||||
<stop offset="0.57" style={{ stopColor: '#FFFFFF' }}></stop>
|
||||
<stop offset="0.6039" style={{ stopColor: '#F0F0F0' }}></stop>
|
||||
</linearGradient>
|
||||
<polygon
|
||||
style={{ fill: 'url(#SVGID_3_)' }}
|
||||
points="451.061,277.073 54.598,319.773 34.381,102.916 430.845,60.216 "
|
||||
></polygon>
|
||||
<path
|
||||
style={{ fill: '#69A092' }}
|
||||
d="M441.853,417.32H70.147C31.566,417.32,0,385.754,0,347.173V168.515h512v178.658 C512,385.754,480.434,417.32,441.853,417.32z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#D4B476' }}
|
||||
d="M441.853,429.594H70.147C31.566,429.594,0,398.028,0,359.447V189.995h512v169.453 C512,398.028,480.434,429.594,441.853,429.594z"
|
||||
></path>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: '#CBBC89' }}
|
||||
d="M41.051,330.321h28.918h7.581c0.686,0,1.357,0,2.012,0c0.655,0,1.171,0.126,1.545,0.375 c0.499,0.312,0.795,0.764,0.889,1.357c0.094,0.594,0.141,1.296,0.141,2.106c0,0.312,0.014,0.608,0.047,0.888 c0.031,0.281-0.016,0.547-0.14,0.796c-0.25,0.998-0.796,1.543-1.638,1.638c-0.843,0.094-1.888,0.14-3.136,0.14h-9.733H53.778 c-0.998,0-2.058-0.015-3.182-0.047c-1.122-0.03-1.965,0.173-2.526,0.608c-0.563,0.375-0.858,1.03-0.89,1.965 c-0.032,0.937-0.047,1.873-0.047,2.808v9.265c0,0.5-0.015,1.123-0.046,1.872c-0.033,0.749,0.014,1.373,0.139,1.871v0.748 c0.125,0.375,0.235,0.735,0.328,1.077c0.093,0.344,0.295,0.608,0.608,0.796c0.562,0.374,1.436,0.547,2.621,0.514 c1.184-0.03,2.214-0.047,3.088-0.047h16.845c0.561,0,1.171-0.014,1.825-0.046c0.655-0.031,1.31-0.031,1.965,0 c0.655,0.032,1.248,0.109,1.779,0.234c0.529,0.126,0.889,0.344,1.076,0.655c0.311,0.375,0.467,0.843,0.467,1.404 c0,0.56,0,1.186,0,1.871c0,0.375,0,0.719,0,1.03c0,0.313-0.062,0.594-0.186,0.842c-0.25,0.625-0.595,0.969-1.03,1.03 c-0.25,0.125-0.485,0.186-0.702,0.186c-0.219,0-0.483,0.033-0.796,0.094c-0.25,0.063-0.514,0.079-0.796,0.047 c-0.281-0.03-0.546-0.047-0.794-0.047h-3.277H54.433c-0.999,0-2.168-0.014-3.51-0.047c-1.342-0.03-2.292,0.172-2.853,0.609 c-0.314,0.25-0.547,0.64-0.702,1.169c-0.156,0.531-0.25,1.14-0.281,1.825c-0.033,0.687-0.033,1.389,0,2.106 c0.03,0.717,0.046,1.357,0.046,1.918v16.096c0,1.062,0.032,2.217,0.094,3.463c0.062,1.249-0.156,2.185-0.655,2.807 c-0.186,0.25-0.469,0.391-0.842,0.422c-0.374,0.032-0.749,0.109-1.123,0.234h-1.779c-0.875,0-1.684-0.03-2.433-0.093 c-0.749-0.062-1.279-0.375-1.59-0.937c-0.25-0.374-0.375-0.888-0.375-1.543c0-0.656,0-1.326,0-2.013v-7.488v-39.119v-10.107 c0-0.686-0.016-1.45-0.047-2.292c-0.032-0.842,0.107-1.512,0.422-2.012c0.249-0.374,0.715-0.685,1.404-0.935 c0.124-0.062,0.264-0.077,0.42-0.047C40.785,330.4,40.925,330.383,41.051,330.321z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#CBBC89' }}
|
||||
d="M96.895,330.508c0.436,0,0.935-0.014,1.496-0.046c0.563-0.032,1.108-0.032,1.639,0 c0.529,0.032,1.013,0.109,1.45,0.234c0.437,0.125,0.749,0.312,0.937,0.561c0.374,0.438,0.576,1.046,0.608,1.825 c0.03,0.78,0.047,1.576,0.047,2.386v9.079v36.124v10.949c0,0.811,0,1.669,0,2.574c0,0.905-0.156,1.606-0.468,2.105 c-0.25,0.314-0.547,0.5-0.889,0.563c-0.344,0.062-0.765,0.156-1.264,0.28h-1.777c-0.874,0-1.684-0.034-2.433-0.094 c-0.749-0.061-1.28-0.374-1.592-0.935c-0.25-0.374-0.375-0.89-0.375-1.545c0-0.655,0-1.325,0-2.012v-7.487V345.95v-10.107 c0-0.686-0.015-1.451-0.045-2.292c-0.034-0.843,0.108-1.514,0.42-2.013c0.249-0.436,0.748-0.748,1.497-0.936 c0.125-0.061,0.25-0.077,0.375-0.047C96.645,330.587,96.769,330.571,96.895,330.508z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#CBBC89' }}
|
||||
d="M126.093,330.321c0.436,0,0.935-0.015,1.498-0.047c0.561-0.03,1.107-0.03,1.638,0 c0.529,0.032,1.012,0.109,1.451,0.234c0.436,0.125,0.748,0.313,0.936,0.562c0.374,0.5,0.56,1.155,0.56,1.965 c0,0.811,0,1.654,0,2.528v9.826v30.042v8.609c0,0.687,0,1.436,0,2.245c0,0.813,0.094,1.468,0.281,1.967 c0.188,0.436,0.529,0.811,1.03,1.122c0.313,0.125,0.717,0.203,1.217,0.235c0.499,0.032,0.998,0.046,1.498,0.046h4.772h16.845 h5.24c0.436,0,0.89-0.014,1.359-0.046c0.467-0.032,0.888,0.016,1.263,0.139c0.81,0.25,1.34,0.594,1.591,1.03 c0.062,0.188,0.124,0.5,0.188,0.937c0.061,0.436,0.107,0.889,0.139,1.357c0.03,0.467,0.015,0.935-0.047,1.402 c-0.062,0.469-0.126,0.829-0.187,1.077c-0.126,0.437-0.406,0.782-0.843,1.029c-0.313,0.25-0.765,0.375-1.357,0.375 c-0.593,0-1.169,0-1.731,0h-6.177H133.58h-6.083c-0.874,0-1.623-0.047-2.246-0.139c-0.624-0.094-1.092-0.39-1.404-0.89 c-0.25-0.374-0.373-0.857-0.373-1.451c0-0.591,0-1.168,0-1.729v-6.74v-25.549v-20.308v-5.802c0-0.561,0.014-1.122,0.045-1.684 c0.032-0.561,0.141-0.998,0.328-1.31c0.249-0.374,0.717-0.685,1.404-0.935c0.124-0.062,0.265-0.077,0.422-0.047 C125.827,330.4,125.968,330.383,126.093,330.321z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#CBBC89' }}
|
||||
d="M187.883,330.321h30.602h8.049c0.686,0,1.372-0.015,2.059-0.047 c0.685-0.03,1.248,0.109,1.684,0.422c0.188,0.125,0.343,0.281,0.468,0.467c0.124,0.188,0.249,0.375,0.374,0.563 c0.125,0.436,0.187,1.138,0.187,2.105c0,0.967-0.062,1.701-0.187,2.199c-0.25,0.874-0.78,1.357-1.592,1.451 c-0.811,0.094-1.777,0.14-2.9,0.14h-9.359h-15.722c-0.998,0-2.169-0.015-3.51-0.047c-1.343-0.03-2.325,0.139-2.949,0.514 c-0.624,0.374-0.951,1.03-0.982,1.966c-0.032,0.936-0.048,1.904-0.048,2.901v9.546c0,0.688-0.015,1.498-0.045,2.433 c-0.034,0.937,0.014,1.686,0.139,2.246c0.188,0.749,0.562,1.249,1.123,1.498c0.249,0.125,0.686,0.25,1.31,0.373h0.935 c0.249,0.063,0.515,0.08,0.796,0.047c0.281-0.03,0.577-0.047,0.89-0.047h3.181h18.343c0.561,0,1.184-0.014,1.872-0.046 c0.685-0.031,1.387-0.031,2.104,0c0.717,0.032,1.372,0.093,1.967,0.187c0.591,0.094,1.044,0.233,1.357,0.421 c0.186,0.126,0.341,0.407,0.467,0.842c0.125,0.438,0.218,0.905,0.282,1.404c0.061,0.5,0.077,1.016,0.045,1.545 c-0.031,0.531-0.109,0.983-0.233,1.357c-0.126,0.561-0.406,0.967-0.842,1.217c-0.127,0.126-0.297,0.203-0.515,0.235 c-0.22,0.032-0.422,0.079-0.608,0.139h-0.563c-0.312,0.063-0.623,0.079-0.935,0.047c-0.313-0.03-0.624-0.047-0.935-0.047h-3.463 h-19.747c-0.747,0-1.482-0.014-2.199-0.047c-0.719-0.03-1.39,0-2.012,0.094c-0.624,0.094-1.171,0.267-1.639,0.515 c-0.467,0.25-0.794,0.687-0.982,1.31c-0.126,0.375-0.173,0.811-0.139,1.31c0.03,0.5,0.045,0.999,0.045,1.498v5.239v8.892 c0,0.873,0.032,1.669,0.094,2.386c0.062,0.718,0.312,1.233,0.749,1.544c0.435,0.313,0.935,0.483,1.497,0.515 c0.561,0.032,1.185,0.046,1.871,0.046h5.99h17.314h5.334c0.499,0,0.998-0.014,1.497-0.046c0.498-0.032,0.936,0.016,1.31,0.139 c0.749,0.25,1.248,0.594,1.498,1.03c0.123,0.188,0.217,0.5,0.281,0.937c0.061,0.436,0.108,0.889,0.14,1.357 c0.03,0.467,0.015,0.935-0.047,1.402c-0.062,0.469-0.126,0.829-0.187,1.077c-0.25,0.5-0.531,0.842-0.842,1.029 c-0.375,0.25-0.858,0.375-1.45,0.375c-0.594,0-1.171,0-1.731,0h-6.552h-24.894h-6.551c-0.874,0-1.623-0.047-2.245-0.139 c-0.624-0.094-1.093-0.39-1.404-0.89c-0.25-0.374-0.374-0.857-0.374-1.451c0-0.591,0-1.168,0-1.729v-6.551v-25.082v-20.964 v-5.802c0-0.561,0-1.122,0-1.684c0-0.561,0.124-0.998,0.374-1.31c0.249-0.435,0.716-0.749,1.404-0.935 c0.125-0.062,0.248-0.077,0.375-0.047C187.632,330.4,187.756,330.383,187.883,330.321z"
|
||||
></path>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: '#98806E' }}
|
||||
d="M41.051,330.321h28.918h7.581c0.686,0,1.357,0,2.012,0c0.655,0,1.171,0.126,1.545,0.375 c0.499,0.312,0.795,0.764,0.889,1.357c0.094,0.594,0.141,1.296,0.141,2.106c0,0.312,0.014,0.608,0.047,0.888 c0.031,0.281-0.016,0.547-0.14,0.796c-0.25,0.998-0.796,1.543-1.638,1.638c-0.843,0.094-1.888,0.14-3.136,0.14h-9.733H53.778 c-0.998,0-2.058-0.015-3.182-0.047c-1.122-0.03-1.965,0.173-2.526,0.608c-0.563,0.375-0.858,1.03-0.89,1.965 c-0.032,0.937-0.047,1.873-0.047,2.808v9.265c0,0.5-0.015,1.123-0.046,1.872c-0.033,0.749,0.014,1.373,0.139,1.871v0.748 c0.125,0.375,0.235,0.735,0.328,1.077c0.093,0.344,0.295,0.608,0.608,0.796c0.562,0.374,1.436,0.547,2.621,0.514 c1.184-0.03,2.214-0.047,3.088-0.047h16.845c0.561,0,1.171-0.014,1.825-0.046c0.655-0.031,1.31-0.031,1.965,0 c0.655,0.032,1.248,0.109,1.779,0.234c0.529,0.126,0.889,0.344,1.076,0.655c0.311,0.375,0.467,0.843,0.467,1.404 c0,0.56,0,1.186,0,1.871c0,0.375,0,0.719,0,1.03c0,0.313-0.062,0.594-0.186,0.842c-0.25,0.625-0.595,0.969-1.03,1.03 c-0.25,0.125-0.485,0.186-0.702,0.186c-0.219,0-0.483,0.033-0.796,0.094c-0.25,0.063-0.514,0.079-0.796,0.047 c-0.281-0.03-0.546-0.047-0.794-0.047h-3.277H54.433c-0.999,0-2.168-0.014-3.51-0.047c-1.342-0.03-2.292,0.172-2.853,0.609 c-0.314,0.25-0.547,0.64-0.702,1.169c-0.156,0.531-0.25,1.14-0.281,1.825c-0.033,0.687-0.033,1.389,0,2.106 c0.03,0.717,0.046,1.357,0.046,1.918v16.096c0,1.062,0.032,2.217,0.094,3.463c0.062,1.249-0.156,2.185-0.655,2.807 c-0.186,0.25-0.469,0.391-0.842,0.422c-0.374,0.032-0.749,0.109-1.123,0.234h-1.779c-0.875,0-1.684-0.03-2.433-0.093 c-0.749-0.062-1.279-0.375-1.59-0.937c-0.25-0.374-0.375-0.888-0.375-1.543c0-0.656,0-1.326,0-2.013v-7.488v-39.119v-10.107 c0-0.686-0.016-1.45-0.047-2.292c-0.032-0.842,0.107-1.512,0.422-2.012c0.249-0.374,0.715-0.685,1.404-0.935 c0.124-0.062,0.264-0.077,0.42-0.047C40.785,330.4,40.925,330.383,41.051,330.321z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#98806E' }}
|
||||
d="M96.895,330.508c0.436,0,0.935-0.014,1.496-0.046c0.563-0.032,1.108-0.032,1.639,0 c0.529,0.032,1.013,0.109,1.45,0.234c0.437,0.125,0.749,0.312,0.937,0.561c0.374,0.438,0.576,1.046,0.608,1.825 c0.03,0.78,0.047,1.576,0.047,2.386v9.079v36.124v10.949c0,0.811,0,1.669,0,2.574c0,0.905-0.156,1.606-0.468,2.105 c-0.25,0.314-0.547,0.5-0.889,0.563c-0.344,0.062-0.765,0.156-1.264,0.28h-1.777c-0.874,0-1.684-0.034-2.433-0.094 c-0.749-0.061-1.28-0.374-1.592-0.935c-0.25-0.374-0.375-0.89-0.375-1.545c0-0.655,0-1.325,0-2.012v-7.487V345.95v-10.107 c0-0.686-0.015-1.451-0.045-2.292c-0.034-0.843,0.108-1.514,0.42-2.013c0.249-0.436,0.748-0.748,1.497-0.936 c0.125-0.061,0.25-0.077,0.375-0.047C96.645,330.587,96.769,330.571,96.895,330.508z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#98806E' }}
|
||||
d="M126.093,330.321c0.436,0,0.935-0.015,1.498-0.047c0.561-0.03,1.107-0.03,1.638,0 c0.529,0.032,1.012,0.109,1.451,0.234c0.436,0.125,0.748,0.313,0.936,0.562c0.374,0.5,0.56,1.155,0.56,1.965 c0,0.811,0,1.654,0,2.528v9.826v30.042v8.609c0,0.687,0,1.436,0,2.245c0,0.813,0.094,1.468,0.281,1.967 c0.188,0.436,0.529,0.811,1.03,1.122c0.313,0.125,0.717,0.203,1.217,0.235c0.499,0.032,0.998,0.046,1.498,0.046h4.772h16.845 h5.24c0.436,0,0.89-0.014,1.359-0.046c0.467-0.032,0.888,0.016,1.263,0.139c0.81,0.25,1.34,0.594,1.591,1.03 c0.062,0.188,0.124,0.5,0.188,0.937c0.061,0.436,0.107,0.889,0.139,1.357c0.03,0.467,0.015,0.935-0.047,1.402 c-0.062,0.469-0.126,0.829-0.187,1.077c-0.126,0.437-0.406,0.782-0.843,1.029c-0.313,0.25-0.765,0.375-1.357,0.375 c-0.593,0-1.169,0-1.731,0h-6.177H133.58h-6.083c-0.874,0-1.623-0.047-2.246-0.139c-0.624-0.094-1.092-0.39-1.404-0.89 c-0.25-0.374-0.373-0.857-0.373-1.451c0-0.591,0-1.168,0-1.729v-6.74v-25.549v-20.308v-5.802c0-0.561,0.014-1.122,0.045-1.684 c0.032-0.561,0.141-0.998,0.328-1.31c0.249-0.374,0.717-0.685,1.404-0.935c0.124-0.062,0.265-0.077,0.422-0.047 C125.827,330.4,125.968,330.383,126.093,330.321z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#98806E' }}
|
||||
d="M187.883,330.321h30.602h8.049c0.686,0,1.372-0.015,2.059-0.047 c0.685-0.03,1.248,0.109,1.684,0.422c0.188,0.125,0.343,0.281,0.468,0.467c0.124,0.188,0.249,0.375,0.374,0.563 c0.125,0.436,0.187,1.138,0.187,2.105c0,0.967-0.062,1.701-0.187,2.199c-0.25,0.874-0.78,1.357-1.592,1.451 c-0.811,0.094-1.777,0.14-2.9,0.14h-9.359h-15.722c-0.998,0-2.169-0.015-3.51-0.047c-1.343-0.03-2.325,0.139-2.949,0.514 c-0.624,0.374-0.951,1.03-0.982,1.966c-0.032,0.936-0.048,1.904-0.048,2.901v9.546c0,0.688-0.015,1.498-0.045,2.433 c-0.034,0.937,0.014,1.686,0.139,2.246c0.188,0.749,0.562,1.249,1.123,1.498c0.249,0.125,0.686,0.25,1.31,0.373h0.935 c0.249,0.063,0.515,0.08,0.796,0.047c0.281-0.03,0.577-0.047,0.89-0.047h3.181h18.343c0.561,0,1.184-0.014,1.872-0.046 c0.685-0.031,1.387-0.031,2.104,0c0.717,0.032,1.372,0.093,1.967,0.187c0.591,0.094,1.044,0.233,1.357,0.421 c0.186,0.126,0.341,0.407,0.467,0.842c0.125,0.438,0.218,0.905,0.282,1.404c0.061,0.5,0.077,1.016,0.045,1.545 c-0.031,0.531-0.109,0.983-0.233,1.357c-0.126,0.561-0.406,0.967-0.842,1.217c-0.127,0.126-0.297,0.203-0.515,0.235 c-0.22,0.032-0.422,0.079-0.608,0.139h-0.563c-0.312,0.063-0.623,0.079-0.935,0.047c-0.313-0.03-0.624-0.047-0.935-0.047h-3.463 h-19.747c-0.747,0-1.482-0.014-2.199-0.047c-0.719-0.03-1.39,0-2.012,0.094c-0.624,0.094-1.171,0.267-1.639,0.515 c-0.467,0.25-0.794,0.687-0.982,1.31c-0.126,0.375-0.173,0.811-0.139,1.31c0.03,0.5,0.045,0.999,0.045,1.498v5.239v8.892 c0,0.873,0.032,1.669,0.094,2.386c0.062,0.718,0.312,1.233,0.749,1.544c0.435,0.313,0.935,0.483,1.497,0.515 c0.561,0.032,1.185,0.046,1.871,0.046h5.99h17.314h5.334c0.499,0,0.998-0.014,1.497-0.046c0.498-0.032,0.936,0.016,1.31,0.139 c0.749,0.25,1.248,0.594,1.498,1.03c0.123,0.188,0.217,0.5,0.281,0.937c0.061,0.436,0.108,0.889,0.14,1.357 c0.03,0.467,0.015,0.935-0.047,1.402c-0.062,0.469-0.126,0.829-0.187,1.077c-0.25,0.5-0.531,0.842-0.842,1.029 c-0.375,0.25-0.858,0.375-1.45,0.375c-0.594,0-1.171,0-1.731,0h-6.552h-24.894h-6.551c-0.874,0-1.623-0.047-2.245-0.139 c-0.624-0.094-1.093-0.39-1.404-0.89c-0.25-0.374-0.374-0.857-0.374-1.451c0-0.591,0-1.168,0-1.729v-6.551v-25.082v-20.964 v-5.802c0-0.561,0-1.122,0-1.684c0-0.561,0.124-0.998,0.374-1.31c0.249-0.435,0.716-0.749,1.404-0.935 c0.125-0.062,0.248-0.077,0.375-0.047C187.632,330.4,187.756,330.383,187.883,330.321z"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<polygon
|
||||
style={{ fill: '#BBAF98' }}
|
||||
points="276.167,208.741 0,302.069 0,186.053 512,186.053 512,302.069 "
|
||||
></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const LogIcon = (props: IconSvgProps) => (
|
||||
<svg
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||
<g
|
||||
id="SVGRepo_tracerCarrier"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<rect width="48" height="48" fill="white" fillOpacity="0.01"></rect>
|
||||
<rect
|
||||
x="13"
|
||||
y="10"
|
||||
width="28"
|
||||
height="34"
|
||||
fill="#2F88FF"
|
||||
stroke="#000000"
|
||||
strokeWidth="4"
|
||||
strokeLinejoin="round"
|
||||
></rect>
|
||||
<path
|
||||
d="M35 10V4H8C7.44772 4 7 4.44772 7 5V38H13"
|
||||
stroke="#000000"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M21 22H33"
|
||||
stroke="white"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M21 30H33"
|
||||
stroke="white"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
@@ -13,7 +13,6 @@ import { useTheme } from '@/hooks/use-theme'
|
||||
import logo from '@/assets/images/logo.png'
|
||||
import type { MenuItem } from '@/config/site'
|
||||
|
||||
import { title } from '../primitives'
|
||||
import Menus from './menus'
|
||||
|
||||
interface SideBarProps {
|
||||
@@ -49,18 +48,14 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
||||
>
|
||||
<motion.div className="w-64 flex flex-col items-stretch h-full transition-transform duration-300 ease-in-out z-30 relative float-right">
|
||||
<div className="flex justify-center items-center mt-2 gap-2">
|
||||
<Image height={40} src={logo} className="mb-2" />
|
||||
<Image radius="none" height={40} src={logo} className="mb-2" />
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center hm-medium',
|
||||
title({
|
||||
shadow: true,
|
||||
color: isDark ? 'violet' : 'pink'
|
||||
}),
|
||||
'!text-2xl'
|
||||
'!text-2xl shiny-text'
|
||||
)}
|
||||
>
|
||||
WebUI
|
||||
NapCat
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto flex flex-col flex-1 px-4">
|
||||
|
89
napcat.webui/src/components/tabs/index.tsx
Normal file
89
napcat.webui/src/components/tabs/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import clsx from 'clsx'
|
||||
import { type ReactNode, createContext, forwardRef, useContext } from 'react'
|
||||
|
||||
export interface TabsContextValue {
|
||||
activeKey: string
|
||||
onChange: (key: string) => void
|
||||
}
|
||||
|
||||
const TabsContext = createContext<TabsContextValue>({
|
||||
activeKey: '',
|
||||
onChange: () => {}
|
||||
})
|
||||
|
||||
export interface TabsProps {
|
||||
activeKey: string
|
||||
onChange: (key: string) => void
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Tabs({ activeKey, onChange, children, className }: TabsProps) {
|
||||
return (
|
||||
<TabsContext.Provider value={{ activeKey, onChange }}>
|
||||
<div className={clsx('flex flex-col gap-2', className)}>{children}</div>
|
||||
</TabsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export interface TabListProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TabList({ children, className }: TabListProps) {
|
||||
return (
|
||||
<div className={clsx('flex items-center gap-1', className)}>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface TabProps extends React.ButtonHTMLAttributes<HTMLDivElement> {
|
||||
value: string
|
||||
className?: string
|
||||
children: ReactNode
|
||||
isSelected?: boolean
|
||||
}
|
||||
|
||||
export const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||
({ className, isSelected, value, ...props }, ref) => {
|
||||
const { onChange } = useContext(TabsContext)
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
onChange(value)
|
||||
props.onClick?.(e)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="tab"
|
||||
aria-selected={isSelected}
|
||||
onClick={handleClick}
|
||||
className={clsx(
|
||||
'px-2 py-1 flex items-center gap-1 text-sm font-medium border-b-2 transition-colors',
|
||||
isSelected
|
||||
? 'border-danger text-danger'
|
||||
: 'border-transparent hover:border-default',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Tab.displayName = 'Tab'
|
||||
|
||||
export interface TabPanelProps {
|
||||
value: string
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TabPanel({ value, children, className }: TabPanelProps) {
|
||||
const { activeKey } = useContext(TabsContext)
|
||||
|
||||
if (value !== activeKey) return null
|
||||
|
||||
return <div className={clsx('flex-1', className)}>{children}</div>
|
||||
}
|
38
napcat.webui/src/components/tabs/sortable_tab.tsx
Normal file
38
napcat.webui/src/components/tabs/sortable_tab.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
|
||||
import { Tab } from '@/components/tabs'
|
||||
import type { TabProps } from '@/components/tabs'
|
||||
|
||||
interface SortableTabProps extends TabProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
export function SortableTab({ id, ...props }: SortableTabProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging
|
||||
} = useSortable({ id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 1 : 0,
|
||||
position: 'relative' as const,
|
||||
touchAction: 'none'
|
||||
}
|
||||
|
||||
return (
|
||||
<Tab
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
@@ -19,7 +19,7 @@ const TailwindMarkdown: React.FC<{ content: string }> = ({ content }) => {
|
||||
p: ({ node, ...props }) => <p className="m-0" {...props} />,
|
||||
a: ({ node, ...props }) => (
|
||||
<a
|
||||
className="text-blue-500 hover:underline"
|
||||
className="text-primary-500 inline-block hover:underline"
|
||||
target="_blank"
|
||||
{...props}
|
||||
/>
|
||||
@@ -32,12 +32,12 @@ const TailwindMarkdown: React.FC<{ content: string }> = ({ content }) => {
|
||||
),
|
||||
blockquote: ({ node, ...props }) => (
|
||||
<blockquote
|
||||
className="border-l-4 border-gray-300 pl-4 italic"
|
||||
className="border-l-4 border-default-300 pl-4 italic"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
code: ({ node, ...props }) => (
|
||||
<code className="bg-gray-100 p-1 rounded" {...props} />
|
||||
<code className="bg-default-100 p-1 rounded text-xs" {...props} />
|
||||
)
|
||||
}}
|
||||
>
|
||||
|
38
napcat.webui/src/components/terminal/terminal-instance.tsx
Normal file
38
napcat.webui/src/components/terminal/terminal-instance.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import TerminalManager from '@/controllers/terminal_manager'
|
||||
|
||||
import XTerm, { XTermRef } from '../xterm'
|
||||
|
||||
interface TerminalInstanceProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
export function TerminalInstance({ id }: TerminalInstanceProps) {
|
||||
const termRef = useRef<XTermRef>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleData = (data: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
if (parsed.data) {
|
||||
termRef.current?.write(parsed.data)
|
||||
}
|
||||
} catch (e) {
|
||||
termRef.current?.write(data)
|
||||
}
|
||||
}
|
||||
|
||||
TerminalManager.connectTerminal(id, handleData)
|
||||
|
||||
return () => {
|
||||
TerminalManager.disconnectTerminal(id, handleData)
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const handleInput = (data: string) => {
|
||||
TerminalManager.sendInput(id, data)
|
||||
}
|
||||
|
||||
return <XTerm ref={termRef} onInput={handleInput} className="w-full h-full" />
|
||||
}
|
12
napcat.webui/src/components/under_construction.tsx
Normal file
12
napcat.webui/src/components/under_construction.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export default function UnderConstruction() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full pt-4">
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<div className="text-6xl font-bold text-gray-500">🚧</div>
|
||||
<div className="text-2xl font-bold text-gray-500">
|
||||
Under Construction
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -22,132 +22,146 @@ export type XTermRef = {
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
const XTerm = forwardRef<XTermRef, React.HTMLAttributes<HTMLDivElement>>(
|
||||
(props, ref) => {
|
||||
const domRef = useRef<HTMLDivElement>(null)
|
||||
const terminalRef = useRef<Terminal | null>(null)
|
||||
const { className, ...rest } = props
|
||||
const { theme } = useTheme()
|
||||
useEffect(() => {
|
||||
if (!domRef.current) {
|
||||
return
|
||||
}
|
||||
const terminal = new Terminal({
|
||||
allowTransparency: true,
|
||||
fontFamily: '"Fira Code", "Harmony", "Noto Serif SC", monospace',
|
||||
cursorInactiveStyle: 'outline',
|
||||
drawBoldTextInBrightColors: false,
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1.0
|
||||
export interface XTermProps
|
||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onInput'> {
|
||||
onInput?: (data: string) => void
|
||||
onKey?: (key: string, event: KeyboardEvent) => void
|
||||
}
|
||||
|
||||
const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
||||
const domRef = useRef<HTMLDivElement>(null)
|
||||
const terminalRef = useRef<Terminal | null>(null)
|
||||
const { className, onInput, onKey, ...rest } = props
|
||||
const { theme } = useTheme()
|
||||
useEffect(() => {
|
||||
if (!domRef.current) {
|
||||
return
|
||||
}
|
||||
const terminal = new Terminal({
|
||||
allowTransparency: true,
|
||||
fontFamily: '"Fira Code", "Harmony", "Noto Serif SC", monospace',
|
||||
cursorInactiveStyle: 'outline',
|
||||
drawBoldTextInBrightColors: false
|
||||
})
|
||||
terminalRef.current = terminal
|
||||
const fitAddon = new FitAddon()
|
||||
terminal.loadAddon(
|
||||
new WebLinksAddon((event, uri) => {
|
||||
if (event.ctrlKey) {
|
||||
window.open(uri, '_blank')
|
||||
}
|
||||
})
|
||||
terminalRef.current = terminal
|
||||
const fitAddon = new FitAddon()
|
||||
terminal.loadAddon(
|
||||
new WebLinksAddon((event, uri) => {
|
||||
if (event.ctrlKey) {
|
||||
window.open(uri, '_blank')
|
||||
}
|
||||
)
|
||||
terminal.loadAddon(fitAddon)
|
||||
terminal.loadAddon(new WebglAddon())
|
||||
terminal.open(domRef.current)
|
||||
|
||||
terminal.writeln(
|
||||
gradientText(
|
||||
'Welcome to NapCat WebUI',
|
||||
[255, 0, 0],
|
||||
[0, 255, 0],
|
||||
true,
|
||||
true,
|
||||
true
|
||||
)
|
||||
)
|
||||
|
||||
terminal.onData((data) => {
|
||||
if (onInput) {
|
||||
onInput(data)
|
||||
}
|
||||
})
|
||||
|
||||
terminal.onKey((event) => {
|
||||
if (onKey) {
|
||||
onKey(event.key, event.domEvent)
|
||||
}
|
||||
})
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon.fit()
|
||||
})
|
||||
|
||||
// 字体加载完成后重新调整终端大小
|
||||
document.fonts.ready.then(() => {
|
||||
fitAddon.fit()
|
||||
|
||||
resizeObserver.observe(domRef.current!)
|
||||
})
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
setTimeout(() => {
|
||||
terminal.dispose()
|
||||
}, 0)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (terminalRef.current) {
|
||||
terminalRef.current.options.theme = {
|
||||
background: theme === 'dark' ? '#00000000' : '#ffffff00',
|
||||
foreground: theme === 'dark' ? '#fff' : '#000',
|
||||
selectionBackground:
|
||||
theme === 'dark'
|
||||
? 'rgba(179, 0, 0, 0.3)'
|
||||
: 'rgba(255, 167, 167, 0.3)',
|
||||
cursor: theme === 'dark' ? '#fff' : '#000',
|
||||
cursorAccent: theme === 'dark' ? '#000' : '#fff',
|
||||
black: theme === 'dark' ? '#fff' : '#000'
|
||||
}
|
||||
terminalRef.current.options.fontWeight =
|
||||
theme === 'dark' ? 'normal' : '600'
|
||||
terminalRef.current.options.fontWeightBold =
|
||||
theme === 'dark' ? 'bold' : '900'
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
write: (...args) => {
|
||||
return terminalRef.current?.write(...args)
|
||||
},
|
||||
writeAsync: async (data) => {
|
||||
return new Promise((resolve) => {
|
||||
terminalRef.current?.write(data, resolve)
|
||||
})
|
||||
)
|
||||
terminal.loadAddon(fitAddon)
|
||||
terminal.loadAddon(new WebglAddon())
|
||||
terminal.open(domRef.current)
|
||||
|
||||
terminal.writeln(
|
||||
gradientText(
|
||||
'Welcome to NapCat WebUI',
|
||||
[255, 0, 0],
|
||||
[0, 255, 0],
|
||||
true,
|
||||
true,
|
||||
true
|
||||
)
|
||||
)
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon.fit()
|
||||
})
|
||||
|
||||
// 字体加载完成后重新调整终端大小
|
||||
document.fonts.ready.then(() => {
|
||||
fitAddon.fit()
|
||||
|
||||
resizeObserver.observe(domRef.current!)
|
||||
})
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
setTimeout(() => {
|
||||
terminal.dispose()
|
||||
}, 0)
|
||||
},
|
||||
writeln: (...args) => {
|
||||
return terminalRef.current?.writeln(...args)
|
||||
},
|
||||
writelnAsync: async (data) => {
|
||||
return new Promise((resolve) => {
|
||||
terminalRef.current?.writeln(data, resolve)
|
||||
})
|
||||
},
|
||||
clear: () => {
|
||||
terminalRef.current?.clear()
|
||||
}
|
||||
}, [])
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (terminalRef.current) {
|
||||
terminalRef.current.options.theme = {
|
||||
background: theme === 'dark' ? '#00000000' : '#ffffff00',
|
||||
foreground: theme === 'dark' ? '#fff' : '#000',
|
||||
selectionBackground:
|
||||
theme === 'dark'
|
||||
? 'rgba(179, 0, 0, 0.3)'
|
||||
: 'rgba(255, 167, 167, 0.3)',
|
||||
cursor: theme === 'dark' ? '#fff' : '#000',
|
||||
cursorAccent: theme === 'dark' ? '#000' : '#fff',
|
||||
black: theme === 'dark' ? '#fff' : '#000'
|
||||
}
|
||||
terminalRef.current.options.fontWeight =
|
||||
theme === 'dark' ? 'normal' : '600'
|
||||
terminalRef.current.options.fontWeightBold =
|
||||
theme === 'dark' ? 'bold' : '900'
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
write: (...args) => {
|
||||
return terminalRef.current?.write(...args)
|
||||
},
|
||||
writeAsync: async (data) => {
|
||||
return new Promise((resolve) => {
|
||||
terminalRef.current?.write(data, resolve)
|
||||
})
|
||||
},
|
||||
writeln: (...args) => {
|
||||
return terminalRef.current?.writeln(...args)
|
||||
},
|
||||
writelnAsync: async (data) => {
|
||||
return new Promise((resolve) => {
|
||||
terminalRef.current?.writeln(data, resolve)
|
||||
})
|
||||
},
|
||||
clear: () => {
|
||||
terminalRef.current?.clear()
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'p-2 rounded-md shadow-sm border border-default-200 w-full h-full overflow-hidden bg-opacity-50 backdrop-blur-sm',
|
||||
theme === 'dark' ? 'bg-black' : 'bg-white',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'p-2 rounded-md shadow-sm border border-default-200 w-full h-full overflow-hidden bg-opacity-50 backdrop-blur-sm',
|
||||
theme === 'dark' ? 'bg-black' : 'bg-white',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
ref={domRef}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
ref={domRef}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default XTerm
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
BugIcon2,
|
||||
FileIcon,
|
||||
InfoIcon,
|
||||
LogIcon,
|
||||
RouteIcon,
|
||||
SettingsIcon,
|
||||
SignalTowerIcon,
|
||||
@@ -49,10 +51,10 @@ export const siteConfig = {
|
||||
href: '/config'
|
||||
},
|
||||
{
|
||||
label: '系统日志',
|
||||
label: 'NapCat日志',
|
||||
icon: (
|
||||
<div className="w-5 h-5">
|
||||
<TerminalIcon />
|
||||
<LogIcon />
|
||||
</div>
|
||||
),
|
||||
href: '/logs'
|
||||
@@ -75,6 +77,24 @@ export const siteConfig = {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '文件管理',
|
||||
icon: (
|
||||
<div className="w-5 h-5">
|
||||
<FileIcon />
|
||||
</div>
|
||||
),
|
||||
href: '/file_manager'
|
||||
},
|
||||
{
|
||||
label: '系统终端',
|
||||
icon: (
|
||||
<div className="w-5 h-5">
|
||||
<TerminalIcon />
|
||||
</div>
|
||||
),
|
||||
href: '/terminal'
|
||||
},
|
||||
{
|
||||
label: '关于我们',
|
||||
icon: (
|
||||
|
98
napcat.webui/src/controllers/file_manager.ts
Normal file
98
napcat.webui/src/controllers/file_manager.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { serverRequest } from '@/utils/request'
|
||||
|
||||
export interface FileInfo {
|
||||
name: string
|
||||
isDirectory: boolean
|
||||
size: number
|
||||
mtime: Date
|
||||
}
|
||||
|
||||
export default class FileManager {
|
||||
public static async listFiles(path: string = '/') {
|
||||
const { data } = await serverRequest.get<ServerResponse<FileInfo[]>>(
|
||||
`/File/list?path=${encodeURIComponent(path)}`
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
// 新增:按目录获取
|
||||
public static async listDirectories(path: string = '/') {
|
||||
const { data } = await serverRequest.get<ServerResponse<FileInfo[]>>(
|
||||
`/File/list?path=${encodeURIComponent(path)}&onlyDirectory=true`
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async createDirectory(path: string): Promise<boolean> {
|
||||
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||
'/File/mkdir',
|
||||
{ path }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async delete(path: string) {
|
||||
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||
'/File/delete',
|
||||
{ path }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async readFile(path: string) {
|
||||
const { data } = await serverRequest.get<ServerResponse<string>>(
|
||||
`/File/read?path=${encodeURIComponent(path)}`
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async writeFile(path: string, content: string) {
|
||||
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||
'/File/write',
|
||||
{ path, content }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async createFile(path: string): Promise<boolean> {
|
||||
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||
'/File/create',
|
||||
{ path }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async batchDelete(paths: string[]) {
|
||||
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||
'/File/batchDelete',
|
||||
{ paths }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async rename(oldPath: string, newPath: string) {
|
||||
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||
'/File/rename',
|
||||
{ oldPath, newPath }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async move(sourcePath: string, targetPath: string) {
|
||||
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||
'/File/move',
|
||||
{ sourcePath, targetPath }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async batchMove(
|
||||
items: { sourcePath: string; targetPath: string }[]
|
||||
) {
|
||||
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||
'/File/batchMove',
|
||||
{ items }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
}
|
118
napcat.webui/src/controllers/terminal_manager.ts
Normal file
118
napcat.webui/src/controllers/terminal_manager.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { serverRequest } from '@/utils/request'
|
||||
|
||||
type TerminalCallback = (data: string) => void
|
||||
|
||||
interface TerminalConnection {
|
||||
ws: WebSocket
|
||||
callbacks: Set<TerminalCallback>
|
||||
isConnected: boolean
|
||||
buffer: string[] // 添加缓存数组
|
||||
}
|
||||
|
||||
export interface TerminalSession {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface TerminalInfo {
|
||||
id: string
|
||||
}
|
||||
|
||||
class TerminalManager {
|
||||
private connections: Map<string, TerminalConnection> = new Map()
|
||||
private readonly MAX_BUFFER_SIZE = 1000 // 限制缓存大小
|
||||
|
||||
async createTerminal(cols: number, rows: number): Promise<TerminalSession> {
|
||||
const { data } = await serverRequest.post<ServerResponse<TerminalSession>>(
|
||||
'/Log/terminal/create',
|
||||
{ cols, rows }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
async closeTerminal(id: string): Promise<void> {
|
||||
await serverRequest.post(`/Log/terminal/${id}/close`)
|
||||
}
|
||||
|
||||
async getTerminalList(): Promise<TerminalInfo[]> {
|
||||
const { data } =
|
||||
await serverRequest.get<ServerResponse<TerminalInfo[]>>(
|
||||
'/Log/terminal/list'
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
connectTerminal(id: string, callback: TerminalCallback): WebSocket {
|
||||
let conn = this.connections.get(id)
|
||||
|
||||
if (!conn) {
|
||||
const url = new URL(window.location.href)
|
||||
url.protocol = url.protocol.replace('http', 'ws')
|
||||
url.pathname = `/api/ws/terminal`
|
||||
url.searchParams.set('id', id)
|
||||
const token = JSON.parse(localStorage.getItem('token') || '')
|
||||
if (!token) {
|
||||
throw new Error('No token found')
|
||||
}
|
||||
url.searchParams.set('token', token)
|
||||
const ws = new WebSocket(url.toString())
|
||||
conn = {
|
||||
ws,
|
||||
callbacks: new Set([callback]),
|
||||
isConnected: false,
|
||||
buffer: [] // 初始化缓存
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = event.data
|
||||
// 保存到缓存
|
||||
conn?.buffer.push(data)
|
||||
if ((conn?.buffer.length ?? 0) > this.MAX_BUFFER_SIZE) {
|
||||
conn?.buffer.shift()
|
||||
}
|
||||
conn?.callbacks.forEach((cb) => cb(data))
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
if (conn) conn.isConnected = true
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
if (conn) conn.isConnected = false
|
||||
}
|
||||
|
||||
this.connections.set(id, conn)
|
||||
} else {
|
||||
conn.callbacks.add(callback)
|
||||
// 恢复历史内容
|
||||
conn.buffer.forEach((data) => callback(data))
|
||||
}
|
||||
|
||||
return conn.ws
|
||||
}
|
||||
|
||||
disconnectTerminal(id: string, callback: TerminalCallback) {
|
||||
const conn = this.connections.get(id)
|
||||
if (!conn) return
|
||||
|
||||
conn.callbacks.delete(callback)
|
||||
}
|
||||
|
||||
removeTerminal(id: string) {
|
||||
const conn = this.connections.get(id)
|
||||
if (conn?.ws.readyState === WebSocket.OPEN) {
|
||||
conn.ws.close()
|
||||
}
|
||||
this.connections.delete(id)
|
||||
}
|
||||
|
||||
sendInput(id: string, data: string) {
|
||||
const conn = this.connections.get(id)
|
||||
if (conn?.ws.readyState === WebSocket.OPEN) {
|
||||
conn.ws.send(JSON.stringify({ type: 'input', data }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const terminalManager = new TerminalManager()
|
||||
|
||||
export default terminalManager
|
@@ -9,6 +9,14 @@ export interface Log {
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface TerminalSession {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface TerminalInfo {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default class WebUIManager {
|
||||
public static async checkWebUiLogined() {
|
||||
const { data } =
|
||||
@@ -24,12 +32,20 @@ export default class WebUIManager {
|
||||
return data.data.Credential
|
||||
}
|
||||
|
||||
public static async changePassword(oldToken: string, newToken: string) {
|
||||
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||
'/auth/update_token',
|
||||
{ oldToken, newToken }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async proxy<T>(url = '') {
|
||||
const data = await serverRequest.get<ServerResponse<string>>(
|
||||
'/base/proxy?url=' + encodeURIComponent(url)
|
||||
)
|
||||
data.data.data = JSON.parse(data.data.data)
|
||||
return data.data as ServerResponse<T>
|
||||
return data.data as ServerResponse<T>
|
||||
}
|
||||
|
||||
public static async getPackageInfo() {
|
||||
|
@@ -14,10 +14,12 @@ const useConfig = () => {
|
||||
key: T,
|
||||
value: OneBotConfig['network'][T][0]
|
||||
) => {
|
||||
if (
|
||||
value.name &&
|
||||
config.network[key].some((item) => item.name === value.name)
|
||||
) {
|
||||
const allNetworkNames = Object.keys(config.network).reduce((acc, key) => {
|
||||
const _key = key as keyof OneBotConfig['network']
|
||||
return acc.concat(config.network[_key].map((item) => item.name))
|
||||
}, [] as string[])
|
||||
|
||||
if (value.name && allNetworkNames.includes(value.name)) {
|
||||
throw new Error('已经存在相同的配置项名')
|
||||
}
|
||||
|
||||
|
@@ -4,28 +4,17 @@ import { Spinner } from '@heroui/spinner'
|
||||
import { useRequest } from 'ahooks'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { BietiaopIcon, WebUIIcon } from '@/components/icons'
|
||||
import HoverTiltedCard from '@/components/hover_titled_card'
|
||||
import NapCatRepoInfo from '@/components/napcat_repo_info'
|
||||
import { title } from '@/components/primitives'
|
||||
|
||||
import logo from '@/assets/images/logo.png'
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
|
||||
import packageJson from '../../../package.json'
|
||||
|
||||
function VersionInfo() {
|
||||
const { data, loading, error } = useRequest(WebUIManager.getPackageInfo)
|
||||
return (
|
||||
<div className="flex items-center gap-2 mb-5">
|
||||
<Chip
|
||||
startContent={
|
||||
<Chip color="danger" size="sm" className="-ml-0.5 select-none">
|
||||
WebUI
|
||||
</Chip>
|
||||
}
|
||||
>
|
||||
{packageJson.version}
|
||||
</Chip>
|
||||
<Chip
|
||||
startContent={
|
||||
<Chip color="warning" size="sm" className="-ml-0.5 select-none">
|
||||
@@ -51,21 +40,8 @@ export default function AboutPage() {
|
||||
<title>关于 NapCat WebUI</title>
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
|
||||
<div className="max-w-full w-[1000px] px-5 flex flex-col items-center">
|
||||
<div className="flex flex-col md:flex-row items-center">
|
||||
<Image
|
||||
alt="logo"
|
||||
className="flex-shrink-0 w-52 md:w-48 mr-2"
|
||||
src={logo}
|
||||
/>
|
||||
<div className="flex -mt-9 md:mt-0">
|
||||
<WebUIIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex opacity-60 items-center gap-2 mb-2 font-ubuntu">
|
||||
Created By
|
||||
<div className="flex scale-80 -ml-5 -mr-5">
|
||||
<BietiaopIcon />
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row items-center mb-6">
|
||||
<HoverTiltedCard imageSrc={logo} />
|
||||
</div>
|
||||
<VersionInfo />
|
||||
<div className="mb-6 flex flex-col items-center gap-4">
|
||||
|
81
napcat.webui/src/pages/dashboard/config/change_password.tsx
Normal file
81
napcat.webui/src/pages/dashboard/config/change_password.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Input } from '@heroui/input'
|
||||
import { useLocalStorage } from '@uidotdev/usehooks'
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import key from '@/const/key'
|
||||
|
||||
import SaveButtons from '@/components/button/save_buttons'
|
||||
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
|
||||
const ChangePasswordCard = () => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit: handleWebuiSubmit,
|
||||
formState: { isSubmitting },
|
||||
reset
|
||||
} = useForm<{
|
||||
oldToken: string
|
||||
newToken: string
|
||||
}>({
|
||||
defaultValues: {
|
||||
oldToken: '',
|
||||
newToken: ''
|
||||
}
|
||||
})
|
||||
|
||||
const navigate = useNavigate()
|
||||
const [_, setToken] = useLocalStorage(key.token, '')
|
||||
|
||||
const onSubmit = handleWebuiSubmit(async (data) => {
|
||||
try {
|
||||
await WebUIManager.changePassword(data.oldToken, data.newToken)
|
||||
toast.success('修改成功')
|
||||
setToken('')
|
||||
localStorage.removeItem(key.token)
|
||||
navigate('/web_login')
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message
|
||||
toast.error(`修改失败: ${msg}`)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>修改密码 - NapCat WebUI</title>
|
||||
<Controller
|
||||
control={control}
|
||||
name="oldToken"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="旧密码"
|
||||
placeholder="请输入旧密码"
|
||||
type="password"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="newToken"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="新密码"
|
||||
placeholder="请输入新密码"
|
||||
type="password"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<SaveButtons
|
||||
onSubmit={onSubmit}
|
||||
reset={reset}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangePasswordCard
|
@@ -1,111 +1,27 @@
|
||||
import { Card, CardBody } from '@heroui/card'
|
||||
import { Tab, Tabs } from '@heroui/tabs'
|
||||
import { useLocalStorage } from '@uidotdev/usehooks'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
import key from '@/const/key'
|
||||
|
||||
import PageLoading from '@/components/page_loading'
|
||||
|
||||
import useConfig from '@/hooks/use-config'
|
||||
import useMusic from '@/hooks/use-music'
|
||||
|
||||
import ChangePasswordCard from './change_password'
|
||||
import OneBotConfigCard from './onebot'
|
||||
import WebUIConfigCard from './webui'
|
||||
|
||||
export default function ConfigPage() {
|
||||
const { config, saveConfigWithoutNetwork, refreshConfig } = useConfig()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const {
|
||||
control: onebotControl,
|
||||
handleSubmit: handleOnebotSubmit,
|
||||
formState: { isSubmitting: isOnebotSubmitting },
|
||||
setValue: setOnebotValue
|
||||
} = useForm<IConfig['onebot']>({
|
||||
defaultValues: {
|
||||
musicSignUrl: '',
|
||||
enableLocalFile2Url: false,
|
||||
parseMultMsg: false
|
||||
}
|
||||
})
|
||||
export interface ConfigPageProps {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const {
|
||||
control: webuiControl,
|
||||
handleSubmit: handleWebuiSubmit,
|
||||
formState: { isSubmitting: isWebuiSubmitting },
|
||||
setValue: setWebuiValue
|
||||
} = useForm<IConfig['webui']>({
|
||||
defaultValues: {
|
||||
background: '',
|
||||
musicListID: '',
|
||||
customIcons: {}
|
||||
}
|
||||
})
|
||||
|
||||
const isMediumUp = useMediaQuery({ minWidth: 768 })
|
||||
const [b64img, setB64img] = useLocalStorage(key.backgroundImage, '')
|
||||
const [customIcons, setCustomIcons] = useLocalStorage<Record<string, string>>(
|
||||
key.customIcons,
|
||||
{}
|
||||
const ConfingPageItem: React.FC<ConfigPageProps> = ({ children }) => {
|
||||
return (
|
||||
<Card className="bg-opacity-50 backdrop-blur-sm">
|
||||
<CardBody className="items-center py-5">
|
||||
<div className="w-96 max-w-full flex flex-col gap-2">{children}</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
const { setListId, listId } = useMusic()
|
||||
const resetOneBot = () => {
|
||||
setOnebotValue('musicSignUrl', config.musicSignUrl)
|
||||
setOnebotValue('enableLocalFile2Url', config.enableLocalFile2Url)
|
||||
setOnebotValue('parseMultMsg', config.parseMultMsg)
|
||||
}
|
||||
}
|
||||
|
||||
const resetWebUI = () => {
|
||||
setWebuiValue('musicListID', listId)
|
||||
setWebuiValue('customIcons', customIcons)
|
||||
setWebuiValue('background', b64img)
|
||||
}
|
||||
|
||||
const onOneBotSubmit = handleOnebotSubmit((data) => {
|
||||
try {
|
||||
saveConfigWithoutNetwork(data)
|
||||
toast.success('保存成功')
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message
|
||||
toast.error(`保存失败: ${msg}`)
|
||||
}
|
||||
})
|
||||
|
||||
const onWebuiSubmit = handleWebuiSubmit((data) => {
|
||||
try {
|
||||
setListId(data.musicListID)
|
||||
setCustomIcons(data.customIcons)
|
||||
setB64img(data.background)
|
||||
toast.success('保存成功')
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message
|
||||
toast.error(`保存失败: ${msg}`)
|
||||
}
|
||||
})
|
||||
|
||||
const onRefresh = async (shotTip = true) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
await refreshConfig()
|
||||
if (shotTip) toast.success('刷新成功')
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message
|
||||
toast.error(`刷新失败: ${msg}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
resetOneBot()
|
||||
resetWebUI()
|
||||
}, [config])
|
||||
|
||||
useEffect(() => {
|
||||
onRefresh(false)
|
||||
}, [])
|
||||
export default function ConfigPage() {
|
||||
const isMediumUp = useMediaQuery({ minWidth: 768 })
|
||||
|
||||
return (
|
||||
<section className="w-[1000px] max-w-full md:mx-auto gap-4 py-8 px-2 md:py-10">
|
||||
@@ -122,23 +38,20 @@ export default function ConfigPage() {
|
||||
}}
|
||||
>
|
||||
<Tab title="OneBot配置" key="onebot">
|
||||
<PageLoading loading={loading} />
|
||||
<OneBotConfigCard
|
||||
isSubmitting={isOnebotSubmitting}
|
||||
onRefresh={onRefresh}
|
||||
onSubmit={onOneBotSubmit}
|
||||
control={onebotControl}
|
||||
reset={resetOneBot}
|
||||
/>
|
||||
<ConfingPageItem>
|
||||
<OneBotConfigCard />
|
||||
</ConfingPageItem>
|
||||
</Tab>
|
||||
<Tab title="WebUI配置" key="webui">
|
||||
<WebUIConfigCard
|
||||
isSubmitting={isWebuiSubmitting}
|
||||
onRefresh={onRefresh}
|
||||
onSubmit={onWebuiSubmit}
|
||||
control={webuiControl}
|
||||
reset={resetWebUI}
|
||||
/>
|
||||
<ConfingPageItem>
|
||||
<WebUIConfigCard />
|
||||
</ConfingPageItem>
|
||||
</Tab>
|
||||
|
||||
<Tab title="修改密码" key="token">
|
||||
<ConfingPageItem>
|
||||
<ChangePasswordCard />
|
||||
</ConfingPageItem>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</section>
|
||||
|
@@ -1,68 +1,110 @@
|
||||
import { Card, CardBody } from '@heroui/card'
|
||||
import { Input } from '@heroui/input'
|
||||
import { Controller } from 'react-hook-form'
|
||||
import type { Control } from 'react-hook-form'
|
||||
import { useEffect, useState } 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 SwitchCard from '@/components/switch_card'
|
||||
|
||||
export interface OneBotConfigCardProps {
|
||||
control: Control<IConfig['onebot']>
|
||||
onSubmit: () => void
|
||||
reset: () => void
|
||||
isSubmitting: boolean
|
||||
onRefresh: () => void
|
||||
}
|
||||
const OneBotConfigCard: React.FC<OneBotConfigCardProps> = (props) => {
|
||||
const { control, onSubmit, reset, isSubmitting, onRefresh } = props
|
||||
import useConfig from '@/hooks/use-config'
|
||||
|
||||
const OneBotConfigCard = () => {
|
||||
const { config, saveConfigWithoutNetwork, refreshConfig } = useConfig()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const {
|
||||
control,
|
||||
handleSubmit: handleOnebotSubmit,
|
||||
formState: { isSubmitting },
|
||||
setValue: setOnebotValue
|
||||
} = useForm<IConfig['onebot']>({
|
||||
defaultValues: {
|
||||
musicSignUrl: '',
|
||||
enableLocalFile2Url: false,
|
||||
parseMultMsg: false
|
||||
}
|
||||
})
|
||||
const reset = () => {
|
||||
setOnebotValue('musicSignUrl', config.musicSignUrl)
|
||||
setOnebotValue('enableLocalFile2Url', config.enableLocalFile2Url)
|
||||
setOnebotValue('parseMultMsg', config.parseMultMsg)
|
||||
}
|
||||
|
||||
const onSubmit = handleOnebotSubmit((data) => {
|
||||
try {
|
||||
saveConfigWithoutNetwork(data)
|
||||
toast.success('保存成功')
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message
|
||||
toast.error(`保存失败: ${msg}`)
|
||||
}
|
||||
})
|
||||
|
||||
const onRefresh = async (shotTip = true) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
await refreshConfig()
|
||||
if (shotTip) toast.success('刷新成功')
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message
|
||||
toast.error(`刷新失败: ${msg}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reset()
|
||||
}, [config])
|
||||
|
||||
useEffect(() => {
|
||||
onRefresh(false)
|
||||
}, [])
|
||||
|
||||
if (loading) return <PageLoading loading={true} />
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>OneBot配置 - NapCat WebUI</title>
|
||||
<Card className="bg-opacity-50 backdrop-blur-sm">
|
||||
<CardBody className="items-center py-5">
|
||||
<div className="w-96 max-w-full flex flex-col gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="musicSignUrl"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="音乐签名地址"
|
||||
placeholder="请输入音乐签名地址"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="enableLocalFile2Url"
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
description="启用本地文件到URL"
|
||||
label="启用本地文件到URL"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parseMultMsg"
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
description="启用上报解析合并消息"
|
||||
label="启用上报解析合并消息"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<SaveButtons
|
||||
onSubmit={onSubmit}
|
||||
reset={reset}
|
||||
isSubmitting={isSubmitting}
|
||||
refresh={onRefresh}
|
||||
/>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Controller
|
||||
control={control}
|
||||
name="musicSignUrl"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="音乐签名地址"
|
||||
placeholder="请输入音乐签名地址"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="enableLocalFile2Url"
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
description="启用本地文件到URL"
|
||||
label="启用本地文件到URL"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parseMultMsg"
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
description="启用上报解析合并消息"
|
||||
label="启用上报解析合并消息"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<SaveButtons
|
||||
onSubmit={onSubmit}
|
||||
reset={reset}
|
||||
isSubmitting={isSubmitting}
|
||||
refresh={onRefresh}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@@ -1,69 +1,99 @@
|
||||
import { Card, CardBody } from '@heroui/card'
|
||||
import { Input } from '@heroui/input'
|
||||
import { Controller } from 'react-hook-form'
|
||||
import type { Control } from 'react-hook-form'
|
||||
import { useLocalStorage } from '@uidotdev/usehooks'
|
||||
import { useEffect } from 'react'
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import key from '@/const/key'
|
||||
|
||||
import SaveButtons from '@/components/button/save_buttons'
|
||||
import ImageInput from '@/components/input/image_input'
|
||||
|
||||
import useMusic from '@/hooks/use-music'
|
||||
|
||||
import { siteConfig } from '@/config/site'
|
||||
|
||||
export interface WebUIConfigCardProps {
|
||||
control: Control<IConfig['webui']>
|
||||
onSubmit: () => void
|
||||
reset: () => void
|
||||
isSubmitting: boolean
|
||||
onRefresh: () => void
|
||||
}
|
||||
const WebUIConfigCard: React.FC<WebUIConfigCardProps> = (props) => {
|
||||
const { control, onSubmit, reset, isSubmitting, onRefresh } = props
|
||||
const WebUIConfigCard = () => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit: handleWebuiSubmit,
|
||||
formState: { isSubmitting },
|
||||
setValue: setWebuiValue
|
||||
} = useForm<IConfig['webui']>({
|
||||
defaultValues: {
|
||||
background: '',
|
||||
musicListID: '',
|
||||
customIcons: {}
|
||||
}
|
||||
})
|
||||
|
||||
const [b64img, setB64img] = useLocalStorage(key.backgroundImage, '')
|
||||
const [customIcons, setCustomIcons] = useLocalStorage<Record<string, string>>(
|
||||
key.customIcons,
|
||||
{}
|
||||
)
|
||||
const { setListId, listId } = useMusic()
|
||||
|
||||
const reset = () => {
|
||||
setWebuiValue('musicListID', listId)
|
||||
setWebuiValue('customIcons', customIcons)
|
||||
setWebuiValue('background', b64img)
|
||||
}
|
||||
|
||||
const onSubmit = handleWebuiSubmit((data) => {
|
||||
try {
|
||||
setListId(data.musicListID)
|
||||
setCustomIcons(data.customIcons)
|
||||
setB64img(data.background)
|
||||
toast.success('保存成功')
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message
|
||||
toast.error(`保存失败: ${msg}`)
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
reset()
|
||||
}, [listId, customIcons, b64img])
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>WebUI配置 - NapCat WebUI</title>
|
||||
<Card className="bg-opacity-50 backdrop-blur-sm">
|
||||
<CardBody className="items-center py-5">
|
||||
<div className="w-96 max-w-full flex flex-col gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="musicListID"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="网易云音乐歌单ID(网页内音乐播放器)"
|
||||
placeholder="请输入歌单ID"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex-shrink-0 w-full">背景图</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="background"
|
||||
render={({ field }) => <ImageInput {...field} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>自定义图标</div>
|
||||
{siteConfig.navItems.map((item) => (
|
||||
<Controller
|
||||
key={item.label}
|
||||
control={control}
|
||||
name={`customIcons.${item.label}`}
|
||||
render={({ field }) => (
|
||||
<ImageInput {...field} label={item.label} />
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<SaveButtons
|
||||
onSubmit={onSubmit}
|
||||
reset={reset}
|
||||
isSubmitting={isSubmitting}
|
||||
refresh={onRefresh}
|
||||
/>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Controller
|
||||
control={control}
|
||||
name="musicListID"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="网易云音乐歌单ID(网页内音乐播放器)"
|
||||
placeholder="请输入歌单ID"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex-shrink-0 w-full">背景图</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="background"
|
||||
render={({ field }) => <ImageInput {...field} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>自定义图标</div>
|
||||
{siteConfig.navItems.map((item) => (
|
||||
<Controller
|
||||
key={item.label}
|
||||
control={control}
|
||||
name={`customIcons.${item.label}`}
|
||||
render={({ field }) => <ImageInput {...field} label={item.label} />}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<SaveButtons
|
||||
onSubmit={onSubmit}
|
||||
reset={reset}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
433
napcat.webui/src/pages/dashboard/file_manager.tsx
Normal file
433
napcat.webui/src/pages/dashboard/file_manager.tsx
Normal file
@@ -0,0 +1,433 @@
|
||||
import { BreadcrumbItem, Breadcrumbs } from '@heroui/breadcrumbs'
|
||||
import { Button } from '@heroui/button'
|
||||
import { Input } from '@heroui/input'
|
||||
import type { Selection, SortDescriptor } from '@react-types/shared'
|
||||
import path from 'path-browserify'
|
||||
import { useEffect, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { FiMove, FiPlus } from 'react-icons/fi'
|
||||
import { MdRefresh } from 'react-icons/md'
|
||||
import { TbTrash } from 'react-icons/tb'
|
||||
import { TiArrowBack } from 'react-icons/ti'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import CreateFileModal from '@/components/file_manage/create_file_modal'
|
||||
import FileEditModal from '@/components/file_manage/file_edit_modal'
|
||||
import FileTable from '@/components/file_manage/file_table'
|
||||
import MoveModal from '@/components/file_manage/move_modal'
|
||||
import RenameModal from '@/components/file_manage/rename_modal'
|
||||
|
||||
import useDialog from '@/hooks/use-dialog'
|
||||
|
||||
import FileManager, { FileInfo } from '@/controllers/file_manager'
|
||||
|
||||
export default function FileManagerPage() {
|
||||
const [files, setFiles] = useState<FileInfo[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||
column: 'name',
|
||||
direction: 'ascending'
|
||||
})
|
||||
const dialog = useDialog()
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
// 修改 currentPath 初始化逻辑,去掉可能的前导斜杠
|
||||
let currentPath = decodeURIComponent(location.hash.slice(1) || '/')
|
||||
if (/^\/[A-Z]:$/i.test(currentPath)) {
|
||||
currentPath = currentPath.slice(1)
|
||||
}
|
||||
const [editingFile, setEditingFile] = useState<{
|
||||
path: string
|
||||
content: string
|
||||
} | null>(null)
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
|
||||
const [newFileName, setNewFileName] = useState('')
|
||||
const [fileType, setFileType] = useState<'file' | 'directory'>('file')
|
||||
const [selectedFiles, setSelectedFiles] = useState<Selection>(new Set())
|
||||
const [isRenameModalOpen, setIsRenameModalOpen] = useState(false)
|
||||
const [isMoveModalOpen, setIsMoveModalOpen] = useState(false)
|
||||
const [renamingFile, setRenamingFile] = useState<string>('')
|
||||
const [moveTargetPath, setMoveTargetPath] = useState('')
|
||||
const [jumpPath, setJumpPath] = useState('')
|
||||
|
||||
const sortFiles = (files: FileInfo[], descriptor: typeof sortDescriptor) => {
|
||||
return [...files].sort((a, b) => {
|
||||
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1
|
||||
const direction = descriptor.direction === 'ascending' ? 1 : -1
|
||||
switch (descriptor.column) {
|
||||
case 'name':
|
||||
return direction * a.name.localeCompare(b.name)
|
||||
case 'type': {
|
||||
const aType = a.isDirectory ? '目录' : '文件'
|
||||
const bType = a.isDirectory ? '目录' : '文件'
|
||||
return direction * aType.localeCompare(bType)
|
||||
}
|
||||
case 'size':
|
||||
return direction * ((a.size || 0) - (b.size || 0))
|
||||
case 'mtime':
|
||||
return (
|
||||
direction *
|
||||
(new Date(a.mtime).getTime() - new Date(b.mtime).getTime())
|
||||
)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const loadFiles = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const fileList = await FileManager.listFiles(currentPath)
|
||||
setFiles(sortFiles(fileList, sortDescriptor))
|
||||
} catch (error) {
|
||||
toast.error('加载文件列表失败')
|
||||
setFiles([])
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadFiles()
|
||||
}, [currentPath])
|
||||
|
||||
const handleSortChange = (descriptor: typeof sortDescriptor) => {
|
||||
setSortDescriptor(descriptor)
|
||||
setFiles((prev) => sortFiles(prev, descriptor))
|
||||
}
|
||||
|
||||
const handleDirectoryClick = (dirPath: string) => {
|
||||
if (dirPath === '..') {
|
||||
if (/^[A-Z]:$/i.test(currentPath)) {
|
||||
navigate('/file_manager#/')
|
||||
return
|
||||
}
|
||||
const parentPath = path.dirname(currentPath)
|
||||
navigate(
|
||||
`/file_manager#${encodeURIComponent(parentPath === currentPath ? '/' : parentPath)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
navigate(
|
||||
`/file_manager#${encodeURIComponent(path.join(currentPath, dirPath))}`
|
||||
)
|
||||
}
|
||||
|
||||
const handleEdit = async (filePath: string) => {
|
||||
try {
|
||||
const content = await FileManager.readFile(filePath)
|
||||
setEditingFile({ path: filePath, content })
|
||||
} catch (error) {
|
||||
toast.error('打开文件失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editingFile) return
|
||||
try {
|
||||
await FileManager.writeFile(editingFile.path, editingFile.content)
|
||||
toast.success('保存成功')
|
||||
setEditingFile(null)
|
||||
loadFiles()
|
||||
} catch (error) {
|
||||
toast.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (filePath: string) => {
|
||||
dialog.confirm({
|
||||
title: '删除文件',
|
||||
content: <div>确定要删除文件 {filePath} 吗?</div>,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await FileManager.delete(filePath)
|
||||
toast.success('删除成功')
|
||||
loadFiles()
|
||||
} catch (error) {
|
||||
toast.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newFileName) return
|
||||
const newPath = path.join(currentPath, newFileName)
|
||||
try {
|
||||
if (fileType === 'directory') {
|
||||
if (!(await FileManager.createDirectory(newPath))) {
|
||||
toast.error('目录已存在')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (!(await FileManager.createFile(newPath))) {
|
||||
toast.error('文件已存在')
|
||||
return
|
||||
}
|
||||
}
|
||||
toast.success('创建成功')
|
||||
setIsCreateModalOpen(false)
|
||||
setNewFileName('')
|
||||
loadFiles()
|
||||
} catch (error) {
|
||||
toast.error((error as Error)?.message || '创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
const selectedArray =
|
||||
selectedFiles instanceof Set
|
||||
? Array.from(selectedFiles)
|
||||
: files.map((f) => f.name)
|
||||
if (selectedArray.length === 0) return
|
||||
dialog.confirm({
|
||||
title: '批量删除',
|
||||
content: <div>确定要删除选中的 {selectedArray.length} 个项目吗?</div>,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const paths = selectedArray.map((key) =>
|
||||
path.join(currentPath, key.toString())
|
||||
)
|
||||
await FileManager.batchDelete(paths)
|
||||
toast.success('批量删除成功')
|
||||
setSelectedFiles(new Set())
|
||||
loadFiles()
|
||||
} catch (error) {
|
||||
toast.error('批量删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRename = async () => {
|
||||
if (!renamingFile || !newFileName) return
|
||||
try {
|
||||
await FileManager.rename(
|
||||
path.join(currentPath, renamingFile),
|
||||
path.join(currentPath, newFileName)
|
||||
)
|
||||
toast.success('重命名成功')
|
||||
setIsRenameModalOpen(false)
|
||||
setRenamingFile('')
|
||||
setNewFileName('')
|
||||
loadFiles()
|
||||
} catch (error) {
|
||||
toast.error('重命名失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleMove = async (sourceName: string) => {
|
||||
if (!moveTargetPath) return
|
||||
try {
|
||||
await FileManager.move(
|
||||
path.join(currentPath, sourceName),
|
||||
path.join(moveTargetPath, sourceName)
|
||||
)
|
||||
toast.success('移动成功')
|
||||
setIsMoveModalOpen(false)
|
||||
setMoveTargetPath('')
|
||||
loadFiles()
|
||||
} catch (error) {
|
||||
toast.error('移动失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchMove = async () => {
|
||||
if (!moveTargetPath) return
|
||||
const selectedArray =
|
||||
selectedFiles instanceof Set
|
||||
? Array.from(selectedFiles)
|
||||
: files.map((f) => f.name)
|
||||
if (selectedArray.length === 0) return
|
||||
try {
|
||||
const items = selectedArray.map((name) => ({
|
||||
sourcePath: path.join(currentPath, name.toString()),
|
||||
targetPath: path.join(moveTargetPath, name.toString())
|
||||
}))
|
||||
await FileManager.batchMove(items)
|
||||
toast.success('批量移动成功')
|
||||
setIsMoveModalOpen(false)
|
||||
setMoveTargetPath('')
|
||||
setSelectedFiles(new Set())
|
||||
loadFiles()
|
||||
} catch (error) {
|
||||
toast.error('批量移动失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopyPath = (fileName: string) => {
|
||||
navigator.clipboard.writeText(path.join(currentPath, fileName))
|
||||
toast.success('路径已复制')
|
||||
}
|
||||
|
||||
const handleMoveClick = (fileName: string) => {
|
||||
setRenamingFile(fileName)
|
||||
setMoveTargetPath('')
|
||||
setIsMoveModalOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-4 flex items-center gap-4 sticky top-14 z-10 bg-content1 py-1">
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
onPress={() => handleDirectoryClick('..')}
|
||||
className="text-lg"
|
||||
>
|
||||
<TiArrowBack />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
onPress={() => setIsCreateModalOpen(true)}
|
||||
className="text-lg"
|
||||
>
|
||||
<FiPlus />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="danger"
|
||||
isLoading={loading}
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
onPress={loadFiles}
|
||||
className="text-lg"
|
||||
>
|
||||
<MdRefresh />
|
||||
</Button>
|
||||
{((selectedFiles instanceof Set && selectedFiles.size > 0) ||
|
||||
selectedFiles === 'all') && (
|
||||
<>
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={handleBatchDelete}
|
||||
className="text-sm"
|
||||
startContent={<TbTrash className="text-lg" />}
|
||||
>
|
||||
(
|
||||
{selectedFiles instanceof Set ? selectedFiles.size : files.length}
|
||||
)
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => {
|
||||
setMoveTargetPath('')
|
||||
setIsMoveModalOpen(true)
|
||||
}}
|
||||
className="text-sm"
|
||||
startContent={<FiMove className="text-lg" />}
|
||||
>
|
||||
(
|
||||
{selectedFiles instanceof Set ? selectedFiles.size : files.length}
|
||||
)
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Breadcrumbs className="flex-1 shadow-small px-2 py-2 rounded-lg">
|
||||
{currentPath.split('/').map((part, index, parts) => (
|
||||
<BreadcrumbItem
|
||||
key={part}
|
||||
isCurrent={index === parts.length - 1}
|
||||
onPress={() => {
|
||||
const newPath = parts.slice(0, index + 1).join('/')
|
||||
navigate(`/file_manager#${encodeURIComponent(newPath)}`)
|
||||
}}
|
||||
>
|
||||
{part}
|
||||
</BreadcrumbItem>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="输入跳转路径"
|
||||
value={jumpPath}
|
||||
onChange={(e) => setJumpPath(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && jumpPath.trim() !== '') {
|
||||
navigate(`/file_manager#${encodeURIComponent(jumpPath.trim())}`)
|
||||
}
|
||||
}}
|
||||
className="ml-auto w-64"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FileTable
|
||||
files={files}
|
||||
currentPath={currentPath}
|
||||
loading={loading}
|
||||
sortDescriptor={sortDescriptor}
|
||||
onSortChange={handleSortChange}
|
||||
selectedFiles={selectedFiles}
|
||||
onSelectionChange={setSelectedFiles}
|
||||
onDirectoryClick={handleDirectoryClick}
|
||||
onEdit={handleEdit}
|
||||
onRenameRequest={(name) => {
|
||||
setRenamingFile(name)
|
||||
setNewFileName(name)
|
||||
setIsRenameModalOpen(true)
|
||||
}}
|
||||
onMoveRequest={handleMoveClick}
|
||||
onCopyPath={handleCopyPath}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<FileEditModal
|
||||
isOpen={!!editingFile}
|
||||
file={editingFile}
|
||||
onClose={() => setEditingFile(null)}
|
||||
onSave={handleSave}
|
||||
onContentChange={(newContent) =>
|
||||
setEditingFile((prev) =>
|
||||
prev ? { ...prev, content: newContent ?? '' } : null
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<CreateFileModal
|
||||
isOpen={isCreateModalOpen}
|
||||
fileType={fileType}
|
||||
newFileName={newFileName}
|
||||
onTypeChange={setFileType}
|
||||
onNameChange={(e) => setNewFileName(e.target.value)}
|
||||
onClose={() => setIsCreateModalOpen(false)}
|
||||
onCreate={handleCreate}
|
||||
/>
|
||||
|
||||
<RenameModal
|
||||
isOpen={isRenameModalOpen}
|
||||
newFileName={newFileName}
|
||||
onNameChange={(e) => setNewFileName(e.target.value)}
|
||||
onClose={() => setIsRenameModalOpen(false)}
|
||||
onRename={handleRename}
|
||||
/>
|
||||
|
||||
<MoveModal
|
||||
isOpen={isMoveModalOpen}
|
||||
moveTargetPath={moveTargetPath}
|
||||
selectionInfo={
|
||||
selectedFiles instanceof Set && selectedFiles.size > 0
|
||||
? `${selectedFiles.size} 个项目`
|
||||
: renamingFile
|
||||
}
|
||||
onClose={() => setIsMoveModalOpen(false)}
|
||||
onMove={() =>
|
||||
selectedFiles instanceof Set && selectedFiles.size > 0
|
||||
? handleBatchMove()
|
||||
: handleMove(renamingFile)
|
||||
}
|
||||
onSelect={(dir) => setMoveTargetPath(dir)} // 替换原有 onTargetChange
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
171
napcat.webui/src/pages/dashboard/terminal.tsx
Normal file
171
napcat.webui/src/pages/dashboard/terminal.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
arrayMove,
|
||||
horizontalListSortingStrategy
|
||||
} from '@dnd-kit/sortable'
|
||||
import { Button } from '@heroui/button'
|
||||
import { useEffect, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { IoAdd, IoClose } from 'react-icons/io5'
|
||||
|
||||
import { TabList, TabPanel, Tabs } from '@/components/tabs'
|
||||
import { SortableTab } from '@/components/tabs/sortable_tab.tsx'
|
||||
import { TerminalInstance } from '@/components/terminal/terminal-instance'
|
||||
|
||||
import terminalManager from '@/controllers/terminal_manager'
|
||||
|
||||
interface TerminalTab {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export default function TerminalPage() {
|
||||
const [tabs, setTabs] = useState<TerminalTab[]>([])
|
||||
const [selectedTab, setSelectedTab] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
// 获取已存在的终端列表
|
||||
terminalManager.getTerminalList().then((terminals) => {
|
||||
if (terminals.length === 0) return
|
||||
|
||||
const newTabs = terminals.map((terminal) => ({
|
||||
id: terminal.id,
|
||||
title: terminal.id
|
||||
}))
|
||||
|
||||
setTabs(newTabs)
|
||||
setSelectedTab(newTabs[0].id)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const createNewTerminal = async () => {
|
||||
try {
|
||||
const { id } = await terminalManager.createTerminal(80, 24)
|
||||
const newTab = {
|
||||
id,
|
||||
title: id
|
||||
}
|
||||
|
||||
setTabs((prev) => [...prev, newTab])
|
||||
setSelectedTab(id)
|
||||
} catch (error) {
|
||||
console.error('Failed to create terminal:', error)
|
||||
toast.error('创建终端失败')
|
||||
}
|
||||
}
|
||||
|
||||
const closeTerminal = async (id: string) => {
|
||||
try {
|
||||
await terminalManager.closeTerminal(id)
|
||||
terminalManager.removeTerminal(id)
|
||||
if (selectedTab === id) {
|
||||
const previousIndex = tabs.findIndex((tab) => tab.id === id) - 1
|
||||
if (previousIndex >= 0) {
|
||||
setSelectedTab(tabs[previousIndex].id)
|
||||
} else {
|
||||
setSelectedTab(tabs[0]?.id || '')
|
||||
}
|
||||
}
|
||||
setTabs((prev) => prev.filter((tab) => tab.id !== id))
|
||||
} catch (error) {
|
||||
toast.error('关闭终端失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (active.id !== over?.id) {
|
||||
setTabs((items) => {
|
||||
const oldIndex = items.findIndex((item) => item.id === active.id)
|
||||
const newIndex = items.findIndex((item) => item.id === over?.id)
|
||||
return arrayMove(items, oldIndex, newIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={selectedTab}
|
||||
onChange={setSelectedTab}
|
||||
className="h-full overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-shrink-0 flex-grow-0">
|
||||
<TabList className="flex-1 !overflow-x-auto w-full hide-scrollbar">
|
||||
<SortableContext
|
||||
items={tabs}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<SortableTab
|
||||
key={tab.id}
|
||||
id={tab.id}
|
||||
value={tab.id}
|
||||
isSelected={selectedTab === tab.id}
|
||||
className="flex gap-2 items-center flex-shrink-0"
|
||||
>
|
||||
{tab.title}
|
||||
<Button
|
||||
isIconOnly
|
||||
radius="full"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
className="min-w-0 w-4 h-4 flex-shrink-0"
|
||||
onPress={() => closeTerminal(tab.id)}
|
||||
color={selectedTab === tab.id ? 'danger' : 'default'}
|
||||
>
|
||||
<IoClose />
|
||||
</Button>
|
||||
</SortableTab>
|
||||
))}
|
||||
</SortableContext>
|
||||
</TabList>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={createNewTerminal}
|
||||
startContent={<IoAdd />}
|
||||
className="text-xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden">
|
||||
{tabs.length === 0 && (
|
||||
<div className="flex flex-col gap-2 items-center justify-center h-full text-gray-500 py-5">
|
||||
<IoAdd className="text-4xl" />
|
||||
<div className="text-sm">点击右上角按钮创建终端</div>
|
||||
</div>
|
||||
)}
|
||||
{tabs.map((tab) => (
|
||||
<TabPanel key={tab.id} value={tab.id} className="h-full">
|
||||
<TerminalInstance id={tab.id} />
|
||||
</TabPanel>
|
||||
))}
|
||||
</div>
|
||||
</Tabs>
|
||||
</DndContext>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -9,8 +9,10 @@ import ConfigPage from './dashboard/config'
|
||||
import DebugPage from './dashboard/debug'
|
||||
import HttpDebug from './dashboard/debug/http'
|
||||
import WSDebug from './dashboard/debug/websocket'
|
||||
import FileManagerPage from './dashboard/file_manager'
|
||||
import LogsPage from './dashboard/logs'
|
||||
import NetworkPage from './dashboard/network'
|
||||
import TerminalPage from './dashboard/terminal'
|
||||
|
||||
export default function IndexPage() {
|
||||
const location = useLocation()
|
||||
@@ -33,6 +35,8 @@ export default function IndexPage() {
|
||||
<Route path="ws" element={<WSDebug />} />
|
||||
<Route path="http" element={<HttpDebug />} />
|
||||
</Route>
|
||||
<Route element={<FileManagerPage />} path="/file_manager" />
|
||||
<Route element={<TerminalPage />} path="/terminal" />
|
||||
<Route element={<AboutPage />} path="/about" />
|
||||
</Routes>
|
||||
</motion.div>
|
||||
|
@@ -1,15 +1,26 @@
|
||||
@import url("./fonts.css");
|
||||
|
||||
@import url('./fonts.css');
|
||||
@import url('./text.css');
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-family: PingFang SC,"Harmony",Helvetica Neue,Microsoft YaHei,sans-serif !important;
|
||||
font-family:
|
||||
PingFang SC,
|
||||
'Harmony',
|
||||
Helvetica Neue,
|
||||
Microsoft YaHei,
|
||||
sans-serif !important;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.hm-medium {
|
||||
font-family: PingFang SC,"Harmony",Helvetica Neue,Microsoft YaHei,sans-serif !important;
|
||||
font-family:
|
||||
PingFang SC,
|
||||
'Harmony',
|
||||
Helvetica Neue,
|
||||
Microsoft YaHei,
|
||||
sans-serif !important;
|
||||
@apply font-bold;
|
||||
}
|
||||
.font-ubuntu {
|
||||
@@ -24,6 +35,20 @@ body {
|
||||
.font-noto-serif {
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
}
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
.hide-scrollbar::-webkit-scrollbar-thumb {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.hide-scrollbar::-webkit-scrollbar-track {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -51,7 +76,8 @@ body {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.monaco-editor, .monaco-editor-background {
|
||||
.monaco-editor,
|
||||
.monaco-editor-background {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
@@ -77,7 +103,12 @@ body {
|
||||
}
|
||||
|
||||
.context-view.monaco-menu-container * {
|
||||
font-family: PingFang SC,"Harmony",Helvetica Neue,Microsoft YaHei,sans-serif !important;
|
||||
font-family:
|
||||
PingFang SC,
|
||||
'Harmony',
|
||||
Helvetica Neue,
|
||||
Microsoft YaHei,
|
||||
sans-serif !important;
|
||||
}
|
||||
|
||||
.ql-hidden {
|
||||
@@ -86,15 +117,3 @@ body {
|
||||
.ql-editor img {
|
||||
@apply inline-block;
|
||||
}
|
||||
/* input.ql-image {
|
||||
@apply hidden;
|
||||
}
|
||||
.ql-image svg {
|
||||
fill: none;
|
||||
}
|
||||
.ql-fill {
|
||||
fill: currentColor;
|
||||
}
|
||||
.ql-stroke {
|
||||
stroke: currentColor;
|
||||
} */
|
||||
|
34
napcat.webui/src/styles/text.css
Normal file
34
napcat.webui/src/styles/text.css
Normal file
@@ -0,0 +1,34 @@
|
||||
@layer base {
|
||||
.shiny-text {
|
||||
@apply text-pink-400 text-opacity-60;
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
animation: shine 5s linear infinite;
|
||||
}
|
||||
.shiny-text {
|
||||
background-image: linear-gradient(
|
||||
120deg,
|
||||
rgba(255, 50, 50, 0) 40%,
|
||||
rgba(255, 76, 76, 0.8) 50%,
|
||||
rgba(255, 50, 50, 0) 60%
|
||||
);
|
||||
}
|
||||
.dark .shiny-text {
|
||||
background-image: linear-gradient(
|
||||
120deg,
|
||||
rgba(255, 255, 255, 0) 40%,
|
||||
rgba(206, 21, 21, 0.8) 50%,
|
||||
rgba(255, 255, 255, 0) 60%
|
||||
);
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% {
|
||||
background-position: 100%;
|
||||
}
|
||||
100% {
|
||||
background-position: -100%;
|
||||
}
|
||||
}
|
||||
}
|
2
napcat.webui/src/types/user.d.ts
vendored
2
napcat.webui/src/types/user.d.ts
vendored
@@ -164,7 +164,7 @@ interface CommonExt {
|
||||
address: string
|
||||
regTime: number
|
||||
interest: string
|
||||
labels: unknown[]
|
||||
labels: string[]
|
||||
qqLevel: QQLevel
|
||||
}
|
||||
|
||||
|
@@ -29,6 +29,11 @@ export default defineConfig(({ mode }) => {
|
||||
base: '/webui/',
|
||||
server: {
|
||||
proxy: {
|
||||
'/api/ws/terminal': {
|
||||
target: backendDebugUrl,
|
||||
ws: true,
|
||||
changeOrigin: true
|
||||
},
|
||||
'/api': backendDebugUrl
|
||||
}
|
||||
},
|
||||
|
21
package.json
21
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.4.12",
|
||||
"version": "4.4.16",
|
||||
"scripts": {
|
||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||
@@ -17,18 +17,16 @@
|
||||
"dev:depend": "npm i && cd napcat.webui && npm i"
|
||||
},
|
||||
"devDependencies": {
|
||||
"json5": "^2.2.3",
|
||||
"esbuild": "0.24.0",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@eslint/compat": "^1.2.2",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@log4js-node/log4js-api": "^1.0.2",
|
||||
"@napneko/nap-proto-core": "^0.0.4",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@sinclair/typebox": "^0.34.9",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.1",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
@@ -39,6 +37,7 @@
|
||||
"async-mutex": "^0.5.0",
|
||||
"commander": "^13.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"esbuild": "0.24.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
@@ -46,19 +45,21 @@
|
||||
"file-type": "^20.0.0",
|
||||
"globals": "^15.12.0",
|
||||
"image-size": "^1.1.1",
|
||||
"json5": "^2.2.3",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "^8.13.0",
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-tsconfig-paths": "^5.1.0",
|
||||
"winston": "^3.17.0"
|
||||
"winston": "^3.17.0",
|
||||
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
|
||||
"@ffmpeg.wasm/main": "^0.13.1",
|
||||
"express-rate-limit": "^7.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffmpeg.wasm/core-mt": "^0.13.2",
|
||||
"@ffmpeg.wasm/main": "^0.13.1",
|
||||
"express": "^5.0.0",
|
||||
"piscina": "^4.7.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"@ffmpeg.wasm/core-mt": "^0.13.2",
|
||||
"express": "^5.0.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { encode } from "silk-wasm";
|
||||
import { encode } from 'silk-wasm';
|
||||
|
||||
export interface EncodeArgs {
|
||||
input: ArrayBufferView | ArrayBuffer
|
||||
|
@@ -4,8 +4,8 @@ import path from 'node:path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { EncodeArgs } from "@/common/audio-worker";
|
||||
import { FFmpegService } from "@/common/ffmpeg";
|
||||
import { EncodeArgs } from '@/common/audio-worker';
|
||||
import { FFmpegService } from '@/common/ffmpeg';
|
||||
|
||||
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
|
||||
|
||||
@@ -19,7 +19,7 @@ const piscina = new Piscina<EncodeArgs, EncodeResult>({
|
||||
|
||||
async function guessDuration(pttPath: string, logger: LogWrapper) {
|
||||
const pttFileInfo = await fsPromise.stat(pttPath);
|
||||
const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
|
||||
const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
|
||||
logger.log('通过文件大小估算语音的时长:', duration);
|
||||
return duration;
|
||||
}
|
||||
@@ -27,12 +27,11 @@ async function guessDuration(pttPath: string, logger: LogWrapper) {
|
||||
async function handleWavFile(
|
||||
file: Buffer,
|
||||
filePath: string,
|
||||
pcmPath: string,
|
||||
logger: LogWrapper
|
||||
pcmPath: string
|
||||
): Promise<{ input: Buffer; sampleRate: number }> {
|
||||
const { fmt } = getWavFileInfo(file);
|
||||
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
|
||||
return { input: await FFmpegService.convert(filePath, pcmPath, logger), sampleRate: 24000 };
|
||||
return { input: await FFmpegService.convert(filePath, pcmPath), sampleRate: 24000 };
|
||||
}
|
||||
return { input: file, sampleRate: fmt.sampleRate };
|
||||
}
|
||||
@@ -45,9 +44,10 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
|
||||
logger.log(`语音文件${filePath}需要转换成silk`);
|
||||
const pcmPath = `${pttPath}.pcm`;
|
||||
const { input, sampleRate } = isWav(file)
|
||||
? (await handleWavFile(file, filePath, pcmPath, logger))
|
||||
: { input: await FFmpegService.convert(filePath, pcmPath, logger), sampleRate: 24000 };
|
||||
? await handleWavFile(file, filePath, pcmPath)
|
||||
: { input: await FFmpegService.convert(filePath, pcmPath), sampleRate: 24000 };
|
||||
const silk = await piscina.run({ input: input, sampleRate: sampleRate });
|
||||
fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e));
|
||||
await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
|
||||
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
|
||||
return {
|
||||
@@ -59,8 +59,8 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
|
||||
let duration = 0;
|
||||
try {
|
||||
duration = getDuration(file) / 1000;
|
||||
} catch (e: any) {
|
||||
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack);
|
||||
} catch (e: unknown) {
|
||||
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, (e as Error).stack);
|
||||
duration = await guessDuration(filePath, logger);
|
||||
}
|
||||
return {
|
||||
@@ -69,8 +69,8 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
|
||||
duration,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.logError('convert silk failed', error.stack);
|
||||
} catch (error: unknown) {
|
||||
logger.logError('convert silk failed', (error as Error).stack);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type TaskExecutor<T> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void, onCancel: (callback: () => void) => void) => void | Promise<void>;
|
||||
|
||||
export class CancelableTask<T> {
|
||||
@@ -7,30 +8,34 @@ export class CancelableTask<T> {
|
||||
private cancelListeners: Array<() => void> = [];
|
||||
|
||||
constructor(executor: TaskExecutor<T>) {
|
||||
this.promise = new Promise<T>(async (resolve, reject) => {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
const onCancel = (callback: () => void) => {
|
||||
this.cancelCallback = callback;
|
||||
};
|
||||
|
||||
try {
|
||||
await executor(
|
||||
(value) => {
|
||||
if (!this.isCanceled) {
|
||||
resolve(value);
|
||||
}
|
||||
},
|
||||
(reason) => {
|
||||
if (!this.isCanceled) {
|
||||
reject(reason);
|
||||
}
|
||||
},
|
||||
onCancel
|
||||
);
|
||||
} catch (error) {
|
||||
if (!this.isCanceled) {
|
||||
reject(error);
|
||||
const execute = async () => {
|
||||
try {
|
||||
await executor(
|
||||
(value) => {
|
||||
if (!this.isCanceled) {
|
||||
resolve(value);
|
||||
}
|
||||
},
|
||||
(reason) => {
|
||||
if (!this.isCanceled) {
|
||||
reject(reason);
|
||||
}
|
||||
},
|
||||
onCancel
|
||||
);
|
||||
} catch (error) {
|
||||
if (!this.isCanceled) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
execute();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,41 +77,4 @@ export class CancelableTask<T> {
|
||||
next: () => this.promise.then(value => ({ value, done: true })),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function demoAwait() {
|
||||
const executor: TaskExecutor<number> = async (resolve, reject, onCancel) => {
|
||||
let count = 0;
|
||||
const intervalId = setInterval(() => {
|
||||
count++;
|
||||
console.log(`Task is running... Count: ${count}`);
|
||||
if (count === 5) {
|
||||
clearInterval(intervalId);
|
||||
resolve(count);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onCancel(() => {
|
||||
clearInterval(intervalId);
|
||||
console.log('Task has been canceled.');
|
||||
reject(new Error('Task was canceled'));
|
||||
});
|
||||
};
|
||||
|
||||
const task = new CancelableTask(executor);
|
||||
|
||||
task.onCancel(() => {
|
||||
console.log('Cancel listener triggered.');
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
task.cancel(); // 取消任务
|
||||
}, 6000);
|
||||
|
||||
try {
|
||||
const result = await task;
|
||||
console.log(`Task completed with result: ${result}`);
|
||||
} catch (error) {
|
||||
console.error('Task failed:', error);
|
||||
}
|
||||
}
|
@@ -39,9 +39,9 @@ export abstract class ConfigBase<T> {
|
||||
if (!fs.existsSync(configPath) && copy_default) {
|
||||
try {
|
||||
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
|
||||
this.core.context.logger.log(`[Core] [Config] 配置文件创建成功!\n`);
|
||||
} catch (e: any) {
|
||||
this.core.context.logger.logError(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
|
||||
this.core.context.logger.log('[Core] [Config] 配置文件创建成功!\n');
|
||||
} catch (e: unknown) {
|
||||
this.core.context.logger.logError('[Core] [Config] 创建配置文件时发生错误:', (e as Error).message);
|
||||
}
|
||||
} else if (!fs.existsSync(configPath) && !copy_default) {
|
||||
fs.writeFileSync(configPath, '{}');
|
||||
@@ -50,11 +50,11 @@ export abstract class ConfigBase<T> {
|
||||
this.configData = json5.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
this.core.context.logger.logDebug(`[Core] [Config] 配置文件${configPath}加载`, this.configData);
|
||||
return this.configData;
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof SyntaxError) {
|
||||
this.core.context.logger.logError(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
|
||||
this.core.context.logger.logError('[Core] [Config] 配置文件格式错误,请检查配置文件:', e.message);
|
||||
} else {
|
||||
this.core.context.logger.logError(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
|
||||
this.core.context.logger.logError('[Core] [Config] 读取配置文件时发生错误:', (e as Error).message);
|
||||
}
|
||||
return {} as T;
|
||||
}
|
||||
@@ -67,8 +67,8 @@ export abstract class ConfigBase<T> {
|
||||
const configPath = this.getConfigPath(selfInfo.uin);
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(newConfigData, this.getKeys(), 2));
|
||||
} catch (e: any) {
|
||||
this.core.context.logger.logError(`保存配置文件 ${configPath} 时发生错误:`, e.message);
|
||||
} catch (e: unknown) {
|
||||
this.core.context.logger.logError(`保存配置文件 ${configPath} 时发生错误:`, (e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,22 +0,0 @@
|
||||
// decoratorAsyncMethod(this,function,wrapper)
|
||||
async function decoratorMethod<T, R>(
|
||||
target: T,
|
||||
method: () => Promise<R>,
|
||||
wrapper: (result: R) => Promise<any>,
|
||||
executeImmediately: boolean = true
|
||||
): Promise<any> {
|
||||
const execute = async () => {
|
||||
try {
|
||||
const result = await method.call(target);
|
||||
return wrapper(result);
|
||||
} catch (error) {
|
||||
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
};
|
||||
|
||||
if (executeImmediately) {
|
||||
return execute();
|
||||
} else {
|
||||
return execute;
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { NodeIQQNTWrapperSession } from '@/core/wrapper';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { ListenerNamingMapping, ServiceNamingMapping } from '@/core';
|
||||
@@ -60,17 +61,22 @@ export class NTEventWrapper {
|
||||
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> };
|
||||
};
|
||||
if (eventNameArr.length > 1) {
|
||||
const serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', '');
|
||||
const serviceName = 'get' + (eventNameArr[0]?.replace('NodeIKernel', '') ?? '');
|
||||
const eventName = eventNameArr[1];
|
||||
const services = (this.WrapperSession as unknown as eventType)[serviceName]();
|
||||
const services = (this.WrapperSession as unknown as eventType)[serviceName]?.();
|
||||
if (!services || !eventName) {
|
||||
return undefined;
|
||||
}
|
||||
let event = services[eventName];
|
||||
|
||||
//重新绑定this
|
||||
event = event.bind(services);
|
||||
event = event?.bind(services);
|
||||
if (event) {
|
||||
return event as T;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
createListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
|
||||
@@ -126,8 +132,8 @@ export class NTEventWrapper {
|
||||
) {
|
||||
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
|
||||
const ListenerNameList = listenerAndMethod.split('/');
|
||||
const ListenerMainName = ListenerNameList[0];
|
||||
const ListenerSubName = ListenerNameList[1];
|
||||
const ListenerMainName = ListenerNameList[0] ?? '';
|
||||
const ListenerSubName = ListenerNameList[1] ?? '';
|
||||
const id = randomUUID();
|
||||
let complete = 0;
|
||||
let retData: Parameters<ListenerType> | undefined = undefined;
|
||||
@@ -205,8 +211,8 @@ export class NTEventWrapper {
|
||||
}
|
||||
|
||||
const ListenerNameList = listenerAndMethod.split('/');
|
||||
const ListenerMainName = ListenerNameList[0];
|
||||
const ListenerSubName = ListenerNameList[1];
|
||||
const ListenerMainName = ListenerNameList[0]??'';
|
||||
const ListenerSubName = ListenerNameList[1]??'';
|
||||
|
||||
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(
|
||||
(resolve, reject) => {
|
||||
|
@@ -1,18 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { FFmpeg } from '@ffmpeg.wasm/main';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { readFileSync, statSync, writeFileSync } from 'fs';
|
||||
import type { LogWrapper } from './log';
|
||||
import type { VideoInfo } from './video';
|
||||
import { fileTypeFromFile } from 'file-type';
|
||||
import imageSize from 'image-size';
|
||||
class FFmpegService {
|
||||
class FFmpegService {
|
||||
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
|
||||
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
|
||||
const videoFileName = `${randomUUID()}.mp4`;
|
||||
const outputFileName = `${randomUUID()}.jpg`;
|
||||
try {
|
||||
ffmpegInstance.fs.writeFile(videoFileName, readFileSync(videoPath));
|
||||
let code = await ffmpegInstance.run('-i', videoFileName, '-ss', '00:00:01.000', '-vframes', '1', outputFileName);
|
||||
const code = await ffmpegInstance.run('-i', videoFileName, '-ss', '00:00:01.000', '-vframes', '1', outputFileName);
|
||||
if (code !== 0) {
|
||||
throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ import imageSize from 'image-size';
|
||||
const params = format === 'amr'
|
||||
? ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, '-ar', '8000', '-b:a', '12.2k', outputFileName]
|
||||
: ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, outputFileName];
|
||||
let code = await ffmpegInstance.run(...params);
|
||||
const code = await ffmpegInstance.run(...params);
|
||||
if (code !== 0) {
|
||||
throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
|
||||
}
|
||||
@@ -67,14 +67,14 @@ import imageSize from 'image-size';
|
||||
}
|
||||
}
|
||||
|
||||
public static async convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
|
||||
public static async convert(filePath: string, pcmPath: string): Promise<Buffer> {
|
||||
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
|
||||
const inputFileName = `${randomUUID()}.input`;
|
||||
const outputFileName = `${randomUUID()}.pcm`;
|
||||
try {
|
||||
ffmpegInstance.fs.writeFile(inputFileName, readFileSync(filePath));
|
||||
const params = ['-y', '-i', inputFileName, '-ar', '24000', '-ac', '1', '-f', 's16le', outputFileName];
|
||||
let code = await ffmpegInstance.run(...params);
|
||||
const code = await ffmpegInstance.run(...params);
|
||||
if (code !== 0) {
|
||||
throw new Error('FFmpeg process exited with code ' + code);
|
||||
}
|
||||
@@ -87,36 +87,36 @@ import imageSize from 'image-size';
|
||||
try {
|
||||
ffmpegInstance.fs.unlink(outputFileName);
|
||||
} catch (unlinkError) {
|
||||
logger.log('Error unlinking output file:', unlinkError);
|
||||
console.error('Error unlinking output file:', unlinkError);
|
||||
}
|
||||
try {
|
||||
ffmpegInstance.fs.unlink(inputFileName);
|
||||
} catch (unlinkError) {
|
||||
logger.log('Error unlinking input file:', unlinkError);
|
||||
console.error('Error unlinking output file:', unlinkError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
|
||||
await FFmpegService.extractThumbnail(videoPath, thumbnailPath);
|
||||
let fileType = (await fileTypeFromFile(videoPath))?.ext ?? 'mp4';
|
||||
const fileType = (await fileTypeFromFile(videoPath))?.ext ?? 'mp4';
|
||||
const inputFileName = `${randomUUID()}.${fileType}`;
|
||||
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
|
||||
ffmpegInstance.fs.writeFile(inputFileName, readFileSync(videoPath));
|
||||
ffmpegInstance.setLogging(true);
|
||||
let duration = 60;
|
||||
ffmpegInstance.setLogger((level, ...msg) => {
|
||||
ffmpegInstance.setLogger((_level, ...msg) => {
|
||||
const message = msg.join(' ');
|
||||
const durationMatch = message.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
|
||||
if (durationMatch) {
|
||||
const hours = parseInt(durationMatch[1], 10);
|
||||
const minutes = parseInt(durationMatch[2], 10);
|
||||
const seconds = parseFloat(durationMatch[3]);
|
||||
const hours = parseInt(durationMatch[1] ?? '0', 10);
|
||||
const minutes = parseInt(durationMatch[2] ?? '0', 10);
|
||||
const seconds = parseFloat(durationMatch[3] ?? '0');
|
||||
duration = hours * 3600 + minutes * 60 + seconds;
|
||||
}
|
||||
});
|
||||
await ffmpegInstance.run('-i', inputFileName);
|
||||
let image = imageSize(thumbnailPath);
|
||||
const image = imageSize(thumbnailPath);
|
||||
ffmpegInstance.fs.unlink(inputFileName);
|
||||
const fileSize = statSync(videoPath).size;
|
||||
return {
|
||||
@@ -126,7 +126,7 @@ import imageSize from 'image-size';
|
||||
format: fileType,
|
||||
size: fileSize,
|
||||
filePath: videoPath
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
type FFmpegMethod = 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
|
||||
@@ -137,15 +137,15 @@ interface FFmpegTask {
|
||||
}
|
||||
export default async function handleFFmpegTask({ method, args }: FFmpegTask): Promise<any> {
|
||||
switch (method) {
|
||||
case 'extractThumbnail':
|
||||
return await FFmpegService.extractThumbnail(...args as [string, string]);
|
||||
case 'convertFile':
|
||||
return await FFmpegService.convertFile(...args as [string, string, string]);
|
||||
case 'convert':
|
||||
return await FFmpegService.convert(...args as [string, string, LogWrapper]);
|
||||
case 'getVideoInfo':
|
||||
return await FFmpegService.getVideoInfo(...args as [string, string]);
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`);
|
||||
case 'extractThumbnail':
|
||||
return await FFmpegService.extractThumbnail(...args as [string, string]);
|
||||
case 'convertFile':
|
||||
return await FFmpegService.convertFile(...args as [string, string, string]);
|
||||
case 'convert':
|
||||
return await FFmpegService.convert(...args as [string, string]);
|
||||
case 'getVideoInfo':
|
||||
return await FFmpegService.getVideoInfo(...args as [string, string]);
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`);
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import Piscina from "piscina";
|
||||
import { VideoInfo } from "./video";
|
||||
import type { LogWrapper } from "./log";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import Piscina from 'piscina';
|
||||
import { VideoInfo } from './video';
|
||||
|
||||
type EncodeArgs = {
|
||||
method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
|
||||
@@ -30,11 +30,11 @@ export class FFmpegService {
|
||||
await piscina.destroy();
|
||||
}
|
||||
|
||||
public static async convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
|
||||
public static async convert(filePath: string, pcmPath: string): Promise<Buffer> {
|
||||
const piscina = new Piscina<EncodeArgs, EncodeResult>({
|
||||
filename: await getWorkerPath(),
|
||||
});
|
||||
const result = await piscina.run({ method: 'convert', args: [filePath, pcmPath, logger] });
|
||||
const result = await piscina.run({ method: 'convert', args: [filePath, pcmPath] });
|
||||
await piscina.destroy();
|
||||
return result;
|
||||
}
|
||||
@@ -47,4 +47,4 @@ export class FFmpegService {
|
||||
await piscina.destroy();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -83,7 +83,7 @@ class FileUUIDManager {
|
||||
this.cache = new TimeBasedCache<string, FileUUIDData>(5000, ttl);
|
||||
}
|
||||
|
||||
public encode(data: FileUUIDData, endString: string = "", customUUID?: string): string {
|
||||
public encode(data: FileUUIDData, endString: string = '', customUUID?: string): string {
|
||||
const uuid = customUUID ? customUUID : randomUUID().replace(/-/g, '') + endString;
|
||||
this.cache.put(uuid, data);
|
||||
return uuid;
|
||||
@@ -101,7 +101,7 @@ export class FileNapCatOneBotUUIDWrap {
|
||||
this.manager = new FileUUIDManager(ttl);
|
||||
}
|
||||
|
||||
public encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = "", customUUID?: string): string {
|
||||
public encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = '', endString: string = '', customUUID?: string): string {
|
||||
return this.manager.encode({ peer, modelId, fileId, fileUUID }, endString, customUUID);
|
||||
}
|
||||
|
||||
@@ -109,8 +109,8 @@ export class FileNapCatOneBotUUIDWrap {
|
||||
return this.manager.decode(uuid);
|
||||
}
|
||||
|
||||
public encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", customUUID?: string): string {
|
||||
return this.manager.encode({ peer, msgId, elementId, fileUUID }, "", customUUID);
|
||||
public encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = '', customUUID?: string): string {
|
||||
return this.manager.encode({ peer, msgId, elementId, fileUUID }, '', customUUID);
|
||||
}
|
||||
|
||||
public decode(uuid: string): FileUUIDData | undefined {
|
||||
|
@@ -58,8 +58,8 @@ function timeoutPromise(timeout: number, errorMsg: string): Promise<void> {
|
||||
async function checkFile(path: string): Promise<void> {
|
||||
try {
|
||||
await stat(path);
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
} catch (error: unknown) {
|
||||
if ((error as Error & { code: string }).code === 'ENOENT') {
|
||||
// 如果文件不存在,则抛出一个错误
|
||||
throw new Error(`文件不存在: ${path}`);
|
||||
} else {
|
||||
@@ -169,6 +169,7 @@ export async function checkUriType(Uri: string) {
|
||||
const data = uri.split(',')[1];
|
||||
if (data) return { Uri: data, Type: FileUriType.Base64 };
|
||||
}
|
||||
return;
|
||||
}, Uri);
|
||||
if (OtherFileRet) return OtherFileRet;
|
||||
|
||||
@@ -190,7 +191,7 @@ export async function uriToLocalFile(dir: string, uri: string, filename: string
|
||||
}
|
||||
|
||||
case FileUriType.Remote: {
|
||||
const buffer = await httpDownload({ url: HandledUri, headers: headers });
|
||||
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {} });
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as crypto from "node:crypto";
|
||||
import { PacketMsg } from "@/core/packet/message/message";
|
||||
import * as crypto from 'node:crypto';
|
||||
import { PacketMsg } from '@/core/packet/message/message';
|
||||
|
||||
interface ForwardMsgJson {
|
||||
app: string
|
||||
@@ -50,15 +50,15 @@ interface ForwardAdaptMsgElement {
|
||||
}
|
||||
|
||||
export class ForwardMsgBuilder {
|
||||
private static build(resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
|
||||
private static build(resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
|
||||
const id = crypto.randomUUID();
|
||||
const isGroupMsg = msg.some(m => m.isGroupMsg);
|
||||
if (!source) {
|
||||
source = isGroupMsg ? "群聊的聊天记录" : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录';
|
||||
source = isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录';
|
||||
}
|
||||
if (!news) {
|
||||
news = msg.length === 0 ? [{
|
||||
text: "Nya~ This message is send from NapCat.Packet!",
|
||||
text: 'Nya~ This message is send from NapCat.Packet!',
|
||||
}] : msg.map(m => ({
|
||||
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
|
||||
}));
|
||||
@@ -67,15 +67,15 @@ export class ForwardMsgBuilder {
|
||||
summary = `查看${msg.length}条转发消息`;
|
||||
}
|
||||
if (!prompt) {
|
||||
prompt = "[聊天记录]";
|
||||
prompt = '[聊天记录]';
|
||||
}
|
||||
return {
|
||||
app: "com.tencent.multimsg",
|
||||
app: 'com.tencent.multimsg',
|
||||
config: {
|
||||
autosize: 1,
|
||||
forward: 1,
|
||||
round: 1,
|
||||
type: "normal",
|
||||
type: 'normal',
|
||||
width: 300
|
||||
},
|
||||
desc: prompt,
|
||||
@@ -93,8 +93,8 @@ export class ForwardMsgBuilder {
|
||||
}
|
||||
},
|
||||
prompt,
|
||||
ver: "0.0.0.5",
|
||||
view: "contact",
|
||||
ver: '0.0.0.5',
|
||||
view: 'contact',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -102,12 +102,12 @@ export class ForwardMsgBuilder {
|
||||
return this.build(resId, []);
|
||||
}
|
||||
|
||||
static fromPacketMsg(resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
|
||||
static fromPacketMsg(resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
|
||||
return this.build(resId, packetMsg.map(msg => ({
|
||||
senderName: msg.senderName,
|
||||
isGroupMsg: msg.groupId !== undefined,
|
||||
msg: msg.msg.map(m => ({
|
||||
preview: m.valid ? m.toPreview() : "[该消息类型暂不支持查看]",
|
||||
preview: m.valid ? m.toPreview() : '[该消息类型暂不支持查看]',
|
||||
}))
|
||||
})), source, news, summary, prompt);
|
||||
}
|
||||
|
@@ -1,14 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import path from 'node:path';
|
||||
import fs from 'fs';
|
||||
import os from 'node:os';
|
||||
import { QQLevel } from '@/core';
|
||||
import { QQVersionConfigType } from './types';
|
||||
|
||||
export async function solveProblem<T extends (...arg: any[]) => any>(func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
|
||||
return new Promise<ReturnType<T> | undefined>((resolve) => {
|
||||
try {
|
||||
const result = func(...args);
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
@@ -166,14 +168,14 @@ export function calcQQLevel(level?: QQLevel) {
|
||||
}
|
||||
|
||||
export function stringifyWithBigInt(obj: any) {
|
||||
return JSON.stringify(obj, (key, value) =>
|
||||
return JSON.stringify(obj, (_key, value) =>
|
||||
typeof value === 'bigint' ? value.toString() : value
|
||||
);
|
||||
}
|
||||
|
||||
export function parseAppidFromMajor(nodeMajor: string): string | undefined {
|
||||
const hexSequence = "A4 09 00 00 00 35";
|
||||
const sequenceBytes = Buffer.from(hexSequence.replace(/ /g, ""), "hex");
|
||||
const hexSequence = 'A4 09 00 00 00 35';
|
||||
const sequenceBytes = Buffer.from(hexSequence.replace(/ /g, ''), 'hex');
|
||||
const filePath = path.resolve(nodeMajor);
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
|
||||
@@ -192,8 +194,8 @@ export function parseAppidFromMajor(nodeMajor: string): string | undefined {
|
||||
const content = fileContent.subarray(start, end);
|
||||
if (!content.every(byte => byte === 0x00)) {
|
||||
try {
|
||||
return content.toString("utf-8");
|
||||
} catch (error) {
|
||||
return content.toString('utf-8');
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import winston, { format, transports } from 'winston';
|
||||
import { truncateString } from '@/common/helper';
|
||||
import path from 'node:path';
|
||||
@@ -34,7 +35,7 @@ class Subscription {
|
||||
for (const history of Subscription.history) {
|
||||
try {
|
||||
listener(history);
|
||||
} catch (_) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
@@ -68,7 +69,7 @@ export class LogWrapper {
|
||||
format: format.combine(
|
||||
format.timestamp({ format: 'MM-DD HH:mm:ss' }),
|
||||
format.printf(({ timestamp, level, message, ...meta }) => {
|
||||
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
|
||||
const userInfo = meta['userInfo'] ? `${meta['userInfo']} | ` : '';
|
||||
return `${timestamp} [${level}] ${userInfo}${message}`;
|
||||
})
|
||||
),
|
||||
@@ -83,7 +84,7 @@ export class LogWrapper {
|
||||
format: format.combine(
|
||||
format.colorize(),
|
||||
format.printf(({ timestamp, level, message, ...meta }) => {
|
||||
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
|
||||
const userInfo = meta['userInfo'] ? `${meta['userInfo']} | ` : '';
|
||||
return `${timestamp} [${level}] ${userInfo}${message}`;
|
||||
})
|
||||
),
|
||||
@@ -303,7 +304,7 @@ function textElementToText(textElement: any): string {
|
||||
const originalContentLines = textElement.content.split('\n');
|
||||
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
|
||||
} else if (textElement.atType === NTMsgAtType.ATTYPEALL) {
|
||||
return `@全体成员`;
|
||||
return '@全体成员';
|
||||
} else if (textElement.atType === NTMsgAtType.ATTYPEONE) {
|
||||
return `${textElement.content} (${textElement.atUid})`;
|
||||
}
|
||||
|
@@ -68,7 +68,10 @@ export class LimitedHashTable<K, V> {
|
||||
const listSize = Math.min(size, keyList.length);
|
||||
for (let i = 0; i < listSize; i++) {
|
||||
const key = keyList[listSize - i];
|
||||
result.push({ key, value: this.keyToValue.get(key)! });
|
||||
if (key !== undefined) {
|
||||
result.push({ key, value: this.keyToValue.get(key)! });
|
||||
}
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -96,8 +99,10 @@ class MessageUniqueWrapper {
|
||||
createUniqueMsgId(peer: Peer, msgId: string) {
|
||||
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`;
|
||||
const hash = crypto.createHash('md5').update(key).digest();
|
||||
//设置第一个bit为0 保证shortId为正数
|
||||
hash[0] &= 0x7f;
|
||||
if (hash[0]) {
|
||||
//设置第一个bit为0 保证shortId为正数
|
||||
hash[0] &= 0x7f;
|
||||
}
|
||||
const shortId = hash.readInt32BE(0);
|
||||
//减少性能损耗
|
||||
this.msgIdMap.set(msgId, shortId);
|
||||
@@ -110,11 +115,11 @@ class MessageUniqueWrapper {
|
||||
if (data) {
|
||||
const [msgId, chatTypeStr, peerUid] = data.split('|');
|
||||
const peer: Peer = {
|
||||
chatType: parseInt(chatTypeStr),
|
||||
peerUid,
|
||||
chatType: parseInt(chatTypeStr ?? '0'),
|
||||
peerUid: peerUid ?? '',
|
||||
guildId: '',
|
||||
};
|
||||
return { MsgId: msgId, Peer: peer };
|
||||
return { MsgId: msgId ?? '0', Peer: peer };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { LogWrapper } from '@/common/log';
|
||||
|
||||
export function proxyHandlerOf(logger: LogWrapper) {
|
||||
@@ -5,6 +6,7 @@ export function proxyHandlerOf(logger: LogWrapper) {
|
||||
get(target: any, prop: any, receiver: any) {
|
||||
if (typeof target[prop] === 'undefined') {
|
||||
// 如果方法不存在,返回一个函数,这个函数调用existentMethod
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
return (..._args: unknown[]) => {
|
||||
logger.logDebug(`${target.constructor.name} has no method ${prop}`);
|
||||
};
|
||||
|
@@ -4,6 +4,7 @@ import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfig
|
||||
import AppidTable from '@/core/external/appid.json';
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { getMajorPath } from '@/core';
|
||||
import { QQAppidTableType, QQPackageInfoType, QQVersionConfigType } from './types';
|
||||
|
||||
export class QQBasicInfoWrapper {
|
||||
QQMainPath: string | undefined;
|
||||
@@ -86,14 +87,14 @@ export class QQBasicInfoWrapper {
|
||||
try {
|
||||
const majorAppid = this.getAppidV2ByMajor(fullVersion);
|
||||
if (majorAppid) {
|
||||
this.context.logger.log(`[QQ版本兼容性检测] 当前版本Appid未内置 通过Major获取 为了更好的性能请尝试更新NapCat`);
|
||||
this.context.logger.log('[QQ版本兼容性检测] 当前版本Appid未内置 通过Major获取 为了更好的性能请尝试更新NapCat');
|
||||
return { appid: majorAppid, qua: this.getQUAFallback() };
|
||||
}
|
||||
} catch (error) {
|
||||
this.context.logger.log(`[QQ版本兼容性检测] 通过Major 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
||||
} catch {
|
||||
this.context.logger.log('[QQ版本兼容性检测] 通过Major 获取Appid异常 请检测NapCat/QQNT是否正常');
|
||||
}
|
||||
// 最终兜底为老版本
|
||||
this.context.logger.log(`[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
||||
this.context.logger.log('[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常');
|
||||
this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,);
|
||||
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import https from 'node:https';
|
||||
import http from 'node:http';
|
||||
|
||||
@@ -41,11 +42,13 @@ export class RequestUtil {
|
||||
|
||||
private static extractCookies(setCookieHeaders: string[], cookies: { [key: string]: string }) {
|
||||
setCookieHeaders.forEach((cookie) => {
|
||||
const parts = cookie.split(';')[0].split('=');
|
||||
const key = parts[0];
|
||||
const value = parts[1];
|
||||
if (key && value && key.length > 0 && value.length > 0) {
|
||||
cookies[key] = value;
|
||||
const parts = cookie.split(';')[0]?.split('=');
|
||||
if (parts) {
|
||||
const key = parts[0];
|
||||
const value = parts[1];
|
||||
if (key && value && key.length > 0 && value.length > 0) {
|
||||
cookies[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
190
src/common/store.ts
Normal file
190
src/common/store.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
export type StoreValueType = string | number | boolean | object | null;
|
||||
|
||||
export type StoreValue<T extends StoreValueType = StoreValueType> = {
|
||||
value: T;
|
||||
expiresAt?: number;
|
||||
};
|
||||
|
||||
class Store {
|
||||
// 使用Map存储键值对
|
||||
private store: Map<string, StoreValue>;
|
||||
// 定时清理器
|
||||
private cleanerTimer: NodeJS.Timeout;
|
||||
// 用于分批次扫描的游标
|
||||
private scanCursor: number = 0;
|
||||
|
||||
/**
|
||||
* Store
|
||||
* @param cleanInterval 清理间隔
|
||||
* @param scanLimit 扫描限制(每次最多检查的键数)
|
||||
*/
|
||||
constructor(
|
||||
cleanInterval: number = 1000, // 默认1秒执行一次
|
||||
private scanLimit: number = 100 // 每次最多检查100个键
|
||||
) {
|
||||
this.store = new Map();
|
||||
this.cleanerTimer = setInterval(() => this.cleanupExpired(), cleanInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键值对
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param ttl 过期时间
|
||||
* @returns void
|
||||
* @example store.set('key', 'value', 60)
|
||||
*/
|
||||
set<T extends StoreValueType>(key: string, value: T, ttl?: number): void {
|
||||
if (ttl && ttl <= 0) {
|
||||
this.del(key);
|
||||
return;
|
||||
}
|
||||
const expiresAt = ttl ? Date.now() + ttl * 1000 : undefined;
|
||||
this.store.set(key, { value, expiresAt });
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期键
|
||||
*/
|
||||
private cleanupExpired(): void {
|
||||
const now = Date.now();
|
||||
const keys = Array.from(this.store.keys());
|
||||
let scanned = 0;
|
||||
|
||||
// 分批次扫描
|
||||
while (scanned < this.scanLimit && this.scanCursor < keys.length) {
|
||||
const key = keys[this.scanCursor++];
|
||||
const entry = this.store.get(key!)!;
|
||||
|
||||
if (entry.expiresAt && entry.expiresAt < now) {
|
||||
this.store.delete(key!);
|
||||
}
|
||||
|
||||
scanned++;
|
||||
}
|
||||
|
||||
// 重置游标(环形扫描)
|
||||
if (this.scanCursor >= keys.length) {
|
||||
this.scanCursor = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值
|
||||
* @param key 键
|
||||
* @returns T | null
|
||||
* @example store.get('key')
|
||||
*/
|
||||
get<T extends StoreValueType>(key: string): T | null {
|
||||
this.checkKeyExpiry(key); // 每次访问都检查
|
||||
const entry = this.store.get(key);
|
||||
return entry ? (entry.value as T) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否过期
|
||||
* @param key 键
|
||||
*/
|
||||
private checkKeyExpiry(key: string): void {
|
||||
const entry = this.store.get(key);
|
||||
if (entry?.expiresAt && entry.expiresAt < Date.now()) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
* @param keys 键
|
||||
* @returns number
|
||||
* @example store.exists('key1', 'key2')
|
||||
*/
|
||||
exists(...keys: string[]): number {
|
||||
return keys.filter((key) => {
|
||||
this.checkKeyExpiry(key);
|
||||
return this.store.has(key);
|
||||
}).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭存储器
|
||||
*/
|
||||
shutdown(): void {
|
||||
clearInterval(this.cleanerTimer);
|
||||
this.store.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键
|
||||
* @param keys 键
|
||||
* @returns number
|
||||
* @example store.del('key1', 'key2')
|
||||
*/
|
||||
del(...keys: string[]): number {
|
||||
return keys.reduce((count, key) => (this.store.delete(key) ? count + 1 : count), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键的过期时间
|
||||
* @param key 键
|
||||
* @param seconds 过期时间(秒)
|
||||
* @returns boolean
|
||||
* @example store.expire('key', 60)
|
||||
*/
|
||||
expire(key: string, seconds: number): boolean {
|
||||
const entry = this.store.get(key);
|
||||
if (!entry) return false;
|
||||
|
||||
entry.expiresAt = Date.now() + seconds * 1000;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键的过期时间
|
||||
* @param key 键
|
||||
* @returns number | null
|
||||
* @example store.ttl('key')
|
||||
*/
|
||||
ttl(key: string): number | null {
|
||||
const entry = this.store.get(key);
|
||||
if (!entry) return null;
|
||||
|
||||
if (!entry.expiresAt) return -1;
|
||||
const remaining = entry.expiresAt - Date.now();
|
||||
return remaining > 0 ? Math.floor(remaining / 1000) : -2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 键值数字递增
|
||||
* @param key 键
|
||||
* @returns number
|
||||
* @example store.incr('key')
|
||||
*/
|
||||
incr(key: string): number {
|
||||
const current = this.get<StoreValueType>(key);
|
||||
|
||||
if (current === null) {
|
||||
this.set(key, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
let numericValue: number;
|
||||
if (typeof current === 'number') {
|
||||
numericValue = current;
|
||||
} else if (typeof current === 'string') {
|
||||
if (!/^-?\d+$/.test(current)) {
|
||||
throw new Error('ERR value is not an integer');
|
||||
}
|
||||
numericValue = parseInt(current, 10);
|
||||
} else {
|
||||
throw new Error('ERR value is not an integer');
|
||||
}
|
||||
|
||||
const newValue = numericValue + 1;
|
||||
this.set(key, newValue);
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Store();
|
||||
|
||||
export default store;
|
@@ -6,7 +6,7 @@ let osName: string;
|
||||
|
||||
try {
|
||||
osName = os.hostname();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
osName = 'NapCat'; // + crypto.randomUUID().substring(0, 4);
|
||||
}
|
||||
|
||||
|
@@ -1,17 +1,17 @@
|
||||
//QQVersionType
|
||||
type QQPackageInfoType = {
|
||||
export type QQPackageInfoType = {
|
||||
version: string;
|
||||
buildVersion: string;
|
||||
platform: string;
|
||||
eleArch: string;
|
||||
}
|
||||
type QQVersionConfigType = {
|
||||
export type QQVersionConfigType = {
|
||||
baseVersion: string;
|
||||
curVersion: string;
|
||||
prevVersion: string;
|
||||
onErrorVersions: Array<any>;
|
||||
onErrorVersions: Array<unknown>;
|
||||
buildId: string;
|
||||
}
|
||||
type QQAppidTableType = {
|
||||
export type QQAppidTableType = {
|
||||
[key: string]: { appid: string, qua: string };
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.4.12';
|
||||
export const napCatVersion = '4.4.16';
|
||||
|
@@ -1,15 +1,16 @@
|
||||
import { MsfChangeReasonType, MsfStatusType } from "@/core/types/adapter";
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { MsfChangeReasonType, MsfStatusType } from '@/core/types/adapter';
|
||||
|
||||
export class NodeIDependsAdapter {
|
||||
onMSFStatusChange(statusType: MsfStatusType, changeReasonType: MsfChangeReasonType) {
|
||||
onMSFStatusChange(_statusType: MsfStatusType, _changeReasonType: MsfChangeReasonType) {
|
||||
|
||||
}
|
||||
|
||||
onMSFSsoError(args: unknown) {
|
||||
onMSFSsoError(_args: unknown) {
|
||||
|
||||
}
|
||||
|
||||
getGroupCode(args: unknown) {
|
||||
getGroupCode(_args: unknown) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
export class NodeIDispatcherAdapter {
|
||||
dispatchRequest(arg: unknown) {
|
||||
dispatchRequest(_arg: unknown) {
|
||||
}
|
||||
|
||||
dispatchCall(arg: unknown) {
|
||||
dispatchCall(_arg: unknown) {
|
||||
}
|
||||
|
||||
dispatchCallWithJson(arg: unknown) {
|
||||
dispatchCallWithJson(_arg: unknown) {
|
||||
}
|
||||
}
|
||||
|
@@ -1,25 +1,26 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
export class NodeIGlobalAdapter {
|
||||
onLog(...args: unknown[]) {
|
||||
onLog(..._args: unknown[]) {
|
||||
}
|
||||
|
||||
onGetSrvCalTime(...args: unknown[]) {
|
||||
onGetSrvCalTime(..._args: unknown[]) {
|
||||
}
|
||||
|
||||
onShowErrUITips(...args: unknown[]) {
|
||||
onShowErrUITips(..._args: unknown[]) {
|
||||
}
|
||||
|
||||
fixPicImgType(...args: unknown[]) {
|
||||
fixPicImgType(..._args: unknown[]) {
|
||||
}
|
||||
|
||||
getAppSetting(...args: unknown[]) {
|
||||
getAppSetting(..._args: unknown[]) {
|
||||
}
|
||||
|
||||
onInstallFinished(...args: unknown[]) {
|
||||
onInstallFinished(..._args: unknown[]) {
|
||||
}
|
||||
|
||||
onUpdateGeneralFlag(...args: unknown[]) {
|
||||
onUpdateGeneralFlag(..._args: unknown[]) {
|
||||
}
|
||||
|
||||
onGetOfflineMsg(...args: unknown[]) {
|
||||
onGetOfflineMsg(..._args: unknown[]) {
|
||||
}
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ import { encodeSilk } from '@/common/audio';
|
||||
import { SendMessageContext } from '@/onebot/api';
|
||||
import { getFileTypeForSendType } from '../helper/msg';
|
||||
import { FFmpegService } from '@/common/ffmpeg';
|
||||
import { rkeyDataType } from '../types/file';
|
||||
|
||||
export class NTQQFileApi {
|
||||
context: InstanceContext;
|
||||
@@ -40,7 +41,7 @@ export class NTQQFileApi {
|
||||
this.rkeyManager = new RkeyManager([
|
||||
'https://rkey.napneko.icu/rkeys'
|
||||
],
|
||||
this.context.logger
|
||||
this.context.logger
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ export class NTQQFileApi {
|
||||
|
||||
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
|
||||
const fileMd5 = await calculateFileMD5(filePath);
|
||||
const extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(e => '');
|
||||
const extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(() => '');
|
||||
const ext = extOrEmpty ? `.${extOrEmpty}` : '';
|
||||
let fileName = `${path.basename(filePath)}`;
|
||||
if (fileName.indexOf('.') === -1) {
|
||||
@@ -140,7 +141,7 @@ export class NTQQFileApi {
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendVideoElement(context: SendMessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
|
||||
async createValidSendVideoElement(context: SendMessageContext, filePath: string, fileName: string = '', _diyThumbPath: string = ''): Promise<SendVideoElement> {
|
||||
let videoInfo = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
@@ -170,13 +171,22 @@ export class NTQQFileApi {
|
||||
const thumbPath = pathLib.join(pathLib.dirname(thumbDir), `${md5}_0.png`);
|
||||
try {
|
||||
videoInfo = await FFmpegService.getVideoInfo(filePath, thumbPath);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
|
||||
}
|
||||
|
||||
if (_diyThumbPath) {
|
||||
try {
|
||||
await this.copyFile(_diyThumbPath, thumbPath);
|
||||
} catch (e) {
|
||||
this.context.logger.logError('复制自定义缩略图失败', e);
|
||||
}
|
||||
}
|
||||
context.deleteAfterSentFiles.push(thumbPath);
|
||||
|
||||
const thumbSize = (await fsPromises.stat(thumbPath)).size;
|
||||
const thumbMd5 = await calculateFileMD5(thumbPath);
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
context.deleteAfterSentFiles.push(thumbPath);
|
||||
|
||||
|
||||
const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith(`.${fileExt.toLocaleLowerCase()}`) ? (fileName || _fileName) : `${fileName || _fileName}.${fileExt}`;
|
||||
return {
|
||||
@@ -207,8 +217,7 @@ export class NTQQFileApi {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
if (converted) {
|
||||
fsPromises.unlink(silkPath).then().catch((e) => this.context.logger.logError('删除临时文件失败', e)
|
||||
);
|
||||
fsPromises.unlink(silkPath).then().catch((e) => this.context.logger.logError('删除临时文件失败', e));
|
||||
}
|
||||
return {
|
||||
elementType: ElementType.PTT,
|
||||
@@ -274,18 +283,18 @@ export class NTQQFileApi {
|
||||
element.elementType === ElementType.FILE
|
||||
) {
|
||||
switch (element.elementType) {
|
||||
case ElementType.PIC:
|
||||
element.picElement!.sourcePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
element.videoElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
element.pttElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
element.fileElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.PIC:
|
||||
element.picElement!.sourcePath = elementResults?.[elementIndex] ?? '';
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
element.videoElement!.filePath = elementResults?.[elementIndex] ?? '';
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
element.pttElement!.filePath = elementResults?.[elementIndex] ?? '';
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
element.fileElement!.filePath = elementResults?.[elementIndex] ?? '';
|
||||
break;
|
||||
}
|
||||
elementIndex++;
|
||||
}
|
||||
@@ -299,7 +308,7 @@ export class NTQQFileApi {
|
||||
if (force) {
|
||||
try {
|
||||
await fsPromises.unlink(sourcePath);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
} else {
|
||||
@@ -401,27 +410,27 @@ export class NTQQFileApi {
|
||||
}
|
||||
|
||||
private async getRkeyData() {
|
||||
const rkeyData = {
|
||||
const rkeyData: rkeyDataType = {
|
||||
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
|
||||
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
|
||||
online_rkey: false
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.core.apis.PacketApi.available) {
|
||||
if (this.core.apis.PacketApi.available && this.packetRkey?.[0] && this.packetRkey?.[1]) {
|
||||
const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
|
||||
const rkey_expired_group = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
|
||||
if (rkey_expired_private || rkey_expired_group) {
|
||||
this.packetRkey = await this.core.apis.PacketApi.pkt.operation.FetchRkey();
|
||||
}
|
||||
if (this.packetRkey && this.packetRkey.length > 0) {
|
||||
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
|
||||
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
|
||||
rkeyData.group_rkey = this.packetRkey[1]?.rkey.slice(6) ?? '';
|
||||
rkeyData.private_rkey = this.packetRkey[0]?.rkey.slice(6) ?? '';
|
||||
rkeyData.online_rkey = true;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.context.logger.logError('获取rkey失败', error.message);
|
||||
} catch (error: unknown) {
|
||||
this.context.logger.logError('获取rkey失败', (error as Error).message);
|
||||
}
|
||||
|
||||
if (!rkeyData.online_rkey) {
|
||||
@@ -438,7 +447,7 @@ export class NTQQFileApi {
|
||||
return rkeyData;
|
||||
}
|
||||
|
||||
private getImageUrlFromParsedUrl(imageFileId: string, appid: string, rkeyData: any): string {
|
||||
private getImageUrlFromParsedUrl(imageFileId: string, appid: string, rkeyData: rkeyDataType): string {
|
||||
const rkey = appid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
if (rkeyData.online_rkey) {
|
||||
return IMAGE_HTTP_HOST_NT + `/download?appid=${appid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||
|
@@ -13,7 +13,7 @@ export class NTQQFriendApi {
|
||||
async setBuddyRemark(uid: string, remark: string) {
|
||||
return this.context.session.getBuddyService().setBuddyRemark({ uid, remark });
|
||||
}
|
||||
async getBuddyV2SimpleInfoMap(refresh = false) {
|
||||
async getBuddyV2SimpleInfoMap() {
|
||||
const buddyService = this.context.session.getBuddyService();
|
||||
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
|
||||
const uids = buddyListV2.data.flatMap(item => item.buddyUids);
|
||||
@@ -24,13 +24,13 @@ export class NTQQFriendApi {
|
||||
);
|
||||
}
|
||||
|
||||
async getBuddy(refresh = false): Promise<FriendV2[]> {
|
||||
return Array.from((await this.getBuddyV2SimpleInfoMap(refresh)).values());
|
||||
async getBuddy(): Promise<FriendV2[]> {
|
||||
return Array.from((await this.getBuddyV2SimpleInfoMap()).values());
|
||||
}
|
||||
|
||||
async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> {
|
||||
async getBuddyIdMap(): Promise<LimitedHashTable<string, string>> {
|
||||
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000);
|
||||
const data = await this.getBuddyV2SimpleInfoMap(refresh);
|
||||
const data = await this.getBuddyV2SimpleInfoMap();
|
||||
data.forEach((value) => retMap.set(value.uin!, value.uid!));
|
||||
return retMap;
|
||||
}
|
||||
|
@@ -59,7 +59,7 @@ export class NTQQGroupApi {
|
||||
}, pskey);
|
||||
}
|
||||
|
||||
async getGroupShutUpMemberList(groupCode: string) {
|
||||
async getGroupShutUpMemberList(groupCode: string): Promise<ShutUpGroupMember[]> {
|
||||
const executor: TaskExecutor<ShutUpGroupMember[]> = async (resolve, reject, onCancel) => {
|
||||
this.core.eventWrapper.registerListen(
|
||||
'NodeIKernelGroupListener/onShutUpMemberListChanged',
|
||||
@@ -215,6 +215,9 @@ export class NTQQGroupApi {
|
||||
guildId: '',
|
||||
peerUid: groupCode,
|
||||
}, msgId, 1, false);
|
||||
if (!MsgData.msgList[0]) {
|
||||
throw new Error('消息不存在');
|
||||
}
|
||||
const param = {
|
||||
groupCode: groupCode,
|
||||
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
||||
@@ -255,6 +258,9 @@ export class NTQQGroupApi {
|
||||
guildId: '',
|
||||
peerUid: groupCode,
|
||||
}, msgId, 1, false);
|
||||
if (!MsgData.msgList[0]) {
|
||||
throw new Error('消息不存在');
|
||||
}
|
||||
const param = {
|
||||
groupCode: groupCode,
|
||||
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
||||
|
@@ -4,4 +4,6 @@ export * from './group';
|
||||
export * from './msg';
|
||||
export * from './user';
|
||||
export * from './webapi';
|
||||
export * from './system';
|
||||
export * from './system';
|
||||
export * from './packet';
|
||||
export * from './file';
|
@@ -201,7 +201,7 @@ export class NTQQMsgApi {
|
||||
return this.context.session.getMsgService().getTempChatInfo(chatType, peerUid);
|
||||
}
|
||||
|
||||
async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
|
||||
async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) {
|
||||
//唉?!我有个想法
|
||||
if (peer.chatType === ChatType.KCHATTYPETEMPC2CFROMGROUP && peer.guildId && peer.guildId !== '') {
|
||||
const member = await this.core.apis.GroupApi.getGroupMember(peer.guildId, peer.peerUid);
|
||||
@@ -268,7 +268,7 @@ export class NTQQMsgApi {
|
||||
if (!arkElement) {
|
||||
continue;
|
||||
}
|
||||
const forwardData: any = JSON.parse(arkElement.arkElement?.bytesData ?? '');
|
||||
const forwardData: { app: string } = JSON.parse(arkElement.arkElement?.bytesData ?? '');
|
||||
if (forwardData.app != 'com.tencent.multimsg') {
|
||||
continue;
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import * as os from 'os';
|
||||
import offset from '@/core/external/offset.json';
|
||||
import { InstanceContext, NapCatCore } from "@/core";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { PacketClientSession } from "@/core/packet/clientSession";
|
||||
import { napCatVersion } from "@/common/version";
|
||||
import { InstanceContext, NapCatCore } from '@/core';
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { PacketClientSession } from '@/core/packet/clientSession';
|
||||
import { napCatVersion } from '@/common/version';
|
||||
|
||||
interface OffsetType {
|
||||
[key: string]: {
|
||||
|
@@ -25,7 +25,7 @@ export class NTQQSystemApi {
|
||||
this.context.session.getMsgService().getOnLineDev();
|
||||
}
|
||||
|
||||
async getArkJsonCollection(cid: string) {
|
||||
async getArkJsonCollection() {
|
||||
return await this.core.eventWrapper.callNoListenerEvent('NodeIKernelCollectionService/collectionArkShare', '1717662698058');
|
||||
}
|
||||
|
||||
|
@@ -69,7 +69,7 @@ export class NTQQUserApi {
|
||||
}
|
||||
|
||||
async fetchUserDetailInfo(uid: string, mode: UserDetailSource = UserDetailSource.KDB) {
|
||||
const [_retData, profile] = await this.core.eventWrapper.callNormalEventV2(
|
||||
const [, profile] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelProfileService/fetchUserDetailInfo',
|
||||
'NodeIKernelProfileListener/onUserDetailInfoChanged',
|
||||
[
|
||||
@@ -130,10 +130,10 @@ export class NTQQUserApi {
|
||||
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + this.core.selfInfo.uin +
|
||||
'&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + this.core.selfInfo.uin + '%2Finfocenter&keyindex=19%27';
|
||||
const data = await RequestUtil.HttpsGetCookies(requestUrl);
|
||||
if (!data.p_skey || data.p_skey.length == 0) {
|
||||
if (!data['p_skey'] || data['p_skey'].length == 0) {
|
||||
try {
|
||||
const pskey = (await this.getPSkey([domain])).domainPskeyMap.get(domain);
|
||||
if (pskey) data.p_skey = pskey;
|
||||
if (pskey) data['p_skey'] = pskey;
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export class NTQQUserApi {
|
||||
return await this.context.session.getTipOffService().getPskey(domainList, true);
|
||||
}
|
||||
|
||||
async getRobotUinRange(): Promise<Array<any>> {
|
||||
async getRobotUinRange(): Promise<Array<unknown>> {
|
||||
const robotUinRanges = await this.context.session.getRobotService().getRobotUinRange({
|
||||
justFetchMsgConfig: '1',
|
||||
type: 1,
|
||||
|
@@ -32,7 +32,7 @@ export class NTQQWebApi {
|
||||
}).toString()}`;
|
||||
try {
|
||||
return RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': this.cookieToString(cookieObject) });
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export class NTQQWebApi {
|
||||
}
|
||||
}
|
||||
|
||||
async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
|
||||
async getGroupMembers(GroupCode: string): Promise<WebApiGroupMember[]> {
|
||||
//logDebug('webapi 获取群成员', GroupCode);
|
||||
const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>();
|
||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||
@@ -88,7 +88,9 @@ export class NTQQWebApi {
|
||||
return [];
|
||||
} else {
|
||||
for (const key in fastRet.mems) {
|
||||
memberData.push(fastRet.mems[key]);
|
||||
if (fastRet.mems[key]) {
|
||||
memberData.push(fastRet.mems[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
//初始化获取PageNum
|
||||
@@ -116,7 +118,9 @@ export class NTQQWebApi {
|
||||
continue;
|
||||
}
|
||||
for (const key in ret.mems) {
|
||||
memberData.push(ret.mems[key]);
|
||||
if (ret.mems[key]) {
|
||||
memberData.push(ret.mems[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return memberData;
|
||||
@@ -185,7 +189,7 @@ export class NTQQWebApi {
|
||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||
);
|
||||
return ret;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -210,12 +214,12 @@ export class NTQQWebApi {
|
||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||
);
|
||||
return ret?.ec === 0 ? ret : undefined;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async getDataInternal(cookieObject: any, groupCode: string, type: number) {
|
||||
private async getDataInternal(cookieObject: { [key: string]: string }, groupCode: string, type: number) {
|
||||
let resJson;
|
||||
try {
|
||||
const res = await RequestUtil.HttpGetText(
|
||||
@@ -228,7 +232,7 @@ export class NTQQWebApi {
|
||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||
);
|
||||
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
|
||||
if (match) {
|
||||
if (match?.[1]) {
|
||||
resJson = JSON.parse(match[1].trim());
|
||||
}
|
||||
return type === 1 ? resJson?.talkativeList : resJson?.actorList;
|
||||
@@ -238,13 +242,18 @@ export class NTQQWebApi {
|
||||
}
|
||||
}
|
||||
|
||||
private async getHonorList(cookieObject: any, groupCode: string, type: number) {
|
||||
private async getHonorList(cookieObject: { [key: string]: string }, groupCode: string, type: number) {
|
||||
const data = await this.getDataInternal(cookieObject, groupCode, type);
|
||||
if (!data) {
|
||||
this.context.logger.logError(`获取类型 ${type} 的荣誉信息失败`);
|
||||
return [];
|
||||
}
|
||||
return data.map((item: any) => ({
|
||||
return data.map((item: {
|
||||
uin: string,
|
||||
name: string,
|
||||
avatar: string,
|
||||
desc: string,
|
||||
}) => ({
|
||||
user_id: item?.uin,
|
||||
nickname: item?.name,
|
||||
avatar: item?.avatar,
|
||||
@@ -254,7 +263,15 @@ export class NTQQWebApi {
|
||||
|
||||
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
|
||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||
const HonorInfo: any = { group_id: groupCode };
|
||||
let HonorInfo = {
|
||||
group_id: groupCode,
|
||||
current_talkative: {},
|
||||
talkative_list: [],
|
||||
performer_list: [],
|
||||
legend_list: [],
|
||||
emotion_list: [],
|
||||
strong_newbie_list: [],
|
||||
};
|
||||
|
||||
if (getType === WebHonorType.TALKATIVE || getType === WebHonorType.ALL) {
|
||||
const talkativeList = await this.getHonorList(cookieObject, groupCode, 1);
|
||||
@@ -284,12 +301,12 @@ export class NTQQWebApi {
|
||||
return HonorInfo;
|
||||
}
|
||||
|
||||
private cookieToString(cookieObject: any) {
|
||||
private cookieToString(cookieObject: { [key: string]: string }) {
|
||||
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ');
|
||||
}
|
||||
|
||||
public getBknFromCookie(cookieObject: any) {
|
||||
const sKey = cookieObject.skey as string;
|
||||
public getBknFromCookie(cookieObject: { [key: string]: string }) {
|
||||
const sKey = cookieObject['skey'] as string;
|
||||
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < sKey.length; i++) {
|
||||
@@ -322,18 +339,18 @@ export class NTQQWebApi {
|
||||
data: pskey,
|
||||
appid: 5
|
||||
},
|
||||
appid: "qun",
|
||||
appid: 'qun',
|
||||
checksum: img_md5,
|
||||
check_type: 0,
|
||||
file_len: img_size,
|
||||
env: {
|
||||
refer: "qzone",
|
||||
deviceInfo: "h5"
|
||||
refer: 'qzone',
|
||||
deviceInfo: 'h5'
|
||||
},
|
||||
model: 0,
|
||||
biz_req: {
|
||||
sPicTitle: img_name,
|
||||
sPicDesc: "",
|
||||
sPicDesc: '',
|
||||
sAlbumName: sAlbumName,
|
||||
sAlbumID: sAlbumID,
|
||||
iAlbumTypeID: 0,
|
||||
@@ -341,7 +358,7 @@ export class NTQQWebApi {
|
||||
iUploadType: 0,
|
||||
iUpPicType: 0,
|
||||
iBatchID: time,
|
||||
sPicPath: "",
|
||||
sPicPath: '',
|
||||
iPicWidth: 0,
|
||||
iPicHight: 0,
|
||||
iWaterType: 0,
|
||||
@@ -349,19 +366,19 @@ export class NTQQWebApi {
|
||||
iNeedFeeds: 1,
|
||||
iUploadTime: time,
|
||||
mapExt: {
|
||||
appid: "qun",
|
||||
appid: 'qun',
|
||||
userid: gc
|
||||
}
|
||||
},
|
||||
session: "",
|
||||
session: '',
|
||||
asy_upload: 0,
|
||||
cmd: "FileUpload"
|
||||
cmd: 'FileUpload'
|
||||
}]
|
||||
};
|
||||
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`;
|
||||
const post = await RequestUtil.HttpGetJson(api, 'POST', body, {
|
||||
"Cookie": cookie,
|
||||
"Content-Type": "application/json"
|
||||
'Cookie': cookie,
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
return post;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// TODO: further refactor in NapCat.Packet v2
|
||||
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
|
||||
import { NapProtoMsg, ProtoField, ScalarType } from '@napneko/nap-proto-core';
|
||||
|
||||
const LikeDetail = {
|
||||
txt: ProtoField(1, ScalarType.STRING),
|
||||
|
@@ -32,7 +32,7 @@ export class RkeyManager {
|
||||
}
|
||||
|
||||
if (this.failureCount >= this.FAILURE_LIMIT) {
|
||||
this.logger.logError(`[Rkey] 服务存在异常, 图片使用FallBack机制`);
|
||||
this.logger.logError('[Rkey] 服务存在异常, 图片使用FallBack机制');
|
||||
throw new Error('获取rkey失败次数过多,请稍后再试');
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export class RkeyManager {
|
||||
return now > this.rkeyData.expired_time;
|
||||
}
|
||||
|
||||
async refreshRkey(): Promise<any> {
|
||||
async refreshRkey() {
|
||||
//刷新rkey
|
||||
for (const url of this.serverUrl) {
|
||||
try {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import os from "node:os";
|
||||
import EventEmitter from "node:events";
|
||||
import os from 'node:os';
|
||||
import EventEmitter from 'node:events';
|
||||
|
||||
export interface SystemStatus {
|
||||
cpu: {
|
||||
@@ -35,9 +35,9 @@ export class StatusHelper {
|
||||
const { total, active } = currentTimes.map((times, index) => {
|
||||
const prevTimes = this.cpuTimes[index];
|
||||
const totalCurrent = times.user + times.nice + times.sys + times.idle + times.irq;
|
||||
const totalPrev = prevTimes.user + prevTimes.nice + prevTimes.sys + prevTimes.idle + prevTimes.irq;
|
||||
const totalPrev = (prevTimes?.user ?? 0) + (prevTimes?.nice ?? 0) + (prevTimes?.sys ?? 0) + (prevTimes?.idle ?? 0) + (prevTimes?.irq ?? 0);
|
||||
const activeCurrent = totalCurrent - times.idle;
|
||||
const activePrev = totalPrev - prevTimes.idle;
|
||||
const activePrev = totalPrev - (prevTimes?.idle ?? 0);
|
||||
return {
|
||||
total: totalCurrent - totalPrev,
|
||||
active: activeCurrent - activePrev
|
||||
@@ -49,8 +49,8 @@ export class StatusHelper {
|
||||
this.cpuTimes = currentTimes;
|
||||
return {
|
||||
usage: this.replaceNaN(((active / total) * 100)).toFixed(2),
|
||||
model: os.cpus()[0].model,
|
||||
speed: os.cpus()[0].speed,
|
||||
model: os.cpus()[0]?.model ?? 'none',
|
||||
speed: os.cpus()[0]?.speed ?? 0,
|
||||
core: os.cpus().length
|
||||
};
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { hostname, systemName, systemVersion } from '@/common/system';
|
||||
import { NTEventWrapper } from '@/common/event';
|
||||
import { GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types';
|
||||
import { KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types';
|
||||
import { NapCatConfigLoader } from '@/core/helper/config';
|
||||
import os from 'node:os';
|
||||
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
||||
@@ -52,13 +52,13 @@ export function loadQQWrapper(QQVersion: string): WrapperNodeApi {
|
||||
}
|
||||
let wrapperNodePath = path.resolve(appPath, 'wrapper.node');
|
||||
if (!fs.existsSync(wrapperNodePath)) {
|
||||
wrapperNodePath = path.join(appPath, `./resources/app/wrapper.node`);
|
||||
wrapperNodePath = path.join(appPath, './resources/app/wrapper.node');
|
||||
}
|
||||
//老版本兼容 未来去掉
|
||||
if (!fs.existsSync(wrapperNodePath)) {
|
||||
wrapperNodePath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/wrapper.node`);
|
||||
}
|
||||
const nativemodule: any = { exports: {} };
|
||||
const nativemodule: { exports: WrapperNodeApi } = { exports: {} as WrapperNodeApi };
|
||||
process.dlopen(nativemodule, wrapperNodePath);
|
||||
return nativemodule.exports;
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export function getMajorPath(QQVersion: string): string {
|
||||
}
|
||||
let majorPath = path.resolve(appPath, 'major.node');
|
||||
if (!fs.existsSync(majorPath)) {
|
||||
majorPath = path.join(appPath, `./resources/app/major.node`);
|
||||
majorPath = path.join(appPath, './resources/app/major.node');
|
||||
}
|
||||
//老版本兼容 未来去掉
|
||||
if (!fs.existsSync(majorPath)) {
|
||||
@@ -177,7 +177,7 @@ export class NapCatCore {
|
||||
profileListener.onSelfStatusChanged = (Info: SelfStatusInfo) => {
|
||||
if (Info.status == 20) {
|
||||
this.selfInfo.online = false;
|
||||
this.context.logger.log("账号状态变更为离线");
|
||||
this.context.logger.log('账号状态变更为离线');
|
||||
} else {
|
||||
this.selfInfo.online = true;
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { LRUCache } from "@/common/lru-cache";
|
||||
import crypto, { createHash } from "crypto";
|
||||
import { OidbPacket, PacketHexStr } from "@/core/packet/transformer/base";
|
||||
import { LogStack } from "@/core/packet/context/clientContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import { LRUCache } from '@/common/lru-cache';
|
||||
import crypto, { createHash } from 'crypto';
|
||||
import { OidbPacket, PacketHexStr } from '@/core/packet/transformer/base';
|
||||
import { LogStack } from '@/core/packet/context/clientContext';
|
||||
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
|
||||
import { PacketLogger } from '@/core/packet/context/loggerContext';
|
||||
|
||||
export interface RecvPacket {
|
||||
type: string, // 仅recv
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { createHash } from "crypto";
|
||||
import path, { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import fs from "fs";
|
||||
import { IPacketClient } from "@/core/packet/client/baseClient";
|
||||
import { constants } from "node:os";
|
||||
import { LRUCache } from "@/common/lru-cache";
|
||||
import { LogStack } from "@/core/packet/context/clientContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import { createHash } from 'crypto';
|
||||
import path, { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
import { IPacketClient } from '@/core/packet/client/baseClient';
|
||||
import { constants } from 'node:os';
|
||||
import { LRUCache } from '@/common/lru-cache';
|
||||
import { LogStack } from '@/core/packet/context/clientContext';
|
||||
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
|
||||
import { PacketLogger } from '@/core/packet/context/loggerContext';
|
||||
|
||||
// 0 send 1 recv
|
||||
export interface NativePacketExportType {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { Data, WebSocket, ErrorEvent } from "ws";
|
||||
import { IPacketClient, RecvPacket } from "@/core/packet/client/baseClient";
|
||||
import { LogStack } from "@/core/packet/context/clientContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import { Data, WebSocket, ErrorEvent } from 'ws';
|
||||
import { IPacketClient, RecvPacket } from '@/core/packet/client/baseClient';
|
||||
import { LogStack } from '@/core/packet/context/clientContext';
|
||||
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
|
||||
import { PacketLogger } from '@/core/packet/context/loggerContext';
|
||||
|
||||
export class WsPacketClient extends IPacketClient {
|
||||
private websocket: WebSocket | null = null;
|
||||
@@ -23,7 +23,7 @@ export class WsPacketClient extends IPacketClient {
|
||||
|
||||
check(): boolean {
|
||||
if (!this.napcore.config.packetServer) {
|
||||
this.logStack.pushLogWarn(`wsPacketClient 未配置服务器地址`);
|
||||
this.logStack.pushLogWarn('wsPacketClient 未配置服务器地址');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -52,7 +52,7 @@ export class WsPacketClient extends IPacketClient {
|
||||
try {
|
||||
await this.connect();
|
||||
return;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
this.reconnectAttempts++;
|
||||
this.logStack.pushLogWarn(`第 ${this.reconnectAttempts}/${this.maxReconnectAttempts} 次尝试重连失败!`);
|
||||
await this.delay(5000);
|
||||
@@ -80,7 +80,7 @@ export class WsPacketClient extends IPacketClient {
|
||||
};
|
||||
this.websocket.onclose = () => {
|
||||
this.available = false;
|
||||
this.logger.warn(`WebSocket 连接关闭,尝试重连...`);
|
||||
this.logger.warn('WebSocket 连接关闭,尝试重连...');
|
||||
reject(new Error('WebSocket 连接关闭'));
|
||||
};
|
||||
this.websocket.onmessage = (event) => this.handleMessage(event.data).catch(err => {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { NapCatCore } from "@/core";
|
||||
import { PacketContext } from '@/core/packet/context/packetContext';
|
||||
import { NapCatCore } from '@/core';
|
||||
|
||||
export class PacketClientSession {
|
||||
private readonly context: PacketContext;
|
||||
|
@@ -1,15 +1,15 @@
|
||||
import { IPacketClient } from "@/core/packet/client/baseClient";
|
||||
import { NativePacketClient } from "@/core/packet/client/nativeClient";
|
||||
import { WsPacketClient } from "@/core/packet/client/wsClient";
|
||||
import { OidbPacket } from "@/core/packet/transformer/base";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { IPacketClient } from '@/core/packet/client/baseClient';
|
||||
import { NativePacketClient } from '@/core/packet/client/nativeClient';
|
||||
import { WsPacketClient } from '@/core/packet/client/wsClient';
|
||||
import { OidbPacket } from '@/core/packet/transformer/base';
|
||||
import { PacketLogger } from '@/core/packet/context/loggerContext';
|
||||
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
|
||||
|
||||
type clientPriority = {
|
||||
type clientPriorityType = {
|
||||
[key: number]: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => IPacketClient;
|
||||
}
|
||||
|
||||
const clientPriority: clientPriority = {
|
||||
const clientPriority: clientPriorityType = {
|
||||
10: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new NativePacketClient(napCore, logger, logStack),
|
||||
1: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new WsPacketClient(napCore, logger, logStack),
|
||||
};
|
||||
@@ -77,22 +77,22 @@ export class PacketClientContext {
|
||||
|
||||
async sendOidbPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
|
||||
const raw = await this._client.sendOidbPacket(pkt, rsp);
|
||||
return (rsp ? Buffer.from(raw.hex_data, "hex") : undefined) as T extends true ? Buffer : void;
|
||||
return (rsp ? Buffer.from(raw.hex_data, 'hex') : undefined) as T extends true ? Buffer : void;
|
||||
}
|
||||
|
||||
private newClient(): IPacketClient {
|
||||
const prefer = this.napCore.config.packetBackend;
|
||||
let client: IPacketClient | null;
|
||||
switch (prefer) {
|
||||
case "native":
|
||||
this.logger.info("使用指定的 NativePacketClient 作为后端");
|
||||
case 'native':
|
||||
this.logger.info('使用指定的 NativePacketClient 作为后端');
|
||||
client = new NativePacketClient(this.napCore, this.logger, this.logStack);
|
||||
break;
|
||||
case "frida":
|
||||
this.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端");
|
||||
case 'frida':
|
||||
this.logger.info('[Core] [Packet] 使用指定的 FridaPacketClient 作为后端');
|
||||
client = new WsPacketClient(this.napCore, this.logger, this.logStack);
|
||||
break;
|
||||
case "auto":
|
||||
case 'auto':
|
||||
case undefined:
|
||||
client = this.judgeClient();
|
||||
break;
|
||||
@@ -101,10 +101,10 @@ export class PacketClientContext {
|
||||
client = null;
|
||||
}
|
||||
if (!client?.check()) {
|
||||
throw new Error("[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!");
|
||||
throw new Error('[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!');
|
||||
}
|
||||
if (!client) {
|
||||
throw new Error("[Core] [Packet] 后端异常,NapCat.Packet将不会加载!");
|
||||
throw new Error('[Core] [Packet] 后端异常,NapCat.Packet将不会加载!');
|
||||
}
|
||||
return client;
|
||||
}
|
||||
@@ -120,7 +120,7 @@ export class PacketClientContext {
|
||||
.sort((a, b) => b.score - a.score);
|
||||
const selectedClient = sortedClients[0]?.client;
|
||||
if (!selectedClient) {
|
||||
throw new Error("[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!");
|
||||
throw new Error('[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!');
|
||||
}
|
||||
this.logger.info(`自动选择 ${selectedClient.constructor.name} 作为后端`);
|
||||
return selectedClient;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { LogLevel, LogWrapper } from "@/common/log";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { LogLevel, LogWrapper } from '@/common/log';
|
||||
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
|
||||
|
||||
// TODO: check bind?
|
||||
export class PacketLogger {
|
||||
@@ -10,7 +11,7 @@ export class PacketLogger {
|
||||
}
|
||||
|
||||
private _log(level: LogLevel, ...msg: any[]): void {
|
||||
this.napLogger._log(level, "[Core] [Packet] " + msg);
|
||||
this.napLogger._log(level, '[Core] [Packet] ' + msg);
|
||||
}
|
||||
|
||||
debug(...msg: any[]): void {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { NapCatCore } from "@/core";
|
||||
import { NapCatCore } from '@/core';
|
||||
|
||||
export interface NapCoreCompatBasicInfo {
|
||||
readonly uin: number;
|
||||
|
@@ -1,20 +1,20 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import * as trans from "@/core/packet/transformer";
|
||||
import { PacketMsg } from "@/core/packet/message/message";
|
||||
import { PacketContext } from '@/core/packet/context/packetContext';
|
||||
import * as trans from '@/core/packet/transformer';
|
||||
import { PacketMsg } from '@/core/packet/message/message';
|
||||
import {
|
||||
PacketMsgFileElement,
|
||||
PacketMsgPicElement,
|
||||
PacketMsgPttElement,
|
||||
PacketMsgVideoElement
|
||||
} from "@/core/packet/message/element";
|
||||
import { ChatType } from "@/core";
|
||||
import { MiniAppRawData, MiniAppReqParams } from "@/core/packet/entities/miniApp";
|
||||
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
||||
import { NapProtoDecodeStructType, NapProtoEncodeStructType } from "@napneko/nap-proto-core";
|
||||
import { IndexNode, MsgInfo } from "@/core/packet/transformer/proto";
|
||||
import { OidbPacket } from "@/core/packet/transformer/base";
|
||||
import { ImageOcrResult } from "@/core/packet/entities/ocrResult";
|
||||
} from '@/core/packet/message/element';
|
||||
import { ChatType } from '@/core';
|
||||
import { MiniAppRawData, MiniAppReqParams } from '@/core/packet/entities/miniApp';
|
||||
import { AIVoiceChatType } from '@/core/packet/entities/aiChat';
|
||||
import { NapProtoDecodeStructType, NapProtoEncodeStructType } from '@napneko/nap-proto-core';
|
||||
import { IndexNode, MsgInfo } from '@/core/packet/transformer/proto';
|
||||
import { OidbPacket } from '@/core/packet/transformer/base';
|
||||
import { ImageOcrResult } from '@/core/packet/entities/ocrResult';
|
||||
|
||||
export class PacketOperationContext {
|
||||
private readonly context: PacketContext;
|
||||
@@ -61,7 +61,7 @@ export class PacketOperationContext {
|
||||
}
|
||||
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn));
|
||||
return { status: 10, ext_status: status };
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { PacketHighwayContext } from "@/core/packet/highway/highwayContext";
|
||||
import { NapCatCore } from "@/core";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketClientContext } from "@/core/packet/context/clientContext";
|
||||
import { PacketOperationContext } from "@/core/packet/context/operationContext";
|
||||
import { PacketMsgConverter } from "@/core/packet/message/converter";
|
||||
import { PacketHighwayContext } from '@/core/packet/highway/highwayContext';
|
||||
import { NapCatCore } from '@/core';
|
||||
import { PacketLogger } from '@/core/packet/context/loggerContext';
|
||||
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
|
||||
import { PacketClientContext } from '@/core/packet/context/clientContext';
|
||||
import { PacketOperationContext } from '@/core/packet/context/operationContext';
|
||||
import { PacketMsgConverter } from '@/core/packet/message/converter';
|
||||
|
||||
export class PacketContext {
|
||||
readonly msgConverter: PacketMsgConverter;
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import * as stream from 'node:stream';
|
||||
import { ReadStream } from "node:fs";
|
||||
import { HighwayTcpUploader } from "@/core/packet/highway/uploader/highwayTcpUploader";
|
||||
import { HighwayHttpUploader } from "@/core/packet/highway/uploader/highwayHttpUploader";
|
||||
import { PacketHighwaySig } from "@/core/packet/highway/highwayContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import { ReadStream } from 'node:fs';
|
||||
import { HighwayTcpUploader } from '@/core/packet/highway/uploader/highwayTcpUploader';
|
||||
import { HighwayHttpUploader } from '@/core/packet/highway/uploader/highwayHttpUploader';
|
||||
import { PacketHighwaySig } from '@/core/packet/highway/highwayContext';
|
||||
import { PacketLogger } from '@/core/packet/context/loggerContext';
|
||||
|
||||
export interface PacketHighwayTrans {
|
||||
uin: string;
|
||||
@@ -27,7 +27,8 @@ export class PacketHighwayClient {
|
||||
port: number = 80;
|
||||
logger: PacketLogger;
|
||||
|
||||
constructor(sig: PacketHighwaySig, logger: PacketLogger, server: string = 'htdata3.qq.com', port: number = 80) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
constructor(sig: PacketHighwaySig, logger: PacketLogger, _server: string = 'htdata3.qq.com', _port: number = 80) {
|
||||
this.sig = sig;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import assert from "node:assert";
|
||||
import assert from 'node:assert';
|
||||
|
||||
export class Frame{
|
||||
static pack(head: Buffer, body: Buffer): Buffer {
|
||||
|
@@ -1,22 +1,22 @@
|
||||
import { PacketHighwayClient } from "@/core/packet/highway/client";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import FetchSessionKey from "@/core/packet/transformer/highway/FetchSessionKey";
|
||||
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
|
||||
import { PacketHighwayClient } from '@/core/packet/highway/client';
|
||||
import { PacketLogger } from '@/core/packet/context/loggerContext';
|
||||
import FetchSessionKey from '@/core/packet/transformer/highway/FetchSessionKey';
|
||||
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from '@/core/packet/highway/utils';
|
||||
import {
|
||||
PacketMsgFileElement,
|
||||
PacketMsgPicElement,
|
||||
PacketMsgPttElement,
|
||||
PacketMsgVideoElement
|
||||
} from "@/core/packet/message/element";
|
||||
import { ChatType, Peer } from "@/core";
|
||||
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
|
||||
import UploadGroupImage from "@/core/packet/transformer/highway/UploadGroupImage";
|
||||
import { NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import * as trans from "@/core/packet/transformer";
|
||||
import fs from "fs";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketClientContext } from "@/core/packet/context/clientContext";
|
||||
} from '@/core/packet/message/element';
|
||||
import { ChatType, Peer } from '@/core';
|
||||
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from '@/core/packet/utils/crypto/hash';
|
||||
import UploadGroupImage from '@/core/packet/transformer/highway/UploadGroupImage';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import * as proto from '@/core/packet/transformer/proto';
|
||||
import * as trans from '@/core/packet/transformer';
|
||||
import fs from 'fs';
|
||||
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
|
||||
import { PacketClientContext } from '@/core/packet/context/clientContext';
|
||||
|
||||
export const BlockSize = 1024 * 1024;
|
||||
|
||||
@@ -142,7 +142,7 @@ export class PacketHighwayContext {
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = UploadGroupImage.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
@@ -179,7 +179,7 @@ export class PacketHighwayContext {
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivateImage.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
@@ -210,14 +210,14 @@ export class PacketHighwayContext {
|
||||
}
|
||||
|
||||
private async uploadGroupVideo(groupUin: number, video: PacketMsgVideoElement): Promise<void> {
|
||||
if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty");
|
||||
if (!video.filePath || !video.thumbPath) throw new Error('video.filePath or video.thumbPath is empty');
|
||||
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
|
||||
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
|
||||
const req = trans.UploadGroupVideo.build(groupUin, video);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadGroupVideo.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
@@ -244,7 +244,7 @@ export class PacketHighwayContext {
|
||||
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
const subFile = preRespData.upload.subFileInfos[0];
|
||||
if (subFile.uKey && subFile.uKey != "") {
|
||||
if (subFile.uKey && subFile.uKey != '') {
|
||||
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
@@ -275,14 +275,14 @@ export class PacketHighwayContext {
|
||||
}
|
||||
|
||||
private async uploadC2CVideo(peerUid: string, video: PacketMsgVideoElement): Promise<void> {
|
||||
if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty");
|
||||
if (!video.filePath || !video.thumbPath) throw new Error('video.filePath or video.thumbPath is empty');
|
||||
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
|
||||
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
|
||||
const req = trans.UploadPrivateVideo.build(peerUid, video);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivateVideo.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
@@ -309,7 +309,7 @@ export class PacketHighwayContext {
|
||||
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
const subFile = preRespData.upload.subFileInfos[0];
|
||||
if (subFile.uKey && subFile.uKey != "") {
|
||||
if (subFile.uKey && subFile.uKey != '') {
|
||||
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
@@ -345,7 +345,7 @@ export class PacketHighwayContext {
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadGroupPtt.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadGroupPttReq get upload ptt ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
@@ -381,7 +381,7 @@ export class PacketHighwayContext {
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivatePtt.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadC2CPttReq get upload ptt ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
@@ -419,7 +419,7 @@ export class PacketHighwayContext {
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadGroupFile.parse(resp);
|
||||
if (!preRespData?.upload?.boolFileExist) {
|
||||
this.logger.debug(`[Highway] uploadGroupFileReq file not exist, need upload!`);
|
||||
this.logger.debug('[Highway] uploadGroupFileReq file not exist, need upload!');
|
||||
const ext = new NapProtoMsg(proto.FileUploadExt).encode({
|
||||
unknown1: 100,
|
||||
unknown2: 1,
|
||||
@@ -439,9 +439,9 @@ export class PacketHighwayContext {
|
||||
},
|
||||
clientInfo: {
|
||||
clientType: 3,
|
||||
appId: "100",
|
||||
appId: '100',
|
||||
terminalType: 3,
|
||||
clientVer: "1.1.1",
|
||||
clientVer: '1.1.1',
|
||||
unknown: 4
|
||||
},
|
||||
fileNameInfo: {
|
||||
@@ -469,7 +469,7 @@ export class PacketHighwayContext {
|
||||
ext
|
||||
);
|
||||
} else {
|
||||
this.logger.debug(`[Highway] uploadGroupFileReq file exist, don't need upload!`);
|
||||
this.logger.debug('[Highway] uploadGroupFileReq file exist, don\'t need upload!');
|
||||
}
|
||||
file.fileUuid = preRespData.upload.fileId;
|
||||
}
|
||||
@@ -482,7 +482,7 @@ export class PacketHighwayContext {
|
||||
const res = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivateFile.parse(res);
|
||||
if (!preRespData.upload?.boolFileExist) {
|
||||
this.logger.debug(`[Highway] uploadC2CFileReq file not exist, need upload!`);
|
||||
this.logger.debug('[Highway] uploadC2CFileReq file not exist, need upload!');
|
||||
const ext = new NapProtoMsg(proto.FileUploadExt).encode({
|
||||
unknown1: 100,
|
||||
unknown2: 1,
|
||||
@@ -500,9 +500,9 @@ export class PacketHighwayContext {
|
||||
},
|
||||
clientInfo: {
|
||||
clientType: 3,
|
||||
appId: "100",
|
||||
appId: '100',
|
||||
terminalType: 3,
|
||||
clientVer: "1.1.1",
|
||||
clientVer: '1.1.1',
|
||||
unknown: 4
|
||||
},
|
||||
fileNameInfo: {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import crypto from "node:crypto";
|
||||
import http from "node:http";
|
||||
import { NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import { IHighwayUploader } from "@/core/packet/highway/uploader/highwayUploader";
|
||||
import { Frame } from "@/core/packet/highway/frame";
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import crypto from 'node:crypto';
|
||||
import http from 'node:http';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { IHighwayUploader } from '@/core/packet/highway/uploader/highwayUploader';
|
||||
import { Frame } from '@/core/packet/highway/frame';
|
||||
import * as proto from '@/core/packet/transformer/proto';
|
||||
|
||||
export class HighwayHttpUploader extends IHighwayUploader {
|
||||
async upload(): Promise<void> {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import net from "node:net";
|
||||
import stream from "node:stream";
|
||||
import crypto from "node:crypto";
|
||||
import { NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import { BlockSize } from "@/core/packet/highway/highwayContext";
|
||||
import { Frame } from "@/core/packet/highway/frame";
|
||||
import { IHighwayUploader } from "@/core/packet/highway/uploader/highwayUploader";
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import net from 'node:net';
|
||||
import stream from 'node:stream';
|
||||
import crypto from 'node:crypto';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { BlockSize } from '@/core/packet/highway/highwayContext';
|
||||
import { Frame } from '@/core/packet/highway/frame';
|
||||
import { IHighwayUploader } from '@/core/packet/highway/uploader/highwayUploader';
|
||||
import * as proto from '@/core/packet/transformer/proto';
|
||||
|
||||
class HighwayTcpUploaderTransform extends stream.Transform {
|
||||
uploader: HighwayTcpUploader;
|
||||
@@ -17,7 +17,8 @@ class HighwayTcpUploaderTransform extends stream.Transform {
|
||||
this.offset = 0;
|
||||
}
|
||||
|
||||
_transform(data: Buffer, _: BufferEncoding, callback: stream.TransformCallback) {
|
||||
// eslint-disable-next-line no-undef
|
||||
override _transform(data: Buffer, _: BufferEncoding, callback: stream.TransformCallback) {
|
||||
let chunkOffset = 0;
|
||||
while (chunkOffset < data.length) {
|
||||
const chunkSize = Math.min(BlockSize, data.length - chunkOffset);
|
||||
@@ -60,6 +61,7 @@ export class HighwayTcpUploader extends IHighwayUploader {
|
||||
socket.end();
|
||||
reject(new Error('Upload aborted due to timeout'));
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [head, _] = Frame.unpack(chunk);
|
||||
handleRspHeader(head);
|
||||
});
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import * as tea from "@/core/packet/utils/crypto/tea";
|
||||
import { NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import { PacketHighwayTrans } from "@/core/packet/highway/client";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import * as tea from '@/core/packet/utils/crypto/tea';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { PacketHighwayTrans } from '@/core/packet/highway/client';
|
||||
import { PacketLogger } from '@/core/packet/context/loggerContext';
|
||||
import * as proto from '@/core/packet/transformer/proto';
|
||||
|
||||
export abstract class IHighwayUploader {
|
||||
readonly trans: PacketHighwayTrans;
|
||||
@@ -32,7 +32,7 @@ export abstract class IHighwayUploader {
|
||||
msgBaseHead: {
|
||||
version: 1,
|
||||
uin: this.trans.uin,
|
||||
command: "PicUp.DataUp",
|
||||
command: 'PicUp.DataUp',
|
||||
seq: 0,
|
||||
retryTimes: 0,
|
||||
appId: 1600001604,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { NapProtoEncodeStructType } from "@napneko/nap-proto-core";
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import { NapProtoEncodeStructType } from '@napneko/nap-proto-core';
|
||||
import * as proto from '@/core/packet/transformer/proto';
|
||||
|
||||
|
||||
export const int32ip2str = (ip: number) => {
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import * as crypto from "crypto";
|
||||
import { PushMsgBody } from "@/core/packet/transformer/proto";
|
||||
import { NapProtoEncodeStructType } from "@napneko/nap-proto-core";
|
||||
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
|
||||
import { IPacketMsgElement, PacketMsgTextElement } from "@/core/packet/message/element";
|
||||
import { SendTextElement } from "@/core";
|
||||
import * as crypto from 'crypto';
|
||||
import { PushMsgBody } from '@/core/packet/transformer/proto';
|
||||
import { NapProtoEncodeStructType } from '@napneko/nap-proto-core';
|
||||
import { PacketMsg, PacketSendMsgElement } from '@/core/packet/message/message';
|
||||
import { IPacketMsgElement, PacketMsgTextElement } from '@/core/packet/message/element';
|
||||
import { SendTextElement } from '@/core';
|
||||
|
||||
export class PacketMsgBuilder {
|
||||
protected static failBackText = new PacketMsgTextElement(
|
||||
{
|
||||
textElement: { content: "[该消息类型暂不支持查看]" }
|
||||
textElement: { content: '[该消息类型暂不支持查看]' }
|
||||
} as SendTextElement
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ export class PacketMsgBuilder {
|
||||
}
|
||||
return {
|
||||
responseHead: {
|
||||
fromUid: "",
|
||||
fromUid: '',
|
||||
fromUin: node.senderUin,
|
||||
toUid: node.groupId ? undefined : selfUid,
|
||||
forward: node.groupId ? undefined : {
|
||||
|
@@ -15,7 +15,7 @@ import {
|
||||
SendStructLongMsgElement,
|
||||
SendTextElement,
|
||||
SendVideoElement
|
||||
} from "@/core";
|
||||
} from '@/core';
|
||||
import {
|
||||
IPacketMsgElement,
|
||||
PacketMsgAtElement,
|
||||
@@ -30,8 +30,8 @@ import {
|
||||
PacketMsgTextElement,
|
||||
PacketMsgVideoElement,
|
||||
PacketMultiMsgElement
|
||||
} from "@/core/packet/message/element";
|
||||
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
|
||||
} from '@/core/packet/message/element';
|
||||
import { PacketMsg, PacketSendMsgElement } from '@/core/packet/message/message';
|
||||
|
||||
const SupportedElementTypes = [
|
||||
ElementType.TEXT,
|
||||
@@ -146,7 +146,7 @@ export class PacketMsgConverter {
|
||||
? msg.sendMemberName
|
||||
: msg.sendNickName && msg.sendNickName !== ''
|
||||
? msg.sendNickName
|
||||
: "QQ用户",
|
||||
: 'QQ用户',
|
||||
time: +msg.msgTime,
|
||||
msg: msg.elements.map((element) => {
|
||||
if (!this.isValidElementType(element.elementType)) return null;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as zlib from "node:zlib";
|
||||
import { NapProtoEncodeStructType, NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import * as zlib from 'node:zlib';
|
||||
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import {
|
||||
CustomFace,
|
||||
Elem,
|
||||
@@ -12,9 +12,8 @@ import {
|
||||
OidbSvcTrpcTcp0XE37_800Response,
|
||||
FileExtra,
|
||||
GroupFileExtra
|
||||
} from "@/core/packet/transformer/proto";
|
||||
} from '@/core/packet/transformer/proto';
|
||||
import {
|
||||
BaseEmojiType,
|
||||
FaceType,
|
||||
NTMsgAtType,
|
||||
PicType,
|
||||
@@ -29,14 +28,15 @@ import {
|
||||
SendStructLongMsgElement,
|
||||
SendTextElement,
|
||||
SendVideoElement
|
||||
} from "@/core";
|
||||
import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
|
||||
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
|
||||
} from '@/core';
|
||||
import { ForwardMsgBuilder } from '@/common/forward-msg-builder';
|
||||
import { PacketMsg, PacketSendMsgElement } from '@/core/packet/message/message';
|
||||
|
||||
// raw <-> packet
|
||||
// TODO: SendStructLongMsgElement
|
||||
export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
|
||||
protected constructor(rawElement: T) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected constructor(_rawElement: T) {
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
@@ -64,7 +64,7 @@ export class PacketMsgTextElement extends IPacketMsgElement<SendTextElement> {
|
||||
this.text = element.textElement.content;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
text: {
|
||||
str: this.text
|
||||
@@ -72,7 +72,7 @@ export class PacketMsgTextElement extends IPacketMsgElement<SendTextElement> {
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
override toPreview(): string {
|
||||
return this.text;
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export class PacketMsgAtElement extends PacketMsgTextElement {
|
||||
this.atAll = element.textElement.atType === NTMsgAtType.ATTYPEALL;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
text: {
|
||||
str: this.text,
|
||||
@@ -127,7 +127,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
return this.messageClientSeq === 0;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
srcMsg: {
|
||||
origSeqs: [this.isGroupReply ? this.messageClientSeq : this.messageSeq],
|
||||
@@ -152,8 +152,8 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[回复消息]";
|
||||
override toPreview(): string {
|
||||
return '[回复消息]';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,18 +169,18 @@ export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
|
||||
this.isLargeFace = element.faceElement.faceType === FaceType.AniSticke;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
if (this.isLargeFace) {
|
||||
return [{
|
||||
commonElem: {
|
||||
serviceType: 37,
|
||||
pbElem: new NapProtoMsg(QBigFaceExtra).encode({
|
||||
aniStickerPackId: "1",
|
||||
aniStickerId: "8",
|
||||
aniStickerPackId: '1',
|
||||
aniStickerId: '8',
|
||||
faceId: this.faceId,
|
||||
sourceType: 1,
|
||||
resultId: this.resultId,
|
||||
preview: "",
|
||||
preview: '',
|
||||
randomType: 1
|
||||
}),
|
||||
businessType: 1
|
||||
@@ -198,8 +198,8 @@ export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
|
||||
serviceType: 33,
|
||||
pbElem: new NapProtoMsg(QSmallFaceExtra).encode({
|
||||
faceId: this.faceId,
|
||||
preview: "",
|
||||
preview2: ""
|
||||
preview: '',
|
||||
preview2: ''
|
||||
}),
|
||||
businessType: 1
|
||||
}
|
||||
@@ -207,8 +207,8 @@ export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
|
||||
}
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[表情]";
|
||||
override toPreview(): string {
|
||||
return '[表情]';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement<SendMarketFaceEl
|
||||
this.emojiKey = element.marketFaceElement.key;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
marketFace: {
|
||||
faceName: this.emojiName,
|
||||
@@ -245,7 +245,7 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement<SendMarketFaceEl
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
override toPreview(): string {
|
||||
return `${this.emojiName}`;
|
||||
}
|
||||
}
|
||||
@@ -280,11 +280,11 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
||||
) : element.picElement.summary;
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
override get valid(): boolean {
|
||||
return !!this.msgInfo;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
if (!this.msgInfo) return [];
|
||||
return [{
|
||||
commonElem: {
|
||||
@@ -295,7 +295,7 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
override toPreview(): string {
|
||||
return this.summary;
|
||||
}
|
||||
}
|
||||
@@ -318,18 +318,18 @@ export class PacketMsgVideoElement extends IPacketMsgElement<SendVideoElement> {
|
||||
this.fileSize = element.videoElement.fileSize;
|
||||
this.filePath = element.videoElement.filePath;
|
||||
this.thumbSize = element.videoElement.thumbSize;
|
||||
this.thumbPath = element.videoElement.thumbPath?.get(0);
|
||||
this.thumbPath = element.videoElement.thumbPath?.get(0) as string | undefined;
|
||||
this.fileMd5 = element.videoElement.videoMd5;
|
||||
this.thumbMd5 = element.videoElement.thumbMd5;
|
||||
this.thumbWidth = element.videoElement.thumbWidth;
|
||||
this.thumbHeight = element.videoElement.thumbHeight;
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
override get valid(): boolean {
|
||||
return !!this.msgInfo;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
if (!this.msgInfo) return [];
|
||||
return [{
|
||||
commonElem: {
|
||||
@@ -340,8 +340,8 @@ export class PacketMsgVideoElement extends IPacketMsgElement<SendVideoElement> {
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[视频]";
|
||||
override toPreview(): string {
|
||||
return '[视频]';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,11 +361,11 @@ export class PacketMsgPttElement extends IPacketMsgElement<SendPttElement> {
|
||||
this.fileDuration = Math.round(element.pttElement.duration); // TODO: cc
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
override get valid(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [];
|
||||
// if (!this.msgInfo) return [];
|
||||
// return [{
|
||||
@@ -377,8 +377,8 @@ export class PacketMsgPttElement extends IPacketMsgElement<SendPttElement> {
|
||||
// }];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[语音]";
|
||||
override toPreview(): string {
|
||||
return '[语音]';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,11 +402,11 @@ export class PacketMsgFileElement extends IPacketMsgElement<SendFileElement> {
|
||||
this.fileSize = +element.fileElement.fileSize;
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
override get valid(): boolean {
|
||||
return this.isGroupFile || Boolean(this._e37_800_rsp);
|
||||
}
|
||||
|
||||
buildContent(): Uint8Array | undefined {
|
||||
override buildContent(): Uint8Array | undefined {
|
||||
if (this.isGroupFile || !this._e37_800_rsp) return undefined;
|
||||
return new NapProtoMsg(FileExtra).encode({
|
||||
file: {
|
||||
@@ -437,7 +437,7 @@ export class PacketMsgFileElement extends IPacketMsgElement<SendFileElement> {
|
||||
});
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
if (!this.isGroupFile) return [];
|
||||
const lb = Buffer.alloc(2);
|
||||
const transElemVal = new NapProtoMsg(GroupFileExtra).encode({
|
||||
@@ -450,7 +450,7 @@ export class PacketMsgFileElement extends IPacketMsgElement<SendFileElement> {
|
||||
fileSize: BigInt(this.fileSize),
|
||||
fileName: this.fileName,
|
||||
fileSha: this.fileSha1,
|
||||
extInfoString: "",
|
||||
extInfoString: '',
|
||||
fileMd5: this.fileMd5,
|
||||
}
|
||||
}
|
||||
@@ -464,7 +464,7 @@ export class PacketMsgFileElement extends IPacketMsgElement<SendFileElement> {
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
override toPreview(): string {
|
||||
return `[文件]${this.fileName}`;
|
||||
}
|
||||
}
|
||||
@@ -477,7 +477,7 @@ export class PacketMsgLightAppElement extends IPacketMsgElement<SendArkElement>
|
||||
this.payload = element.arkElement.bytesData;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
lightAppElem: {
|
||||
data: Buffer.concat([
|
||||
@@ -488,8 +488,8 @@ export class PacketMsgLightAppElement extends IPacketMsgElement<SendArkElement>
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[卡片消息]";
|
||||
override toPreview(): string {
|
||||
return '[卡片消息]';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,7 +501,7 @@ export class PacketMsgMarkDownElement extends IPacketMsgElement<SendMarkdownElem
|
||||
this.content = element.markdownElement.content;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
commonElem: {
|
||||
serviceType: 45,
|
||||
@@ -513,7 +513,7 @@ export class PacketMsgMarkDownElement extends IPacketMsgElement<SendMarkdownElem
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
override toPreview(): string {
|
||||
return `[Markdown消息 ${this.content}]`;
|
||||
}
|
||||
}
|
||||
@@ -528,7 +528,7 @@ export class PacketMultiMsgElement extends IPacketMsgElement<SendStructLongMsgEl
|
||||
this.message = message ?? [];
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
lightAppElem: {
|
||||
data: Buffer.concat([
|
||||
@@ -539,7 +539,7 @@ export class PacketMultiMsgElement extends IPacketMsgElement<SendStructLongMsgEl
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[聊天记录]";
|
||||
override toPreview(): string {
|
||||
return '[聊天记录]';
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { IPacketMsgElement } from "@/core/packet/message/element";
|
||||
import { SendMessageElement, SendStructLongMsgElement } from "@/core";
|
||||
import { IPacketMsgElement } from '@/core/packet/message/element';
|
||||
import { SendMessageElement, SendStructLongMsgElement } from '@/core';
|
||||
|
||||
export type PacketSendMsgElement = SendMessageElement | SendStructLongMsgElement
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import { NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
|
||||
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
|
||||
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
||||
import * as proto from '@/core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
|
||||
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
|
||||
import { AIVoiceChatType } from '@/core/packet/entities/aiChat';
|
||||
|
||||
class FetchAiVoiceList extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0X929D_0Resp> {
|
||||
constructor() {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import { NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
|
||||
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
|
||||
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
||||
import * as proto from '@/core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
|
||||
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
|
||||
import { AIVoiceChatType } from '@/core/packet/entities/aiChat';
|
||||
|
||||
class GetAiVoice extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0X929B_0Resp> {
|
||||
constructor() {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import { NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from "@/core/packet/transformer/base";
|
||||
import { MiniAppReqParams } from "@/core/packet/entities/miniApp";
|
||||
import * as proto from '@/core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from '@/core/packet/transformer/base';
|
||||
import { MiniAppReqParams } from '@/core/packet/entities/miniApp';
|
||||
|
||||
class GetMiniAppAdaptShareInfo extends PacketTransformer<typeof proto.MiniAppAdaptShareInfoResp> {
|
||||
constructor() {
|
||||
@@ -23,24 +23,24 @@ class GetMiniAppAdaptShareInfo extends PacketTransformer<typeof proto.MiniAppAda
|
||||
templateType: req.templateType,
|
||||
businessType: req.businessType,
|
||||
picUrl: req.picUrl,
|
||||
vidUrl: "",
|
||||
vidUrl: '',
|
||||
jumpUrl: req.jumpUrl,
|
||||
iconUrl: req.iconUrl,
|
||||
verType: req.verType,
|
||||
shareType: req.shareType,
|
||||
versionId: req.versionId,
|
||||
withShareTicket: req.withShareTicket,
|
||||
webURL: req.webUrl ?? "",
|
||||
webURL: req.webUrl ?? '',
|
||||
appidRich: Buffer.alloc(0),
|
||||
template: {
|
||||
templateId: "",
|
||||
templateData: ""
|
||||
templateId: '',
|
||||
templateData: ''
|
||||
},
|
||||
field20: ""
|
||||
field20: ''
|
||||
}
|
||||
});
|
||||
return {
|
||||
cmd: "LightAppSvc.mini_app_share.AdaptShareInfo",
|
||||
cmd: 'LightAppSvc.mini_app_share.AdaptShareInfo',
|
||||
data: PacketHexStrBuilder(data)
|
||||
};
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user