Compare commits

...

172 Commits

Author SHA1 Message Date
时瑾
a9e569771c feat: CodeQL不认可 受不了 2025-09-09 00:35:43 +08:00
时瑾
84fa9da762 feat: 增强文件路径处理逻辑,添加安全验证以防止路径遍历攻击,并优化查询参数提取 2025-09-09 00:27:48 +08:00
时瑾
2f9bcd88ba feat: 增强路径处理逻辑,添加安全验证以防止路径遍历攻击 2025-09-08 23:50:17 +08:00
时瑾
8eacaa294f refactor: 优化网络显示卡片按钮样式和行为,调整按钮属性以提升用户体验 2025-09-08 23:40:48 +08:00
时瑾
5a2939f4ec feat: 给文件管理添加WebUI配置文件的脱敏处理和验证逻辑 2025-09-08 23:07:35 +08:00
时瑾
d3013e32e1 feat: 更新密码功能增强,添加新密码强度验证和旧密码检查 2025-09-08 22:35:36 +08:00
时瑾
5499b5fbc9 refactor: 日志路由进行脱敏,生成随机密码使用node:crypto.randomBytes 2025-09-08 22:24:37 +08:00
时瑾
ed0b8408df refactor: 将默认密码相关逻辑重构为后端处理 2025-09-08 22:13:56 +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
128 changed files with 30187 additions and 496 deletions

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.Framework.Windows.Once.zip
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"}'

1
.gitignore vendored
View File

@@ -1,6 +1,5 @@
# Develop
node_modules/
package-lock.json
pnpm-lock.yaml
out/
dist/

View File

@@ -9,4 +9,9 @@
"css.customData": [
".vscode/tailwindcss.json"
],
}
"editor.formatOnPaste": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "never"
},
}

View File

