Compare commits

..

492 Commits

Author SHA1 Message Date
手瓜一十雪
e3eb129a52 Update ffmpeg native binaries for all platforms
Replaces ffmpegAddon binaries for Darwin ARM64, Linux ARM64, Linux x64, and Windows x64 with new versions. Ensures compatibility and includes latest native changes.
2025-11-07 17:04:29 +08:00
手瓜一十雪
7654e9f2bb Update ffmpeg native binaries for all platforms
Replaces ffmpegAddon binaries for Darwin ARM64, Linux ARM64, Linux x64, and Windows x64. Ensures compatibility and includes latest native code updates.
2025-11-07 12:54:36 +08:00
Mlikiowa
a60c03f42f release: v4.9.26 2025-11-06 15:15:20 +00:00
手瓜一十雪
60aae228a1 feat: add Node NapCat Test 2025-11-06 17:57:57 +08:00
手瓜一十雪
b1417f9b56 feat: support node test 2025-11-06 10:57:54 +08:00
手瓜一十雪
eeeaddbb60 Remove development guide from README
Deleted the section detailing code checks and TypeScript error notes prior to code submission. This streamlines the README and removes internal development instructions.
2025-11-05 12:24:21 +08:00
Mlikiowa
b1109022bb release: v4.9.25 2025-11-04 13:55:32 +00:00
手瓜一十雪
1807789511 Add send and recv mappings for 3.2.21-41857-arm64
Updated napi2native.json to include the 'send' and 'recv' addresses for the 3.2.21-41857-arm64 version, which were previously missing.
2025-11-04 21:55:09 +08:00
手瓜一十雪
49a5b631c2 Add support for 41857 app versions and update mappings
Added new entries for version 41857 in appid.json, napi2native.json, and packet.json for Windows, Linux, and Mac platforms. Updated mappings for send/recv addresses to support the latest application versions.
2025-11-04 21:23:41 +08:00
Mlikiowa
e7aaec81e2 release: v4.9.24 2025-11-04 09:53:36 +00:00
phelogges
05f8e8f3c3 fix: 修复了Unix终端打开失败的bug (#1355)
Bug复现:
mlikiowa/napcat-docker:v4.9.23,登陆账号后,在WebUI中打开系统终端失败,查看容器日志报错如下
Failed to create terminal: TypeError: Cannot read properties of undefined (reading 'fork')
    at new UnixTerminal (file:///app/napcat/napcat.mjs:67721:22)
    at spawn (file:///app/napcat/napcat.mjs:67873:10)
    at TerminalManager.createTerminal (file:///app/napcat/napcat.mjs:67963:17)
    at CreateTerminalHandler (file:///app/napcat/napcat.mjs:68069:36)
    at Layer.handleRequest (/app/napcat/node_modules/router/lib/layer.js:152:17)
    at next (/app/napcat/node_modules/router/lib/route.js:157:13)
    at Route.dispatch (/app/napcat/node_modules/router/lib/route.js:117:3)
    at handle (/app/napcat/node_modules/router/index.js:435:11)
    at Layer.handleRequest (/app/napcat/node_modules/router/lib/layer.js:152:17)
    at /app/napcat/node_modules/router/index.js:295:15

定位到源码https://github.com/NapNeko/NapCatQQ/blob/main/src/pty/prebuild-loader.ts#L5
注意到源码中的pty.node路径与容器中实际不符,修改为正确的路径

验证测试:
笔者没有重新构建,而是保持代码逻辑,反过来将pty.node的路径复制到代码中要求的位置,测试发现bug修复

Extra:
注意到native模块中不止有pty模块,还有ffmpeg等其他模块,笔者没有继续看其他模块的加载情况了,如有必要可能需要确认一并load路径
2025-11-04 17:51:46 +08:00
Mlikiowa
e760876470 release: v4.9.23 2025-11-03 15:18:57 +00:00
手瓜一十雪
e0ec4d4ebb Refactor busiId type and comparisons in group API
Changed the type of jsonGrayTipElement.busiId to always be a string in element types. Updated related group API logic to compare busiId as a string directly, improving type consistency and reducing unnecessary conversions.
2025-11-03 23:18:34 +08:00
Mlikiowa
79fa0ade0d release: v4.9.22 2025-11-03 15:13:03 +00:00
手瓜一十雪
652b5d6118 Enhance GrayTip JSON event handling and types
Added the XmlToJsonParam interface and extended the GrayTipElement type to support additional JSON event fields. Updated group API logic to handle busiId as string or number and improved event type checks for robustness.
2025-11-03 23:11:48 +08:00
手瓜一十雪
f4dedf4803 Refactor Store to use per-key timers for expiration
Simplifies the Store implementation by removing batch expiration scanning and using per-key setTimeout timers for key expiration. This change improves code clarity and ensures more precise key expiration handling.
2025-11-03 17:06:44 +08:00
时瑾
06f6a542f5 refactor: 优化eslint配置,提升代码质量 (#1341)
* feat: 统一并标准化eslint

* lint: napcat.webui

* lint: napcat.webui

* lint: napcat.core

* build: fix

* lint: napcat.webui

* refactor: 重构eslint

* Update README.md
2025-11-03 16:30:45 +08:00
Mlikiowa
d5b8f886d6 release: v4.9.21 2025-11-02 13:33:28 +00:00
手瓜一十雪
97b6dccc30 fix: 修复 linux 音频转换问题 2025-11-02 21:33:03 +08:00
Mlikiowa
a755487e22 release: v4.9.20 2025-11-02 03:30:09 +00:00
手瓜一十雪
5407392f08 Update appid, packet, and napi2native configs for 41785
Added new entries for version 41785 in appid.json, napi2native.json, and packet.json to support updated app and protocol versions. Also updated the napi2native.darwin.arm64.node binary to match the new version.
2025-11-02 11:29:45 +08:00
Mlikiowa
3359c5ded9 release: v4.9.19 2025-11-01 15:23:54 +00:00
手瓜一十雪
caaf8be3b2 Refactor PCM conversion to return result and sample rate
Updated the FFmpeg adapter interfaces and implementations so that PCM conversion methods now return an object containing the conversion result and sample rate, instead of a Buffer. Adjusted audio processing logic to accommodate this change and improved error logging. Updated native ffmpeg addon binaries.
2025-11-01 23:23:15 +08:00
手瓜一十雪
28ce5d3cb4 Add storeID and otherBusinessInfo to PttElement
Extended the PttElement interface and related code to include storeID and otherBusinessInfo fields, supporting additional metadata for PTT elements. Also fixed minor formatting issues in function parameter spacing.
2025-11-01 22:49:21 +08:00
手瓜一十雪
3dd56c711e Throw original error in sendMsg method
Replaces the re-throwing of a new Error with the original error object in the sendMsg method, preserving the original error stack and type for better debugging.
2025-11-01 22:32:26 +08:00
Mlikiowa
511fb82ce0 release: v4.9.18 2025-11-01 13:54:19 +00:00
手瓜一十雪
1e5524a009 Add message sequence support for emoji like events
Updated group API and OB11GroupMsgEmojiLikeEvent to include an optional message sequence (msgSeq/messageSeq) parameter. This allows more precise identification of messages when handling emoji like events in group chats.
2025-11-01 21:53:53 +08:00
手瓜一十雪
d5e6afc7b9 feat: 支持不是自己的表情回应 2025-11-01 21:00:34 +08:00
Mlikiowa
91e633b0fb release: v4.9.17 2025-11-01 10:43:55 +00:00
手瓜一十雪
397b9880b9 feat: arm64 enable neon 2025-11-01 18:43:30 +08:00
Mlikiowa
54eb26ba67 release: v4.9.16 2025-11-01 08:19:18 +00:00
手瓜一十雪
355f7fb4a0 Update napi2native mappings for new and existing versions
Added mappings for versions 6.9.82-40824-arm64 and 6.9.82-40768-arm64. Updated 'send' addresses for versions 6.9.82-40990-arm64 and 6.9.83-41679-arm64 to reflect new offsets.
2025-11-01 16:18:51 +08:00
手瓜一十雪
325c455e38 Update native Linux ARM64 binary
Replaced the napi2native.linux.arm64.node binary with a new version. This may include bug fixes, performance improvements, or compatibility updates for ARM64 Linux systems.
2025-11-01 16:14:18 +08:00
Mlikiowa
a7a9792efe release: v4.9.15 2025-11-01 06:08:11 +00:00
时瑾
4bab93e545 fix: close #1334 2025-11-01 14:07:41 +08:00
手瓜一十雪
3c60997b1c Improve error handling in NCoreInitShell session creation
Refactored session creation logic to add nested try-catch blocks. Now logs specific errors for both StartupSession and Session creation failures, and throws if session creation fails.
2025-11-01 11:11:10 +08:00
Mlikiowa
bf684d9166 release: v4.9.14 2025-10-31 08:53:45 +00:00
手瓜一十雪
5c4ee30f37 Update ffmpeg native binaries for Linux
Replaced ffmpegAddon.linux.arm64.node and ffmpegAddon.linux.x64.node with new versions. This likely includes bug fixes, performance improvements, or compatibility updates for the native ffmpeg bindings.
2025-10-31 16:53:11 +08:00
Mlikiowa
346374b442 release: v4.9.13 2025-10-31 07:15:49 +00:00
手瓜一十雪
cc8a387bde re: glibc 2025-10-31 15:12:46 +08:00
Mlikiowa
962685ade6 release: v4.9.11 2025-10-31 06:43:40 +00:00
手瓜一十雪
36fdaac406 feat: 适配41697 2025-10-31 14:42:46 +08:00
Mlikiowa
db16db911b release: v4.9.10 2025-10-31 04:38:12 +00:00
手瓜一十雪
bb84dfcc27 Add new version mappings to external JSON configs
Updated appid.json, napi2native.json, and packet.json to include new version entries for 6.8.83-41679, 6.9.82-40990-arm64, and 6.9.83-41679-arm64. These changes add support for additional client versions and architectures.
2025-10-31 12:37:42 +08:00
手瓜一十雪
3e71e541e6 Update qqnt.json to version 9.9.22-40990
Bumped version, verHash, linuxVersion, linuxVerHash, and buildVersion fields in qqnt.json to reflect the new release 9.9.22-40990.
2025-10-31 00:11:28 +08:00
Mlikiowa
bcc856d583 release: v4.9.9 2025-10-30 16:08:57 +00:00
手瓜一十雪
1eaf480a7d Add napi2native mapping for 9.9.23-41679-x64
Introduced new send and recv address mappings for version 9.9.23-41679-x64 in napi2native.json.
2025-10-31 00:08:26 +08:00
手瓜一十雪
86123af7fc Update session instantiation and appid format
Changed session creation to use the constructor instead of the create() method in base.ts. Updated the appid.json key format from '9.9.23.41679' to '9.9.23-41679'. Added a constructor signature to NodeIQQNTWrapperSession interface. Updated NapCatWinBootHook.dll binary.
2025-10-31 00:06:31 +08:00
Mlikiowa
c3cd2aaf89 release: v4.9.8 2025-10-30 14:28:31 +00:00
手瓜一十雪
599afdc7ba Add entries for version 9.9.23.41679 in appid and packet
Updated appid.json and packet.json to include new entries for version 9.9.23.41679, specifying appid, qua, and packet send/recv values for the new version.
2025-10-30 22:24:05 +08:00
手瓜一十雪
ffe54af8d9 Fix addon path resolution and error handling
Corrects the construction of the ffmpeg addon filename and improves error handling when the addon is not found. Also simplifies the isAvailable method by removing redundant existence checks.
2025-10-30 22:13:57 +08:00
Mlikiowa
a7b30ef844 release: v4.9.7 2025-10-30 13:56:19 +00:00
手瓜一十雪
50b8cb14dc Comment out packet logging in initialization
Disabled the console logging of all packets in both NCoreInitFramework and NCoreInitShell by commenting out the nativePacketHandler.onAll debug statements. This reduces console noise during normal operation.
2025-10-30 21:55:50 +08:00
Mlikiowa
0b18f868bc release: v4.9.6 2025-10-30 13:49:11 +00:00
手瓜一十雪
c9acb9ae28 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-10-30 21:44:59 +08:00
手瓜一十雪
2e1506a05d Update native binaries for all major platforms
Rebuilt and replaced the napi2native.node binaries for Darwin ARM64, Linux ARM64, Linux x64, and Windows x64. This likely includes bug fixes, performance improvements, or compatibility updates in the native module.
2025-10-30 21:44:43 +08:00
Mlikiowa
b35283f970 release: v4.9.4 2025-10-30 13:40:21 +00:00
手瓜一十雪
54ac072bfb Replace console.error with console.log in error handler
Changed error logging in FFmpegAddonAdapter from console.error to console.log when addon loading fails.
2025-10-30 21:39:17 +08:00
pk5ls20
58cefb9cdc fix: napi2native linux offset 2025-10-30 21:00:35 +08:00
手瓜一十雪
be4344634d Add mappings for 3.2.20 versions in napi2native.json
Added send and recv address mappings for 3.2.20-x64 and 3.2.20-arm64 builds to support additional versions in napi2native.json.
2025-10-30 12:35:15 +08:00
手瓜一十雪
2da5d242f7 Refactor addon path resolution and rename Windows addon
Simplifies the logic for resolving the ffmpeg addon path by dynamically constructing the filename from process.platform and process.arch. Also renames the Windows x64 addon file to ffmpegAddon.win32.x64.node for consistency.
2025-10-30 11:29:54 +08:00
手瓜一十雪
fbd00b2576 feat: 9.9.22-40824 & 9.9.22-40768 2025-10-30 11:07:51 +08:00
手瓜一十雪
72548c9575 feat: packet能力增强 2025-10-30 11:05:19 +08:00
手瓜一十雪
003f3e946d refactor: 规范化 2025-10-30 11:01:45 +08:00
手瓜一十雪
dc51d01351 feat: raw包能力增强完成 2025-10-30 10:58:02 +08:00
手瓜一十雪
c5db525f4a refactor: 重构目录删除旧支持 2025-10-30 10:08:32 +08:00
手瓜一十雪
c1377e6de7 Remove 'bmp24' argument from getVideoInfo call
Updated the extractThumbnail method to call addon.getVideoInfo without the 'bmp24' argument, aligning with the updated addon API.
2025-10-30 09:23:25 +08:00
手瓜一十雪
d2c4f425c7 Remove baseClient.ts from packet client module
Deleted the src/core/packet/client/baseClient.ts file, which contained the PacketClient class and related interfaces. This may be part of a refactor or cleanup to remove unused or redundant code.
2025-10-30 09:11:58 +08:00
手瓜一十雪
803b1a6c77 feat: ffmpeg enhance for native node addon 2025-10-30 09:06:48 +08:00
手瓜一十雪
9a35ee9cd1 refactor: 大幅度调整send 2025-10-29 21:42:19 +08:00
手瓜一十雪
458d22223c fix: 简化代码 2025-10-29 21:37:55 +08:00
手瓜一十雪
4ed61136b2 fix: getQQBuildStr 2025-10-29 21:35:00 +08:00
手瓜一十雪
1445a29e15 Remove debug log for process PID in napcat.ts
Eliminated an unnecessary console.log statement that printed the process PID. This cleans up the output and removes leftover debugging code.
2025-10-29 21:25:24 +08:00
手瓜一十雪
55ef040852 Remove baseClient and simplify packet client selection
Deleted baseClient.ts and moved its logic into nativeClient.ts, making NativePacketClient a standalone class. Refactored PacketClientContext to always use NativePacketClient, removing support for multiple packet backends and related selection logic. Updated binary for napi2native.win32.x64.node.
2025-10-29 21:24:28 +08:00
手瓜一十雪
0c88319248 feat: Add FFmpeg native addon and TypeScript definitions
Introduced FFmpeg Node.js native addon binaries for multiple platforms (darwin-arm64, linux-arm64, linux-x64, win-x64) and added TypeScript type definitions for the addon interface, including video info extraction, duration detection, audio conversion, and PCM decoding.
2025-10-29 21:14:16 +08:00
手瓜一十雪
6778bd69de feat: new napcat-4.9.0-beta 2025-10-29 20:35:58 +08:00
手瓜一十雪
f9c9b3a852 Add native module loading and improve logging
Loaded a native module in NTQQGroupApi and added test calls to sendSsoCmdReqByContend with different parameter types. Changed fileLog default to true in config. Enhanced NativePacketClient with detailed send/receive logging. Updated NodeIKernelMsgService to accept unknown type for sendSsoCmdReqByContend param. Added process PID logging in napcat shell.
2025-10-26 20:05:21 +08:00
手瓜一十雪
9ce9e46c57 Merge branch 'main' into pr/1303 2025-10-25 16:17:51 +08:00
Mlikiowa
656bde25c8 release: v4.8.124 2025-10-22 13:12:25 +00:00
手瓜一十雪
791a360f28 Add 40990 version entries to appid and offset configs
Added new entries for version 40990 for multiple platforms in appid.json and offset.json, including appid, qua, and send/recv offsets. This update supports the latest client versions.
2025-10-22 21:09:37 +08:00
Mlikiowa
376245b749 release: v4.8.123 2025-10-21 15:06:10 +00:00
时瑾
d3e3527c2b fix: go-cqhttp 上传接口返回 file_id (UploadGroupFile, UploadPrivateFile) 2025-10-21 22:49:45 +08:00
Mlikiowa
5b78dfbd5a release: v4.8.122 2025-10-16 13:28:29 +00:00
手瓜一十雪
1e461aae3c Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-10-16 21:13:54 +08:00
手瓜一十雪
42abc2b9cb fix: #1286 2025-10-16 21:13:50 +08:00
Mlikiowa
4f8c320658 release: v4.8.121 2025-10-15 14:31:35 +00:00
手瓜一十雪
cbacc89907 Add appid and offset entries for version 40824
Added new entries for versions 3.2.20-40824, 9.9.22-40824, and 6.9.82-40824 in appid.json and corresponding offset values for x64 and arm64 architectures in offset.json.
2025-10-15 22:31:08 +08:00
Mlikiowa
8e6da5e2d0 release: v4.8.120 2025-10-14 12:33:43 +00:00
手瓜一十雪
02980c4d1a feat: Update QQ version data and add macOS ARM64 native module
Updated qqnt.json, appid.json, and offset.json to support QQ version 9.9.22-40768 and related Linux/macOS versions. Modified calcQQLevel in helper.ts to remove penguinNum from calculation. Added MoeHoo.darwin.arm64.new.node for macOS ARM64 support and updated LiteLoaderWrapper.zip binary.
2025-10-14 20:32:41 +08:00
时瑾
0129188739 fix: #1315 2025-10-14 09:38:18 +08:00
时瑾
98ef642cd1 feat: 取消群精华接口支持传递原始参数
- 1. onebot v11标准: 传递message_id
- 2. 通过官方http接口获取到的group_id、msg_random、msg_seq

二者任选其一
2025-10-12 21:13:56 +08:00
手瓜一十雪
32e886e53b Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-10-12 20:38:38 +08:00
风小七
315d847f06 Update helper.ts (#1311)
修复256级以后等级清零的问题。
2025-10-12 20:36:31 +08:00
手瓜一十雪
381d320967 feat: Add image and record stream download actions
Introduces BaseDownloadStream as a shared base class for streaming file downloads. Adds DownloadFileImageStream and DownloadFileRecordStream for image and audio file streaming with support for format conversion. Refactors DownloadFileStream to use the new base class, and updates action registration and router to include the new actions.
2025-10-12 15:50:34 +08:00
Clansty
2f5b62decb fix: get_group_info ownerUin is "0" 2025-10-09 02:30:08 +08:00
Mlikiowa
2afdb2a0da release: v4.8.119 2025-10-03 04:36:00 +00:00
手瓜一十雪
5bfbf92c21 Update key for 9.9.22-40362 offsets in offset.json
Changed the key from '9.9.22-40362' to '9.9.22-40362-x64' to clarify architecture specificity in the offsets mapping.
2025-10-03 12:35:28 +08:00
Mlikiowa
a775a0dde9 release: v4.8.118 2025-10-03 04:34:56 +00:00
手瓜一十雪
d7f00c0594 Fix batch variable quoting and case consistency
Updated batch scripts to use proper variable quoting and consistent casing for 'QQPath'. This improves reliability when handling paths with spaces and ensures environment variable names are used consistently.
2025-10-03 12:34:29 +08:00
Mlikiowa
77c8f874b6 release: v4.8.117 2025-10-03 04:21:22 +00:00
手瓜一十雪
fb0a20919b Add support for version 9.9.22-40362
Updated appid.json and offset.json to include entries for version 9.9.22-40362, specifying the new appid, qua, and offset values for send and recv.
2025-10-03 12:20:21 +08:00
手瓜一十雪
0300ba4648 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-10-03 12:20:15 +08:00
时瑾
d472eee777 fix: Reset pagination when navigating between directories in file manager
Fix: Reset pagination when navigating between directories in file manager
2025-10-02 09:40:37 +08:00
copilot-swe-agent[bot]
41bd06e50a Fix: Reset pagination to page 1 when navigating directories
Co-authored-by: sj817 <74231782+sj817@users.noreply.github.com>
2025-10-02 01:16:14 +00:00
copilot-swe-agent[bot]
97334dfbf5 Initial plan 2025-10-02 01:10:22 +00:00
手瓜一十雪
e3d8c8e940 fix: #1260 2025-09-29 16:39:37 +08:00
手瓜一十雪
f2c62db76e Update README with new features in v4.8.115+
Added a section describing new features in version v4.8.115+, including Stream API support and recommendations to use string types for message_id, user_id, and group_id. Also explained the benefits of these changes for Docker, cross-device, large file transfers, and better compatibility with languages lacking large integer support.
2025-09-21 13:29:35 +08:00
手瓜一十雪
b1b051c4ce Update DeepWiki badge formatting in README
Reformatted the DeepWiki badge section in the README to match the table style used for other community links.
2025-09-20 16:45:27 +08:00
手瓜一十雪
a754b2ecc7 Add DeepWiki badge to README
Added a DeepWiki badge with a link to the project's DeepWiki page for increased visibility and resource access.
2025-09-20 16:44:57 +08:00
Mlikiowa
e0eb625b75 release: v4.8.116 2025-09-20 08:20:05 +00:00
手瓜一十雪
937be7678e Add file_retention parameter to upload test
Introduces the 'file_retention' field with a value of 30,000 to the upload test payload in OneBotUploadTester. This may be used to specify file retention duration in milliseconds.
2025-09-20 16:19:39 +08:00
手瓜一十雪
9b88946209 Add file retention option to UploadFileStream
Introduces a 'file_retention' parameter to control how long uploaded files are retained before automatic deletion. If set, files are deleted after the specified duration; otherwise, they are not automatically removed. This helps manage temporary file storage and cleanup.
2025-09-20 16:13:25 +08:00
Mlikiowa
74de3d9100 release: v4.8.115 2025-09-20 07:57:47 +00:00
手瓜一十雪
42d50014a1 Refactor event handling to use async/await across adapters
Updated all network adapters' onEvent methods to be asynchronous and return Promises, ensuring consistent async event emission and handling. Adjusted related methods and event emission logic to properly await asynchronous operations, improving reliability for streaming, plugin, HTTP, and WebSocket event flows. Also improved error handling and messaging in stream and WebSocket actions.
2025-09-20 15:55:37 +08:00
手瓜一十雪
a36ae315b0 Fix useStream variable scope in HTTP server adapter
Moved the declaration of useStream inside the action check to prevent referencing it when action is undefined.
2025-09-20 15:23:37 +08:00
手瓜一十雪
2161ec5fa7 feat: 标准化 2025-09-20 15:20:43 +08:00
手瓜一十雪
32bba007cd Add Readme for stream API actions
Introduces a Readme.txt file in the stream action directory, providing an overview and usage notes for stream-related API functions such as file upload, download, and cleanup.
2025-09-16 23:33:48 +08:00
Mlikiowa
84d3dc9f40 release: v4.8.114 2025-09-16 15:24:25 +00:00
手瓜一十雪
890d032794 Add streaming file upload and download actions
Introduces new OneBot actions for streaming file upload and download, including chunked file transfer with memory/disk management and SHA256 verification. Adds CleanStreamTempFile, DownloadFileStream, UploadFileStream, and TestStreamDownload actions, updates action routing and network adapters to support streaming via HTTP and WebSocket, and provides Python test scripts for concurrent upload testing.
2025-09-16 23:24:00 +08:00
837951602
66f30e1ebf 找不到类型时显式报错 (#1256) 2025-09-16 17:19:59 +08:00
MliKiowa
ada614d007 Fix link for QQ Group#2 in README
Updated the link for QQ Group#2 in the README.
2025-09-13 17:23:21 +08:00
MliKiowa
ea3ab7f13f Fix QQ Group links in README.md 2025-09-13 17:22:53 +08:00
MliKiowa
a5e4c24de3 Revise README for clarity and additional resources
Updated README with links to documentation and community guidelines.
2025-09-13 17:20:07 +08:00
Mlikiowa
bcc7d25b64 release: v4.8.113 2025-09-13 05:51:02 +00:00
手瓜一十雪
aae676fdc7 Update default host and token length in config
Changed the default host to '0.0.0.0' and increased the default token length from 8 to 12 characters in WebUiConfigSchema. Also removed unused getDefaultHost import and made minor formatting adjustments.
2025-09-13 13:49:18 +08:00
Mlikiowa
0e9aa43476 release: v4.8.112 2025-09-12 14:15:08 +00:00
时瑾
b2ff556aa6 fix: 更新默认主机地址获取逻辑,支持Docker环境 2025-09-12 21:56:12 +08:00
Mlikiowa
69c5b78678 release: v4.8.111 2025-09-12 11:19:26 +00:00
时瑾
8be7f74e9f fix: 移除 defaultToken 字段,彻底移除硬编码的默认密码,采用全随机密码 2025-09-12 18:50:21 +08:00
时瑾
a05150ebe1 fix(dos): 修复红红的ci 2025-09-12 15:36:30 +08:00
Mlikiowa
5e6b607ded release: v4.8.110 2025-09-11 05:15:29 +00:00
时瑾
df2dabfe76 refactor: 将默认密码相关逻辑重构为后端处理 (#1247)
* refactor: 将默认密码相关逻辑重构为后端处理

* refactor: 日志路由进行脱敏,生成随机密码使用node:crypto.randomBytes

* feat: 更新密码功能增强,添加新密码强度验证和旧密码检查

* feat: 给文件管理添加WebUI配置文件的脱敏处理和验证逻辑

* refactor: 优化网络显示卡片按钮样式和行为,调整按钮属性以提升用户体验

* feat: 增强路径处理逻辑,添加安全验证以防止路径遍历攻击

* feat: 增强文件路径处理逻辑,添加安全验证以防止路径遍历攻击,并优化查询参数提取

* feat: CodeQL不认可 受不了
2025-09-11 13:13:00 +08:00
手瓜一十雪
5e032fcc6a upate: package 2025-09-08 16:16:38 +08:00
Mlikiowa
44200a2208 release: v4.8.109 2025-09-08 08:11:05 +00:00
手瓜一十雪
e39bb05f01 fix: 多层解析 2025-09-08 16:05:10 +08:00
手瓜一十雪
677731dd70 Add qq-chat-exporter to recommended tools
Included qq-chat-exporter, a NapCat-based message export tool, in the list of recommended related projects in the README.
2025-09-07 15:20:17 +08:00
Mlikiowa
fa8e6f2c59 release: v4.8.108 2025-09-07 05:57:27 +00:00
手瓜一十雪
509b23ff04 Update Telegram link in About page
Changed the Telegram href in the About page from MelodicMoonlight to napcatqq to reflect the correct contact or channel.
2025-09-07 13:54:44 +08:00
Mlikiowa
cf1765f5a4 release: v4.8.107 2025-09-07 05:48:15 +00:00
手瓜一十雪
c541c7e257 Update Telegram link in README
Changed the Telegram badge and link from MelodicMoonlight to napcatqq for accuracy.
2025-09-07 13:47:23 +08:00
手瓜一十雪
298b8b71c8 Log WebUI panel URL on server start
Adds logging of the WebUI user panel URL with localhost address when the server starts. Also adjusts QQ login callback invocation in OneBot adapter for improved login flow.
2025-09-07 13:29:37 +08:00
手瓜一十雪
5c120a8231 Add WebUI token update and callback handling
Introduces callback mechanisms for WebUI token changes and QQ login status updates. The WebUI token is now updated and communicated via a callback after login, and related runtime and type definitions are extended to support these features. Also sets a static default token value in the config schema.
2025-09-06 18:15:35 +08:00
手瓜一十雪
88ee8f89fe Comment out documentation links in README
The section containing links to documentation and Telegram has been commented out in the README.md. This may be for temporary removal or future revision.
2025-09-06 13:13:14 +08:00
Mlikiowa
12b8130372 release: v4.8.106 2025-09-06 03:53:33 +00:00
手瓜一十雪
58332dad24 feat: 支持禁用webui 速率配置 禁用外网访问 禁用webui 2025-09-06 11:53:04 +08:00
手瓜一十雪
e97f3e1283 feat: 进一步提高密码安全性 2025-09-06 11:42:12 +08:00
手瓜一十雪
e406dca7ae feat: 禁止默认密码 2025-09-06 11:41:06 +08:00
手瓜一十雪
e4c1807f76 feat: 安全性提升 2025-09-06 11:32:09 +08:00
手瓜一十雪
f4412bb086 feat: 安全性提升 2025-09-06 11:23:09 +08:00
手瓜一十雪
27af8e52ac feat: 安全性提升 2025-09-06 10:49:29 +08:00
手瓜一十雪
4c9a220300 Add README for example plugin
Introduces installation instructions for the example plugin, detailing how to place the build output in the appropriate plugins directory.
2025-09-02 22:54:44 +08:00
手瓜一十雪
1fe822cd20 Update import paths and remove plugin tsconfig
Changed import paths in example-plugin to use alias '@' instead of relative paths. Deleted the example-plugin/tsconfig.json file, likely consolidating TypeScript configuration or relying on a root config.
2025-09-02 22:48:01 +08:00
手瓜一十雪
0ab8d025bf Declare config property in OB11PluginMangerAdapter
Adds an explicit declaration for the 'config' property in the OB11PluginMangerAdapter class to improve type safety and clarity.
2025-09-02 22:42:09 +08:00
Mlikiowa
a0f3d66607 release: v4.8.105 2025-09-02 14:33:33 +00:00
手瓜一十雪
06e7c3363a Add quick_reply support to message parsing
Introduces the quick_reply boolean option to message history schema and message parsing logic. Updates relevant functions to handle quick_reply, allowing for conditional behavior during message reply and segment parsing.
2025-09-02 22:33:05 +08:00
手瓜一十雪
4d200de6b7 Refactor packet client and update message history actions
Replaced LRUCache with Map for callback and event management in packet clients, and standardized callback hash usage. Updated GetFriendMsgHistory and GetGroupMsgHistory actions to use snake_case for payload keys. Modified OneBotMsgApi to support disabling URL retrieval for ptt elements via a new parameter.
2025-09-02 22:24:53 +08:00
手瓜一十雪
6200097f7c Add resource health management and enhance message parsing
Introduces a ResourceManager for health checking and retry logic in src/common/health.ts. Updates OneBot message parsing to support disabling URL fetching and multi-message parsing via new payload options. File, image, video, and ptt URL retrievals now use resource health management for improved reliability. Also refactors packet API to allow configurable timeout for FetchRkey.
#1220
2025-09-02 21:19:49 +08:00
手瓜一十雪
c7af0384fb Add plugin manager and example plugin system
Introduces a plugin manager (OB11PluginMangerAdapter) for dynamic plugin loading, initialization, and event handling. Adds an example plugin with configuration files and updates related code to support plugin directory detection and loading. Refactors plugin adapter logic for extensibility and modularity.
2025-09-02 20:42:54 +08:00
Mlikiowa
dc87615bd6 release: v4.8.104 2025-09-02 08:32:22 +00:00
手瓜一十雪
ff2cfcee97 Add appid and offset entries for new versions
Added appid and qua mappings for versions 3.2.19-39038 and 9.9.21-39038 in appid.json. Updated offset.json with send/recv offsets for these new versions and architectures (x64, arm64).
2025-09-02 16:31:24 +08:00
手瓜一十雪
f3c07ed8fc Add timeout parameter to file and packet API methods
Introduces an optional timeout parameter (defaulting to 20000ms) to various file and packet API methods for improved control over request duration. Updates all relevant method calls and internal usages to support the new timeout argument, including OneBot message API calls with a shorter timeout for file, video, and ptt URL retrieval.
2025-09-02 09:54:42 +08:00
手瓜一十雪
7ab44dcb34 Refactor video thumbnail generation logic
Moved video thumbnail generation to occur before custom thumbnail copying, ensuring fallback to default thumbnail only if video info retrieval fails. Also reordered rkeyManager URLs for consistency.
2025-09-02 09:35:56 +08:00
Mlikiowa
aa6699d06e release: v4.8.103 2025-08-31 13:32:47 +00:00
手瓜一十雪
3cb51a17a6 Add new appid and offset entries for version 38960
Updated appid.json and offset.json to include new entries for versions 3.2.19-38960 and 9.9.21-38960, supporting both x64 and arm64 architectures.
2025-08-31 21:32:15 +08:00
Mlikiowa
994e8ced3e release: v4.8.102 2025-08-26 06:59:25 +00:00
手瓜一十雪
75d26465f1 Add group album media actions and API integration
Introduces new OneBot actions for group album media: listing, commenting, liking, and deleting. Adds supporting API methods and data structures for album media operations in NTQQWebApi and NodeIKernelAlbumService. Updates action router and index to register new actions.
2025-08-26 14:58:11 +08:00
LgCookie
f5052935bd fix: special char of token in webui url should be url encoded (#1209) 2025-08-26 08:37:39 +08:00
Mlikiowa
84b89de2a6 release: v4.8.101 2025-08-25 11:05:45 +00:00
手瓜一十雪
c4f9c4f630 fix 2025-08-25 19:05:13 +08:00
手瓜一十雪
c213cd6c3a Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-08-25 19:04:31 +08:00
手瓜一十雪
9d22d6e3a0 Add NTQQ album list API and update related logic
Introduces getAlbumListByNTQQ to NTQQWebApi for retrieving group album lists via NTQQ. Updates NodeIKernelAlbumService interface for typed getAlbumList parameters and response. Refactors GetQunAlbumList action to use the new NTQQ API and return the correct album list format. Also fixes cookie and bkn usage in album-related methods for consistency.
2025-08-25 19:04:22 +08:00
Mlikiowa
a38419e3cb release: v4.8.100 2025-08-25 07:51:18 +00:00
手瓜一十雪
a64779684e fix #1171 && Improve message recall handling and cleanup
Changed recallMsg to return the result of the event call. Added a 5-second cache cleanup for recall events in DeleteMsg. Removed an unnecessary blank line in plugin.ts.
2025-08-25 15:50:05 +08:00
手瓜一十雪
ecd7012eee Remove redundant upload success log
Eliminated a logger statement that logged successful uploads in the NTQQWebApi class. This reduces unnecessary log output during chunked uploads.
2025-08-25 15:24:30 +08:00
手瓜一十雪
74a1011fcc Refactor Qun album image upload logic
Reworked the group album image upload process to use a new slice-based upload method, replacing the previous chunked upload implementation. Updated related interfaces and removed unused chunk upload code for improved maintainability and clarity.
2025-08-25 14:45:52 +08:00
手瓜一十雪
d4b0a4acca Simplify recall check for self-operated messages 2025-08-25 13:04:22 +08:00
手瓜一十雪
ac6e593315 Add cookie header to getAlbumList API request
The getAlbumList method in NTQQWebApi now includes a 'Cookie' header with skey, pskey, and uin for authentication. Updated GetQunAlbumList action to use the correct return type from NTQQWebApi.getAlbumList.
2025-08-24 20:39:07 +08:00
手瓜一十雪
b1e77b1658 feat: Add group album upload utilities and refactor API && close #1116
Introduces src/core/data/webapi.ts with utilities for chunked group album uploads, including session creation and chunk management. Refactors NTQQWebApi in webapi.ts to use these utilities, adds getAlbumList and uploadImageToQunAlbum methods, and improves upload logic for efficiency and maintainability.
2025-08-24 20:12:35 +08:00
手瓜一十雪
722c3554e9 feat: #1121 & Add cache cleaning for specific directories
Extended the CleanCache action to remove files from Pic, Ptt, Video, File, and log directories under the nt_data path. This improves cache management by ensuring these directories are also cleaned, with logging for successful and failed deletions. 增强缓存清理
2025-08-24 16:02:05 +08:00
手瓜一十雪
1d08966571 feat: #1179 2025-08-24 15:50:08 +08:00
手瓜一十雪
fb50ae7544 chore: Update LiteLoaderWrapper.zip binary && close #1169
Replaces the existing LiteLoaderWrapper.zip file with a new version. Details of the changes within the binary are not shown in this commit.
2025-08-24 15:18:49 +08:00
手瓜一十雪
ea695fc9e9 fix: #1171 2025-08-24 14:53:58 +08:00
Mlikiowa
5c6d1e6a14 release: v4.8.99 2025-08-24 06:02:30 +00:00
手瓜一十雪
b030c40853 feat: 38711 2025-08-24 14:02:03 +08:00
子寻
d6782c35e2 fix: /get_msg interface returns group type message with group_name. (#1205) 2025-08-23 11:38:24 +08:00
Mlikiowa
120e6db119 release: v4.8.98 2025-08-17 15:35:05 +00:00
手瓜一十雪
fa10f8ce19 Fix typo in getFullQQVersion method call
Corrects the method name from getFullQQVesion to getFullQQVersion in the WebUiDataRuntime.setQQVersion call to ensure proper retrieval of QQ version information.
2025-08-17 23:34:41 +08:00
Mlikiowa
31494b4687 release: v4.8.97 2025-08-17 14:59:12 +00:00
手瓜一十雪
857ed0f343 fix: error 2025-08-17 22:58:30 +08:00
837951602
8133ff08a7 fix: offset.json (#1193)
fix win add lin x64
2025-08-17 17:31:24 +08:00
Mlikiowa
2d315c4d8e release: v4.8.96 2025-08-15 11:17:15 +00:00
手瓜一十雪
505f7b6ac9 Update group.ts 2025-08-15 19:12:13 +08:00
手瓜一十雪
2735eb14bd fix: #1191 2025-08-15 19:12:04 +08:00
dependabot[bot]
7afbc95eda build(deps-dev): bump vite from 6.3.5 to 7.1.1 (#1183)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 7.1.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.1/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 19:09:03 +08:00
dependabot[bot]
91bb83d8c1 build(deps-dev): bump @typescript-eslint/parser from 8.38.0 to 8.39.0 (#1182)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.38.0 to 8.39.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.39.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 19:08:57 +08:00
dependabot[bot]
55550790e4 build(deps-dev): bump @eslint/js from 9.32.0 to 9.33.0 (#1185)
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.32.0 to 9.33.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.33.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.33.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 19:08:40 +08:00
dependabot[bot]
40221926a9 build(deps-dev): bump @sinclair/typebox from 0.34.35 to 0.34.38 (#1187)
Bumps [@sinclair/typebox](https://github.com/sinclairzx81/typebox) from 0.34.35 to 0.34.38.
- [Commits](https://github.com/sinclairzx81/typebox/compare/0.34.35...0.34.38)

---
updated-dependencies:
- dependency-name: "@sinclair/typebox"
  dependency-version: 0.34.38
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 19:08:30 +08:00
手瓜一十雪
d069374a95 Add explicit type annotations to AST traversal paths
Updated the performance monitor Vite plugin to add explicit type annotations to AST traversal callback parameters, improving type safety and clarity. Also removed a duplicate import in src/plugin/index.ts.
2025-08-15 19:02:53 +08:00
手瓜一十雪
1183fe2057 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-08-15 19:00:57 +08:00
手瓜一十雪
f4605d4f74 Add appid and offset entries for new versions
Added appid and qua for versions 9.9.21-38503 and 3.2.19-38503 in appid.json. Added send and recv offsets for 9.9.21-38503 in offset.json to support new application versions.
2025-08-15 18:57:43 +08:00
dependabot[bot]
3ce3fb685b build(deps-dev): bump @eslint/js from 9.31.0 to 9.32.0 (#1160)
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.31.0 to 9.32.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.32.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.32.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 20:39:28 +08:00
dependabot[bot]
07c2f7371f build(deps-dev): bump esbuild from 0.25.5 to 0.25.8 (#1159)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.25.5 to 0.25.8.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.5...v0.25.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 20:39:14 +08:00
囧囧JOJO
114aae98a9 feat: 添加可选参数 'count' 到 SetGroupAddRequest 以支持动态获取通知数量(#1113 补充) (#1146)
* feat: 添加可选参数 'count' 到 SetGroupAddRequest 以支持动态获取通知数量(#1113 补充)

* feat: 设置 SetGroupAddRequest 中 'count' 的默认值为 100

* Update src/onebot/action/group/SetGroupAddRequest.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-06 20:38:57 +08:00
dependabot[bot]
0bd6548f45 build(deps): bump ws from 8.18.2 to 8.18.3 (#1157)
Bumps [ws](https://github.com/websockets/ws) from 8.18.2 to 8.18.3.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.2...8.18.3)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.18.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 20:38:41 +08:00
dependabot[bot]
06c5b7807b build(deps-dev): bump @typescript-eslint/parser from 8.37.0 to 8.38.0 (#1158)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.37.0 to 8.38.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.38.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 20:38:15 +08:00
手瓜一十雪
e8ef08cae2 Pr enhance (#1176)
* fix

* Refactor PacketApi status checks and fix typos

Replaced all usages of PacketApi.available with PacketApi.packetStatus for more accurate status checking. Fixed a typo in getFullQQVesion to getFullQQVersion. Updated plugin_onmessage to fetch and parse messages for a user. These changes improve reliability and consistency in API status handling.

* Fix typo in getFullQQVersion method name

Corrected the method name from getFullQQVesion to getFullQQVersion in multiple locations to ensure consistency and prevent potential runtime errors.

* Remove performance CLI and demo, fix typos, update proto

Deleted the performance CLI and demo files. Fixed a typo in getFullQQVesion to getFullQQVersion across multiple files. Changed the 'time' field type from UINT32 to UINT64 in Oidb.0x9067_202 proto. Commented out performanceMonitorPlugin in vite.config.ts. Removed an unimplemented log statement in NodeIKernelBuddyListener.

* Comment out default plugin adapter registration

The default registration of OB11PluginAdapter in NapCatOneBot11Adapter was commented out, likely to prevent automatic plugin loading or to allow for more flexible plugin management. Also, removed an unnecessary blank line in the plugin_onmessage function.

* fix

* Add shell-analysis mode with performance monitoring

Introduces a new .env.shell-analysis file and a dev:shell-analysis npm script for building in shell-analysis mode. Updates vite.config.ts to support the new mode, enabling the performance monitor plugin with an updated exclude list. Also extends the plugin's exclude patterns to filter out 'packet' files.

* Delete performance-api.ts

* Add commented export for performance-monitor

Added a commented-out export statement for '@/common/performance-monitor' in napcat.ts, possibly for future use or reference. No functional changes to the file.
2025-08-06 20:37:45 +08:00
手瓜一十雪
0d251a9343 Remove performance CLI and demo, fix typos, update proto
Deleted the performance CLI and demo files. Fixed a typo in getFullQQVesion to getFullQQVersion across multiple files. Changed the 'time' field type from UINT32 to UINT64 in Oidb.0x9067_202 proto. Commented out performanceMonitorPlugin in vite.config.ts. Removed an unimplemented log statement in NodeIKernelBuddyListener.
2025-08-06 19:57:36 +08:00
手瓜一十雪
69a19b0e32 Fix typo in getFullQQVersion method name
Corrected the method name from getFullQQVesion to getFullQQVersion in multiple locations to ensure consistency and prevent potential runtime errors.
2025-08-06 19:32:14 +08:00
手瓜一十雪
1b0b5f3494 Refactor PacketApi status checks and fix typos
Replaced all usages of PacketApi.available with PacketApi.packetStatus for more accurate status checking. Fixed a typo in getFullQQVesion to getFullQQVersion. Updated plugin_onmessage to fetch and parse messages for a user. These changes improve reliability and consistency in API status handling.
2025-08-06 19:31:24 +08:00
手瓜一十雪
5abdc8c538 fix 2025-08-06 18:43:44 +08:00
Mlikiowa
9f0ba6d385 release: v4.8.95 2025-07-26 12:19:32 +00:00
手瓜一十雪
7863a0f45f Add new appid and offset entries for version 37625
Updated appid.json and offset.json to include new entries for versions 9.9.20-37625 and 3.2.18-37625, supporting both x64 and arm64 architectures.
2025-07-26 20:19:04 +08:00
Mlikiowa
ef4c2a935c release: v4.8.94 2025-07-21 11:49:43 +00:00
手瓜一十雪
40d2e948e4 feat: 添加新的appid和offset配置以支持版本9.9.20-37475和3.2.18-37475 2025-07-21 19:48:59 +08:00
dependabot[bot]
3de4e905d3 build(deps-dev): bump eslint-plugin-import from 2.31.0 to 2.32.0 (#1123)
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.31.0 to 2.32.0.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.31.0...v2.32.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-import
  dependency-version: 2.32.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 手瓜一十雪 <nanaeonn@outlook.com>
2025-07-21 19:19:51 +08:00
dependabot[bot]
655f4e199c build(deps-dev): bump eslint-import-resolver-typescript (#1124)
Bumps [eslint-import-resolver-typescript](https://github.com/import-js/eslint-import-resolver-typescript) from 4.4.3 to 4.4.4.
- [Release notes](https://github.com/import-js/eslint-import-resolver-typescript/releases)
- [Changelog](https://github.com/import-js/eslint-import-resolver-typescript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-import-resolver-typescript/compare/v4.4.3...v4.4.4)

---
updated-dependencies:
- dependency-name: eslint-import-resolver-typescript
  dependency-version: 4.4.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-21 19:18:22 +08:00
dependabot[bot]
13c3d3a2fb build(deps-dev): bump @eslint/js from 9.30.1 to 9.31.0 (#1125)
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.30.1 to 9.31.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.31.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.31.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-21 19:18:10 +08:00
dependabot[bot]
db2dca45f6 build(deps-dev): bump @typescript-eslint/parser from 8.35.1 to 8.37.0 (#1140)
---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.37.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-21 19:17:45 +08:00
手瓜一十雪
7330a05c78 fix 2025-07-11 19:28:01 +08:00
Mlikiowa
a39c932868 release: v4.8.93 2025-07-09 11:21:08 +00:00
dependabot[bot]
a2c24c9197 build(deps-dev): bump @eslint/js from 9.28.0 to 9.30.1 (#1110)
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.28.0 to 9.30.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.30.1/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.30.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-09 19:11:09 +08:00
dependabot[bot]
5c3efc681f build(deps-dev): bump @eslint/compat from 1.3.0 to 1.3.1 (#1099)
Bumps [@eslint/compat](https://github.com/eslint/rewrite/tree/HEAD/packages/compat) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/packages/compat/CHANGELOG.md)
- [Commits](https://github.com/eslint/rewrite/commits/compat-v1.3.1/packages/compat)

---
updated-dependencies:
- dependency-name: "@eslint/compat"
  dependency-version: 1.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-09 19:10:54 +08:00
dependabot[bot]
e70d2bd708 build(deps-dev): bump typescript-eslint from 8.34.0 to 8.35.1 (#1112)
---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.35.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-09 19:10:14 +08:00
dependabot[bot]
cf75a961fb build(deps-dev): bump @rollup/plugin-typescript from 12.1.2 to 12.1.4 (#1098)
Bumps [@rollup/plugin-typescript](https://github.com/rollup/plugins/tree/HEAD/packages/typescript) from 12.1.2 to 12.1.4.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/typescript/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/typescript-v12.1.4/packages/typescript)

---
updated-dependencies:
- dependency-name: "@rollup/plugin-typescript"
  dependency-version: 12.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-09 19:10:02 +08:00
手瓜一十雪
159f317071 Add support for 3.2.18-37051 and 9.9.20-37051 versions
Updated appid.json and offset.json to include new entries for versions 3.2.18-37051 and 9.9.20-37051, including their respective app IDs, QUA values, and offset mappings for x64 and arm64 architectures.
2025-07-09 19:06:47 +08:00
Mlikiowa
713eef592a release: v4.8.92 2025-07-07 12:47:57 +00:00
囧囧JOJO
cf03ad8fd9 feat: 向 /get_system_msg 添加可选参数 'count' (#1113)
* feat: Add the optional parameter "count" to /get_system_msg

* Refactor GetGroupSystemMsg to use TypeBox schema

Introduced TypeBox for payload validation in GetGroupSystemMsg, replacing manual count handling with a schema-based approach. Updated the handler to use the new payload type and schema, improving type safety and input validation.

---------

Co-authored-by: 手瓜一十雪 <nanaeonn@outlook.com>
2025-07-07 20:46:26 +08:00
Mlikiowa
0c0b27901a release: v4.8.91 2025-07-07 12:41:15 +00:00
手瓜一十雪
137fe3c8f2 feat: 37012 2025-07-07 20:40:36 +08:00
Mlikiowa
d96174076a release: v4.8.90 2025-06-29 02:01:31 +00:00
手瓜一十雪
6d5662d96e feat: Add new Linux native modules for arm64 and x64
Added MoeHoo.linux.arm64.new.node and MoeHoo.linux.x64.new.node binaries to support native packet functionality on both ARM64 and x64 Linux platforms.
2025-06-29 09:58:32 +08:00
Mlikiowa
57abd47d99 release: v4.7.85 2025-06-26 10:35:59 +00:00
手瓜一十雪
5092b3d791 fix: package 2025-06-26 18:35:12 +08:00
Mlikiowa
649409d1be release: v4.7.81 2025-06-26 10:32:56 +00:00
手瓜一十雪
8f549d896a feat: package 2025-06-26 18:32:31 +08:00
Mlikiowa
a1359ddbb5 release: v4.7.80 2025-06-26 10:30:32 +00:00
手瓜一十雪
304a0dda3e feat: 初步验证win 36580 2025-06-26 18:30:06 +08:00
手瓜一十雪
fff9c4a4d8 feat: 36580 2025-06-26 17:06:37 +08:00
Wang Zeng
2c76102fc4 ci: dispatch docker build workflow after release (#1078) 2025-06-15 23:26:17 +08:00
手瓜一十雪
f576cd9417 fix: type 2025-06-13 16:58:05 +08:00
时瑾
9cfd224b74 fix: 优化get_group_ignored_notifies接口返回值 2025-06-12 20:14:11 +08:00
时瑾
c12f8de8b4 feat: get_collection_list 2025-06-12 13:28:31 +08:00
时瑾
ed9a7c52e2 feat: get_group_ignore_add_request 2025-06-12 13:23:22 +08:00
Mlikiowa
38fcaaa28b release: v4.7.78 2025-06-12 04:30:05 +00:00
手瓜一十雪
5317a1c1a9 fix: 35951 2025-06-12 12:29:14 +08:00
时瑾
4bc5933ea2 fix: 修正部分接口的参数、返回值,提高兼容性 (#1072)
* fix: 修正`get_group_system_msg` `get_group_honor_info`接口返回值 提升兼容性

* fix: `create_group_file_folder` 接口兼容性提升
2025-06-11 12:37:29 +08:00
Mlikiowa
6a6bd33fe5 release: v4.7.77 2025-06-10 06:28:20 +00:00
手瓜一十雪
8256942a3d fix: #1051 2025-06-10 14:27:50 +08:00
手瓜一十雪
697632eee8 feat: 35951 2025-06-10 13:19:19 +08:00
手瓜一十雪
6bbf5b254d fix: #1049 2025-06-10 13:05:36 +08:00
手瓜一十雪
5831898c4a fix: #1058 2025-06-10 12:54:29 +08:00
dependabot[bot]
2cc413bec1 build(deps-dev): bump multer from 1.4.5-lts.2 to 2.0.1 (#1070)
Bumps [multer](https://github.com/expressjs/multer) from 1.4.5-lts.2 to 2.0.1.
- [Release notes](https://github.com/expressjs/multer/releases)
- [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/expressjs/multer/compare/v1.4.5-lts.2...v2.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 12:48:39 +08:00
时瑾
0af36e89d9 fix: 转发消息接口返回值兼容gocq (#1066) 2025-06-09 10:02:27 +08:00
837951602
b2c0f5d2e5 /get_group_system_msg description (#1064) 2025-06-08 10:38:32 +08:00
手瓜一十雪
80b74c7da9 Merge pull request #1054 from NapNeko/dependabot/npm_and_yarn/file-type-21.0.0
build(deps-dev): bump file-type from 20.5.0 to 21.0.0
2025-06-06 11:53:31 +08:00
手瓜一十雪
f14f13b158 build(deps-dev): bump esbuild from 0.25.4 to 0.25.5 (#1056)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.25.4 to 0.25.5.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-06 11:53:06 +08:00
lzw
9dda00b6fa chore: add host in listen log
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-06-05 12:59:01 +08:00
Lan Zongwei
a29debb738 fix: fix missing host in onebot http-server listen 2025-06-05 12:59:01 +08:00
dependabot[bot]
b990fc43df build(deps-dev): bump esbuild from 0.25.4 to 0.25.5
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.25.4 to 0.25.5.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 09:14:28 +00:00
dependabot[bot]
915e9552ee build(deps-dev): bump file-type from 20.5.0 to 21.0.0
Bumps [file-type](https://github.com/sindresorhus/file-type) from 20.5.0 to 21.0.0.
- [Release notes](https://github.com/sindresorhus/file-type/releases)
- [Commits](https://github.com/sindresorhus/file-type/compare/v20.5.0...v21.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 09:09:48 +00:00
Mlikiowa
c522e0a386 release: v4.7.76 2025-05-29 13:58:02 +00:00
手瓜一十雪
c9cc08a9ba fix: #1048 2025-05-29 21:15:07 +08:00
手瓜一十雪
66e1b1662f fix: 支持registerCallback 2025-05-29 20:45:35 +08:00
手瓜一十雪
9372e83bd8 feat: nativeLoader功能预备 2025-05-29 14:39:09 +08:00
Mlikiowa
b38a240dbb release: v4.7.75 2025-05-26 12:19:44 +00:00
手瓜一十雪
76b9506395 fix: #1043 2025-05-26 19:58:50 +08:00
手瓜一十雪
f1cf636aa2 Merge pull request #1041 from Neboer/main
允许使用环境变量指定napcat工作路径。
2025-05-26 14:41:01 +08:00
Mlikiowa
312dcd0e13 release: v4.7.74 2025-05-26 05:57:44 +00:00
手瓜一十雪
42c2419613 Revert "fix: #1038"
This reverts commit 4e7c96634c.
2025-05-26 13:56:48 +08:00
手瓜一十雪
8f7f748e82 Revert "fix: #1039"
This reverts commit 1eda3f2e33.
2025-05-26 13:56:14 +08:00
Neboer
7ad3bad1be 修改环境变量名字NAPCAT_WRITEPATH为NAPCAT_WORKDIR 2025-05-26 05:36:07 +00:00
Neboer
5cd682e69f 允许使用NAPCAT_WRITEPATH环境变量指定napcat工作路径。 2025-05-26 04:59:20 +00:00
Mlikiowa
5d57780e84 release: v4.7.73 2025-05-26 03:52:01 +00:00
手瓜一十雪
f399955204 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-05-26 11:51:35 +08:00
手瓜一十雪
770652fe6b fix: remove debug 2025-05-26 11:51:25 +08:00
Mlikiowa
9ed5fa8c67 release: v4.7.72 2025-05-26 03:51:12 +00:00
手瓜一十雪
5a4ad29727 fix: #1040 2025-05-26 11:50:45 +08:00
手瓜一十雪
1eda3f2e33 fix: #1039 2025-05-26 10:58:01 +08:00
Mlikiowa
95cb95ef96 release: v4.7.70 2025-05-25 08:56:20 +00:00
手瓜一十雪
4e7c96634c fix: #1038 2025-05-25 16:55:32 +08:00
手瓜一十雪
58587b8aea fix 2025-05-25 16:30:15 +08:00
手瓜一十雪
3fbf6239db fix: #1031 2025-05-25 16:18:50 +08:00
手瓜一十雪
faec53d497 feat: #1031 2025-05-25 16:09:06 +08:00
手瓜一十雪
482dcc534e feat: kill-update 2025-05-24 10:33:14 +08:00
手瓜一十雪
854f61dda6 feat: createGrayTip 2025-05-23 17:22:07 +08:00
Mlikiowa
fca38713a1 release: v4.7.68 2025-05-22 03:48:00 +00:00
手瓜一十雪
5dd3bade53 fix: #1029 2025-05-22 11:47:31 +08:00
手瓜一十雪
665360f48d fix: #1027 2025-05-22 11:33:23 +08:00
Mlikiowa
65719cb56a release: v4.7.67 2025-05-21 04:59:16 +00:00
手瓜一十雪
bdb76d4639 fix: make ts happy 2025-05-21 12:58:53 +08:00
Mlikiowa
15634412ef release: v4.7.66 2025-05-21 04:53:55 +00:00
手瓜一十雪
bbcf9649fa feat: 35341 2025-05-21 12:53:21 +08:00
Mlikiowa
e845d7314e release: v4.7.65 2025-05-18 14:32:18 +00:00
手瓜一十雪
6927b1c94f Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-05-18 20:59:17 +08:00
手瓜一十雪
a09c6acd0d fix 2025-05-18 20:59:11 +08:00
Mlikiowa
0963650ccb release: v4.6.65 2025-05-18 12:58:07 +00:00
手瓜一十雪
380688b353 fix 2025-05-18 20:57:41 +08:00
手瓜一十雪
ad5466bff8 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-05-18 20:55:13 +08:00
手瓜一十雪
a83652bf3f feat: 更优美的代码 2025-05-18 20:55:11 +08:00
Mlikiowa
c632de314d release: v4.7.64 2025-05-18 12:49:37 +00:00
手瓜一十雪
259c9610d5 Merge pull request #1022 from NapNeko/poke_enhance
fix: #1018
2025-05-18 20:45:08 +08:00
手瓜一十雪
e9936c5524 fix 2025-05-18 20:42:03 +08:00
手瓜一十雪
3f60440e72 fix 2025-05-18 20:24:49 +08:00
手瓜一十雪
71a15f92fb fix 2025-05-18 20:19:53 +08:00
手瓜一十雪
32bc0dd820 fix 2025-05-18 20:16:55 +08:00
手瓜一十雪
20d1ac9d01 fix: #1018 2025-05-18 20:15:38 +08:00
手瓜一十雪
18baf89e0e Merge pull request #1021 from NapNeko/feat-new-context
feat: 隔离context传递 避免高并发干扰一个实例
2025-05-18 19:27:23 +08:00
手瓜一十雪
3a1d1f2e59 feat: 隔离context传递 避免高并发干扰一个实例 2025-05-18 19:21:14 +08:00
手瓜一十雪
e9a048721d fix: readonly 2025-05-18 19:02:31 +08:00
手瓜一十雪
68f0c7ff1a fix: readonly 2025-05-18 19:01:57 +08:00
手瓜一十雪
2875fe94ea Merge pull request #1020 from pohgxz/main
增加抽象类,修改继承关系
2025-05-18 18:59:25 +08:00
手瓜一十雪
1870427c0f fix: readonly 2025-05-18 18:58:27 +08:00
手瓜一十雪
636568fd30 fix: 字面量
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-18 18:56:59 +08:00
手瓜一十雪
bbc2391bf8 fix: 别名
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-18 18:55:56 +08:00
手瓜一十雪
401684542a fix: override
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-05-18 18:53:01 +08:00
手瓜一十雪
870edb2513 fix: override
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-05-18 18:52:07 +08:00
Nepenthe
7ad09169ea 增加抽象类,修改继承关系 2025-05-18 18:32:23 +08:00
手瓜一十雪
c1a0f8915b docs: mai 2025-05-17 17:54:53 +08:00
Mlikiowa
dcdab8e5a1 release: v4.7.63 2025-05-17 05:09:36 +00:00
手瓜一十雪
eb3278fdab feat: 35184 2025-05-17 11:16:39 +08:00
手瓜一十雪
34db3af48d fix: #1007 2025-05-15 21:10:21 +08:00
Mlikiowa
198da960dd release: v4.7.62 2025-05-12 12:37:01 +00:00
手瓜一十雪
cb83918fb3 fix 2025-05-12 20:36:39 +08:00
Mlikiowa
f59a48540b release: v4.7.61 2025-05-12 12:34:18 +00:00
手瓜一十雪
ccf9c1a5fb Merge pull request #1005 from NapNeko/fix-1001
refactor: remove image-size
2025-05-12 20:22:35 +08:00
手瓜一十雪
ba6a85142a fix 2025-05-12 19:29:40 +08:00
手瓜一十雪
440baccd2a fix 2025-05-12 19:27:46 +08:00
手瓜一十雪
690c073328 fix 2025-05-12 19:27:01 +08:00
手瓜一十雪
3f0730ed4f fix 2025-05-12 19:25:03 +08:00
手瓜一十雪
01d5663bc8 fix: image size 2025-05-12 19:18:33 +08:00
手瓜一十雪
49806cd00e Create index.ts 2025-05-12 19:17:17 +08:00
手瓜一十雪
935b0848e5 Merge pull request #1004 from NapNeko/dependabot/npm_and_yarn/esbuild-0.25.4
build(deps-dev): bump esbuild from 0.25.0 to 0.25.4
2025-05-12 18:45:23 +08:00
dependabot[bot]
5ca20a89a2 build(deps-dev): bump esbuild from 0.25.0 to 0.25.4
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.25.0 to 0.25.4.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.0...v0.25.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 08:37:11 +00:00
Mlikiowa
e89a2266ec release: v4.7.60 2025-05-11 03:32:29 +00:00
手瓜一十雪
6607533311 fix: #996 2025-05-11 11:31:52 +08:00
Mlikiowa
4057054220 release: v4.7.58 2025-05-11 03:24:02 +00:00
手瓜一十雪
055e43845e feat: ffmpeg下载来源更换 2025-05-11 11:23:43 +08:00
Mlikiowa
d67270f2f8 release: v4.7.57 2025-05-10 13:15:37 +00:00
手瓜一十雪
d061b6c190 feat: 34958 2025-05-10 21:15:07 +08:00
Mlikiowa
945f87d77f release: v4.7.56 2025-05-09 11:28:05 +00:00
手瓜一十雪
6c9be52d39 feat: 34740 2025-05-09 19:16:25 +08:00
手瓜一十雪
98e347f010 fix: 过滤掉已读 2025-05-09 18:51:00 +08:00
手瓜一十雪
607e367bb1 fix 2025-05-09 13:17:47 +08:00
Mlikiowa
7a25dc1ef1 release: v4.7.55 2025-05-08 10:11:36 +00:00
手瓜一十雪
e22ec4be09 Merge pull request #991 from NapNeko/fix-upload-foward-cahce-del
fix: upload-foward-cache-del
2025-05-08 18:10:45 +08:00
手瓜一十雪
51a06622f9 fix: typo
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-05-08 18:04:40 +08:00
手瓜一十雪
22faf5b831 fix 2025-05-08 17:58:12 +08:00
手瓜一十雪
e781c662b2 fix 2025-05-08 15:20:55 +08:00
手瓜一十雪
5744698d24 docs: 清空不好的影响&推荐一下 2025-05-08 15:20:06 +08:00
Mlikiowa
2c2ab3cd48 release: v4.7.51 2025-05-07 15:16:50 +00:00
手瓜一十雪
cfae4f5acd fix: 增强 2025-05-07 22:26:25 +08:00
Mlikiowa
de541e3249 release: v4.5.50 2025-05-07 14:10:15 +00:00
手瓜一十雪
f5187c5c01 Merge pull request #989 from NapNeko/file-url-feat
feat: 消息上报Url重构
2025-05-07 22:09:29 +08:00
手瓜一十雪
9936279443 fix 2025-05-07 22:03:00 +08:00
手瓜一十雪
2818773fd4 fix 2025-05-07 20:53:36 +08:00
手瓜一十雪
b9293cbcd0 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-07 20:51:50 +08:00
手瓜一十雪
5b9e44ddfc fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-07 20:51:28 +08:00
手瓜一十雪
1791accab7 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-07 20:51:12 +08:00
手瓜一十雪
08081360f3 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-07 20:50:51 +08:00
手瓜一十雪
e933a95e97 fix 2025-05-07 18:10:58 +08:00
手瓜一十雪
4ef457fe6f feat: fileUrl Get 2025-05-07 18:10:49 +08:00
Mlikiowa
bd9cae8921 release: v4.7.49 2025-05-07 09:16:28 +00:00
手瓜一十雪
303a74f8fd feat: 背压问题 2025-05-07 17:14:57 +08:00
Mlikiowa
0b7f126ce1 release: v4.7.48 2025-05-07 08:21:34 +00:00
Clansty
308b5c027f fix: at 变成负数 2025-05-07 03:46:17 +08:00
手瓜一十雪
ed3abc4b43 feat 2025-05-04 21:11:34 +08:00
Mlikiowa
87ecb3b380 release: v4.7.47 2025-05-03 14:27:49 +00:00
手瓜一十雪
7e31763a25 fix 2025-05-03 22:26:41 +08:00
Mlikiowa
c9df57d16a release: v4.7.46 2025-05-03 08:08:25 +00:00
手瓜一十雪
3d0f8ee657 fix 2025-05-03 16:06:51 +08:00
手瓜一十雪
6421bb4f5c feat: normalize 2025-05-02 15:10:31 +08:00
Mlikiowa
3919743885 release: v4.7.45 2025-04-30 13:43:59 +00:00
pk5ls20
a5a57b9e20 fix: fxxking fake forward element
- close #972, #977, #666
2025-04-30 20:31:03 +08:00
手瓜一十雪
e31d2810ad fix 2025-04-29 22:06:01 +08:00
Mlikiowa
140e62fdcd release: v4.7.44 2025-04-28 14:04:40 +00:00
手瓜一十雪
014b4deb87 feat: 34740 2025-04-28 22:04:20 +08:00
Mlikiowa
956b6cd172 release: v4.7.43 2025-04-26 11:10:37 +00:00
手瓜一十雪
bbaca3f044 fix 2025-04-26 19:10:00 +08:00
Mlikiowa
bb8a44b918 release: v4.7.42 2025-04-26 11:02:25 +00:00
手瓜一十雪
b5574d5999 fix: #976 2025-04-26 19:00:31 +08:00
手瓜一十雪
06dde072da Merge pull request #975 from pohgxz/main
接口 _get_model_show 的 model 设置为可选属性
2025-04-26 18:31:10 +08:00
Nepenthe
8e92a81bb9 接口 _get_model_show 的 model 设置为可选属性 2025-04-26 14:48:35 +08:00
Nepenthe
2c7345ae88 Merge branch 'NapNeko:main' into main 2025-04-26 14:40:38 +08:00
Mlikiowa
33d4696155 release: v4.7.41 2025-04-24 09:43:32 +00:00
手瓜一十雪
7d2dcc10e5 fix 2025-04-24 17:43:13 +08:00
Mlikiowa
e82687454c release: v4.7.40 2025-04-24 07:57:16 +00:00
手瓜一十雪
84382caebc fix 2025-04-24 15:56:55 +08:00
Mlikiowa
662530e507 release: v4.7.36 2025-04-24 07:53:59 +00:00
手瓜一十雪
edf81d0a2e feat: 34606 2025-04-24 15:37:44 +08:00
手瓜一十雪
7cbae86941 Revert "fix: 私聊撤回"
This reverts commit 8ff7420a5e.
2025-04-24 11:34:07 +08:00
手瓜一十雪
8ff7420a5e fix: 私聊撤回 2025-04-24 11:33:11 +08:00
手瓜一十雪
7ae59b1419 Merge pull request #971 from Sn0wo2/main
fix: temp_source
2025-04-24 09:54:29 +08:00
手瓜一十雪
41036f8ee8 fix: 969 2025-04-24 09:50:26 +08:00
Me0wo
380777ca04 fix: #970 2025-04-24 04:11:31 +08:00
Mlikiowa
c658cd1096 release: v4.7.35 2025-04-23 08:52:43 +00:00
手瓜一十雪
c7b9946d2f feat: doubt friends支持 2025-04-23 16:46:09 +08:00
手瓜一十雪
0caca473d6 feat: 34566 2025-04-23 16:18:48 +08:00
手瓜一十雪
3e5d35957d fix 2025-04-23 16:12:56 +08:00
手瓜一十雪
6b8b14aba2 fix: #963 2025-04-23 11:47:58 +08:00
手瓜一十雪
5db7a90a24 feat: 301 302自动跟随下载 2025-04-21 18:43:44 +08:00
Mlikiowa
88b86611a3 release: v4.7.34 2025-04-20 14:12:47 +00:00
手瓜一十雪
886fe2052e feat: 避免危险信息 2025-04-20 22:12:12 +08:00
手瓜一十雪
e4dd194d4a fix: #960
神经设计
2025-04-20 22:10:24 +08:00
手瓜一十雪
a47af60f58 feat: disband 2025-04-20 19:28:35 +08:00
Mlikiowa
35f24eb806 release: v4.7.33 2025-04-19 12:17:18 +00:00
手瓜一十雪
36e3119d34 feat: 支持https 面板 2025-04-19 20:16:24 +08:00
手瓜一十雪
8ff3ad824e feat: 支持环境变量禁用ffmpeg下载支持 2025-04-19 20:03:00 +08:00
手瓜一十雪
556000c002 feat: 优雅的回车登录 2025-04-19 19:59:11 +08:00
手瓜一十雪
fda050d3fe feat: 加强安全性 传输过程使用salt sha256 2025-04-19 19:50:52 +08:00
手瓜一十雪
b1047309c9 feat: 消息context增强识别 2025-04-19 11:36:27 +08:00
Mlikiowa
d766c4945e release: v4.7.32 2025-04-19 03:17:47 +00:00
手瓜一十雪
43c98c45b9 fix 2025-04-19 11:13:02 +08:00
手瓜一十雪
f7556b5af3 fix 2025-04-19 11:10:04 +08:00
手瓜一十雪
cd781c4cf6 feat: 回归ajv 2025-04-19 11:07:01 +08:00
手瓜一十雪
cd8698b157 fix 2025-04-19 11:03:03 +08:00
手瓜一十雪
d921dcddf1 Revert "package->dev"
This reverts commit 45d6ebf084.
2025-04-19 11:01:45 +08:00
手瓜一十雪
9f318ddaef Revert "feat: 区分resId和普通消息Id"
This reverts commit 7ecdd63bef.
2025-04-19 11:01:12 +08:00
手瓜一十雪
5c35ea11c3 Revert "fix: 修掉漏掉的"
This reverts commit 7a42f8c26f.
2025-04-19 11:01:06 +08:00
手瓜一十雪
3b16effff0 Revert "fix"
This reverts commit 40b06daf1e.
2025-04-19 10:59:04 +08:00
手瓜一十雪
d3a27ad701 Revert "fix:coerce"
This reverts commit dd895d7c17.
2025-04-19 10:58:56 +08:00
手瓜一十雪
2a4589e268 Revert "fix: checker"
This reverts commit 941978b578.
2025-04-19 10:58:46 +08:00
手瓜一十雪
80a34c82b9 Revert "fix: zod boolean强制转换"
This reverts commit 17ef3231df.
2025-04-19 10:58:39 +08:00
手瓜一十雪
fca7a65ee0 Revert "fix"
This reverts commit 3f6249f39c.
2025-04-19 10:57:36 +08:00
手瓜一十雪
30a75bc581 Revert "fix"
This reverts commit 54e6d5c3f2.
2025-04-19 10:57:32 +08:00
手瓜一十雪
7b365367f7 Revert "fix"
This reverts commit 41dccd98a9.
2025-04-19 10:57:28 +08:00
手瓜一十雪
3ed5f543e2 Revert "fix"
This reverts commit bd3e06520f.
2025-04-19 10:57:25 +08:00
手瓜一十雪
ceea50b116 Revert "fix: coerce"
This reverts commit fb20b2e16c.
2025-04-19 10:53:29 +08:00
手瓜一十雪
a5455e27d1 feat: 34467 2025-04-19 09:44:00 +08:00
手瓜一十雪
6f83d01321 feat: 34362 2025-04-18 18:19:12 +08:00
手瓜一十雪
c453b82e9f feat: #954 2025-04-18 12:12:18 +08:00
Mlikiowa
b7da316447 release: v4.7.31 2025-04-17 14:17:57 +00:00
手瓜一十雪
fb20b2e16c fix: coerce 2025-04-17 22:17:35 +08:00
Mlikiowa
9df7c341a9 release: v4.7.30 2025-04-17 10:07:28 +00:00
手瓜一十雪
7c113d6e04 fix: 一些问题 2025-04-17 18:07:07 +08:00
Mlikiowa
a6f22167ff release: v4.7.29 2025-04-17 09:59:29 +00:00
手瓜一十雪
d49e69735a fix: 自动化验证环境变量的ffmpeg 2025-04-17 17:59:06 +08:00
Mlikiowa
eca73eae18 release: v4.7.28 2025-04-17 09:49:05 +00:00
手瓜一十雪
d3a34dfdf9 feat: 增强异常处理 2025-04-17 17:48:13 +08:00
手瓜一十雪
623188d884 feat: 34362 2025-04-17 17:07:09 +08:00
Mlikiowa
f093f52792 release: v4.7.27 2025-04-17 06:39:48 +00:00
手瓜一十雪
d53607a118 fix 2025-04-17 14:39:30 +08:00
Mlikiowa
5f637e064a release: v4.7.26 2025-04-17 06:29:11 +00:00
手瓜一十雪
e4b21e94f5 feat: ffmpeg download auto 2025-04-17 14:28:51 +08:00
手瓜一十雪
fc37288827 fix: ffmpeg 2025-04-17 13:55:31 +08:00
Mlikiowa
dad7245a3a release: v4.7.25 2025-04-17 05:28:19 +00:00
手瓜一十雪
4190831081 fix: 扬了ffmpeg.wasm 2025-04-17 13:26:24 +08:00
Mlikiowa
c509a01d7d release: v4.7.24 2025-04-17 01:57:25 +00:00
手瓜一十雪
6d259593fd Merge pull request #953 from NapNeko/fix-zod-boolean
fix: zod boolean强制转换
2025-04-17 09:56:48 +08:00
手瓜一十雪
bd3e06520f fix 2025-04-17 09:56:12 +08:00
手瓜一十雪
41dccd98a9 fix 2025-04-17 09:54:12 +08:00
手瓜一十雪
54e6d5c3f2 fix 2025-04-17 09:52:03 +08:00
手瓜一十雪
3f6249f39c fix 2025-04-17 09:48:59 +08:00
手瓜一十雪
a888714629 fix: napcat log 2025-04-17 09:47:39 +08:00
手瓜一十雪
17ef3231df fix: zod boolean强制转换 2025-04-17 09:38:38 +08:00
Mlikiowa
cc30b51d58 release: v4.7.23 2025-04-15 10:38:47 +00:00
手瓜一十雪
9d40eacc15 fix: 34231 linux arm64 docker问题 2025-04-15 18:38:24 +08:00
Mlikiowa
a0415c5f4e release: v4.7.22 2025-04-15 04:35:48 +00:00
手瓜一十雪
941978b578 fix: checker 2025-04-15 12:34:33 +08:00
Mlikiowa
06538b9122 release: v4.7.21 2025-04-15 04:25:59 +00:00
手瓜一十雪
dd895d7c17 fix:coerce 2025-04-15 12:25:37 +08:00
Mlikiowa
9257a6cfde release: v4.7.20 2025-04-14 14:37:43 +00:00
手瓜一十雪
f8260067ab Merge pull request #944 from clansty/fix/isReverseOrder
fix: isReverseOrder
2025-04-14 21:44:42 +08:00
手瓜一十雪
40b06daf1e fix 2025-04-14 21:44:25 +08:00
手瓜一十雪
e30915a06b Merge pull request #946 from NapNeko/fix-923
feat: 区分resId和普通消息Id
2025-04-14 19:05:01 +08:00
手瓜一十雪
2ab3898d28 readme: new 2025-04-14 13:25:00 +08:00
Clansty
6e38e748b8 fix: isReverseOrder 2025-04-14 05:40:11 +08:00
手瓜一十雪
7ecdd63bef feat: 区分resId和普通消息Id 2025-04-13 20:33:25 +08:00
手瓜一十雪
8056962203 Merge pull request #943 from NapNeko/zod-refactor
fix: 修掉漏掉的
2025-04-13 20:17:08 +08:00
手瓜一十雪
7a42f8c26f fix: 修掉漏掉的 2025-04-13 20:14:35 +08:00
手瓜一十雪
6c510e42e8 Merge pull request #942 from NapNeko/zod-refactor
迁移类型校验到zod
2025-04-13 20:10:43 +08:00
手瓜一十雪
45d6ebf084 package->dev 2025-04-13 20:06:30 +08:00
手瓜一十雪
2147c4ffee 迁移类型校验到zod 2025-04-13 20:05:11 +08:00
Mlikiowa
d4ab191f34 release: v4.7.19 2025-04-12 04:25:39 +00:00
手瓜一十雪
a5e53a713b feat: 增强win 输出可读性 2025-04-12 12:23:38 +08:00
Mlikiowa
e3f965a9d6 release: v4.7.18 2025-04-12 04:04:09 +00:00
手瓜一十雪
dd00d4c8a5 fix: 小问题 2025-04-12 11:59:29 +08:00
手瓜一十雪
4d292a75fa fix 2025-04-12 11:36:46 +08:00
pk5ls20
50b6733f57 Merge pull request #938 from Fahaxikiii/patch-2
Update README.md
2025-04-12 00:11:23 +08:00
万里
19479b4b3c Update README.md
换成美国vps
2025-04-12 00:09:19 +08:00
pk5ls20
540f58d8ec Merge pull request #937 from Fahaxikiii/patch-1 2025-04-11 23:18:46 +08:00
万里
749f1dfcf9 Update README.md
服务器到期了呜呜呜
2025-04-11 22:03:28 +08:00
Mlikiowa
176691bb96 release: v4.7.17 2025-04-11 07:12:53 +00:00
手瓜一十雪
b9ec8ac9b1 feat: LL Framework适配 2025-04-11 15:12:32 +08:00
手瓜一十雪
28d973b9cb Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-04-11 14:40:31 +08:00
手瓜一十雪
a6be54937c feat: 删掉不必要的启动脚本 2025-04-11 14:40:22 +08:00
Mlikiowa
0664b9af84 release: v4.7.16 2025-04-11 06:21:19 +00:00
Nepenthe
faf390bb18 Merge branch 'NapNeko:main' into main 2025-02-21 21:19:02 +08:00
Nepenthe
941b30847b Merge branch 'NapNeko:main' into main 2025-01-25 23:14:26 +08:00
Nepenthe
4c5a26698e Merge branch 'NapNeko:main' into main 2025-01-15 21:33:43 +08:00
Nepenthe
d14a1dd948 Merge branch 'NapNeko:main' into main 2024-11-17 17:28:31 +08:00
Nepenthe
1c0b434f47 Merge branch 'NapNeko:main' into main 2024-10-31 19:15:25 +08:00
Nepenthe
573451bade 修复<get_record>接口 2024-10-30 21:07:01 +08:00
703 changed files with 66355 additions and 32555 deletions

View File

@@ -15,7 +15,7 @@ charset = utf-8
# 4 space indentation # 4 space indentation
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}] [*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
indent_style = space indent_style = space
indent_size = 4 indent_size = 2
[*.bat] [*.bat]
charset = latin1 charset = latin1

2
.env.shell-analysis Normal file
View File

@@ -0,0 +1,2 @@
VITE_BUILD_TYPE = DEBUG
VITE_BUILD_PLATFORM = Shell

View File

@@ -150,3 +150,15 @@ jobs:
NapCat.Shell.zip NapCat.Shell.zip
NapCat.Framework.Windows.Once.zip NapCat.Framework.Windows.Once.zip
draft: true draft: true
build-docker:
needs: release-napcat
runs-on: ubuntu-latest
steps:
- name: Dispatch Docker Build
run: |
curl -X POST \
-H "Authorization: Bearer ${{ secrets.NAPCAT_BUILD }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/NapNeko/NapCat-Docker/actions/workflows/docker-publish.yml/dispatches \
-d '{"ref": "main"}'

2
.gitignore vendored
View File

@@ -1,6 +1,5 @@
# Develop # Develop
node_modules/ node_modules/
package-lock.json
pnpm-lock.yaml pnpm-lock.yaml
out/ out/
dist/ dist/
@@ -15,3 +14,4 @@ devconfig/*
*.db *.db
checkVersion.sh checkVersion.sh
bun.lockb bun.lockb
tests/run/

View File

@@ -1,10 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 120,
"endOfLine": "auto"
}

27
.vscode/settings.json vendored
View File

@@ -3,10 +3,35 @@
"explorer.fileNesting.expand": false, "explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": { "explorer.fileNesting.patterns": {
".env.universal": ".env.*", ".env.universal": ".env.*",
"tsconfig.json": "tsconfig.*.json, env.d.ts, vite.config.ts", "vite.config.ts": "vite*.ts",
"README.md": "CODE_OF_CONDUCT.md, RELEASES.md, CONTRIBUTING.md, CHANGELOG.md, SECURITY.md",
"tsconfig.json": "tsconfig.*.json, env.d.ts",
"package.json": "package-lock.json, eslint*, .prettier*, .editorconfig, manifest.json, logo.png, .gitignore, LICENSE" "package.json": "package-lock.json, eslint*, .prettier*, .editorconfig, manifest.json, logo.png, .gitignore, LICENSE"
}, },
"css.customData": [ "css.customData": [
".vscode/tailwindcss.json" ".vscode/tailwindcss.json"
], ],
"editor.detectIndentation": false,
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.formatOnType": false,
"editor.formatOnPaste": true,
"editor.formatOnSaveMode": "file",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
},
"files.autoSave": "onFocusChange",
"javascript.preferences.quoteStyle": "single",
"typescript.preferences.quoteStyle": "single",
"javascript.format.semicolons": "insert",
"typescript.format.semicolons": "insert",
"javascript.format.insertSpaceBeforeFunctionParenthesis": true,
"typescript.format.insertSpaceBeforeFunctionParenthesis": true,
"typescript.format.insertSpaceAfterConstructor": true,
"javascript.format.insertSpaceAfterConstructor": true,
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "minimal",
"javascript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.importModuleSpecifierEnding": "minimal",
"typescript.disableAutomaticTypeAcquisition": true,
} }

View File

@@ -1,9 +1,8 @@
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
<div align="center"> <div align="center">
# NapCat # NapCat
![NapCatQQ](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Fnewlogo.png&name=1&owner=1&pattern=Diagonal+Stripes&stargazers=1&theme=Auto)
_Modern protocol-side framework implemented based on NTQQ._ _Modern protocol-side framework implemented based on NTQQ._
> 云起兮风生,心向远方兮路未曾至. > 云起兮风生,心向远方兮路未曾至.
@@ -12,49 +11,77 @@ _Modern protocol-side framework implemented based on NTQQ._
--- ---
## New Feature
在 v4.8.115+ 版本开始
1. NapCatQQ 支持 [Stream Api](https://napneko.github.io/develop/file)
2. NapCatQQ 推荐 message_id/user_id/group_id 均使用字符串类型
- [1] 解决 Docker/跨设备/大文件 的多媒体上下传问题
- [2] 采用字符串可以解决扩展到int64的问题同时也可以解决部分语言如JavaScript对大整数支持不佳的问题增加极少成本。
## Welcome ## Welcome
+ NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
- NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
- NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现 - NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## Feature ## Feature
+ **Easy to Use** - **Easy to Use**
- 作为初学者能够轻松使用. - 作为初学者能够轻松使用.
+ **Quick and Efficient** - **Quick and Efficient**
- 在低内存操作系统长时运行. - 在低内存操作系统长时运行.
+ **Rich API Interface** - **Rich API Interface**
- 完整实现了大部分标准接口. - 完整实现了大部分标准接口.
+ **Stable and Reliable** - **Stable and Reliable**
- 持续稳定的开发与维护. - 持续稳定的开发与维护.
## Quick Start ## Quick Start
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本 可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
**首次使用**请务必查看如下文档看使用教程 **首次使用**请务必查看如下文档看使用教程
> 项目非盈利,对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
## Link ## Link
| Docs | [![Github.IO](https://img.shields.io/badge/docs%20on-Github.IO-orange)](https://napneko.github.io/) | [![Cloudflare.Worker](https://img.shields.io/badge/docs%20on-Cloudflare.Worker-black)](https://doc.napneko.icu/) | [![Cloudflare.HKServer](https://img.shields.io/badge/docs%20on-Cloudflare.HKServer-informational)](https://napcat.napneko.icu/) | | Docs | [![Github.IO](https://img.shields.io/badge/docs%20on-Github.IO-orange)](https://napneko.github.io/) | [![Cloudflare.Worker](https://img.shields.io/badge/docs%20on-Cloudflare.Worker-black)](https://doc.napneko.icu/) | [![Cloudflare.HKServer](https://img.shields.io/badge/docs%20on-Cloudflare.HKServer-informational)](https://napcat.napneko.icu/) |
|:-:|:-:|:-:|:-:| |:-:|:-:|:-:|:-:|
| Docs | [![Cloudflare.Pages](https://img.shields.io/badge/docs%20on-Cloudflare.Pages-blue)](https://napneko.pages.dev/) | [![Server.Other](https://img.shields.io/badge/docs%20on-Server.Other-green)](https://docs.napcat.cyou/) | [![NapCat.Wiki](https://img.shields.io/badge/docs%20on-NapCat.Wiki-red)](https://www.napcat.wiki) | | Docs | [![Cloudflare.Pages](https://img.shields.io/badge/docs%20on-Cloudflare.Pages-blue)](https://napneko.pages.dev/) | [![Server.Other](https://img.shields.io/badge/docs%20on-Server.Other-green)](https://napcat.cyou/) | [![NapCat.Wiki](https://img.shields.io/badge/docs%20on-NapCat.Wiki-red)](https://www.napcat.wiki) |
|:-:|:-:|:-:|:-:| |:-:|:-:|:-:|:-:|
| Contact | [![QQ Group#1](https://img.shields.io/badge/QQ%20Group%231-Join-blue)](https://qm.qq.com/q/I6LU87a0Yq) | [![QQ Group#2](https://img.shields.io/badge/QQ%20Group%232-Join-blue)](https://qm.qq.com/q/HaRcfrHpUk) | [![Telegram](https://img.shields.io/badge/Telegram-MelodicMoonlight-blue)](https://t.me/MelodicMoonlight) | | QQ Group | [![QQ Group#4](https://img.shields.io/badge/QQ%20Group%234-Join-blue)](https://qm.qq.com/q/CMmPbGw0jA) | [![QQ Group#3](https://img.shields.io/badge/QQ%20Group%233-Join-blue)](https://qm.qq.com/q/8zJMLjqy2Y) | [![QQ Group#2](https://img.shields.io/badge/QQ%20Group%232-Join-blue)](https://qm.qq.com/q/CMmPbGw0jA) | [![QQ Group#1](https://img.shields.io/badge/QQ%20Group%231-Join-blue)](https://qm.qq.com/q/I6LU87a0Yq) |
|:-:|:-:|:-:|:-:| |:-:|:-:|:-:|:-:|:-:|
| Telegram | [![Telegram](https://img.shields.io/badge/Telegram-napcatqq-blue)](https://t.me/napcatqq) |
|:-:|:-:|
| DeepWiki | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/NapNeko/NapCatQQ) |
|:-:|:-:|
> 请不要在其余社区提及本项目(包括其余协议端/相关应用端项目)引发争论如有建议到达官方交流群讨论或PR。
## Thanks ## Thanks
+ [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权 - [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
+ [LLOneBot](https://github.com/LLOneBot/LLOneBot) 相关的开发曾参与本项目部分开发 - [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
+ 不过最最重要的 还是需要感谢屏幕前的你哦~ - [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) 基于NapCat的消息导出工具 在此推荐一下
- 不过最最重要的 还是需要感谢屏幕前的你哦~
--- ---
## License ## License
本项目采用 混合协议 开源,因此使用本项目时,你需要注意以下几点: 本项目采用 混合协议 开源,因此使用本项目时,你需要注意以下几点:
1. 第三方库代码或修改部分遵循其原始开源许可. 1. 第三方库代码或修改部分遵循其原始开源许可.
2. 本项目获取部分项目授权而不受部分约束 2. 本项目获取部分项目授权而不受部分约束
2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE). 2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE).

View File

@@ -1,32 +1,52 @@
import eslint from '@eslint/js'; import neostandard from 'neostandard';
import tsEslintPlugin from '@typescript-eslint/eslint-plugin';
import tsEslintParser from '@typescript-eslint/parser';
import globals from "globals";
const customTsFlatConfig = [ /** 尾随逗号 */
{ const commaDangle = val => {
name: 'typescript-eslint/base', if (val?.rules?.['@stylistic/comma-dangle']?.[0] === 'warn') {
languageOptions: { const rule = val?.rules?.['@stylistic/comma-dangle']?.[1];
parser: tsEslintParser, Object.keys(rule).forEach(key => {
sourceType: 'module', rule[key] = 'always-multiline';
globals: { });
...globals.browser, val.rules['@stylistic/comma-dangle'][1] = rule;
...globals.node, }
NodeJS: 'readonly', // 添加 NodeJS 全局变量
}, /** 三元表达式 */
}, if (val?.rules?.['@stylistic/indent']) {
files: ['**/*.{ts,tsx}'], val.rules['@stylistic/indent'][2] = {
rules: { ...val.rules?.['@stylistic/indent']?.[2],
...tsEslintPlugin.configs.recommended.rules, flatTernaryExpressions: true,
'quotes': ['error', 'single'], // 使用单引号 offsetTernaryExpressions: false,
'semi': ['error', 'always'], // 强制使用分号 };
'indent': ['error', 4], // 使用 4 空格缩进 }
},
plugins: { /** 支持下划线 - 禁用 camelcase 规则 */
'@typescript-eslint': tsEslintPlugin, if (val?.rules?.camelcase) {
}, val.rules.camelcase = 'off';
ignores: ['src/webui/**'], // 忽略 src/webui/ 目录所有文件 }
},
/** 未使用的变量强制报错 */
if (val?.rules?.['@typescript-eslint/no-unused-vars']) {
val.rules['@typescript-eslint/no-unused-vars'] = ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}];
}
return val;
};
/** 忽略的文件 */
const ignores = [
'node_modules',
'**/dist/**',
'launcher',
]; ];
export default [eslint.configs.recommended, ...customTsFlatConfig]; const options = neostandard({
ts: true,
ignores,
semi: true, // 强制使用分号
}).map(commaDangle);
export default options;

Binary file not shown.

BIN
external/logo.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1 +0,0 @@
带有34231数字的是指QQ 9.9.19-34231 适配的启动脚本

View File

@@ -1,32 +0,0 @@
@echo off
chcp 65001
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook_34231.dll
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b
goto :napcat_boot
)
:napcat_boot
for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa"
)
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid
pause
exit /b
)
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
pause

View File

@@ -7,7 +7,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read :loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do ( for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b set "RetString=%%~b"
goto :napcat_boot goto :napcat_boot
) )
@@ -16,7 +16,7 @@ for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa" set "pathWithoutUninstall=%%~dpa"
) )
SET QQPath=%pathWithoutUninstall%QQ.exe set "QQPath=%pathWithoutUninstall%QQ.exe"
if not exist "%QQpath%" ( if not exist "%QQpath%" (
echo provided QQ path is invalid echo provided QQ path is invalid

View File

@@ -1,33 +0,0 @@
@echo off
chcp 65001
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook_34231.dll
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b
goto :napcat_boot
)
:napcat_boot
for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa"
)
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid
pause
exit /b
)
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456
pause

View File

@@ -7,7 +7,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read :loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do ( for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b set "RetString=%%~b"
goto :napcat_boot goto :napcat_boot
) )
@@ -16,7 +16,7 @@ for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa" set "pathWithoutUninstall=%%~dpa"
) )
SET QQPath=%pathWithoutUninstall%QQ.exe set "QQPath=%pathWithoutUninstall%QQ.exe"
if not exist "%QQpath%" ( if not exist "%QQpath%" (
echo provided QQ path is invalid echo provided QQ path is invalid

View File

@@ -16,7 +16,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read :loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do ( for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b set "RetString=%%~b"
goto :napcat_boot goto :napcat_boot
) )
@@ -25,7 +25,7 @@ for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa" set "pathWithoutUninstall=%%~dpa"
) )
SET QQPath=%pathWithoutUninstall%QQ.exe set "QQPath=%pathWithoutUninstall%QQ.exe"
if not exist "%QQPath%" ( if not exist "%QQPath%" (
echo provided QQ path is invalid echo provided QQ path is invalid

View File

@@ -16,7 +16,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read :loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do ( for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b set "RetString=%%~b"
goto :napcat_boot goto :napcat_boot
) )
@@ -25,7 +25,7 @@ for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa" set "pathWithoutUninstall=%%~dpa"
) )
SET QQPath=%pathWithoutUninstall%QQ.exe set "QQPath=%pathWithoutUninstall%QQ.exe"
if not exist "%QQPath%" ( if not exist "%QQPath%" (
echo provided QQ path is invalid echo provided QQ path is invalid

View File

@@ -1,9 +1,9 @@
{ {
"name": "qq-chat", "name": "qq-chat",
"version": "9.9.18-32869", "verHash": "2c9d3f6c",
"verHash": "e735296c", "version": "9.9.22-40990",
"linuxVersion": "3.2.16-32869", "linuxVersion": "3.2.20-40990",
"linuxVerHash": "4c192ba9", "linuxVerHash": "ec800879",
"private": true, "private": true,
"description": "QQ", "description": "QQ",
"productName": "QQ", "productName": "QQ",
@@ -16,25 +16,8 @@
"bin": { "bin": {
"qd": "externals/devtools/cli/index.js" "qd": "externals/devtools/cli/index.js"
}, },
"appid": {
"win32": "537258389",
"darwin": "537258412",
"linux": "537258424"
},
"main": "./loadNapCat.js", "main": "./loadNapCat.js",
"peerDependenciesMeta": { "buildVersion": "40990",
"*": {
"optional": true
}
},
"pnpm": {
"patchedDependencies": {
"@vue/runtime-dom@3.5.12": "patches/@vue__runtime-dom@3.5.12.patch",
"@swc/helpers@0.5.3": "patches/@swc__helpers@0.5.3.patch",
"vuex@4.1.0": "patches/vuex@4.1.0.patch"
}
},
"buildVersion": "32869",
"isPureShell": true, "isPureShell": true,
"isByteCodeShell": true, "isByteCodeShell": true,
"platform": "win32", "platform": "win32",

View File

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

View File

@@ -1,7 +0,0 @@
dist
*.md
*.html
yarn.lock
package-lock.json
node_modules
pnpm-lock.yaml

View File

@@ -1,23 +0,0 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"bracketSpacing": true,
"importOrder": [
"<THIRD_PARTY_MODULES>",
"^@/const/(.*)$",
"^@/store/(.*)$",
"^@/components/(.*)$",
"^@/contexts/(.*)$",
"^@/hooks/(.*)$",
"^@/utils/(.*)$",
"^@/(.*)$",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@trivago/prettier-plugin-sort-imports"]
}

View File

@@ -1,91 +1,2 @@
import eslint_js from '@eslint/js' import eslintConfig from '../eslint.config.mjs';
import tsEslintPlugin from '@typescript-eslint/eslint-plugin' export default eslintConfig;
import tsEslintParser from '@typescript-eslint/parser'
import eslintConfigPrettier from 'eslint-config-prettier'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import reactPlugin from 'eslint-plugin-react'
import reactHooksPlugin from 'eslint-plugin-react-hooks'
import globals from 'globals'
const customTsFlatConfig = [
{
name: 'typescript-eslint/base',
languageOptions: {
parser: tsEslintParser,
sourceType: 'module'
},
files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
rules: {
...tsEslintPlugin.configs.recommended.rules
},
plugins: {
'@typescript-eslint': tsEslintPlugin
}
}
]
export default [
eslint_js.configs.recommended,
eslintPluginPrettierRecommended,
...customTsFlatConfig,
{
name: 'global config',
languageOptions: {
globals: {
...globals.es2022,
...globals.browser,
...globals.node
},
parserOptions: {
warnOnUnsupportedTypeScriptVersion: false
}
},
rules: {
'prettier/prettier': 'error',
'no-unused-vars': 'off',
'no-undef': 'off',
//关闭不能再promise中使用ansyc
'no-async-promise-executor': 'off',
//关闭不能再常量中使用??
'no-constant-binary-expression': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-unused-vars': 'off',
//禁止失去精度的字面数字
'@typescript-eslint/no-loss-of-precision': 'off',
//禁止使用any
'@typescript-eslint/no-explicit-any': 'error'
}
},
{
ignores: ['**/node_modules', '**/dist', '**/output']
},
{
name: 'react-eslint',
files: ['src/*.{js,jsx,mjs,cjs,ts,tsx}'],
plugins: {
react: reactPlugin,
'react-hooks': reactHooksPlugin
},
languageOptions: {
...reactPlugin.configs.recommended.languageOptions
},
rules: {
...reactPlugin.configs.recommended.rules,
'react/react-in-jsx-scope': 'off'
},
settings: {
react: {
// 需要显示安装 react
version: 'detect'
}
}
},
{
languageOptions: { globals: { ...globals.browser, ...globals.node } }
},
eslintConfigPrettier
]

15995
napcat.webui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -55,6 +55,7 @@
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"axios": "^1.7.9", "axios": "^1.7.9",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",
"framer-motion": "^12.0.6", "framer-motion": "^12.0.6",
@@ -85,9 +86,9 @@
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0",
"@react-types/shared": "^3.26.0", "@react-types/shared": "^3.26.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/crypto-js": "^4.2.2",
"@types/event-source-polyfill": "^1.0.5", "@types/event-source-polyfill": "^1.0.5",
"@types/fabric": "^5.3.9", "@types/fabric": "^5.3.9",
"@types/node": "^22.12.0", "@types/node": "^22.12.0",
@@ -95,20 +96,14 @@
"@types/react": "^19.0.8", "@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
"@types/react-window": "^1.8.8", "@types/react-window": "^1.8.8",
"@typescript-eslint/eslint-plugin": "^8.22.0",
"@typescript-eslint/parser": "^8.22.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.19.0", "eslint": "^9.19.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "5.2.3", "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.5.1", "postcss": "^8.5.1",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript": "^5.7.3", "typescript": "^5.7.3",

View File

@@ -1,33 +1,33 @@
import { Suspense, lazy, useEffect } from 'react' import { Suspense, lazy, useEffect } from 'react';
import { Provider } from 'react-redux' import { Provider } from 'react-redux';
import { Route, Routes, useNavigate } from 'react-router-dom' import { Route, Routes, useNavigate } from 'react-router-dom';
import PageBackground from '@/components/page_background' import PageBackground from '@/components/page_background';
import PageLoading from '@/components/page_loading' import PageLoading from '@/components/page_loading';
import Toaster from '@/components/toaster' import Toaster from '@/components/toaster';
import DialogProvider from '@/contexts/dialog' import DialogProvider from '@/contexts/dialog';
import AudioProvider from '@/contexts/songs' import AudioProvider from '@/contexts/songs';
import useAuth from '@/hooks/auth' import useAuth from '@/hooks/auth';
import store from '@/store' import store from '@/store';
const WebLoginPage = lazy(() => import('@/pages/web_login')) const WebLoginPage = lazy(() => import('@/pages/web_login'));
const IndexPage = lazy(() => import('@/pages/index')) const IndexPage = lazy(() => import('@/pages/index'));
const QQLoginPage = lazy(() => import('@/pages/qq_login')) const QQLoginPage = lazy(() => import('@/pages/qq_login'));
const DashboardIndexPage = lazy(() => import('@/pages/dashboard')) const DashboardIndexPage = lazy(() => import('@/pages/dashboard'));
const AboutPage = lazy(() => import('@/pages/dashboard/about')) const AboutPage = lazy(() => import('@/pages/dashboard/about'));
const ConfigPage = lazy(() => import('@/pages/dashboard/config')) const ConfigPage = lazy(() => import('@/pages/dashboard/config'));
const DebugPage = lazy(() => import('@/pages/dashboard/debug')) const DebugPage = lazy(() => import('@/pages/dashboard/debug'));
const HttpDebug = lazy(() => import('@/pages/dashboard/debug/http')) const HttpDebug = lazy(() => import('@/pages/dashboard/debug/http'));
const WSDebug = lazy(() => import('@/pages/dashboard/debug/websocket')) const WSDebug = lazy(() => import('@/pages/dashboard/debug/websocket'));
const FileManagerPage = lazy(() => import('@/pages/dashboard/file_manager')) const FileManagerPage = lazy(() => import('@/pages/dashboard/file_manager'));
const LogsPage = lazy(() => import('@/pages/dashboard/logs')) const LogsPage = lazy(() => import('@/pages/dashboard/logs'));
const NetworkPage = lazy(() => import('@/pages/dashboard/network')) const NetworkPage = lazy(() => import('@/pages/dashboard/network'));
const TerminalPage = lazy(() => import('@/pages/dashboard/terminal')) const TerminalPage = lazy(() => import('@/pages/dashboard/terminal'));
function App() { function App () {
return ( return (
<DialogProvider> <DialogProvider>
<Provider store={store}> <Provider store={store}>
@@ -42,49 +42,49 @@ function App() {
</AudioProvider> </AudioProvider>
</Provider> </Provider>
</DialogProvider> </DialogProvider>
) );
} }
function AuthChecker({ children }: { children: React.ReactNode }) { function AuthChecker ({ children }: { children: React.ReactNode }) {
const { isAuth } = useAuth() const { isAuth } = useAuth();
const navigate = useNavigate() const navigate = useNavigate();
useEffect(() => { useEffect(() => {
if (!isAuth) { if (!isAuth) {
const search = new URLSearchParams(window.location.search) const search = new URLSearchParams(window.location.search);
const token = search.get('token') const token = search.get('token');
let url = '/web_login' let url = '/web_login';
if (token) { if (token) {
url += `?token=${token}` url += `?token=${token}`;
} }
navigate(url, { replace: true }) navigate(url, { replace: true });
} }
}, [isAuth, navigate]) }, [isAuth, navigate]);
return <>{children}</> return <>{children}</>;
} }
function AppRoutes() { function AppRoutes () {
return ( return (
<Routes> <Routes>
<Route path="/" element={<IndexPage />}> <Route path='/' element={<IndexPage />}>
<Route index element={<DashboardIndexPage />} /> <Route index element={<DashboardIndexPage />} />
<Route path="network" element={<NetworkPage />} /> <Route path='network' element={<NetworkPage />} />
<Route path="config" element={<ConfigPage />} /> <Route path='config' element={<ConfigPage />} />
<Route path="logs" element={<LogsPage />} /> <Route path='logs' element={<LogsPage />} />
<Route path="debug" element={<DebugPage />}> <Route path='debug' element={<DebugPage />}>
<Route path="ws" element={<WSDebug />} /> <Route path='ws' element={<WSDebug />} />
<Route path="http" element={<HttpDebug />} /> <Route path='http' element={<HttpDebug />} />
</Route> </Route>
<Route path="file_manager" element={<FileManagerPage />} /> <Route path='file_manager' element={<FileManagerPage />} />
<Route path="terminal" element={<TerminalPage />} /> <Route path='terminal' element={<TerminalPage />} />
<Route path="about" element={<AboutPage />} /> <Route path='about' element={<AboutPage />} />
</Route> </Route>
<Route path="/qq_login" element={<QQLoginPage />} /> <Route path='/qq_login' element={<QQLoginPage />} />
<Route path="/web_login" element={<WebLoginPage />} /> <Route path='/web_login' element={<WebLoginPage />} />
</Routes> </Routes>
) );
} }
export default App export default App;

View File

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

View File

@@ -1,30 +1,30 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Card, CardBody, CardHeader } from '@heroui/card' import { Card, CardBody, CardHeader } from '@heroui/card';
import { Image } from '@heroui/image' import { Image } from '@heroui/image';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import { Slider } from '@heroui/slider' import { Slider } from '@heroui/slider';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import { useLocalStorage } from '@uidotdev/usehooks' import { useLocalStorage } from '@uidotdev/usehooks';
import clsx from 'clsx' import clsx from 'clsx';
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react';
import { import {
BiSolidSkipNextCircle, BiSolidSkipNextCircle,
BiSolidSkipPreviousCircle BiSolidSkipPreviousCircle,
} from 'react-icons/bi' } from 'react-icons/bi';
import { import {
FaPause, FaPause,
FaPlay, FaPlay,
FaRegHandPointRight, FaRegHandPointRight,
FaRepeat, FaRepeat,
FaShuffle FaShuffle,
} from 'react-icons/fa6' } from 'react-icons/fa6';
import { TbRepeatOnce } from 'react-icons/tb' import { TbRepeatOnce } from 'react-icons/tb';
import { useMediaQuery } from 'react-responsive' import { useMediaQuery } from 'react-responsive';
import { PlayMode } from '@/const/enum' import { PlayMode } from '@/const/enum';
import key from '@/const/key' import key from '@/const/key';
import { VolumeHighIcon, VolumeLowIcon } from './icons' import { VolumeHighIcon, VolumeLowIcon } from './icons';
export interface AudioPlayerProps export interface AudioPlayerProps
extends React.AudioHTMLAttributes<HTMLAudioElement> { extends React.AudioHTMLAttributes<HTMLAudioElement> {
@@ -39,7 +39,7 @@ export interface AudioPlayerProps
mode?: PlayMode mode?: PlayMode
} }
export default function AudioPlayer(props: AudioPlayerProps) { export default function AudioPlayer (props: AudioPlayerProps) {
const { const {
src, src,
pressNext, pressNext,
@@ -56,116 +56,116 @@ export default function AudioPlayer(props: AudioPlayerProps) {
autoPlay, autoPlay,
mode = PlayMode.Loop, mode = PlayMode.Loop,
...rest ...rest
} = props } = props;
const [currentTime, setCurrentTime] = useState(0) const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0) const [duration, setDuration] = useState(0);
const [isPlaying, setIsPlaying] = useState(false) const [isPlaying, setIsPlaying] = useState(false);
const [volume, setVolume] = useState(100) const [volume, setVolume] = useState(100);
const [isCollapsed, setIsCollapsed] = useLocalStorage( const [isCollapsed, setIsCollapsed] = useLocalStorage(
key.isCollapsedMusicPlayer, key.isCollapsedMusicPlayer,
false false
) );
const audioRef = useRef<HTMLAudioElement>(null) const audioRef = useRef<HTMLAudioElement>(null);
const cardRef = useRef<HTMLDivElement>(null) const cardRef = useRef<HTMLDivElement>(null);
const startY = useRef(0) const startY = useRef(0);
const startX = useRef(0) const startX = useRef(0);
const [translateY, setTranslateY] = useState(0) const [translateY, setTranslateY] = useState(0);
const [translateX, setTranslateX] = useState(0) const [translateX, setTranslateX] = useState(0);
const isSmallScreen = useMediaQuery({ maxWidth: 767 }) const isSmallScreen = useMediaQuery({ maxWidth: 767 });
const isMediumUp = useMediaQuery({ minWidth: 768 }) const isMediumUp = useMediaQuery({ minWidth: 768 });
const shouldAdd = useRef(false) const shouldAdd = useRef(false);
const currentProgress = (currentTime / duration) * 100 const currentProgress = (currentTime / duration) * 100;
const [storageAutoPlay, setStorageAutoPlay] = useLocalStorage( const [storageAutoPlay, setStorageAutoPlay] = useLocalStorage(
key.autoPlay, key.autoPlay,
true true
) );
const handleTimeUpdate = (event: React.SyntheticEvent<HTMLAudioElement>) => { const handleTimeUpdate = (event: React.SyntheticEvent<HTMLAudioElement>) => {
const audio = event.target as HTMLAudioElement const audio = event.target as HTMLAudioElement;
setCurrentTime(audio.currentTime) setCurrentTime(audio.currentTime);
onTimeUpdate?.(event) onTimeUpdate?.(event);
} };
const handleLoadedData = (event: React.SyntheticEvent<HTMLAudioElement>) => { const handleLoadedData = (event: React.SyntheticEvent<HTMLAudioElement>) => {
const audio = event.target as HTMLAudioElement const audio = event.target as HTMLAudioElement;
setDuration(audio.duration) setDuration(audio.duration);
onLoadedData?.(event) onLoadedData?.(event);
} };
const handlePlay = (e: React.SyntheticEvent<HTMLAudioElement>) => { const handlePlay = (e: React.SyntheticEvent<HTMLAudioElement>) => {
setIsPlaying(true) setIsPlaying(true);
setStorageAutoPlay(true) setStorageAutoPlay(true);
onPlay?.(e) onPlay?.(e);
} };
const handlePause = (e: React.SyntheticEvent<HTMLAudioElement>) => { const handlePause = (e: React.SyntheticEvent<HTMLAudioElement>) => {
setIsPlaying(false) setIsPlaying(false);
onPause?.(e) onPause?.(e);
} };
const changeMode = () => { const changeMode = () => {
const modes = [PlayMode.Loop, PlayMode.Random, PlayMode.Single] const modes = [PlayMode.Loop, PlayMode.Random, PlayMode.Single];
const currentIndex = modes.findIndex((_mode) => _mode === mode) const currentIndex = modes.findIndex((_mode) => _mode === mode);
const nextIndex = currentIndex + 1 const nextIndex = currentIndex + 1;
const nextMode = modes[nextIndex] || modes[0] const nextMode = modes[nextIndex] || modes[0];
onChangeMode?.(nextMode) onChangeMode?.(nextMode);
} };
const volumeChange = (value: number) => { const volumeChange = (value: number) => {
setVolume(value) setVolume(value);
} };
useEffect(() => { useEffect(() => {
const audio = audioRef.current const audio = audioRef.current;
if (audio) { if (audio) {
audio.volume = volume / 100 audio.volume = volume / 100;
} }
}, [volume]) }, [volume]);
const handleTouchStart = (e: React.TouchEvent) => { const handleTouchStart = (e: React.TouchEvent) => {
startY.current = e.touches[0].clientY startY.current = e.touches[0].clientY;
startX.current = e.touches[0].clientX startX.current = e.touches[0].clientX;
} };
const handleTouchMove = (e: React.TouchEvent) => { const handleTouchMove = (e: React.TouchEvent) => {
const deltaY = e.touches[0].clientY - startY.current const deltaY = e.touches[0].clientY - startY.current;
const deltaX = e.touches[0].clientX - startX.current const deltaX = e.touches[0].clientX - startX.current;
const container = cardRef.current const container = cardRef.current;
const header = cardRef.current?.querySelector('[data-header]') const header = cardRef.current?.querySelector('[data-header]');
const headerHeight = header?.clientHeight || 20 const headerHeight = header?.clientHeight || 20;
const addHeight = (container?.clientHeight || headerHeight) - headerHeight const addHeight = (container?.clientHeight || headerHeight) - headerHeight;
const _shouldAdd = isCollapsed && deltaY < 0 const _shouldAdd = isCollapsed && deltaY < 0;
if (isSmallScreen) { if (isSmallScreen) {
shouldAdd.current = _shouldAdd shouldAdd.current = _shouldAdd;
setTranslateY(_shouldAdd ? deltaY + addHeight : deltaY) setTranslateY(_shouldAdd ? deltaY + addHeight : deltaY);
} else { } else {
setTranslateX(deltaX) setTranslateX(deltaX);
} }
} };
const handleTouchEnd = () => { const handleTouchEnd = () => {
if (isSmallScreen) { if (isSmallScreen) {
const container = cardRef.current const container = cardRef.current;
const header = cardRef.current?.querySelector('[data-header]') const header = cardRef.current?.querySelector('[data-header]');
const headerHeight = header?.clientHeight || 20 const headerHeight = header?.clientHeight || 20;
const addHeight = (container?.clientHeight || headerHeight) - headerHeight const addHeight = (container?.clientHeight || headerHeight) - headerHeight;
const _translateY = translateY - (shouldAdd.current ? addHeight : 0) const _translateY = translateY - (shouldAdd.current ? addHeight : 0);
if (_translateY > 100) { if (_translateY > 100) {
setIsCollapsed(true) setIsCollapsed(true);
} else if (_translateY < -100) { } else if (_translateY < -100) {
setIsCollapsed(false) setIsCollapsed(false);
} }
setTranslateY(0) setTranslateY(0);
} else { } else {
if (translateX > 100) { if (translateX > 100) {
setIsCollapsed(true) setIsCollapsed(true);
} else if (translateX < -100) { } else if (translateX < -100) {
setIsCollapsed(false) setIsCollapsed(false);
} }
setTranslateX(0) setTranslateX(0);
} }
} };
const dragTranslate = isSmallScreen const dragTranslate = isSmallScreen
? translateY ? translateY
@@ -173,16 +173,16 @@ export default function AudioPlayer(props: AudioPlayerProps) {
: '' : ''
: translateX : translateX
? `translateX(${translateX}px)` ? `translateX(${translateX}px)`
: '' : '';
const collapsedTranslate = isCollapsed const collapsedTranslate = isCollapsed
? isSmallScreen ? isSmallScreen
? 'translateY(90%)' ? 'translateY(90%)'
: 'translateX(96%)' : 'translateX(96%)'
: '' : '';
const translateStyle = dragTranslate || collapsedTranslate const translateStyle = dragTranslate || collapsedTranslate;
if (!src) return null if (!src) return null;
return ( return (
<div <div
@@ -192,7 +192,7 @@ export default function AudioPlayer(props: AudioPlayerProps) {
isCollapsed && 'md:hover:!translate-x-80' isCollapsed && 'md:hover:!translate-x-80'
)} )}
style={{ style={{
transform: translateStyle transform: translateStyle,
}} }}
> >
<audio <audio
@@ -216,10 +216,10 @@ export default function AudioPlayer(props: AudioPlayerProps) {
isSmallScreen ? 'rounded-t-3xl' : 'md:rounded-l-xl' isSmallScreen ? 'rounded-t-3xl' : 'md:rounded-l-xl'
)} )}
classNames={{ classNames={{
body: 'p-0' body: 'p-0',
}} }}
shadow="sm" shadow='sm'
radius="none" radius='none'
> >
{isMediumUp && ( {isMediumUp && (
<Button <Button
@@ -230,9 +230,9 @@ export default function AudioPlayer(props: AudioPlayerProps) {
? 'top-0 left-0 w-full h-full rounded-xl bg-opacity-0 hover:bg-opacity-30' ? 'top-0 left-0 w-full h-full rounded-xl bg-opacity-0 hover:bg-opacity-30'
: 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md' : 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md'
)} )}
variant="solid" variant='solid'
color="primary" color='primary'
size="sm" size='sm'
onPress={() => setIsCollapsed(!isCollapsed)} onPress={() => setIsCollapsed(!isCollapsed)}
> >
<FaRegHandPointRight /> <FaRegHandPointRight />
@@ -241,65 +241,65 @@ export default function AudioPlayer(props: AudioPlayerProps) {
{isSmallScreen && ( {isSmallScreen && (
<CardHeader <CardHeader
data-header data-header
className="flex-row justify-center pt-4" className='flex-row justify-center pt-4'
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove} onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd} onTouchEnd={handleTouchEnd}
onClick={() => setIsCollapsed(!isCollapsed)} onClick={() => setIsCollapsed(!isCollapsed)}
> >
<div className="w-24 h-2 rounded-full bg-content2-foreground shadow-sm"></div> <div className='w-24 h-2 rounded-full bg-content2-foreground shadow-sm' />
</CardHeader> </CardHeader>
)} )}
<CardBody> <CardBody>
<div className="grid grid-cols-6 md:grid-cols-12 gap-6 md:gap-4 items-center justify-center overflow-hidden p-6 md:p-2 m-0"> <div className='grid grid-cols-6 md:grid-cols-12 gap-6 md:gap-4 items-center justify-center overflow-hidden p-6 md:p-2 m-0'>
<div className="relative col-span-6 md:col-span-4 flex justify-center"> <div className='relative col-span-6 md:col-span-4 flex justify-center'>
<Image <Image
alt="Album cover" alt='Album cover'
className="object-cover" className='object-cover'
classNames={{ classNames={{
wrapper: 'w-36 aspect-square md:w-24 flex', wrapper: 'w-36 aspect-square md:w-24 flex',
img: 'block w-full h-full' img: 'block w-full h-full',
}} }}
shadow="md" shadow='md'
src={cover} src={cover}
width="100%" width='100%'
/> />
</div> </div>
<div className="flex flex-col col-span-6 md:col-span-8"> <div className='flex flex-col col-span-6 md:col-span-8'>
<div className="flex flex-col gap-0"> <div className='flex flex-col gap-0'>
<h1 className="font-medium truncate">{title}</h1> <h1 className='font-medium truncate'>{title}</h1>
<p className="text-xs text-foreground/80 truncate">{artist}</p> <p className='text-xs text-foreground/80 truncate'>{artist}</p>
</div> </div>
<div className="flex flex-col"> <div className='flex flex-col'>
<Slider <Slider
aria-label="Music progress" aria-label='Music progress'
classNames={{ classNames={{
track: 'bg-default-500/30 border-none', track: 'bg-default-500/30 border-none',
thumb: 'w-2 h-2 after:w-1.5 after:h-1.5', thumb: 'w-2 h-2 after:w-1.5 after:h-1.5',
filler: 'rounded-full' filler: 'rounded-full',
}} }}
color="foreground" color='foreground'
value={currentProgress || 0} value={currentProgress || 0}
defaultValue={0} defaultValue={0}
size="sm" size='sm'
onChange={(value) => { onChange={(value) => {
value = Array.isArray(value) ? value[0] : value value = Array.isArray(value) ? value[0] : value;
const audio = audioRef.current const audio = audioRef.current;
if (audio) { if (audio) {
audio.currentTime = (value / 100) * duration audio.currentTime = (value / 100) * duration;
} }
}} }}
/> />
<div className="flex justify-between h-3"> <div className='flex justify-between h-3'>
<p className="text-xs"> <p className='text-xs'>
{Math.floor(currentTime / 60)}: {Math.floor(currentTime / 60)}:
{Math.floor(currentTime % 60) {Math.floor(currentTime % 60)
.toString() .toString()
.padStart(2, '0')} .padStart(2, '0')}
</p> </p>
<p className="text-xs text-foreground/50"> <p className='text-xs text-foreground/50'>
{Math.floor(duration / 60)}: {Math.floor(duration / 60)}:
{Math.floor(duration % 60) {Math.floor(duration % 60)
.toString() .toString()
@@ -308,7 +308,7 @@ export default function AudioPlayer(props: AudioPlayerProps) {
</div> </div>
</div> </div>
<div className="flex w-full items-center justify-center"> <div className='flex w-full items-center justify-center'>
<Tooltip <Tooltip
content={ content={
mode === PlayMode.Loop mode === PlayMode.Loop
@@ -320,30 +320,30 @@ export default function AudioPlayer(props: AudioPlayerProps) {
> >
<Button <Button
isIconOnly isIconOnly
className="data-[hover]:bg-foreground/10 text-lg md:text-medium" className='data-[hover]:bg-foreground/10 text-lg md:text-medium'
radius="full" radius='full'
variant="light" variant='light'
size="md" size='md'
onPress={changeMode} onPress={changeMode}
> >
{mode === PlayMode.Loop && ( {mode === PlayMode.Loop && (
<FaRepeat className="text-foreground/80" /> <FaRepeat className='text-foreground/80' />
)} )}
{mode === PlayMode.Random && ( {mode === PlayMode.Random && (
<FaShuffle className="text-foreground/80" /> <FaShuffle className='text-foreground/80' />
)} )}
{mode === PlayMode.Single && ( {mode === PlayMode.Single && (
<TbRepeatOnce className="text-foreground/80 text-xl" /> <TbRepeatOnce className='text-foreground/80 text-xl' />
)} )}
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip content="上一首"> <Tooltip content='上一首'>
<Button <Button
isIconOnly isIconOnly
className="data-[hover]:bg-foreground/10 text-2xl md:text-xl" className='data-[hover]:bg-foreground/10 text-2xl md:text-xl'
radius="full" radius='full'
variant="light" variant='light'
size="md" size='md'
onPress={pressPrevious} onPress={pressPrevious}
> >
<BiSolidSkipPreviousCircle /> <BiSolidSkipPreviousCircle />
@@ -352,66 +352,66 @@ export default function AudioPlayer(props: AudioPlayerProps) {
<Tooltip content={isPlaying ? '暂停' : '播放'}> <Tooltip content={isPlaying ? '暂停' : '播放'}>
<Button <Button
isIconOnly isIconOnly
className="data-[hover]:bg-foreground/10 text-3xl md:text-3xl" className='data-[hover]:bg-foreground/10 text-3xl md:text-3xl'
radius="full" radius='full'
variant="light" variant='light'
size="lg" size='lg'
onPress={() => { onPress={() => {
if (isPlaying) { if (isPlaying) {
audioRef.current?.pause() audioRef.current?.pause();
setStorageAutoPlay(false) setStorageAutoPlay(false);
} else { } else {
audioRef.current?.play() audioRef.current?.play();
} }
}} }}
> >
{isPlaying ? <FaPause /> : <FaPlay className="ml-1" />} {isPlaying ? <FaPause /> : <FaPlay className='ml-1' />}
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip content="下一首"> <Tooltip content='下一首'>
<Button <Button
isIconOnly isIconOnly
className="data-[hover]:bg-foreground/10 text-2xl md:text-xl" className='data-[hover]:bg-foreground/10 text-2xl md:text-xl'
radius="full" radius='full'
variant="light" variant='light'
size="md" size='md'
onPress={pressNext} onPress={pressNext}
> >
<BiSolidSkipNextCircle /> <BiSolidSkipNextCircle />
</Button> </Button>
</Tooltip> </Tooltip>
<Popover <Popover
placement="top" placement='top'
classNames={{ classNames={{
content: 'bg-opacity-30 backdrop-blur-md' content: 'bg-opacity-30 backdrop-blur-md',
}} }}
> >
<PopoverTrigger> <PopoverTrigger>
<Button <Button
isIconOnly isIconOnly
className="data-[hover]:bg-foreground/10 text-xl md:text-xl" className='data-[hover]:bg-foreground/10 text-xl md:text-xl'
radius="full" radius='full'
variant="light" variant='light'
size="md" size='md'
> >
<VolumeHighIcon /> <VolumeHighIcon />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent> <PopoverContent>
<Slider <Slider
orientation="vertical" orientation='vertical'
showTooltip showTooltip
aria-label="Volume" aria-label='Volume'
className="h-40" className='h-40'
color="primary" color='primary'
defaultValue={volume} defaultValue={volume}
onChange={(value) => { onChange={(value) => {
value = Array.isArray(value) ? value[0] : value value = Array.isArray(value) ? value[0] : value;
volumeChange(value) volumeChange(value);
}} }}
startContent={<VolumeHighIcon className="text-2xl" />} startContent={<VolumeHighIcon className='text-2xl' />}
size="sm" size='sm'
endContent={<VolumeLowIcon className="text-2xl" />} endContent={<VolumeLowIcon className='text-2xl' />}
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
@@ -421,5 +421,5 @@ export default function AudioPlayer(props: AudioPlayerProps) {
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
) );
} }

View File

@@ -1,87 +1,87 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { import {
Dropdown, Dropdown,
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
DropdownTrigger DropdownTrigger,
} from '@heroui/dropdown' } from '@heroui/dropdown';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import { FaRegCircleQuestion } from 'react-icons/fa6' import { FaRegCircleQuestion } from 'react-icons/fa6';
import { IoAddCircleOutline } from 'react-icons/io5' import { IoAddCircleOutline } from 'react-icons/io5';
import { import {
HTTPClientIcon, HTTPClientIcon,
HTTPServerIcon, HTTPServerIcon,
PCIcon, PCIcon,
PlusIcon, PlusIcon,
WebsocketIcon WebsocketIcon,
} from '../icons' } from '../icons';
export interface AddButtonProps { export interface AddButtonProps {
onOpen: (key: keyof OneBotConfig['network']) => void onOpen: (key: keyof OneBotConfig['network']) => void
} }
const AddButton: React.FC<AddButtonProps> = (props) => { const AddButton: React.FC<AddButtonProps> = (props) => {
const { onOpen } = props const { onOpen } = props;
return ( return (
<Dropdown <Dropdown
classNames={{ classNames={{
content: 'bg-opacity-30 backdrop-blur-md' content: 'bg-opacity-30 backdrop-blur-md',
}} }}
placement="right" placement='right'
> >
<DropdownTrigger> <DropdownTrigger>
<Button <Button
color="primary" color='primary'
startContent={<IoAddCircleOutline className="text-2xl" />} startContent={<IoAddCircleOutline className='text-2xl' />}
> >
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu <DropdownMenu
aria-label="Create Network Config" aria-label='Create Network Config'
color="primary" color='primary'
variant="flat" variant='flat'
onAction={(key) => { onAction={(key) => {
onOpen(key as keyof OneBotConfig['network']) onOpen(key as keyof OneBotConfig['network']);
}} }}
> >
<DropdownItem <DropdownItem
key="title" key='title'
isReadOnly isReadOnly
className="cursor-default hover:!bg-transparent" className='cursor-default hover:!bg-transparent'
textValue="title" textValue='title'
> >
<div className="flex items-center gap-2 justify-center"> <div className='flex items-center gap-2 justify-center'>
<div className="w-5 h-5 -ml-3"> <div className='w-5 h-5 -ml-3'>
<PlusIcon /> <PlusIcon />
</div> </div>
<div className="text-primary-400"></div> <div className='text-primary-400'></div>
</div> </div>
</DropdownItem> </DropdownItem>
<DropdownItem <DropdownItem
key="httpServers" key='httpServers'
textValue="httpServers" textValue='httpServers'
startContent={ startContent={
<div className="w-6 h-6"> <div className='w-6 h-6'>
<HTTPServerIcon /> <HTTPServerIcon />
</div> </div>
} }
> >
<div className="flex gap-1 items-center"> <div className='flex gap-1 items-center'>
HTTP服务器 HTTP服务器
<Tooltip <Tooltip
content="「由NapCat建立」一个HTTP服务器你可以「使用框架连接」此服务器或者「自己构造请求发送」至此服务器。NapCat会根据你配置的IP和端口等建立一个地址你或者你的框架应该连接到这个地址。" content='「由NapCat建立」一个HTTP服务器你可以「使用框架连接」此服务器或者「自己构造请求发送」至此服务器。NapCat会根据你配置的IP和端口等建立一个地址你或者你的框架应该连接到这个地址。'
showArrow showArrow
className="max-w-64" className='max-w-64'
> >
<Button <Button
isIconOnly isIconOnly
radius="full" radius='full'
size="sm" size='sm'
variant="light" variant='light'
className="w-4 h-4 min-w-0" className='w-4 h-4 min-w-0'
> >
<FaRegCircleQuestion /> <FaRegCircleQuestion />
</Button> </Button>
@@ -89,27 +89,27 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
</div> </div>
</DropdownItem> </DropdownItem>
<DropdownItem <DropdownItem
key="httpSseServers" key='httpSseServers'
textValue="httpSseServers" textValue='httpSseServers'
startContent={ startContent={
<div className="w-6 h-6"> <div className='w-6 h-6'>
<HTTPServerIcon /> <HTTPServerIcon />
</div> </div>
} }
> >
<div className="flex gap-1 items-center"> <div className='flex gap-1 items-center'>
HTTP SSE服务器 HTTP SSE服务器
<Tooltip <Tooltip
content="「由NapCat建立」一个HTTP SSE服务器你可以「使用框架连接」此服务器或者「自己构造请求发送」至此服务器。NapCat会根据你配置的IP和端口等建立一个地址你或者你的框架应该连接到这个地址。" content='「由NapCat建立」一个HTTP SSE服务器你可以「使用框架连接」此服务器或者「自己构造请求发送」至此服务器。NapCat会根据你配置的IP和端口等建立一个地址你或者你的框架应该连接到这个地址。'
showArrow showArrow
className="max-w-64" className='max-w-64'
> >
<Button <Button
isIconOnly isIconOnly
radius="full" radius='full'
size="sm" size='sm'
variant="light" variant='light'
className="w-4 h-4 min-w-0" className='w-4 h-4 min-w-0'
> >
<FaRegCircleQuestion /> <FaRegCircleQuestion />
</Button> </Button>
@@ -117,27 +117,27 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
</div> </div>
</DropdownItem> </DropdownItem>
<DropdownItem <DropdownItem
key="httpClients" key='httpClients'
textValue="httpClients" textValue='httpClients'
startContent={ startContent={
<div className="w-6 h-6"> <div className='w-6 h-6'>
<HTTPClientIcon /> <HTTPClientIcon />
</div> </div>
} }
> >
<div className="flex gap-1 items-center"> <div className='flex gap-1 items-center'>
HTTP客户端 HTTP客户端
<Tooltip <Tooltip
content="「由框架或者你自己建立」的一个用于「接收」NapCat向你发送请求的客户端通常框架会提供一个HTTP地址。这个地址是你使用的框架提供的NapCat会主动连接它。" content='「由框架或者你自己建立」的一个用于「接收」NapCat向你发送请求的客户端通常框架会提供一个HTTP地址。这个地址是你使用的框架提供的NapCat会主动连接它。'
showArrow showArrow
className="max-w-64" className='max-w-64'
> >
<Button <Button
isIconOnly isIconOnly
radius="full" radius='full'
size="sm" size='sm'
variant="light" variant='light'
className="w-4 h-4 min-w-0" className='w-4 h-4 min-w-0'
> >
<FaRegCircleQuestion /> <FaRegCircleQuestion />
</Button> </Button>
@@ -145,27 +145,27 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
</div> </div>
</DropdownItem> </DropdownItem>
<DropdownItem <DropdownItem
key="websocketServers" key='websocketServers'
textValue="websocketServers" textValue='websocketServers'
startContent={ startContent={
<div className="w-6 h-6"> <div className='w-6 h-6'>
<WebsocketIcon /> <WebsocketIcon />
</div> </div>
} }
> >
<div className="flex gap-1 items-center"> <div className='flex gap-1 items-center'>
Websocket服务器 Websocket服务器
<Tooltip <Tooltip
content="「由NapCat建立」一个WebSocket服务器你的框架应该连接到此服务器。NapCat会根据你配置的IP和端口等建立一个WebSocket地址你或者你的框架应该连接到这个地址。" content='「由NapCat建立」一个WebSocket服务器你的框架应该连接到此服务器。NapCat会根据你配置的IP和端口等建立一个WebSocket地址你或者你的框架应该连接到这个地址。'
showArrow showArrow
className="max-w-64" className='max-w-64'
> >
<Button <Button
isIconOnly isIconOnly
radius="full" radius='full'
size="sm" size='sm'
variant="light" variant='light'
className="w-4 h-4 min-w-0" className='w-4 h-4 min-w-0'
> >
<FaRegCircleQuestion /> <FaRegCircleQuestion />
</Button> </Button>
@@ -173,27 +173,27 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
</div> </div>
</DropdownItem> </DropdownItem>
<DropdownItem <DropdownItem
key="websocketClients" key='websocketClients'
textValue="websocketClients" textValue='websocketClients'
startContent={ startContent={
<div className="w-6 h-6"> <div className='w-6 h-6'>
<PCIcon /> <PCIcon />
</div> </div>
} }
> >
<div className="flex gap-1 items-center"> <div className='flex gap-1 items-center'>
Websocket客户端 Websocket客户端
<Tooltip <Tooltip
content="「由框架或者你自己建立」的WebSocket通常框架会「提供」一个ws地址NapCat会主动连接它。" content='「由框架或者你自己建立」的WebSocket通常框架会「提供」一个ws地址NapCat会主动连接它。'
showArrow showArrow
className="max-w-64" className='max-w-64'
> >
<Button <Button
isIconOnly isIconOnly
radius="full" radius='full'
size="sm" size='sm'
variant="light" variant='light'
className="w-4 h-4 min-w-0" className='w-4 h-4 min-w-0'
> >
<FaRegCircleQuestion /> <FaRegCircleQuestion />
</Button> </Button>
@@ -202,7 +202,7 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
) );
} };
export default AddButton export default AddButton;

View File

@@ -1,7 +1,7 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import clsx from 'clsx' import clsx from 'clsx';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { IoMdRefresh } from 'react-icons/io' import { IoMdRefresh } from 'react-icons/io';
export interface SaveButtonsProps { export interface SaveButtonsProps {
onSubmit: () => void onSubmit: () => void
@@ -16,7 +16,7 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
reset, reset,
isSubmitting, isSubmitting,
refresh, refresh,
className className,
}) => ( }) => (
<div <div
className={clsx( className={clsx(
@@ -24,18 +24,18 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
className className
)} )}
> >
<div className="flex items-center justify-center gap-2 mt-5"> <div className='flex items-center justify-center gap-2 mt-5'>
<Button <Button
color="default" color='default'
onPress={() => { onPress={() => {
reset() reset();
toast.success('重置成功') toast.success('重置成功');
}} }}
> >
</Button> </Button>
<Button <Button
color="primary" color='primary'
isLoading={isSubmitting} isLoading={isSubmitting}
onPress={() => onSubmit()} onPress={() => onSubmit()}
> >
@@ -44,9 +44,9 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
{refresh && ( {refresh && (
<Button <Button
isIconOnly isIconOnly
color="secondary" color='secondary'
radius="full" radius='full'
variant="flat" variant='flat'
onPress={() => refresh()} onPress={() => refresh()}
> >
<IoMdRefresh size={24} /> <IoMdRefresh size={24} />
@@ -54,6 +54,6 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
)} )}
</div> </div>
</div> </div>
) );
export default SaveButtons export default SaveButtons;

View File

@@ -1,170 +1,170 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import clsx from 'clsx' import clsx from 'clsx';
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { FaMicrophone } from 'react-icons/fa6' import { FaMicrophone } from 'react-icons/fa6';
import { IoMic } from 'react-icons/io5' import { IoMic } from 'react-icons/io5';
import { MdEdit, MdUpload } from 'react-icons/md' import { MdEdit, MdUpload } from 'react-icons/md';
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message' import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
import { isURI } from '@/utils/url' import { isURI } from '@/utils/url';
import type { OB11Segment } from '@/types/onebot' import type { OB11Segment } from '@/types/onebot';
const AudioInsert = () => { const AudioInsert = () => {
const [audioUrl, setAudioUrl] = useState<string>('') const [audioUrl, setAudioUrl] = useState<string>('');
const audioInputRef = useRef<HTMLInputElement>(null) const audioInputRef = useRef<HTMLInputElement>(null);
const showStructuredMessage = useShowStructuredMessage() const showStructuredMessage = useShowStructuredMessage();
const showAudioSegment = (file: string) => { const showAudioSegment = (file: string) => {
const messages: OB11Segment[] = [ const messages: OB11Segment[] = [
{ {
type: 'record', type: 'record',
data: { data: {
file: file file,
} },
} },
] ];
showStructuredMessage(messages) showStructuredMessage(messages);
} };
const [isRecording, setIsRecording] = useState(false) const [isRecording, setIsRecording] = useState(false);
const mediaRecorderRef = useRef<MediaRecorder | null>(null) const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const audioChunksRef = useRef<Blob[]>([]) const audioChunksRef = useRef<Blob[]>([]);
const [audioPreview, setAudioPreview] = useState<string | null>(null) const [audioPreview, setAudioPreview] = useState<string | null>(null);
const [showPreview, setShowPreview] = useState(false) const [showPreview, setShowPreview] = useState(false);
const streamRef = useRef<MediaStream | null>(null) const streamRef = useRef<MediaStream | null>(null);
const [recordingTime, setRecordingTime] = useState(0) const [recordingTime, setRecordingTime] = useState(0);
const recordingIntervalRef = useRef<NodeJS.Timeout | null>(null) const recordingIntervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
if (isRecording) { if (isRecording) {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
streamRef.current = stream streamRef.current = stream;
const recorder = new MediaRecorder(stream) const recorder = new MediaRecorder(stream);
mediaRecorderRef.current = recorder mediaRecorderRef.current = recorder;
recorder.start() recorder.start();
recorder.ondataavailable = (event) => { recorder.ondataavailable = (event) => {
if (event.data.size > 0) { if (event.data.size > 0) {
audioChunksRef.current.push(event.data) audioChunksRef.current.push(event.data);
} }
} };
recorder.onstop = () => { recorder.onstop = () => {
if (audioChunksRef.current.length > 0) { if (audioChunksRef.current.length > 0) {
const audioBlob = new Blob(audioChunksRef.current, { const audioBlob = new Blob(audioChunksRef.current, {
type: 'audio/wav' type: 'audio/wav',
}) });
const reader = new FileReader() const reader = new FileReader();
reader.readAsDataURL(audioBlob) reader.readAsDataURL(audioBlob);
reader.onloadend = () => { reader.onloadend = () => {
const base64Audio = reader.result as string const base64Audio = reader.result as string;
setAudioPreview(base64Audio) setAudioPreview(base64Audio);
setShowPreview(true) setShowPreview(true);
} };
audioChunksRef.current = [] audioChunksRef.current = [];
} }
stream.getTracks().forEach((track) => track.stop()) stream.getTracks().forEach((track) => track.stop());
} };
}) });
recordingIntervalRef.current = setInterval(() => { recordingIntervalRef.current = setInterval(() => {
setRecordingTime((prevTime) => prevTime + 1) setRecordingTime((prevTime) => prevTime + 1);
}, 1000) }, 1000);
} else { } else {
mediaRecorderRef.current?.stop() mediaRecorderRef.current?.stop();
if (recordingIntervalRef.current) { if (recordingIntervalRef.current) {
clearInterval(recordingIntervalRef.current) clearInterval(recordingIntervalRef.current);
recordingIntervalRef.current = null recordingIntervalRef.current = null;
} }
} }
}, [isRecording]) }, [isRecording]);
const startRecording = () => { const startRecording = () => {
setAudioPreview(null) setAudioPreview(null);
setShowPreview(false) setShowPreview(false);
setRecordingTime(0) setRecordingTime(0);
setIsRecording(true) setIsRecording(true);
} };
const stopRecording = () => { const stopRecording = () => {
setIsRecording(false) setIsRecording(false);
} };
const handleShowPreview = () => { const handleShowPreview = () => {
if (audioPreview) { if (audioPreview) {
showAudioSegment(audioPreview) showAudioSegment(audioPreview);
} }
} };
const formatTime = (time: number) => { const formatTime = (time: number) => {
const minutes = Math.floor(time / 60) const minutes = Math.floor(time / 60);
const seconds = time % 60 const seconds = time % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}` return `${minutes}:${seconds.toString().padStart(2, '0')}`;
} };
return ( return (
<> <>
<Popover> <Popover>
<Tooltip content="发送音频"> <Tooltip content='发送音频'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger> <PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full"> <Button color='primary' variant='flat' isIconOnly radius='full'>
<IoMic className="text-xl" /> <IoMic className='text-xl' />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="flex-row gap-2 p-4"> <PopoverContent className='flex-row gap-2 p-4'>
<Tooltip content="上传音频"> <Tooltip content='上传音频'>
<Button <Button
className="text-lg" className='text-lg'
color="primary" color='primary'
isIconOnly isIconOnly
variant="flat" variant='flat'
radius="full" radius='full'
onPress={() => { onPress={() => {
audioInputRef?.current?.click() audioInputRef?.current?.click();
}} }}
> >
<MdUpload /> <MdUpload />
</Button> </Button>
</Tooltip> </Tooltip>
<Popover> <Popover>
<Tooltip content="输入音频地址"> <Tooltip content='输入音频地址'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger tooltip="输入音频地址"> <PopoverTrigger tooltip='输入音频地址'>
<Button <Button
className="text-lg" className='text-lg'
color="primary" color='primary'
isIconOnly isIconOnly
variant="flat" variant='flat'
radius="full" radius='full'
> >
<MdEdit /> <MdEdit />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="flex-row gap-1 p-2"> <PopoverContent className='flex-row gap-1 p-2'>
<Input <Input
value={audioUrl} value={audioUrl}
onChange={(e) => setAudioUrl(e.target.value)} onChange={(e) => setAudioUrl(e.target.value)}
placeholder="请输入音频地址" placeholder='请输入音频地址'
/> />
<Button <Button
color="primary" color='primary'
variant="flat" variant='flat'
isIconOnly isIconOnly
radius="full" radius='full'
onPress={() => { onPress={() => {
if (!isURI(audioUrl)) { if (!isURI(audioUrl)) {
toast.error('请输入正确的音频地址') toast.error('请输入正确的音频地址');
return return;
} }
showAudioSegment(audioUrl) showAudioSegment(audioUrl);
setAudioUrl('') setAudioUrl('');
}} }}
> >
<FaMicrophone /> <FaMicrophone />
@@ -172,34 +172,34 @@ const AudioInsert = () => {
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<Popover> <Popover>
<Tooltip content="录制音频"> <Tooltip content='录制音频'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger> <PopoverTrigger>
<Button <Button
className="text-lg" className='text-lg'
color="primary" color='primary'
isIconOnly isIconOnly
variant="flat" variant='flat'
radius="full" radius='full'
> >
<IoMic /> <IoMic />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="flex-col gap-2 p-4"> <PopoverContent className='flex-col gap-2 p-4'>
<div className="flex gap-2"> <div className='flex gap-2'>
<Button <Button
color={isRecording ? 'primary' : 'primary'} color={isRecording ? 'primary' : 'primary'}
variant="flat" variant='flat'
onPress={isRecording ? stopRecording : startRecording} onPress={isRecording ? stopRecording : startRecording}
> >
{isRecording ? '停止录制' : '开始录制'} {isRecording ? '停止录制' : '开始录制'}
</Button> </Button>
{showPreview && audioPreview && ( {showPreview && audioPreview && (
<Button <Button
color="primary" color='primary'
variant="flat" variant='flat'
onPress={handleShowPreview} onPress={handleShowPreview}
> >
@@ -207,7 +207,7 @@ const AudioInsert = () => {
)} )}
</div> </div>
{(isRecording || audioPreview) && ( {(isRecording || audioPreview) && (
<div className="flex gap-1 items-center"> <div className='flex gap-1 items-center'>
<span <span
className={clsx( className={clsx(
'w-4 h-4 rounded-full', 'w-4 h-4 rounded-full',
@@ -215,7 +215,7 @@ const AudioInsert = () => {
? 'animate-pulse bg-primary-400' ? 'animate-pulse bg-primary-400'
: 'bg-success-400' : 'bg-success-400'
)} )}
></span> />
<span>: {formatTime(recordingTime)}</span> <span>: {formatTime(recordingTime)}</span>
</div> </div>
)} )}
@@ -228,27 +228,27 @@ const AudioInsert = () => {
</Popover> </Popover>
<input <input
type="file" type='file'
ref={audioInputRef} ref={audioInputRef}
hidden hidden
accept="audio/*" accept='audio/*'
className="hidden" className='hidden'
onChange={(e) => { onChange={(e) => {
const file = e.target.files?.[0] const file = e.target.files?.[0];
if (!file) { if (!file) {
return return;
} }
const reader = new FileReader() const reader = new FileReader();
reader.readAsDataURL(file) reader.readAsDataURL(file);
reader.onload = (event) => { reader.onload = (event) => {
const dataURL = event.target?.result const dataURL = event.target?.result;
showAudioSegment(dataURL as string) showAudioSegment(dataURL as string);
e.target.value = '' e.target.value = '';
} };
}} }}
/> />
</> </>
) );
} };
export default AudioInsert export default AudioInsert;

View File

@@ -1,31 +1,31 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import { BsDice3Fill } from 'react-icons/bs' import { BsDice3Fill } from 'react-icons/bs';
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message' import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
const DiceInsert = () => { const DiceInsert = () => {
const showStructuredMessage = useShowStructuredMessage() const showStructuredMessage = useShowStructuredMessage();
return ( return (
<Tooltip content="发送骰子"> <Tooltip content='发送骰子'>
<Button <Button
color="primary" color='primary'
variant="flat" variant='flat'
isIconOnly isIconOnly
radius="full" radius='full'
onPress={() => { onPress={() => {
showStructuredMessage([ showStructuredMessage([
{ {
type: 'dice' type: 'dice',
} },
]) ]);
}} }}
> >
<BsDice3Fill className="text-lg" /> <BsDice3Fill className='text-lg' />
</Button> </Button>
</Tooltip> </Tooltip>
) );
} };
export default DiceInsert export default DiceInsert;

View File

@@ -1,20 +1,20 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Image } from '@heroui/image' import { Image } from '@heroui/image';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import { data, getUrl } from 'qface' import { data, getUrl } from 'qface';
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react';
import { MdEmojiEmotions } from 'react-icons/md' import { MdEmojiEmotions } from 'react-icons/md';
import { EmojiValue } from '../formats/emoji_blot' import { EmojiValue } from '../formats/emoji_blot';
const emojis = data.map((item) => { const emojis = data.map((item) => {
return { return {
alt: item.QDes, alt: item.QDes,
src: getUrl(item.QSid), src: getUrl(item.QSid),
id: item.QSid id: item.QSid,
} as EmojiValue } as EmojiValue;
}) });
export interface EmojiPickerProps { export interface EmojiPickerProps {
onInsertEmoji: (emoji: EmojiValue) => void onInsertEmoji: (emoji: EmojiValue) => void
@@ -22,62 +22,62 @@ export interface EmojiPickerProps {
} }
const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => { const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
const [visibleEmojis, setVisibleEmojis] = useState<EmojiValue[]>([]) const [visibleEmojis, setVisibleEmojis] = useState<EmojiValue[]>([]);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false) const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (isPopoverOpen) { if (isPopoverOpen) {
setVisibleEmojis([]) // Reset visible emojis setVisibleEmojis([]); // Reset visible emojis
requestAnimationFrame(() => loadEmojis()) // Start loading emojis requestAnimationFrame(() => loadEmojis()); // Start loading emojis
} }
}, [isPopoverOpen]) }, [isPopoverOpen]);
const loadEmojis = (index = 0, batchSize = 10) => { const loadEmojis = (index = 0, batchSize = 10) => {
if (index < emojis.length) { if (index < emojis.length) {
setVisibleEmojis((prev) => [ setVisibleEmojis((prev) => [
...prev, ...prev,
...emojis.slice(index, index + batchSize) ...emojis.slice(index, index + batchSize),
]) ]);
requestAnimationFrame(() => loadEmojis(index + batchSize, batchSize)) requestAnimationFrame(() => loadEmojis(index + batchSize, batchSize));
} }
} };
return ( return (
<div ref={containerRef}> <div ref={containerRef}>
<Popover <Popover
portalContainer={containerRef.current!} portalContainer={containerRef.current!}
shouldCloseOnScroll={false} shouldCloseOnScroll={false}
placement="right-start" placement='right-start'
onOpenChange={(v) => { onOpenChange={(v) => {
onOpenChange(v) onOpenChange(v);
setIsPopoverOpen(v) setIsPopoverOpen(v);
}} }}
> >
<Tooltip content="插入表情"> <Tooltip content='插入表情'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger> <PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full"> <Button color='primary' variant='flat' isIconOnly radius='full'>
<MdEmojiEmotions className="text-xl" /> <MdEmojiEmotions className='text-xl' />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="grid grid-cols-8 gap-1 flex-wrap justify-start items-start overflow-y-auto max-w-full max-h-96 p-2"> <PopoverContent className='grid grid-cols-8 gap-1 flex-wrap justify-start items-start overflow-y-auto max-w-full max-h-96 p-2'>
{visibleEmojis.map((emoji) => ( {visibleEmojis.map((emoji) => (
<Button <Button
key={emoji.id} key={emoji.id}
color="primary" color='primary'
variant="flat" variant='flat'
isIconOnly isIconOnly
radius="full" radius='full'
onPress={() => onInsertEmoji(emoji)} onPress={() => onInsertEmoji(emoji)}
> >
<Image src={emoji.src} alt={emoji.alt} className="w-6 h-6" /> <Image src={emoji.src} alt={emoji.alt} className='w-6 h-6' />
</Button> </Button>
))} ))}
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
) );
} };
export default EmojiPicker export default EmojiPicker;

View File

@@ -1,95 +1,95 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import { useRef, useState } from 'react' import { useRef, useState } from 'react';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { FaFolder } from 'react-icons/fa6' import { FaFolder } from 'react-icons/fa6';
import { LuFilePlus2 } from 'react-icons/lu' import { LuFilePlus2 } from 'react-icons/lu';
import { MdEdit, MdUpload } from 'react-icons/md' import { MdEdit, MdUpload } from 'react-icons/md';
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message' import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
import { isURI } from '@/utils/url' import { isURI } from '@/utils/url';
import type { OB11Segment } from '@/types/onebot' import type { OB11Segment } from '@/types/onebot';
const FileInsert = () => { const FileInsert = () => {
const [fileUrl, setFileUrl] = useState<string>('') const [fileUrl, setFileUrl] = useState<string>('');
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null);
const showStructuredMessage = useShowStructuredMessage() const showStructuredMessage = useShowStructuredMessage();
const showFileSegment = (file: string) => { const showFileSegment = (file: string) => {
const messages: OB11Segment[] = [ const messages: OB11Segment[] = [
{ {
type: 'file', type: 'file',
data: { data: {
file: file file,
} },
} },
] ];
showStructuredMessage(messages) showStructuredMessage(messages);
} };
return ( return (
<> <>
<Popover> <Popover>
<Tooltip content="发送文件"> <Tooltip content='发送文件'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger> <PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full"> <Button color='primary' variant='flat' isIconOnly radius='full'>
<FaFolder className="text-lg" /> <FaFolder className='text-lg' />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="flex-row gap-2 p-4"> <PopoverContent className='flex-row gap-2 p-4'>
<Tooltip content="上传文件"> <Tooltip content='上传文件'>
<Button <Button
className="text-lg" className='text-lg'
color="primary" color='primary'
isIconOnly isIconOnly
variant="flat" variant='flat'
radius="full" radius='full'
onPress={() => { onPress={() => {
fileInputRef?.current?.click() fileInputRef?.current?.click();
}} }}
> >
<MdUpload /> <MdUpload />
</Button> </Button>
</Tooltip> </Tooltip>
<Popover> <Popover>
<Tooltip content="输入文件地址"> <Tooltip content='输入文件地址'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger tooltip="输入文件地址"> <PopoverTrigger tooltip='输入文件地址'>
<Button <Button
className="text-lg" className='text-lg'
color="primary" color='primary'
isIconOnly isIconOnly
variant="flat" variant='flat'
radius="full" radius='full'
> >
<MdEdit /> <MdEdit />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="flex-row gap-1 p-2"> <PopoverContent className='flex-row gap-1 p-2'>
<Input <Input
value={fileUrl} value={fileUrl}
onChange={(e) => setFileUrl(e.target.value)} onChange={(e) => setFileUrl(e.target.value)}
placeholder="请输入文件地址" placeholder='请输入文件地址'
/> />
<Button <Button
color="primary" color='primary'
variant="flat" variant='flat'
isIconOnly isIconOnly
radius="full" radius='full'
onPress={() => { onPress={() => {
if (!isURI(fileUrl)) { if (!isURI(fileUrl)) {
toast.error('请输入正确的文件地址') toast.error('请输入正确的文件地址');
return return;
} }
showFileSegment(fileUrl) showFileSegment(fileUrl);
setFileUrl('') setFileUrl('');
}} }}
> >
<LuFilePlus2 /> <LuFilePlus2 />
@@ -100,26 +100,26 @@ const FileInsert = () => {
</Popover> </Popover>
<input <input
type="file" type='file'
ref={fileInputRef} ref={fileInputRef}
hidden hidden
className="hidden" className='hidden'
onChange={(e) => { onChange={(e) => {
const file = e.target.files?.[0] const file = e.target.files?.[0];
if (!file) { if (!file) {
return return;
} }
const reader = new FileReader() const reader = new FileReader();
reader.readAsDataURL(file) reader.readAsDataURL(file);
reader.onload = (event) => { reader.onload = (event) => {
const dataURL = event.target?.result const dataURL = event.target?.result;
showFileSegment(dataURL as string) showFileSegment(dataURL as string);
e.target.value = '' e.target.value = '';
} };
}} }}
/> />
</> </>
) );
} };
export default FileInsert export default FileInsert;

View File

@@ -1,12 +1,12 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import { useRef, useState } from 'react' import { useRef, useState } from 'react';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { MdAddPhotoAlternate, MdEdit, MdImage, MdUpload } from 'react-icons/md' import { MdAddPhotoAlternate, MdEdit, MdImage, MdUpload } from 'react-icons/md';
import { isURI } from '@/utils/url' import { isURI } from '@/utils/url';
export interface ImageInsertProps { export interface ImageInsertProps {
insertImage: (url: string) => void insertImage: (url: string) => void
@@ -14,70 +14,70 @@ export interface ImageInsertProps {
} }
const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => { const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
const [imgUrl, setImgUrl] = useState<string>('') const [imgUrl, setImgUrl] = useState<string>('');
const imageInputRef = useRef<HTMLInputElement>(null) const imageInputRef = useRef<HTMLInputElement>(null);
return ( return (
<> <>
<Popover onOpenChange={onOpenChange}> <Popover onOpenChange={onOpenChange}>
<Tooltip content="插入图片"> <Tooltip content='插入图片'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger> <PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full"> <Button color='primary' variant='flat' isIconOnly radius='full'>
<MdImage className="text-xl" /> <MdImage className='text-xl' />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="flex-row gap-2 p-4"> <PopoverContent className='flex-row gap-2 p-4'>
<Tooltip content="上传图片"> <Tooltip content='上传图片'>
<Button <Button
className="text-lg" className='text-lg'
color="primary" color='primary'
isIconOnly isIconOnly
variant="flat" variant='flat'
radius="full" radius='full'
onPress={() => { onPress={() => {
imageInputRef?.current?.click() imageInputRef?.current?.click();
}} }}
> >
<MdUpload /> <MdUpload />
</Button> </Button>
</Tooltip> </Tooltip>
<Popover> <Popover>
<Tooltip content="输入图片地址"> <Tooltip content='输入图片地址'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger tooltip="输入图片地址"> <PopoverTrigger tooltip='输入图片地址'>
<Button <Button
className="text-lg" className='text-lg'
color="primary" color='primary'
isIconOnly isIconOnly
variant="flat" variant='flat'
radius="full" radius='full'
> >
<MdEdit /> <MdEdit />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="flex-row gap-1 p-2"> <PopoverContent className='flex-row gap-1 p-2'>
<Input <Input
value={imgUrl} value={imgUrl}
onChange={(e) => setImgUrl(e.target.value)} onChange={(e) => setImgUrl(e.target.value)}
placeholder="请输入图片地址" placeholder='请输入图片地址'
/> />
<Button <Button
color="primary" color='primary'
variant="flat" variant='flat'
isIconOnly isIconOnly
radius="full" radius='full'
onPress={() => { onPress={() => {
if (!isURI(imgUrl)) { if (!isURI(imgUrl)) {
toast.error('请输入正确的图片地址') toast.error('请输入正确的图片地址');
return return;
} }
insertImage(imgUrl) insertImage(imgUrl);
setImgUrl('') setImgUrl('');
}} }}
> >
<MdAddPhotoAlternate /> <MdAddPhotoAlternate />
@@ -88,27 +88,27 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
</Popover> </Popover>
<input <input
type="file" type='file'
ref={imageInputRef} ref={imageInputRef}
hidden hidden
accept="image/*" accept='image/*'
className="hidden" className='hidden'
onChange={(e) => { onChange={(e) => {
const file = e.target.files?.[0] const file = e.target.files?.[0];
if (!file) { if (!file) {
return return;
} }
const reader = new FileReader() const reader = new FileReader();
reader.readAsDataURL(file) reader.readAsDataURL(file);
reader.onload = (event) => { reader.onload = (event) => {
const dataURL = event.target?.result const dataURL = event.target?.result;
insertImage(dataURL as string) insertImage(dataURL as string);
e.target.value = '' e.target.value = '';
} };
}} }}
/> />
</> </>
) );
} };
export default ImageInsert export default ImageInsert;

View File

@@ -1,35 +1,35 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Form } from '@heroui/form' import { Form } from '@heroui/form';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import { Select, SelectItem } from '@heroui/select' import { Select, SelectItem } from '@heroui/select';
import type { SharedSelection } from '@heroui/system' import type { SharedSelection } from '@heroui/system';
import { Tab, Tabs } from '@heroui/tabs' import { Tab, Tabs } from '@heroui/tabs';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import type { Key } from '@react-types/shared' import type { Key } from '@react-types/shared';
import { useRef, useState } from 'react' import { useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form' import { Controller, useForm } from 'react-hook-form';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { IoMusicalNotes } from 'react-icons/io5' import { IoMusicalNotes } from 'react-icons/io5';
import { TbMusicPlus } from 'react-icons/tb' import { TbMusicPlus } from 'react-icons/tb';
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message' import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
import { isURI } from '@/utils/url' import { isURI } from '@/utils/url';
import type { import type {
CustomMusicSegment, CustomMusicSegment,
MusicSegment, MusicSegment,
OB11Segment OB11Segment,
} from '@/types/onebot' } from '@/types/onebot';
type MusicData = CustomMusicSegment['data'] | MusicSegment['data'] type MusicData = CustomMusicSegment['data'] | MusicSegment['data'];
const MusicInsert = () => { const MusicInsert = () => {
const [musicId, setMusicId] = useState<string>('') const [musicId, setMusicId] = useState<string>('');
const [musicType, setMusicType] = useState<SharedSelection>(new Set(['163'])) const [musicType, setMusicType] = useState<SharedSelection>(new Set(['163']));
const [mode, setMode] = useState<Key>('default') const [mode, setMode] = useState<Key>('default');
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null);
const { control, handleSubmit, reset } = useForm< const { control, handleSubmit, reset } = useForm<
Omit<CustomMusicSegment['data'], 'type'> Omit<CustomMusicSegment['data'], 'type'>
>({ >({
@@ -38,82 +38,84 @@ const MusicInsert = () => {
audio: '', audio: '',
title: '', title: '',
image: '', image: '',
content: '' content: '',
} },
}) });
const showStructuredMessage = useShowStructuredMessage() const showStructuredMessage = useShowStructuredMessage();
const showMusicSegment = (data: MusicData) => { const showMusicSegment = (data: MusicData) => {
const messages: OB11Segment[] = [] const messages: OB11Segment[] = [];
if (data.type === 'custom') { if (data.type === 'custom') {
messages.push({ messages.push({
type: 'music', type: 'music',
data: { data: {
...data, ...data,
type: 'custom' type: 'custom',
} },
}) });
} else { } else {
messages.push({ messages.push({
type: 'music', type: 'music',
data data,
}) });
} }
showStructuredMessage(messages) showStructuredMessage(messages);
} };
const onSubmit = (data: Omit<CustomMusicSegment['data'], 'type'>) => { const onSubmit = (data: Omit<CustomMusicSegment['data'], 'type'>) => {
showMusicSegment({ showMusicSegment({
type: 'custom', type: 'custom',
...data ...data,
}) });
reset() reset();
} };
return ( return (
<div ref={containerRef} className="overflow-visible"> <div ref={containerRef} className='overflow-visible'>
<Popover <Popover
placement="right-start" placement='right-start'
shouldCloseOnScroll={false} shouldCloseOnScroll={false}
portalContainer={containerRef.current!} portalContainer={containerRef.current!}
> >
<Tooltip content="发送音乐"> <Tooltip content='发送音乐'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger> <PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full"> <Button color='primary' variant='flat' isIconOnly radius='full'>
<IoMusicalNotes className="text-xl" /> <IoMusicalNotes className='text-xl' />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="gap-2 p-4"> <PopoverContent className='gap-2 p-4'>
<Tabs <Tabs
placement="top" placement='top'
className="w-96" className='w-96'
fullWidth fullWidth
selectedKey={mode} selectedKey={mode}
onSelectionChange={setMode} onSelectionChange={(key) => {
if (key !== null) setMode(key);
}}
> >
<Tab title="主流平台" key="default" className="flex flex-col gap-2"> <Tab title='主流平台' key='default' className='flex flex-col gap-2'>
<Select <Select
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
aria-label="音乐平台" aria-label='音乐平台'
selectedKeys={musicType} selectedKeys={musicType}
label="音乐平台" label='音乐平台'
placeholder="请选择音乐平台" placeholder='请选择音乐平台'
items={[ items={[
{ {
name: 'QQ音乐', name: 'QQ音乐',
id: 'qq' id: 'qq',
}, },
{ {
name: '网易云音乐', name: '网易云音乐',
id: '163' id: '163',
}, },
{ {
name: '虾米音乐', name: '虾米音乐',
id: 'xm' id: 'xm',
} },
]} ]}
onSelectionChange={setMusicType} onSelectionChange={setMusicType}
> >
@@ -126,27 +128,27 @@ const MusicInsert = () => {
<Input <Input
value={musicId} value={musicId}
onChange={(e) => setMusicId(e.target.value)} onChange={(e) => setMusicId(e.target.value)}
placeholder="请输入音乐ID" placeholder='请输入音乐ID'
label="音乐ID" label='音乐ID'
/> />
<Button <Button
fullWidth fullWidth
size="lg" size='lg'
color="primary" color='primary'
variant="flat" variant='flat'
radius="full" radius='full'
onPress={() => { onPress={() => {
if (!musicId) { if (!musicId) {
toast.error('请输入音乐ID') toast.error('请输入音乐ID');
return return;
} }
showMusicSegment({ showMusicSegment({
type: Array.from( type: Array.from(
musicType musicType
)[0] as MusicSegment['data']['type'], )[0] as MusicSegment['data']['type'],
id: musicId id: musicId,
}) });
setMusicId('') setMusicId('');
}} }}
startContent={<TbMusicPlus />} startContent={<TbMusicPlus />}
> >
@@ -154,92 +156,92 @@ const MusicInsert = () => {
</Button> </Button>
</Tab> </Tab>
<Tab <Tab
title="自定义音乐" title='自定义音乐'
key="custom" key='custom'
className="flex flex-col gap-2" className='flex flex-col gap-2'
> >
<Form <Form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-2" className='flex flex-col gap-2'
validationBehavior="native" validationBehavior='native'
> >
<Controller <Controller
name="url" name='url'
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Input <Input
{...field} {...field}
isRequired isRequired
validate={(v) => { validate={(v) => {
return !isURI(v) ? '请输入正确的音乐URL' : null return !isURI(v) ? '请输入正确的音乐URL' : null;
}} }}
size="sm" size='sm'
placeholder="请输入音乐URL" placeholder='请输入音乐URL'
label="音乐URL" label='音乐URL'
/> />
)} )}
/> />
<Controller <Controller
name="audio" name='audio'
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Input <Input
{...field} {...field}
isRequired isRequired
validate={(v) => { validate={(v) => {
return !isURI(v) ? '请输入正确的音频URL' : null return !isURI(v) ? '请输入正确的音频URL' : null;
}} }}
size="sm" size='sm'
placeholder="请输入音频URL" placeholder='请输入音频URL'
label="音频URL" label='音频URL'
/> />
)} )}
/> />
<Controller <Controller
name="title" name='title'
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Input <Input
{...field} {...field}
isRequired isRequired
size="sm" size='sm'
errorMessage="请输入音乐标题" errorMessage='请输入音乐标题'
placeholder="请输入音乐标题" placeholder='请输入音乐标题'
label="音乐标题" label='音乐标题'
/> />
)} )}
/> />
<Controller <Controller
name="image" name='image'
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Input <Input
{...field} {...field}
size="sm" size='sm'
placeholder="请输入封面图片URL" placeholder='请输入封面图片URL'
label="封面图片URL" label='封面图片URL'
/> />
)} )}
/> />
<Controller <Controller
name="content" name='content'
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Input <Input
{...field} {...field}
size="sm" size='sm'
placeholder="请输入音乐描述" placeholder='请输入音乐描述'
label="音乐描述" label='音乐描述'
/> />
)} )}
/> />
<Button <Button
fullWidth fullWidth
size="lg" size='lg'
color="primary" color='primary'
variant="flat" variant='flat'
radius="full" radius='full'
type="submit" type='submit'
startContent={<TbMusicPlus />} startContent={<TbMusicPlus />}
> >
@@ -250,7 +252,7 @@ const MusicInsert = () => {
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
) );
} };
export default MusicInsert export default MusicInsert;

View File

@@ -1,50 +1,50 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import { useState } from 'react' import { useState } from 'react';
import { BsChatQuoteFill } from 'react-icons/bs' import { BsChatQuoteFill } from 'react-icons/bs';
import { MdAdd } from 'react-icons/md' import { MdAdd } from 'react-icons/md';
export interface ReplyInsertProps { export interface ReplyInsertProps {
insertReply: (messageId: string) => void insertReply: (messageId: string) => void
} }
const ReplyInsert = ({ insertReply }: ReplyInsertProps) => { const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
const [replyId, setReplyId] = useState<string>('') const [replyId, setReplyId] = useState<string>('');
return ( return (
<> <>
<Popover> <Popover>
<Tooltip content="回复消息"> <Tooltip content='回复消息'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger> <PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full"> <Button color='primary' variant='flat' isIconOnly radius='full'>
<BsChatQuoteFill className="text-lg" /> <BsChatQuoteFill className='text-lg' />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="flex-row gap-2 p-4"> <PopoverContent className='flex-row gap-2 p-4'>
<Input <Input
placeholder="输入消息 ID" placeholder='输入消息 ID'
value={replyId} value={replyId}
onChange={(e) => { onChange={(e) => {
const value = e.target.value const value = e.target.value;
const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/ const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/;
if (isNumberReg.test(value)) { if (isNumberReg.test(value)) {
setReplyId(value) setReplyId(value);
} }
}} }}
/> />
<Button <Button
color="primary" color='primary'
variant="flat" variant='flat'
radius="full" radius='full'
isIconOnly isIconOnly
onPress={() => { onPress={() => {
insertReply(replyId) insertReply(replyId);
setReplyId('') setReplyId('');
}} }}
> >
<MdAdd /> <MdAdd />
@@ -52,7 +52,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</> </>
) );
} };
export default ReplyInsert export default ReplyInsert;

View File

@@ -1,31 +1,31 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import { LiaHandScissors } from 'react-icons/lia' import { LiaHandScissors } from 'react-icons/lia';
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message' import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
const RPSInsert = () => { const RPSInsert = () => {
const showStructuredMessage = useShowStructuredMessage() const showStructuredMessage = useShowStructuredMessage();
return ( return (
<Tooltip content="发送猜拳"> <Tooltip content='发送猜拳'>
<Button <Button
color="primary" color='primary'
variant="flat" variant='flat'
isIconOnly isIconOnly
radius="full" radius='full'
onPress={() => { onPress={() => {
showStructuredMessage([ showStructuredMessage([
{ {
type: 'rps' type: 'rps',
} },
]) ]);
}} }}
> >
<LiaHandScissors className="text-2xl" /> <LiaHandScissors className='text-2xl' />
</Button> </Button>
</Tooltip> </Tooltip>
) );
} };
export default RPSInsert export default RPSInsert;

View File

@@ -1,6 +1,6 @@
import { Snippet } from '@heroui/snippet' import { Snippet } from '@heroui/snippet';
import { OB11Segment } from '@/types/onebot' import { OB11Segment } from '@/types/onebot';
export interface ShowStructedMessageProps { export interface ShowStructedMessageProps {
messages: OB11Segment[] messages: OB11Segment[]
@@ -11,22 +11,22 @@ const ShowStructedMessage = ({ messages }: ShowStructedMessageProps) => {
<Snippet <Snippet
hideSymbol hideSymbol
tooltipProps={{ tooltipProps={{
content: '点击复制' content: '点击复制',
}} }}
classNames={{ classNames={{
copyButton: 'self-start sticky top-0 right-0' copyButton: 'self-start sticky top-0 right-0',
}} }}
className="bg-content1 h-96 overflow-y-scroll items-start" className='bg-content1 h-96 overflow-y-scroll items-start'
> >
{JSON.stringify(messages, null, 2) {JSON.stringify(messages, null, 2)
.split('\n') .split('\n')
.map((line, i) => ( .map((line, i) => (
<span key={i} className="whitespace-pre-wrap break-all"> <span key={i} className='whitespace-pre-wrap break-all'>
{line} {line}
</span> </span>
))} ))}
</Snippet> </Snippet>
) );
} };
export default ShowStructedMessage export default ShowStructedMessage;

View File

@@ -1,95 +1,95 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import { useRef, useState } from 'react' import { useRef, useState } from 'react';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { IoVideocam } from 'react-icons/io5' import { IoVideocam } from 'react-icons/io5';
import { MdEdit, MdUpload } from 'react-icons/md' import { MdEdit, MdUpload } from 'react-icons/md';
import { TbVideoPlus } from 'react-icons/tb' import { TbVideoPlus } from 'react-icons/tb';
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message' import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
import { isURI } from '@/utils/url' import { isURI } from '@/utils/url';
import type { OB11Segment } from '@/types/onebot' import type { OB11Segment } from '@/types/onebot';
const VideoInsert = () => { const VideoInsert = () => {
const [videoUrl, setVideoUrl] = useState<string>('') const [videoUrl, setVideoUrl] = useState<string>('');
const videoInputRef = useRef<HTMLInputElement>(null) const videoInputRef = useRef<HTMLInputElement>(null);
const showStructuredMessage = useShowStructuredMessage() const showStructuredMessage = useShowStructuredMessage();
const showVideoSegment = (file: string) => { const showVideoSegment = (file: string) => {
const messages: OB11Segment[] = [ const messages: OB11Segment[] = [
{ {
type: 'video', type: 'video',
data: { data: {
file: file file,
} },
} },
] ];
showStructuredMessage(messages) showStructuredMessage(messages);
} };
return ( return (
<> <>
<Popover> <Popover>
<Tooltip content="发送视频"> <Tooltip content='发送视频'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger> <PopoverTrigger>
<Button color="primary" variant="flat" isIconOnly radius="full"> <Button color='primary' variant='flat' isIconOnly radius='full'>
<IoVideocam className="text-xl" /> <IoVideocam className='text-xl' />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="flex-row gap-2 p-4"> <PopoverContent className='flex-row gap-2 p-4'>
<Tooltip content="上传视频"> <Tooltip content='上传视频'>
<Button <Button
className="text-lg" className='text-lg'
color="primary" color='primary'
isIconOnly isIconOnly
variant="flat" variant='flat'
radius="full" radius='full'
onPress={() => { onPress={() => {
videoInputRef?.current?.click() videoInputRef?.current?.click();
}} }}
> >
<MdUpload /> <MdUpload />
</Button> </Button>
</Tooltip> </Tooltip>
<Popover> <Popover>
<Tooltip content="输入视频地址"> <Tooltip content='输入视频地址'>
<div className="max-w-fit"> <div className='max-w-fit'>
<PopoverTrigger tooltip="输入视频地址"> <PopoverTrigger tooltip='输入视频地址'>
<Button <Button
className="text-lg" className='text-lg'
color="primary" color='primary'
isIconOnly isIconOnly
variant="flat" variant='flat'
radius="full" radius='full'
> >
<MdEdit /> <MdEdit />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </div>
</Tooltip> </Tooltip>
<PopoverContent className="flex-row gap-1 p-2"> <PopoverContent className='flex-row gap-1 p-2'>
<Input <Input
value={videoUrl} value={videoUrl}
onChange={(e) => setVideoUrl(e.target.value)} onChange={(e) => setVideoUrl(e.target.value)}
placeholder="请输入视频地址" placeholder='请输入视频地址'
/> />
<Button <Button
color="primary" color='primary'
variant="flat" variant='flat'
isIconOnly isIconOnly
radius="full" radius='full'
onPress={() => { onPress={() => {
if (!isURI(videoUrl)) { if (!isURI(videoUrl)) {
toast.error('请输入正确的视频地址') toast.error('请输入正确的视频地址');
return return;
} }
showVideoSegment(videoUrl) showVideoSegment(videoUrl);
setVideoUrl('') setVideoUrl('');
}} }}
> >
<TbVideoPlus /> <TbVideoPlus />
@@ -100,27 +100,27 @@ const VideoInsert = () => {
</Popover> </Popover>
<input <input
type="file" type='file'
ref={videoInputRef} ref={videoInputRef}
hidden hidden
accept="video/*" accept='video/*'
className="hidden" className='hidden'
onChange={(e) => { onChange={(e) => {
const file = e.target.files?.[0] const file = e.target.files?.[0];
if (!file) { if (!file) {
return return;
} }
const reader = new FileReader() const reader = new FileReader();
reader.readAsDataURL(file) reader.readAsDataURL(file);
reader.onload = (event) => { reader.onload = (event) => {
const dataURL = event.target?.result const dataURL = event.target?.result;
showVideoSegment(dataURL as string) showVideoSegment(dataURL as string);
e.target.value = '' e.target.value = '';
} };
}} }}
/> />
</> </>
) );
} };
export default VideoInsert export default VideoInsert;

View File

@@ -1,4 +1,4 @@
import Quill from 'quill' import Quill from 'quill';
// eslint-disable-next-line // eslint-disable-next-line
const Embed = Quill.import('blots/embed') as any const Embed = Quill.import('blots/embed') as any
@@ -8,34 +8,34 @@ export interface EmojiValue {
id: string id: string
} }
class EmojiBlot extends Embed { class EmojiBlot extends Embed {
static blotName: string = 'emoji' static blotName: string = 'emoji';
static tagName: string = 'img' static tagName: string = 'img';
static classNames: string[] = ['w-6', 'h-6'] static classNames: string[] = ['w-6', 'h-6'];
static create(value: HTMLImageElement) { static create (value: HTMLImageElement) {
const node = super.create(value) const node = super.create(value);
node.setAttribute('alt', value.alt) node.setAttribute('alt', value.alt);
node.setAttribute('src', value.src) node.setAttribute('src', value.src);
node.setAttribute('data-id', value.id) node.setAttribute('data-id', value.id);
node.classList.add(...EmojiBlot.classNames) node.classList.add(...EmojiBlot.classNames);
return node return node;
} }
static formats(node: HTMLImageElement): EmojiValue { static formats (node: HTMLImageElement): EmojiValue {
return { return {
alt: node.getAttribute('alt') ?? '', alt: node.getAttribute('alt') ?? '',
src: node.getAttribute('src') ?? '', src: node.getAttribute('src') ?? '',
id: node.getAttribute('data-id') ?? '' id: node.getAttribute('data-id') ?? '',
} };
} }
static value(node: HTMLImageElement): EmojiValue { static value (node: HTMLImageElement): EmojiValue {
return { return {
alt: node.getAttribute('alt') ?? '', alt: node.getAttribute('alt') ?? '',
src: node.getAttribute('src') ?? '', src: node.getAttribute('src') ?? '',
id: node.getAttribute('data-id') ?? '' id: node.getAttribute('data-id') ?? '',
} };
} }
} }
export default EmojiBlot export default EmojiBlot;

View File

@@ -1,4 +1,4 @@
import Quill from 'quill' import Quill from 'quill';
// eslint-disable-next-line // eslint-disable-next-line
const Embed = Quill.import('blots/embed') as any const Embed = Quill.import('blots/embed') as any
@@ -7,24 +7,24 @@ export interface ImageValue {
src: string src: string
} }
class ImageBlot extends Embed { class ImageBlot extends Embed {
static blotName = 'image' static blotName = 'image';
static tagName = 'img' static tagName = 'img';
static classNames: string[] = ['max-w-48', 'max-h-48', 'align-bottom'] static classNames: string[] = ['max-w-48', 'max-h-48', 'align-bottom'];
static create(value: ImageValue) { static create (value: ImageValue) {
let node = super.create() const node = super.create();
node.setAttribute('alt', value.alt) node.setAttribute('alt', value.alt);
node.setAttribute('src', value.src) node.setAttribute('src', value.src);
node.classList.add(...ImageBlot.classNames) node.classList.add(...ImageBlot.classNames);
return node return node;
} }
static value(node: HTMLImageElement): ImageValue { static value (node: HTMLImageElement): ImageValue {
return { return {
alt: node.getAttribute('alt') ?? '', alt: node.getAttribute('alt') ?? '',
src: node.getAttribute('src') ?? '' src: node.getAttribute('src') ?? '',
} };
} }
} }
export default ImageBlot export default ImageBlot;

View File

@@ -1,4 +1,4 @@
import Quill from 'quill' import Quill from 'quill';
// eslint-disable-next-line // eslint-disable-next-line
const BlockEmbed = Quill.import('blots/block/embed') as any const BlockEmbed = Quill.import('blots/block/embed') as any
@@ -6,38 +6,38 @@ export interface ReplyBlockValue {
messageId: string messageId: string
} }
class ReplyBlock extends BlockEmbed { class ReplyBlock extends BlockEmbed {
static blotName = 'reply' static blotName = 'reply';
static tagName = 'div' static tagName = 'div';
static classNames = [ static classNames = [
'p-2', 'p-2',
'select-none', 'select-none',
'bg-default-100', 'bg-default-100',
'rounded-md', 'rounded-md',
'pointer-events-none' 'pointer-events-none',
] ];
static create(value: ReplyBlockValue) { static create (value: ReplyBlockValue) {
const node = super.create() const node = super.create();
node.setAttribute('data-message-id', value.messageId) node.setAttribute('data-message-id', value.messageId);
node.setAttribute('contenteditable', 'false') node.setAttribute('contenteditable', 'false');
node.classList.add(...ReplyBlock.classNames) node.classList.add(...ReplyBlock.classNames);
const innerDom = document.createElement('div') const innerDom = document.createElement('div');
innerDom.classList.add('text-sm', 'text-default-500', 'relative') innerDom.classList.add('text-sm', 'text-default-500', 'relative');
const svgContainer = document.createElement('div') const svgContainer = document.createElement('div');
svgContainer.classList.add('w-3', 'h-3', 'absolute', 'top-0', 'right-0') svgContainer.classList.add('w-3', 'h-3', 'absolute', 'top-0', 'right-0');
const svg = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M15.9082 12.3714H20.5982C20.5182 17.0414 19.5982 17.8114 16.7282 19.5114C16.3982 19.7114 16.2882 20.1314 16.4882 20.4714C16.6882 20.8014 17.1082 20.9114 17.4482 20.7114C20.8282 18.7114 22.0082 17.4914 22.0082 11.6714V6.28141C22.0082 4.57141 20.6182 3.19141 18.9182 3.19141H15.9182C14.1582 3.19141 12.8282 4.52141 12.8282 6.28141V9.28141C12.8182 11.0414 14.1482 12.3714 15.9082 12.3714Z" fill="#292D32"></path> <path d="M5.09 12.3714H9.78C9.7 17.0414 8.78 17.8114 5.91 19.5114C5.58 19.7114 5.47 20.1314 5.67 20.4714C5.87 20.8014 6.29 20.9114 6.63 20.7114C10.01 18.7114 11.19 17.4914 11.19 11.6714V6.28141C11.19 4.57141 9.8 3.19141 8.1 3.19141H5.1C3.33 3.19141 2 4.52141 2 6.28141V9.28141C2 11.0414 3.33 12.3714 5.09 12.3714Z" fill="#292D32"></path> </g></svg>` const svg = '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M15.9082 12.3714H20.5982C20.5182 17.0414 19.5982 17.8114 16.7282 19.5114C16.3982 19.7114 16.2882 20.1314 16.4882 20.4714C16.6882 20.8014 17.1082 20.9114 17.4482 20.7114C20.8282 18.7114 22.0082 17.4914 22.0082 11.6714V6.28141C22.0082 4.57141 20.6182 3.19141 18.9182 3.19141H15.9182C14.1582 3.19141 12.8282 4.52141 12.8282 6.28141V9.28141C12.8182 11.0414 14.1482 12.3714 15.9082 12.3714Z" fill="#292D32"></path> <path d="M5.09 12.3714H9.78C9.7 17.0414 8.78 17.8114 5.91 19.5114C5.58 19.7114 5.47 20.1314 5.67 20.4714C5.87 20.8014 6.29 20.9114 6.63 20.7114C10.01 18.7114 11.19 17.4914 11.19 11.6714V6.28141C11.19 4.57141 9.8 3.19141 8.1 3.19141H5.1C3.33 3.19141 2 4.52141 2 6.28141V9.28141C2 11.0414 3.33 12.3714 5.09 12.3714Z" fill="#292D32"></path> </g></svg>';
svgContainer.innerHTML = svg svgContainer.innerHTML = svg;
innerDom.innerHTML = `消息ID${value.messageId}` innerDom.innerHTML = `消息ID${value.messageId}`;
innerDom.appendChild(svgContainer) innerDom.appendChild(svgContainer);
node.appendChild(innerDom) node.appendChild(innerDom);
return node return node;
} }
static value(node: HTMLElement): ReplyBlockValue { static value (node: HTMLElement): ReplyBlockValue {
return { return {
messageId: node.getAttribute('data-message-id') || '' messageId: node.getAttribute('data-message-id') || '',
} };
} }
} }
export default ReplyBlock export default ReplyBlock;

View File

@@ -1,55 +1,55 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import type { Range } from 'quill' import type { Range } from 'quill';
import 'quill/dist/quill.core.css' import 'quill/dist/quill.core.css';
import { useRef } from 'react' import { useRef } from 'react';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { useCustomQuill } from '@/hooks/use_custom_quill' import { useCustomQuill } from '@/hooks/use_custom_quill';
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message' import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
import { quillToMessage } from '@/utils/onebot' import { quillToMessage } from '@/utils/onebot';
import type { OB11Segment } from '@/types/onebot' import type { OB11Segment } from '@/types/onebot';
import AudioInsert from './components/audio_insert' import AudioInsert from './components/audio_insert';
import DiceInsert from './components/dice_insert' import DiceInsert from './components/dice_insert';
import EmojiPicker from './components/emoji_picker' import EmojiPicker from './components/emoji_picker';
import FileInsert from './components/file_insert' import FileInsert from './components/file_insert';
import ImageInsert from './components/image_insert' import ImageInsert from './components/image_insert';
import MusicInsert from './components/music_insert' import MusicInsert from './components/music_insert';
import ReplyInsert from './components/reply_insert' import ReplyInsert from './components/reply_insert';
import RPSInsert from './components/rps_insert' import RPSInsert from './components/rps_insert';
import VideoInsert from './components/video_insert' import VideoInsert from './components/video_insert';
import EmojiBlot from './formats/emoji_blot' import EmojiBlot from './formats/emoji_blot';
import type { EmojiValue } from './formats/emoji_blot' import type { EmojiValue } from './formats/emoji_blot';
import ImageBlot from './formats/image_blot' import ImageBlot from './formats/image_blot';
import ReplyBlock from './formats/reply_blot' import ReplyBlock from './formats/reply_blot';
const ChatInput = () => { const ChatInput = () => {
const memorizedRange = useRef<Range | null>(null) const memorizedRange = useRef<Range | null>(null);
const showStructuredMessage = useShowStructuredMessage() const showStructuredMessage = useShowStructuredMessage();
const formats: string[] = ['image', 'emoji', 'reply'] const formats: string[] = ['image', 'emoji', 'reply'];
const modules = { const modules = {
toolbar: '#toolbar' toolbar: '#toolbar',
} };
const { quillRef, quill, Quill } = useCustomQuill({ const { quillRef, quill, Quill } = useCustomQuill({
modules, modules,
formats, formats,
placeholder: '请输入消息' placeholder: '请输入消息',
}) });
if (Quill && !quill) { if (Quill && !quill) {
Quill.register('formats/emoji', EmojiBlot) Quill.register('formats/emoji', EmojiBlot);
Quill.register('formats/image', ImageBlot, true) Quill.register('formats/image', ImageBlot, true);
Quill.register('formats/reply', ReplyBlock) Quill.register('formats/reply', ReplyBlock);
} }
if (quill) { if (quill) {
quill.on('selection-change', (range) => { quill.on('selection-change', (range) => {
if (range) { if (range) {
const editorContent = quill.getContents() const editorContent = quill.getContents();
const firstOp = editorContent.ops[0] const firstOp = editorContent.ops[0];
if ( if (
typeof firstOp?.insert !== 'string' && typeof firstOp?.insert !== 'string' &&
@@ -57,126 +57,126 @@ const ChatInput = () => {
range.index === 0 && range.index === 0 &&
range.length !== quill.getLength() range.length !== quill.getLength()
) { ) {
quill.setSelection(1, Quill.sources.SILENT) quill.setSelection(1, Quill.sources.SILENT);
} }
} }
}) });
quill.on('text-change', () => { quill.on('text-change', () => {
const editorContent = quill.getContents() const editorContent = quill.getContents();
const firstOp = editorContent.ops[0] const firstOp = editorContent.ops[0];
if ( if (
firstOp && firstOp &&
typeof firstOp.insert !== 'string' && typeof firstOp.insert !== 'string' &&
firstOp.insert?.reply && firstOp.insert?.reply &&
quill.getLength() === 1 quill.getLength() === 1
) { ) {
quill.insertText(1, '\n', Quill.sources.SILENT) quill.insertText(1, '\n', Quill.sources.SILENT);
} }
}) });
quill.on('editor-change', (eventName: string) => { quill.on('editor-change', (eventName: string) => {
if (eventName === 'text-change') { if (eventName === 'text-change') {
const editorContent = quill.getContents() const editorContent = quill.getContents();
const firstOp = editorContent.ops[0] const firstOp = editorContent.ops[0];
if ( if (
firstOp && firstOp &&
typeof firstOp.insert !== 'string' && typeof firstOp.insert !== 'string' &&
firstOp.insert?.reply && firstOp.insert?.reply &&
quill.getLength() === 1 quill.getLength() === 1
) { ) {
quill.insertText(1, '\n', Quill.sources.SILENT) quill.insertText(1, '\n', Quill.sources.SILENT);
} }
} }
}) });
quill.root.addEventListener('compositionstart', () => { quill.root.addEventListener('compositionstart', () => {
const editorContent = quill.getContents() const editorContent = quill.getContents();
const firstOp = editorContent.ops[0] const firstOp = editorContent.ops[0];
if ( if (
firstOp && firstOp &&
typeof firstOp.insert !== 'string' && typeof firstOp.insert !== 'string' &&
firstOp.insert?.reply && firstOp.insert?.reply &&
quill.getLength() === 1 quill.getLength() === 1
) { ) {
quill.insertText(1, '\n', Quill.sources.SILENT) quill.insertText(1, '\n', Quill.sources.SILENT);
} }
}) });
} }
const onOpenChange = (open: boolean) => { const onOpenChange = (open: boolean) => {
if (open) { if (open) {
const selection = quill?.getSelection() const selection = quill?.getSelection();
if (selection) memorizedRange.current = selection if (selection) memorizedRange.current = selection;
} }
} };
const insertImage = (url: string) => { const insertImage = (url: string) => {
const selection = memorizedRange.current || quill?.getSelection() const selection = memorizedRange.current || quill?.getSelection();
quill?.deleteText(selection?.index || 0, selection?.length || 0) quill?.deleteText(selection?.index || 0, selection?.length || 0);
quill?.insertEmbed(selection?.index || 0, 'image', { quill?.insertEmbed(selection?.index || 0, 'image', {
src: url, src: url,
alt: '图片' alt: '图片',
}) });
quill?.setSelection((selection?.index || 0) + 1, 0) quill?.setSelection((selection?.index || 0) + 1, 0);
} };
function insertReplyBlock(messageId: string) { function insertReplyBlock (messageId: string) {
const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/ const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/;
if (!isNumberReg.test(messageId)) { if (!isNumberReg.test(messageId)) {
toast.error('请输入正确的消息ID') toast.error('请输入正确的消息ID');
return return;
} }
const editorContent = quill?.getContents() const editorContent = quill?.getContents();
const firstOp = editorContent?.ops[0] const firstOp = editorContent?.ops[0];
const currentSelection = quill?.getSelection() const currentSelection = quill?.getSelection();
if ( if (
firstOp && firstOp &&
typeof firstOp.insert !== 'string' && typeof firstOp.insert !== 'string' &&
firstOp.insert?.reply firstOp.insert?.reply
) { ) {
const delta = quill?.getContents() const delta = quill?.getContents();
if (delta) { if (delta) {
delta.ops[0] = { delta.ops[0] = {
insert: { reply: { messageId } } insert: { reply: { messageId } },
} };
quill?.setContents(delta, Quill.sources.USER) quill?.setContents(delta, Quill.sources.USER);
} }
} else { } else {
quill?.insertEmbed(0, 'reply', { messageId }, Quill.sources.USER) quill?.insertEmbed(0, 'reply', { messageId }, Quill.sources.USER);
} }
quill?.setSelection((currentSelection?.index || 0) + 1, 0) quill?.setSelection((currentSelection?.index || 0) + 1, 0);
quill?.blur() quill?.blur();
} }
const onInsertEmoji = (emoji: EmojiValue) => { const onInsertEmoji = (emoji: EmojiValue) => {
const selection = memorizedRange.current || quill?.getSelection() const selection = memorizedRange.current || quill?.getSelection();
quill?.deleteText(selection?.index || 0, selection?.length || 0) quill?.deleteText(selection?.index || 0, selection?.length || 0);
quill?.insertEmbed(selection?.index || 0, 'emoji', { quill?.insertEmbed(selection?.index || 0, 'emoji', {
alt: emoji.alt, alt: emoji.alt,
src: emoji.src, src: emoji.src,
id: emoji.id id: emoji.id,
}) });
quill?.setSelection((selection?.index || 0) + 1, 0) quill?.setSelection((selection?.index || 0) + 1, 0);
} };
const getChatMessage = () => { const getChatMessage = () => {
const delta = quill?.getContents() const delta = quill?.getContents();
const ops = const ops =
delta?.ops?.filter((op) => { delta?.ops?.filter((op) => {
return op.insert !== '\n' return op.insert !== '\n';
}) ?? [] }) ?? [];
const messages: OB11Segment[] = ops.map((op) => { const messages: OB11Segment[] = ops.map((op) => {
return quillToMessage(op) return quillToMessage(op);
}) });
return messages return messages;
} };
return ( return (
<div> <div>
<div <div
ref={quillRef} ref={quillRef}
className="border border-default-200 rounded-md !mb-2 !text-base !h-64" className='border border-default-200 rounded-md !mb-2 !text-base !h-64'
/> />
<div id="toolbar" className="!border-none flex gap-2"> <div id='toolbar' className='!border-none flex gap-2'>
<ImageInsert insertImage={insertImage} onOpenChange={onOpenChange} /> <ImageInsert insertImage={insertImage} onOpenChange={onOpenChange} />
<EmojiPicker <EmojiPicker
onInsertEmoji={onInsertEmoji} onInsertEmoji={onInsertEmoji}
@@ -190,18 +190,18 @@ const ChatInput = () => {
<DiceInsert /> <DiceInsert />
<RPSInsert /> <RPSInsert />
<Button <Button
color="primary" color='primary'
onPress={() => { onPress={() => {
const messages = getChatMessage() const messages = getChatMessage();
showStructuredMessage(messages) showStructuredMessage(messages);
}} }}
className="ml-auto" className='ml-auto'
> >
JSON格式 JSON格式
</Button> </Button>
</div> </div>
</div> </div>
) );
} };
export default ChatInput export default ChatInput;

View File

@@ -1,42 +1,42 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { import {
Modal, Modal,
ModalBody, ModalBody,
ModalContent, ModalContent,
ModalFooter, ModalFooter,
ModalHeader, ModalHeader,
useDisclosure useDisclosure,
} from '@heroui/modal' } from '@heroui/modal';
import ChatInput from '.' import ChatInput from '.';
export default function ChatInputModal() { export default function ChatInputModal () {
const { isOpen, onOpen, onOpenChange } = useDisclosure() const { isOpen, onOpen, onOpenChange } = useDisclosure();
return ( return (
<> <>
<Button onPress={onOpen} color="primary" radius="full" variant="flat"> <Button onPress={onOpen} color='primary' radius='full' variant='flat'>
</Button> </Button>
<Modal <Modal
size="4xl" size='4xl'
scrollBehavior="inside" scrollBehavior='inside'
isOpen={isOpen} isOpen={isOpen}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
> >
<ModalContent> <ModalContent>
{(onClose) => ( {(onClose) => (
<> <>
<ModalHeader className="flex flex-col gap-1"> <ModalHeader className='flex flex-col gap-1'>
</ModalHeader> </ModalHeader>
<ModalBody className="overflow-y-auto"> <ModalBody className='overflow-y-auto'>
<div className="overflow-y-auto"> <div className='overflow-y-auto'>
<ChatInput /> <ChatInput />
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" onPress={onClose} variant="flat"> <Button color='primary' onPress={onClose} variant='flat'>
</Button> </Button>
</ModalFooter> </ModalFooter>
@@ -45,5 +45,5 @@ export default function ChatInputModal() {
</ModalContent> </ModalContent>
</Modal> </Modal>
</> </>
) );
} }

View File

@@ -1,46 +1,46 @@
import Editor, { OnMount } from '@monaco-editor/react' import Editor, { OnMount, loader } from '@monaco-editor/react';
import { loader } from '@monaco-editor/react'
import React from 'react'
import { useTheme } from '@/hooks/use-theme' import React from 'react';
import monaco from '@/monaco' import { useTheme } from '@/hooks/use-theme';
import monaco from '@/monaco';
loader.config({ loader.config({
monaco, monaco,
paths: { paths: {
vs: '/webui/monaco-editor/min/vs' vs: '/webui/monaco-editor/min/vs',
} },
}) });
loader.config({ loader.config({
'vs/nls': { 'vs/nls': {
availableLanguages: { '*': 'zh-cn' } availableLanguages: { '*': 'zh-cn' },
} },
}) });
export interface CodeEditorProps extends React.ComponentProps<typeof Editor> { export interface CodeEditorProps extends React.ComponentProps<typeof Editor> {
test?: string test?: string
} }
export type CodeEditorRef = monaco.editor.IStandaloneCodeEditor export type CodeEditorRef = monaco.editor.IStandaloneCodeEditor;
const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>( const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>(
(props, ref) => { (props, ref) => {
const { isDark } = useTheme() const { isDark } = useTheme();
const handleEditorDidMount: OnMount = (editor, monaco) => { const handleEditorDidMount: OnMount = (editor, monaco) => {
if (ref) { if (ref) {
if (typeof ref === 'function') { if (typeof ref === 'function') {
ref(editor) ref(editor);
} else { } else {
;(ref as React.RefObject<CodeEditorRef>).current = editor (ref as React.RefObject<CodeEditorRef>).current = editor;
} }
} }
if (props.onMount) { if (props.onMount) {
props.onMount(editor, monaco) props.onMount(editor, monaco);
} }
} };
return ( return (
<Editor <Editor
@@ -48,8 +48,8 @@ const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>(
onMount={handleEditorDidMount} onMount={handleEditorDidMount}
theme={isDark ? 'vs-dark' : 'light'} theme={isDark ? 'vs-dark' : 'light'}
/> />
) );
} }
) );
export default CodeEditor export default CodeEditor;

View File

@@ -1,13 +1,13 @@
import { Button, ButtonGroup } from '@heroui/button' import { Button, ButtonGroup } from '@heroui/button';
import { Switch } from '@heroui/switch' import { Switch } from '@heroui/switch';
import { useState } from 'react' import { useState } from 'react';
import { CgDebug } from 'react-icons/cg' import { CgDebug } from 'react-icons/cg';
import { FiEdit3 } from 'react-icons/fi' import { FiEdit3 } from 'react-icons/fi';
import { MdDeleteForever } from 'react-icons/md' import { MdDeleteForever } from 'react-icons/md';
import DisplayCardContainer from './container' import DisplayCardContainer from './container';
type NetworkType = OneBotConfig['network'] type NetworkType = OneBotConfig['network'];
export type NetworkDisplayCardFields<T extends keyof NetworkType> = Array<{ export type NetworkDisplayCardFields<T extends keyof NetworkType> = Array<{
label: string label: string
@@ -15,7 +15,7 @@ export type NetworkDisplayCardFields<T extends keyof NetworkType> = Array<{
render?: ( render?: (
value: NetworkType[T][0][keyof NetworkType[T][0]] value: NetworkType[T][0][keyof NetworkType[T][0]]
) => React.ReactNode ) => React.ReactNode
}> }>;
export interface NetworkDisplayCardProps<T extends keyof NetworkType> { export interface NetworkDisplayCardProps<T extends keyof NetworkType> {
data: NetworkType[T][0] data: NetworkType[T][0]
@@ -36,25 +36,25 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
onEdit, onEdit,
onEnable, onEnable,
onDelete, onDelete,
onEnableDebug onEnableDebug,
}: NetworkDisplayCardProps<T>) => { }: NetworkDisplayCardProps<T>) => {
const { name, enable, debug } = data const { name, enable, debug } = data;
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false);
const handleEnable = () => { const handleEnable = () => {
setEditing(true) setEditing(true);
onEnable().finally(() => setEditing(false)) onEnable().finally(() => setEditing(false));
} };
const handleDelete = () => { const handleDelete = () => {
setEditing(true) setEditing(true);
onDelete().finally(() => setEditing(false)) onDelete().finally(() => setEditing(false));
} };
const handleEnableDebug = () => { const handleEnableDebug = () => {
setEditing(true) setEditing(true);
onEnableDebug().finally(() => setEditing(false)) onEnableDebug().finally(() => setEditing(false));
} };
return ( return (
<DisplayCardContainer <DisplayCardContainer
@@ -62,24 +62,39 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
<ButtonGroup <ButtonGroup
fullWidth fullWidth
isDisabled={editing} isDisabled={editing}
radius="full" radius='sm'
size="sm" size='sm'
variant="shadow" variant='flat'
> >
<Button color="warning" startContent={<FiEdit3 />} onPress={onEdit}> <Button
color='warning'
startContent={<FiEdit3 size={16} />}
onPress={onEdit}
>
</Button> </Button>
<Button <Button
color={debug ? 'success' : 'default'} color={debug ? 'secondary' : 'success'}
startContent={<CgDebug />} variant='flat'
startContent={
<CgDebug
style={{
width: '16px',
height: '16px',
minWidth: '16px',
minHeight: '16px',
}}
/>
}
onPress={handleEnableDebug} onPress={handleEnableDebug}
> >
{debug ? '关闭调试' : '开启调试'} {debug ? '关闭调试' : '开启调试'}
</Button> </Button>
<Button <Button
color="primary" className='bg-danger/20 text-danger hover:bg-danger/30 transition-colors'
startContent={<MdDeleteForever />} variant='flat'
startContent={<MdDeleteForever size={16} />}
onPress={handleDelete} onPress={handleDelete}
> >
@@ -96,7 +111,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
tag={showType && typeLabel} tag={showType && typeLabel}
title={name} title={name}
> >
<div className="grid grid-cols-2 gap-1"> <div className='grid grid-cols-2 gap-1'>
{fields.map((field, index) => ( {fields.map((field, index) => (
<div <div
key={index} key={index}
@@ -104,17 +119,19 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
field.label === 'URL' ? 'col-span-2' : '' field.label === 'URL' ? 'col-span-2' : ''
}`} }`}
> >
<span className="text-default-400">{field.label}</span> <span className='text-default-400'>{field.label}</span>
{field.render ? ( {field.render
field.render(field.value) ? (
) : ( field.render(field.value)
<span>{field.value}</span> )
)} : (
<span>{field.value}</span>
)}
</div> </div>
))} ))}
</div> </div>
</DisplayCardContainer> </DisplayCardContainer>
) );
} };
export default NetworkDisplayCard export default NetworkDisplayCard;

View File

@@ -1,7 +1,7 @@
import { Card, CardBody, CardFooter, CardHeader } from '@heroui/card' import { Card, CardBody, CardFooter, CardHeader } from '@heroui/card';
import clsx from 'clsx' import clsx from 'clsx';
import { title } from '../primitives' import { title } from '../primitives';
export interface ContainerProps { export interface ContainerProps {
title: string title: string
@@ -24,13 +24,13 @@ const DisplayCardContainer: React.FC<ContainerProps> = ({
action, action,
tag, tag,
enableSwitch, enableSwitch,
children children,
}) => { }) => {
return ( return (
<Card className="bg-opacity-50 backdrop-blur-sm"> <Card className='bg-opacity-50 backdrop-blur-sm'>
<CardHeader className={'pb-0 flex items-center'}> <CardHeader className='pb-0 flex items-center'>
{tag && ( {tag && (
<div className="text-center text-default-400 mb-1 absolute top-0 left-1/2 -translate-x-1/2 text-sm pointer-events-none bg-warning-100 dark:bg-warning-50 px-2 rounded-b"> <div className='text-center text-default-400 mb-1 absolute top-0 left-1/2 -translate-x-1/2 text-sm pointer-events-none bg-warning-100 dark:bg-warning-50 px-2 rounded-b'>
{tag} {tag}
</div> </div>
)} )}
@@ -39,19 +39,19 @@ const DisplayCardContainer: React.FC<ContainerProps> = ({
title({ title({
color: 'foreground', color: 'foreground',
size: 'xs', size: 'xs',
shadow: true shadow: true,
}), }),
'truncate' 'truncate'
)} )}
> >
{_title} {_title}
</h2> </h2>
<div className="ml-auto">{enableSwitch}</div> <div className='ml-auto'>{enableSwitch}</div>
</CardHeader> </CardHeader>
<CardBody className="text-sm">{children}</CardBody> <CardBody className='text-sm'>{children}</CardBody>
<CardFooter>{action}</CardFooter> <CardFooter>{action}</CardFooter>
</Card> </Card>
) );
} };
export default DisplayCardContainer export default DisplayCardContainer;

View File

@@ -1,7 +1,7 @@
import { Chip } from '@heroui/chip' import { Chip } from '@heroui/chip';
import NetworkDisplayCard from './common_card' import NetworkDisplayCard from './common_card';
import type { NetworkDisplayCardFields } from './common_card' import type { NetworkDisplayCardFields } from './common_card';
interface HTTPClientDisplayCardProps { interface HTTPClientDisplayCardProps {
data: OneBotConfig['network']['httpClients'][0] data: OneBotConfig['network']['httpClients'][0]
@@ -13,8 +13,8 @@ interface HTTPClientDisplayCardProps {
} }
const HTTPClientDisplayCard: React.FC<HTTPClientDisplayCardProps> = (props) => { const HTTPClientDisplayCard: React.FC<HTTPClientDisplayCardProps> = (props) => {
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props;
const { url, reportSelfMessage, messagePostFormat } = data const { url, reportSelfMessage, messagePostFormat } = data;
const fields: NetworkDisplayCardFields<'httpClients'> = [ const fields: NetworkDisplayCardFields<'httpClients'> = [
{ label: 'URL', value: url }, { label: 'URL', value: url },
@@ -23,25 +23,25 @@ const HTTPClientDisplayCard: React.FC<HTTPClientDisplayCardProps> = (props) => {
label: '上报自身消息', label: '上报自身消息',
value: reportSelfMessage, value: reportSelfMessage,
render: (value) => ( render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat"> <Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
{value ? '是' : '否'} {value ? '是' : '否'}
</Chip> </Chip>
) ),
} },
] ];
return ( return (
<NetworkDisplayCard <NetworkDisplayCard
data={data} data={data}
showType={showType} showType={showType}
typeLabel="HTTP客户端" typeLabel='HTTP客户端'
fields={fields} fields={fields}
onEdit={onEdit} onEdit={onEdit}
onEnable={onEnable} onEnable={onEnable}
onDelete={onDelete} onDelete={onDelete}
onEnableDebug={onEnableDebug} onEnableDebug={onEnableDebug}
/> />
) );
} };
export default HTTPClientDisplayCard export default HTTPClientDisplayCard;

View File

@@ -1,7 +1,7 @@
import { Chip } from '@heroui/chip' import { Chip } from '@heroui/chip';
import NetworkDisplayCard from './common_card' import NetworkDisplayCard from './common_card';
import type { NetworkDisplayCardFields } from './common_card' import type { NetworkDisplayCardFields } from './common_card';
interface HTTPServerDisplayCardProps { interface HTTPServerDisplayCardProps {
data: OneBotConfig['network']['httpServers'][0] data: OneBotConfig['network']['httpServers'][0]
@@ -13,8 +13,8 @@ interface HTTPServerDisplayCardProps {
} }
const HTTPServerDisplayCard: React.FC<HTTPServerDisplayCardProps> = (props) => { const HTTPServerDisplayCard: React.FC<HTTPServerDisplayCardProps> = (props) => {
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props;
const { host, port, enableCors, enableWebsocket, messagePostFormat } = data const { host, port, enableCors, enableWebsocket, messagePostFormat } = data;
const fields: NetworkDisplayCardFields<'httpServers'> = [ const fields: NetworkDisplayCardFields<'httpServers'> = [
{ label: '主机', value: host }, { label: '主机', value: host },
@@ -24,34 +24,34 @@ const HTTPServerDisplayCard: React.FC<HTTPServerDisplayCardProps> = (props) => {
label: 'CORS', label: 'CORS',
value: enableCors, value: enableCors,
render: (value) => ( render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat"> <Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
{value ? '已启用' : '未启用'} {value ? '已启用' : '未启用'}
</Chip> </Chip>
) ),
}, },
{ {
label: 'WS', label: 'WS',
value: enableWebsocket, value: enableWebsocket,
render: (value) => ( render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat"> <Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
{value ? '已启用' : '未启用'} {value ? '已启用' : '未启用'}
</Chip> </Chip>
) ),
} },
] ];
return ( return (
<NetworkDisplayCard <NetworkDisplayCard
data={data} data={data}
showType={showType} showType={showType}
typeLabel="HTTP服务器" typeLabel='HTTP服务器'
fields={fields} fields={fields}
onEdit={onEdit} onEdit={onEdit}
onEnable={onEnable} onEnable={onEnable}
onDelete={onDelete} onDelete={onDelete}
onEnableDebug={onEnableDebug} onEnableDebug={onEnableDebug}
/> />
) );
} };
export default HTTPServerDisplayCard export default HTTPServerDisplayCard;

View File

@@ -1,7 +1,7 @@
import { Chip } from '@heroui/chip' import { Chip } from '@heroui/chip';
import NetworkDisplayCard from './common_card' import NetworkDisplayCard from './common_card';
import type { NetworkDisplayCardFields } from './common_card' import type { NetworkDisplayCardFields } from './common_card';
interface HTTPSSEServerDisplayCardProps { interface HTTPSSEServerDisplayCardProps {
data: OneBotConfig['network']['httpSseServers'][0] data: OneBotConfig['network']['httpSseServers'][0]
@@ -15,8 +15,8 @@ interface HTTPSSEServerDisplayCardProps {
const HTTPSSEServerDisplayCard: React.FC<HTTPSSEServerDisplayCardProps> = ( const HTTPSSEServerDisplayCard: React.FC<HTTPSSEServerDisplayCardProps> = (
props props
) => { ) => {
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props;
const { host, port, enableCors, enableWebsocket, messagePostFormat } = data const { host, port, enableCors, enableWebsocket, messagePostFormat } = data;
const fields: NetworkDisplayCardFields<'httpServers'> = [ const fields: NetworkDisplayCardFields<'httpServers'> = [
{ label: '主机', value: host }, { label: '主机', value: host },
@@ -26,34 +26,34 @@ const HTTPSSEServerDisplayCard: React.FC<HTTPSSEServerDisplayCardProps> = (
label: 'CORS', label: 'CORS',
value: enableCors, value: enableCors,
render: (value) => ( render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat"> <Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
{value ? '已启用' : '未启用'} {value ? '已启用' : '未启用'}
</Chip> </Chip>
) ),
}, },
{ {
label: 'WS', label: 'WS',
value: enableWebsocket, value: enableWebsocket,
render: (value) => ( render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat"> <Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
{value ? '已启用' : '未启用'} {value ? '已启用' : '未启用'}
</Chip> </Chip>
) ),
} },
] ];
return ( return (
<NetworkDisplayCard <NetworkDisplayCard
data={data} data={data}
showType={showType} showType={showType}
typeLabel="HTTP服务器" typeLabel='HTTP服务器'
fields={fields} fields={fields}
onEdit={onEdit} onEdit={onEdit}
onEnable={onEnable} onEnable={onEnable}
onDelete={onDelete} onDelete={onDelete}
onEnableDebug={onEnableDebug} onEnableDebug={onEnableDebug}
/> />
) );
} };
export default HTTPSSEServerDisplayCard export default HTTPSSEServerDisplayCard;

View File

@@ -1,7 +1,7 @@
import { Chip } from '@heroui/chip' import { Chip } from '@heroui/chip';
import NetworkDisplayCard from './common_card' import NetworkDisplayCard from './common_card';
import type { NetworkDisplayCardFields } from './common_card' import type { NetworkDisplayCardFields } from './common_card';
interface WebsocketClientDisplayCardProps { interface WebsocketClientDisplayCardProps {
data: OneBotConfig['network']['websocketClients'][0] data: OneBotConfig['network']['websocketClients'][0]
@@ -15,14 +15,14 @@ interface WebsocketClientDisplayCardProps {
const WebsocketClientDisplayCard: React.FC<WebsocketClientDisplayCardProps> = ( const WebsocketClientDisplayCard: React.FC<WebsocketClientDisplayCardProps> = (
props props
) => { ) => {
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props;
const { const {
url, url,
heartInterval, heartInterval,
reconnectInterval, reconnectInterval,
messagePostFormat, messagePostFormat,
reportSelfMessage reportSelfMessage,
} = data } = data;
const fields: NetworkDisplayCardFields<'websocketClients'> = [ const fields: NetworkDisplayCardFields<'websocketClients'> = [
{ label: 'URL', value: url }, { label: 'URL', value: url },
@@ -33,25 +33,25 @@ const WebsocketClientDisplayCard: React.FC<WebsocketClientDisplayCardProps> = (
label: '上报自身消息', label: '上报自身消息',
value: reportSelfMessage, value: reportSelfMessage,
render: (value) => ( render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat"> <Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
{value ? '是' : '否'} {value ? '是' : '否'}
</Chip> </Chip>
) ),
} },
] ];
return ( return (
<NetworkDisplayCard <NetworkDisplayCard
data={data} data={data}
showType={showType} showType={showType}
typeLabel="Websocket客户端" typeLabel='Websocket客户端'
fields={fields} fields={fields}
onEdit={onEdit} onEdit={onEdit}
onEnable={onEnable} onEnable={onEnable}
onDelete={onDelete} onDelete={onDelete}
onEnableDebug={onEnableDebug} onEnableDebug={onEnableDebug}
/> />
) );
} };
export default WebsocketClientDisplayCard export default WebsocketClientDisplayCard;

View File

@@ -1,7 +1,7 @@
import { Chip } from '@heroui/chip' import { Chip } from '@heroui/chip';
import NetworkDisplayCard from './common_card' import NetworkDisplayCard from './common_card';
import type { NetworkDisplayCardFields } from './common_card' import type { NetworkDisplayCardFields } from './common_card';
interface WebsocketServerDisplayCardProps { interface WebsocketServerDisplayCardProps {
data: OneBotConfig['network']['websocketServers'][0] data: OneBotConfig['network']['websocketServers'][0]
@@ -15,15 +15,15 @@ interface WebsocketServerDisplayCardProps {
const WebsocketServerDisplayCard: React.FC<WebsocketServerDisplayCardProps> = ( const WebsocketServerDisplayCard: React.FC<WebsocketServerDisplayCardProps> = (
props props
) => { ) => {
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props;
const { const {
host, host,
port, port,
heartInterval, heartInterval,
messagePostFormat, messagePostFormat,
reportSelfMessage, reportSelfMessage,
enableForcePushEvent enableForcePushEvent,
} = data } = data;
const fields: NetworkDisplayCardFields<'websocketServers'> = [ const fields: NetworkDisplayCardFields<'websocketServers'> = [
{ label: '主机', value: host }, { label: '主机', value: host },
@@ -34,34 +34,34 @@ const WebsocketServerDisplayCard: React.FC<WebsocketServerDisplayCardProps> = (
label: '上报自身消息', label: '上报自身消息',
value: reportSelfMessage, value: reportSelfMessage,
render: (value) => ( render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat"> <Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
{value ? '是' : '否'} {value ? '是' : '否'}
</Chip> </Chip>
) ),
}, },
{ {
label: '强制推送事件', label: '强制推送事件',
value: enableForcePushEvent, value: enableForcePushEvent,
render: (value) => ( render: (value) => (
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat"> <Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
{value ? '是' : '否'} {value ? '是' : '否'}
</Chip> </Chip>
) ),
} },
] ];
return ( return (
<NetworkDisplayCard <NetworkDisplayCard
data={data} data={data}
showType={showType} showType={showType}
typeLabel="Websocket服务器" typeLabel='Websocket服务器'
fields={fields} fields={fields}
onEdit={onEdit} onEdit={onEdit}
onEnable={onEnable} onEnable={onEnable}
onDelete={onDelete} onDelete={onDelete}
onEnableDebug={onEnableDebug} onEnableDebug={onEnableDebug}
/> />
) );
} };
export default WebsocketServerDisplayCard export default WebsocketServerDisplayCard;

View File

@@ -1,7 +1,7 @@
import { Card, CardBody } from '@heroui/card' import { Card, CardBody } from '@heroui/card';
import clsx from 'clsx' import clsx from 'clsx';
import { title } from '@/components/primitives' import { title } from '@/components/primitives';
export interface NetworkItemDisplayProps { export interface NetworkItemDisplayProps {
count: number count: number
@@ -12,7 +12,7 @@ export interface NetworkItemDisplayProps {
const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
count, count,
label, label,
size = 'md' size = 'md',
}) => { }) => {
return ( return (
<Card <Card
@@ -22,16 +22,16 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
? 'col-span-8 md:col-span-2 bg-primary-50 shadow-primary-100' ? 'col-span-8 md:col-span-2 bg-primary-50 shadow-primary-100'
: 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200' : 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200'
)} )}
shadow="sm" shadow='sm'
> >
<CardBody className="items-center md:gap-1 p-1 md:p-2"> <CardBody className='items-center md:gap-1 p-1 md:p-2'>
<div <div
className={clsx( className={clsx(
'flex-1', 'flex-1',
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl', size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
title({ title({
color: size === 'md' ? 'pink' : 'yellow', color: size === 'md' ? 'pink' : 'yellow',
size size,
}) })
)} )}
> >
@@ -44,7 +44,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
title({ title({
color: size === 'md' ? 'pink' : 'yellow', color: size === 'md' ? 'pink' : 'yellow',
shadow: true, shadow: true,
size: 'xxs' size: 'xxs',
}) })
)} )}
> >
@@ -52,7 +52,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
) );
} };
export default NetworkItemDisplay export default NetworkItemDisplay;

View File

@@ -1,6 +1,6 @@
import { Card, CardProps } from '@heroui/card' import { Card, CardProps } from '@heroui/card';
import clsx from 'clsx' import clsx from 'clsx';
import React from 'react' import React from 'react';
export interface HoverEffectCardProps extends CardProps { export interface HoverEffectCardProps extends CardProps {
children: React.ReactNode children: React.ReactNode
@@ -18,15 +18,15 @@ const HoverEffectCard: React.FC<HoverEffectCardProps> = (props) => {
className, className,
style, style,
lightClassName, lightClassName,
lightStyle lightStyle,
} = props } = props;
const cardRef = React.useRef<HTMLDivElement | null>(null) const cardRef = React.useRef<HTMLDivElement | null>(null);
const lightRef = React.useRef<HTMLDivElement | null>(null) const lightRef = React.useRef<HTMLDivElement | null>(null);
const [isShowLight, setIsShowLight] = React.useState(false) const [isShowLight, setIsShowLight] = React.useState(false);
const [pos, setPos] = React.useState({ const [pos, setPos] = React.useState({
left: 0, left: 0,
top: 0 top: 0,
}) });
return ( return (
<Card <Card
@@ -40,53 +40,53 @@ const HoverEffectCard: React.FC<HoverEffectCardProps> = (props) => {
willChange: 'transform', willChange: 'transform',
transform: transform:
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)', 'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)',
...style ...style,
}} }}
onMouseEnter={() => { onMouseEnter={() => {
if (cardRef.current) { if (cardRef.current) {
cardRef.current.style.transition = 'transform 0.3s ease-out' cardRef.current.style.transition = 'transform 0.3s ease-out';
} }
}} }}
onMouseLeave={() => { onMouseLeave={() => {
setIsShowLight(false) setIsShowLight(false);
if (cardRef.current) { if (cardRef.current) {
cardRef.current.style.transition = 'transform 0.5s' cardRef.current.style.transition = 'transform 0.5s';
cardRef.current.style.transform = cardRef.current.style.transform =
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)' 'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)';
} }
}} }}
onMouseMove={(e: React.MouseEvent<HTMLDivElement>) => { onMouseMove={(e: React.MouseEvent<HTMLDivElement>) => {
if (cardRef.current) { if (cardRef.current) {
setIsShowLight(true) setIsShowLight(true);
const { x, y } = cardRef.current.getBoundingClientRect() const { x, y } = cardRef.current.getBoundingClientRect();
const { clientX, clientY } = e const { clientX, clientY } = e;
const offsetX = clientX - x const offsetX = clientX - x;
const offsetY = clientY - y const offsetY = clientY - y;
const lightWidth = lightStyle?.width?.toString() || '100' const lightWidth = lightStyle?.width?.toString() || '100';
const lightHeight = lightStyle?.height?.toString() || '100' const lightHeight = lightStyle?.height?.toString() || '100';
const lightWidthNum = parseInt(lightWidth) const lightWidthNum = parseInt(lightWidth);
const lightHeightNum = parseInt(lightHeight) const lightHeightNum = parseInt(lightHeight);
const left = offsetX - lightWidthNum / 2 const left = offsetX - lightWidthNum / 2;
const top = offsetY - lightHeightNum / 2 const top = offsetY - lightHeightNum / 2;
setPos({ setPos({
left, left,
top top,
}) });
cardRef.current.style.transition = 'transform 0.1s' cardRef.current.style.transition = 'transform 0.1s';
const rangeX = 400 / 2 const rangeX = 400 / 2;
const rangeY = 400 / 2 const rangeY = 400 / 2;
const rotateX = ((offsetY - rangeY) / rangeY) * maxXRotation const rotateX = ((offsetY - rangeY) / rangeY) * maxXRotation;
const rotateY = -1 * ((offsetX - rangeX) / rangeX) * maxYRotation const rotateY = -1 * ((offsetX - rangeX) / rangeX) * maxYRotation;
cardRef.current.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)` cardRef.current.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
} }
}} }}
> >
@@ -98,12 +98,12 @@ const HoverEffectCard: React.FC<HoverEffectCardProps> = (props) => {
lightClassName lightClassName
)} )}
style={{ style={{
...pos ...pos,
}} }}
/> />
{children} {children}
</Card> </Card>
) );
} };
export default HoverEffectCard export default HoverEffectCard;

View File

@@ -1,30 +1,30 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Code } from '@heroui/code' import { Code } from '@heroui/code';
import { MdError } from 'react-icons/md' import { MdError } from 'react-icons/md';
export interface ErrorFallbackProps { export interface ErrorFallbackProps {
error: Error error: Error
resetErrorBoundary: () => void resetErrorBoundary: () => void
} }
function errorFallbackRender({ function errorFallbackRender ({
error, error,
resetErrorBoundary resetErrorBoundary,
}: ErrorFallbackProps) { }: ErrorFallbackProps) {
return ( return (
<div className="pt-32 flex flex-col justify-center items-center"> <div className='pt-32 flex flex-col justify-center items-center'>
<div className="flex items-center"> <div className='flex items-center'>
<MdError className="mr-2" color="red" size={30} /> <MdError className='mr-2' color='red' size={30} />
<h1 className="text-2xl"></h1> <h1 className='text-2xl'></h1>
</div> </div>
<div className="my-6 flex flex-col justify-center items-center"> <div className='my-6 flex flex-col justify-center items-center'>
<p className="mb-2"></p> <p className='mb-2'></p>
<Code>{error.message}</Code> <Code>{error.message}</Code>
</div> </div>
<Button color="primary" size="md" onPress={resetErrorBoundary}> <Button color='primary' size='md' onPress={resetErrorBoundary}>
</Button> </Button>
</div> </div>
) );
} }
export default errorFallbackRender export default errorFallbackRender;

View File

@@ -11,8 +11,8 @@ import {
FaFileVideo, FaFileVideo,
FaFileWord, FaFileWord,
FaFileZipper, FaFileZipper,
FaFolderClosed FaFolderClosed,
} from 'react-icons/fa6' } from 'react-icons/fa6';
export interface FileIconProps { export interface FileIconProps {
name?: string name?: string
@@ -20,12 +20,12 @@ export interface FileIconProps {
} }
const FileIcon = (props: FileIconProps) => { const FileIcon = (props: FileIconProps) => {
const { name, isDirectory = false } = props const { name, isDirectory = false } = props;
if (isDirectory) { if (isDirectory) {
return <FaFolderClosed className="text-yellow-500" /> return <FaFolderClosed className='text-yellow-500' />;
} }
const ext = name?.split('.').pop() || '' const ext = name?.split('.').pop() || '';
if (ext) { if (ext) {
switch (ext.toLowerCase()) { switch (ext.toLowerCase()) {
case 'jpg': case 'jpg':
@@ -50,20 +50,20 @@ const FileIcon = (props: FileIconProps) => {
case 'fig': case 'fig':
case 'xd': case 'xd':
case 'svgz': case 'svgz':
return <FaFileImage className="text-green-500" /> return <FaFileImage className='text-green-500' />;
case 'pdf': case 'pdf':
return <FaFilePdf className="text-red-500" /> return <FaFilePdf className='text-red-500' />;
case 'doc': case 'doc':
case 'docx': case 'docx':
return <FaFileWord className="text-blue-500" /> return <FaFileWord className='text-blue-500' />;
case 'xls': case 'xls':
case 'xlsx': case 'xlsx':
return <FaFileExcel className="text-green-500" /> return <FaFileExcel className='text-green-500' />;
case 'csv': case 'csv':
return <FaFileCsv className="text-green-500" /> return <FaFileCsv className='text-green-500' />;
case 'ppt': case 'ppt':
case 'pptx': case 'pptx':
return <FaFilePowerpoint className="text-red-500" /> return <FaFilePowerpoint className='text-red-500' />;
case 'zip': case 'zip':
case 'rar': case 'rar':
case '7z': case '7z':
@@ -79,18 +79,18 @@ const FileIcon = (props: FileIconProps) => {
case 'taz': case 'taz':
case 'tz': case 'tz':
case 'tzo': case 'tzo':
return <FaFileZipper className="text-green-500" /> return <FaFileZipper className='text-green-500' />;
case 'txt': case 'txt':
return <FaFileLines className="text-gray-500" /> return <FaFileLines className='text-gray-500' />;
case 'mp3': case 'mp3':
case 'wav': case 'wav':
case 'flac': case 'flac':
return <FaFileAudio className="text-green-500" /> return <FaFileAudio className='text-green-500' />;
case 'mp4': case 'mp4':
case 'avi': case 'avi':
case 'mov': case 'mov':
case 'wmv': case 'wmv':
return <FaFileVideo className="text-red-500" /> return <FaFileVideo className='text-red-500' />;
case 'html': case 'html':
case 'css': case 'css':
case 'js': case 'js':
@@ -154,13 +154,13 @@ const FileIcon = (props: FileIconProps) => {
case 'userosscache': case 'userosscache':
case 'sln.docstates': case 'sln.docstates':
case 'dll': case 'dll':
return <FaFileCode className="text-blue-500" /> return <FaFileCode className='text-blue-500' />;
default: default:
return <FaFile className="text-gray-500" /> return <FaFile className='text-gray-500' />;
} }
} }
return <FaFile className="text-gray-500" /> return <FaFile className='text-gray-500' />;
} };
export default FileIcon export default FileIcon;

View File

@@ -1,12 +1,12 @@
import { Button, ButtonGroup } from '@heroui/button' import { Button, ButtonGroup } from '@heroui/button';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import { import {
Modal, Modal,
ModalBody, ModalBody,
ModalContent, ModalContent,
ModalFooter, ModalFooter,
ModalHeader ModalHeader,
} from '@heroui/modal' } from '@heroui/modal';
interface CreateFileModalProps { interface CreateFileModalProps {
isOpen: boolean isOpen: boolean
@@ -18,22 +18,22 @@ interface CreateFileModalProps {
onCreate: () => void onCreate: () => void
} }
export default function CreateFileModal({ export default function CreateFileModal ({
isOpen, isOpen,
fileType, fileType,
newFileName, newFileName,
onTypeChange, onTypeChange,
onNameChange, onNameChange,
onClose, onClose,
onCreate onCreate,
}: CreateFileModalProps) { }: CreateFileModalProps) {
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <Modal isOpen={isOpen} onClose={onClose}>
<ModalContent> <ModalContent>
<ModalHeader></ModalHeader> <ModalHeader></ModalHeader>
<ModalBody> <ModalBody>
<div className="flex flex-col gap-4"> <div className='flex flex-col gap-4'>
<ButtonGroup color="primary"> <ButtonGroup color='primary'>
<Button <Button
variant={fileType === 'file' ? 'solid' : 'flat'} variant={fileType === 'file' ? 'solid' : 'flat'}
onPress={() => onTypeChange('file')} onPress={() => onTypeChange('file')}
@@ -47,18 +47,18 @@ export default function CreateFileModal({
</Button> </Button>
</ButtonGroup> </ButtonGroup>
<Input label="名称" value={newFileName} onChange={onNameChange} /> <Input label='名称' value={newFileName} onChange={onNameChange} />
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" variant="flat" onPress={onClose}> <Button color='primary' variant='flat' onPress={onClose}>
</Button> </Button>
<Button color="primary" onPress={onCreate}> <Button color='primary' onPress={onCreate}>
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</Modal> </Modal>
) );
} }

View File

@@ -1,14 +1,14 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Code } from '@heroui/code' import { Code } from '@heroui/code';
import { import {
Modal, Modal,
ModalBody, ModalBody,
ModalContent, ModalContent,
ModalFooter, ModalFooter,
ModalHeader ModalHeader,
} from '@heroui/modal' } from '@heroui/modal';
import CodeEditor from '@/components/code_editor' import CodeEditor from '@/components/code_editor';
interface FileEditModalProps { interface FileEditModalProps {
isOpen: boolean isOpen: boolean
@@ -18,61 +18,61 @@ interface FileEditModalProps {
onContentChange: (newContent?: string) => void onContentChange: (newContent?: string) => void
} }
export default function FileEditModal({ export default function FileEditModal ({
isOpen, isOpen,
file, file,
onClose, onClose,
onSave, onSave,
onContentChange onContentChange,
}: FileEditModalProps) { }: FileEditModalProps) {
// 根据文件后缀返回对应语言 // 根据文件后缀返回对应语言
const getLanguage = (filePath: string) => { const getLanguage = (filePath: string) => {
if (filePath.endsWith('.js')) return 'javascript' if (filePath.endsWith('.js')) return 'javascript';
if (filePath.endsWith('.ts')) return 'typescript' if (filePath.endsWith('.ts')) return 'typescript';
if (filePath.endsWith('.tsx')) return 'tsx' if (filePath.endsWith('.tsx')) return 'tsx';
if (filePath.endsWith('.jsx')) return 'jsx' if (filePath.endsWith('.jsx')) return 'jsx';
if (filePath.endsWith('.vue')) return 'vue' if (filePath.endsWith('.vue')) return 'vue';
if (filePath.endsWith('.svelte')) return 'svelte' if (filePath.endsWith('.svelte')) return 'svelte';
if (filePath.endsWith('.json')) return 'json' if (filePath.endsWith('.json')) return 'json';
if (filePath.endsWith('.html')) return 'html' if (filePath.endsWith('.html')) return 'html';
if (filePath.endsWith('.css')) return 'css' if (filePath.endsWith('.css')) return 'css';
if (filePath.endsWith('.scss')) return 'scss' if (filePath.endsWith('.scss')) return 'scss';
if (filePath.endsWith('.less')) return 'less' if (filePath.endsWith('.less')) return 'less';
if (filePath.endsWith('.md')) return 'markdown' if (filePath.endsWith('.md')) return 'markdown';
if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) return 'yaml' if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) return 'yaml';
if (filePath.endsWith('.xml')) return 'xml' if (filePath.endsWith('.xml')) return 'xml';
if (filePath.endsWith('.sql')) return 'sql' if (filePath.endsWith('.sql')) return 'sql';
if (filePath.endsWith('.sh')) return 'shell' if (filePath.endsWith('.sh')) return 'shell';
if (filePath.endsWith('.bat')) return 'bat' if (filePath.endsWith('.bat')) return 'bat';
if (filePath.endsWith('.php')) return 'php' if (filePath.endsWith('.php')) return 'php';
if (filePath.endsWith('.java')) return 'java' if (filePath.endsWith('.java')) return 'java';
if (filePath.endsWith('.c')) return 'c' if (filePath.endsWith('.c')) return 'c';
if (filePath.endsWith('.cpp')) return 'cpp' if (filePath.endsWith('.cpp')) return 'cpp';
if (filePath.endsWith('.h')) return 'h' if (filePath.endsWith('.h')) return 'h';
if (filePath.endsWith('.hpp')) return 'hpp' if (filePath.endsWith('.hpp')) return 'hpp';
if (filePath.endsWith('.go')) return 'go' if (filePath.endsWith('.go')) return 'go';
if (filePath.endsWith('.py')) return 'python' if (filePath.endsWith('.py')) return 'python';
if (filePath.endsWith('.rb')) return 'ruby' if (filePath.endsWith('.rb')) return 'ruby';
if (filePath.endsWith('.cs')) return 'csharp' if (filePath.endsWith('.cs')) return 'csharp';
if (filePath.endsWith('.swift')) return 'swift' if (filePath.endsWith('.swift')) return 'swift';
if (filePath.endsWith('.vb')) return 'vb' if (filePath.endsWith('.vb')) return 'vb';
if (filePath.endsWith('.lua')) return 'lua' if (filePath.endsWith('.lua')) return 'lua';
if (filePath.endsWith('.pl')) return 'perl' if (filePath.endsWith('.pl')) return 'perl';
if (filePath.endsWith('.r')) return 'r' if (filePath.endsWith('.r')) return 'r';
return 'plaintext' return 'plaintext';
} };
return ( return (
<Modal size="full" isOpen={isOpen} onClose={onClose}> <Modal size='full' isOpen={isOpen} onClose={onClose}>
<ModalContent> <ModalContent>
<ModalHeader className="flex items-center gap-2 bg-content2 bg-opacity-50"> <ModalHeader className='flex items-center gap-2 bg-content2 bg-opacity-50'>
<span></span> <span></span>
<Code className="text-xs">{file?.path}</Code> <Code className='text-xs'>{file?.path}</Code>
</ModalHeader> </ModalHeader>
<ModalBody className="p-0"> <ModalBody className='p-0'>
<div className="h-full"> <div className='h-full'>
<CodeEditor <CodeEditor
height="100%" height='100%'
value={file?.content || ''} value={file?.content || ''}
onChange={onContentChange} onChange={onContentChange}
options={{ wordWrap: 'on' }} options={{ wordWrap: 'on' }}
@@ -81,14 +81,14 @@ export default function FileEditModal({
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" variant="flat" onPress={onClose}> <Button color='primary' variant='flat' onPress={onClose}>
</Button> </Button>
<Button color="primary" onPress={onSave}> <Button color='primary' onPress={onSave}>
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</Modal> </Modal>
) );
} }

View File

@@ -1,17 +1,17 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { import {
Modal, Modal,
ModalBody, ModalBody,
ModalContent, ModalContent,
ModalFooter, ModalFooter,
ModalHeader ModalHeader,
} from '@heroui/modal' } from '@heroui/modal';
import { Spinner } from '@heroui/spinner' import { Spinner } from '@heroui/spinner';
import { useRequest } from 'ahooks' import { useRequest } from 'ahooks';
import path from 'path-browserify' import path from 'path-browserify';
import { useEffect } from 'react' import { useEffect } from 'react';
import FileManager from '@/controllers/file_manager' import FileManager from '@/controllers/file_manager';
interface FilePreviewModalProps { interface FilePreviewModalProps {
isOpen: boolean isOpen: boolean
@@ -19,74 +19,74 @@ interface FilePreviewModalProps {
onClose: () => void onClose: () => void
} }
export const videoExts = ['.mp4', '.webm'] export const videoExts = ['.mp4', '.webm'];
export const audioExts = ['.mp3', '.wav'] export const audioExts = ['.mp3', '.wav'];
export const supportedPreviewExts = [...videoExts, ...audioExts] export const supportedPreviewExts = [...videoExts, ...audioExts];
export default function FilePreviewModal({ export default function FilePreviewModal ({
isOpen, isOpen,
filePath, filePath,
onClose onClose,
}: FilePreviewModalProps) { }: FilePreviewModalProps) {
const ext = path.extname(filePath).toLowerCase() const ext = path.extname(filePath).toLowerCase();
const { data, loading, error, run } = useRequest( const { data, loading, error, run } = useRequest(
async () => FileManager.downloadToURL(filePath), async () => FileManager.downloadToURL(filePath),
{ {
refreshDeps: [filePath], refreshDeps: [filePath],
manual: true, manual: true,
refreshDepsAction: () => { refreshDepsAction: () => {
const ext = path.extname(filePath).toLowerCase() const ext = path.extname(filePath).toLowerCase();
if (!filePath || !supportedPreviewExts.includes(ext)) { if (!filePath || !supportedPreviewExts.includes(ext)) {
return return;
} }
run() run();
} },
} }
) );
useEffect(() => { useEffect(() => {
if (filePath) { if (filePath) {
run() run();
} }
}, [filePath]) }, [filePath]);
let contentElement = null let contentElement = null;
if (!supportedPreviewExts.includes(ext)) { if (!supportedPreviewExts.includes(ext)) {
contentElement = <div></div> contentElement = <div></div>;
} else if (error) { } else if (error) {
contentElement = <div></div> contentElement = <div></div>;
} else if (loading || !data) { } else if (loading || !data) {
contentElement = ( contentElement = (
<div className="flex justify-center items-center h-full"> <div className='flex justify-center items-center h-full'>
<Spinner /> <Spinner />
</div> </div>
) );
} else if (videoExts.includes(ext)) { } else if (videoExts.includes(ext)) {
contentElement = <video src={data} controls className="max-w-full" /> contentElement = <video src={data} controls className='max-w-full' />;
} else if (audioExts.includes(ext)) { } else if (audioExts.includes(ext)) {
contentElement = <audio src={data} controls className="w-full" /> contentElement = <audio src={data} controls className='w-full' />;
} else { } else {
contentElement = ( contentElement = (
<div className="flex justify-center items-center h-full"> <div className='flex justify-center items-center h-full'>
<Spinner /> <Spinner />
</div> </div>
) );
} }
return ( return (
<Modal isOpen={isOpen} onClose={onClose} scrollBehavior="inside" size="3xl"> <Modal isOpen={isOpen} onClose={onClose} scrollBehavior='inside' size='3xl'>
<ModalContent> <ModalContent>
<ModalHeader></ModalHeader> <ModalHeader></ModalHeader>
<ModalBody className="flex justify-center items-center"> <ModalBody className='flex justify-center items-center'>
{contentElement} {contentElement}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" variant="flat" onPress={onClose}> <Button color='primary' variant='flat' onPress={onClose}>
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</Modal> </Modal>
) );
} }

View File

@@ -1,6 +1,6 @@
import { Button, ButtonGroup } from '@heroui/button' import { Button, ButtonGroup } from '@heroui/button';
import { Pagination } from '@heroui/pagination' import { Pagination } from '@heroui/pagination';
import { Spinner } from '@heroui/spinner' import { Spinner } from '@heroui/spinner';
import { import {
type Selection, type Selection,
type SortDescriptor, type SortDescriptor,
@@ -9,20 +9,20 @@ import {
TableCell, TableCell,
TableColumn, TableColumn,
TableHeader, TableHeader,
TableRow TableRow,
} from '@heroui/table' } from '@heroui/table';
import path from 'path-browserify' import path from 'path-browserify';
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react';
import { BiRename } from 'react-icons/bi' import { BiRename } from 'react-icons/bi';
import { FiCopy, FiDownload, FiMove, FiTrash2 } from 'react-icons/fi' import { FiCopy, FiDownload, FiMove, FiTrash2 } from 'react-icons/fi';
import { PhotoSlider } from 'react-photo-view' import { PhotoSlider } from 'react-photo-view';
import FileIcon from '@/components/file_icon' import FileIcon from '@/components/file_icon';
import type { FileInfo } from '@/controllers/file_manager' import type { FileInfo } from '@/controllers/file_manager';
import { supportedPreviewExts } from './file_preview_modal' import { supportedPreviewExts } from './file_preview_modal';
import ImageNameButton, { PreviewImage, imageExts } from './image_name_button' import ImageNameButton, { PreviewImage, imageExts } from './image_name_button';
export interface FileTableProps { export interface FileTableProps {
files: FileInfo[] files: FileInfo[]
@@ -42,9 +42,9 @@ export interface FileTableProps {
onDownload: (filePath: string) => void onDownload: (filePath: string) => void
} }
const PAGE_SIZE = 20 const PAGE_SIZE = 20;
export default function FileTable({ export default function FileTable ({
files, files,
currentPath, currentPath,
loading, loading,
@@ -59,39 +59,40 @@ export default function FileTable({
onMoveRequest, onMoveRequest,
onCopyPath, onCopyPath,
onDelete, onDelete,
onDownload onDownload,
}: FileTableProps) { }: FileTableProps) {
const [page, setPage] = useState(1) const [page, setPage] = useState(1);
const pages = Math.ceil(files.length / PAGE_SIZE) || 1 const pages = Math.ceil(files.length / PAGE_SIZE) || 1;
const start = (page - 1) * PAGE_SIZE const start = (page - 1) * PAGE_SIZE;
const end = start + PAGE_SIZE const end = start + PAGE_SIZE;
const displayFiles = files.slice(start, end) const displayFiles = files.slice(start, end);
const [showImage, setShowImage] = useState(false) const [showImage, setShowImage] = useState(false);
const [previewIndex, setPreviewIndex] = useState(0) const [previewIndex, setPreviewIndex] = useState(0);
const [previewImages, setPreviewImages] = useState<PreviewImage[]>([]) const [previewImages, setPreviewImages] = useState<PreviewImage[]>([]);
const addPreviewImage = useCallback((image: PreviewImage) => { const addPreviewImage = useCallback((image: PreviewImage) => {
setPreviewImages((prev) => { setPreviewImages((prev) => {
const exists = prev.some((p) => p.key === image.key) const exists = prev.some((p) => p.key === image.key);
if (exists) return prev if (exists) return prev;
return [...prev, image] return [...prev, image];
}) });
}, []) }, []);
useEffect(() => { useEffect(() => {
setPreviewImages([]) setPreviewImages([]);
setPreviewIndex(0) setPreviewIndex(0);
setShowImage(false) setShowImage(false);
}, [currentPath]) setPage(1);
}, [currentPath]);
const onPreviewImage = (name: string, images: PreviewImage[]) => { const onPreviewImage = (name: string, images: PreviewImage[]) => {
const index = images.findIndex((image) => image.key === name) const index = images.findIndex((image) => image.key === name);
if (index === -1) { if (index === -1) {
return return;
} }
setPreviewIndex(index) setPreviewIndex(index);
setShowImage(true) setShowImage(true);
} };
return ( return (
<> <>
@@ -103,20 +104,20 @@ export default function FileTable({
onIndexChange={setPreviewIndex} onIndexChange={setPreviewIndex}
/> />
<Table <Table
aria-label="文件列表" aria-label='文件列表'
sortDescriptor={sortDescriptor} sortDescriptor={sortDescriptor}
onSortChange={onSortChange} onSortChange={onSortChange}
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
defaultSelectedKeys={[]} defaultSelectedKeys={[]}
selectedKeys={selectedFiles} selectedKeys={selectedFiles}
selectionMode="multiple" selectionMode='multiple'
bottomContent={ bottomContent={
<div className="flex w-full justify-center"> <div className='flex w-full justify-center'>
<Pagination <Pagination
isCompact isCompact
showControls showControls
showShadow showShadow
color="primary" color='primary'
page={page} page={page}
total={pages} total={pages}
onChange={(page) => setPage(page)} onChange={(page) => setPage(page)}
@@ -125,64 +126,65 @@ export default function FileTable({
} }
> >
<TableHeader> <TableHeader>
<TableColumn key="name" allowsSorting> <TableColumn key='name' allowsSorting>
</TableColumn> </TableColumn>
<TableColumn key="type" allowsSorting> <TableColumn key='type' allowsSorting>
</TableColumn> </TableColumn>
<TableColumn key="size" allowsSorting> <TableColumn key='size' allowsSorting>
</TableColumn> </TableColumn>
<TableColumn key="mtime" allowsSorting> <TableColumn key='mtime' allowsSorting>
</TableColumn> </TableColumn>
<TableColumn key="actions"></TableColumn> <TableColumn key='actions'></TableColumn>
</TableHeader> </TableHeader>
<TableBody <TableBody
isLoading={loading} isLoading={loading}
loadingContent={ loadingContent={
<div className="flex justify-center items-center h-full"> <div className='flex justify-center items-center h-full'>
<Spinner /> <Spinner />
</div> </div>
} }
> >
{displayFiles.map((file: FileInfo) => { {displayFiles.map((file: FileInfo) => {
const filePath = path.join(currentPath, file.name) const filePath = path.join(currentPath, file.name);
const ext = path.extname(file.name).toLowerCase() const ext = path.extname(file.name).toLowerCase();
const previewable = supportedPreviewExts.includes(ext) const previewable = supportedPreviewExts.includes(ext);
const images = previewImages const images = previewImages;
return ( return (
<TableRow key={file.name}> <TableRow key={file.name}>
<TableCell> <TableCell>
{imageExts.includes(ext) ? ( {imageExts.includes(ext)
<ImageNameButton ? (
name={file.name} <ImageNameButton
filePath={filePath} name={file.name}
onPreview={() => onPreviewImage(file.name, images)} filePath={filePath}
onAddPreview={addPreviewImage} onPreview={() => onPreviewImage(file.name, images)}
/> onAddPreview={addPreviewImage}
) : ( />
<Button )
variant="light" : (
onPress={() => <Button
file.isDirectory variant='light'
? onDirectoryClick(file.name) onPress={() =>
: previewable file.isDirectory
? onPreview(filePath) ? onDirectoryClick(file.name)
: onEdit(filePath) : previewable
? onPreview(filePath)
: onEdit(filePath)}
className='text-left justify-start'
startContent={
<FileIcon
name={file.name}
isDirectory={file.isDirectory}
/>
} }
className="text-left justify-start" >
startContent={ {file.name}
<FileIcon </Button>
name={file.name} )}
isDirectory={file.isDirectory}
/>
}
>
{file.name}
</Button>
)}
</TableCell> </TableCell>
<TableCell>{file.isDirectory ? '目录' : '文件'}</TableCell> <TableCell>{file.isDirectory ? '目录' : '文件'}</TableCell>
<TableCell> <TableCell>
@@ -192,43 +194,43 @@ export default function FileTable({
</TableCell> </TableCell>
<TableCell>{new Date(file.mtime).toLocaleString()}</TableCell> <TableCell>{new Date(file.mtime).toLocaleString()}</TableCell>
<TableCell> <TableCell>
<ButtonGroup size="sm"> <ButtonGroup size='sm'>
<Button <Button
isIconOnly isIconOnly
color="primary" color='primary'
variant="flat" variant='flat'
onPress={() => onRenameRequest(file.name)} onPress={() => onRenameRequest(file.name)}
> >
<BiRename /> <BiRename />
</Button> </Button>
<Button <Button
isIconOnly isIconOnly
color="primary" color='primary'
variant="flat" variant='flat'
onPress={() => onMoveRequest(file.name)} onPress={() => onMoveRequest(file.name)}
> >
<FiMove /> <FiMove />
</Button> </Button>
<Button <Button
isIconOnly isIconOnly
color="primary" color='primary'
variant="flat" variant='flat'
onPress={() => onCopyPath(file.name)} onPress={() => onCopyPath(file.name)}
> >
<FiCopy /> <FiCopy />
</Button> </Button>
<Button <Button
isIconOnly isIconOnly
color="primary" color='primary'
variant="flat" variant='flat'
onPress={() => onDownload(filePath)} onPress={() => onDownload(filePath)}
> >
<FiDownload /> <FiDownload />
</Button> </Button>
<Button <Button
isIconOnly isIconOnly
color="primary" color='primary'
variant="flat" variant='flat'
onPress={() => onDelete(filePath)} onPress={() => onDelete(filePath)}
> >
<FiTrash2 /> <FiTrash2 />
@@ -236,10 +238,10 @@ export default function FileTable({
</ButtonGroup> </ButtonGroup>
</TableCell> </TableCell>
</TableRow> </TableRow>
) );
})} })}
</TableBody> </TableBody>
</Table> </Table>
</> </>
) );
} }

View File

@@ -1,20 +1,20 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Image } from '@heroui/image' import { Image } from '@heroui/image';
import { Spinner } from '@heroui/spinner' import { Spinner } from '@heroui/spinner';
import { useRequest } from 'ahooks' import { useRequest } from 'ahooks';
import path from 'path-browserify' import path from 'path-browserify';
import { useEffect } from 'react' import { useEffect } from 'react';
import FileManager from '@/controllers/file_manager' import FileManager from '@/controllers/file_manager';
import FileIcon from '../file_icon' import FileIcon from '../file_icon';
export interface PreviewImage { export interface PreviewImage {
key: string key: string
src: string src: string
alt: string alt: string
} }
export const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp'] export const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp'];
export interface ImageNameButtonProps { export interface ImageNameButtonProps {
name: string name: string
@@ -23,11 +23,11 @@ export interface ImageNameButtonProps {
onAddPreview: (image: PreviewImage) => void onAddPreview: (image: PreviewImage) => void
} }
export default function ImageNameButton({ export default function ImageNameButton ({
name, name,
filePath, filePath,
onPreview, onPreview,
onAddPreview onAddPreview,
}: ImageNameButtonProps) { }: ImageNameButtonProps) {
const { data, loading, error, run } = useRequest( const { data, loading, error, run } = useRequest(
async () => FileManager.downloadToURL(filePath), async () => FileManager.downloadToURL(filePath),
@@ -35,54 +35,58 @@ export default function ImageNameButton({
refreshDeps: [filePath], refreshDeps: [filePath],
manual: true, manual: true,
refreshDepsAction: () => { refreshDepsAction: () => {
const ext = path.extname(filePath).toLowerCase() const ext = path.extname(filePath).toLowerCase();
if (!filePath || !imageExts.includes(ext)) { if (!filePath || !imageExts.includes(ext)) {
return return;
} }
run() run();
} },
} }
) );
useEffect(() => { useEffect(() => {
if (data) { if (data) {
onAddPreview({ onAddPreview({
key: name, key: name,
src: data, src: data,
alt: name alt: name,
}) });
} }
}, [data, name, onAddPreview]) }, [data, name, onAddPreview]);
useEffect(() => { useEffect(() => {
if (filePath) { if (filePath) {
run() run();
} }
}, []) }, []);
return ( return (
<Button <Button
variant="light" variant='light'
className="text-left justify-start" className='text-left justify-start'
onPress={onPreview} onPress={onPreview}
startContent={ startContent={
error ? ( error
<FileIcon name={name} isDirectory={false} /> ? (
) : loading || !data ? ( <FileIcon name={name} isDirectory={false} />
<Spinner size="sm" /> )
) : ( : loading || !data
<Image ? (
src={data} <Spinner size='sm' />
alt={name} )
className="w-8 h-8 flex-shrink-0" : (
classNames={{ <Image
wrapper: 'w-8 h-8 flex-shrink-0' src={data}
}} alt={name}
radius="sm" className='w-8 h-8 flex-shrink-0'
/> classNames={{
) wrapper: 'w-8 h-8 flex-shrink-0',
}}
radius='sm'
/>
)
} }
> >
{name} {name}
</Button> </Button>
) );
} }

View File

@@ -1,92 +1,92 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { import {
Modal, Modal,
ModalBody, ModalBody,
ModalContent, ModalContent,
ModalFooter, ModalFooter,
ModalHeader ModalHeader,
} from '@heroui/modal' } from '@heroui/modal';
import { Spinner } from '@heroui/spinner' import { Spinner } from '@heroui/spinner';
import clsx from 'clsx' import clsx from 'clsx';
import path from 'path-browserify' import path from 'path-browserify';
import { useState } from 'react' import { useState } from 'react';
import { IoAdd, IoRemove } from 'react-icons/io5' import { IoAdd, IoRemove } from 'react-icons/io5';
import FileManager from '@/controllers/file_manager' import FileManager from '@/controllers/file_manager';
interface MoveModalProps { interface MoveModalProps {
isOpen: boolean isOpen: boolean;
moveTargetPath: string moveTargetPath: string;
selectionInfo: string selectionInfo: string;
onClose: () => void onClose: () => void;
onMove: () => void onMove: () => void;
onSelect: (dir: string) => void // 新增回调 onSelect: (dir: string) => void; // 新增回调
} }
// 将 DirectoryTree 改为递归组件 // 将 DirectoryTree 改为递归组件
// 新增 selectedPath 属性,用于标识当前选中的目录 // 新增 selectedPath 属性,用于标识当前选中的目录
function DirectoryTree({ function DirectoryTree ({
basePath, basePath,
onSelect, onSelect,
selectedPath selectedPath,
}: { }: {
basePath: string basePath: string;
onSelect: (dir: string) => void onSelect: (dir: string) => void;
selectedPath?: string selectedPath?: string;
}) { }) {
const [dirs, setDirs] = useState<string[]>([]) const [dirs, setDirs] = useState<string[]>([]);
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false);
// 新增loading状态 // 新增loading状态
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false);
const fetchDirectories = async () => { const fetchDirectories = async () => {
try { try {
// 直接使用 basePath 调用接口,移除 process.platform 判断 // 直接使用 basePath 调用接口,移除 process.platform 判断
const list = await FileManager.listDirectories(basePath) const list = await FileManager.listDirectories(basePath);
setDirs(list.map((item) => item.name)) setDirs(list.map((item) => item.name));
} catch (error) { } catch (_error) {
// ...error handling... // ...error handling...
} }
} };
const handleToggle = async () => { const handleToggle = async () => {
if (!expanded) { if (!expanded) {
setExpanded(true) setExpanded(true);
setLoading(true) setLoading(true);
await fetchDirectories() await fetchDirectories();
setLoading(false) setLoading(false);
} else { } else {
setExpanded(false) setExpanded(false);
} }
} };
const handleClick = () => { const handleClick = () => {
onSelect(basePath) onSelect(basePath);
handleToggle() handleToggle();
} };
// 计算显示的名称 // 计算显示的名称
const getDisplayName = () => { const getDisplayName = () => {
if (basePath === '/') return '/' if (basePath === '/') return '/';
if (/^[A-Z]:$/i.test(basePath)) return basePath if (/^[A-Z]:$/i.test(basePath)) return basePath;
return path.basename(basePath) return path.basename(basePath);
} };
// 更新 Button 的 variant 逻辑 // 更新 Button 的 variant 逻辑
const isSeleted = selectedPath === basePath const isSeleted = selectedPath === basePath;
const variant = isSeleted const variant = isSeleted
? 'solid' ? 'solid'
: selectedPath && path.dirname(selectedPath) === basePath : selectedPath && path.dirname(selectedPath) === basePath
? 'flat' ? 'flat'
: 'light' : 'light';
return ( return (
<div className="ml-4"> <div className='ml-4'>
<Button <Button
onPress={handleClick} onPress={handleClick}
className="py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md" className='py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md'
size="sm" size='sm'
color="primary" color='primary'
variant={variant} variant={variant}
startContent={ startContent={
<div <div
@@ -103,66 +103,68 @@ function DirectoryTree({
</Button> </Button>
{expanded && ( {expanded && (
<div> <div>
{loading ? ( {loading
<div className="flex py-1 px-8"> ? (
<Spinner size="sm" color="primary" /> <div className='flex py-1 px-8'>
</div> <Spinner size='sm' color='primary' />
) : ( </div>
dirs.map((dirName) => { )
const childPath = : (
basePath === '/' && /^[A-Z]:$/i.test(dirName) dirs.map((dirName) => {
? dirName const childPath =
: path.join(basePath, dirName) basePath === '/' && /^[A-Z]:$/i.test(dirName)
return ( ? dirName
<DirectoryTree : path.join(basePath, dirName);
key={childPath} return (
basePath={childPath} <DirectoryTree
onSelect={onSelect} key={childPath}
selectedPath={selectedPath} basePath={childPath}
/> onSelect={onSelect}
) selectedPath={selectedPath}
}) />
)} );
})
)}
</div> </div>
)} )}
</div> </div>
) );
} }
export default function MoveModal({ export default function MoveModal ({
isOpen, isOpen,
moveTargetPath, moveTargetPath,
selectionInfo, selectionInfo,
onClose, onClose,
onMove, onMove,
onSelect onSelect,
}: MoveModalProps) { }: MoveModalProps) {
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <Modal isOpen={isOpen} onClose={onClose}>
<ModalContent> <ModalContent>
<ModalHeader></ModalHeader> <ModalHeader></ModalHeader>
<ModalBody> <ModalBody>
<div className="rounded-md p-2 border border-default-300 overflow-auto max-h-60"> <div className='rounded-md p-2 border border-default-300 overflow-auto max-h-60'>
<DirectoryTree <DirectoryTree
basePath="/" basePath='/'
onSelect={onSelect} onSelect={onSelect}
selectedPath={moveTargetPath} selectedPath={moveTargetPath}
/> />
</div> </div>
<p className="text-sm text-default-500 mt-2"> <p className='text-sm text-default-500 mt-2'>
{moveTargetPath || '未选择'} {moveTargetPath || '未选择'}
</p> </p>
<p className="text-sm text-default-500">{selectionInfo}</p> <p className='text-sm text-default-500'>{selectionInfo}</p>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" variant="flat" onPress={onClose}> <Button color='primary' variant='flat' onPress={onClose}>
</Button> </Button>
<Button color="primary" onPress={onMove}> <Button color='primary' onPress={onMove}>
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</Modal> </Modal>
) );
} }

View File

@@ -1,12 +1,12 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import { import {
Modal, Modal,
ModalBody, ModalBody,
ModalContent, ModalContent,
ModalFooter, ModalFooter,
ModalHeader ModalHeader,
} from '@heroui/modal' } from '@heroui/modal';
interface RenameModalProps { interface RenameModalProps {
isOpen: boolean isOpen: boolean
@@ -16,29 +16,29 @@ interface RenameModalProps {
onRename: () => void onRename: () => void
} }
export default function RenameModal({ export default function RenameModal ({
isOpen, isOpen,
newFileName, newFileName,
onNameChange, onNameChange,
onClose, onClose,
onRename onRename,
}: RenameModalProps) { }: RenameModalProps) {
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <Modal isOpen={isOpen} onClose={onClose}>
<ModalContent> <ModalContent>
<ModalHeader></ModalHeader> <ModalHeader></ModalHeader>
<ModalBody> <ModalBody>
<Input label="新名称" value={newFileName} onChange={onNameChange} /> <Input label='新名称' value={newFileName} onChange={onNameChange} />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" variant="flat" onPress={onClose}> <Button color='primary' variant='flat' onPress={onClose}>
</Button> </Button>
<Button color="primary" onPress={onRename}> <Button color='primary' onPress={onRename}>
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</Modal> </Modal>
) );
} }

View File

@@ -1,4 +1,4 @@
import clsx from 'clsx' import clsx from 'clsx';
export interface IconWrapperProps { export interface IconWrapperProps {
children?: React.ReactNode children?: React.ReactNode
@@ -14,6 +14,6 @@ const IconWrapper = ({ children, className }: IconWrapperProps) => (
> >
{children} {children}
</div> </div>
) );
export default IconWrapper export default IconWrapper;

View File

@@ -1,10 +1,10 @@
import { ChevronRightIcon } from '../icons' import { ChevronRightIcon } from '../icons';
const ItemCounter = ({ number }: { number: number }) => ( const ItemCounter = ({ number }: { number: number }) => (
<div className="flex items-center gap-1 text-default-400"> <div className='flex items-center gap-1 text-default-400'>
<span className="text-small">{number}</span> <span className='text-small'>{number}</span>
<ChevronRightIcon className="text-xl" /> <ChevronRightIcon className='text-xl' />
</div> </div>
) );
export default ItemCounter export default ItemCounter;

View File

@@ -1,40 +1,40 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react';
import { getReleaseTime } from '@/utils/time' import { getReleaseTime } from '@/utils/time';
import type { GithubRelease as GithubReleaseType } from '@/types/github' import type { GithubRelease as GithubReleaseType } from '@/types/github';
export interface GithubReleaseProps { export interface GithubReleaseProps {
releaseData: GithubReleaseType releaseData: GithubReleaseType
} }
const GithubRelease: React.FC<GithubReleaseProps> = (props) => { const GithubRelease: React.FC<GithubReleaseProps> = (props) => {
const { releaseData } = props const { releaseData } = props;
const [releaseTime, setReleaseTime] = useState<string | null>(null) const [releaseTime, setReleaseTime] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
if (releaseData) { if (releaseData) {
const timer = setInterval(() => { const timer = setInterval(() => {
const time = getReleaseTime(releaseData.published_at) const time = getReleaseTime(releaseData.published_at);
setReleaseTime(time) setReleaseTime(time);
}, 1000) }, 1000);
return () => clearInterval(timer) return () => clearInterval(timer);
} }
}, [releaseData]) }, [releaseData]);
return ( return (
<div className="flex flex-col gap-1"> <div className='flex flex-col gap-1'>
<span>Releases</span> <span>Releases</span>
<div className="px-2 py-1 rounded-small bg-default-100 bg-opacity-50 backdrop-blur-sm group-data-[hover=true]:bg-default-200"> <div className='px-2 py-1 rounded-small bg-default-100 bg-opacity-50 backdrop-blur-sm group-data-[hover=true]:bg-default-200'>
<span className="text-tiny text-default-600">{releaseData.name}</span> <span className='text-tiny text-default-600'>{releaseData.name}</span>
<div className="flex gap-2 text-tiny"> <div className='flex gap-2 text-tiny'>
<span className="text-default-500">{releaseTime}</span> <span className='text-default-500'>{releaseTime}</span>
<span className="text-success">Latest</span> <span className='text-success'>Latest</span>
</div> </div>
</div> </div>
</div> </div>
) );
} };
export default GithubRelease export default GithubRelease;

View File

@@ -1,76 +1,78 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import { useRequest } from 'ahooks' import { useRequest } from 'ahooks';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { IoCopy, IoRefresh } from 'react-icons/io5' import { IoCopy, IoRefresh } from 'react-icons/io5';
import { request } from '@/utils/request' import { request } from '@/utils/request';
import PageLoading from './page_loading' import PageLoading from './page_loading';
export default function Hitokoto() { export default function Hitokoto () {
const { const {
data: dataOri, data: dataOri,
error, error,
loading, loading,
run run,
} = useRequest(() => request.get<IHitokoto>('https://hitokoto.152710.xyz/'), { } = useRequest(() => request.get<IHitokoto>('https://hitokoto.152710.xyz/'), {
pollingInterval: 10000, pollingInterval: 10000,
throttleWait: 1000 throttleWait: 1000,
}) });
const data = dataOri?.data const data = dataOri?.data;
const onCopy = () => { const onCopy = () => {
try { try {
const text = `${data?.hitokoto} —— ${data?.from} ${data?.from_who}` const text = `${data?.hitokoto} —— ${data?.from} ${data?.from_who}`;
navigator.clipboard.writeText(text) navigator.clipboard.writeText(text);
toast.success('复制成功') toast.success('复制成功');
} catch (error) { } catch (_error) {
toast.error('复制失败, 请手动复制') toast.error('复制失败, 请手动复制');
} }
} };
return ( return (
<div> <div>
<div className="relative"> <div className='relative'>
{loading && <PageLoading />} {loading && <PageLoading />}
{error ? ( {error
<div className="text-primary-400">{error.message}</div> ? (
) : ( <div className='text-primary-400'>{error.message}</div>
<> )
<div>{data?.hitokoto}</div> : (
<div className="text-right"> <>
<span className="text-default-400">{data?.from}</span>{' '} <div>{data?.hitokoto}</div>
{data?.from_who} <div className='text-right'>
</div> <span className='text-default-400'>{data?.from}</span>{' '}
</> {data?.from_who}
)} </div>
</>
)}
</div> </div>
<div className="flex gap-2"> <div className='flex gap-2'>
<Tooltip content="刷新" placement="top"> <Tooltip content='刷新' placement='top'>
<Button <Button
onPress={run} onPress={run}
size="sm" size='sm'
isLoading={loading} isLoading={loading}
isIconOnly isIconOnly
radius="full" radius='full'
color="primary" color='primary'
variant="flat" variant='flat'
> >
<IoRefresh /> <IoRefresh />
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip content="复制" placement="top"> <Tooltip content='复制' placement='top'>
<Button <Button
onPress={onCopy} onPress={onCopy}
size="sm" size='sm'
isIconOnly isIconOnly
radius="full" radius='full'
color="success" color='success'
variant="flat" variant='flat'
> >
<IoCopy /> <IoCopy />
</Button> </Button>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
) );
} }

View File

@@ -1,11 +1,11 @@
import { motion, useMotionValue, useSpring } from 'motion/react' import { motion, useMotionValue, useSpring } from 'motion/react';
import { useRef, useState } from 'react' import { useRef, useState } from 'react';
const springValues = { const springValues = {
damping: 30, damping: 30,
stiffness: 100, stiffness: 100,
mass: 2 mass: 2,
} };
export interface HoverTiltedCardProps { export interface HoverTiltedCardProps {
imageSrc: string imageSrc: string
@@ -22,7 +22,7 @@ export interface HoverTiltedCardProps {
displayOverlayContent?: boolean displayOverlayContent?: boolean
} }
export default function HoverTiltedCard({ export default function HoverTiltedCard ({
imageSrc, imageSrc,
altText = 'NapCat', altText = 'NapCat',
captionText = 'NapCat', captionText = 'NapCat',
@@ -34,95 +34,95 @@ export default function HoverTiltedCard({
rotateAmplitude = 14, rotateAmplitude = 14,
showTooltip = false, showTooltip = false,
overlayContent = ( overlayContent = (
<div className="text-center mt-6 px-4 py-0.5 shadow-lg rounded-full bg-primary-600 text-default-100 bg-opacity-80"> <div className='text-center mt-6 px-4 py-0.5 shadow-lg rounded-full bg-primary-600 text-default-100 bg-opacity-80'>
NapCat NapCat
</div> </div>
), ),
displayOverlayContent = true displayOverlayContent = true,
}: HoverTiltedCardProps) { }: HoverTiltedCardProps) {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null);
const x = useMotionValue(0) const x = useMotionValue(0);
const y = useMotionValue(0) const y = useMotionValue(0);
const rotateX = useSpring(useMotionValue(0), springValues) const rotateX = useSpring(useMotionValue(0), springValues);
const rotateY = useSpring(useMotionValue(0), springValues) const rotateY = useSpring(useMotionValue(0), springValues);
const scale = useSpring(1, springValues) const scale = useSpring(1, springValues);
const opacity = useSpring(0) const opacity = useSpring(0);
const rotateFigcaption = useSpring(0, { const rotateFigcaption = useSpring(0, {
stiffness: 350, stiffness: 350,
damping: 30, damping: 30,
mass: 1 mass: 1,
}) });
const [lastY, setLastY] = useState(0) const [lastY, setLastY] = useState(0);
function handleMouse(e: React.MouseEvent) { function handleMouse (e: React.MouseEvent) {
if (!ref.current) return if (!ref.current) return;
const rect = ref.current.getBoundingClientRect() const rect = ref.current.getBoundingClientRect();
const offsetX = e.clientX - rect.left - rect.width / 2 const offsetX = e.clientX - rect.left - rect.width / 2;
const offsetY = e.clientY - rect.top - rect.height / 2 const offsetY = e.clientY - rect.top - rect.height / 2;
const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude;
const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude;
rotateX.set(rotationX) rotateX.set(rotationX);
rotateY.set(rotationY) rotateY.set(rotationY);
x.set(e.clientX - rect.left) x.set(e.clientX - rect.left);
y.set(e.clientY - rect.top) y.set(e.clientY - rect.top);
const velocityY = offsetY - lastY const velocityY = offsetY - lastY;
rotateFigcaption.set(-velocityY * 0.6) rotateFigcaption.set(-velocityY * 0.6);
setLastY(offsetY) setLastY(offsetY);
} }
function handleMouseEnter() { function handleMouseEnter () {
scale.set(scaleOnHover) scale.set(scaleOnHover);
opacity.set(1) opacity.set(1);
} }
function handleMouseLeave() { function handleMouseLeave () {
opacity.set(0) opacity.set(0);
scale.set(1) scale.set(1);
rotateX.set(0) rotateX.set(0);
rotateY.set(0) rotateY.set(0);
rotateFigcaption.set(0) rotateFigcaption.set(0);
} }
return ( return (
<figure <figure
ref={ref} ref={ref}
className="relative w-full h-full [perspective:800px] flex flex-col items-center justify-center" className='relative w-full h-full [perspective:800px] flex flex-col items-center justify-center'
style={{ style={{
height: containerHeight, height: containerHeight,
width: containerWidth width: containerWidth,
}} }}
onMouseMove={handleMouse} onMouseMove={handleMouse}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
> >
<motion.div <motion.div
className="relative [transform-style:preserve-3d]" className='relative [transform-style:preserve-3d]'
style={{ style={{
width: imageWidth, width: imageWidth,
height: imageHeight, height: imageHeight,
rotateX, rotateX,
rotateY, rotateY,
scale scale,
}} }}
> >
<motion.img <motion.img
src={imageSrc} src={imageSrc}
alt={altText} alt={altText}
className="absolute top-0 left-0 object-cover rounded-md will-change-transform [transform:translateZ(0)] pointer-events-none select-none" className='absolute top-0 left-0 object-cover rounded-md will-change-transform [transform:translateZ(0)] pointer-events-none select-none'
style={{ style={{
width: imageWidth, width: imageWidth,
height: imageHeight height: imageHeight,
}} }}
/> />
{displayOverlayContent && overlayContent && ( {displayOverlayContent && overlayContent && (
<motion.div className="absolute top-0 left-0 right-0 z-10 flex justify-center will-change-transform [transform:translateZ(30px)]"> <motion.div className='absolute top-0 left-0 right-0 z-10 flex justify-center will-change-transform [transform:translateZ(30px)]'>
{overlayContent} {overlayContent}
</motion.div> </motion.div>
)} )}
@@ -130,17 +130,17 @@ export default function HoverTiltedCard({
{showTooltip && ( {showTooltip && (
<motion.figcaption <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" 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={{ style={{
x, x,
y, y,
opacity, opacity,
rotate: rotateFigcaption rotate: rotateFigcaption,
}} }}
> >
{captionText} {captionText}
</motion.figcaption> </motion.figcaption>
)} )}
</figure> </figure>
) );
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,7 +1,7 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Image } from '@heroui/image' import { Image } from '@heroui/image';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import { useRef } from 'react' import { useRef } from 'react';
export interface ImageInputProps { export interface ImageInputProps {
onChange: (base64: string) => void onChange: (base64: string) => void
@@ -10,47 +10,47 @@ export interface ImageInputProps {
} }
const ImageInput: React.FC<ImageInputProps> = ({ onChange, value, label }) => { const ImageInput: React.FC<ImageInputProps> = ({ onChange, value, label }) => {
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null);
return ( return (
<div className="flex items-end gap-2"> <div className='flex items-end gap-2'>
<div className="w-5 h-5 flex-shrink-0"> <div className='w-5 h-5 flex-shrink-0'>
<Image <Image
src={value} src={value}
alt={label} alt={label}
className="w-5 h-5 flex-shrink-0 rounded-none" className='w-5 h-5 flex-shrink-0 rounded-none'
/> />
</div> </div>
<Input <Input
ref={inputRef} ref={inputRef}
label={label} label={label}
type="file" type='file'
placeholder="选择图片" placeholder='选择图片'
accept="image/*" accept='image/*'
onChange={async (e) => { onChange={async (e) => {
const file = e.target.files?.[0] const file = e.target.files?.[0];
if (file) { if (file) {
const reader = new FileReader() const reader = new FileReader();
reader.onload = async () => { reader.onload = async () => {
const base64 = reader.result as string const base64 = reader.result as string;
onChange(base64) onChange(base64);
} };
reader.readAsDataURL(file) reader.readAsDataURL(file);
} }
}} }}
/> />
<Button <Button
onPress={() => { onPress={() => {
onChange('') onChange('');
if (inputRef.current) inputRef.current.value = '' if (inputRef.current) inputRef.current.value = '';
}} }}
color="primary" color='primary'
variant="flat" variant='flat'
size="sm" size='sm'
> >
</Button> </Button>
</div> </div>
) );
} };
export default ImageInput export default ImageInput;

View File

@@ -1,15 +1,15 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Card, CardBody, CardHeader } from '@heroui/card' import { Card, CardBody, CardHeader } from '@heroui/card';
import { Select, SelectItem } from '@heroui/select' import { Select, SelectItem } from '@heroui/select';
import type { Selection } from '@react-types/shared' import type { Selection } from '@react-types/shared';
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react';
import { colorizeLogLevel } from '@/utils/terminal' import { colorizeLogLevel } from '@/utils/terminal';
import PageLoading from '../page_loading' import PageLoading from '../page_loading';
import XTerm from '../xterm' import XTerm from '../xterm';
import type { XTermRef } from '../xterm' import type { XTermRef } from '../xterm';
import LogLevelSelect from './log_level_select' import LogLevelSelect from './log_level_select';
export interface HistoryLogsProps { export interface HistoryLogsProps {
list: string[] list: string[]
@@ -32,80 +32,80 @@ const HistoryLogs: React.FC<HistoryLogsProps> = (props) => {
listLoading, listLoading,
logContent, logContent,
listError, listError,
logLoading logLoading,
} = props } = props;
const Xterm = useRef<XTermRef>(null) const Xterm = useRef<XTermRef>(null);
const [logLevel, setLogLevel] = useState<Selection>( const [logLevel, setLogLevel] = useState<Selection>(
new Set(['info', 'warn', 'error']) new Set(['info', 'warn', 'error'])
) );
const logToColored = (log: string) => { const logToColored = (log: string) => {
const logs = log const logs = log
.split('\n') .split('\n')
.map((line) => { .map((line) => {
const colored = colorizeLogLevel(line) const colored = colorizeLogLevel(line);
return colored return colored;
}) })
.filter((log) => { .filter((log) => {
if (logLevel === 'all') { if (logLevel === 'all') {
return true return true;
} }
return logLevel.has(log.level) return logLevel.has(log.level);
}) })
.map((log) => log.content) .map((log) => log.content)
.join('\r\n') .join('\r\n');
return logs return logs;
} };
const onDownloadLog = () => { const onDownloadLog = () => {
if (!logContent) { if (!logContent) {
return return;
} }
const blob = new Blob([logContent], { type: 'text/plain' }) const blob = new Blob([logContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob);
const a = document.createElement('a') const a = document.createElement('a');
a.href = url a.href = url;
a.download = `${selectedLog}.log` a.download = `${selectedLog}.log`;
a.click() a.click();
URL.revokeObjectURL(url) URL.revokeObjectURL(url);
} };
useEffect(() => { useEffect(() => {
if (!Xterm.current || !logContent) { if (!Xterm.current || !logContent) {
return return;
} }
Xterm.current.clear() Xterm.current.clear();
const _logContent = logToColored(logContent) const _logContent = logToColored(logContent);
Xterm.current.write(_logContent + '\r\nnapcat@webui:~$ ') Xterm.current.write(_logContent + '\r\nnapcat@webui:~$ ');
}, [logContent, logLevel]) }, [logContent, logLevel]);
return ( return (
<> <>
<title> - NapCat WebUI</title> <title> - NapCat WebUI</title>
<Card className="max-w-full h-full bg-opacity-50 backdrop-blur-sm"> <Card className='max-w-full h-full bg-opacity-50 backdrop-blur-sm'>
<CardHeader className="flex-row justify-start gap-3"> <CardHeader className='flex-row justify-start gap-3'>
<Select <Select
label="选择日志" label='选择日志'
size="sm" size='sm'
isLoading={listLoading} isLoading={listLoading}
errorMessage={listError?.message} errorMessage={listError?.message}
classNames={{ classNames={{
trigger: trigger:
'hover:!bg-content3 bg-opacity-50 backdrop-blur-sm hover:!bg-opacity-60' 'hover:!bg-content3 bg-opacity-50 backdrop-blur-sm hover:!bg-opacity-60',
}} }}
placeholder="选择日志" placeholder='选择日志'
onChange={(e) => { onChange={(e) => {
const value = e.target.value const value = e.target.value;
if (!value) { if (!value) {
return return;
} }
onSelect(value) onSelect(value);
}} }}
selectedKeys={[selectedLog || '']} selectedKeys={[selectedLog || '']}
items={list.map((name) => ({ items={list.map((name) => ({
value: name, value: name,
label: name label: name,
}))} }))}
> >
{(item) => ( {(item) => (
@@ -118,19 +118,19 @@ const HistoryLogs: React.FC<HistoryLogsProps> = (props) => {
selectedKeys={logLevel} selectedKeys={logLevel}
onSelectionChange={setLogLevel} onSelectionChange={setLogLevel}
/> />
<Button className="flex-shrink-0" onPress={onDownloadLog}> <Button className='flex-shrink-0' onPress={onDownloadLog}>
</Button> </Button>
<Button onPress={refreshList}></Button> <Button onPress={refreshList}></Button>
<Button onPress={refreshLog}></Button> <Button onPress={refreshLog}></Button>
</CardHeader> </CardHeader>
<CardBody className="relative"> <CardBody className='relative'>
<PageLoading loading={logLoading} /> <PageLoading loading={logLoading} />
<XTerm className="w-full h-full" ref={Xterm} /> <XTerm className='w-full h-full' ref={Xterm} />
</CardBody> </CardBody>
</Card> </Card>
</> </>
) );
} };
export default HistoryLogs export default HistoryLogs;

View File

@@ -1,9 +1,9 @@
import { Chip } from '@heroui/chip' import { Chip } from '@heroui/chip';
import { Select, SelectItem } from '@heroui/select' import { Select, SelectItem } from '@heroui/select';
import { SharedSelection } from '@heroui/system' import { SharedSelection } from '@heroui/system';
import type { Selection } from '@react-types/shared' import type { Selection } from '@react-types/shared';
import { LogLevel } from '@/const/enum' import { LogLevel } from '@/const/enum';
export interface LogLevelSelectProps { export interface LogLevelSelectProps {
selectedKeys: Selection selectedKeys: Selection
@@ -22,57 +22,57 @@ const logLevelColor: {
[LogLevel.INFO]: 'primary', [LogLevel.INFO]: 'primary',
[LogLevel.WARN]: 'warning', [LogLevel.WARN]: 'warning',
[LogLevel.ERROR]: 'primary', [LogLevel.ERROR]: 'primary',
[LogLevel.FATAL]: 'primary' [LogLevel.FATAL]: 'primary',
} };
const LogLevelSelect = (props: LogLevelSelectProps) => { const LogLevelSelect = (props: LogLevelSelectProps) => {
const { selectedKeys, onSelectionChange } = props const { selectedKeys, onSelectionChange } = props;
return ( return (
<Select <Select
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
onSelectionChange={(selectedKeys) => { onSelectionChange={(selectedKeys) => {
if (selectedKeys !== 'all' && selectedKeys?.size === 0) { if (selectedKeys !== 'all' && selectedKeys?.size === 0) {
selectedKeys = 'all' selectedKeys = 'all';
} }
onSelectionChange(selectedKeys) onSelectionChange(selectedKeys);
}} }}
label="日志级别" label='日志级别'
selectionMode="multiple" selectionMode='multiple'
aria-label="Log Level" aria-label='Log Level'
classNames={{ classNames={{
label: 'mb-2', label: 'mb-2',
trigger: 'bg-opacity-50 backdrop-blur-sm hover:!bg-opacity-60', trigger: 'bg-opacity-50 backdrop-blur-sm hover:!bg-opacity-60',
popoverContent: 'bg-opacity-50 backdrop-blur-sm' popoverContent: 'bg-opacity-50 backdrop-blur-sm',
}} }}
size="sm" size='sm'
items={[ items={[
{ label: 'Debug', value: LogLevel.DEBUG }, { label: 'Debug', value: LogLevel.DEBUG },
{ label: 'Info', value: LogLevel.INFO }, { label: 'Info', value: LogLevel.INFO },
{ label: 'Warn', value: LogLevel.WARN }, { label: 'Warn', value: LogLevel.WARN },
{ label: 'Error', value: LogLevel.ERROR }, { label: 'Error', value: LogLevel.ERROR },
{ label: 'Fatal', value: LogLevel.FATAL } { label: 'Fatal', value: LogLevel.FATAL },
]} ]}
renderValue={(value) => { renderValue={(value) => {
if (value.length === 5) { if (value.length === 5) {
return ( return (
<Chip size="sm" color="primary" variant="flat"> <Chip size='sm' color='primary' variant='flat'>
</Chip> </Chip>
) );
} }
return ( return (
<div className="flex gap-2"> <div className='flex gap-2'>
{value.map((v) => ( {value.map((v) => (
<Chip <Chip
size="sm" size='sm'
key={v.key} key={v.key}
color={logLevelColor[v.data?.value as LogLevel]} color={logLevelColor[v.data?.value as LogLevel]}
variant="flat" variant='flat'
> >
{v.data?.label} {v.data?.label}
</Chip> </Chip>
))} ))}
</div> </div>
) );
}} }}
> >
{(item) => ( {(item) => (
@@ -81,7 +81,7 @@ const LogLevelSelect = (props: LogLevelSelectProps) => {
</SelectItem> </SelectItem>
)} )}
</Select> </Select>
) );
} };
export default LogLevelSelect export default LogLevelSelect;

View File

@@ -1,114 +1,114 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import type { Selection } from '@react-types/shared' import type { Selection } from '@react-types/shared';
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { IoDownloadOutline } from 'react-icons/io5' import { IoDownloadOutline } from 'react-icons/io5';
import { colorizeLogLevelWithTag } from '@/utils/terminal' import { colorizeLogLevelWithTag } from '@/utils/terminal';
import WebUIManager, { Log } from '@/controllers/webui_manager' import WebUIManager, { Log } from '@/controllers/webui_manager';
import type { XTermRef } from '../xterm' import type { XTermRef } from '../xterm';
import XTerm from '../xterm' import XTerm from '../xterm';
import LogLevelSelect from './log_level_select' import LogLevelSelect from './log_level_select';
const RealTimeLogs = () => { const RealTimeLogs = () => {
const Xterm = useRef<XTermRef>(null) const Xterm = useRef<XTermRef>(null);
const [logLevel, setLogLevel] = useState<Selection>( const [logLevel, setLogLevel] = useState<Selection>(
new Set(['info', 'warn', 'error']) new Set(['info', 'warn', 'error'])
) );
const [dataArr, setDataArr] = useState<Log[]>([]) const [dataArr, setDataArr] = useState<Log[]>([]);
const onDownloadLog = () => { const onDownloadLog = () => {
const logContent = dataArr const logContent = dataArr
.filter((log) => { .filter((log) => {
if (logLevel === 'all') { if (logLevel === 'all') {
return true return true;
} }
return logLevel.has(log.level) return logLevel.has(log.level);
}) })
.map((log) => colorizeLogLevelWithTag(log.message, log.level)) .map((log) => colorizeLogLevelWithTag(log.message, log.level))
.join('\r\n') .join('\r\n');
const blob = new Blob([logContent], { type: 'text/plain' }) const blob = new Blob([logContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob);
const a = document.createElement('a') const a = document.createElement('a');
a.href = url a.href = url;
a.download = 'napcat.log' a.download = 'napcat.log';
a.click() a.click();
URL.revokeObjectURL(url) URL.revokeObjectURL(url);
} };
const writeStream = () => { const writeStream = () => {
try { try {
const _data = dataArr const _data = dataArr
.filter((log) => { .filter((log) => {
if (logLevel === 'all') { if (logLevel === 'all') {
return true return true;
} }
return logLevel.has(log.level) return logLevel.has(log.level);
}) })
.map((log) => colorizeLogLevelWithTag(log.message, log.level)) .map((log) => colorizeLogLevelWithTag(log.message, log.level))
.join('\r\n') .join('\r\n');
Xterm.current?.clear() Xterm.current?.clear();
Xterm.current?.write(_data) Xterm.current?.write(_data);
} catch (error) { } catch (error) {
console.error(error) console.error(error);
toast.error('获取实时日志失败') toast.error('获取实时日志失败');
} }
} };
useEffect(() => { useEffect(() => {
writeStream() writeStream();
}, [logLevel, dataArr]) }, [logLevel, dataArr]);
useEffect(() => { useEffect(() => {
const subscribeLogs = () => { const subscribeLogs = () => {
try { try {
const source = WebUIManager.getRealTimeLogs((data) => { const source = WebUIManager.getRealTimeLogs((data) => {
setDataArr((prev) => { setDataArr((prev) => {
const newData = [...prev, ...data] const newData = [...prev, ...data];
if (newData.length > 1000) { if (newData.length > 1000) {
newData.splice(0, newData.length - 1000) newData.splice(0, newData.length - 1000);
} }
return newData return newData;
}) });
}) });
return () => { return () => {
source.close() source.close();
} };
} catch (error) { } catch (_error) {
toast.error('获取实时日志失败') toast.error('获取实时日志失败');
} }
} };
const close = subscribeLogs() const close = subscribeLogs();
return () => { return () => {
console.log('close') console.log('close');
close?.() close?.();
} };
}, []) }, []);
return ( return (
<> <>
<title> - NapCat WebUI</title> <title> - NapCat WebUI</title>
<div className="flex items-center gap-2"> <div className='flex items-center gap-2'>
<LogLevelSelect <LogLevelSelect
selectedKeys={logLevel} selectedKeys={logLevel}
onSelectionChange={setLogLevel} onSelectionChange={setLogLevel}
/> />
<Button <Button
className="flex-shrink-0" className='flex-shrink-0'
onPress={onDownloadLog} onPress={onDownloadLog}
startContent={<IoDownloadOutline className="text-lg" />} startContent={<IoDownloadOutline className='text-lg' />}
> >
</Button> </Button>
</div> </div>
<div className="flex-1 h-full overflow-hidden"> <div className='flex-1 h-full overflow-hidden'>
<XTerm ref={Xterm} /> <XTerm ref={Xterm} />
</div> </div>
</> </>
) );
} };
export default RealTimeLogs export default RealTimeLogs;

View File

@@ -1,13 +1,13 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { import {
ModalBody, ModalBody,
ModalContent, ModalContent,
ModalFooter, ModalFooter,
ModalHeader, ModalHeader,
Modal as NextUIModal, Modal as NextUIModal,
useDisclosure useDisclosure,
} from '@heroui/modal' } from '@heroui/modal';
import React from 'react' import React from 'react';
export interface ModalProps { export interface ModalProps {
content: React.ReactNode content: React.ReactNode
@@ -37,8 +37,8 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
onConfirm, onConfirm,
onCancel, onCancel,
...rest ...rest
} = props } = props;
const { onClose: onNativeClose } = useDisclosure() const { onClose: onNativeClose } = useDisclosure();
return ( return (
<NextUIModal <NextUIModal
@@ -46,12 +46,12 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
backdrop={backdrop} backdrop={backdrop}
isDismissable={dismissible} isDismissable={dismissible}
onClose={() => { onClose={() => {
onClose?.() onClose?.();
onNativeClose() onNativeClose();
}} }}
classNames={{ classNames={{
backdrop: 'z-[99]', backdrop: 'z-[99]',
wrapper: 'z-[99]' wrapper: 'z-[99]',
}} }}
{...rest} {...rest}
> >
@@ -59,27 +59,27 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
{(nativeClose) => ( {(nativeClose) => (
<> <>
{title && ( {title && (
<ModalHeader className="flex flex-col gap-1">{title}</ModalHeader> <ModalHeader className='flex flex-col gap-1'>{title}</ModalHeader>
)} )}
<ModalBody className="break-all">{content}</ModalBody> <ModalBody className='break-all'>{content}</ModalBody>
<ModalFooter> <ModalFooter>
{showCancel && ( {showCancel && (
<Button <Button
color="primary" color='primary'
variant="light" variant='light'
onPress={() => { onPress={() => {
onCancel?.() onCancel?.();
nativeClose() nativeClose();
}} }}
> >
{cancelText} {cancelText}
</Button> </Button>
)} )}
<Button <Button
color="primary" color='primary'
onPress={() => { onPress={() => {
onConfirm?.() onConfirm?.();
nativeClose() nativeClose();
}} }}
> >
{confirmText} {confirmText}
@@ -89,9 +89,9 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
)} )}
</ModalContent> </ModalContent>
</NextUIModal> </NextUIModal>
) );
}) });
Modal.displayName = 'Modal' Modal.displayName = 'Modal';
export default Modal export default Modal;

View File

@@ -1,11 +1,11 @@
import { Listbox, ListboxItem } from '@heroui/listbox' import { Listbox, ListboxItem } from '@heroui/listbox';
import { Spinner } from '@heroui/spinner' import { Spinner } from '@heroui/spinner';
import { useRequest } from 'ahooks' import { useRequest } from 'ahooks';
import { MdError } from 'react-icons/md' import { MdError } from 'react-icons/md';
import IconWrapper from '@/components/github_info/icon_wrapper' import IconWrapper from '@/components/github_info/icon_wrapper';
import ItemCounter from '@/components/github_info/item_counter' import ItemCounter from '@/components/github_info/item_counter';
import GithubRelease from '@/components/github_info/release' import GithubRelease from '@/components/github_info/release';
import { import {
BookIcon, BookIcon,
BugIcon, BugIcon,
@@ -13,193 +13,197 @@ import {
StarIcon, StarIcon,
TagIcon, TagIcon,
UsersIcon, UsersIcon,
WatchersIcon WatchersIcon,
} from '@/components/icons' } from '@/components/icons';
import { request } from '@/utils/request' import { request } from '@/utils/request';
import { openUrl } from '@/utils/url' import { openUrl } from '@/utils/url';
import type { import type {
GirhubRepo, GirhubRepo,
GithubContributor, GithubContributor,
GithubPullRequest, GithubPullRequest,
GithubRelease as GithubReleaseType GithubRelease as GithubReleaseType,
} from '@/types/github' } from '@/types/github';
function displayData(data: number, loading: boolean, error?: Error) { function displayData (data: number, loading: boolean, error?: Error) {
if (error) { if (error) {
return <MdError className="text-primary-400" /> return <MdError className='text-primary-400' />;
} }
if (loading) { if (loading) {
return <Spinner size="sm" /> return <Spinner size='sm' />;
} }
return <ItemCounter number={data} /> return <ItemCounter number={data} />;
} }
export default function NapCatRepoInfo() { export default function NapCatRepoInfo () {
// repo info // repo info
const { const {
data: repoOriData, data: repoOriData,
error: repoError, error: repoError,
loading: repoLoading loading: repoLoading,
} = useRequest(() => } = useRequest(() =>
request.get<GirhubRepo>('https://api.github.com/repos/NapNeko/NapCatQQ') request.get<GirhubRepo>('https://api.github.com/repos/NapNeko/NapCatQQ')
) );
// release info // release info
const { const {
data: releaseOriData, data: releaseOriData,
error: releaseError, error: releaseError,
loading: releaseLoading loading: releaseLoading,
} = useRequest(() => } = useRequest(() =>
request.get<GithubReleaseType[]>( request.get<GithubReleaseType[]>(
'https://api.github.com/repos/NapNeko/NapCatQQ/releases' 'https://api.github.com/repos/NapNeko/NapCatQQ/releases'
) )
) );
// pr info // pr info
const { const {
data: prData, data: prData,
error: prError, error: prError,
loading: prLoading loading: prLoading,
} = useRequest(() => } = useRequest(() =>
request.get<GithubPullRequest[]>( request.get<GithubPullRequest[]>(
'https://api.github.com/repos/NapNeko/NapCatQQ/pulls' 'https://api.github.com/repos/NapNeko/NapCatQQ/pulls'
) )
) );
// contributors info // contributors info
const { const {
data: contributorsData, data: contributorsData,
error: contributorsError, error: contributorsError,
loading: contributorsLoading loading: contributorsLoading,
} = useRequest(() => } = useRequest(() =>
request.get<GithubContributor[]>( request.get<GithubContributor[]>(
'https://api.github.com/repos/NapNeko/NapCatQQ/contributors' 'https://api.github.com/repos/NapNeko/NapCatQQ/contributors'
) )
) );
const repoData = repoOriData?.data const repoData = repoOriData?.data;
const releaseData = releaseOriData?.data?.[0] const releaseData = releaseOriData?.data?.[0];
const prCount = prData?.data?.length || 0 const prCount = prData?.data?.length || 0;
const contributorsCount = contributorsData?.data?.length || 0 const contributorsCount = contributorsData?.data?.length || 0;
const releaseCount = releaseOriData?.data?.length || 0 const releaseCount = releaseOriData?.data?.length || 0;
return ( return (
<Listbox <Listbox
aria-label="NapCat Repo Info" aria-label='NapCat Repo Info'
className="p-0 gap-0 divide-y divide-default-300/50 dark:divide-default-100/80 bg-content1 max-w-[300px] overflow-visible shadow-small rounded-medium bg-opacity-50 backdrop-blur-sm" className='p-0 gap-0 divide-y divide-default-300/50 dark:divide-default-100/80 bg-content1 max-w-[300px] overflow-visible shadow-small rounded-medium bg-opacity-50 backdrop-blur-sm'
itemClasses={{ itemClasses={{
base: 'px-3 first:rounded-t-medium last:rounded-b-medium rounded-none gap-3 h-12 data-[hover=true]:bg-default-100/80' base: 'px-3 first:rounded-t-medium last:rounded-b-medium rounded-none gap-3 h-12 data-[hover=true]:bg-default-100/80',
}} }}
onAction={(key: React.Key) => { onAction={(key: React.Key) => {
switch (key) { switch (key) {
case 'releases': case 'releases':
openUrl('https://github.com/NapNeko/NapCatQQ/releases', true) openUrl('https://github.com/NapNeko/NapCatQQ/releases', true);
break break;
case 'contributors': case 'contributors':
openUrl( openUrl(
'https://github.com/NapNeko/NapCatQQ/graphs/contributors', 'https://github.com/NapNeko/NapCatQQ/graphs/contributors',
true true
) );
break break;
case 'license': case 'license':
openUrl( openUrl(
'https://github.com/NapNeko/NapCatQQ/blob/main/LICENSE', 'https://github.com/NapNeko/NapCatQQ/blob/main/LICENSE',
true true
) );
break break;
case 'watchers': case 'watchers':
openUrl('https://github.com/NapNeko/NapCatQQ/watchers', true) openUrl('https://github.com/NapNeko/NapCatQQ/watchers', true);
break break;
case 'star': case 'star':
openUrl('https://github.com/NapNeko/NapCatQQ/stargazers', true) openUrl('https://github.com/NapNeko/NapCatQQ/stargazers', true);
break break;
case 'issues': case 'issues':
openUrl('https://github.com/NapNeko/NapCatQQ/issues', true) openUrl('https://github.com/NapNeko/NapCatQQ/issues', true);
break break;
case 'pull_requests': case 'pull_requests':
openUrl('https://github.com/NapNeko/NapCatQQ/pulls', true) openUrl('https://github.com/NapNeko/NapCatQQ/pulls', true);
break break;
default: default:
openUrl('https://github.com/NapNeko/NapCatQQ', true) openUrl('https://github.com/NapNeko/NapCatQQ', true);
} }
}} }}
> >
<ListboxItem <ListboxItem
key="star" key='star'
endContent={displayData( endContent={displayData(
repoData?.stargazers_count ?? 0, repoData?.stargazers_count ?? 0,
false, false,
repoError repoError
)} )}
startContent={ startContent={
<IconWrapper className="bg-success/10 text-success"> <IconWrapper className='bg-success/10 text-success'>
<StarIcon className="text-lg" /> <StarIcon className='text-lg' />
</IconWrapper> </IconWrapper>
} }
> >
Star Star
</ListboxItem> </ListboxItem>
<ListboxItem <ListboxItem
key="issues" key='issues'
endContent={displayData( endContent={displayData(
repoData?.open_issues_count ?? 0, repoData?.open_issues_count ?? 0,
false, false,
repoError repoError
)} )}
startContent={ startContent={
<IconWrapper className="bg-success/10 text-success"> <IconWrapper className='bg-success/10 text-success'>
<BugIcon className="text-lg" /> <BugIcon className='text-lg' />
</IconWrapper> </IconWrapper>
} }
> >
Issues Issues
</ListboxItem> </ListboxItem>
<ListboxItem <ListboxItem
key="pull_requests" key='pull_requests'
endContent={displayData(prCount, prLoading, prError)} endContent={displayData(prCount, prLoading, prError)}
startContent={ startContent={
<IconWrapper className="bg-primary/10 text-primary"> <IconWrapper className='bg-primary/10 text-primary'>
<PullRequestIcon className="text-lg" /> <PullRequestIcon className='text-lg' />
</IconWrapper> </IconWrapper>
} }
> >
Pull Requests Pull Requests
</ListboxItem> </ListboxItem>
<ListboxItem <ListboxItem
key="releases" key='releases'
className="group h-auto py-3" className='group h-auto py-3'
endContent={ endContent={
releaseError ? ( releaseError
<MdError className="text-primary-400" /> ? (
) : releaseLoading ? ( <MdError className='text-primary-400' />
<Spinner size="sm" /> )
) : ( : releaseLoading
<ItemCounter number={releaseCount} /> ? (
) <Spinner size='sm' />
)
: (
<ItemCounter number={releaseCount} />
)
} }
startContent={ startContent={
<IconWrapper className="bg-primary/10 text-primary"> <IconWrapper className='bg-primary/10 text-primary'>
<TagIcon className="text-lg" /> <TagIcon className='text-lg' />
</IconWrapper> </IconWrapper>
} }
textValue="Releases" textValue='Releases'
> >
{releaseData && <GithubRelease releaseData={releaseData} />} {releaseData && <GithubRelease releaseData={releaseData} />}
</ListboxItem> </ListboxItem>
<ListboxItem <ListboxItem
key="contributors" key='contributors'
endContent={displayData( endContent={displayData(
contributorsCount, contributorsCount,
contributorsLoading, contributorsLoading,
contributorsError contributorsError
)} )}
startContent={ startContent={
<IconWrapper className="bg-warning/10 text-warning"> <IconWrapper className='bg-warning/10 text-warning'>
<UsersIcon /> <UsersIcon />
</IconWrapper> </IconWrapper>
} }
@@ -207,14 +211,14 @@ export default function NapCatRepoInfo() {
Contributors Contributors
</ListboxItem> </ListboxItem>
<ListboxItem <ListboxItem
key="watchers" key='watchers'
endContent={displayData( endContent={displayData(
repoData?.watchers_count ?? 0, repoData?.watchers_count ?? 0,
repoLoading, repoLoading,
repoError repoError
)} )}
startContent={ startContent={
<IconWrapper className="bg-default/50 text-foreground"> <IconWrapper className='bg-default/50 text-foreground'>
<WatchersIcon /> <WatchersIcon />
</IconWrapper> </IconWrapper>
} }
@@ -222,14 +226,14 @@ export default function NapCatRepoInfo() {
Watchers Watchers
</ListboxItem> </ListboxItem>
<ListboxItem <ListboxItem
key="license" key='license'
endContent={ endContent={
<span className="text-small text-default-400"> <span className='text-small text-default-400'>
{repoData?.license?.name ?? 'unknown'} {repoData?.license?.name ?? 'unknown'}
</span> </span>
} }
startContent={ startContent={
<IconWrapper className="bg-primary/10 text-primary dark:text-primary-500"> <IconWrapper className='bg-primary/10 text-primary dark:text-primary-500'>
<BookIcon /> <BookIcon />
</IconWrapper> </IconWrapper>
} }
@@ -237,5 +241,5 @@ export default function NapCatRepoInfo() {
License License
</ListboxItem> </ListboxItem>
</Listbox> </Listbox>
) );
} }

View File

@@ -1,87 +1,87 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import { ModalBody, ModalFooter } from '@heroui/modal' import { ModalBody, ModalFooter } from '@heroui/modal';
import { Select, SelectItem } from '@heroui/select' import { Select, SelectItem } from '@heroui/select';
import { ReactElement, useEffect } from 'react' import { ReactElement, useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form' import { Controller, useForm } from 'react-hook-form';
import type { import type {
DefaultValues, DefaultValues,
Path, Path,
PathValue, PathValue,
SubmitHandler SubmitHandler,
} from 'react-hook-form' } from 'react-hook-form';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import SwitchCard from '../switch_card' import SwitchCard from '../switch_card';
export type FieldTypes = 'input' | 'select' | 'switch' export type FieldTypes = 'input' | 'select' | 'switch';
type NetworkConfigType = OneBotConfig['network'] type NetworkConfigType = OneBotConfig['network'];
export interface Field<T extends keyof OneBotConfig['network']> { export interface Field<T extends keyof OneBotConfig['network']> {
name: keyof NetworkConfigType[T][0] name: keyof NetworkConfigType[T][0];
label: string label: string;
type: FieldTypes type: FieldTypes;
options?: Array<{ key: string; value: string }> options?: Array<{ key: string; value: string; }>;
placeholder?: string placeholder?: string;
isRequired?: boolean isRequired?: boolean;
isDisabled?: boolean isDisabled?: boolean;
description?: string description?: string;
colSpan?: 1 | 2 colSpan?: 1 | 2;
} }
export interface GenericFormProps<T extends keyof NetworkConfigType> { export interface GenericFormProps<T extends keyof NetworkConfigType> {
data?: NetworkConfigType[T][0] data?: NetworkConfigType[T][0];
defaultValues: DefaultValues<NetworkConfigType[T][0]> defaultValues: DefaultValues<NetworkConfigType[T][0]>;
onClose: () => void onClose: () => void;
onSubmit: (data: NetworkConfigType[T][0]) => Promise<void> onSubmit: (data: NetworkConfigType[T][0]) => Promise<void>;
fields: Array<Field<T>> fields: Array<Field<T>>;
} }
const GenericForm = <T extends keyof NetworkConfigType>({ const GenericForm = <T extends keyof NetworkConfigType> ({
data, data,
defaultValues, defaultValues,
onClose, onClose,
onSubmit, onSubmit,
fields fields,
}: GenericFormProps<T>): ReactElement => { }: GenericFormProps<T>): ReactElement => {
const { control, handleSubmit, formState, setValue, reset } = useForm< const { control, handleSubmit, formState, setValue, reset } = useForm<
NetworkConfigType[T][0] NetworkConfigType[T][0]
>({ >({
defaultValues defaultValues,
}) });
const submitAction: SubmitHandler<NetworkConfigType[T][0]> = async (data) => { const submitAction: SubmitHandler<NetworkConfigType[T][0]> = async (data) => {
await onSubmit(data) await onSubmit(data);
onClose() onClose();
} };
const _onSubmit = handleSubmit(submitAction, (e) => { const _onSubmit = handleSubmit(submitAction, (e) => {
for (const error in e) { const errors = Object.values(e);
toast.error(e[error]?.message as string) if (errors.length > 0) {
return toast.error(errors[0]?.message as string);
} }
}) });
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const keys = Object.keys(data) as Path<NetworkConfig[T][0]>[] const keys = Object.keys(data) as Path<NetworkConfig[T][0]>[];
for (const key of keys) { for (const key of keys) {
const value = data[key] as PathValue< const value = data[key] as PathValue<
NetworkConfig[T][0], NetworkConfig[T][0],
Path<NetworkConfig[T][0]> Path<NetworkConfig[T][0]>
> >;
setValue(key, value) setValue(key, value);
} }
} else { } else {
reset() reset();
} }
}, [data, reset, setValue]) }, [data, reset, setValue]);
return ( return (
<> <>
<ModalBody> <ModalBody>
<div className="grid grid-cols-2 gap-y-4 gap-x-2 w-full"> <div className='grid grid-cols-2 gap-y-4 gap-x-2 w-full'>
{fields.map((field) => ( {fields.map((field) => (
<div <div
key={field.name as string} key={field.name as string}
@@ -93,9 +93,9 @@ const GenericForm = <T extends keyof NetworkConfigType>({
rules={ rules={
field.isRequired field.isRequired
? { ? {
required: `请填写${field.label}` required: `请填写${field.label}`,
} }
: void 0 : undefined
} }
render={({ field: controllerField }) => { render={({ field: controllerField }) => {
switch (field.type) { switch (field.type) {
@@ -103,15 +103,14 @@ const GenericForm = <T extends keyof NetworkConfigType>({
return ( return (
<Input <Input
value={controllerField.value as string} value={controllerField.value as string}
onChange={controllerField.onChange} onValueChange={(value) => controllerField.onChange(value)}
onBlur={controllerField.onBlur}
ref={controllerField.ref} ref={controllerField.ref}
isRequired={field.isRequired} isRequired={field.isRequired}
isDisabled={field.isDisabled} isDisabled={field.isDisabled}
label={field.label} label={field.label}
placeholder={field.placeholder} placeholder={field.placeholder}
/> />
) );
case 'select': case 'select':
return ( return (
<Select <Select
@@ -129,7 +128,7 @@ const GenericForm = <T extends keyof NetworkConfigType>({
</SelectItem> </SelectItem>
)) || <></>} )) || <></>}
</Select> </Select>
) );
case 'switch': case 'switch':
return ( return (
<SwitchCard <SwitchCard
@@ -138,9 +137,9 @@ const GenericForm = <T extends keyof NetworkConfigType>({
description={field.description} description={field.description}
label={field.label} label={field.label}
/> />
) );
default: default:
return <></> return <></>;
} }
}} }}
/> />
@@ -150,15 +149,15 @@ const GenericForm = <T extends keyof NetworkConfigType>({
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color='primary'
isDisabled={formState.isSubmitting} isDisabled={formState.isSubmitting}
variant="light" variant='light'
onPress={onClose} onPress={onClose}
> >
</Button> </Button>
<Button <Button
color="primary" color='primary'
isLoading={formState.isSubmitting} isLoading={formState.isSubmitting}
onPress={() => _onSubmit()} onPress={() => _onSubmit()}
> >
@@ -166,7 +165,16 @@ const GenericForm = <T extends keyof NetworkConfigType>({
</Button> </Button>
</ModalFooter> </ModalFooter>
</> </>
) );
} };
export default GenericForm export default GenericForm;
export function random_token (length: number) {
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()-_=+[]{}|;:,.<>?';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}

View File

@@ -1,5 +1,5 @@
import GenericForm from './generic_form' import GenericForm, { random_token } from './generic_form';
import type { Field } from './generic_form' import type { Field } from './generic_form';
export interface HTTPClientFormProps { export interface HTTPClientFormProps {
data?: OneBotConfig['network']['httpClients'][0] data?: OneBotConfig['network']['httpClients'][0]
@@ -7,12 +7,12 @@ export interface HTTPClientFormProps {
onSubmit: (data: OneBotConfig['network']['httpClients'][0]) => Promise<void> onSubmit: (data: OneBotConfig['network']['httpClients'][0]) => Promise<void>
} }
type HTTPClientFormType = OneBotConfig['network']['httpClients'] type HTTPClientFormType = OneBotConfig['network']['httpClients'];
const HTTPClientForm: React.FC<HTTPClientFormProps> = ({ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
data, data,
onClose, onClose,
onSubmit onSubmit,
}) => { }) => {
const defaultValues: HTTPClientFormType[0] = { const defaultValues: HTTPClientFormType[0] = {
enable: false, enable: false,
@@ -20,9 +20,9 @@ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
url: 'http://localhost:8080', url: 'http://localhost:8080',
reportSelfMessage: false, reportSelfMessage: false,
messagePostFormat: 'array', messagePostFormat: 'array',
token: '', token: random_token(16),
debug: false debug: false,
} };
const fields: Field<'httpClients'>[] = [ const fields: Field<'httpClients'>[] = [
{ {
@@ -30,14 +30,14 @@ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
label: '启用', label: '启用',
type: 'switch', type: 'switch',
description: '保存后启用此配置', description: '保存后启用此配置',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'debug', name: 'debug',
label: '开启Debug', label: '开启Debug',
type: 'switch', type: 'switch',
description: '是否开启调试模式', description: '是否开启调试模式',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'name', name: 'name',
@@ -45,21 +45,21 @@ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
type: 'input', type: 'input',
placeholder: '请输入名称', placeholder: '请输入名称',
isRequired: true, isRequired: true,
isDisabled: !!data isDisabled: !!data,
}, },
{ {
name: 'url', name: 'url',
label: 'URL', label: 'URL',
type: 'input', type: 'input',
placeholder: '请输入URL', placeholder: '请输入URL',
isRequired: true isRequired: true,
}, },
{ {
name: 'reportSelfMessage', name: 'reportSelfMessage',
label: '上报自身消息', label: '上报自身消息',
type: 'switch', type: 'switch',
description: '是否上报自身消息', description: '是否上报自身消息',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'messagePostFormat', name: 'messagePostFormat',
@@ -69,17 +69,17 @@ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
isRequired: true, isRequired: true,
options: [ options: [
{ key: 'array', value: 'Array' }, { key: 'array', value: 'Array' },
{ key: 'string', value: 'String' } { key: 'string', value: 'String' },
], ],
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'token', name: 'token',
label: 'Token', label: 'Token',
type: 'input', type: 'input',
placeholder: '请输入Token' placeholder: '请输入Token',
} },
] ];
return ( return (
<GenericForm <GenericForm
@@ -89,7 +89,7 @@ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
onSubmit={onSubmit} onSubmit={onSubmit}
fields={fields} fields={fields}
/> />
) );
} };
export default HTTPClientForm export default HTTPClientForm;

View File

@@ -1,5 +1,5 @@
import GenericForm from './generic_form' import GenericForm, { random_token } from './generic_form';
import type { Field } from './generic_form' import type { Field } from './generic_form';
export interface HTTPServerFormProps { export interface HTTPServerFormProps {
data?: OneBotConfig['network']['httpServers'][0] data?: OneBotConfig['network']['httpServers'][0]
@@ -7,24 +7,24 @@ export interface HTTPServerFormProps {
onSubmit: (data: OneBotConfig['network']['httpServers'][0]) => Promise<void> onSubmit: (data: OneBotConfig['network']['httpServers'][0]) => Promise<void>
} }
type HTTPServerFormType = OneBotConfig['network']['httpServers'] type HTTPServerFormType = OneBotConfig['network']['httpServers'];
const HTTPServerForm: React.FC<HTTPServerFormProps> = ({ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
data, data,
onClose, onClose,
onSubmit onSubmit,
}) => { }) => {
const defaultValues: HTTPServerFormType[0] = { const defaultValues: HTTPServerFormType[0] = {
enable: false, enable: false,
name: '', name: '',
host: '0.0.0.0', host: '127.0.0.1',
port: 3000, port: 3000,
enableCors: true, enableCors: true,
enableWebsocket: true, enableWebsocket: true,
messagePostFormat: 'array', messagePostFormat: 'array',
token: '', token: random_token(16),
debug: false debug: false,
} };
const fields: Field<'httpServers'>[] = [ const fields: Field<'httpServers'>[] = [
{ {
@@ -32,14 +32,14 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
label: '启用', label: '启用',
type: 'switch', type: 'switch',
description: '保存后启用此配置', description: '保存后启用此配置',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'debug', name: 'debug',
label: '开启Debug', label: '开启Debug',
type: 'switch', type: 'switch',
description: '是否开启调试模式', description: '是否开启调试模式',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'name', name: 'name',
@@ -47,35 +47,35 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
type: 'input', type: 'input',
placeholder: '请输入名称', placeholder: '请输入名称',
isRequired: true, isRequired: true,
isDisabled: !!data isDisabled: !!data,
}, },
{ {
name: 'host', name: 'host',
label: 'Host', label: 'Host',
type: 'input', type: 'input',
placeholder: '请输入主机地址', placeholder: '请输入主机地址',
isRequired: true isRequired: true,
}, },
{ {
name: 'port', name: 'port',
label: 'Port', label: 'Port',
type: 'input', type: 'input',
placeholder: '请输入端口', placeholder: '请输入端口',
isRequired: true isRequired: true,
}, },
{ {
name: 'enableCors', name: 'enableCors',
label: '启用CORS', label: '启用CORS',
type: 'switch', type: 'switch',
description: '是否启用CORS跨域', description: '是否启用CORS跨域',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'enableWebsocket', name: 'enableWebsocket',
label: '启用Websocket', label: '启用Websocket',
type: 'switch', type: 'switch',
description: '是否启用Websocket', description: '是否启用Websocket',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'messagePostFormat', name: 'messagePostFormat',
@@ -85,16 +85,16 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
isRequired: true, isRequired: true,
options: [ options: [
{ key: 'array', value: 'Array' }, { key: 'array', value: 'Array' },
{ key: 'string', value: 'String' } { key: 'string', value: 'String' },
] ],
}, },
{ {
name: 'token', name: 'token',
label: 'Token', label: 'Token',
type: 'input', type: 'input',
placeholder: '请输入Token' placeholder: '请输入Token',
} },
] ];
return ( return (
<GenericForm <GenericForm
@@ -104,7 +104,7 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
onSubmit={onSubmit} onSubmit={onSubmit}
fields={fields} fields={fields}
/> />
) );
} };
export default HTTPServerForm export default HTTPServerForm;

View File

@@ -1,5 +1,5 @@
import GenericForm from './generic_form' import GenericForm, { random_token } from './generic_form';
import type { Field } from './generic_form' import type { Field } from './generic_form';
export interface HTTPServerSSEFormProps { export interface HTTPServerSSEFormProps {
data?: OneBotConfig['network']['httpSseServers'][0] data?: OneBotConfig['network']['httpSseServers'][0]
@@ -9,25 +9,25 @@ export interface HTTPServerSSEFormProps {
) => Promise<void> ) => Promise<void>
} }
type HTTPServerSSEFormType = OneBotConfig['network']['httpSseServers'] type HTTPServerSSEFormType = OneBotConfig['network']['httpSseServers'];
const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
data, data,
onClose, onClose,
onSubmit onSubmit,
}) => { }) => {
const defaultValues: HTTPServerSSEFormType[0] = { const defaultValues: HTTPServerSSEFormType[0] = {
enable: false, enable: false,
name: '', name: '',
host: '0.0.0.0', host: '127.0.0.1',
port: 3000, port: 3000,
enableCors: true, enableCors: true,
enableWebsocket: true, enableWebsocket: true,
messagePostFormat: 'array', messagePostFormat: 'array',
token: '', token: random_token(16),
debug: false, debug: false,
reportSelfMessage: false reportSelfMessage: false,
} };
const fields: Field<'httpSseServers'>[] = [ const fields: Field<'httpSseServers'>[] = [
{ {
@@ -35,14 +35,14 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
label: '启用', label: '启用',
type: 'switch', type: 'switch',
description: '保存后启用此配置', description: '保存后启用此配置',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'debug', name: 'debug',
label: '开启Debug', label: '开启Debug',
type: 'switch', type: 'switch',
description: '是否开启调试模式', description: '是否开启调试模式',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'name', name: 'name',
@@ -50,35 +50,35 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
type: 'input', type: 'input',
placeholder: '请输入名称', placeholder: '请输入名称',
isRequired: true, isRequired: true,
isDisabled: !!data isDisabled: !!data,
}, },
{ {
name: 'host', name: 'host',
label: 'Host', label: 'Host',
type: 'input', type: 'input',
placeholder: '请输入主机地址', placeholder: '请输入主机地址',
isRequired: true isRequired: true,
}, },
{ {
name: 'port', name: 'port',
label: 'Port', label: 'Port',
type: 'input', type: 'input',
placeholder: '请输入端口', placeholder: '请输入端口',
isRequired: true isRequired: true,
}, },
{ {
name: 'enableCors', name: 'enableCors',
label: '启用CORS', label: '启用CORS',
type: 'switch', type: 'switch',
description: '是否启用CORS跨域', description: '是否启用CORS跨域',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'enableWebsocket', name: 'enableWebsocket',
label: '启用Websocket', label: '启用Websocket',
type: 'switch', type: 'switch',
description: '是否启用Websocket', description: '是否启用Websocket',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'messagePostFormat', name: 'messagePostFormat',
@@ -88,23 +88,23 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
isRequired: true, isRequired: true,
options: [ options: [
{ key: 'array', value: 'Array' }, { key: 'array', value: 'Array' },
{ key: 'string', value: 'String' } { key: 'string', value: 'String' },
] ],
}, },
{ {
name: 'token', name: 'token',
label: 'Token', label: 'Token',
type: 'input', type: 'input',
placeholder: '请输入Token' placeholder: '请输入Token',
}, },
{ {
name: 'reportSelfMessage', name: 'reportSelfMessage',
label: '上报自身消息', label: '上报自身消息',
type: 'switch', type: 'switch',
description: '是否上报自身消息', description: '是否上报自身消息',
colSpan: 1 colSpan: 1,
} },
] ];
return ( return (
<GenericForm <GenericForm
@@ -114,7 +114,7 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
onSubmit={onSubmit} onSubmit={onSubmit}
fields={fields} fields={fields}
/> />
) );
} };
export default HTTPServerSSEForm export default HTTPServerSSEForm;

View File

@@ -1,54 +1,54 @@
import { Modal, ModalContent, ModalHeader } from '@heroui/modal' import { Modal, ModalContent, ModalHeader } from '@heroui/modal';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import useConfig from '@/hooks/use-config' import useConfig from '@/hooks/use-config';
import HTTPClientForm from './http_client' import HTTPClientForm from './http_client';
import HTTPServerForm from './http_server' import HTTPServerForm from './http_server';
import HTTPServerSSEForm from './http_sse' import HTTPServerSSEForm from './http_sse';
import WebsocketClientForm from './ws_client' import WebsocketClientForm from './ws_client';
import WebsocketServerForm from './ws_server' import WebsocketServerForm from './ws_server';
const modalTitle = { const modalTitle = {
httpServers: 'HTTP Server', httpServers: 'HTTP Server',
httpClients: 'HTTP Client', httpClients: 'HTTP Client',
httpSseServers: 'HTTP SSE Server', httpSseServers: 'HTTP SSE Server',
websocketServers: 'Websocket Server', websocketServers: 'Websocket Server',
websocketClients: 'Websocket Client' websocketClients: 'Websocket Client',
} };
export interface NetworkFormModalProps< export interface NetworkFormModalProps<
T extends keyof OneBotConfig['network'] T extends keyof OneBotConfig['network']
> { > {
isOpen: boolean isOpen: boolean;
field: T field: T;
data?: OneBotConfig['network'][T][0] data?: OneBotConfig['network'][T][0];
onOpenChange: (isOpen: boolean) => void onOpenChange: (isOpen: boolean) => void;
} }
const NetworkFormModal = <T extends keyof OneBotConfig['network']>( const NetworkFormModal = <T extends keyof OneBotConfig['network']> (
props: NetworkFormModalProps<T> props: NetworkFormModalProps<T>
) => { ) => {
const { isOpen, onOpenChange, field, data } = props const { isOpen, onOpenChange, field, data } = props;
const { createNetworkConfig, updateNetworkConfig } = useConfig() const { createNetworkConfig, updateNetworkConfig } = useConfig();
const isCreate = !data const isCreate = !data;
const onSubmit = async (data: OneBotConfig['network'][typeof field][0]) => { const onSubmit = async (data: OneBotConfig['network'][typeof field][0]) => {
try { try {
if (isCreate) { if (isCreate) {
await createNetworkConfig(field, data) await createNetworkConfig(field, data);
} else { } else {
await updateNetworkConfig(field, data) await updateNetworkConfig(field, data);
} }
toast.success('保存配置成功') toast.success('保存配置成功');
} catch (error) { } catch (error) {
const msg = (error as Error).message const msg = (error as Error).message;
toast.error(`保存配置失败: ${msg}`) toast.error(`保存配置失败: ${msg}`);
throw error throw error;
} }
} };
const renderFormComponent = (onClose: () => void) => { const renderFormComponent = (onClose: () => void) => {
switch (field) { switch (field) {
@@ -59,7 +59,7 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
onClose={onClose} onClose={onClose}
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
) );
case 'httpClients': case 'httpClients':
return ( return (
<HTTPClientForm <HTTPClientForm
@@ -67,7 +67,7 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
onClose={onClose} onClose={onClose}
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
) );
case 'websocketServers': case 'websocketServers':
return ( return (
<WebsocketServerForm <WebsocketServerForm
@@ -75,7 +75,7 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
onClose={onClose} onClose={onClose}
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
) );
case 'websocketClients': case 'websocketClients':
return ( return (
<WebsocketClientForm <WebsocketClientForm
@@ -83,7 +83,7 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
onClose={onClose} onClose={onClose}
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
) );
case 'httpSseServers': case 'httpSseServers':
return ( return (
<HTTPServerSSEForm <HTTPServerSSEForm
@@ -91,25 +91,25 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
onClose={onClose} onClose={onClose}
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
) );
default: default:
return null return null;
} }
} };
return ( return (
<Modal <Modal
backdrop="blur" backdrop='blur'
isDismissable={false} isDismissable={false}
isOpen={isOpen} isOpen={isOpen}
size="lg" size='lg'
scrollBehavior="outside" scrollBehavior='outside'
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
> >
<ModalContent> <ModalContent>
{(onClose) => ( {(onClose) => (
<> <>
<ModalHeader className="flex flex-col gap-1"> <ModalHeader className='flex flex-col gap-1'>
{modalTitle[field]} {modalTitle[field]}
</ModalHeader> </ModalHeader>
{renderFormComponent(onClose)} {renderFormComponent(onClose)}
@@ -117,7 +117,7 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
)} )}
</ModalContent> </ModalContent>
</Modal> </Modal>
) );
} };
export default NetworkFormModal export default NetworkFormModal;

View File

@@ -1,5 +1,5 @@
import GenericForm from './generic_form' import GenericForm, { random_token } from './generic_form';
import type { Field } from './generic_form' import type { Field } from './generic_form';
export interface WebsocketClientFormProps { export interface WebsocketClientFormProps {
data?: OneBotConfig['network']['websocketClients'][0] data?: OneBotConfig['network']['websocketClients'][0]
@@ -9,12 +9,12 @@ export interface WebsocketClientFormProps {
) => Promise<void> ) => Promise<void>
} }
type WebsocketClientFormType = OneBotConfig['network']['websocketClients'] type WebsocketClientFormType = OneBotConfig['network']['websocketClients'];
const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
data, data,
onClose, onClose,
onSubmit onSubmit,
}) => { }) => {
const defaultValues: WebsocketClientFormType[0] = { const defaultValues: WebsocketClientFormType[0] = {
enable: false, enable: false,
@@ -22,11 +22,11 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
url: 'ws://localhost:8082', url: 'ws://localhost:8082',
reportSelfMessage: false, reportSelfMessage: false,
messagePostFormat: 'array', messagePostFormat: 'array',
token: '', token: random_token(16),
debug: false, debug: false,
heartInterval: 30000, heartInterval: 30000,
reconnectInterval: 30000 reconnectInterval: 30000,
} };
const fields: Field<'websocketClients'>[] = [ const fields: Field<'websocketClients'>[] = [
{ {
@@ -34,14 +34,14 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
label: '启用', label: '启用',
type: 'switch', type: 'switch',
description: '保存后启用此配置', description: '保存后启用此配置',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'debug', name: 'debug',
label: '开启Debug', label: '开启Debug',
type: 'switch', type: 'switch',
description: '是否开启调试模式', description: '是否开启调试模式',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'name', name: 'name',
@@ -49,21 +49,21 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
type: 'input', type: 'input',
placeholder: '请输入名称', placeholder: '请输入名称',
isRequired: true, isRequired: true,
isDisabled: !!data isDisabled: !!data,
}, },
{ {
name: 'url', name: 'url',
label: 'URL', label: 'URL',
type: 'input', type: 'input',
placeholder: '请输入URL', placeholder: '请输入URL',
isRequired: true isRequired: true,
}, },
{ {
name: 'reportSelfMessage', name: 'reportSelfMessage',
label: '上报自身消息', label: '上报自身消息',
type: 'switch', type: 'switch',
description: '是否上报自身消息', description: '是否上报自身消息',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'messagePostFormat', name: 'messagePostFormat',
@@ -73,15 +73,15 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
isRequired: true, isRequired: true,
options: [ options: [
{ key: 'array', value: 'Array' }, { key: 'array', value: 'Array' },
{ key: 'string', value: 'String' } { key: 'string', value: 'String' },
], ],
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'token', name: 'token',
label: 'Token', label: 'Token',
type: 'input', type: 'input',
placeholder: '请输入Token' placeholder: '请输入Token',
}, },
{ {
name: 'heartInterval', name: 'heartInterval',
@@ -89,7 +89,7 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
type: 'input', type: 'input',
placeholder: '请输入心跳间隔', placeholder: '请输入心跳间隔',
isRequired: true, isRequired: true,
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'reconnectInterval', name: 'reconnectInterval',
@@ -97,9 +97,9 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
type: 'input', type: 'input',
placeholder: '请输入重连间隔', placeholder: '请输入重连间隔',
isRequired: true, isRequired: true,
colSpan: 1 colSpan: 1,
} },
] ];
return ( return (
<GenericForm <GenericForm
@@ -109,7 +109,7 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
onSubmit={onSubmit} onSubmit={onSubmit}
fields={fields} fields={fields}
/> />
) );
} };
export default WebsocketClientForm export default WebsocketClientForm;

View File

@@ -1,5 +1,5 @@
import GenericForm from './generic_form' import GenericForm, { random_token } from './generic_form';
import type { Field } from './generic_form' import type { Field } from './generic_form';
export interface WebsocketServerFormProps { export interface WebsocketServerFormProps {
data?: OneBotConfig['network']['websocketServers'][0] data?: OneBotConfig['network']['websocketServers'][0]
@@ -9,25 +9,25 @@ export interface WebsocketServerFormProps {
) => Promise<void> ) => Promise<void>
} }
type WebsocketServerFormType = OneBotConfig['network']['websocketServers'] type WebsocketServerFormType = OneBotConfig['network']['websocketServers'];
const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
data, data,
onClose, onClose,
onSubmit onSubmit,
}) => { }) => {
const defaultValues: WebsocketServerFormType[0] = { const defaultValues: WebsocketServerFormType[0] = {
enable: false, enable: false,
name: '', name: '',
host: '0.0.0.0', host: '127.0.0.1',
port: 3001, port: 3001,
reportSelfMessage: false, reportSelfMessage: false,
enableForcePushEvent: true, enableForcePushEvent: true,
messagePostFormat: 'array', messagePostFormat: 'array',
token: '', token: random_token(16),
debug: false, debug: false,
heartInterval: 30000 heartInterval: 30000,
} };
const fields: Field<'websocketServers'>[] = [ const fields: Field<'websocketServers'>[] = [
{ {
@@ -35,14 +35,14 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
label: '启用', label: '启用',
type: 'switch', type: 'switch',
description: '保存后启用此配置', description: '保存后启用此配置',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'debug', name: 'debug',
label: '开启Debug', label: '开启Debug',
type: 'switch', type: 'switch',
description: '是否开启调试模式', description: '是否开启调试模式',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'name', name: 'name',
@@ -50,14 +50,14 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
type: 'input', type: 'input',
placeholder: '请输入名称', placeholder: '请输入名称',
isRequired: true, isRequired: true,
isDisabled: !!data isDisabled: !!data,
}, },
{ {
name: 'host', name: 'host',
label: 'Host', label: 'Host',
type: 'input', type: 'input',
placeholder: '请输入主机地址', placeholder: '请输入主机地址',
isRequired: true isRequired: true,
}, },
{ {
name: 'port', name: 'port',
@@ -65,7 +65,7 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
type: 'input', type: 'input',
placeholder: '请输入端口', placeholder: '请输入端口',
isRequired: true, isRequired: true,
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'messagePostFormat', name: 'messagePostFormat',
@@ -75,38 +75,38 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
isRequired: true, isRequired: true,
options: [ options: [
{ key: 'array', value: 'Array' }, { key: 'array', value: 'Array' },
{ key: 'string', value: 'String' } { key: 'string', value: 'String' },
], ],
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'reportSelfMessage', name: 'reportSelfMessage',
label: '上报自身消息', label: '上报自身消息',
type: 'switch', type: 'switch',
description: '是否上报自身消息', description: '是否上报自身消息',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'enableForcePushEvent', name: 'enableForcePushEvent',
label: '强制推送事件', label: '强制推送事件',
type: 'switch', type: 'switch',
description: '是否强制推送事件', description: '是否强制推送事件',
colSpan: 1 colSpan: 1,
}, },
{ {
name: 'token', name: 'token',
label: 'Token', label: 'Token',
type: 'input', type: 'input',
placeholder: '请输入Token' placeholder: '请输入Token',
}, },
{ {
name: 'heartInterval', name: 'heartInterval',
label: '心跳间隔', label: '心跳间隔',
type: 'input', type: 'input',
placeholder: '请输入心跳间隔', placeholder: '请输入心跳间隔',
isRequired: true isRequired: true,
} },
] ];
return ( return (
<GenericForm <GenericForm
@@ -116,7 +116,7 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
onSubmit={onSubmit} onSubmit={onSubmit}
fields={fields} fields={fields}
/> />
) );
} };
export default WebsocketServerForm export default WebsocketServerForm;

View File

@@ -1,133 +1,131 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Card, CardBody, CardHeader } from '@heroui/card' import { Card, CardBody, CardHeader } from '@heroui/card';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import { Snippet } from '@heroui/snippet' import { Snippet } from '@heroui/snippet';
import { useLocalStorage } from '@uidotdev/usehooks' import { useLocalStorage } from '@uidotdev/usehooks';
import { motion } from 'motion/react' import { motion } from 'motion/react';
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { IoLink, IoSend } from 'react-icons/io5' import { IoLink, IoSend } from 'react-icons/io5';
import { PiCatDuotone } from 'react-icons/pi' import { PiCatDuotone } from 'react-icons/pi';
import key from '@/const/key' import key from '@/const/key';
import { OneBotHttpApiContent, OneBotHttpApiPath } from '@/const/ob_api' import { OneBotHttpApiContent, OneBotHttpApiPath } from '@/const/ob_api';
import ChatInputModal from '@/components/chat_input/modal' import ChatInputModal from '@/components/chat_input/modal';
import CodeEditor from '@/components/code_editor' import CodeEditor from '@/components/code_editor';
import PageLoading from '@/components/page_loading' import PageLoading from '@/components/page_loading';
import { request } from '@/utils/request' import { request } from '@/utils/request';
import { parseAxiosResponse } from '@/utils/url' import { parseAxiosResponse } from '@/utils/url';
import { generateDefaultJson, parse } from '@/utils/zod' import { generateDefaultJson, parse } from '@/utils/zod';
import DisplayStruct from './display_struct' import DisplayStruct from './display_struct';
export interface OneBotApiDebugProps { export interface OneBotApiDebugProps {
path: OneBotHttpApiPath path: OneBotHttpApiPath;
data: OneBotHttpApiContent data: OneBotHttpApiContent;
} }
const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => { const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
const { path, data } = props const { path, data } = props;
const currentURL = new URL(window.location.origin) const currentURL = new URL(window.location.origin);
currentURL.port = '3000' currentURL.port = '3000';
const defaultHttpUrl = currentURL.href const defaultHttpUrl = currentURL.href;
const [httpConfig, setHttpConfig] = useLocalStorage(key.httpDebugConfig, { const [httpConfig, setHttpConfig] = useLocalStorage(key.httpDebugConfig, {
url: defaultHttpUrl, url: defaultHttpUrl,
token: '' token: '',
}) });
const [requestBody, setRequestBody] = useState('{}') const [requestBody, setRequestBody] = useState('{}');
const [responseContent, setResponseContent] = useState('') const [responseContent, setResponseContent] = useState('');
const [isCodeEditorOpen, setIsCodeEditorOpen] = useState(false) const [isCodeEditorOpen, setIsCodeEditorOpen] = useState(false);
const [isResponseOpen, setIsResponseOpen] = useState(false) const [isResponseOpen, setIsResponseOpen] = useState(false);
const [isFetching, setIsFetching] = useState(false) const [isFetching, setIsFetching] = useState(false);
const responseRef = useRef<HTMLDivElement>(null) const responseRef = useRef<HTMLDivElement>(null);
const parsedRequest = parse(data.request) const parsedRequest = parse(data.request);
const parsedResponse = parse(data.response) const parsedResponse = parse(data.response);
const sendRequest = async () => { const sendRequest = async () => {
if (isFetching) return if (isFetching) return;
setIsFetching(true) setIsFetching(true);
const r = toast.loading('正在发送请求...') const r = toast.loading('正在发送请求...');
try { try {
const parsedRequestBody = JSON.parse(requestBody) const parsedRequestBody = JSON.parse(requestBody);
const requestURL = new URL(httpConfig.url) const requestURL = new URL(httpConfig.url);
requestURL.pathname = path requestURL.pathname = path;
request request
.post(requestURL.href, parsedRequestBody, { .post(requestURL.href, parsedRequestBody, {
headers: { headers: {
Authorization: `Bearer ${httpConfig.token}` Authorization: `Bearer ${httpConfig.token}`,
}, },
responseType: 'text' responseType: 'text',
}) })
.then((res) => { .then((res) => {
setResponseContent(parseAxiosResponse(res)) setResponseContent(parseAxiosResponse(res));
toast.success('请求发送完成,请查看响应') toast.success('请求发送完成,请查看响应');
}) })
.catch((err) => { .catch((err) => {
toast.error('请求发送失败:' + err.message) toast.error('请求发送失败:' + err.message);
setResponseContent(parseAxiosResponse(err.response)) setResponseContent(parseAxiosResponse(err.response));
}) })
.finally(() => { .finally(() => {
setIsFetching(false) setIsFetching(false);
setIsResponseOpen(true) setIsResponseOpen(true);
responseRef.current?.scrollIntoView({ responseRef.current?.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'start' block: 'start',
}) });
toast.dismiss(r) toast.dismiss(r);
}) });
} catch (error) { } catch (_error) {
toast.error('请求体 JSON 格式错误') toast.error('请求体 JSON 格式错误');
setIsFetching(false) setIsFetching(false);
toast.dismiss(r) toast.dismiss(r);
} }
} };
useEffect(() => { useEffect(() => {
setRequestBody(generateDefaultJson(data.request)) setRequestBody(generateDefaultJson(data.request));
setResponseContent('') setResponseContent('');
}, [path]) }, [path]);
return ( return (
<section className="p-4 pt-14 rounded-lg shadow-md"> <section className='p-4 pt-14 rounded-lg shadow-md'>
<h1 className="text-2xl font-bold mb-4 flex items-center gap-1 text-primary-400"> <h1 className='text-2xl font-bold mb-4 flex items-center gap-1 text-primary-400'>
<PiCatDuotone /> <PiCatDuotone />
{data.description} {data.description}
</h1> </h1>
<h1 className="text-lg font-bold mb-4"> <h1 className='text-lg font-bold mb-4'>
<Snippet <Snippet
className="bg-default-50 bg-opacity-50 backdrop-blur-md" className='bg-default-50 bg-opacity-50 backdrop-blur-md'
symbol={<IoLink size={18} className="inline-block mr-1" />} symbol={<IoLink size={18} className='inline-block mr-1' />}
tooltipProps={{ tooltipProps={{
content: '点击复制地址' content: '点击复制地址',
}} }}
> >
{path} {path}
</Snippet> </Snippet>
</h1> </h1>
<div className="flex gap-2 items-center"> <div className='flex gap-2 items-center'>
<Input <Input
label="HTTP URL" label='HTTP URL'
placeholder="输入 HTTP URL" placeholder='输入 HTTP URL'
value={httpConfig.url} value={httpConfig.url}
onChange={(e) => onChange={(e) =>
setHttpConfig({ ...httpConfig, url: e.target.value }) setHttpConfig({ ...httpConfig, url: e.target.value })}
}
/> />
<Input <Input
label="Token" label='Token'
placeholder="输入 Token" placeholder='输入 Token'
value={httpConfig.token} value={httpConfig.token}
onChange={(e) => onChange={(e) =>
setHttpConfig({ ...httpConfig, token: e.target.value }) setHttpConfig({ ...httpConfig, token: e.target.value })}
}
/> />
<Button <Button
onPress={sendRequest} onPress={sendRequest}
color="primary" color='primary'
size="lg" size='lg'
radius="full" radius='full'
isIconOnly isIconOnly
isDisabled={isFetching} isDisabled={isFetching}
> >
@@ -135,17 +133,17 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
</Button> </Button>
</div> </div>
<Card <Card
shadow="sm" shadow='sm'
className="my-4 bg-opacity-50 backdrop-blur-md overflow-visible" className='my-4 bg-opacity-50 backdrop-blur-md overflow-visible'
> >
<CardHeader className="font-bold text-lg gap-1 pb-0"> <CardHeader className='font-bold text-lg gap-1 pb-0'>
<span className="mr-2"></span> <span className='mr-2'></span>
<Button <Button
color="warning" color='warning'
variant="flat" variant='flat'
onPress={() => setIsCodeEditorOpen(!isCodeEditorOpen)} onPress={() => setIsCodeEditorOpen(!isCodeEditorOpen)}
size="sm" size='sm'
radius="full" radius='full'
> >
{isCodeEditorOpen ? '收起' : '展开'} {isCodeEditorOpen ? '收起' : '展开'}
</Button> </Button>
@@ -156,24 +154,23 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
initial={{ opacity: 0, height: 0 }} initial={{ opacity: 0, height: 0 }}
animate={{ animate={{
opacity: isCodeEditorOpen ? 1 : 0, opacity: isCodeEditorOpen ? 1 : 0,
height: isCodeEditorOpen ? 'auto' : 0 height: isCodeEditorOpen ? 'auto' : 0,
}} }}
> >
<CodeEditor <CodeEditor
value={requestBody} value={requestBody}
onChange={(value) => setRequestBody(value ?? '')} onChange={(value) => setRequestBody(value ?? '')}
language="json" language='json'
height="400px" height='400px'
/> />
<div className="flex justify-end gap-1"> <div className='flex justify-end gap-1'>
<ChatInputModal /> <ChatInputModal />
<Button <Button
color="primary" color='primary'
variant="flat" variant='flat'
onPress={() => onPress={() =>
setRequestBody(generateDefaultJson(data.request)) setRequestBody(generateDefaultJson(data.request))}
}
> >
</Button> </Button>
@@ -182,61 +179,61 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
</CardBody> </CardBody>
</Card> </Card>
<Card <Card
shadow="sm" shadow='sm'
className="my-4 relative bg-opacity-50 backdrop-blur-md" className='my-4 relative bg-opacity-50 backdrop-blur-md'
> >
<PageLoading loading={isFetching} /> <PageLoading loading={isFetching} />
<CardHeader className="font-bold text-lg gap-1 pb-0"> <CardHeader className='font-bold text-lg gap-1 pb-0'>
<span className="mr-2"></span> <span className='mr-2'></span>
<Button <Button
color="warning" color='warning'
variant="flat" variant='flat'
onPress={() => setIsResponseOpen(!isResponseOpen)} onPress={() => setIsResponseOpen(!isResponseOpen)}
size="sm" size='sm'
radius="full" radius='full'
> >
{isResponseOpen ? '收起' : '展开'} {isResponseOpen ? '收起' : '展开'}
</Button> </Button>
<Button <Button
color="success" color='success'
variant="flat" variant='flat'
onPress={() => { onPress={() => {
navigator.clipboard.writeText(responseContent) navigator.clipboard.writeText(responseContent);
toast.success('响应内容已复制到剪贴板') toast.success('响应内容已复制到剪贴板');
}} }}
size="sm" size='sm'
radius="full" radius='full'
> >
</Button> </Button>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<motion.div <motion.div
className="overflow-y-auto text-sm" className='overflow-y-auto text-sm'
initial={{ opacity: 0, height: 0 }} initial={{ opacity: 0, height: 0 }}
animate={{ animate={{
opacity: isResponseOpen ? 1 : 0, opacity: isResponseOpen ? 1 : 0,
height: isResponseOpen ? 300 : 0 height: isResponseOpen ? 300 : 0,
}} }}
> >
<pre> <pre>
<code> <code>
{responseContent || ( {responseContent || (
<div className="text-gray-400"></div> <div className='text-gray-400'></div>
)} )}
</code> </code>
</pre> </pre>
</motion.div> </motion.div>
</CardBody> </CardBody>
</Card> </Card>
<div className="p-2 md:p-4 border border-default-50 dark:border-default-200 rounded-lg backdrop-blur-sm"> <div className='p-2 md:p-4 border border-default-50 dark:border-default-200 rounded-lg backdrop-blur-sm'>
<h2 className="text-xl font-semibold mb-2"></h2> <h2 className='text-xl font-semibold mb-2'></h2>
<DisplayStruct schema={parsedRequest} /> <DisplayStruct schema={parsedRequest} />
<h2 className="text-xl font-semibold mt-4 mb-2"></h2> <h2 className='text-xl font-semibold mt-4 mb-2'></h2>
<DisplayStruct schema={parsedResponse} /> <DisplayStruct schema={parsedResponse} />
</div> </div>
</section> </section>
) );
} };
export default OneBotApiDebug export default OneBotApiDebug;

View File

@@ -1,11 +1,11 @@
import { Chip } from '@heroui/chip' import { Chip } from '@heroui/chip';
import { Tooltip } from '@heroui/tooltip' import { Tooltip } from '@heroui/tooltip';
import { motion } from 'motion/react' import { motion } from 'motion/react';
import React, { useState } from 'react' import React, { useState } from 'react';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { TbSquareRoundedChevronRightFilled } from 'react-icons/tb' import { TbSquareRoundedChevronRightFilled } from 'react-icons/tb';
import type { LiteralValue, ParsedSchema } from '@/utils/zod' import type { LiteralValue, ParsedSchema } from '@/utils/zod';
interface DisplayStructProps { interface DisplayStructProps {
schema: ParsedSchema | ParsedSchema[] schema: ParsedSchema | ParsedSchema[]
@@ -13,84 +13,86 @@ interface DisplayStructProps {
const SchemaType = ({ const SchemaType = ({
type, type,
value value,
}: { }: {
type: string type: string
value?: LiteralValue value?: LiteralValue
}) => { }) => {
let name = type let name = type;
switch (type) { switch (type) {
case 'union': case 'union':
name = '联合类型' name = '联合类型';
break break;
case 'value': case 'value':
name = '固定值' name = '固定值';
break break;
} }
let chipColor: 'primary' | 'success' | 'primary' | 'warning' | 'secondary' = let chipColor: 'primary' | 'success' | 'primary' | 'warning' | 'secondary' =
'primary' 'primary';
switch (type) { switch (type) {
case 'enum': case 'enum':
chipColor = 'warning' chipColor = 'warning';
break break;
case 'union': case 'union':
chipColor = 'secondary' chipColor = 'secondary';
break break;
case 'array': case 'array':
chipColor = 'primary' chipColor = 'primary';
break break;
case 'object': case 'object':
chipColor = 'success' chipColor = 'success';
break break;
} }
return ( return (
<Chip size="sm" color={chipColor} variant="flat"> <Chip size='sm' color={chipColor} variant='flat'>
{name} {name}
{type === 'value' && ( {type === 'value' && (
<span className="px-1 rounded-full bg-primary-400 text-white ml-1"> <span className='px-1 rounded-full bg-primary-400 text-white ml-1'>
{value} {value}
</span> </span>
)} )}
</Chip> </Chip>
) );
} };
const SchemaLabel: React.FC<{ const SchemaLabel: React.FC<{
schema: ParsedSchema schema: ParsedSchema
}> = ({ schema }) => ( }> = ({ schema }) => (
<> <>
{Array.isArray(schema.type) ? ( {Array.isArray(schema.type)
schema.type.map((type) => ( ? (
<SchemaType key={type} type={type} value={schema?.value} /> schema.type.map((type) => (
)) <SchemaType key={type} type={type} value={schema?.value} />
) : ( ))
<SchemaType type={schema.type} value={schema?.value} /> )
)} : (
<SchemaType type={schema.type} value={schema?.value} />
)}
{schema.optional && ( {schema.optional && (
<Chip size="sm" color="default" variant="flat"> <Chip size='sm' color='default' variant='flat'>
</Chip> </Chip>
)} )}
{schema.description && ( {schema.description && (
<span className="text-xs text-default-400">{schema.description}</span> <span className='text-xs text-default-400'>{schema.description}</span>
)} )}
</> </>
) );
const SchemaContainer: React.FC<{ const SchemaContainer: React.FC<{
schema: ParsedSchema schema: ParsedSchema
children: React.ReactNode children: React.ReactNode
}> = ({ schema, children }) => { }> = ({ schema, children }) => {
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false);
const toggleExpand = () => setExpanded(!expanded) const toggleExpand = () => setExpanded(!expanded);
return ( return (
<div className="mb-2"> <div className='mb-2'>
<div <div
onClick={toggleExpand} onClick={toggleExpand}
className="md:cursor-pointer flex items-center gap-1" className='md:cursor-pointer flex items-center gap-1'
> >
<motion.div <motion.div
initial={{ rotate: 0 }} initial={{ rotate: 0 }}
@@ -98,13 +100,13 @@ const SchemaContainer: React.FC<{
> >
<TbSquareRoundedChevronRightFilled /> <TbSquareRoundedChevronRightFilled />
</motion.div> </motion.div>
<Tooltip content="点击复制" placement="top" showArrow> <Tooltip content='点击复制' placement='top' showArrow>
<span <span
className="border-b border-transparent border-dashed hover:border-primary-400" className='border-b border-transparent border-dashed hover:border-primary-400'
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
navigator.clipboard.writeText(schema.name || '') navigator.clipboard.writeText(schema.name || '');
toast.success('已复制') toast.success('已复制');
}} }}
> >
{schema.name} {schema.name}
@@ -113,30 +115,32 @@ const SchemaContainer: React.FC<{
<SchemaLabel schema={schema} /> <SchemaLabel schema={schema} />
</div> </div>
<motion.div <motion.div
className="ml-5 overflow-hidden" className='ml-5 overflow-hidden'
initial={{ opacity: 0, height: 0 }} initial={{ opacity: 0, height: 0 }}
animate={{ opacity: expanded ? 1 : 0, height: expanded ? 'auto' : 0 }} animate={{ opacity: expanded ? 1 : 0, height: expanded ? 'auto' : 0 }}
> >
<div className="h-2"></div> <div className='h-2' />
{children} {children}
</motion.div> </motion.div>
</div> </div>
) );
} };
const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => { const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => {
if (schema.type === 'object') { if (schema.type === 'object') {
return ( return (
<SchemaContainer schema={schema}> <SchemaContainer schema={schema}>
{schema.children && schema.children.length > 0 ? ( {schema.children && schema.children.length > 0
schema.children.map((child, i) => ( ? (
<RenderSchema key={child.name || i} schema={child} /> schema.children.map((child, i) => (
)) <RenderSchema key={child.name || i} schema={child} />
) : ( ))
<div>{`{}`}</div> )
)} : (
<div>{'{}'}</div>
)}
</SchemaContainer> </SchemaContainer>
) );
} }
if (schema.type === 'array' || schema.type === 'union') { if (schema.type === 'array' || schema.type === 'union') {
@@ -146,37 +150,37 @@ const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => {
<RenderSchema key={child.name || i} schema={child} /> <RenderSchema key={child.name || i} schema={child} />
))} ))}
</SchemaContainer> </SchemaContainer>
) );
} }
if (schema.type === 'enum' && Array.isArray(schema.enum)) { if (schema.type === 'enum' && Array.isArray(schema.enum)) {
return ( return (
<SchemaContainer schema={schema}> <SchemaContainer schema={schema}>
<div className="flex gap-1 items-center"> <div className='flex gap-1 items-center'>
{schema.enum?.map((value, i) => ( {schema.enum?.map((value, i) => (
<Chip <Chip
key={value?.toString() || i} key={value?.toString() || i}
size="sm" size='sm'
variant="flat" variant='flat'
color="success" color='success'
> >
{value?.toString()} {value?.toString()}
</Chip> </Chip>
))} ))}
</div> </div>
</SchemaContainer> </SchemaContainer>
) );
} }
return ( return (
<div className="mb-2 flex items-center gap-1 pl-5"> <div className='mb-2 flex items-center gap-1 pl-5'>
<Tooltip content="点击复制" placement="top" showArrow> <Tooltip content='点击复制' placement='top' showArrow>
<span <span
className="border-b border-transparent border-dashed hover:border-primary-400 md:cursor-pointer" className='border-b border-transparent border-dashed hover:border-primary-400 md:cursor-pointer'
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
navigator.clipboard.writeText(schema.name || '') navigator.clipboard.writeText(schema.name || '');
toast.success('已复制') toast.success('已复制');
}} }}
> >
{schema.name} {schema.name}
@@ -184,19 +188,21 @@ const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => {
</Tooltip> </Tooltip>
<SchemaLabel schema={schema} /> <SchemaLabel schema={schema} />
</div> </div>
) );
} };
const DisplayStruct: React.FC<DisplayStructProps> = ({ schema }) => { const DisplayStruct: React.FC<DisplayStructProps> = ({ schema }) => {
return ( return (
<div className="p-4 bg-content2 rounded-lg bg-opacity-50"> <div className='p-4 bg-content2 rounded-lg bg-opacity-50'>
{Array.isArray(schema) ? ( {Array.isArray(schema)
schema.map((s, i) => <RenderSchema key={s.name || i} schema={s} />) ? (
) : ( schema.map((s, i) => <RenderSchema key={s.name || i} schema={s} />)
<RenderSchema schema={schema} /> )
)} : (
<RenderSchema schema={schema} />
)}
</div> </div>
) );
} };
export default DisplayStruct export default DisplayStruct;

View File

@@ -1,10 +1,10 @@
import { Card, CardBody } from '@heroui/card' import { Card, CardBody } from '@heroui/card';
import { Input } from '@heroui/input' import { Input } from '@heroui/input';
import clsx from 'clsx' import clsx from 'clsx';
import { motion } from 'motion/react' import { motion } from 'motion/react';
import { useState } from 'react' import { useState } from 'react';
import type { OneBotHttpApi, OneBotHttpApiPath } from '@/const/ob_api' import type { OneBotHttpApi, OneBotHttpApiPath } from '@/const/ob_api';
export interface OneBotApiNavListProps { export interface OneBotApiNavListProps {
data: OneBotHttpApi data: OneBotHttpApi
@@ -14,8 +14,8 @@ export interface OneBotApiNavListProps {
} }
const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => { const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
const { data, selectedApi, onSelect, openSideBar } = props const { data, selectedApi, onSelect, openSideBar } = props;
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('');
return ( return (
<motion.div <motion.div
className={clsx( className={clsx(
@@ -26,21 +26,21 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
transition={{ transition={{
type: openSideBar ? 'spring' : 'tween', type: openSideBar ? 'spring' : 'tween',
stiffness: 150, stiffness: 150,
damping: 15 damping: 15,
}} }}
animate={{ width: openSideBar ? '16rem' : '0rem' }} animate={{ width: openSideBar ? '16rem' : '0rem' }}
style={{ overflowY: openSideBar ? 'auto' : 'hidden' }} style={{ overflowY: openSideBar ? 'auto' : 'hidden' }}
> >
<div className="w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0"> <div className='w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0'>
<Input <Input
className="sticky top-0 z-10 text-primary-600" className='sticky top-0 z-10 text-primary-600'
classNames={{ classNames={{
inputWrapper: inputWrapper:
'bg-opacity-30 bg-primary-50 backdrop-blur-sm border border-primary-300 mb-2', 'bg-opacity-30 bg-primary-50 backdrop-blur-sm border border-primary-300 mb-2',
input: 'bg-transparent !text-primary-400 !placeholder-primary-400' input: 'bg-transparent !text-primary-400 !placeholder-primary-400',
}} }}
radius="full" radius='full'
placeholder="搜索 API" placeholder='搜索 API'
value={searchValue} value={searchValue}
onChange={(e) => setSearchValue(e.target.value)} onChange={(e) => setSearchValue(e.target.value)}
isClearable isClearable
@@ -49,28 +49,28 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
{Object.entries(data).map(([apiName, api]) => ( {Object.entries(data).map(([apiName, api]) => (
<Card <Card
key={apiName} key={apiName}
shadow="none" shadow='none'
className={clsx( className={clsx(
'w-full border border-primary-100 rounded-lg mb-1 bg-opacity-30 backdrop-blur-sm text-primary-400', 'w-full border border-primary-100 rounded-lg mb-1 bg-opacity-30 backdrop-blur-sm text-primary-400',
{ {
hidden: !( hidden: !(
apiName.includes(searchValue) || apiName.includes(searchValue) ||
api.description?.includes(searchValue) api.description?.includes(searchValue)
) ),
}, },
{ {
'!bg-opacity-40 border border-primary-400 bg-primary-50 text-primary-600': '!bg-opacity-40 border border-primary-400 bg-primary-50 text-primary-600':
apiName === selectedApi apiName === selectedApi,
} }
)} )}
isPressable isPressable
onPress={() => onSelect(apiName as OneBotHttpApiPath)} onPress={() => onSelect(apiName as OneBotHttpApiPath)}
> >
<CardBody> <CardBody>
<h2 className="font-bold">{api.description}</h2> <h2 className='font-bold'>{api.description}</h2>
<div <div
className={clsx('text-sm text-primary-200', { className={clsx('text-sm text-primary-200', {
'!text-primary-400': apiName === selectedApi '!text-primary-400': apiName === selectedApi,
})} })}
> >
{apiName} {apiName}
@@ -80,7 +80,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
))} ))}
</div> </div>
</motion.div> </motion.div>
) );
} };
export default OneBotApiNavList export default OneBotApiNavList;

View File

@@ -1,16 +1,16 @@
import { Avatar } from '@heroui/avatar' import { Avatar } from '@heroui/avatar';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import clsx from 'clsx' import clsx from 'clsx';
import { isOB11GroupMessage } from '@/utils/onebot' import { isOB11GroupMessage } from '@/utils/onebot';
import type { import type {
OB11GroupMessage, OB11GroupMessage,
OB11Message, OB11Message,
OB11PrivateMessage OB11PrivateMessage,
} from '@/types/onebot' } from '@/types/onebot';
import { renderMessageContent } from '../render_message' import { renderMessageContent } from '../render_message';
export interface OneBotMessageProps { export interface OneBotMessageProps {
data: OB11Message data: OB11Message
@@ -26,11 +26,11 @@ export interface OneBotMessagePrivateProps {
const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => { const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
return ( return (
<div className="h-full flex flex-col overflow-hidden flex-1"> <div className='h-full flex flex-col overflow-hidden flex-1'>
<div className="flex gap-2 items-center flex-shrink-0"> <div className='flex gap-2 items-center flex-shrink-0'>
<div className="font-bold"> <div className='font-bold'>
{isOB11GroupMessage(data) && data.sender.card && ( {isOB11GroupMessage(data) && data.sender.card && (
<span className="mr-1">{data.sender.card}</span> <span className='mr-1'>{data.sender.card}</span>
)} )}
<span <span
className={clsx( className={clsx(
@@ -43,12 +43,12 @@ const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
</span> </span>
</div> </div>
<div>({data.sender.user_id})</div> <div>({data.sender.user_id})</div>
<div className="text-sm">ID: {data.message_id}</div> <div className='text-sm'>ID: {data.message_id}</div>
</div> </div>
<Popover showArrow triggerScaleOnOpen={false}> <Popover showArrow triggerScaleOnOpen={false}>
<PopoverTrigger> <PopoverTrigger>
<div className="flex-1 break-all overflow-hidden whitespace-pre-wrap border border-default-100 p-2 rounded-md hover:bg-content2 md:cursor-pointer transition-background relative group"> <div className='flex-1 break-all overflow-hidden whitespace-pre-wrap border border-default-100 p-2 rounded-md hover:bg-content2 md:cursor-pointer transition-background relative group'>
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 text-default-300"> <div className='absolute right-2 top-2 opacity-0 group-hover:opacity-100 text-default-300'>
</div> </div>
{Array.isArray(data.message) {Array.isArray(data.message)
@@ -57,7 +57,7 @@ const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
</div> </div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent> <PopoverContent>
<div className="p-2"> <div className='p-2'>
{Array.isArray(data.message) {Array.isArray(data.message)
? renderMessageContent(data.message) ? renderMessageContent(data.message)
: data.raw_message} : data.raw_message}
@@ -65,58 +65,58 @@ const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
) );
} };
const OneBotMessageGroup: React.FC<OneBotMessageGroupProps> = ({ data }) => { const OneBotMessageGroup: React.FC<OneBotMessageGroupProps> = ({ data }) => {
return ( return (
<div className="h-full overflow-hidden flex flex-col w-full"> <div className='h-full overflow-hidden flex flex-col w-full'>
<div className="flex items-center p-1 flex-shrink-0"> <div className='flex items-center p-1 flex-shrink-0'>
<Avatar <Avatar
src={`https://p.qlogo.cn/gh/${data.group_id}/${data.group_id}/640/`} src={`https://p.qlogo.cn/gh/${data.group_id}/${data.group_id}/640/`}
alt="群头像" alt='群头像'
size="sm" size='sm'
className="flex-shrink-0 mr-2" className='flex-shrink-0 mr-2'
/> />
<div> {data.group_id}</div> <div> {data.group_id}</div>
</div> </div>
<div className="flex items-start p-1 rounded-md h-full flex-1 border border-default-100"> <div className='flex items-start p-1 rounded-md h-full flex-1 border border-default-100'>
<Avatar <Avatar
src={`https://q1.qlogo.cn/g?b=qq&nk=${data.sender.user_id}&s=100`} src={`https://q1.qlogo.cn/g?b=qq&nk=${data.sender.user_id}&s=100`}
alt="用户头像" alt='用户头像'
size="md" size='md'
className="flex-shrink-0 mr-2" className='flex-shrink-0 mr-2'
/> />
<MessageContent data={data} /> <MessageContent data={data} />
</div> </div>
</div> </div>
) );
} };
const OneBotMessagePrivate: React.FC<OneBotMessagePrivateProps> = ({ const OneBotMessagePrivate: React.FC<OneBotMessagePrivateProps> = ({
data data,
}) => { }) => {
return ( return (
<div className="flex items-start p-2 rounded-md h-full flex-1"> <div className='flex items-start p-2 rounded-md h-full flex-1'>
<Avatar <Avatar
src={`https://q1.qlogo.cn/g?b=qq&nk=${data.sender.user_id}&s=100`} src={`https://q1.qlogo.cn/g?b=qq&nk=${data.sender.user_id}&s=100`}
alt="用户头像" alt='用户头像'
size="md" size='md'
className="flex-shrink-0 mr-2" className='flex-shrink-0 mr-2'
/> />
<MessageContent data={data} /> <MessageContent data={data} />
</div> </div>
) );
} };
const OneBotMessage: React.FC<OneBotMessageProps> = ({ data }) => { const OneBotMessage: React.FC<OneBotMessageProps> = ({ data }) => {
if (data.message_type === 'group') { if (data.message_type === 'group') {
return <OneBotMessageGroup data={data} /> return <OneBotMessageGroup data={data} />;
} else if (data.message_type === 'private') { } else if (data.message_type === 'private') {
return <OneBotMessagePrivate data={data} /> return <OneBotMessagePrivate data={data} />;
} else { } else {
return <div></div> return <div></div>;
} }
} };
export default OneBotMessage export default OneBotMessage;

View File

@@ -1,12 +1,12 @@
import { Chip } from '@heroui/chip' import { Chip } from '@heroui/chip';
import { getLifecycleColor, getLifecycleName } from '@/utils/onebot' import { getLifecycleColor, getLifecycleName } from '@/utils/onebot';
import type { import type {
OB11Meta, OB11Meta,
OneBot11Heartbeat, OneBot11Heartbeat,
OneBot11Lifecycle OneBot11Lifecycle,
} from '@/types/onebot' } from '@/types/onebot';
export interface OneBotDisplayMetaProps { export interface OneBotDisplayMetaProps {
data: OB11Meta data: OB11Meta
@@ -21,32 +21,32 @@ export interface OneBotDisplayMetaLifecycleProps {
} }
const OneBotDisplayMetaHeartbeat: React.FC<OneBotDisplayMetaHeartbeatProps> = ({ const OneBotDisplayMetaHeartbeat: React.FC<OneBotDisplayMetaHeartbeatProps> = ({
data data,
}) => { }) => {
return ( return (
<div className="flex gap-2"> <div className='flex gap-2'>
<Chip></Chip> <Chip></Chip>
<Chip> {data.status.interval}ms</Chip> <Chip> {data.status.interval}ms</Chip>
</div> </div>
) );
} };
const OneBotDisplayMetaLifecycle: React.FC<OneBotDisplayMetaLifecycleProps> = ({ const OneBotDisplayMetaLifecycle: React.FC<OneBotDisplayMetaLifecycleProps> = ({
data data,
}) => { }) => {
return ( return (
<div className="flex gap-2"> <div className='flex gap-2'>
<Chip></Chip> <Chip></Chip>
<Chip color={getLifecycleColor(data.sub_type)}> <Chip color={getLifecycleColor(data.sub_type)}>
{getLifecycleName(data.sub_type)} {getLifecycleName(data.sub_type)}
</Chip> </Chip>
</div> </div>
) );
} };
const OneBotDisplayMeta: React.FC<OneBotDisplayMetaProps> = ({ data }) => { const OneBotDisplayMeta: React.FC<OneBotDisplayMetaProps> = ({ data }) => {
return ( return (
<div className="h-full flex items-center"> <div className='h-full flex items-center'>
{data.meta_event_type === 'lifecycle' && ( {data.meta_event_type === 'lifecycle' && (
<OneBotDisplayMetaLifecycle data={data} /> <OneBotDisplayMetaLifecycle data={data} />
)} )}
@@ -54,7 +54,7 @@ const OneBotDisplayMeta: React.FC<OneBotDisplayMetaProps> = ({ data }) => {
<OneBotDisplayMetaHeartbeat data={data} /> <OneBotDisplayMetaHeartbeat data={data} />
)} )}
</div> </div>
) );
} };
export default OneBotDisplayMeta export default OneBotDisplayMeta;

View File

@@ -1,6 +1,6 @@
import { Chip } from '@heroui/chip' import { Chip } from '@heroui/chip';
import { getNoticeTypeName } from '@/utils/onebot' import { getNoticeTypeName } from '@/utils/onebot';
import { import {
OB11Notice, OB11Notice,
@@ -18,8 +18,8 @@ import {
OneBot11GroupUpload, OneBot11GroupUpload,
OneBot11Honor, OneBot11Honor,
OneBot11LuckyKing, OneBot11LuckyKing,
OneBot11Poke OneBot11Poke,
} from '@/types/onebot' } from '@/types/onebot';
export interface OneBotNoticeProps { export interface OneBotNoticeProps {
data: OB11Notice data: OB11Notice
@@ -30,9 +30,9 @@ export interface NoticeProps<T> {
} }
const GroupUploadNotice: React.FC<NoticeProps<OneBot11GroupUpload>> = ({ const GroupUploadNotice: React.FC<NoticeProps<OneBot11GroupUpload>> = ({
data data,
}) => { }) => {
const { group_id, user_id, file } = data const { group_id, user_id, file } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
@@ -40,26 +40,26 @@ const GroupUploadNotice: React.FC<NoticeProps<OneBot11GroupUpload>> = ({
<div>: {file.name}</div> <div>: {file.name}</div>
<div>: {file.size} </div> <div>: {file.size} </div>
</> </>
) );
} };
const GroupAdminNotice: React.FC<NoticeProps<OneBot11GroupAdmin>> = ({ const GroupAdminNotice: React.FC<NoticeProps<OneBot11GroupAdmin>> = ({
data data,
}) => { }) => {
const { group_id, user_id, sub_type } = data const { group_id, user_id, sub_type } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
<div>ID: {user_id}</div> <div>ID: {user_id}</div>
<div>: {sub_type === 'set' ? '设置管理员' : '取消管理员'}</div> <div>: {sub_type === 'set' ? '设置管理员' : '取消管理员'}</div>
</> </>
) );
} };
const GroupDecreaseNotice: React.FC<NoticeProps<OneBot11GroupDecrease>> = ({ const GroupDecreaseNotice: React.FC<NoticeProps<OneBot11GroupDecrease>> = ({
data data,
}) => { }) => {
const { group_id, operator_id, user_id, sub_type } = data const { group_id, operator_id, user_id, sub_type } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
@@ -67,13 +67,13 @@ const GroupDecreaseNotice: React.FC<NoticeProps<OneBot11GroupDecrease>> = ({
<div>ID: {user_id}</div> <div>ID: {user_id}</div>
<div>: {sub_type}</div> <div>: {sub_type}</div>
</> </>
) );
} };
const GroupIncreaseNotice: React.FC<NoticeProps<OneBot11GroupIncrease>> = ({ const GroupIncreaseNotice: React.FC<NoticeProps<OneBot11GroupIncrease>> = ({
data data,
}) => { }) => {
const { group_id, operator_id, user_id, sub_type } = data const { group_id, operator_id, user_id, sub_type } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
@@ -81,11 +81,11 @@ const GroupIncreaseNotice: React.FC<NoticeProps<OneBot11GroupIncrease>> = ({
<div>ID: {user_id}</div> <div>ID: {user_id}</div>
<div>: {sub_type}</div> <div>: {sub_type}</div>
</> </>
) );
} };
const GroupBanNotice: React.FC<NoticeProps<OneBot11GroupBan>> = ({ data }) => { const GroupBanNotice: React.FC<NoticeProps<OneBot11GroupBan>> = ({ data }) => {
const { group_id, operator_id, user_id, sub_type, duration } = data const { group_id, operator_id, user_id, sub_type, duration } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
@@ -94,24 +94,24 @@ const GroupBanNotice: React.FC<NoticeProps<OneBot11GroupBan>> = ({ data }) => {
<div>: {sub_type}</div> <div>: {sub_type}</div>
<div>: {duration} </div> <div>: {duration} </div>
</> </>
) );
} };
const FriendAddNotice: React.FC<NoticeProps<OneBot11FriendAdd>> = ({ const FriendAddNotice: React.FC<NoticeProps<OneBot11FriendAdd>> = ({
data data,
}) => { }) => {
const { user_id } = data const { user_id } = data;
return ( return (
<> <>
<div>ID: {user_id}</div> <div>ID: {user_id}</div>
</> </>
) );
} };
const GroupRecallNotice: React.FC<NoticeProps<OneBot11GroupRecall>> = ({ const GroupRecallNotice: React.FC<NoticeProps<OneBot11GroupRecall>> = ({
data data,
}) => { }) => {
const { group_id, user_id, operator_id, message_id } = data const { group_id, user_id, operator_id, message_id } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
@@ -119,60 +119,60 @@ const GroupRecallNotice: React.FC<NoticeProps<OneBot11GroupRecall>> = ({
<div>ID: {operator_id}</div> <div>ID: {operator_id}</div>
<div>ID: {message_id}</div> <div>ID: {message_id}</div>
</> </>
) );
} };
const FriendRecallNotice: React.FC<NoticeProps<OneBot11FriendRecall>> = ({ const FriendRecallNotice: React.FC<NoticeProps<OneBot11FriendRecall>> = ({
data data,
}) => { }) => {
const { user_id, message_id } = data const { user_id, message_id } = data;
return ( return (
<> <>
<div>ID: {user_id}</div> <div>ID: {user_id}</div>
<div>ID: {message_id}</div> <div>ID: {message_id}</div>
</> </>
) );
} };
const PokeNotice: React.FC<NoticeProps<OneBot11Poke>> = ({ data }) => { const PokeNotice: React.FC<NoticeProps<OneBot11Poke>> = ({ data }) => {
const { group_id, user_id, target_id } = data const { group_id, user_id, target_id } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
<div>ID: {user_id}</div> <div>ID: {user_id}</div>
<div>ID: {target_id}</div> <div>ID: {target_id}</div>
</> </>
) );
} };
const LuckyKingNotice: React.FC<NoticeProps<OneBot11LuckyKing>> = ({ const LuckyKingNotice: React.FC<NoticeProps<OneBot11LuckyKing>> = ({
data data,
}) => { }) => {
const { group_id, user_id, target_id } = data const { group_id, user_id, target_id } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
<div>ID: {user_id}</div> <div>ID: {user_id}</div>
<div>ID: {target_id}</div> <div>ID: {target_id}</div>
</> </>
) );
} };
const HonorNotice: React.FC<NoticeProps<OneBot11Honor>> = ({ data }) => { const HonorNotice: React.FC<NoticeProps<OneBot11Honor>> = ({ data }) => {
const { group_id, user_id, honor_type } = data const { group_id, user_id, honor_type } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
<div>ID: {user_id}</div> <div>ID: {user_id}</div>
<div>: {honor_type}</div> <div>: {honor_type}</div>
</> </>
) );
} };
const GroupMessageReactionNotice: React.FC< const GroupMessageReactionNotice: React.FC<
NoticeProps<OneBot11GroupMessageReaction> NoticeProps<OneBot11GroupMessageReaction>
> = ({ data }) => { > = ({ data }) => {
const { group_id, user_id, message_id, likes } = data const { group_id, user_id, message_id, likes } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
@@ -185,13 +185,13 @@ const GroupMessageReactionNotice: React.FC<
.join(', ')} .join(', ')}
</div> </div>
</> </>
) );
} };
const GroupEssenceNotice: React.FC<NoticeProps<OneBot11GroupEssence>> = ({ const GroupEssenceNotice: React.FC<NoticeProps<OneBot11GroupEssence>> = ({
data data,
}) => { }) => {
const { group_id, message_id, sender_id, operator_id, sub_type } = data const { group_id, message_id, sender_id, operator_id, sub_type } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
@@ -200,13 +200,13 @@ const GroupEssenceNotice: React.FC<NoticeProps<OneBot11GroupEssence>> = ({
<div>ID: {operator_id}</div> <div>ID: {operator_id}</div>
<div>: {sub_type}</div> <div>: {sub_type}</div>
</> </>
) );
} };
const GroupCardNotice: React.FC<NoticeProps<OneBot11GroupCard>> = ({ const GroupCardNotice: React.FC<NoticeProps<OneBot11GroupCard>> = ({
data data,
}) => { }) => {
const { group_id, user_id, card_new, card_old } = data const { group_id, user_id, card_new, card_old } = data;
return ( return (
<> <>
<div>: {group_id}</div> <div>: {group_id}</div>
@@ -214,79 +214,79 @@ const GroupCardNotice: React.FC<NoticeProps<OneBot11GroupCard>> = ({
<div>: {card_new}</div> <div>: {card_new}</div>
<div>: {card_old}</div> <div>: {card_old}</div>
</> </>
) );
} };
const OneBotNotice: React.FC<OneBotNoticeProps> = ({ data }) => { const OneBotNotice: React.FC<OneBotNoticeProps> = ({ data }) => {
let NoticeComponent: React.ReactNode let NoticeComponent: React.ReactNode;
switch (data.notice_type) { switch (data.notice_type) {
case OB11NoticeType.GroupUpload: case OB11NoticeType.GroupUpload:
NoticeComponent = <GroupUploadNotice data={data} /> NoticeComponent = <GroupUploadNotice data={data} />;
break break;
case OB11NoticeType.GroupAdmin: case OB11NoticeType.GroupAdmin:
NoticeComponent = <GroupAdminNotice data={data} /> NoticeComponent = <GroupAdminNotice data={data} />;
break break;
case OB11NoticeType.GroupDecrease: case OB11NoticeType.GroupDecrease:
NoticeComponent = <GroupDecreaseNotice data={data} /> NoticeComponent = <GroupDecreaseNotice data={data} />;
break break;
case OB11NoticeType.GroupIncrease: case OB11NoticeType.GroupIncrease:
NoticeComponent = ( NoticeComponent = (
<GroupIncreaseNotice data={data as OneBot11GroupIncrease} /> <GroupIncreaseNotice data={data as OneBot11GroupIncrease} />
) );
break break;
case OB11NoticeType.GroupBan: case OB11NoticeType.GroupBan:
NoticeComponent = <GroupBanNotice data={data} /> NoticeComponent = <GroupBanNotice data={data} />;
break break;
case OB11NoticeType.FriendAdd: case OB11NoticeType.FriendAdd:
NoticeComponent = <FriendAddNotice data={data as OneBot11FriendAdd} /> NoticeComponent = <FriendAddNotice data={data as OneBot11FriendAdd} />;
break break;
case OB11NoticeType.GroupRecall: case OB11NoticeType.GroupRecall:
NoticeComponent = <GroupRecallNotice data={data as OneBot11GroupRecall} /> NoticeComponent = <GroupRecallNotice data={data as OneBot11GroupRecall} />;
break break;
case OB11NoticeType.FriendRecall: case OB11NoticeType.FriendRecall:
NoticeComponent = ( NoticeComponent = (
<FriendRecallNotice data={data as OneBot11FriendRecall} /> <FriendRecallNotice data={data as OneBot11FriendRecall} />
) );
break break;
case OB11NoticeType.Notify: case OB11NoticeType.Notify:
switch (data.sub_type) { switch (data.sub_type) {
case 'poke': case 'poke':
NoticeComponent = <PokeNotice data={data as OneBot11Poke} /> NoticeComponent = <PokeNotice data={data as OneBot11Poke} />;
break break;
case 'lucky_king': case 'lucky_king':
NoticeComponent = <LuckyKingNotice data={data as OneBot11LuckyKing} /> NoticeComponent = <LuckyKingNotice data={data as OneBot11LuckyKing} />;
break break;
case 'honor': case 'honor':
NoticeComponent = <HonorNotice data={data as OneBot11Honor} /> NoticeComponent = <HonorNotice data={data as OneBot11Honor} />;
break break;
} }
break break;
case OB11NoticeType.GroupMsgEmojiLike: case OB11NoticeType.GroupMsgEmojiLike:
NoticeComponent = ( NoticeComponent = (
<GroupMessageReactionNotice <GroupMessageReactionNotice
data={data as OneBot11GroupMessageReaction} data={data as OneBot11GroupMessageReaction}
/> />
) );
break break;
case OB11NoticeType.GroupEssence: case OB11NoticeType.GroupEssence:
NoticeComponent = ( NoticeComponent = (
<GroupEssenceNotice data={data as OneBot11GroupEssence} /> <GroupEssenceNotice data={data as OneBot11GroupEssence} />
) );
break break;
case OB11NoticeType.GroupCard: case OB11NoticeType.GroupCard:
NoticeComponent = <GroupCardNotice data={data as OneBot11GroupCard} /> NoticeComponent = <GroupCardNotice data={data as OneBot11GroupCard} />;
break break;
} }
return ( return (
<div className="flex gap-2 items-center"> <div className='flex gap-2 items-center'>
<Chip color="warning" variant="flat"> <Chip color='warning' variant='flat'>
</Chip> </Chip>
<Chip>{getNoticeTypeName(data.notice_type)}</Chip> <Chip>{getNoticeTypeName(data.notice_type)}</Chip>
{NoticeComponent} {NoticeComponent}
</div> </div>
) );
} };
export default OneBotNotice export default OneBotNotice;

View File

@@ -1,24 +1,24 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Card, CardBody, CardHeader } from '@heroui/card' import { Card, CardBody, CardHeader } from '@heroui/card';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import { Snippet } from '@heroui/snippet' import { Snippet } from '@heroui/snippet';
import { motion } from 'motion/react' import { motion } from 'motion/react';
import { IoCode } from 'react-icons/io5' import { IoCode } from 'react-icons/io5';
import OneBotDisplayMeta from '@/components/onebot/display_card/meta' import OneBotDisplayMeta from '@/components/onebot/display_card/meta';
import { getEventName, isOB11Event } from '@/utils/onebot' import { getEventName, isOB11Event } from '@/utils/onebot';
import { timestampToDateString } from '@/utils/time' import { timestampToDateString } from '@/utils/time';
import type { import type {
AllOB11WsResponse, AllOB11WsResponse,
OB11AllEvent, OB11AllEvent,
OB11Request OB11Request,
} from '@/types/onebot' } from '@/types/onebot';
import OneBotMessage from './message' import OneBotMessage from './message';
import OneBotNotice from './notice' import OneBotNotice from './notice';
import OneBotDisplayResponse from './response' import OneBotDisplayResponse from './response';
const itemVariants = { const itemVariants = {
hidden: { opacity: 0, scale: 0.8, y: 50 }, hidden: { opacity: 0, scale: 0.8, y: 50 },
@@ -26,12 +26,12 @@ const itemVariants = {
opacity: 1, opacity: 1,
scale: 1, scale: 1,
y: 0, y: 0,
transition: { type: 'spring', stiffness: 300, damping: 20 } transition: { type: 'spring' as const, stiffness: 300, damping: 20 },
} },
} };
function RequestComponent({ data: _ }: { data: OB11Request }) { function RequestComponent ({ data: _ }: { data: OB11Request }) {
return <div>Request消息</div> return <div>Request消息</div>;
} }
export interface OneBotItemRenderProps { export interface OneBotItemRenderProps {
@@ -42,78 +42,78 @@ export interface OneBotItemRenderProps {
export const getItemSize = (event: OB11AllEvent['post_type']) => { export const getItemSize = (event: OB11AllEvent['post_type']) => {
if (event === 'meta_event') { if (event === 'meta_event') {
return 100 return 100;
} }
if (event === 'message') { if (event === 'message') {
return 180 return 180;
} }
if (event === 'request') { if (event === 'request') {
return 100 return 100;
} }
if (event === 'notice') { if (event === 'notice') {
return 100 return 100;
} }
if (event === 'message_sent') { if (event === 'message_sent') {
return 250 return 250;
} }
return 100 return 100;
} };
const renderDetail = (data: AllOB11WsResponse) => { const renderDetail = (data: AllOB11WsResponse) => {
if (isOB11Event(data)) { if (isOB11Event(data)) {
switch (data.post_type) { switch (data.post_type) {
case 'meta_event': case 'meta_event':
return <OneBotDisplayMeta data={data} /> return <OneBotDisplayMeta data={data} />;
case 'message': case 'message':
return <OneBotMessage data={data} /> return <OneBotMessage data={data} />;
case 'request': case 'request':
return <RequestComponent data={data} /> return <RequestComponent data={data} />;
case 'notice': case 'notice':
return <OneBotNotice data={data} /> return <OneBotNotice data={data} />;
case 'message_sent': case 'message_sent':
return <OneBotMessage data={data} /> return <OneBotMessage data={data} />;
default: default:
return <div></div> return <div></div>;
} }
} }
return <OneBotDisplayResponse data={data} /> return <OneBotDisplayResponse data={data} />;
} };
const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => { const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
const msg = data[index] const msg = data[index];
const isEvent = isOB11Event(msg) const isEvent = isOB11Event(msg);
return ( return (
<div style={style} className="p-1 overflow-visible w-full h-full"> <div style={style} className='p-1 overflow-visible w-full h-full'>
<motion.div <motion.div
variants={itemVariants} variants={itemVariants}
initial="hidden" initial='hidden'
animate="visible" animate='visible'
className="h-full px-2" className='h-full px-2'
> >
<Card className="w-full h-full py-2 bg-opacity-50 backdrop-blur-sm"> <Card className='w-full h-full py-2 bg-opacity-50 backdrop-blur-sm'>
<CardHeader className="py-0 text-default-500 flex-row gap-2"> <CardHeader className='py-0 text-default-500 flex-row gap-2'>
<div className="font-bold"> <div className='font-bold'>
{isEvent ? getEventName(msg.post_type) : '请求响应'} {isEvent ? getEventName(msg.post_type) : '请求响应'}
</div> </div>
<div className="text-sm"> <div className='text-sm'>
{isEvent && timestampToDateString(msg.time)} {isEvent && timestampToDateString(msg.time)}
</div> </div>
<div className="ml-auto"> <div className='ml-auto'>
<Popover <Popover
placement="left" placement='left'
showArrow showArrow
classNames={{ classNames={{
content: 'max-h-96 max-w-96 overflow-hidden p-0' content: 'max-h-96 max-w-96 overflow-hidden p-0',
}} }}
> >
<PopoverTrigger> <PopoverTrigger>
<Button <Button
size="sm" size='sm'
color="primary" color='primary'
variant="flat" variant='flat'
radius="full" radius='full'
isIconOnly isIconOnly
className="text-medium" className='text-medium'
> >
<IoCode /> <IoCode />
</Button> </Button>
@@ -122,17 +122,17 @@ const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
<Snippet <Snippet
hideSymbol hideSymbol
tooltipProps={{ tooltipProps={{
content: '点击复制' content: '点击复制',
}} }}
classNames={{ classNames={{
copyButton: 'self-start sticky top-0 right-0' copyButton: 'self-start sticky top-0 right-0',
}} }}
className="bg-content1 h-full overflow-y-scroll items-start" className='bg-content1 h-full overflow-y-scroll items-start'
> >
{JSON.stringify(msg, null, 2) {JSON.stringify(msg, null, 2)
.split('\n') .split('\n')
.map((line, i) => ( .map((line, i) => (
<span key={i} className="whitespace-pre-wrap break-all"> <span key={i} className='whitespace-pre-wrap break-all'>
{line} {line}
</span> </span>
))} ))}
@@ -141,11 +141,11 @@ const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
</Popover> </Popover>
</div> </div>
</CardHeader> </CardHeader>
<CardBody className="py-0">{renderDetail(msg)}</CardBody> <CardBody className='py-0'>{renderDetail(msg)}</CardBody>
</Card> </Card>
</motion.div> </motion.div>
</div> </div>
) );
} };
export default OneBotItemRender export default OneBotItemRender;

View File

@@ -1,39 +1,39 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { Chip } from '@heroui/chip' import { Chip } from '@heroui/chip';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import { Snippet } from '@heroui/snippet' import { Snippet } from '@heroui/snippet';
import { getResponseStatusColor, getResponseStatusText } from '@/utils/onebot' import { getResponseStatusColor, getResponseStatusText } from '@/utils/onebot';
import { RequestResponse } from '@/types/onebot' import { RequestResponse } from '@/types/onebot';
export interface OneBotDisplayResponseProps { export interface OneBotDisplayResponseProps {
data: RequestResponse data: RequestResponse
} }
const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({ const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
data data,
}) => { }) => {
return ( return (
<div className="flex gap-2 items-center"> <div className='flex gap-2 items-center'>
<Chip color={getResponseStatusColor(data.status)} variant="flat"> <Chip color={getResponseStatusColor(data.status)} variant='flat'>
{getResponseStatusText(data.status)} {getResponseStatusText(data.status)}
</Chip> </Chip>
{data.data && ( {data.data && (
<Popover <Popover
placement="right" placement='right'
showArrow showArrow
classNames={{ classNames={{
content: 'max-h-96 max-w-96 overflow-hidden p-0' content: 'max-h-96 max-w-96 overflow-hidden p-0',
}} }}
> >
<PopoverTrigger> <PopoverTrigger>
<Button <Button
size="sm" size='sm'
color="primary" color='primary'
variant="flat" variant='flat'
radius="full" radius='full'
className="text-medium" className='text-medium'
> >
</Button> </Button>
@@ -42,17 +42,17 @@ const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
<Snippet <Snippet
hideSymbol hideSymbol
tooltipProps={{ tooltipProps={{
content: '点击复制' content: '点击复制',
}} }}
classNames={{ classNames={{
copyButton: 'self-start sticky top-0 right-0' copyButton: 'self-start sticky top-0 right-0',
}} }}
className="bg-content1 h-full overflow-y-scroll items-start" className='bg-content1 h-full overflow-y-scroll items-start'
> >
{JSON.stringify(data.data, null, 2) {JSON.stringify(data.data, null, 2)
.split('\n') .split('\n')
.map((line, i) => ( .map((line, i) => (
<span key={i} className="whitespace-pre-wrap break-all"> <span key={i} className='whitespace-pre-wrap break-all'>
{line} {line}
</span> </span>
))} ))}
@@ -61,15 +61,15 @@ const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
</Popover> </Popover>
)} )}
{data.message && ( {data.message && (
<Chip className="pl-0.5" variant="flat"> <Chip className='pl-0.5' variant='flat'>
<Chip color="warning" size="sm" className="-ml-2 mr-1" variant="flat"> <Chip color='warning' size='sm' className='-ml-2 mr-1' variant='flat'>
</Chip> </Chip>
{data.message} {data.message}
</Chip> </Chip>
)} )}
</div> </div>
) );
} };
export default OneBotDisplayResponse export default OneBotDisplayResponse;

View File

@@ -1,6 +1,6 @@
import { Select, SelectItem } from '@heroui/select' import { Select, SelectItem } from '@heroui/select';
import { SharedSelection } from '@heroui/system' import { SharedSelection } from '@heroui/system';
import type { Selection } from '@react-types/shared' import type { Selection } from '@react-types/shared';
export interface FilterMessageTypeProps { export interface FilterMessageTypeProps {
filterTypes: Selection filterTypes: Selection
@@ -11,27 +11,27 @@ const items = [
{ label: '消息', value: 'message' }, { label: '消息', value: 'message' },
{ label: '请求', value: 'request' }, { label: '请求', value: 'request' },
{ label: '通知', value: 'notice' }, { label: '通知', value: 'notice' },
{ label: '消息发送', value: 'message_sent' } { label: '消息发送', value: 'message_sent' },
] ];
const FilterMessageType: React.FC<FilterMessageTypeProps> = (props) => { const FilterMessageType: React.FC<FilterMessageTypeProps> = (props) => {
const { filterTypes, onSelectionChange } = props const { filterTypes, onSelectionChange } = props;
return ( return (
<Select <Select
selectedKeys={filterTypes} selectedKeys={filterTypes}
onSelectionChange={(selectedKeys) => { onSelectionChange={(selectedKeys) => {
if (selectedKeys !== 'all' && selectedKeys?.size === 0) { if (selectedKeys !== 'all' && selectedKeys?.size === 0) {
selectedKeys = 'all' selectedKeys = 'all';
} }
onSelectionChange(selectedKeys) onSelectionChange(selectedKeys);
}} }}
label="筛选消息类型" label='筛选消息类型'
selectionMode="multiple" selectionMode='multiple'
items={items} items={items}
renderValue={(value) => { renderValue={(value) => {
if (value.length === items.length) { if (value.length === items.length) {
return '全部' return '全部';
} }
return value.map((v) => v.data?.label).join(',') return value.map((v) => v.data?.label).join(',');
}} }}
> >
{(item) => ( {(item) => (
@@ -40,8 +40,8 @@ const FilterMessageType: React.FC<FilterMessageTypeProps> = (props) => {
</SelectItem> </SelectItem>
)} )}
</Select> </Select>
) );
} };
export const renderFilterMessageType = ( export const renderFilterMessageType = (
filterTypes: Selection, filterTypes: Selection,
@@ -52,7 +52,7 @@ export const renderFilterMessageType = (
filterTypes={filterTypes} filterTypes={filterTypes}
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
/> />
) );
} };
export default FilterMessageType export default FilterMessageType;

View File

@@ -1,62 +1,62 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react';
import { VariableSizeList } from 'react-window' import { VariableSizeList } from 'react-window';
import OneBotItemRender, { import OneBotItemRender, {
getItemSize getItemSize,
} from '@/components/onebot/display_card/render' } from '@/components/onebot/display_card/render';
import { isOB11Event } from '@/utils/onebot' import { isOB11Event } from '@/utils/onebot';
import type { AllOB11WsResponse } from '@/types/onebot' import type { AllOB11WsResponse } from '@/types/onebot';
export interface OneBotMessageListProps { export interface OneBotMessageListProps {
messages: AllOB11WsResponse[] messages: AllOB11WsResponse[]
} }
const OneBotMessageList: React.FC<OneBotMessageListProps> = (props) => { const OneBotMessageList: React.FC<OneBotMessageListProps> = (props) => {
const { messages } = props const { messages } = props;
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null);
const listRef = useRef<VariableSizeList>(null) const listRef = useRef<VariableSizeList>(null);
const [containerHeight, setContainerHeight] = useState(400) const [containerHeight, setContainerHeight] = useState(400);
useEffect(() => { useEffect(() => {
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
if (containerRef.current) { if (containerRef.current) {
setContainerHeight(containerRef.current.offsetHeight) setContainerHeight(containerRef.current.offsetHeight);
} }
}) });
if (containerRef.current) { if (containerRef.current) {
resizeObserver.observe(containerRef.current) resizeObserver.observe(containerRef.current);
} }
return () => { return () => {
resizeObserver.disconnect() resizeObserver.disconnect();
} };
}, []) }, []);
useEffect(() => { useEffect(() => {
if (listRef.current) { if (listRef.current) {
listRef.current.resetAfterIndex(0, true) listRef.current.resetAfterIndex(0, true);
} }
}, [messages]) }, [messages]);
return ( return (
<div className="w-full h-full overflow-hidden" ref={containerRef}> <div className='w-full h-full overflow-hidden' ref={containerRef}>
<VariableSizeList <VariableSizeList
ref={listRef} ref={listRef}
itemCount={messages.length} itemCount={messages.length}
width="100%" width='100%'
style={{ style={{
overflowX: 'hidden' overflowX: 'hidden',
}} }}
itemSize={(idx) => { itemSize={(idx) => {
const msg = messages[idx] const msg = messages[idx];
if (isOB11Event(msg)) { if (isOB11Event(msg)) {
const size = getItemSize(msg.post_type) const size = getItemSize(msg.post_type);
return size return size;
} else { } else {
return 100 return 100;
} }
}} }}
height={containerHeight} height={containerHeight}
@@ -66,7 +66,7 @@ const OneBotMessageList: React.FC<OneBotMessageListProps> = (props) => {
{OneBotItemRender} {OneBotItemRender}
</VariableSizeList> </VariableSizeList>
</div> </div>
) );
} };
export default OneBotMessageList export default OneBotMessageList;

View File

@@ -1,8 +1,8 @@
import { Image } from '@heroui/image' import { Image } from '@heroui/image';
import qface from 'qface' import qface from 'qface';
import { FaReply } from 'react-icons/fa6' import { FaReply } from 'react-icons/fa6';
import { OB11Segment } from '@/types/onebot' import { OB11Segment } from '@/types/onebot';
export const renderMessageContent = ( export const renderMessageContent = (
segments: OB11Segment[], segments: OB11Segment[],
@@ -11,27 +11,27 @@ export const renderMessageContent = (
return segments.map((segment, index) => { return segments.map((segment, index) => {
switch (segment.type) { switch (segment.type) {
case 'text': case 'text':
return <span key={index}>{segment.data.text}</span> return <span key={index}>{segment.data.text}</span>;
case 'face': case 'face':
return ( return (
<Image <Image
removeWrapper removeWrapper
classNames={{ classNames={{
img: 'w-6 h-6 inline !text-[0px] m-0 -mt-1.5 !p-0' img: 'w-6 h-6 inline !text-[0px] m-0 -mt-1.5 !p-0',
}} }}
key={index} key={index}
src={qface.getUrl(segment.data.id)} src={qface.getUrl(segment.data.id)}
alt={`face-${segment.data.id}`} alt={`face-${segment.data.id}`}
/> />
) );
case 'image': case 'image':
return ( return (
<Image <Image
classNames={{ classNames={{
wrapper: 'block !text-[0px] !m-0 !p-0', wrapper: 'block !text-[0px] !m-0 !p-0',
img: 'block' img: 'block',
}} }}
radius="sm" radius='sm'
className={ className={
small small
? 'max-h-16 object-cover' ? 'max-h-16 object-cover'
@@ -39,10 +39,10 @@ export const renderMessageContent = (
} }
key={index} key={index}
src={segment.data.url || segment.data.file} src={segment.data.url || segment.data.file}
alt="image" alt='image'
referrerPolicy="no-referrer" referrerPolicy='no-referrer'
/> />
) );
case 'record': case 'record':
return ( return (
<audio <audio
@@ -50,7 +50,7 @@ export const renderMessageContent = (
controls controls
src={segment.data.url || segment.data.file} src={segment.data.url || segment.data.file}
/> />
) );
case 'video': case 'video':
return ( return (
<video <video
@@ -58,107 +58,109 @@ export const renderMessageContent = (
controls controls
src={segment.data.url || segment.data.file} src={segment.data.url || segment.data.file}
/> />
) );
case 'at': case 'at':
return ( return (
<span key={index} className="text-blue-500"> <span key={index} className='text-blue-500'>
@ @
{segment.data.qq === 'all' ? ( {segment.data.qq === 'all'
'所有人' ? (
) : ( '所有人'
<span> )
{segment.data.name}({segment.data.qq}) : (
</span> <span>
)} {segment.data.name}({segment.data.qq})
</span>
)}
</span> </span>
) );
case 'rps': case 'rps':
return <span key={index}>[]</span> return <span key={index}>[]</span>;
case 'dice': case 'dice':
return <span key={index}>[]</span> return <span key={index}>[]</span>;
case 'shake': case 'shake':
return <span key={index}>[]</span> return <span key={index}>[]</span>;
case 'poke': case 'poke':
return ( return (
<span key={index}> <span key={index}>
[: {segment.data.name || segment.data.id}] [: {segment.data.name || segment.data.id}]
</span> </span>
) );
case 'anonymous': case 'anonymous':
return <span key={index}>[]</span> return <span key={index}>[]</span>;
case 'share': case 'share':
return ( return (
<a <a
key={index} key={index}
href={segment.data.url} href={segment.data.url}
target="_blank" target='_blank'
rel="noopener noreferrer" rel='noopener noreferrer'
> >
{segment.data.title} {segment.data.title}
</a> </a>
) );
case 'contact': case 'contact':
return ( return (
<span key={index}> <span key={index}>
[{segment.data.type === 'qq' ? '好友' : '群'}: {segment.data.id} [{segment.data.type === 'qq' ? '好友' : '群'}: {segment.data.id}
] ]
</span> </span>
) );
case 'location': case 'location':
return <span key={index}>[: {segment.data.title || '未知'}]</span> return <span key={index}>[: {segment.data.title || '未知'}]</span>;
case 'music': case 'music':
if (segment.data.type === 'custom') { if (segment.data.type === 'custom') {
return ( return (
<a <a
key={index} key={index}
href={segment.data.url} href={segment.data.url}
target="_blank" target='_blank'
rel="noopener noreferrer" rel='noopener noreferrer'
> >
{segment.data.title} {segment.data.title}
</a> </a>
) );
} }
return ( return (
<span key={index}> <span key={index}>
[: {segment.data.type} - {segment.data.id}] [: {segment.data.type} - {segment.data.id}]
</span> </span>
) );
case 'reply': case 'reply':
return ( return (
<div <div
key={index} key={index}
className="bg-content3 py-1 px-2 rounded-md flex items-center gap-1" className='bg-content3 py-1 px-2 rounded-md flex items-center gap-1'
> >
<FaReply className="text-default-500" /> <FaReply className='text-default-500' />
ID: {segment.data.id} ID: {segment.data.id}
</div> </div>
) );
case 'forward': case 'forward':
return <span key={index}>[: {segment.data.id}]</span> return <span key={index}>[: {segment.data.id}]</span>;
case 'node': case 'node':
return <span key={index}>[]</span> return <span key={index}>[]</span>;
case 'xml': case 'xml':
return <pre key={index}>{segment.data.data}</pre> return <pre key={index}>{segment.data.data}</pre>;
case 'json': case 'json':
return ( return (
<pre key={index} className="break-all whitespace-break-spaces"> <pre key={index} className='break-all whitespace-break-spaces'>
{segment.data.data} {segment.data.data}
</pre> </pre>
) );
case 'file': case 'file':
return ( return (
<a <a
key={index} key={index}
href={segment.data.file} href={segment.data.file}
target="_blank" target='_blank'
rel="noopener noreferrer" rel='noopener noreferrer'
> >
[] []
</a> </a>
) );
default: default:
return <span key={index}>[]</span> return <span key={index}>[]</span>;
} }
}) });
} };

View File

@@ -1,70 +1,70 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button';
import { import {
Modal, Modal,
ModalBody, ModalBody,
ModalContent, ModalContent,
ModalFooter, ModalFooter,
ModalHeader, ModalHeader,
useDisclosure useDisclosure,
} from '@heroui/modal' } from '@heroui/modal';
import { useCallback, useRef } from 'react' import { useCallback, useRef } from 'react';
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import ChatInputModal from '@/components/chat_input/modal' import ChatInputModal from '@/components/chat_input/modal';
import CodeEditor from '@/components/code_editor' import CodeEditor from '@/components/code_editor';
import type { CodeEditorRef } from '@/components/code_editor' import type { CodeEditorRef } from '@/components/code_editor';
export interface OneBotSendModalProps { export interface OneBotSendModalProps {
sendMessage: (msg: string) => void sendMessage: (msg: string) => void;
} }
const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => { const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
const { sendMessage } = props const { sendMessage } = props;
const { isOpen, onOpen, onOpenChange } = useDisclosure() const { isOpen, onOpen, onOpenChange } = useDisclosure();
const editorRef = useRef<CodeEditorRef | null>(null) const editorRef = useRef<CodeEditorRef | null>(null);
const handleSendMessage = useCallback( const handleSendMessage = useCallback(
(onClose: () => void) => { (onClose: () => void) => {
const msg = editorRef.current?.getValue() const msg = editorRef.current?.getValue();
if (!msg) { if (!msg) {
toast.error('消息不能为空') toast.error('消息不能为空');
return return;
} }
try { try {
sendMessage(msg) sendMessage(msg);
toast.success('消息发送成功') toast.success('消息发送成功');
onClose() onClose();
} catch (error) { } catch (_error) {
toast.error('消息发送失败') toast.error('消息发送失败');
} }
}, },
[sendMessage] [sendMessage]
) );
return ( return (
<> <>
<Button onPress={onOpen} color="primary" radius="full" variant="flat"> <Button onPress={onOpen} color='primary' radius='full' variant='flat'>
</Button> </Button>
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
placement="top-center" placement='top-center'
size="5xl" size='5xl'
scrollBehavior="outside" scrollBehavior='outside'
isDismissable={false} isDismissable={false}
> >
<ModalContent> <ModalContent>
{(onClose) => ( {(onClose) => (
<> <>
<ModalHeader className="flex flex-col gap-1"> <ModalHeader className='flex flex-col gap-1'>
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div className="h-96 dark:bg-[rgb(30,30,30)] p-2 rounded-md border border-default-100"> <div className='h-96 dark:bg-[rgb(30,30,30)] p-2 rounded-md border border-default-100'>
<CodeEditor <CodeEditor
height="100%" height='100%'
defaultLanguage="json" defaultLanguage='json'
defaultValue={`{ defaultValue={`{
"action": "get_group_list" "action": "get_group_list"
}`} }`}
@@ -75,11 +75,11 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
<ModalFooter> <ModalFooter>
<ChatInputModal /> <ChatInputModal />
<Button color="primary" variant="flat" onPress={onClose}> <Button color='primary' variant='flat' onPress={onClose}>
</Button> </Button>
<Button <Button
color="primary" color='primary'
onPress={() => handleSendMessage(onClose)} onPress={() => handleSendMessage(onClose)}
> >
@@ -90,6 +90,6 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
</ModalContent> </ModalContent>
</Modal> </Modal>
</> </>
) );
} };
export default OneBotSendModal export default OneBotSendModal;

View File

@@ -1,39 +1,39 @@
import clsx from 'clsx' import clsx from 'clsx';
import { ReadyState } from 'react-use-websocket' import { ReadyState } from 'react-use-websocket';
export interface WSStatusProps { export interface WSStatusProps {
state: ReadyState state: ReadyState
} }
function StatusTag({ function StatusTag ({
title, title,
color color,
}: { }: {
title: string title: string
color: 'success' | 'primary' | 'warning' color: 'success' | 'primary' | 'warning'
}) { }) {
const textClassName = `text-${color} text-sm` const textClassName = `text-${color} text-sm`;
const bgClassName = `bg-${color}` const bgClassName = `bg-${color}`;
return ( return (
<div className="flex flex-col justify-center items-center gap-1 rounded-md px-2 col-span-2 md:col-span-1"> <div className='flex flex-col justify-center items-center gap-1 rounded-md px-2 col-span-2 md:col-span-1'>
<div className={clsx('w-4 h-4 rounded-full', bgClassName)}></div> <div className={clsx('w-4 h-4 rounded-full', bgClassName)} />
<div className={textClassName}>{title}</div> <div className={textClassName}>{title}</div>
</div> </div>
) );
} }
export default function WSStatus({ state }: WSStatusProps) { export default function WSStatus ({ state }: WSStatusProps) {
if (state === ReadyState.OPEN) { if (state === ReadyState.OPEN) {
return <StatusTag title="已连接" color="success" /> return <StatusTag title='已连接' color='success' />;
} }
if (state === ReadyState.CLOSED) { if (state === ReadyState.CLOSED) {
return <StatusTag title="已关闭" color="primary" /> return <StatusTag title='已关闭' color='primary' />;
} }
if (state === ReadyState.CONNECTING) { if (state === ReadyState.CONNECTING) {
return <StatusTag title="连接中" color="warning" /> return <StatusTag title='连接中' color='warning' />;
} }
if (state === ReadyState.CLOSING) { if (state === ReadyState.CLOSING) {
return <StatusTag title="关闭中" color="warning" /> return <StatusTag title='关闭中' color='warning' />;
} }
return null return null;
} }

View File

@@ -1,25 +1,24 @@
import { Image } from '@heroui/image' import { Image } from '@heroui/image';
import React from 'react'
import bkg_color from '@/assets/images/bkg-color.png' import bkg_color from '@/assets/images/bkg-color.png';
const PageBackground = () => { const PageBackground = () => {
return ( return (
<React.Fragment> <>
<div className="fixed w-full h-full -z-[0] flex justify-end opacity-80"> <div className='fixed w-full h-full -z-[0] flex justify-end opacity-80'>
<Image <Image
className="overflow-hidden object-contain -top-42 h-[160%] -right-[30%] -rotate-45 pointer-events-none select-none -z-10 relative" className='overflow-hidden object-contain -top-42 h-[160%] -right-[30%] -rotate-45 pointer-events-none select-none -z-10 relative'
src={bkg_color} src={bkg_color}
/> />
</div> </div>
<div className="fixed w-full h-full overflow-hidden -z-[0] hue-rotate-90 flex justify-start opacity-80"> <div className='fixed w-full h-full overflow-hidden -z-[0] hue-rotate-90 flex justify-start opacity-80'>
<Image <Image
className="relative -top-92 h-[180%] object-contain pointer-events-none rotate-90 select-none -z-10 top-44" className='relative -top-92 h-[180%] object-contain pointer-events-none rotate-90 select-none -z-10 top-44'
src={bkg_color} src={bkg_color}
/> />
</div> </div>
</React.Fragment> </>
) );
} };
export default PageBackground export default PageBackground;

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