@@ -44,7 +44,7 @@ _Modern protocol-side framework implemented based on NTQQ._
| 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/HaRcfrHpUk) | [![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-MelodicMoonlight-blue)](https://t.me/MelodicMoonlight) |
| Telegram | [![Telegram](https://img.shields.io/badge/Telegram-napcatqq-blue)](https://t.me/napcatqq) |
|:-:|:-:|
## Thanks
@@ -54,7 +54,9 @@ _Modern protocol-side framework implemented based on NTQQ._
+ [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的消息导出工具 在此推荐一下
+ 不过最最重要的 还是需要感谢屏幕前的你哦~
---

Binary file not shown.

View File

@@ -1,9 +1,9 @@
{
"name": "qq-chat",
"version": "9.9.19-34740",
"verHash": "f31348f2",
"linuxVersion": "3.2.17-34740",
"linuxVerHash": "5aa2d8d6",
"verHash": "cc326038",
"version": "9.9.21-39038",
"linuxVersion": "3.2.19-39038",
"linuxVerHash": "c773cdf7",
"private": true,
"description": "QQ",
"productName": "QQ",
@@ -17,7 +17,7 @@
"qd": "externals/devtools/cli/index.js"
},
"main": "./loadNapCat.js",
"buildVersion": "34740",
"buildVersion": "39038",
"isPureShell": true,
"isByteCodeShell": true,
"platform": "win32",

View File

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -92,7 +92,9 @@ const MusicInsert = () => {
className="w-96"
fullWidth
selectedKey={mode}
onSelectionChange={setMode}
onSelectionChange={(key) => {
if (key !== null) setMode(key)
}}
>
<Tab title="主流平台" key="default" className="flex flex-col gap-2">
<Select

View File

@@ -62,24 +62,39 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
<ButtonGroup
fullWidth
isDisabled={editing}
radius="full"
radius="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
color={debug ? 'success' : 'default'}
startContent={<CgDebug />}
color={debug ? 'secondary' : 'success'}
variant="flat"
startContent={
<CgDebug
style={{
width: '16px',
height: '16px',
minWidth: '16px',
minHeight: '16px'
}}
/>
}
onPress={handleEnableDebug}
>
{debug ? '关闭调试' : '开启调试'}
</Button>
<Button
color="primary"
startContent={<MdDeleteForever />}
className="bg-danger/20 text-danger hover:bg-danger/30 transition-colors"
variant="flat"
startContent={<MdDeleteForever size={16} />}
onPress={handleDelete}
>

View File

@@ -170,3 +170,11 @@ const GenericForm = <T extends keyof NetworkConfigType>({
}
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,4 +1,4 @@
import GenericForm from './generic_form'
import GenericForm, { random_token } from './generic_form'
import type { Field } from './generic_form'
export interface HTTPClientFormProps {
@@ -20,7 +20,7 @@ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
url: 'http://localhost:8080',
reportSelfMessage: false,
messagePostFormat: 'array',
token: '',
token: random_token(16),
debug: false
}

View File

@@ -1,4 +1,4 @@
import GenericForm from './generic_form'
import GenericForm, { random_token } from './generic_form'
import type { Field } from './generic_form'
export interface HTTPServerFormProps {
@@ -17,12 +17,12 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
const defaultValues: HTTPServerFormType[0] = {
enable: false,
name: '',
host: '0.0.0.0',
host: '127.0.0.1',
port: 3000,
enableCors: true,
enableWebsocket: true,
messagePostFormat: 'array',
token: '',
token: random_token(16),
debug: false
}

View File

@@ -1,4 +1,4 @@
import GenericForm from './generic_form'
import GenericForm, { random_token } from './generic_form'
import type { Field } from './generic_form'
export interface HTTPServerSSEFormProps {
@@ -19,12 +19,12 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
const defaultValues: HTTPServerSSEFormType[0] = {
enable: false,
name: '',
host: '0.0.0.0',
host: '127.0.0.1',
port: 3000,
enableCors: true,
enableWebsocket: true,
messagePostFormat: 'array',
token: '',
token: random_token(16),
debug: false,
reportSelfMessage: false
}

View File

@@ -1,4 +1,4 @@
import GenericForm from './generic_form'
import GenericForm, { random_token } from './generic_form'
import type { Field } from './generic_form'
export interface WebsocketClientFormProps {
@@ -22,7 +22,7 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
url: 'ws://localhost:8082',
reportSelfMessage: false,
messagePostFormat: 'array',
token: '',
token: random_token(16),
debug: false,
heartInterval: 30000,
reconnectInterval: 30000

View File

@@ -1,4 +1,4 @@
import GenericForm from './generic_form'
import GenericForm, { random_token } from './generic_form'
import type { Field } from './generic_form'
export interface WebsocketServerFormProps {
@@ -19,12 +19,12 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
const defaultValues: WebsocketServerFormType[0] = {
enable: false,
name: '',
host: '0.0.0.0',
host: '127.0.0.1',
port: 3001,
reportSelfMessage: false,
enableForcePushEvent: true,
messagePostFormat: 'array',
token: '',
token: random_token(16),
debug: false,
heartInterval: 30000
}

View File

@@ -26,7 +26,7 @@ const itemVariants = {
opacity: 1,
scale: 1,
y: 0,
transition: { type: 'spring', stiffness: 300, damping: 20 }
transition: { type: 'spring' as const, stiffness: 300, damping: 20 }
}
}

View File

@@ -24,9 +24,7 @@ const oneBotHttpApiGroup = {
},
'/get_group_system_msg': {
description: '获取群系统消息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
InvitedRequest: z
@@ -37,6 +35,7 @@ const oneBotHttpApiGroup = {
invitor_uin: z.string().describe('邀请人 QQ 号'),
invitor_nick: z.string().describe('邀请人昵称'),
group_id: z.string().describe('群号'),
message: z.string().describe('入群回答'),
group_name: z.string().describe('群名称'),
checked: z.boolean().describe('是否已处理'),
actor: z.string().describe('处理人 QQ 号')
@@ -50,6 +49,7 @@ const oneBotHttpApiGroup = {
requester_uin: z.string().describe('请求人 QQ 号'),
requester_nick: z.string().describe('请求人昵称'),
group_id: z.string().describe('群号'),
message: z.string().describe('入群回答'),
group_name: z.string().describe('群名称'),
checked: z.boolean().describe('是否已处理'),
actor: z.string().describe('处理人 QQ 号')
@@ -604,7 +604,7 @@ const oneBotHttpApiGroup = {
response: baseResponseSchema.extend({
data: z
.object({
group_id: z.string().describe('群号'),
group_id: z.number().describe('群号'),
current_talkative: z
.object({
user_id: z.number().describe('QQ 号'),

View File

@@ -33,13 +33,6 @@ export default class WebUIManager {
return data.data
}
public static async checkUsingDefaultToken() {
const { data } = await serverRequest.get<ServerResponse<boolean>>(
'/auth/check_using_default_token'
)
return data.data
}
public static async proxy<T>(url = '') {
const data = await serverRequest.get<ServerResponse<string>>(
'/base/proxy?url=' + encodeURIComponent(url)
@@ -152,4 +145,55 @@ export default class WebUIManager {
return eventSource
}
// 获取WebUI基础配置
public static async getWebUIConfig() {
const { data } = await serverRequest.get<ServerResponse<WebUIConfig>>(
'/WebUIConfig/GetConfig'
)
return data.data
}
// 更新WebUI基础配置
public static async updateWebUIConfig(config: Partial<WebUIConfig>) {
const { data } = await serverRequest.post<ServerResponse<boolean>>(
'/WebUIConfig/UpdateConfig',
config
)
return data.data
}
// 获取是否禁用WebUI
public static async getDisableWebUI() {
const { data } = await serverRequest.get<ServerResponse<boolean>>(
'/WebUIConfig/GetDisableWebUI'
)
return data.data
}
// 更新是否禁用WebUI
public static async updateDisableWebUI(disable: boolean) {
const { data } = await serverRequest.post<ServerResponse<boolean>>(
'/WebUIConfig/UpdateDisableWebUI',
{ disable }
)
return data.data
}
// 获取是否禁用非局域网访问
public static async getDisableNonLANAccess() {
const { data } = await serverRequest.get<ServerResponse<boolean>>(
'/WebUIConfig/GetDisableNonLANAccess'
)
return data.data
}
// 更新是否禁用非局域网访问
public static async updateDisableNonLANAccess(disable: boolean) {
const { data } = await serverRequest.post<ServerResponse<boolean>>(
'/WebUIConfig/UpdateDisableNonLANAccess',
{ disable }
)
return data.data
}
}

View File

@@ -155,7 +155,7 @@ export default function AboutPage() {
shadow="sm"
isPressable
isExternal
href="https://t.me/MelodicMoonlight"
href="https://t.me/napcatqq"
>
<CardBody className="flex-row items-center gap-2">
<span className="p-2 rounded-small bg-primary-50 text-primary-500">

View File

@@ -14,8 +14,9 @@ const ChangePasswordCard = () => {
const {
control,
handleSubmit: handleWebuiSubmit,
formState: { isSubmitting },
reset
formState: { isSubmitting, errors },
reset,
watch
} = useForm<{
oldToken: string
newToken: string
@@ -29,9 +30,14 @@ const ChangePasswordCard = () => {
const navigate = useNavigate()
const [_, setToken] = useLocalStorage(key.token, '')
// 监听旧密码的值
const oldTokenValue = watch('oldToken')
const onSubmit = handleWebuiSubmit(async (data) => {
try {
// 使用正常密码更新流程
await WebUIManager.changePassword(data.oldToken, data.newToken)
toast.success('修改成功')
setToken('')
localStorage.removeItem(key.token)
@@ -45,30 +51,75 @@ const ChangePasswordCard = () => {
return (
<>
<title> - NapCat WebUI</title>
<Controller
control={control}
name="oldToken"
rules={{
required: '旧密码不能为空',
validate: (value) => {
if (!value || value.trim().length === 0) {
return '旧密码不能为空'
}
return true
}
}}
render={({ field }) => (
<Input
{...field}
label="旧密码"
placeholder="请输入旧密码"
type="password"
isRequired
isInvalid={!!errors.oldToken}
errorMessage={errors.oldToken?.message}
/>
)}
/>
<Controller
control={control}
name="newToken"
rules={{
required: '新密码不能为空',
minLength: {
value: 6,
message: '新密码至少需要6个字符'
},
validate: (value) => {
if (!value || value.trim().length === 0) {
return '新密码不能为空'
}
if (value.trim().length !== value.length) {
return '新密码不能包含前后空格'
}
if (value === oldTokenValue) {
return '新密码不能与旧密码相同'
}
// 检查是否包含字母
if (!/[a-zA-Z]/.test(value)) {
return '新密码必须包含字母'
}
// 检查是否包含数字
if (!/[0-9]/.test(value)) {
return '新密码必须包含数字'
}
return true
}
}}
render={({ field }) => (
<Input
{...field}
label="新密码"
placeholder="请输入新密码"
placeholder="至少6位包含字母和数字"
type="password"
isRequired
isInvalid={!!errors.newToken}
errorMessage={errors.newToken?.message}
/>
)}
/>
<SaveButtons
onSubmit={onSubmit}
reset={reset}

View File

@@ -7,6 +7,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'
import ChangePasswordCard from './change_password'
import LoginConfigCard from './login'
import OneBotConfigCard from './onebot'
import ServerConfigCard from './server'
import ThemeConfigCard from './theme'
import WebUIConfigCard from './webui'
@@ -67,6 +68,11 @@ export default function ConfigPage() {
<OneBotConfigCard />
</ConfingPageItem>
</Tab>
<Tab title="服务器配置" key="server">
<ConfingPageItem>
<ServerConfigCard />
</ConfingPageItem>
</Tab>
<Tab title="WebUI配置" key="webui">
<ConfingPageItem>
<WebUIConfigCard />

View File

@@ -0,0 +1,185 @@
import { Input } from '@heroui/input'
import { Switch } from '@heroui/switch'
import { useRequest } from 'ahooks'
import { useEffect } from 'react'
import { Controller, useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
import SaveButtons from '@/components/button/save_buttons'
import PageLoading from '@/components/page_loading'
import WebUIManager from '@/controllers/webui_manager'
const ServerConfigCard = () => {
const {
data: configData,
loading: configLoading,
error: configError,
refreshAsync: refreshConfig
} = useRequest(WebUIManager.getWebUIConfig)
const {
control,
handleSubmit: handleConfigSubmit,
formState: { isSubmitting },
setValue: setConfigValue
} = useForm<{
host: string
port: number
loginRate: number
disableWebUI: boolean
disableNonLANAccess: boolean
}>({
defaultValues: {
host: '0.0.0.0',
port: 6099,
loginRate: 10,
disableWebUI: false,
disableNonLANAccess: false
}
})
const reset = () => {
if (configData) {
setConfigValue('host', configData.host)
setConfigValue('port', configData.port)
setConfigValue('loginRate', configData.loginRate)
setConfigValue('disableWebUI', configData.disableWebUI)
setConfigValue('disableNonLANAccess', configData.disableNonLANAccess)
}
}
const onSubmit = handleConfigSubmit(async (data) => {
try {
await WebUIManager.updateWebUIConfig(data)
toast.success('保存成功')
} catch (error) {
const msg = (error as Error).message
toast.error(`保存失败: ${msg}`)
}
})
const onRefresh = async () => {
try {
await refreshConfig()
toast.success('刷新成功')
} catch (error) {
const msg = (error as Error).message
toast.error(`刷新失败: ${msg}`)
}
}
useEffect(() => {
reset()
}, [configData])
if (configLoading) return <PageLoading loading={true} />
return (
<>
<title> - NapCat WebUI</title>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex-shrink-0 w-full"></div>
<Controller
control={control}
name="host"
render={({ field }) => (
<Input
{...field}
label="监听地址"
placeholder="请输入监听地址"
description="服务器监听的IP地址0.0.0.0表示监听所有网卡"
isDisabled={!!configError}
errorMessage={configError ? '获取配置失败' : undefined}
/>
)}
/>
<Controller
control={control}
name="port"
render={({ field }) => (
<Input
{...field}
type="number"
value={field.value?.toString() || ''}
label="监听端口"
placeholder="请输入监听端口"
description="服务器监听的端口号范围1-65535"
isDisabled={!!configError}
errorMessage={configError ? '获取配置失败' : undefined}
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
/>
)}
/>
<Controller
control={control}
name="loginRate"
render={({ field }) => (
<Input
{...field}
type="number"
value={field.value?.toString() || ''}
label="登录速率限制"
placeholder="请输入登录速率限制"
description="每小时允许的登录尝试次数"
isDisabled={!!configError}
errorMessage={configError ? '获取配置失败' : undefined}
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
/>
)}
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex-shrink-0 w-full"></div>
<Controller
control={control}
name="disableWebUI"
render={({ field }) => (
<Switch
isSelected={field.value}
onValueChange={field.onChange}
isDisabled={!!configError}
>
<div className="flex flex-col">
<span>WebUI</span>
<span className="text-sm text-default-400">
WebUI服务
</span>
</div>
</Switch>
)}
/>
<Controller
control={control}
name="disableNonLANAccess"
render={({ field }) => (
<Switch
isSelected={field.value}
onValueChange={field.onChange}
isDisabled={!!configError}
>
<div className="flex flex-col">
<span>访</span>
<span className="text-sm text-default-400">
访WebUI
</span>
</div>
</Switch>
)}
/>
</div>
</div>
<SaveButtons
onSubmit={onSubmit}
reset={reset}
isSubmitting={isSubmitting || configLoading}
refresh={onRefresh}
/>
</>
)
}
export default ServerConfigCard

View File

@@ -56,9 +56,9 @@ export default function TerminalPage() {
setTabs((prev) => [...prev, newTab])
setSelectedTab(id)
} catch (error) {
} catch (error: unknown) {
console.error('Failed to create terminal:', error)
toast.error('创建终端失败')
toast.error((error as Error).message)
}
}

View File

@@ -1,46 +1,15 @@
import { Spinner } from '@heroui/spinner'
import { AnimatePresence, motion } from 'motion/react'
import { Suspense, useEffect } from 'react'
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import { Suspense } from 'react'
import { Outlet, useLocation } from 'react-router-dom'
import useAuth from '@/hooks/auth'
import useDialog from '@/hooks/use-dialog'
import WebUIManager from '@/controllers/webui_manager'
import DefaultLayout from '@/layouts/default'
const CheckDefaultPassword = () => {
const { isAuth } = useAuth()
const dialog = useDialog()
const navigate = useNavigate()
const checkDefaultPassword = async () => {
const data = await WebUIManager.checkUsingDefaultToken()
if (data) {
dialog.confirm({
title: '修改默认密码',
content: '检测到当前密码为默认密码,请尽快修改密码。',
confirmText: '前往修改',
onConfirm: () => {
navigate('/config?tab=token')
}
})
}
}
useEffect(() => {
if (isAuth) {
checkDefaultPassword()
}
}, [isAuth])
return null
}
export default function IndexPage() {
const location = useLocation()
return (
<DefaultLayout>
<CheckDefaultPassword />
<Suspense
fallback={
<div className="flex justify-center px-10">

View File

@@ -25,7 +25,6 @@ export default function WebLoginPage() {
const [tokenValue, setTokenValue] = useState<string>(token || '')
const [isLoading, setIsLoading] = useState<boolean>(false)
const [, setLocalToken] = useLocalStorage<string>(key.token, '')
const onSubmit = async () => {
if (!tokenValue) {
toast.error('请输入token')
@@ -93,42 +92,65 @@ export default function WebLoginPage() {
</CardHeader>
<CardBody className="flex gap-5 py-5 px-5 md:px-10">
<Input
isClearable
type="password"
classNames={{
label: 'text-black/50 dark:text-white/90',
input: [
'bg-transparent',
'text-black/90 dark:text-white/90',
'placeholder:text-default-700/50 dark:placeholder:text-white/60'
],
innerWrapper: 'bg-transparent',
inputWrapper: [
'shadow-xl',
'bg-default-100/70',
'dark:bg-default/60',
'backdrop-blur-xl',
'backdrop-saturate-200',
'hover:bg-default-0/70',
'dark:hover:bg-default/70',
'group-data-[focus=true]:bg-default-100/50',
'dark:group-data-[focus=true]:bg-default/60',
'!cursor-text'
]
<form
onSubmit={(e) => {
e.preventDefault()
onSubmit()
}}
isDisabled={isLoading}
label="Token"
placeholder="请输入token"
radius="lg"
size="lg"
startContent={
<IoKeyOutline className="text-black/50 mb-0.5 dark:text-white/90 text-slate-400 pointer-events-none flex-shrink-0" />
}
value={tokenValue}
onChange={(e) => setTokenValue(e.target.value)}
onClear={() => setTokenValue('')}
/>
>
{/* 隐藏的用户名字段,帮助浏览器识别登录表单 */}
<input
type="text"
name="username"
value="napcat-webui"
autoComplete="username"
className="absolute -left-[9999px] opacity-0 pointer-events-none"
readOnly
tabIndex={-1}
aria-label="Username"
/>
<Input
isClearable
type="password"
name="password"
autoComplete="current-password"
classNames={{
label: 'text-black/50 dark:text-white/90',
input: [
'bg-transparent',
'text-black/90 dark:text-white/90',
'placeholder:text-default-700/50 dark:placeholder:text-white/60'
],
innerWrapper: 'bg-transparent',
inputWrapper: [
'shadow-xl',
'bg-default-100/70',
'dark:bg-default/60',
'backdrop-blur-xl',
'backdrop-saturate-200',
'hover:bg-default-0/70',
'dark:hover:bg-default/70',
'group-data-[focus=true]:bg-default-100/50',
'dark:group-data-[focus=true]:bg-default/60',
'!cursor-text'
]
}}
isDisabled={isLoading}
label="Token"
placeholder="请输入token"
radius="lg"
size="lg"
startContent={
<IoKeyOutline className="text-black/50 mb-0.5 dark:text-white/90 text-slate-400 pointer-events-none flex-shrink-0" />
}
value={tokenValue}
onChange={(e) => setTokenValue(e.target.value)}
onClear={() => setTokenValue('')}
/>
</form>
<div className="text-center text-small text-default-600 dark:text-default-400 px-2">
💡 NapCat
</div>
<Button
className="mx-10 mt-10 text-lg py-7"
color="primary"

View File

@@ -181,3 +181,11 @@ interface ThemeConfig {
dark: ThemeConfigItem
light: ThemeConfigItem
}
interface WebUIConfig {
host: string
port: number
loginRate: number
disableWebUI: boolean
disableNonLANAccess: boolean
}

8502
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.7.64",
"version": "4.8.109",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
@@ -11,22 +11,28 @@
"dev:universal": "vite build --mode universal",
"dev:framework": "vite build --mode framework",
"dev:shell": "vite build --mode shell",
"dev:shell-analysis": "vite build --mode shell-analysis",
"dev:webui": "cd napcat.webui && npm run dev",
"lint": "eslint --fix src/**/*.{js,ts,vue}",
"depend": "cd dist && npm install --omit=dev",
"dev:depend": "npm i && cd napcat.webui && npm i"
},
"devDependencies": {
"@babel/core": "^7.28.0",
"@babel/generator": "^7.28.0",
"@babel/parser": "^7.28.0",
"@babel/preset-typescript": "^7.24.7",
"@eslint/compat": "^1.2.2",
"@babel/traverse": "^7.28.0",
"@babel/types": "^7.28.2",
"@eslint/compat": "^1.3.1",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
"@eslint/js": "^9.33.0",
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
"@log4js-node/log4js-api": "^1.0.2",
"@napneko/nap-proto-core": "^0.0.4",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-typescript": "^12.1.2",
"@sinclair/typebox": "^0.34.9",
"@rollup/plugin-typescript": "^12.1.4",
"@sinclair/typebox": "^0.34.38",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/multer": "^1.4.12",
@@ -37,33 +43,33 @@
"@types/type-is": "^1.6.7",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@typescript-eslint/parser": "^8.39.0",
"ajv": "^8.13.0",
"async-mutex": "^0.5.0",
"commander": "^13.0.0",
"compressing": "^1.10.1",
"cors": "^2.8.5",
"esbuild": "0.25.4",
"esbuild": "0.25.8",
"eslint": "^9.14.0",
"eslint-import-resolver-typescript": "^4.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"express-rate-limit": "^7.5.0",
"fast-xml-parser": "^4.3.6",
"file-type": "^20.0.0",
"file-type": "^21.0.0",
"globals": "^16.0.0",
"json5": "^2.2.3",
"multer": "^1.4.5-lts.1",
"multer": "^2.0.1",
"napcat.protobuf": "^1.1.4",
"typescript": "^5.3.3",
"typescript-eslint": "^8.13.0",
"vite": "^6.0.1",
"typescript-eslint": "^8.35.1",
"vite": "^7.1.1",
"vite-plugin-cp": "^6.0.0",
"vite-tsconfig-paths": "^5.1.0",
"napcat.protobuf": "^1.1.4",
"winston": "^3.17.0",
"compressing": "^1.10.1"
"winston": "^3.17.0"
},
"dependencies": {
"express": "^5.0.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
"ws": "^8.18.3"
}
}

View File

@@ -10,10 +10,14 @@ import { LogWrapper } from './log';
const downloadOri = "https://github.com/NapNeko/ffmpeg-build/releases/download/v1.0.0/ffmpeg-7.1.1-win64.zip"
const urls = [
"https://github.moeyy.xyz/" + downloadOri,
"https://ghp.ci/" + downloadOri,
"https://gh.api.99988866.xyz/" + downloadOri,
"https://gh.api.99988866.xyz/" + downloadOri,
"https://j.1win.ggff.net/" + downloadOri,
"https://git.yylx.win/" + downloadOri,
"https://ghfile.geekertao.top/" + downloadOri,
"https://gh-proxy.net/" + downloadOri,
"https://ghm.078465.xyz/" + downloadOri,
"https://gitproxy.127731.xyz/" + downloadOri,
"https://jiashu.1win.eu.org/" + downloadOri,
"https://github.tbedu.top/" + downloadOri,
downloadOri
];

328
src/common/health.ts Normal file
View File

@@ -0,0 +1,328 @@
export interface ResourceConfig<T extends any[], R> {
/** 资源获取函数 */
resourceFn: (...args: T) => Promise<R>;
/** 失败后禁用时间(毫秒),默认 30 秒 */
disableTime?: number;
/** 最大重试次数,默认 3 次 */
maxRetries?: number;
/** 主动测试间隔(毫秒),默认 60 秒 */
healthCheckInterval?: number;
/** 最大健康检查失败次数,超过后永久禁用,默认 5 次 */
maxHealthCheckFailures?: number;
/** 资源名称(用于日志) */
name?: string;
/** 测试参数(用于健康检查) */
testArgs?: T;
/** 健康检查函数,如果提供则优先使用此函数进行健康检查 */
healthCheckFn?: (...args: T) => Promise<boolean>;
}
interface ResourceState<T extends any[], R> {
config: ResourceConfig<T, R>;
isEnabled: boolean;
disableUntil: number;
currentRetries: number;
healthCheckFailureCount: number;
isPermanentlyDisabled: boolean;
lastError?: Error;
lastHealthCheckTime: number;
registrationKey: string;
}
export class ResourceManager {
private resources = new Map<string, ResourceState<any, any>>();
private destroyed = false;
private healthCheckTimer?: NodeJS.Timeout;
private readonly HEALTH_CHECK_TASK_INTERVAL = 5000; // 5秒执行一次健康检查任务
constructor() {
this.startHealthCheckTask();
}
/**
* 注册资源(注册即调用,重复注册只实际注册一次)
*/
async register<T extends any[], R>(
key: string,
config: ResourceConfig<T, R>,
...args: T
): Promise<R> {
if (this.destroyed) {
throw new Error('ResourceManager has been destroyed');
}
const registrationKey = this.generateRegistrationKey(key, config);
// 检查是否已经注册
if (this.resources.has(key)) {
const existingState = this.resources.get(key)!;
// 如果是相同的配置,直接调用
if (existingState.registrationKey === registrationKey) {
return this.callResource<T, R>(key, ...args);
}
// 配置不同,清理旧的并重新注册
this.unregister(key);
}
// 创建新的资源状态
const state: ResourceState<T, R> = {
config: {
disableTime: 30000,
maxRetries: 3,
healthCheckInterval: 60000,
maxHealthCheckFailures: 5,
name: key,
...config
},
isEnabled: true,
disableUntil: 0,
currentRetries: 0,
healthCheckFailureCount: 0,
isPermanentlyDisabled: false,
lastHealthCheckTime: 0,
registrationKey
};
this.resources.set(key, state);
// 注册即调用
return await this.callResource<T, R>(key, ...args);
}
/**
* 调用资源
*/
async callResource<T extends any[], R>(key: string, ...args: T): Promise<R> {
const state = this.resources.get(key) as ResourceState<T, R> | undefined;
if (!state) {
throw new Error(`Resource ${key} not registered`);
}
if (state.isPermanentlyDisabled) {
throw new Error(`Resource ${key} is permanently disabled due to repeated health check failures`);
}
if (!this.isResourceAvailable(key)) {
const disableUntilDate = new Date(state.disableUntil).toISOString();
throw new Error(`Resource ${key} is currently disabled until ${disableUntilDate}`);
}
try {
const result = await state.config.resourceFn(...args);
this.onResourceSuccess(state);
return result;
} catch (error) {
this.onResourceFailure(state, error as Error);
throw error;
}
}
/**
* 检查资源是否可用
*/
isResourceAvailable(key: string): boolean {
const state = this.resources.get(key);
if (!state) {
return false;
}
if (state.isPermanentlyDisabled || !state.isEnabled) {
return false;
}
return Date.now() >= state.disableUntil;
}
/**
* 注销资源
*/
unregister(key: string): boolean {
return this.resources.delete(key);
}
/**
* 销毁管理器,清理所有资源
*/
destroy(): void {
if (this.destroyed) {
return;
}
this.stopHealthCheckTask();
this.resources.clear();
this.destroyed = true;
}
private generateRegistrationKey<T extends any[], R>(key: string, config: ResourceConfig<T, R>): string {
const configStr = JSON.stringify({
name: config.name,
disableTime: config.disableTime,
maxRetries: config.maxRetries,
healthCheckInterval: config.healthCheckInterval,
maxHealthCheckFailures: config.maxHealthCheckFailures,
functionStr: config.resourceFn.toString(),
healthCheckFnStr: config.healthCheckFn?.toString()
});
return `${key}_${this.simpleHash(configStr)}`;
}
private simpleHash(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash).toString(36);
}
private onResourceSuccess<T extends any[], R>(state: ResourceState<T, R>): void {
state.currentRetries = 0;
state.disableUntil = 0;
state.healthCheckFailureCount = 0;
state.lastError = undefined;
}
private onResourceFailure<T extends any[], R>(state: ResourceState<T, R>, error: Error): void {
state.currentRetries++;
state.lastError = error;
// 如果重试次数达到上限,禁用资源
if (state.currentRetries >= state.config.maxRetries!) {
state.disableUntil = Date.now() + state.config.disableTime!;
state.currentRetries = 0;
}
}
private startHealthCheckTask(): void {
if (this.healthCheckTimer) {
return;
}
this.healthCheckTimer = setInterval(() => {
this.runHealthCheckTask();
}, this.HEALTH_CHECK_TASK_INTERVAL);
}
private stopHealthCheckTask(): void {
if (this.healthCheckTimer) {
clearInterval(this.healthCheckTimer);
this.healthCheckTimer = undefined;
}
}
private async runHealthCheckTask(): Promise<void> {
if (this.destroyed) {
return;
}
const now = Date.now();
for (const [key, state] of this.resources) {
// 跳过永久禁用或可用的资源
if (state.isPermanentlyDisabled || this.isResourceAvailable(key)) {
continue;
}
// 跳过还在禁用期内的资源
if (now < state.disableUntil) {
continue;
}
// 检查是否需要进行健康检查(根据间隔时间)
const lastHealthCheck = state.lastHealthCheckTime || 0;
const healthCheckInterval = state.config.healthCheckInterval!;
if (now - lastHealthCheck < healthCheckInterval) {
continue;
}
// 执行健康检查
await this.performHealthCheck(state);
}
}
private async performHealthCheck<T extends any[], R>(state: ResourceState<T, R>): Promise<void> {
state.lastHealthCheckTime = Date.now();
try {
let healthCheckResult: boolean;
// 如果有专门的健康检查函数,使用它
if (state.config.healthCheckFn) {
const testArgs = state.config.testArgs || [] as unknown as T;
healthCheckResult = await state.config.healthCheckFn(...testArgs);
} else {
// 否则使用原始函数进行检查
const testArgs = state.config.testArgs || [] as unknown as T;
await state.config.resourceFn(...testArgs);
healthCheckResult = true;
}
if (healthCheckResult) {
// 健康检查成功,重新启用
state.isEnabled = true;
state.disableUntil = 0;
state.currentRetries = 0;
state.healthCheckFailureCount = 0;
state.lastError = undefined;
} else {
throw new Error('Health check function returned false');
}
} catch (error) {
// 健康检查失败,增加失败计数
state.healthCheckFailureCount++;
state.lastError = error as Error;
// 检查是否达到最大健康检查失败次数
if (state.healthCheckFailureCount >= state.config.maxHealthCheckFailures!) {
// 永久禁用资源
state.isPermanentlyDisabled = true;
state.disableUntil = 0;
} else {
// 继续禁用一段时间
state.disableUntil = Date.now() + state.config.disableTime!;
}
}
}
}
// 创建全局实例
export const resourceManager = new ResourceManager();
// 便捷函数
export async function registerResource<T extends any[], R>(
key: string,
config: ResourceConfig<T, R>,
...args: T
): Promise<R> {
return resourceManager.register(key, config, ...args);
}
// 使用示例:
/*
await registerResource(
'api-with-health-check',
{
resourceFn: async (id: string) => {
const response = await fetch(`https://api.example.com/data/${id}`);
return response.json();
},
healthCheckFn: async (id: string) => {
try {
const response = await fetch(`https://api.example.com/health`);
return response.ok;
} catch {
return false;
}
},
testArgs: ['health-check-id'],
healthCheckInterval: 30000,
maxHealthCheckFailures: 3
},
'user123'
);
*/

View File

@@ -9,17 +9,23 @@ export class NapCatPathWrapper {
configPath: string;
cachePath: string;
staticPath: string;
pluginPath: string;
constructor(mainPath: string = dirname(fileURLToPath(import.meta.url))) {
this.binaryPath = mainPath;
let writePath: string;
if (os.platform() === 'darwin') {
if (process.env['NAPCAT_WORKDIR']) {
writePath = process.env['NAPCAT_WORKDIR'];
} else if (os.platform() === 'darwin') {
writePath = path.join(os.homedir(), 'Library', 'Application Support', 'QQ', 'NapCat');
} else {
writePath = this.binaryPath;
}
this.logsPath = path.join(writePath, 'logs');
this.configPath = path.join(writePath, 'config');
this.pluginPath = path.join(writePath, 'plugins');//dynamic load
this.cachePath = path.join(writePath, 'cache');
this.staticPath = path.join(this.binaryPath, 'static');
if (!fs.existsSync(this.logsPath)) {

View File

@@ -0,0 +1,316 @@
/**
* 性能监控器 - 用于统计函数调用次数、耗时等信息
*/
import * as fs from 'fs';
import * as path from 'path';
export interface FunctionStats {
name: string;
callCount: number;
totalTime: number;
averageTime: number;
minTime: number;
maxTime: number;
fileName?: string;
lineNumber?: number;
}
export class PerformanceMonitor {
private static instance: PerformanceMonitor;
private stats = new Map<string, FunctionStats>();
private startTimes = new Map<string, number>();
private reportInterval: NodeJS.Timeout | null = null;
static getInstance(): PerformanceMonitor {
if (!PerformanceMonitor.instance) {
PerformanceMonitor.instance = new PerformanceMonitor();
// 启动定时统计报告
PerformanceMonitor.instance.startPeriodicReport();
}
return PerformanceMonitor.instance;
}
/**
* 开始定时统计报告 (每60秒)
*/
private startPeriodicReport(): void {
if (this.reportInterval) {
clearInterval(this.reportInterval);
}
this.reportInterval = setInterval(() => {
if (this.stats.size > 0) {
this.printPeriodicReport();
this.writeDetailedLogToFile();
}
}, 60000); // 60秒
}
/**
* 停止定时统计报告
*/
stopPeriodicReport(): void {
if (this.reportInterval) {
clearInterval(this.reportInterval);
this.reportInterval = null;
}
}
/**
* 打印定时统计报告 (简化版本)
*/
private printPeriodicReport(): void {
const now = new Date().toLocaleString();
console.log(`\n=== 性能监控定时报告 [${now}] ===`);
const totalFunctions = this.stats.size;
const totalCalls = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.callCount, 0);
const totalTime = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.totalTime, 0);
console.log(`📊 总览: ${totalFunctions} 个函数, ${totalCalls} 次调用, 总耗时: ${totalTime.toFixed(2)}ms`);
// 显示Top 5最活跃的函数
console.log('\n🔥 最活跃函数 (Top 5):');
this.getTopByCallCount(5).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms`);
});
// 显示Top 5最耗时的函数
console.log('\n⏱ 最耗时函数 (Top 5):');
this.getTopByTotalTime(5).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均: ${stat.averageTime.toFixed(2)}ms`);
});
console.log('===============================\n');
}
/**
* 将详细统计数据写入日志文件
*/
private writeDetailedLogToFile(): void {
try {
const now = new Date();
const dateStr = now.toISOString().replace(/[:.]/g, '-').split('T')[0];
const timeStr = now.toTimeString().split(' ')[0]?.replace(/:/g, '-') || 'unknown-time';
const timestamp = `${dateStr}_${timeStr}`;
const fileName = `${timestamp}.log.txt`;
const logPath = path.join(process.cwd(), 'logs', fileName);
// 确保logs目录存在
const logsDir = path.dirname(logPath);
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const totalFunctions = this.stats.size;
const totalCalls = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.callCount, 0);
const totalTime = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.totalTime, 0);
let logContent = '';
logContent += `=== 性能监控详细报告 ===\n`;
logContent += `生成时间: ${now.toLocaleString()}\n`;
logContent += `统计周期: 60秒\n`;
logContent += `总览: ${totalFunctions} 个函数, ${totalCalls} 次调用, 总耗时: ${totalTime.toFixed(2)}ms\n\n`;
// 详细函数统计
logContent += `=== 所有函数详细统计 ===\n`;
const allStats = this.getStats().sort((a, b) => b.totalTime - a.totalTime);
allStats.forEach((stat, index) => {
logContent += `${index + 1}. 函数: ${stat.name}\n`;
logContent += ` 文件: ${stat.fileName || 'N/A'}\n`;
logContent += ` 行号: ${stat.lineNumber || 'N/A'}\n`;
logContent += ` 调用次数: ${stat.callCount}\n`;
logContent += ` 总耗时: ${stat.totalTime.toFixed(4)}ms\n`;
logContent += ` 平均耗时: ${stat.averageTime.toFixed(4)}ms\n`;
logContent += ` 最小耗时: ${stat.minTime === Infinity ? 'N/A' : stat.minTime.toFixed(4)}ms\n`;
logContent += ` 最大耗时: ${stat.maxTime.toFixed(4)}ms\n`;
logContent += ` 性能占比: ${((stat.totalTime / totalTime) * 100).toFixed(2)}%\n`;
logContent += `\n`;
});
// 排行榜统计
logContent += `=== 总耗时排行榜 (Top 20) ===\n`;
this.getTopByTotalTime(20).forEach((stat, index) => {
logContent += `${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 调用: ${stat.callCount}次, 平均: ${stat.averageTime.toFixed(2)}ms\n`;
});
logContent += `\n=== 调用次数排行榜 (Top 20) ===\n`;
this.getTopByCallCount(20).forEach((stat, index) => {
logContent += `${index + 1}. ${stat.name} - 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均: ${stat.averageTime.toFixed(2)}ms\n`;
});
logContent += `\n=== 平均耗时排行榜 (Top 20) ===\n`;
this.getTopByAverageTime(20).forEach((stat, index) => {
logContent += `${index + 1}. ${stat.name} - 平均: ${stat.averageTime.toFixed(2)}ms, 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms\n`;
});
logContent += `\n=== 性能热点分析 ===\n`;
// 找出最耗时的前10个函数
const hotSpots = this.getTopByTotalTime(10);
hotSpots.forEach((stat, index) => {
const efficiency = stat.callCount / stat.totalTime; // 每毫秒的调用次数
logContent += `${index + 1}. ${stat.name}\n`;
logContent += ` 性能影响: ${((stat.totalTime / totalTime) * 100).toFixed(2)}%\n`;
logContent += ` 调用效率: ${efficiency.toFixed(4)} 调用/ms\n`;
logContent += ` 优化建议: ${stat.averageTime > 10 ? '考虑优化此函数的执行效率' :
stat.callCount > 1000 ? '考虑减少此函数的调用频率' :
'性能表现良好'}\n\n`;
});
logContent += `=== 报告结束 ===\n`;
// 写入文件
fs.writeFileSync(logPath, logContent, 'utf8');
console.log(`📄 详细性能报告已保存到: ${logPath}`);
} catch (error) {
console.error('写入性能日志文件时出错:', error);
}
}
/**
* 开始记录函数调用
*/
startFunction(functionName: string, fileName?: string, lineNumber?: number): string {
const callId = `${functionName}_${Date.now()}_${Math.random()}`;
this.startTimes.set(callId, performance.now());
// 初始化或更新统计信息
if (!this.stats.has(functionName)) {
this.stats.set(functionName, {
name: functionName,
callCount: 0,
totalTime: 0,
averageTime: 0,
minTime: Infinity,
maxTime: 0,
fileName,
lineNumber
});
}
const stat = this.stats.get(functionName)!;
stat.callCount++;
return callId;
}
/**
* 结束记录函数调用
*/
endFunction(callId: string, functionName: string): void {
const startTime = this.startTimes.get(callId);
if (!startTime) return;
const endTime = performance.now();
const duration = endTime - startTime;
this.startTimes.delete(callId);
const stat = this.stats.get(functionName);
if (!stat) return;
stat.totalTime += duration;
stat.averageTime = stat.totalTime / stat.callCount;
stat.minTime = Math.min(stat.minTime, duration);
stat.maxTime = Math.max(stat.maxTime, duration);
}
/**
* 获取所有统计信息
*/
getStats(): FunctionStats[] {
return Array.from(this.stats.values());
}
/**
* 获取排行榜 - 按总耗时排序
*/
getTopByTotalTime(limit = 20): FunctionStats[] {
return this.getStats()
.sort((a, b) => b.totalTime - a.totalTime)
.slice(0, limit);
}
/**
* 获取排行榜 - 按调用次数排序
*/
getTopByCallCount(limit = 20): FunctionStats[] {
return this.getStats()
.sort((a, b) => b.callCount - a.callCount)
.slice(0, limit);
}
/**
* 获取排行榜 - 按平均耗时排序
*/
getTopByAverageTime(limit = 20): FunctionStats[] {
return this.getStats()
.sort((a, b) => b.averageTime - a.averageTime)
.slice(0, limit);
}
/**
* 清空统计数据
*/
clear(): void {
this.stats.clear();
this.startTimes.clear();
}
/**
* 打印统计报告
*/
printReport(): void {
console.log('\n=== 函数性能监控报告 ===');
console.log('\n🔥 总耗时排行榜 (Top 10):');
this.getTopByTotalTime(10).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 调用次数: ${stat.callCount}, 平均耗时: ${stat.averageTime.toFixed(2)}ms`);
});
console.log('\n📈 调用次数排行榜 (Top 10):');
this.getTopByCallCount(10).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 调用次数: ${stat.callCount}, 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均耗时: ${stat.averageTime.toFixed(2)}ms`);
});
console.log('\n⏱ 平均耗时排行榜 (Top 10):');
this.getTopByAverageTime(10).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 平均耗时: ${stat.averageTime.toFixed(2)}ms, 调用次数: ${stat.callCount}, 总耗时: ${stat.totalTime.toFixed(2)}ms`);
});
console.log('\n========================\n');
}
/**
* 获取JSON格式的统计数据
*/
toJSON(): FunctionStats[] {
return this.getStats();
}
}
// 全局性能监控器实例
export const performanceMonitor = PerformanceMonitor.getInstance();
// 在进程退出时打印报告并停止定时器
if (typeof process !== 'undefined') {
process.on('exit', () => {
performanceMonitor.stopPeriodicReport();
performanceMonitor.printReport();
});
process.on('SIGINT', () => {
performanceMonitor.stopPeriodicReport();
performanceMonitor.printReport();
process.exit(0);
});
process.on('SIGTERM', () => {
performanceMonitor.stopPeriodicReport();
performanceMonitor.printReport();
process.exit(0);
});
}

View File

@@ -42,7 +42,7 @@ export class QQBasicInfoWrapper {
return this.isQuickUpdate ? this.QQVersionConfig?.buildId : this.QQPackageInfo?.buildVersion;
}
getFullQQVesion() {
getFullQQVersion() {
const version = this.isQuickUpdate ? this.QQVersionConfig?.curVersion : this.QQPackageInfo?.version;
if (!version) throw new Error('QQ版本获取失败');
return version;
@@ -57,9 +57,9 @@ export class QQBasicInfoWrapper {
//此方法不要直接使用
getQUAFallback() {
const platformMapping: Partial<Record<NodeJS.Platform, string>> = {
win32: `V1_WIN_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
darwin: `V1_MAC_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
linux: `V1_LNX_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
win32: `V1_WIN_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`,
darwin: `V1_MAC_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`,
linux: `V1_LNX_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`,
};
return platformMapping[systemPlatform] ?? (platformMapping.win32)!;
}
@@ -76,7 +76,7 @@ export class QQBasicInfoWrapper {
getAppidV2(): { appid: string; qua: string } {
// 通过已有表 性能好
const appidTbale = AppidTable as unknown as QQAppidTableType;
const fullVersion = this.getFullQQVesion();
const fullVersion = this.getFullQQVersion();
if (fullVersion) {
const data = appidTbale[fullVersion];
if (data) {

View File

@@ -1 +1 @@
export const napCatVersion = '4.7.64';
export const napCatVersion = '4.8.109';

View File

@@ -42,8 +42,8 @@ export class NTQQFileApi {
this.context = context;
this.core = core;
this.rkeyManager = new RkeyManager([
'https://secret-service.bietiaop.com/rkeys',
'http://ss.xingzhige.com/music_card/rkey',
'https://secret-service.bietiaop.com/rkeys',
],
this.context.logger
);
@@ -64,13 +64,13 @@ export class NTQQFileApi {
}
}
async getFileUrl(chatType: ChatType, peer: string, fileUUID?: string, file10MMd5?: string | undefined) {
if (this.core.apis.PacketApi.available) {
async getFileUrl(chatType: ChatType, peer: string, fileUUID?: string, file10MMd5?: string | undefined,timeout: number = 20000) {
if (this.core.apis.PacketApi.packetStatus) {
try {
if (chatType === ChatType.KCHATTYPEGROUP && fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetGroupFileUrl(+peer, fileUUID);
return this.core.apis.PacketApi.pkt.operation.GetGroupFileUrl(+peer, fileUUID, timeout);
} else if (file10MMd5 && fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetPrivateFileUrl(peer, fileUUID, file10MMd5);
return this.core.apis.PacketApi.pkt.operation.GetPrivateFileUrl(peer, fileUUID, file10MMd5, timeout);
}
} catch (error) {
this.context.logger.logError('获取文件URL失败', (error as Error).message);
@@ -79,8 +79,8 @@ export class NTQQFileApi {
throw new Error('fileUUID or file10MMd5 is undefined');
}
async getPttUrl(peer: string, fileUUID?: string) {
if (this.core.apis.PacketApi.available && fileUUID) {
async getPttUrl(peer: string, fileUUID?: string,timeout: number = 20000) {
if (this.core.apis.PacketApi.packetStatus && fileUUID) {
let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid;
try {
if (appid && appid === 1403) {
@@ -90,7 +90,7 @@ export class NTQQFileApi {
uploadTime: 0,
ttl: 0,
subType: 0,
});
}, timeout);
} else if (fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetPttUrl(peer, {
fileUuid: fileUUID,
@@ -98,7 +98,7 @@ export class NTQQFileApi {
uploadTime: 0,
ttl: 0,
subType: 0,
});
}, timeout);
}
} catch (error) {
this.context.logger.logError('获取文件URL失败', (error as Error).message);
@@ -107,8 +107,8 @@ export class NTQQFileApi {
throw new Error('packet cant get ptt url');
}
async getVideoUrlPacket(peer: string, fileUUID?: string) {
if (this.core.apis.PacketApi.available && fileUUID) {
async getVideoUrlPacket(peer: string, fileUUID?: string,timeout: number = 20000) {
if (this.core.apis.PacketApi.packetStatus && fileUUID) {
let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid;
try {
if (appid && appid === 1415) {
@@ -118,7 +118,7 @@ export class NTQQFileApi {
uploadTime: 0,
ttl: 0,
subType: 0,
});
}, timeout);
} else if (fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetVideoUrl(peer, {
fileUuid: fileUUID,
@@ -126,7 +126,7 @@ export class NTQQFileApi {
uploadTime: 0,
ttl: 0,
subType: 0,
});
}, timeout);
}
} catch (error) {
this.context.logger.logError('获取文件URL失败', (error as Error).message);
@@ -260,23 +260,22 @@ export class NTQQFileApi {
const thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
fs.mkdirSync(pathLib.dirname(thumbDir), { recursive: true });
const thumbPath = pathLib.join(pathLib.dirname(thumbDir), `${md5}_0.png`);
try {
videoInfo = await FFmpegService.getVideoInfo(filePath, thumbPath);
if (!fs.existsSync(thumbPath)) {
this.context.logger.logError('获取视频缩略图失败', new Error('缩略图不存在'));
throw new Error('获取视频缩略图失败');
}
} catch (e) {
this.context.logger.logError('获取视频信息失败', e);
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
}
if (_diyThumbPath) {
try {
await this.copyFile(_diyThumbPath, thumbPath);
} catch (e) {
this.context.logger.logError('复制自定义缩略图失败', e);
}
} else {
try {
videoInfo = await FFmpegService.getVideoInfo(filePath, thumbPath);
if (!fs.existsSync(thumbPath)) {
this.context.logger.logError('获取视频缩略图失败', new Error('缩略图不存在'));
throw new Error('获取视频缩略图失败');
}
} catch (e) {
this.context.logger.logError('获取视频信息失败', e);
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
}
}
context.deleteAfterSentFiles.push(thumbPath);
const thumbSize = (await fsPromises.stat(thumbPath)).size;
@@ -502,7 +501,7 @@ export class NTQQFileApi {
};
try {
if (this.core.apis.PacketApi.available) {
if (this.core.apis.PacketApi.packetStatus) {
const rkey_expired_private = !this.packetRkey || (this.packetRkey[0] && this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000);
const rkey_expired_group = !this.packetRkey || (this.packetRkey[0] && this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000);
if (rkey_expired_private || rkey_expired_group) {

View File

@@ -10,11 +10,14 @@ import {
GroupNotify,
GroupInfoSource,
ShutUpGroupMember,
Peer,
ChatType,
} from '@/core';
import { isNumeric, solveAsyncProblem } from '@/common/helper';
import { LimitedHashTable } from '@/common/message-unique';
import { NTEventWrapper } from '@/common/event';
import { CancelableTask, TaskExecutor } from '@/common/cancel-task';
import { createGroupDetailInfoV2Param, createGroupExtFilter, createGroupExtInfo } from '../data';
export class NTQQGroupApi {
context: InstanceContext;
@@ -47,6 +50,22 @@ export class NTQQGroupApi {
this.initCache().then().catch(e => this.context.logger.logError(e));
}
async createGrayTip(groupCode: string, tip: string) {
return this.context.session.getMsgService().addLocalJsonGrayTipMsg(
{
chatType: ChatType.KCHATTYPEGROUP,
peerUid: groupCode,
} as Peer,
{
busiId: 2201,
jsonStr: JSON.stringify({ "align": "center", "items": [{ "txt": tip, "type": "nor" }] }),
recentAbstract: tip,
isServer: false
},
true,
true
)
}
async initCache() {
for (const group of await this.getGroups(true)) {
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
@@ -95,6 +114,58 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().setHeader(groupCode, filePath);
}
// 0 0 无需管理员审核
// 0 2 需要管理员审核
// 1 2 禁止Bot入群( 最好只传一个1 )
async setGroupRobotAddOption(groupCode: string, robotMemberSwitch?: number, robotMemberExamine?: number) {
let extInfo = createGroupExtInfo(groupCode);
let groupExtFilter = createGroupExtFilter();
if (robotMemberSwitch !== undefined) {
extInfo.extInfo.inviteRobotMemberSwitch = robotMemberSwitch;
groupExtFilter.inviteRobotMemberSwitch = 1;
}
if (robotMemberExamine !== undefined) {
extInfo.extInfo.inviteRobotMemberExamine = robotMemberExamine;
groupExtFilter.inviteRobotMemberExamine = 1;
}
return this.context.session.getGroupService().modifyGroupExtInfoV2(extInfo, groupExtFilter);
}
async setGroupAddOption(groupCode: string, option: {
addOption: number;
groupQuestion?: string;
groupAnswer?: string;
}) {
let param = createGroupDetailInfoV2Param(groupCode);
// 设置要修改的目标
param.filter.addOption = 1;
if (option.addOption == 4 || option.addOption == 5) {
// 4 问题进入答案 5 问题管理员批准
param.filter.groupQuestion = 1;
param.filter.groupAnswer = option.addOption == 4 ? 1 : 0;
param.modifyInfo.groupQuestion = option.groupQuestion || '';
param.modifyInfo.groupAnswer = option.addOption == 4 ? option.groupAnswer || '' : '';
}
param.modifyInfo.addOption = option.addOption;
return this.context.session.getGroupService().modifyGroupDetailInfoV2(param, 0);
}
async setGroupSearch(groupCode: string, option: {
noCodeFingerOpenFlag?: number;
noFingerOpenFlag?: number;
}) {
let param = createGroupDetailInfoV2Param(groupCode);
if (option.noCodeFingerOpenFlag) {
param.filter.noCodeFingerOpenFlag = 1;
param.modifyInfo.noCodeFingerOpenFlag = option.noCodeFingerOpenFlag;
}
if (option.noFingerOpenFlag) {
param.filter.noFingerOpenFlag = 1;
param.modifyInfo.noFingerOpenFlag = option.noFingerOpenFlag;
}
return this.context.session.getGroupService().modifyGroupDetailInfoV2(param, 0);
}
async getGroups(forced: boolean = false) {
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getGroupList',

View File

@@ -142,7 +142,6 @@ export class NTQQMsgApi {
}
async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) {
console.log(peer, SendersUid);
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
@@ -192,7 +191,7 @@ export class NTQQMsgApi {
}
async recallMsg(peer: Peer, msgId: string) {
await this.core.eventWrapper.callNormalEventV2(
return await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelMsgService/recallMsg',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
[peer, [msgId]],

View File

@@ -21,20 +21,26 @@ export class NTQQPacketApi {
qqVersion: string | undefined;
pkt!: PacketClientSession;
errStack: string[] = [];
packetStatus: boolean = false;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
this.logger = core.context.logger;
}
async initApi() {
await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
.then()
this.packetStatus = (await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVersion())
.then((result) => {
return result;
})
.catch((err) => {
this.logger.logError(err);
this.errStack.push(err);
});
return false;
})) && this.pkt?.available;
}
get available(): boolean {
return this.pkt?.available ?? false;
}
@@ -61,6 +67,12 @@ export class NTQQPacketApi {
}
this.pkt = new PacketClientSession(this.core);
await this.pkt.init(process.pid, table.recv, table.send);
try {
await this.pkt.operation.FetchRkey(1500);
} catch (error) {
this.logger.logError('测试Packet状态异常', error);
return false;
}
return true;
}
}

View File

@@ -8,10 +8,11 @@ import {
WebHonorType,
} from '@/core';
import { NapCatCore } from '..';
import { readFileSync } from 'node:fs';
import { createReadStream, readFileSync, statSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { basename } from 'node:path';
import { qunAlbumControl } from '../data/webapi';
import { createAlbumCommentRequest, createAlbumFeedPublish, createAlbumMediaFeed } from '../data/album';
export class NTQQWebApi {
context: InstanceContext;
core: NapCatCore;
@@ -264,7 +265,7 @@ export class NTQQWebApi {
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
let HonorInfo = {
group_id: groupCode,
group_id: Number(groupCode),
current_talkative: {},
talkative_list: [],
performer_list: [],
@@ -323,64 +324,193 @@ export class NTQQWebApi {
}
return (hash & 0x7FFFFFFF).toString();
}
async createQunAlbumSession(gc: string, sAlbumID: string, sAlbumName: string, path: string, skey: string, pskey: string, uin: string) {
async getAlbumListByNTQQ(gc: string) {
return await this.context.session.getAlbumService().getAlbumList({
qun_id: gc,
attach_info: '',
seq: 3331,
request_time_line: {
request_invoke_time: "0"
}
})
}
async getAlbumList(gc: string) {
const skey = await this.core.apis.UserApi.getSKey() || '';
const pskey = (await this.core.apis.UserApi.getPSkey(['qzone.qq.com'])).domainPskeyMap.get('qzone.qq.com') || '';
const bkn = this.getBknFromSKey(skey);
const uin = this.core.selfInfo.uin || '10001';
const cookies = `p_uin=o${this.core.selfInfo.uin}; p_skey=${pskey}; skey=${skey}; uin=o${uin} `;
const api = `https://h5.qzone.qq.com/proxy/domain/u.photo.qzone.qq.com/cgi-bin/upp/qun_list_album_v2?`;
const params = new URLSearchParams({
random: '7570',
g_tk: bkn,
format: 'json',
inCharset: 'utf-8',
outCharset: 'utf-8',
qua: 'V1_IPH_SQ_6.2.0_0_HDBM_T',
cmd: 'qunGetAlbumList',
qunId: gc,
qunid: gc,
start: '0',
num: '1000',
uin: uin,
getMemberRole: '0'
});
const response = await RequestUtil.HttpGetJson<{ data: { album: Array<{ id: string, title: string }> } }>(api + params.toString(), 'GET', '', {
'Cookie': cookies
});
return response.data.album;
}
async createQunAlbumSession(gc: string, sAlbumID: string, sAlbumName: string, path: string, skey: string, pskey: string, img_md5: string, uin: string) {
const img = readFileSync(path);
const img_md5 = createHash('md5').update(img).digest('hex');
const img_size = img.length;
const img_name = basename(path);
const time = Math.floor(Date.now() / 1000);
const GTK = this.getBknFromSKey(pskey);
const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`;
const body = {
control_req: [{
uin: uin,
token: {
type: 4,
data: pskey,
appid: 5
},
appid: 'qun',
checksum: img_md5,
check_type: 0,
file_len: img_size,
env: {
refer: 'qzone',
deviceInfo: 'h5'
},
model: 0,
biz_req: {
sPicTitle: img_name,
sPicDesc: '',
sAlbumName: sAlbumName,
sAlbumID: sAlbumID,
iAlbumTypeID: 0,
iBitmap: 0,
iUploadType: 0,
iUpPicType: 0,
iBatchID: time,
sPicPath: '',
iPicWidth: 0,
iPicHight: 0,
iWaterType: 0,
iDistinctUse: 0,
iNeedFeeds: 1,
iUploadTime: time,
mapExt: {
appid: 'qun',
userid: gc
}
},
session: '',
asy_upload: 0,
cmd: 'FileUpload'
}]
};
const GTK = this.getBknFromSKey(skey);
const cookie = `p_uin=o${uin}; p_skey=${pskey}; skey=${skey}; uin=o${uin}`;
const body = qunAlbumControl({
uin,
group_id: gc,
pskey,
pic_md5: img_md5,
img_size,
img_name,
sAlbumName: sAlbumName,
sAlbumID: sAlbumID
});
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`;
const post = await RequestUtil.HttpGetJson(api, 'POST', body, {
const post = await RequestUtil.HttpGetJson<{ data: { session: string }, ret: number, msg: string }>(api, 'POST', body, {
'Cookie': cookie,
'Content-Type': 'application/json'
});
return post;
}
}
async uploadQunAlbumSlice(path: string, session: string, skey: string, pskey: string, uin: string, slice_size: number) {
const img_size = statSync(path).size;
let seq = 0;
let offset = 0;
const GTK = this.getBknFromSKey(skey);
const cookie = `p_uin=o${uin}; p_skey=${pskey}; skey=${skey}; uin=o${uin}`;
const stream = createReadStream(path, { highWaterMark: slice_size });
for await (const chunk of stream) {
const end = Math.min(offset + chunk.length, img_size);
const form = new FormData();
form.append('uin', uin);
form.append('appid', 'qun');
form.append('session', session);
form.append('offset', offset.toString());
form.append('data', new Blob([chunk], { type: 'application/octet-stream' }), 'blob');
form.append('checksum', '');
form.append('check_type', '0');
form.append('retry', '0');
form.append('seq', seq.toString());
form.append('end', end.toString());
form.append('cmd', 'FileUpload');
form.append('slice_size', slice_size.toString());
form.append('biz_req.iUploadType', '0');
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileUpload?seq=${seq}&retry=0&offset=${offset}&end=${end}&total=${img_size}&type=form&g_tk=${GTK}`;
const response = await fetch(api, {
method: 'POST',
headers: {
'Cookie': cookie,
},
body: form
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const post = await response.json() as { ret: number, msg: string }; if (post.ret !== 0) {
throw new Error(`分片 ${seq} 上传失败: ${post.msg}`);
}
offset += chunk.length;
seq++;
}
return { success: true, message: '上传完成' };
}
async uploadImageToQunAlbum(gc: string, sAlbumID: string, sAlbumName: string, path: string) {
const skey = await this.core.apis.UserApi.getSKey() || '';
const pskey = (await this.core.apis.UserApi.getPSkey(['qzone.qq.com'])).domainPskeyMap.get('qzone.qq.com') || '';
const img_md5 = createHash('md5').update(readFileSync(path)).digest('hex');
const uin = this.core.selfInfo.uin || '10001';
const session = (await this.createQunAlbumSession(gc, sAlbumID, sAlbumName, path, skey, pskey, img_md5, uin)).data.session;
if (!session) throw new Error('创建群相册会话失败');
await this.uploadQunAlbumSlice(path, session, skey, pskey, uin, 16384);
}
async getAlbumMediaListByNTQQ(gc: string, albumId: string, attach_info: string = '') {
return (await this.context.session.getAlbumService().getMediaList({
qun_id: gc,
attach_info: attach_info,
seq: 0,
request_time_line: {
request_invoke_time: "0"
},
album_id: albumId,
lloc: '',
batch_id: ''
})).response;
}
async doAlbumMediaPlainCommentByNTQQ(
qunId: string,
albumId: string,
lloc: string,
content: string) {
const random_seq = Math.floor(Math.random() * 9000) + 1000;
const uin = this.core.selfInfo.uin || '10001';
//16位number数字
const client_key = Date.now() * 1000
return await this.context.session.getAlbumService().doQunComment(
random_seq, {
map_info: [],
map_bytes_info: [],
map_user_account: []
},
qunId,
2,
createAlbumMediaFeed(uin, albumId, lloc),
createAlbumCommentRequest(uin, content, client_key)
);
}
async deleteAlbumMediaByNTQQ(
qunId: string,
albumId: string,
lloc: string) {
const random_seq = Math.floor(Math.random() * 9000) + 1000;
return await this.context.session.getAlbumService().deleteMedias(
random_seq,
qunId,
albumId,
[lloc],
[]
);
}
async doAlbumMediaLikeByNTQQ(
qunId: string,
albumId: string,
lloc: string,
id: string) {
const random_seq = Math.floor(Math.random() * 9000) + 1000;
const uin = this.core.selfInfo.uin || '10001';
return await this.context.session.getAlbumService().doQunLike(
random_seq, {
map_info: [],
map_bytes_info: [],
map_user_account: []
}, {
id: id,
status: 1
},
createAlbumFeedPublish(qunId, uin, albumId, lloc)
)
}
}

221
src/core/data/album.ts Normal file
View File

@@ -0,0 +1,221 @@
/**
* 群相册列表请求参数接口
*/
export interface AlbumListRequest {
qun_id: string;
attach_info: string;
seq: number;
request_time_line: {
request_invoke_time: string;
};
album_id: string;
lloc: string;
batch_id: string;
}
/**
* 创建群相册列表请求参数
* @param qunId 群号
* @param albumId 相册ID
* @param seq 请求序列号默认值为0
* @returns 请求参数对象
*/
export function createAlbumListRequest(
qunId: string,
albumId: string,
seq: number = 0
): AlbumListRequest {
return {
qun_id: qunId,
attach_info: "",
seq: seq,
request_time_line: {
request_invoke_time: "0"
},
album_id: albumId,
lloc: "",
batch_id: ""
};
}
/**
* 相册媒体项请求接口
*/
export interface AlbumMediaFeed {
cell_common: {
time: string;
};
cell_user_info: {
user: {
uin: string;
};
};
cell_media: {
album_id: string;
batch_id: string;
media_items: Array<{
image: {
lloc: string;
};
}>;
};
}
/**
* 创建相册媒体请求参数
* @param uin 用户QQ号
* @param albumId 相册ID
* @param lloc
* @returns 媒体请求参数对象
*/
export function createAlbumMediaFeed(
uin: string,
albumId: string,
lloc: string
): AlbumMediaFeed {
return {
cell_common: {
time: ""
},
cell_user_info: {
user: {
uin: uin
}
},
cell_media: {
album_id: albumId,
batch_id: "",
media_items: [{
image: {
lloc: lloc
}
}]
}
};
}
/**
* 相册评论内容接口
*/
export interface AlbumCommentContent {
type: number;
content: string;
who: number;
uid: string;
name: string;
url: string;
}
/**
* 相册评论请求接口
*/
export interface AlbumCommentReplyContent {
client_key: number;
content: AlbumCommentContent[];
user: {
uin: string;
};
}
export enum RichMsgType {
KRICHMSGTYPEPLAINTEXT,
KRICHMSGTYPEAT,
KRICHMSGTYPEURL,
KRICHMSGTYPEMEDIA
}
/**
* 创建相册评论请求参数
* @param uin 用户QQ号
* @param content 评论内容
* @param client_key 客户端鉴权密钥
* @returns 评论请求参数对象
*/
export function createAlbumCommentRequest(
uin: string,
content: string,
client_key: number
): AlbumCommentReplyContent {
return {
client_key: client_key,
//暂时只支持纯文本吧
content: [{
type: RichMsgType.KRICHMSGTYPEPLAINTEXT,
content: content,
who: 0,
uid: "",
name: "",
url: ""
}],
user: {
uin: uin
}
};
}
export interface AlbumFeedLikePublish {
cell_common: {
time: number;
feed_id: string;
};
cell_user_info: {
user: {
uin: string;
};
};
cell_media: {
album_id: string;
batch_id: number;
media_items: Array<{
type: number;
image: {
lloc: string;
sloc: string;
};
}>;
};
cell_qun_info: {
qun_id: string;
};
}
/**
* 创建相册动态发布请求参数
* @param qunId 群号
* @param uin 用户QQ号
* @param albumId 相册ID
* @param lloc 信息
* @param sloc 信息(可选默认与lloc相同)
* @returns 动态发布请求参数对象
*/
export function createAlbumFeedPublish(
qunId: string,
uin: string,
albumId: string,
lloc: string,
sloc?: string
): AlbumFeedLikePublish {
return {
cell_common: {
time: Date.now(),
feed_id: ""
},
cell_user_info: {
user: {
uin: uin
}
},
cell_media: {
album_id: albumId,
batch_id: 0,
media_items: [{
type: 0,
image: {
lloc: lloc,
sloc: sloc || lloc
}
}]
},
cell_qun_info: {
qun_id: qunId
}
};
}

245
src/core/data/group.ts Normal file
View File

@@ -0,0 +1,245 @@
import { GroupDetailInfoV2Param, GroupExtInfo, GroupExtFilter } from "../types";
export function createGroupDetailInfoV2Param(group_code: string): GroupDetailInfoV2Param {
return {
groupCode: group_code,
filter:
{
noCodeFingerOpenFlag: 0,
noFingerOpenFlag: 0,
groupName: 0,
classExt: 0,
classText: 0,
fingerMemo: 0,
richFingerMemo: 0,
tagRecord: 0,
groupGeoInfo:
{
ownerUid: 0,
setTime: 0,
cityId: 0,
longitude: 0,
latitude: 0,
geoContent: 0,
poiId: 0
},
groupExtAdminNum: 0,
flag: 0,
groupMemo: 0,
groupAioSkinUrl: 0,
groupBoardSkinUrl: 0,
groupCoverSkinUrl: 0,
groupGrade: 0,
activeMemberNum: 0,
certificationType: 0,
certificationText: 0,
groupNewGuideLines:
{
enabled: 0,
content: 0
},
groupFace: 0,
addOption: 0,
shutUpTime: 0,
groupTypeFlag: 0,
appPrivilegeFlag: 0,
appPrivilegeMask: 0,
groupExtOnly:
{
tribeId: 0,
moneyForAddGroup: 0
}, groupSecLevel: 0,
groupSecLevelInfo: 0,
subscriptionUin: 0,
subscriptionUid: "",
allowMemberInvite: 0,
groupQuestion: 0,
groupAnswer: 0,
groupFlagExt3: 0,
groupFlagExt3Mask: 0,
groupOpenAppid: 0,
rootId: 0,
msgLimitFrequency: 0,
hlGuildAppid: 0,
hlGuildSubType: 0,
hlGuildOrgId: 0,
groupFlagExt4: 0,
groupFlagExt4Mask: 0,
groupSchoolInfo: {
location: 0,
grade: 0,
school: 0
},
groupCardPrefix:
{
introduction: 0,
rptPrefix: 0
}, allianceId: 0,
groupFlagPro1: 0,
groupFlagPro1Mask: 0
},
modifyInfo: {
noCodeFingerOpenFlag: 0,
noFingerOpenFlag: 0,
groupName: "",
classExt: 0,
classText: "",
fingerMemo: "",
richFingerMemo: "",
tagRecord: [],
groupGeoInfo: {
ownerUid: "",
SetTime: 0,
CityId: 0,
Longitude: "",
Latitude: "",
GeoContent: "",
poiId: ""
},
groupExtAdminNum: 0,
flag: 0,
groupMemo: "",
groupAioSkinUrl: "",
groupBoardSkinUrl: "",
groupCoverSkinUrl: "",
groupGrade: 0,
activeMemberNum: 0,
certificationType: 0,
certificationText: "",
groupNewGuideLines: {
enabled: false,
content: ""
}, groupFace: 0,
addOption: 0,
shutUpTime: 0,
groupTypeFlag: 0,
appPrivilegeFlag: 0,
appPrivilegeMask: 0,
groupExtOnly: {
tribeId: 0,
moneyForAddGroup: 0
},
groupSecLevel: 0,
groupSecLevelInfo: 0,
subscriptionUin: "",
subscriptionUid: "",
allowMemberInvite: 0,
groupQuestion: "",
groupAnswer: "",
groupFlagExt3: 0,
groupFlagExt3Mask: 0,
groupOpenAppid: 0,
rootId: "",
msgLimitFrequency: 0,
hlGuildAppid: 0,
hlGuildSubType: 0,
hlGuildOrgId: 0,
groupFlagExt4: 0,
groupFlagExt4Mask: 0,
groupSchoolInfo: {
location: "",
grade: 0,
school: ""
},
groupCardPrefix:
{
introduction: "",
rptPrefix: []
},
allianceId: "",
groupFlagPro1: 0,
groupFlagPro1Mask: 0
}
}
}
export function createGroupExtInfo(group_code: string): GroupExtInfo {
return {
groupCode: group_code,
resultCode: 0,
extInfo: {
groupInfoExtSeq: 0,
reserve: 0,
luckyWordId: '',
lightCharNum: 0,
luckyWord: '',
starId: 0,
essentialMsgSwitch: 0,
todoSeq: 0,
blacklistExpireTime: 0,
isLimitGroupRtc: 0,
companyId: 0,
hasGroupCustomPortrait: 0,
bindGuildId: '',
groupOwnerId: {
memberUin: '',
memberUid: '',
memberQid: '',
},
essentialMsgPrivilege: 0,
msgEventSeq: '',
inviteRobotSwitch: 0,
gangUpId: '',
qqMusicMedalSwitch: 0,
showPlayTogetherSwitch: 0,
groupFlagPro1: '',
groupBindGuildIds: {
guildIds: [],
},
viewedMsgDisappearTime: '',
groupExtFlameData: {
switchState: 0,
state: 0,
dayNums: [],
version: 0,
updateTime: '',
isDisplayDayNum: false,
},
groupBindGuildSwitch: 0,
groupAioBindGuildId: '',
groupExcludeGuildIds: {
guildIds: [],
},
fullGroupExpansionSwitch: 0,
fullGroupExpansionSeq: '',
inviteRobotMemberSwitch: 0,
inviteRobotMemberExamine: 0,
groupSquareSwitch: 0,
}
}
}
export function createGroupExtFilter(): GroupExtFilter {
return {
groupInfoExtSeq: 0,
reserve: 0,
luckyWordId: 0,
lightCharNum: 0,
luckyWord: 0,
starId: 0,
essentialMsgSwitch: 0,
todoSeq: 0,
blacklistExpireTime: 0,
isLimitGroupRtc: 0,
companyId: 0,
hasGroupCustomPortrait: 0,
bindGuildId: 0,
groupOwnerId: 0,
essentialMsgPrivilege: 0,
msgEventSeq: 0,
inviteRobotSwitch: 0,
gangUpId: 0,
qqMusicMedalSwitch: 0,
showPlayTogetherSwitch: 0,
groupFlagPro1: 0,
groupBindGuildIds: 0,
viewedMsgDisappearTime: 0,
groupExtFlameData: 0,
groupBindGuildSwitch: 0,
groupAioBindGuildId: 0,
groupExcludeGuildIds: 0,
fullGroupExpansionSwitch: 0,
fullGroupExpansionSeq: 0,
inviteRobotMemberSwitch: 0,
inviteRobotMemberExamine: 0,
groupSquareSwitch: 0,
}
};

1
src/core/data/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from "./group";

177
src/core/data/webapi.ts Normal file
View File

@@ -0,0 +1,177 @@
export interface ControlReq {
appid?: string;
asy_upload?: number;
biz_req?: BizReq;
check_type?: number;
checksum?: string;
cmd?: string;
env?: Env;
file_len?: number;
model?: number;
session?: string;
token?: Token;
uin?: string;
}
export interface BizReq {
iAlbumTypeID: number;
iBatchID: number;
iBitmap: number;
iDistinctUse: number;
iNeedFeeds: number;
iPicHight: number;
iPicWidth: number;
iUploadTime: number;
iUploadType: number;
iUpPicType: number;
iWaterType: number;
mapExt: MapExt;
sAlbumID: string;
sAlbumName: string;
sPicDesc: string;
sPicPath: string;
sPicTitle: string;
stExtendInfo: StExtendInfo;
}
export interface MapExt {
appid: string;
userid: string;
}
export interface StExtendInfo {
mapParams: MapParams;
}
export interface MapParams {
batch_num: string;
photo_num: string;
video_num: string;
}
export interface Env {
deviceInfo: string;
refer: string;
}
export interface Token {
appid: number;
data: string;
type: number;
}
export function qunAlbumControl({
uin,
group_id,
pskey,
pic_md5,
img_size,
img_name,
sAlbumName,
sAlbumID,
photo_num = "1",
video_num = "0",
batch_num = "1"
}: {
uin: string,
group_id: string,
pskey: string,
pic_md5: string,
img_size: number,
img_name: string,
sAlbumName: string,
sAlbumID: string,
photo_num?: string,
video_num?: string,
batch_num?: string
}
): {
control_req: ControlReq[]
} {
const timestamp = Math.floor(Date.now() / 1000);
return {
control_req: [
{
uin: uin,
token: {
type: 4,
data: pskey,
appid: 5
},
appid: "qun",
checksum: pic_md5,
check_type: 0,
file_len: img_size,
env: {
refer: "qzone",
deviceInfo: "h5"
},
model: 0,
biz_req: {
sPicTitle: img_name,
sPicDesc: "",
sAlbumName: sAlbumName,
sAlbumID: sAlbumID,
iAlbumTypeID: 0,
iBitmap: 0,
iUploadType: 0,
iUpPicType: 0,
iBatchID: timestamp,
sPicPath: "",
iPicWidth: 0,
iPicHight: 0,
iWaterType: 0,
iDistinctUse: 0,
iNeedFeeds: 1,
iUploadTime: timestamp,
mapExt: {
appid: "qun",
userid: group_id
},
stExtendInfo: {
mapParams: {
photo_num: photo_num,
video_num: video_num,
batch_num: batch_num
}
}
},
session: "",
asy_upload: 0,
cmd: "FileUpload"
}]
}
}
export function createStreamUpload(
{
uin,
session,
offset,
seq,
end,
slice_size,
data
}: { uin: string, session: string, offset: number, seq: number, end: number, slice_size: number, data: string }
) {
return {
uin: uin,
appid: "qun",
session: session,
offset: offset,//分片起始位置
data: data,//base64编码数据
checksum: "",
check_type: 0,
retry: 0,//重试次数
seq: seq,//分片序号
end: end,//分片结束位置 文件总大小
cmd: "FileUpload",
slice_size: slice_size,//分片大小16KB 16384
biz_req: {
iUploadType: 3
}
};
}

View File

@@ -294,5 +294,97 @@
"9.9.19-35184": {
"appid": 537291048,
"qua": "V1_WIN_NQ_9.9.19_35184_GW_B"
},
"3.2.17-35341": {
"appid": 537291383,
"qua": "V1_LNX_NQ_3.2.17_35341_GW_B"
},
"9.9.19-35341": {
"appid": 537291347,
"qua": "V1_WIN_NQ_9.9.19_35341_GW_B"
},
"9.9.19-35469": {
"appid": 537291398,
"qua": "V1_WIN_NQ_9.9.19_35469_GW_B"
},
"3.2.18-35951": {
"appid": 537296013,
"qua": "V1_LNX_NQ_3.2.18_35951_GW_B"
},
"9.9.20-35951": {
"appid": 537295977,
"qua": "V1_WIN_NQ_9.9.20_35951_GW_B"
},
"3.2.18-36580": {
"appid": 537298509,
"qua": "V1_LNX_NQ_3.2.18_36580_GW_B"
},
"9.9.20-36580": {
"appid": 537298473,
"qua": "V1_WIN_NQ_9.9.20_36580_GW_B"
},
"9.9.20-37012": {
"appid": 537304071,
"qua": "V1_WIN_NQ_9.9.20_37012_GW_B"
},
"3.2.18-37012": {
"appid": 537304107,
"qua": "V1_LNX_NQ_3.2.18_37012_GW_B"
},
"3.2.18-37051": {
"appid": 537304158,
"qua": "V1_LNX_NQ_3.2.18_37051_GW_B"
},
"9.9.20-37051": {
"appid": 537304122,
"qua": "V1_WIN_NQ_9.9.20_37051_GW_B"
},
"9.9.20-37475": {
"appid": 537304173,
"qua": "V1_WIN_NQ_9.9.20_37475_GW_B"
},
"3.2.18-37475": {
"appid": 537304210,
"qua": "V1_LNX_NQ_3.2.18_37475_GW_B"
},
"9.9.20-37625": {
"appid": 537304224,
"qua": "V1_WIN_NQ_9.9.20_37625_GW_B"
},
"3.2.18-37625": {
"appid": 537304261,
"qua": "V1_LNX_NQ_3.2.18_37625_GW_B"
},
"9.9.21-38503": {
"appid": 537307604,
"qua": "V1_WIN_NQ_9.9.21_38503_GW_B"
},
"3.2.19-38503": {
"appid": 537307640,
"qua": "V1_LNX_NQ_3.2.19_38503_GW_B"
},
"3.2.19-38626": {
"appid": 537307691,
"qua": "V1_LNX_NQ_3.2.19_38626_GW_B"
},
"9.9.21-38711": {
"appid": 537307655,
"qua": "V1_WIN_NQ_9.9.21_38626_GW_B"
},
"9.9.21-38960": {
"appid": 537313855,
"qua": "V1_WIN_NQ_9.9.21_38960_GW_B"
},
"3.2.19-38960": {
"appid": 537313891,
"qua": "V1_LNX_NQ_3.2.19_38960_GW_B"
},
"3.2.19-39038": {
"appid": 537313942,
"qua": "V1_LNX_NQ_3.2.19_39038_GW_B"
},
"9.9.21-39038": {
"appid": 537313906,
"qua": "V1_WIN_NQ_9.9.21_39038_GW_B"
}
}

View File

@@ -378,5 +378,137 @@
"9.9.19-35184-x64": {
"send": "3BE5A10",
"recv": "3BEA210"
},
"9.9.19-35341-x64": {
"send": "3BF1D50",
"recv": "3BF6550"
},
"9.9.19-35469-x64": {
"send": "3BF1D50",
"recv": "3BF6550"
},
"3.2.17-35341-x64": {
"send": "AE2F700",
"recv": "AE33120"
},
"3.2.17-35341-arm64": {
"send": "778D840",
"recv": "7791170"
},
"9.9.20-35951-x64": {
"send": "3034BAC",
"recv": "3038354"
},
"3.2.18-35951-x64": {
"send": "AFBBB00",
"recv": "AFBF520"
},
"9.9.20-36580-x64": {
"send": "30824B8",
"recv": "3085C5C"
},
"3.2.18-36580-x64": {
"send": "B0853E0",
"recv": "B088E60"
},
"3.2.18-36580-arm64": {
"send": "793DAC8",
"recv": "7941458"
},
"3.2.18-37012-x64": {
"send": "B20F960",
"recv": "B2133E0"
},
"3.2.18-37012-arm64": {
"send": "7A19E00",
"recv": "7A1D790"
},
"9.9.20-37012-x64": {
"send": "30CC958",
"recv": "30D00FC"
},
"3.2.18-37051-x64": {
"send": "B20F960",
"recv": "B2133E0"
},
"3.2.18-37051-arm64": {
"send": "7A19E00",
"recv": "7A1D790"
},
"9.9.20-37051-x64": {
"send": "30CC958",
"recv": "30D00FC"
},
"9.9.20-37475-x64": {
"send": "30D30D8",
"recv": "30D687C"
},
"3.2.18-37475-x64": {
"send": "B238EC0",
"recv": "B23C940"
},
"3.2.18-37475-arm64": {
"send": "7A34B38",
"recv": "7A384C8"
},
"9.9.20-37625-x64": {
"send": "30D39D8",
"recv": "30D717C"
},
"3.2.18-37625-x64": {
"send": "B2397E0",
"recv": "B23D260"
},
"3.2.18-37625-arm64": {
"send": "7A350D8",
"recv": "7A38A68"
},
"9.9.21-38503-x64": {
"send": "3105F38",
"recv": "31096DC"
},
"3.2.19-38503-x64": {
"send": "B2C1A60",
"recv": "B2C54E0"
},
"3.2.19-38626-x64": {
"send": "B2C1BE0",
"recv": "B2C5660"
},
"9.9.21-38626-x64": {
"send": "310A758",
"recv": "310DEFC"
},
"3.2.19-38626-arm64": {
"send": "7A8A490",
"recv": "7A8DE20"
},
"9.9.21-38711-x64": {
"send": "310A758",
"recv": "310DEFC"
},
"3.2.19-38960-x64": {
"send": "B3740E0",
"recv": "B377B60"
},
"9.9.21-38960-x64": {
"send": "313F7D8",
"recv": "3142F7C"
},
"3.2.19-38960-arm64": {
"send": "7B01D98",
"recv": "7B05728"
},
"3.2.19-39038-x64": {
"send": "B3759E0",
"recv": "B379460"
},
"3.2.19-39038-arm64": {
"send": "7B025C8",
"recv": "7B05F58"
},
"9.9.21-39038-x64": {
"send": "313FB58",
"recv": "31432FC"
}
}

View File

@@ -40,7 +40,6 @@ export class NodeIKernelBuddyListener {
}
onDelBatchBuddyInfos(_arg: unknown): any {
console.log('onDelBatchBuddyInfos not implemented', ...arguments);
}
onDoubtBuddyReqChange(_arg:

View File

@@ -1,4 +1,3 @@
import { LRUCache } from '@/common/lru-cache';
import crypto, { createHash } from 'crypto';
import { OidbPacket, PacketHexStr } from '@/core/packet/transformer/base';
import { LogStack } from '@/core/packet/context/clientContext';
@@ -7,7 +6,6 @@ import { PacketLogger } from '@/core/packet/context/loggerContext';
export interface RecvPacket {
type: string, // 仅recv
trace_id_md5?: string,
data: RecvPacketData
}
@@ -30,7 +28,7 @@ function randText(len: number): string {
export abstract class IPacketClient {
protected readonly napcore: NapCoreContext;
protected readonly logger: PacketLogger;
protected readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
protected readonly cb = new Map<string, (json: RecvPacketData) => Promise<any> | any>(); // hash-type callback
logStack: LogStack;
available: boolean = false;
@@ -44,24 +42,21 @@ export abstract class IPacketClient {
abstract init(pid: number, recv: string, send: string): Promise<void>;
abstract sendCommandImpl(cmd: string, data: string, trace_id: string): void;
abstract sendCommandImpl(cmd: string, data: string, hash: string, timeout: number): void;
private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise<void>): Promise<void> {
this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback);
}
private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => {
private async sendCommand(cmd: string, data: string, trace_data: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => {
}): Promise<RecvPacketData> {
return new Promise<RecvPacketData>((resolve, reject) => {
if (!this.available) {
reject(new Error('packetBackend 当前不可用!'));
}
let hash = createHash('md5').update(trace_data).digest('hex');
const timeoutHandle = setTimeout(() => {
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`));
this.cb.delete(hash + 'send');
this.cb.delete(hash + 'recv');
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with hash ${hash}`));
}, timeout);
this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => {
this.cb.set(hash + 'send', async (json: RecvPacketData) => {
sendcb(json);
if (!rsp) {
clearTimeout(timeoutHandle);
@@ -70,25 +65,24 @@ export abstract class IPacketClient {
});
if (rsp) {
this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => {
this.cb.set(hash + 'recv', async (json: RecvPacketData) => {
clearTimeout(timeoutHandle);
resolve(json);
});
}
this.sendCommandImpl(cmd, data, trace_id);
this.sendCommandImpl(cmd, data, hash, timeout);
});
}
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
async sendPacket(cmd: string, data: PacketHexStr, rsp = false, timeout = 20000): Promise<RecvPacketData> {
const md5 = crypto.createHash('md5').update(data).digest('hex');
const trace_id = (randText(4) + md5 + data).slice(0, data.length / 2);
return this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
await this.napcore.sendSsoCmdReqByContend(cmd, trace_id);
const trace_data = (randText(4) + md5 + data).slice(0, data.length / 2);// trace_data
return this.sendCommand(cmd, data, trace_data, rsp, timeout, async () => {
await this.napcore.sendSsoCmdReqByContend(cmd, trace_data);
});
}
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
return this.sendPacket(pkt.cmd, pkt.data, rsp);
async sendOidbPacket(pkt: OidbPacket, rsp = false, timeout = 20000): Promise<RecvPacketData> {
return this.sendPacket(pkt.cmd, pkt.data, rsp, timeout);
}
}

View File

@@ -4,7 +4,6 @@ import { fileURLToPath } from 'url';
import fs from 'fs';
import { IPacketClient } from '@/core/packet/client/baseClient';
import { constants } from 'node:os';
import { LRUCache } from '@/common/lru-cache';
import { LogStack } from '@/core/packet/context/clientContext';
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
import { PacketLogger } from '@/core/packet/context/loggerContext';
@@ -18,7 +17,8 @@ export interface NativePacketExportType {
export class NativePacketClient extends IPacketClient {
private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64', 'darwin.x64', 'darwin.arm64'];
private readonly MoeHooExport: { exports: NativePacketExportType } = { exports: {} };
private readonly sendEvent = new LRUCache<number, string>(500); // seq->trace_id
private readonly sendEvent = new Map<number, string>(); // seq - hash
private readonly timeEvent = new Map<string, NodeJS.Timeout>(); // hash - timeout
constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
super(napCore, logger, logStack);
@@ -40,29 +40,36 @@ export class NativePacketClient extends IPacketClient {
async init(_pid: number, recv: string, send: string): Promise<void> {
const platform = process.platform + '.' + process.arch;
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node');
const isNewQQ = this.napcore.basicInfo.requireMinNTQQBuild("36580");
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + (isNewQQ ? '.new' : '') + '.node');
process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY);
this.MoeHooExport.exports.InitHook?.(send, recv, (type: number, _uin: string, cmd: string, seq: number, hex_data: string) => {
const trace_id = createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex');
if (type === 0 && this.cb.get(trace_id + 'recv')) {
const hash = createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex');
if (type === 0 && this.cb.get(hash + 'recv')) {
//此时为send 提取seq
this.sendEvent.put(seq, trace_id);
this.sendEvent.set(seq, hash);
setTimeout(() => {
this.sendEvent.delete(seq);
this.timeEvent.delete(hash);
}, +(this.timeEvent.get(hash) ?? 20000));
//正式send完成 无recv v
//均无异常 v
}
if (type === 1 && this.sendEvent.get(seq)) {
//此时为recv 调用callback
const trace_id = this.sendEvent.get(seq);
const callback = this.cb.get(trace_id + 'recv');
// console.log('callback:', callback, trace_id);
const hash = this.sendEvent.get(seq);
const callback = this.cb.get(hash + 'recv');
callback?.({ seq, cmd, hex_data });
}
}, this.napcore.config.o3HookMode == 1);
this.available = true;
}
sendCommandImpl(cmd: string, data: string, trace_id: string): void {
const trace_id_md5 = createHash('md5').update(trace_id).digest('hex');
this.MoeHooExport.exports.SendPacket?.(cmd, data, trace_id_md5);
this.cb.get(trace_id_md5 + 'send')?.({ seq: 0, cmd, hex_data: '' });
sendCommandImpl(cmd: string, data: string, hash: string, timeout: number): void {
this.timeEvent.set(hash, setTimeout(() => {
this.timeEvent.delete(hash);//考虑情况为正式send都没进
}, timeout));
this.MoeHooExport.exports.SendPacket?.(cmd, data, hash);
this.cb.get(hash + 'send')?.({ seq: 0, cmd, hex_data: '' });
}
}

View File

@@ -73,8 +73,8 @@ export class PacketClientContext {
await this._client.init(pid, recv, send);
}
async sendOidbPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
const raw = await this._client.sendOidbPacket(pkt, rsp);
async sendOidbPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T, timeout?: number): Promise<T extends true ? Buffer : void> {
const raw = await this._client.sendOidbPacket(pkt, rsp, timeout);
return (rsp ? Buffer.from(raw.hex_data, 'hex') : undefined) as T extends true ? Buffer : void;
}

View File

@@ -1,6 +1,7 @@
import { NapCatCore } from '@/core';
export interface NapCoreCompatBasicInfo {
readonly requireMinNTQQBuild: (buildVer: string) => boolean;
readonly uin: number;
readonly uid: string;
readonly uin2uid: (uin: number) => Promise<string>;
@@ -21,6 +22,7 @@ export class NapCoreContext {
get basicInfo() {
return {
requireMinNTQQBuild: (buildVer: string) => this.core.context.basicInfoWrapper.requireMinNTQQBuild(buildVer),
uin: +this.core.selfInfo.uin,
uid: this.core.selfInfo.uid,
uin2uid: (uin: number) => this.core.apis.UserApi.getUidByUinV2(String(uin)).then(res => res ?? ''),

View File

@@ -34,10 +34,13 @@ export class PacketOperationContext {
const req = trans.SendPoke.build(is_group, peer, target ?? peer);
await this.context.client.sendOidbPacket(req);
}
async FetchRkey() {
async SetGroupTodo(groupUin: number, msgSeq: string) {
const req = trans.SetGroupTodo.build(groupUin, msgSeq);
await this.context.client.sendOidbPacket(req, true);
}
async FetchRkey(timeout: number = 10000) {
const req = trans.FetchRkey.build();
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.FetchRkey.parse(resp);
return res.data.rkeyList;
}
@@ -119,37 +122,37 @@ export class PacketOperationContext {
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetPttUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
async GetPttUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>,timeout: number = 20000) {
const req = trans.DownloadPtt.build(selfUid, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadPtt.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetVideoUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
async GetVideoUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>, timeout: number = 20000) {
const req = trans.DownloadVideo.build(selfUid, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadVideo.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>, timeout: number = 20000) {
const req = trans.DownloadGroupImage.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>, timeout: number = 20000) {
const req = trans.DownloadGroupPtt.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupVideoUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
async GetGroupVideoUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>, timeout: number = 20000) {
const req = trans.DownloadGroupVideo.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
@@ -240,16 +243,16 @@ export class PacketOperationContext {
return res.rename.retCode;
}
async GetGroupFileUrl(groupUin: number, fileUUID: string) {
async GetGroupFileUrl(groupUin: number, fileUUID: string,timeout: number = 20000) {
const req = trans.DownloadGroupFile.build(groupUin, fileUUID);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadGroupFile.parse(resp);
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
}
async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string) {
async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string, timeout: number = 20000) {
const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadPrivateFile.parse(resp);
return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`;
}

View File

@@ -0,0 +1,24 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
class SetGroupTodo extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
constructor() {
super();
}
build(peer: number, msgSeq: string): OidbPacket {
const data = new NapProtoMsg(proto.OidbSvcTrpcTcp0XF90_1).encode({
groupUin: peer,
msgSeq: BigInt(msgSeq)
});
return OidbBase.build(0xF90, 1, data);
}
parse(data: Buffer) {
return OidbBase.parse(data);
}
}
export default new SetGroupTodo();

View File

@@ -8,3 +8,4 @@ export { default as SetSpecialTitle } from './SetSpecialTitle';
export { default as ImageOCR } from './ImageOCR';
export { default as MoveGroupFile } from './MoveGroupFile';
export { default as RenameGroupFile } from './RenameGroupFile';
export { default as SetGroupTodo } from './SetGroupTodo';

View File

@@ -30,3 +30,4 @@ export * from './oidb/Oidb.0xED3_1';
export * from './oidb/Oidb.0XFE1_2';
export * from './oidb/OidbBase';
export * from './oidb/Oidb.0xE07';
export * from './oidb/Oidb.0xf90_1';

View File

@@ -0,0 +1,6 @@
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcp0XF90_1 = {
groupUin: ProtoField(1, ScalarType.UINT32),
msgSeq: ProtoField(2, ScalarType.UINT64),
};

View File

@@ -1,22 +1,52 @@
import { AlbumCommentReplyContent, AlbumFeedLikePublish, AlbumListRequest, AlbumMediaFeed } from "../data/album";
export interface NodeIKernelAlbumService {
setAlbumServiceInfo(...args: unknown[]): unknown;// needs 3 arguments
getMainPage(...args: unknown[]): unknown;// needs 2 arguments
getAlbumList(...args: unknown[]): unknown;// needs 1 arguments
getAlbumList(params: {
qun_id: string,
attach_info: string,
seq: number,
request_time_line: {
request_invoke_time: string
}
}): Promise<{
response: {
seq: number,
result: number,
errMs: string,//没错就是errMs不是errMsg
trace_id: string,
is_from_cache: boolean,
request_time_line: unknown,
album_list: Array<{ name: string, album_id: string }>,
attach_info: string,
has_more: boolean,
right: unknown,
banner: unknown
}
}>
getAlbumInfo(...args: unknown[]): unknown;// needs 1 arguments
deleteAlbum(...args: unknown[]): unknown;// needs 3 arguments
addAlbum(...args: unknown[]): unknown;// needs 2 arguments
deleteMedias(...args: unknown[]): unknown;// needs 4 arguments
deleteMedias(seq: number, group_code: string, album_id: string, media_ids: string[], ban_ids: unknown[]): Promise<unknown>;// needs 4 arguments
modifyAlbum(...args: unknown[]): unknown;// needs 3 arguments
getMediaList(...args: unknown[]): unknown;// needs 1 arguments
getMediaList(param: AlbumListRequest): Promise<{
response: {
seq: number,
result: number,
errMs: string,//没错就是errMs不是errMsg
trace_id: string,
request_time_line: unknown,
}
}>;// needs 1 arguments
quoteToQzone(...args: unknown[]): unknown;// needs 1 arguments
@@ -35,12 +65,36 @@ export interface NodeIKernelAlbumService {
getQunLikes(...args: unknown[]): unknown;// needs 4 arguments
deleteQunFeed(...args: unknown[]): unknown;// needs 1 arguments
doQunComment(...args: unknown[]): unknown;// needs 6 arguments
//seq random
//stCommonExt {"map_info":[],"map_bytes_info":[],"map_user_account":[]}
//qunId string
doQunComment(seq: number, ext: {
map_info: unknown[],
map_bytes_info: unknown[],
map_user_account: unknown[]
},
qunId: string,
commentType: number,
feed: AlbumMediaFeed,
content: AlbumCommentReplyContent,
): Promise<unknown>;// needs 6 arguments
doQunReply(...args: unknown[]): unknown;// needs 7 arguments
doQunLike(...args: unknown[]): unknown;// needs 5 arguments
doQunLike(
seq: number,
ext: {
map_info: unknown[],
map_bytes_info: unknown[],
map_user_account: unknown[]
},
param: {
//{"id":"421_1_0_1012959257|V61Yiali4PELg90bThrH4Bo2iI1M5Kab|V5bCgAxMDEyOTU5MjU3e*KqaLVYdic!^||^421_1_0_1012959257|V61Yiali4PELg90bThrH4Bo2iI1M5Kab|17560336594^||^1","status":1}
id: string,
status: number
},
like: AlbumFeedLikePublish
): Promise<unknown>;// needs 5 arguments
getRedPoints(...args: unknown[]): unknown;// needs 3 arguments

View File

@@ -8,10 +8,22 @@ import {
GroupNotifyMsgType,
NTGroupRequestOperateTypes,
KickMemberV2Req,
GroupDetailInfoV2Param,
GroupExtInfo,
GroupExtFilter,
} from '@/core/types';
import { GeneralCallResult } from '@/core/services/common';
export interface NodeIKernelGroupService {
modifyGroupExtInfoV2(groupExtInfo: GroupExtInfo, groupExtFilter: GroupExtFilter): Promise<GeneralCallResult &
{
result: {
groupCode: string,
result: number
}
}>;
// --->
// 待启用 For Next Version 3.2.0
// isTroopMember ? 0 : 111
@@ -169,6 +181,9 @@ export interface NodeIKernelGroupService {
modifyGroupDetailInfo(groupCode: string, arg: unknown): void;
// 第二个参数在大多数情况为0 设置群成员权限 例如上传群文件权限和群成员付费/加入邀请加入时为8
modifyGroupDetailInfoV2(param: GroupDetailInfoV2Param, arg: number): Promise<GeneralCallResult>;
setGroupMsgMask(groupCode: string, arg: unknown): void;
changeGroupShieldSettingTemp(groupCode: string, arg: unknown): void;

View File

@@ -58,6 +58,7 @@ export interface GrayTipRovokeElement {
operatorUid: string;
operatorNick: string;
operatorRemark: string;
isSelfOperate: boolean; // 是否是自己撤回的
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
}

View File

@@ -1,4 +1,97 @@
import { QQLevel, NTSex } from './user';
export interface GroupExtInfo {
groupCode: string;
resultCode: number;
extInfo: EXTInfo;
}
export interface GroupExtFilter {
groupInfoExtSeq: number;
reserve: number;
luckyWordId: number;
lightCharNum: number;
luckyWord: number;
starId: number;
essentialMsgSwitch: number;
todoSeq: number;
blacklistExpireTime: number;
isLimitGroupRtc: number;
companyId: number;
hasGroupCustomPortrait: number;
bindGuildId: number;
groupOwnerId: number;
essentialMsgPrivilege: number;
msgEventSeq: number;
inviteRobotSwitch: number;
gangUpId: number;
qqMusicMedalSwitch: number;
showPlayTogetherSwitch: number;
groupFlagPro1: number;
groupBindGuildIds: number;
viewedMsgDisappearTime: number;
groupExtFlameData: number;
groupBindGuildSwitch: number;
groupAioBindGuildId: number;
groupExcludeGuildIds: number;
fullGroupExpansionSwitch: number;
fullGroupExpansionSeq: number;
inviteRobotMemberSwitch: number;
inviteRobotMemberExamine: number;
groupSquareSwitch: number;
};
export interface EXTInfo {
groupInfoExtSeq: number;
reserve: number;
luckyWordId: string;
lightCharNum: number;
luckyWord: string;
starId: number;
essentialMsgSwitch: number;
todoSeq: number;
blacklistExpireTime: number;
isLimitGroupRtc: number;
companyId: number;
hasGroupCustomPortrait: number;
bindGuildId: string;
groupOwnerId: GroupOwnerID;
essentialMsgPrivilege: number;
msgEventSeq: string;
inviteRobotSwitch: number;
gangUpId: string;
qqMusicMedalSwitch: number;
showPlayTogetherSwitch: number;
groupFlagPro1: string;
groupBindGuildIds: GroupGuildIDS;
viewedMsgDisappearTime: string;
groupExtFlameData: GroupEXTFlameData;
groupBindGuildSwitch: number;
groupAioBindGuildId: string;
groupExcludeGuildIds: GroupGuildIDS;
fullGroupExpansionSwitch: number;
fullGroupExpansionSeq: string;
inviteRobotMemberSwitch: number;
inviteRobotMemberExamine: number;
groupSquareSwitch: number;
}
export interface GroupGuildIDS {
guildIds: any[];
}
export interface GroupEXTFlameData {
switchState: number;
state: number;
dayNums: any[];
version: number;
updateTime: string;
isDisplayDayNum: boolean;
}
export interface GroupOwnerID {
memberUin: string;
memberUid: string;
memberQid: string;
}
export interface KickMemberInfo {
optFlag: number;
@@ -7,6 +100,185 @@ export interface KickMemberInfo {
optBytesMsg: string;
}
export interface GroupDetailInfoV2Param {
groupCode: string;
filter: Filter;
modifyInfo: ModifyInfo;
}
export interface Filter {
noCodeFingerOpenFlag: number;
noFingerOpenFlag: number;
groupName: number;
classExt: number;
classText: number;
fingerMemo: number;
richFingerMemo: number;
tagRecord: number;
groupGeoInfo: FilterGroupGeoInfo;
groupExtAdminNum: number;
flag: number;
groupMemo: number;
groupAioSkinUrl: number;
groupBoardSkinUrl: number;
groupCoverSkinUrl: number;
groupGrade: number;
activeMemberNum: number;
certificationType: number;
certificationText: number;
groupNewGuideLines: FilterGroupNewGuideLines;
groupFace: number;
addOption: number;
shutUpTime: number;
groupTypeFlag: number;
appPrivilegeFlag: number;
appPrivilegeMask: number;
groupExtOnly: GroupEXTOnly;
groupSecLevel: number;
groupSecLevelInfo: number;
subscriptionUin: number;
subscriptionUid: string;
allowMemberInvite: number;
groupQuestion: number;
groupAnswer: number;
groupFlagExt3: number;
groupFlagExt3Mask: number;
groupOpenAppid: number;
rootId: number;
msgLimitFrequency: number;
hlGuildAppid: number;
hlGuildSubType: number;
hlGuildOrgId: number;
groupFlagExt4: number;
groupFlagExt4Mask: number;
groupSchoolInfo: FilterGroupSchoolInfo;
groupCardPrefix: FilterGroupCardPrefix;
allianceId: number;
groupFlagPro1: number;
groupFlagPro1Mask: number;
}
export interface FilterGroupCardPrefix {
introduction: number;
rptPrefix: number;
}
export interface GroupEXTOnly {
tribeId: number;
moneyForAddGroup: number;
}
export interface FilterGroupGeoInfo {
ownerUid: number;
setTime: number;
cityId: number;
longitude: number;
latitude: number;
geoContent: number;
poiId: number;
}
export interface FilterGroupNewGuideLines {
enabled: number;
content: number;
}
export interface FilterGroupSchoolInfo {
location: number;
grade: number;
school: number;
}
export interface ModifyInfo {
noCodeFingerOpenFlag: number;
noFingerOpenFlag: number;
groupName: string;
classExt: number;
classText: string;
fingerMemo: string;
richFingerMemo: string;
tagRecord: any[];
groupGeoInfo: ModifyInfoGroupGeoInfo;
groupExtAdminNum: number;
flag: number;
groupMemo: string;
groupAioSkinUrl: string;
groupBoardSkinUrl: string;
groupCoverSkinUrl: string;
groupGrade: number;
activeMemberNum: number;
certificationType: number;
certificationText: string;
groupNewGuideLines: ModifyInfoGroupNewGuideLines;
groupFace: number;
addOption: number;// 0 空设置 1 任何人都可以进入 2 需要管理员批准 3 不允许任何人入群 4 问题进入答案 5 问题管理员批准
shutUpTime: number;
groupTypeFlag: number;
appPrivilegeFlag: number;
// 需要管理员审核
// 0000 0000 0000 0000 0000 0000 0000
// 无需审核入群
// 0000 0001 0000 0000 0000 0000 0000
// 成员数100内无审核
// 0100 0000 0000 0000 0000 0000 0000
// 禁用 群成员邀请好友
// 0100 0000 0000 0000 0000 0000 0000
appPrivilegeMask: number;
// 0110 0001 0000 0000 0000 0000 0000
// 101711872
groupExtOnly: GroupEXTOnly;
groupSecLevel: number;
groupSecLevelInfo: number;
subscriptionUin: string;
subscriptionUid: string;
allowMemberInvite: number;
groupQuestion: string;
groupAnswer: string;
groupFlagExt3: number;
groupFlagExt3Mask: number;
groupOpenAppid: number;
rootId: string;
msgLimitFrequency: number;
hlGuildAppid: number;
hlGuildSubType: number;
hlGuildOrgId: number;
groupFlagExt4: number;
groupFlagExt4Mask: number;
groupSchoolInfo: ModifyInfoGroupSchoolInfo;
groupCardPrefix: ModifyInfoGroupCardPrefix;
allianceId: string;
groupFlagPro1: number;
groupFlagPro1Mask: number;
}
export interface ModifyInfoGroupCardPrefix {
introduction: string;
rptPrefix: any[];
}
export interface ModifyInfoGroupGeoInfo {
ownerUid: string;
SetTime: number;
CityId: number;
Longitude: string;
Latitude: string;
GeoContent: string;
poiId: string;
}
export interface ModifyInfoGroupNewGuideLines {
enabled: boolean;
content: string;
}
export interface ModifyInfoGroupSchoolInfo {
location: string;
grade: number;
school: string;
}
// 获取群详细信息的来源类型
export enum GroupInfoSource {
KUNSPECIFIED,

View File

@@ -0,0 +1,3 @@
# Example Plugin
## Install
安装只需要将dist产物 `index.mjs` 目录放入 napcat根目录下`plugins/example`,如果没有这个目录请创建。

View File

@@ -0,0 +1,12 @@
import { EventType } from "@/onebot/event/OneBotEvent";
import type { PluginModule } from "@/onebot/network/plugin-manger";
const plugin_init: PluginModule["plugin_init"] = async (_core, _obContext, _actions, _instance) => {
console.log(`[Plugin: example] 插件已初始化`);
}
const plugin_onmessage: PluginModule["plugin_onmessage"] = async (adapter, _core, _obCtx, event, actions, instance) => {
if (event.post_type === EventType.MESSAGE && event.raw_message.includes('ping')) {
await actions.get('send_group_msg')?.handle({ group_id: String(event.group_id), message: 'pong' }, adapter, instance.config);
}
}
export { plugin_init, plugin_onmessage };

View File

@@ -0,0 +1,10 @@
{
"name": "advanced-plugin",
"version": "1.0.0",
"type": "module",
"main": "index.mjs",
"description": "一个高级的 NapCat 插件示例",
"scripts": {
"build": "vite build"
}
}

View File

@@ -0,0 +1,30 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import nodeResolve from '@rollup/plugin-node-resolve';
import { builtinModules } from 'module';
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
export default defineConfig({
resolve: {
conditions: ['node', 'default'],
alias: {
'@/core': resolve(__dirname, '../core'),
'@': resolve(__dirname, '../'),
},
},
build: {
sourcemap: false,
target: 'esnext',
minify: false,
lib: {
entry: 'index.ts',
formats: ['es'],
fileName: () => 'index.mjs',
},
rollupOptions: {
external: [...nodeModules],
},
},
plugins: [nodeResolve()],
});

View File

@@ -15,7 +15,7 @@ import { FFmpegService } from '@/common/ffmpeg';
//Framework ES入口文件
export async function getWebUiUrl() {
const WebUiConfigData = (await WebUiConfig.GetWebUIConfig());
return 'http://127.0.0.1:' + webUiRuntimePort + '/webui/?token=' + WebUiConfigData.token;
return 'http://127.0.0.1:' + webUiRuntimePort + '/webui/?token=' + encodeURIComponent(WebUiConfigData.token);
}
export async function NCoreInitFramework(
@@ -37,7 +37,7 @@ export async function NCoreInitFramework(
const pathWrapper = new NapCatPathWrapper();
const logger = new LogWrapper(pathWrapper.logsPath);
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
if (!process.env['NAPCAT_DISABLE_FFMPEG_DOWNLOAD']) {
downloadFFmpegIfNotExists(logger).then(({ path, reset }) => {
if (reset && path) {
@@ -48,6 +48,12 @@ export async function NCoreInitFramework(
});
}
//直到登录成功后,执行下一步
// const selfInfo = {
// uid: 'u_FUSS0_x06S_9Tf4na_WpUg',
// uin: '3684714082',
// nick: '',
// online: true
// }
const selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
const loginListener = new NodeIKernelLoginListener();
loginListener.onQRCodeLoginSucceed = async (loginResult) => {

View File

@@ -0,0 +1,26 @@
const fs = require('fs');
const path = require('path');
async function initializeNapCat(session, loginService, registerCallback) {
//const logFile = path.join(currentPath, 'napcat.log');
console.log('[NapCat] [Info] 开始初始化NapCat');
//fs.writeFileSync(logFile, '', { flag: 'w' });
//fs.writeFileSync(logFile, '[NapCat] [Info] NapCat 初始化成功\n', { flag: 'a' });
try {
const currentPath = path.dirname(__filename);
const { NCoreInitFramework } = await import('file://' + path.join(currentPath, './napcat.mjs'));
await NCoreInitFramework(session, loginService, (callback) => { registerCallback(callback) });
} catch (error) {
console.log('[NapCat] [Error] 初始化NapCat', error);
//fs.writeFileSync(logFile, `[NapCat] [Error] 初始化NapCat失败: ${error.message}\n`, { flag: 'a' });
}
}
module.exports = {
initializeNapCat: initializeNapCat
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,24 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
lloc: Type.String()
});
type Payload = Static<typeof SchemaData>;
export class DelGroupAlbumMedia extends OneBotAction<Payload, unknown> {
override actionName = ActionName.DelGroupAlbumMedia;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
return await this.core.apis.WebApi.deleteAlbumMediaByNTQQ(
payload.group_id,
payload.album_id,
payload.lloc
);
}
}

View File

@@ -0,0 +1,26 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
lloc: Type.String(),
content: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class DoGroupAlbumComment extends OneBotAction<Payload, unknown> {
override actionName = ActionName.DoGroupAlbumComment;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
return await this.core.apis.WebApi.doAlbumMediaPlainCommentByNTQQ(
payload.group_id,
payload.album_id,
payload.lloc,
payload.content
);
}
}

View File

@@ -0,0 +1,24 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
attach_info: Type.String({ default: "" }),
});
type Payload = Static<typeof SchemaData>;
export class GetGroupAlbumMediaList extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetGroupAlbumMediaList;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
return await this.core.apis.WebApi.getAlbumMediaListByNTQQ(
payload.group_id,
payload.album_id,
payload.attach_info
);
}
}

View File

@@ -0,0 +1,19 @@
import { NTQQWebApi } from '@/core/apis';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String()
});
type Payload = Static<typeof SchemaData>;
export class GetQunAlbumList extends OneBotAction<Payload,Awaited<ReturnType<NTQQWebApi['getAlbumListByNTQQ']>>['response']['album_list']> {
override actionName = ActionName.GetQunAlbumList;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
return (await this.core.apis.WebApi.getAlbumListByNTQQ(payload.group_id)).response.album_list;
}
}

View File

@@ -0,0 +1,28 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
add_type: Type.Number(),
group_question: Type.Optional(Type.String()),
group_answer: Type.Optional(Type.String()),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupAddOption extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupAddOption;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
let ret = await this.core.apis.GroupApi.setGroupAddOption(payload.group_id, {
addOption: payload.add_type,
groupQuestion: payload.group_question,
groupAnswer: payload.group_answer,
});
if (ret.result != 0) {
throw new Error(`设置群添加选项失败, ${ret.result}:${ret.errMsg}`);
}
return null;
}
}

View File

@@ -0,0 +1,27 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
lloc: Type.String(),
id: Type.String(),//421_1_0_1012959257|V61Yiali4PELg90bThrH4Bo2iI1M5Kab|V5bCgAxMDEyOTU5MjU3.PyqaPndPxg!^||^421_1_0_1012959257|V61Yiali4PELg90bThrH4Bo2iI1M5Kab|17560363448^||^1
set: Type.Boolean({default: true})//true=点赞 false=取消点赞 未实现
});
type Payload = Static<typeof SchemaData>;
export class SetGroupAlbumMediaLike extends OneBotAction<Payload, unknown> {
override actionName = ActionName.SetGroupAlbumMediaLike;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
return await this.core.apis.WebApi.doAlbumMediaLikeByNTQQ(
payload.group_id,
payload.album_id,
payload.lloc,
payload.id
);
}
}

View File

@@ -0,0 +1,23 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
user_id: Type.Array(Type.String()),
reject_add_request: Type.Optional(Type.Union([Type.Boolean(), Type.String()])),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupKickMembers extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupKickMembers;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
const rejectReq = payload.reject_add_request?.toString() == 'true';
const uids: string[] = await Promise.all(payload.user_id.map(async uin => await this.core.apis.UserApi.getUidByUinV2(uin)));
await this.core.apis.GroupApi.kickMember(payload.group_id.toString(), uids.filter(uid => !!uid), rejectReq);
return null;
}
}

View File

@@ -0,0 +1,27 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
robot_member_switch: Type.Optional(Type.Number()),
robot_member_examine: Type.Optional(Type.Number()),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupRobotAddOption extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupRobotAddOption;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
let ret = await this.core.apis.GroupApi.setGroupRobotAddOption(
payload.group_id,
payload.robot_member_switch,
payload.robot_member_examine,
);
if (ret.result != 0) {
throw new Error(`设置群机器人添加选项失败, ${ret.result}:${ret.errMsg}`);
}
return null;
}
}

View File

@@ -0,0 +1,26 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
no_code_finger_open: Type.Optional(Type.Number()),
no_finger_open: Type.Optional(Type.Number()),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupSearch extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupSearch;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
let ret = await this.core.apis.GroupApi.setGroupSearch(payload.group_id, {
noCodeFingerOpenFlag: payload.no_code_finger_open,
noFingerOpenFlag: payload.no_finger_open,
});
if (ret.result != 0) {
throw new Error(`设置群搜索失败, ${ret.result}:${ret.errMsg}`);
}
return null;
}
}

View File

@@ -0,0 +1,31 @@
import { uriToLocalFile } from '@/common/file';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { existsSync } from 'node:fs';
import { unlink } from 'node:fs/promises';
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
album_name: Type.String(),
file: Type.String()
});
type Payload = Static<typeof SchemaData>;
export class UploadImageToQunAlbum extends OneBotAction<Payload, unknown> {
override actionName = ActionName.UploadImageToQunAlbum;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
const downloadResult = await uriToLocalFile(this.core.NapCatTempPath, payload.file);
try {
return await this.core.apis.WebApi.uploadImageToQunAlbum(payload.group_id, payload.album_id, payload.album_name, downloadResult.path);
} finally {
if (downloadResult.path && existsSync(downloadResult.path)) {
await unlink(downloadResult.path);
}
}
}
}

View File

@@ -4,7 +4,10 @@ import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
folder_name: Type.String(),
// 兼容gocq 与name二选一
folder_name: Type.Optional(Type.String()),
// 兼容gocq 与folder_name二选一
name: Type.Optional(Type.String()),
});
type Payload = Static<typeof SchemaData>;
@@ -16,6 +19,7 @@ export class CreateGroupFileFolder extends OneBotAction<Payload, ResponseType>
override actionName = ActionName.GoCQHTTP_CreateGroupFileFolder;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem;
const folderName = payload.folder_name || payload.name;
return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), folderName!)).resultWithGroupItem;
}
}

View File

@@ -130,7 +130,7 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, {
throw new Error('消息不存在或已过期');
}
// 6. 解析消息内容
const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;
const resMsg = (await this.obContext.apis.MsgApi.parseMessage(singleMsg, 'array', true));
const forwardContent = (resMsg?.message?.[0] as OB11MessageForward)?.data?.content;
if (forwardContent) {

View File

@@ -14,7 +14,10 @@ const SchemaData = Type.Object({
user_id: Type.String(),
message_seq: Type.Optional(Type.String()),
count: Type.Number({ default: 20 }),
reverseOrder: Type.Boolean({ default: false })
reverseOrder: Type.Boolean({ default: false }),
disable_get_url: Type.Boolean({ default: false }),
parse_mult_msg: Type.Boolean({ default: true }),
quick_reply: Type.Boolean({ default: false }),
});
@@ -41,7 +44,7 @@ export default class GetFriendMsgHistory extends OneBotAction<Payload, Response>
}));
//烘焙消息
const ob11MsgList = (await Promise.all(
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat)))
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat, payload.parse_mult_msg, payload.disable_get_url)))
).filter(msg => msg !== undefined);
return { 'messages': ob11MsgList };
}

View File

@@ -14,7 +14,10 @@ const SchemaData = Type.Object({
group_id: Type.String(),
message_seq: Type.Optional(Type.String()),
count: Type.Number({ default: 20 }),
reverseOrder: Type.Boolean({ default: false })
reverseOrder: Type.Boolean({ default: false }),
disable_get_url: Type.Boolean({ default: false }),
parse_mult_msg: Type.Boolean({ default: true }),
quick_reply: Type.Boolean({ default: false }),
});
@@ -39,7 +42,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<Payload, Re
}));
//烘焙消息
const ob11MsgList = (await Promise.all(
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat)))
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat, payload.parse_mult_msg, payload.disable_get_url, payload.quick_reply)))
).filter(msg => msg !== undefined);
return { 'messages': ob11MsgList };
}

View File

@@ -0,0 +1,27 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
});
type Payload = Static<typeof SchemaData>;
export class GetGroupDetailInfo extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetGroupDetailInfo;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
const data = await this.core.apis.GroupApi.fetchGroupDetail(payload.group_id.toString());
return {
...data,
group_all_shut: data.shutUpAllTimestamp > 0 ? -1 : 0,
group_remark: '',
group_id: +payload.group_id,
group_name: data.groupName,
member_count: data.memberNum,
max_member_count: data.maxMemberNum,
};
}
}

View File

@@ -4,6 +4,7 @@ import { ActionName } from '@/onebot/action/router';
import { Notify } from '@/onebot/types';
interface RetData {
invited_requests: Notify[];
InvitedRequest: Notify[];
join_requests: Notify[];
}
@@ -13,7 +14,7 @@ export class GetGroupIgnoredNotifies extends OneBotAction<void, RetData> {
async _handle(): Promise<RetData> {
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50);
const retData: RetData = { InvitedRequest: [], join_requests: [] };
const retData: RetData = { invited_requests: [], InvitedRequest: [], join_requests: [] };
const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => {
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
@@ -38,7 +39,7 @@ export class GetGroupIgnoredNotifies extends OneBotAction<void, RetData> {
});
await Promise.all(notifyPromises);
retData.invited_requests = retData.InvitedRequest;
return retData;
}
}

View File

@@ -8,10 +8,16 @@ interface GroupNotice {
notice_id: string;
message: {
text: string
// 保持一段时间兼容性 防止以往版本出现问题 后续版本可考虑移除
image: Array<{
height: string
width: string
id: string
}>,
images: Array<{
height: string
width: string
id: string
}>
};
}
@@ -40,15 +46,18 @@ export class GetGroupNotice extends OneBotAction<Payload, GroupNotice[]> {
continue;
}
const retApiNotice: WebApiGroupNoticeFeed = ret.feeds[key];
const image = retApiNotice.msg.pics?.map((pic) => {
return { id: pic.id, height: pic.h, width: pic.w };
}) || [];
const retNotice: GroupNotice = {
notice_id: retApiNotice.fid,
sender_id: retApiNotice.u,
publish_time: retApiNotice.pubt,
message: {
text: retApiNotice.msg.text,
image: retApiNotice.msg.pics?.map((pic) => {
return { id: pic.id, height: pic.h, width: pic.w };
}) || [],
image,
images: image,
},
};
retNotices.push(retNotice);

View File

@@ -1,5 +1,5 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { NTGroupRequestOperateTypes } from '@/core/types';
import { GroupNotify, NTGroupRequestOperateTypes } from '@/core/types';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
@@ -7,6 +7,7 @@ const SchemaData = Type.Object({
flag: Type.Union([Type.String(), Type.Number()]),
approve: Type.Optional(Type.Union([Type.Boolean(), Type.String()])),
reason: Type.Optional(Type.Union([Type.String({ default: ' ' }), Type.Null()])),
count: Type.Optional(Type.Number({ default: 100 })),
});
type Payload = Static<typeof SchemaData>;
@@ -19,8 +20,12 @@ export default class SetGroupAddRequest extends OneBotAction<Payload, null> {
const flag = payload.flag.toString();
const approve = payload.approve?.toString() !== 'false';
const reason = payload.reason ?? ' ';
const count = payload.count;
const invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(flag);
const { doubt, notify } = invite_notify ? { doubt: false, notify: invite_notify } : await this.findNotify(flag);
const { doubt, notify } = invite_notify ? {
doubt: false,
notify: invite_notify,
} : await this.findNotify(flag, count);
if (!notify) {
throw new Error('No such request');
}
@@ -33,12 +38,15 @@ export default class SetGroupAddRequest extends OneBotAction<Payload, null> {
return null;
}
private async findNotify(flag: string) {
let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq == flag);
private async findNotify(flag: string, count: number = 100): Promise<{
doubt: boolean,
notify: GroupNotify | undefined
}> {
let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, count)).find(e => e.seq == flag);
if (!notify) {
notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100)).find(e => e.seq == flag);
notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, count)).find(e => e.seq == flag);
return { doubt: true, notify };
}
return { doubt: false, notify };
}
}
}

View File

@@ -116,10 +116,36 @@ import { CleanCache } from './system/CleanCache';
import SetFriendRemark from './user/SetFriendRemark';
import { SetDoubtFriendsAddRequest } from './new/SetDoubtFriendsAddRequest';
import { GetDoubtFriendsAddRequest } from './new/GetDoubtFriendsAddRequest';
import SetGroupAddOption from './extends/SetGroupAddOption';
import SetGroupSearch from './extends/SetGroupSearch';
import SetGroupRobotAddOption from './extends/SetGroupRobotAddOption';
import SetGroupKickMembers from './extends/SetGroupKickMembers';
import { GetGroupDetailInfo } from './group/GetGroupDetailInfo';
import GetGroupAddRequest from './extends/GetGroupAddRequest';
import { GetCollectionList } from './extends/GetCollectionList';
import { SetGroupTodo } from './packet/SetGroupTodo';
import { GetQunAlbumList } from './extends/GetQunAlbumList';
import { UploadImageToQunAlbum } from './extends/UploadImageToQunAlbum';
import { DoGroupAlbumComment } from './extends/DoGroupAlbumComment';
import { GetGroupAlbumMediaList } from './extends/GetGroupAlbumMediaList';
import { SetGroupAlbumMediaLike } from './extends/SetGroupAlbumMediaLike';
import { DelGroupAlbumMedia } from './extends/DelGroupAlbumMedia';
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
const actionHandlers = [
new DelGroupAlbumMedia(obContext, core),
new SetGroupAlbumMediaLike(obContext, core),
new DoGroupAlbumComment(obContext, core),
new GetGroupAlbumMediaList(obContext, core),
new GetQunAlbumList(obContext, core),
new UploadImageToQunAlbum(obContext, core),
new SetGroupTodo(obContext, core),
new GetGroupDetailInfo(obContext, core),
new SetGroupKickMembers(obContext, core),
new SetGroupAddOption(obContext, core),
new SetGroupRobotAddOption(obContext, core),
new SetGroupSearch(obContext, core),
new SetDoubtFriendsAddRequest(obContext, core),
new GetDoubtFriendsAddRequest(obContext, core),
new SetFriendRemark(obContext, core),
@@ -247,6 +273,8 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new GetPrivateFileUrl(obContext, core),
new GetUnidirectionalFriendList(obContext, core),
new CleanCache(obContext, core),
new GetGroupAddRequest(obContext, core),
new GetCollectionList(obContext, core),
];
type HandlerUnion = typeof actionHandlers[number];

View File

@@ -16,6 +16,9 @@ class DeleteMsg extends OneBotAction<Payload, void> {
async _handle(payload: Payload) {
const msg = MessageUnique.getMsgIdAndPeerByShortId(Number(payload.message_id));
if (msg) {
this.obContext.recallEventCache.set(msg.MsgId, setTimeout(() => {
this.obContext.recallEventCache.delete(msg.MsgId);
}, 5000));
await this.core.apis.MsgApi.recallMsg(msg.Peer, msg.MsgId);
} else {
throw new Error('Recall failed');

View File

@@ -28,13 +28,13 @@ class GetMsg extends OneBotAction<Payload, OB11Message> {
throw new Error('消息不存在');
}
const peer = { guildId: '', peerUid: msgIdWithPeer?.Peer.peerUid, chatType: msgIdWithPeer.Peer.chatType };
const orimsg = this.obContext.recallMsgCache.get(msgIdWithPeer.MsgId);
//const orimsg = this.obContext.recallMsgCache.get(msgIdWithPeer.MsgId);
let msg: RawMessage|undefined;
if (orimsg) {
msg = orimsg;
} else {
// if (orimsg) {
// msg = orimsg;
// } else {
msg = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgIdWithPeer?.MsgId || payload.message_id.toString()])).msgList[0];
}
//}
if (!msg) throw Error('消息不存在');
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat);
if (!retMsg) throw Error('消息为空');

View File

@@ -19,6 +19,7 @@ import { rawMsgWithSendMsg } from '@/core/packet/message/converter';
export interface ReturnDataType {
message_id: number;
res_id?: string;
forward_id?: string;
}
export enum ContextMode {
@@ -129,7 +130,7 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
);
if (getSpecialMsgNum(payload, OB11MessageDataType.node)) {
const packetMode = this.core.apis.PacketApi.available;
const packetMode = this.core.apis.PacketApi.packetStatus;
let returnMsgAndResId: { message: RawMessage | null, res_id?: string } | null;
try {
returnMsgAndResId = packetMode
@@ -147,7 +148,10 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
peerUid: peer.peerUid,
chatType: peer.chatType,
}, (returnMsgAndResId.message).msgId);
return { message_id: msgShortId!, res_id: returnMsgAndResId.res_id! };
// 对gocq的forward_id进行兼容
const resId = returnMsgAndResId.res_id!;
return { message_id: msgShortId!, res_id: resId, forward_id: resId };
} else if (returnMsgAndResId.res_id && !returnMsgAndResId.message) {
throw Error(`发送转发消息res_id${returnMsgAndResId.res_id} 失败`);
}

View File

@@ -4,7 +4,7 @@ import { ActionName, BaseCheckResult } from '@/onebot/action/router';
export abstract class GetPacketStatusDepends<PT, RT> extends OneBotAction<PT, RT> {
protected override async check(payload: PT): Promise<BaseCheckResult>{
if (!this.core.apis.PacketApi.available) {
if (!this.core.apis.PacketApi.packetStatus) {
return {
valid: false,
message: 'packetBackend不可用请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置' +

View File

@@ -13,7 +13,7 @@ export class SendPokeBase extends GetPacketStatusDepends<Payload, void> {
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
// 这里的 !! 可以传入空字符串和0 忽略这些数据有利用接口统一接口
// 这里的 !! 可以传入空字符串 忽略这些数据有利用接口统一接口
const target_id = !!payload.target_id ? payload.target_id : payload.user_id;
const peer_id = !!payload.group_id ? payload.group_id : payload.user_id;

View File

@@ -0,0 +1,31 @@
import { MessageUnique } from '@/common/message-unique';
import { ChatType, Peer } from '@/core';
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
import { ActionName } from '../router';
const SchemaData = Type.Object({
group_id: Type.String(),
message_id: Type.String(),
message_seq: Type.Optional(Type.String())
});
type Payload = Static<typeof SchemaData>;
export class SetGroupTodo extends GetPacketStatusDepends<Payload, void> {
override payloadSchema = SchemaData;
override actionName = ActionName.SetGroupTodo;
async _handle(payload: Payload) {
if (payload.message_seq) {
return await this.core.apis.PacketApi.pkt.operation.SetGroupTodo(+payload.group_id, payload.message_seq);
}
const peer: Peer = {
chatType: ChatType.KCHATTYPEGROUP,
peerUid: payload.group_id
};
const { MsgId, Peer } = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id) ?? { Peer: peer, MsgId: payload.message_id };
let msg = (await this.core.apis.MsgApi.getMsgsByMsgId(Peer, [MsgId])).msgList[0];
if (!msg) throw new Error('消息不存在');
await this.core.apis.PacketApi.pkt.operation.SetGroupTodo(+payload.group_id, msg.msgSeq);
}
}

View File

@@ -10,6 +10,17 @@ export interface InvalidCheckResult {
}
export const ActionName = {
DelGroupAlbumMedia: 'del_group_album_media',
SetGroupAlbumMediaLike: 'set_group_album_media_like',
DoGroupAlbumComment: 'do_group_album_comment',
GetGroupAlbumMediaList: 'get_group_album_media_list',
UploadImageToQunAlbum: 'upload_image_to_qun_album',
GetQunAlbumList: 'get_qun_album_list',
SetGroupTodo: 'set_group_todo',
SetGroupKickMembers: 'set_group_kick_members',
SetGroupRobotAddOption: 'set_group_robot_add_option',
SetGroupAddOption: 'set_group_add_option',
SetGroupSearch: 'set_group_search',
// new extends 完全差异OneBot类别
GetDoubtFriendsAddRequest: 'get_doubt_friends_add_request',
SetDoubtFriendsAddRequest: 'set_doubt_friends_add_request',
@@ -59,7 +70,7 @@ export const ActionName = {
GetStatus: 'get_status',
GetVersionInfo: 'get_version_info',
// Reboot : 'set_restart',
CleanCache : 'clean_cache',
CleanCache: 'clean_cache',
Exit: 'bot_exit',
// go-cqhttp
SetQQProfile: 'set_qq_profile',
@@ -128,6 +139,7 @@ export const ActionName = {
FetchEmojiLike: 'fetch_emoji_like',
SetInputStatus: 'set_input_status',
GetGroupInfoEx: 'get_group_info_ex',
GetGroupDetailInfo: 'get_group_detail_info',
GetGroupIgnoreAddRequest: 'get_group_ignore_add_request',
DelGroupNotice: '_del_group_notice',
FriendPoke: 'friend_poke',

View File

@@ -1,7 +1,7 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { unlink, readdir } from 'fs/promises';
import { join } from 'path';
import path, { join } from 'path';
export class CleanCache extends OneBotAction<void, void> {
override actionName = ActionName.CleanCache;
@@ -28,6 +28,37 @@ export class CleanCache extends OneBotAction<void, void> {
// 等待所有删除操作完成
await Promise.all(deletePromises);
let basic_path = path.join(this.core.dataPath, this.core.selfInfo.uin || '10001', 'nt_qq', 'nt_data');
// 需要清理的目录列表
const dirsToClean = ['Pic', 'Ptt', 'Video', 'File', 'log'];
// 清理每个指定目录
for (const dir of dirsToClean) {
const dirPath = path.join(basic_path, dir);
try {
// 检查目录是否存在
const files = await readdir(dirPath).catch(() => null);
if (files) {
// 删除目录下的所有文件
const dirDeletePromises = files.map(async (file) => {
const filePath = path.join(dirPath, file);
try {
await unlink(filePath);
this.core.context.logger.log(`已删除文件: ${filePath}`);
} catch (err: unknown) {
this.core.context.logger.log(`删除文件 ${filePath} 失败: ${(err as Error).message}`);
}
});
await Promise.all(dirDeletePromises);
this.core.context.logger.log(`目录清理完成: ${dirPath}`);
}
} catch (err: unknown) {
this.core.context.logger.log(`清理目录 ${dirPath} 失败: ${(err as Error).message}`);
}
}
this.core.context.logger.log(`临时文件夹清理完成: ${tempPath}`);
} catch (err: unknown) {

View File

@@ -2,18 +2,27 @@ import { GroupNotifyMsgStatus } from '@/core';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Notify } from '@/onebot/types';
import { Static, Type } from '@sinclair/typebox';
interface RetData {
invited_requests: Notify[];
InvitedRequest: Notify[];
join_requests: Notify[];
}
export class GetGroupSystemMsg extends OneBotAction<void, RetData> {
override actionName = ActionName.GetGroupSystemMsg;
const SchemaData = Type.Object({
count: Type.Union([Type.Number(), Type.String()], { default: 50 }),
});
async _handle(): Promise<RetData> {
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50);
const retData: RetData = { InvitedRequest: [], join_requests: [] };
type Payload = Static<typeof SchemaData>;
export class GetGroupSystemMsg extends OneBotAction<Payload, RetData> {
override actionName = ActionName.GetGroupSystemMsg;
override payloadSchema = SchemaData;
async _handle(params: Payload): Promise<RetData> {
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, +params.count);
const retData: RetData = { invited_requests: [], InvitedRequest: [], join_requests: [] };
const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => {
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
@@ -39,6 +48,7 @@ export class GetGroupSystemMsg extends OneBotAction<void, RetData> {
await Promise.all(notifyPromises);
retData.invited_requests = retData.InvitedRequest;
return retData;
}
}
}

View File

@@ -189,7 +189,7 @@ export class OneBotGroupApi {
this.core,
parseInt(msg.peerUid),
MessageUnique.getShortIdByMsgId(msgData.msgList[0].msgId)!,
parseInt(msgData.msgList[0].senderUin),
parseInt(msgData.msgList[0].senderUin ?? await this.core.apis.UserApi.getUinByUidV2(msgData.msgList[0].senderUid)),
parseInt(realMsg?.add_digest_uin ?? '0'),
);
}

View File

@@ -46,6 +46,7 @@ import { GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody } from '@/core/p
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest';
import { LRUCache } from '@/common/lru-cache';
import { cleanTaskQueue } from '@/common/clean-task';
import { registerResource } from '@/common/health';
type RawToOb11Converters = {
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
@@ -69,7 +70,9 @@ export type SendMessageContext = {
}
export type RecvMessageContext = {
parseMultMsg: boolean
parseMultMsg: boolean,
disableGetUrl: boolean,
quick_reply: boolean
}
function keyCanBeParsed(key: string, parser: RawToOb11Converters): key is keyof RawToOb11Converters {
@@ -109,7 +112,7 @@ export class OneBotMsgApi {
}
},
picElement: async (element, msg, elementWrapper) => {
picElement: async (element, msg, elementWrapper, { disableGetUrl }) => {
try {
const peer = {
chatType: msg.chatType,
@@ -129,7 +132,7 @@ export class OneBotMsgApi {
summary: element.summary,
file: element.fileName,
sub_type: element.picSubType,
url: await this.core.apis.FileApi.getImageUrl(element),
url: disableGetUrl ? (element.filePath ?? '') : await this.core.apis.FileApi.getImageUrl(element),
file_size: element.fileSize,
},
};
@@ -139,7 +142,7 @@ export class OneBotMsgApi {
}
},
fileElement: async (element, msg, elementWrapper) => {
fileElement: async (element, msg, elementWrapper, { disableGetUrl }) => {
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
@@ -147,10 +150,24 @@ export class OneBotMsgApi {
};
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileUuid);
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
if (this.core.apis.PacketApi.available) {
if (this.core.apis.PacketApi.packetStatus && !disableGetUrl) {
let url;
try {
url = await this.core.apis.FileApi.getFileUrl(msg.chatType, msg.peerUid, element.fileUuid, element.file10MMd5)
//url = await this.core.apis.FileApi.getFileUrl(msg.chatType, msg.peerUid, element.fileUuid, element.file10MMd5, 1500)
url = await registerResource(
'file-url-get',
{
resourceFn: async () => {
return await this.core.apis.FileApi.getFileUrl(msg.chatType, msg.peerUid, element.fileUuid, element.file10MMd5, 1500);
},
healthCheckFn: async () => {
return await this.core.apis.PacketApi.pkt.operation.FetchRkey().then(() => true).catch(() => false);
},
testArgs: [],
healthCheckInterval: 30000,
maxHealthCheckFailures: 3
},
);
} catch (error) {
url = '';
}
@@ -240,7 +257,7 @@ export class OneBotMsgApi {
};
},
replyElement: async (element, msg) => {
replyElement: async (element, msg, _, quick_reply) => {
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
@@ -277,7 +294,10 @@ export class OneBotMsgApi {
: replyMsgList.find(msg => msg.msgSeq === msgSeq);
if (replyMsg) return replyMsg;
if (quick_reply) {
this.core.context.logger.logWarn(`快速回复跳过方法1查询序号: ${msgSeq}, 消息数: ${replyMsgList.length}`);
return undefined;
}
this.core.context.logger.logWarn(`方法1查询失败序号: ${msgSeq}, 消息数: ${replyMsgList.length}`);
}
@@ -345,7 +365,7 @@ export class OneBotMsgApi {
return null;
},
videoElement: async (element, msg, elementWrapper) => {
videoElement: async (element, msg, elementWrapper, { disableGetUrl }) => {
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
@@ -390,10 +410,24 @@ export class OneBotMsgApi {
}
//开始兜底
if (!videoDownUrl) {
if (this.core.apis.PacketApi.available) {
if (!videoDownUrl && !disableGetUrl) {
if (this.core.apis.PacketApi.packetStatus) {
try {
videoDownUrl = await this.core.apis.FileApi.getVideoUrlPacket(msg.peerUid, element.fileUuid);
//videoDownUrl = await this.core.apis.FileApi.getVideoUrlPacket(msg.peerUid, element.fileUuid, 1500);
videoDownUrl = await registerResource(
'video-url-get',
{
resourceFn: async () => {
return await this.core.apis.FileApi.getVideoUrlPacket(msg.peerUid, element.fileUuid, 1500);
},
healthCheckFn: async () => {
return await this.core.apis.PacketApi.pkt.operation.FetchRkey().then(() => true).catch(() => false);
},
testArgs: [],
healthCheckInterval: 30000,
maxHealthCheckFailures: 3
},
);
} catch (e) {
this.core.context.logger.logError('获取视频url失败', (e as Error).stack);
videoDownUrl = element.filePath;
@@ -414,7 +448,7 @@ export class OneBotMsgApi {
};
},
pttElement: async (element, msg, elementWrapper) => {
pttElement: async (element, msg, elementWrapper, { disableGetUrl }) => {
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
@@ -422,9 +456,23 @@ export class OneBotMsgApi {
};
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, '', element.fileName);
let pttUrl = '';
if (this.core.apis.PacketApi.available) {
if (this.core.apis.PacketApi.packetStatus && !disableGetUrl) {
try {
pttUrl = await this.core.apis.FileApi.getPttUrl(msg.peerUid, element.fileUuid);
pttUrl = await registerResource(
'ptt-url-get',
{
resourceFn: async () => {
return await this.core.apis.FileApi.getPttUrl(msg.peerUid, element.fileUuid, 1500);
},
healthCheckFn: async () => {
return await this.core.apis.PacketApi.pkt.operation.FetchRkey().then(() => true).catch(() => false);
},
testArgs: [],
healthCheckInterval: 30000,
maxHealthCheckFailures: 3
},
);
//pttUrl = await this.core.apis.FileApi.getPttUrl(msg.peerUid, element.fileUuid, 1500);
} catch (e) {
this.core.context.logger.logError('获取语音url失败', (e as Error).stack);
pttUrl = element.filePath;
@@ -908,17 +956,21 @@ export class OneBotMsgApi {
async parseMessage(
msg: RawMessage,
messagePostFormat: string,
parseMultMsg: boolean = true
parseMultMsg: boolean = true,
disableGetUrl: boolean = false,
quick_reply: boolean = false
) {
if (messagePostFormat === 'string') {
return (await this.parseMessageV2(msg, parseMultMsg))?.stringMsg;
return (await this.parseMessageV2(msg, parseMultMsg, disableGetUrl, quick_reply))?.stringMsg;
}
return (await this.parseMessageV2(msg, parseMultMsg))?.arrayMsg;
return (await this.parseMessageV2(msg, parseMultMsg, disableGetUrl, quick_reply))?.arrayMsg;
}
async parseMessageV2(
msg: RawMessage,
parseMultMsg: boolean = true
parseMultMsg: boolean = true,
disableGetUrl: boolean = false,
quick_reply: boolean = false
) {
if (msg.senderUin == '0' || msg.senderUin == '') return;
if (msg.peerUin == '0' || msg.peerUin == '') return;
@@ -939,7 +991,7 @@ export class OneBotMsgApi {
return undefined;
}
const validSegments = await this.parseMessageSegments(msg, parseMultMsg);
const validSegments = await this.parseMessageSegments(msg, parseMultMsg, disableGetUrl, quick_reply);
resMsg.message = validSegments;
resMsg.raw_message = validSegments.map(msg => encodeCQCode(msg)).join('').trim();
@@ -974,6 +1026,7 @@ export class OneBotMsgApi {
private async handleGroupMessage(resMsg: OB11Message, msg: RawMessage) {
resMsg.sub_type = 'normal';
resMsg.group_id = parseInt(msg.peerUin);
resMsg.group_name = msg.peerName;
let member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin);
if (!member) member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin);
if (member) {
@@ -1009,7 +1062,7 @@ export class OneBotMsgApi {
}
}
private async parseMessageSegments(msg: RawMessage, parseMultMsg: boolean): Promise<OB11MessageData[]> {
private async parseMessageSegments(msg: RawMessage, parseMultMsg: boolean, disableGetUrl: boolean = false, quick_reply: boolean = false): Promise<OB11MessageData[]> {
const msgSegments = await Promise.allSettled(msg.elements.map(
async (element) => {
for (const key in element) {
@@ -1024,7 +1077,7 @@ export class OneBotMsgApi {
element[key],
msg,
element,
{ parseMultMsg }
{ parseMultMsg, disableGetUrl, quick_reply }
);
if (key === 'faceElement' && !parsedElement) {
return null;
@@ -1178,12 +1231,12 @@ export class OneBotMsgApi {
let url = '';
if (mixElement?.picElement && rawMessage) {
const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageImage | undefined;
await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, { parseMultMsg: false, disableGetUrl: false, quick_reply: false }) as OB11MessageImage | undefined;
url = tempData?.data.url ?? '';
}
if (mixElement?.videoElement && rawMessage) {
const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageVideo | undefined;
await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, { parseMultMsg: false, disableGetUrl: false, quick_reply: false }) as OB11MessageVideo | undefined;
url = tempData?.data.url ?? '';
}
return url !== '' ? url : await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', '');
@@ -1209,7 +1262,6 @@ export class OneBotMsgApi {
async waitGroupNotify(groupUin: string, memberUid?: string, operatorUid?: string) {
const groupRole = this.core.apis.GroupApi.groupMemberCache.get(groupUin)?.get(this.core.selfInfo.uid.toString())?.role;
const isAdminOrOwner = groupRole === 3 || groupRole === 4;
if (isAdminOrOwner && !operatorUid) {
let dataNotify: GroupNotify | undefined;
await this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onGroupNotifiesUpdated',
@@ -1239,7 +1291,7 @@ export class OneBotMsgApi {
const operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(),
groupChange.memberUid,
groupChange.operatorInfo ? Buffer.from(groupChange.operatorInfo).toString() : ''
groupChange.operatorInfo ? new TextDecoder('utf-8').decode(groupChange.operatorInfo) : undefined
);
return new OB11GroupIncreaseEvent(
this.core,
@@ -1251,13 +1303,42 @@ export class OneBotMsgApi {
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
// 自身被踢出时operatorInfo会是一个protobuf 否则大多数情况为一个string
let operator_uid_parse: string | undefined = undefined;
if (groupChange.operatorInfo) {
// 先判断是否可能是protobuf自身被踢出或以0a开头
if (groupChange.decreaseType === 3 || Buffer.from(groupChange.operatorInfo).toString('hex').startsWith('0a')) {
// 可能是protobuf尝试解析
try {
operator_uid_parse = new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid;
} catch (error) {
// protobuf解析失败fallback到字符串解析
try {
const decoded = new TextDecoder('utf-8').decode(groupChange.operatorInfo);
// 检查是否包含非ASCII字符如果包含则丢弃
const isAsciiOnly = [...decoded].every(char => char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126);
operator_uid_parse = isAsciiOnly ? decoded : '';
} catch (e2) {
operator_uid_parse = '';
}
}
} else {
// 直接进行字符串解析
try {
const decoded = new TextDecoder('utf-8').decode(groupChange.operatorInfo);
// 检查是否包含非ASCII字符如果包含则丢弃
const isAsciiOnly = [...decoded].every(char => char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126);
operator_uid_parse = isAsciiOnly ? decoded : '';
} catch (e) {
operator_uid_parse = '';
}
}
}
const operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(),
groupChange.memberUid,
groupChange.decreaseType === 3 && groupChange.operatorInfo ?
new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid :
groupChange.operatorInfo?.toString()
operator_uid_parse
);
if (groupChange.memberUid === this.core.selfInfo.uid) {
setTimeout(() => {

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