Compare commits

..

167 Commits

Author SHA1 Message Date
手瓜一十雪
5284e0ac5a Update OpenRouter model in release workflow
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Changed the OPENROUTER_MODEL environment variable in the release workflow to use 'copilot/gemini-3-flash-preview' instead of 'copilot/ant/gemini-3-flash-preview'.
2026-01-17 18:57:42 +08:00
手瓜一十雪
67d6cd3f2e Refactor worker restart to control quick login param
Modified the restartWorker and startWorker functions to control whether the quick login parameter (-q/--qq) is passed to the worker process. On restart, quick login is not passed, while on unexpected exits, it is preserved. This improves process management and parameter handling during worker lifecycle events.
2026-01-17 18:56:53 +08:00
手瓜一十雪
0ba5862753 Pass CLI args to worker and update login script example
The quickLoginExample.bat script was updated to use the new launcher script names and argument format. In napcat.ts, the master process now forwards its command line arguments to the worker process, enabling better parameter handling.
2026-01-17 18:54:18 +08:00
手瓜一十雪
d4478275ee Add auto-restart for unexpected worker exits
Introduces an isRestarting flag to distinguish between intentional and unexpected worker restarts. If the worker process exits unexpectedly, the system now attempts to automatically restart it and logs relevant warnings and errors.
2026-01-17 18:38:12 +08:00
手瓜一十雪
163bb88751 Remove unused isFile variable in GetPluginListHandler
Cleaned up the GetPluginListHandler by removing the unused isFile variable, as it was no longer needed for plugin list processing.
2026-01-17 16:27:24 +08:00
手瓜一十雪
ec6762d916 Add plugin enable/disable config and status management
Introduces a persistent plugins.json config to track enabled/disabled status for plugins, updates the plugin manager to respect this config when loading plugins, and adds API and frontend support for toggling plugin status. The backend now reports plugin status as 'active', 'stopped', or 'disabled', and the frontend displays these states with appropriate labels. Also updates the built-in plugin package.json with author info.
2026-01-17 16:24:46 +08:00
手瓜一十雪
ed1872a349 Add plugin management to WebUI backend and frontend
Implemented backend API and router for plugin management (list, reload, enable/disable, uninstall) and exposed corresponding frontend controller and dashboard page. Updated navigation and site config to include plugin management. Refactored plugin manager adapter for public methods and improved plugin metadata handling.
2026-01-17 16:14:46 +08:00
手瓜一十雪
a7fd70ac3a Add napcat-plugin-builtin build step to CI workflows
Updated build and release GitHub Actions workflows to include a build step for napcat-plugin-builtin. This ensures the plugin is built alongside other packages during CI processes.
2026-01-17 15:50:20 +08:00
手瓜一十雪
7e38f1d227 Add builtin plugin package and enhance action map
Introduces the napcat-plugin-builtin package with initialization, message handling, and build configuration. Also adds a type-safe 'call' helper to the action map in napcat-onebot for improved action invocation.
2026-01-17 15:48:48 +08:00
时瑾
0ca68010a5 feat: 优化离线重连机制,支持通过前端实现重新登录
* feat: 优化离线重连机制,增加前端登录错误提示与二维码刷新功能

- 增加全局掉线检测弹窗
- 增强登录错误解析,支持显示 serverErrorCode 和 message
- 优化二维码登录 UI,错误时显示详细原因并提供大按钮重新获取
- 核心层解耦,通过事件抛出 KickedOffLine 通知
- 支持前端点击刷新二维码接口

* feat: 新增看门狗汪汪汪

* cp napcat-shell-loader/launcher-win.bat

* refactor: 重构重启流程,移除旧的重启逻辑,新增基于 WebUI 的重启请求处理

* fix: 刷新二维码清楚错误信息
2026-01-17 15:38:24 +08:00
手瓜一十雪
822f683a14 Disable multi-process in development environment
Set NAPCAT_DISABLE_MULTI_PROCESS environment variable to '1' to disable restart and multi-process features during development.
2026-01-17 15:12:30 +08:00
手瓜一十雪
f4d3d33954 Remove explicit status code from sendError calls
Eliminated the explicit 500 status code parameter from sendError calls in RestartProcessHandler, allowing sendError to use its default behavior.
2026-01-17 15:10:21 +08:00
手瓜一十雪
d1abf788a5 Remove redundant comments in worker process handler
Cleaned up unnecessary comments in the message handler for process restart and shutdown signals to improve code readability.
2026-01-17 15:08:39 +08:00
手瓜一十雪
9ba6b2ed40 Remove redundant worker creation log statements
Deleted duplicate and unnecessary log messages related to worker process creation and spawning to reduce log clutter.
2026-01-17 15:06:44 +08:00
手瓜一十雪
3a880e389b Refactor process management with unified API
Introduces a new process-api.ts module to abstract process management for both Electron and Node.js environments. Refactors napcat.ts to use this unified API, improving clarity and maintainability of worker/master process logic, restart handling, and environment detection. Removes unused import from base.ts.
2026-01-17 15:02:54 +08:00
手瓜一十雪
1c7ac42a46 Add support to disable multi-process and named pipe via env
Introduces NAPCAT_DISABLE_MULTI_PROCESS and NAPCAT_DISABLE_PIPE environment variables to allow disabling multi-process mode and named pipe connection, respectively. Also simplifies process termination logic by always using SIGKILL.
2026-01-17 14:46:57 +08:00
手瓜一十雪
3e8b575015 Add process restart feature via WebUI
Introduces backend and frontend support for restarting the worker process from the WebUI. Adds API endpoint, controller, and UI button for process management. Refactors napcat-shell to support master/worker process lifecycle and restart logic.
2026-01-17 14:42:07 +08:00
手瓜一十雪
7c22170e1e Add support for version 9.9.26-44725
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Updated appid.json, napi2native.json, and packet.json to include entries for version 9.9.26-44725, adding relevant appid, qua, send, and recv values.
2026-01-16 17:25:29 +08:00
手瓜一十雪
f143da6ba8 Revert "Update pnpm install to use --no-frozen-lockfile"
Some checks failed
Build NapCat Artifacts / Build-Framework (push) Has been cancelled
Build NapCat Artifacts / Build-Shell (push) Has been cancelled
This reverts commit d0d3934869.
2026-01-15 11:19:15 +08:00
手瓜一十雪
d0d3934869 Update pnpm install to use --no-frozen-lockfile
Replaces 'pnpm i' with 'pnpm i --no-frozen-lockfile' in build and release GitHub workflows to allow installation even if lockfile changes are detected. This helps prevent CI failures due to lockfile mismatches.
2026-01-15 11:15:38 +08:00
手瓜一十雪
808165b008 Add napi2native mapping for 3.2.21-42086-arm64
Introduced native address mappings for the 3.2.21-42086-arm64 version, including 'send' and 'recv' function offsets.
2026-01-15 10:53:58 +08:00
手瓜一十雪
d23785f34d Add isActive property to plugin adapters
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Introduces an isActive getter to OB11PluginAdapter and OB11PluginMangerAdapter, which returns true only if the adapter is enabled and has loaded plugins. Updates event emission logic to use isActive instead of isEnable, ensuring events are only sent to active adapters.
2026-01-14 18:53:32 +08:00
手瓜一十雪
31daf41135 Add onLoginRecordUpdate method to listener
Introduces the onLoginRecordUpdate method to NodeIKernelLoginListener, preparing for future handling of login record updates.
2026-01-14 18:53:31 +08:00
手瓜一十雪
a2450b72be Refactor network adapter activation and message handling
Introduces isActive property to network adapters for more accurate activation checks, refactors message dispatch logic to use only active adapters, and improves heartbeat management for WebSocket adapters. Also sets default enableWebsocket to false in config and frontend forms, and adds a security dialog for missing tokens in the web UI.
2026-01-14 18:53:31 +08:00
手瓜一十雪
fbccf8be24 Make emoji_likes_list optional in OB11Message
Changed the OB11Message interface to make emoji_likes_list optional and updated GetMsg to initialize emoji_likes_list as an empty array before populating it. This prevents errors when the field is missing and improves type safety.
2026-01-13 17:08:31 +08:00
手瓜一十雪
37ae17b53f Remove unused imports and update method params
Removed the unused 'readFileSync' import from ffmpeg-addon-adapter.ts. Updated parameter names in convertToNTSilkTct method of ffmpeg-exec-adapter.ts to use underscores, indicating unused variables.
2026-01-13 17:01:00 +08:00
手瓜一十雪
35566970fd Update pnpm-lock.yaml 2026-01-13 16:57:00 +08:00
手瓜一十雪
e70cd1eff7 Update QQ download links to version 44343
Updated Windows and Linux QQ download links in default.md and release_note_prompt.txt to point to version 9.9.26-44343 and 3.2.23-44343, replacing previous 40990 links.
2026-01-13 16:54:57 +08:00
手瓜一十雪
fbd3241845 Improve version info UI and update model config
Refined the system info version comparison layout for better responsiveness and readability, especially on smaller screens. Updated the OpenRouter model name in the release workflow and improved dark mode text color handling in sidebar menu items.
2026-01-13 16:50:46 +08:00
手瓜一十雪
cf69ccdbc9 Add emoji likes list support to message types
Introduces the emojiLikesList property to RawMessage and maps it to the new emoji_likes_list field in OB11Message, which is populated in the GetMsg action. Also updates type definitions for stricter typing and consistency.
2026-01-13 16:43:00 +08:00
Makoto
f3de4d48d3 feat: add settings field to group notice API response (#1505)
* feat: add settings field to group notice API response

- Add settings field to GroupNotice interface
- Include announcement configuration options (is_show_edit_card, remind_ts, tip_window_type, confirm_required)
- Fixes #1503

* refactor: make settings field optional for backward compatibility

- Mark settings as optional in GroupNotice interface
- Mark settings as optional in WebApiGroupNoticeFeed type
- Prevents runtime errors when processing older or malformed notices
- Addresses code review feedback on PR #1505

* Update GetGroupNotice.ts

---------

Co-authored-by: 手瓜一十雪 <nanaeonn@outlook.com>
2026-01-13 16:32:03 +08:00
手瓜一十雪
17d5110069 Add convertToNTSilkTct to FFmpeg adapters and update usage (#1517)
Introduces the convertToNTSilkTct method to FFmpeg adapter interfaces and implementations, updating audio conversion logic to use this new method for Silk format conversion. Refactors FFmpegService to rename convertFile to convertAudioFmt and updates related usages. Removes 'audio-worker' entry from vite configs in napcat-framework and napcat-shell. Also fixes a typo in appid.json.

Remove silk-wasm dependency and refactor audio handling

Eliminated the silk-wasm package and related code, including audio-worker and direct Silk encoding/decoding logic. Audio format conversion and Silk detection are now handled via FFmpeg adapters. Updated related OneBot actions and configuration files to remove all references to silk-wasm and streamline audio processing.
2026-01-13 16:18:32 +08:00
手瓜一十雪
c5de5e00fc Add mappings for version 3.2.23-44343 (arm64 and x64)
Updated napi2native.json to include send and recv addresses for 3.2.23-44343 on both arm64 and x64 architectures.
2026-01-09 15:33:15 +08:00
手瓜一十雪
ea7cd7f7e1 Add new version mappings to external JSON files
Updated appid.json, napi2native.json, and packet.json to include mappings for versions 9.9.26-44343 and 3.2.23-44343, including x64 and arm64 variants. This ensures compatibility with the latest application versions and platforms.
2026-01-09 13:35:15 +08:00
手瓜一十雪
cc23599776 Enhance HTTP debug UI with command palette and UI improvements
Added a new CommandPalette component for quick API selection and execution (Ctrl/Cmd+K). Refactored the HTTP debug page to use the command palette, improved tab and panel UI, and enhanced the code editor's appearance and theme integration. Updated OneBotApiDebug to support imperative methods for request body and sending, improved response panel resizing, and made various UI/UX refinements across related components.
2026-01-04 20:38:08 +08:00
手瓜一十雪
c6ec2126e0 Refactor theme font handling and preview logic
Moved font configuration to be managed via theme.css, eliminating the need for separate font initialization and caching. Updated backend to generate @font-face rules and font variables in theme.css. Frontend now uses a dedicated style tag for real-time font preview in the theme config page, and removes legacy font cache logic for improved consistency.
2026-01-04 18:48:16 +08:00
手瓜一十雪
f1756c4d1c Optimize version fetching and update logic
Introduces lazy loading for release and action artifact versions, adds support for nightly.link mirrors, and improves artifact retrieval reliability. Removes unused loginService references, refactors update logic to handle action artifacts, and streamlines frontend/backend API parameters for version selection.
2026-01-04 12:41:21 +08:00
手瓜一十雪
4940d72867 Update release workflow
Updates the release workflow to download and include NapCat.Shell.Windows.OneKey.zip in the release artifacts.
2026-01-03 18:37:17 +08:00
手瓜一十雪
91e0839ed5 Add upload_file option for file upload actions
Introduces an 'upload_file' boolean option to group and private file upload actions, allowing control over whether files are uploaded to group storage or sent directly. Updates the NTQQFileApi and OneBotFileApi to support this option and adjusts file handling logic accordingly.
2026-01-03 16:25:38 +08:00
手瓜一十雪
334c4233e6 Update message retrieval and parsing logic
Changed the protocol fallback logic to pass an additional argument to parseMessageV2 and updated message retrieval to use getMsgHistory instead of getMsgsByMsgId. This improves compatibility and ensures correct message fetching.
2026-01-03 16:05:03 +08:00
手瓜一十雪
71bb4f68f3 Improve senderUin handling in sendMsg method
If senderUin is missing or '0', attempt to retrieve it using senderUid before returning. This ensures messages are not dropped when senderUid is available but senderUin is not.
2026-01-03 16:01:24 +08:00
手瓜一十雪
47983e2915 Add PTT element type to message element filters
Updated the filtering logic in SendMsgBase to include ElementType.PTT alongside FILE, VIDEO, and ARK types. This ensures PTT elements are handled consistently with other single-element message types.
2026-01-03 15:38:13 +08:00
手瓜一十雪
ae42eed6e2 Fix font reset on unmount with unsaved changes
Added a ref to track unsaved changes and updated the cleanup logic to only restore the saved font settings if there are unsaved changes. This prevents the font from being unintentionally reset when the page is refreshed or the component is unmounted without changes.
2026-01-03 15:36:42 +08:00
手瓜一十雪
cb061890d3 Enhance artifact handling and display for action builds
Extended artifact metadata to include workflow run ID and head SHA. Updated backend to filter artifacts by environment and provide additional metadata. Improved frontend to display new artifact details and adjusted UI for better clarity.
2026-01-03 15:28:18 +08:00
手瓜一十雪
31feec26b5 Update release.yml 2026-01-03 15:11:58 +08:00
手瓜一十雪
e93cd3529f Update pr-build.yml 2026-01-03 15:10:03 +08:00
手瓜一十雪
1ad700b935 Update release workflow and documentation prompts
Refactored the release workflow to add semantic version validation, improved commit and file diff collection, and enhanced release note generation with more context and formatting. Updated release note and default documentation prompts for clarity, conciseness, and better user guidance. Fixed owner typo in workflow and improved error handling for missing tags.
2026-01-03 15:01:10 +08:00
手瓜一十雪
68c8b984ad Refactor update logging to use logger interface
Replaces all console logging in the update process with the ILogWrapper logger interface for consistent logging. Updates applyPendingUpdates to require a logger parameter and propagates this change to all relevant initialization code. Also removes duplicate and unnecessary lines in workflow YAML files.
2026-01-03 14:51:56 +08:00
手瓜一十雪
8eb1aa2fb4 Refactor GitHub tag fetching and mirror management
Replaces legacy tag fetching logic in napcat-common with a new mirror.ts module that centralizes GitHub mirror configuration, selection, and tag retrieval. Updates helper.ts to use the new mirror system and semver comparison, and exports compareSemVer for broader use. Updates workflows and scripts to generate and propagate build version information, and improves build status comment formatting for PRs. Also updates release workflow to use a new OpenAI key and model.
2026-01-03 14:42:24 +08:00
Makoto
2d3f4e696b feat: Add OB11GroupGrayTipEvent for detecting forged gray tip attacks (#1492)
* feat: Add OB11GroupGrayTipEvent for detecting forged gray tip attacks

- Add new OB11GroupGrayTipEvent class to report unknown gray tip messages
- Modify parseOtherJsonEvent to detect forged gray tips by checking senderUin
- Real system gray tips have senderUin='0', forged ones expose attacker's QQ
- Include message_id in event for downstream recall capability
- Add try/catch for JSON.parse to handle malformed content
- Use Number() for consistent type conversion

* fix: simplify logWarn to match upstream style

* fix: remove extra closing brace that broke class structure

* fix: add validation for malformed title gray tip events
2026-01-02 20:55:24 +08:00
时瑾
b241881c74 fix: 修复用户ID类型转换错误并移除不必要的标签渲染 2026-01-02 20:50:13 +08:00
时瑾
aecf33f4dc fix: close #1488 2026-01-02 17:07:39 +08:00
时瑾
dd4374389b fix: close #1435 (#1485)
* fix: close #1435

* fix: 优化视频缩略图生成和清理逻辑,处理文件不存在的情况
2026-01-01 21:41:01 +08:00
时瑾
100efb03ab fix: close #1477 (#1484) 2026-01-01 21:40:49 +08:00
时瑾
ce9482f19d feat: 优化webui界面和文件管理器 (#1472) 2026-01-01 21:40:39 +08:00
手瓜一十雪
4e37b002f9 Add support for version 9.9.26-44175 and fix import type
Added entries for version 9.9.26-44175 in appid.json, napi2native.json, and packet.json to support the new version. Also updated the import of createActionMap in napcat-plugin/index.ts to use a type-only import.
2026-01-01 10:32:59 +08:00
Nepenthe
7e7262415b 更新插件示例,修复插件打包问题 (#1486)
* fix: 修复打包错误

* fix: 完善插件模板

* Update index.ts
2025-12-31 13:58:55 +08:00
时瑾
3365211507 ci: 添加构建结果评论中的下载链接和获取 artifacts 列表功能 2025-12-29 03:14:17 +08:00
时瑾
05b38825c0 ci: 使用 type 导入 PullRequest 和 BuildStatus 类型 2025-12-29 03:01:21 +08:00
时瑾
95f4a4d37e ci: pr build 2025-12-29 02:55:11 +08:00
时瑾
cd495fc7a0 fix: close #1467 close #1471 2025-12-27 16:27:54 +08:00
时瑾
656279d74b fix: close #1463 2025-12-27 00:20:59 +08:00
手瓜一十雪
377c780d1a Comment out manual chunking for @heroui in Vite config
The manual chunking logic for '@heroui' modules in the Vite configuration has been commented out. This may be to simplify chunk splitting or address build issues related to custom chunking.
2025-12-24 19:31:52 +08:00
手瓜一十雪
aefa8985b1 Remove vite-plugin-font and related dependencies
This update removes vite-plugin-font and its associated dependencies from pnpm-lock.yaml. This likely reflects a cleanup of unused packages or a migration away from font-related build tooling.
2025-12-24 18:30:55 +08:00
手瓜一十雪
b034940dfd Remove vite-plugin-font from dependencies
Deleted the vite-plugin-font package from both the root and frontend package.json files as it is no longer required.
2025-12-24 18:29:17 +08:00
手瓜一十雪
cb8e10cc7e Add sw_template.js to build and improve service worker loading
Updated the Vite config to copy sw_template.js to the static assets during build. Modified backend to load sw_template.js from the static directory if available, falling back to the source assets if not. This ensures the service worker template is correctly served in production builds.
2025-12-24 18:20:51 +08:00
手瓜一十雪
afed164ba1 Add background-aware styling to sidebar and usage pie
Updated sidebar, navigation list, and usage pie components to adjust their styles based on the presence of a custom background image. This improves visual integration when a background image is set, ensuring text and UI elements remain readable and aesthetically consistent.
2025-12-24 18:14:04 +08:00
手瓜一十雪
a34a86288b Refactor font handling and theme config, switch to CodeMirror editor
Replaces Monaco editor with CodeMirror in the frontend, removing related dependencies and configuration. Refactors font management to support multiple formats (woff, woff2, ttf, otf) and dynamic font switching, including backend API and frontend theme config UI. Adds gzip compression middleware to backend. Updates theme config to allow font selection and custom font upload, and improves theme preview and color customization UI. Cleans up unused code and improves sidebar and terminal font sizing responsiveness.
2025-12-24 18:02:54 +08:00
手瓜一十雪
50bcd71144 Remove unused dependencies and optimize Monaco workers
Removed @simplewebauthn/browser, framer-motion, and react-responsive from dependencies as they are no longer used. Updated Monaco editor configuration to only load the JSON worker for improved performance, falling back to the basic editor worker for other languages. Refactored the new version tip UI to use Chip and Spinner instead of Button and removed unused react-icons import. Also updated Vite config to stop sharing react-icons.
2025-12-24 15:32:21 +08:00
手瓜一十雪
fa3a229827 Refactor dashboard and components, remove echarts
Replaces echarts-based usage pie chart with a custom SVG implementation, removing the echarts dependency. Improves caching for version and system info requests, simplifies page background to static elements, and switches dashboard state to use localStorage for persistence. Also removes polling from hitokoto and updates button styling in system info.
2025-12-24 13:56:34 +08:00
手瓜一十雪
e56b912bbd Remove debug log from emoji_like event handler
Eliminated a console.log statement in the 'event:emoji_like' event handler to clean up debug output.
2025-12-22 17:08:32 +08:00
手瓜一十雪
da0dd01460 Remove debug console.log statements from DebugAdapter
Eliminated several console.log statements used for debugging in the DebugAdapter and DebugAdapterManager classes to clean up console output.
2025-12-22 16:29:05 +08:00
手瓜一十雪
578dda2f17 feat: 支持免配置调试 2025-12-22 16:27:06 +08:00
手瓜一十雪
649165bf00 Redesign OneBot API debug UI and improve usability
Refactored the OneBot API debug interface for a more modern, tabbed layout with improved sidebar navigation, request/response panels, and better mobile support. Enhanced code editor, response display, and message construction modal. Updated system info and status display for cleaner visuals. Improved xterm font sizing and rendering logic for mobile. WebSocket debug page now features a unified header, status bar, and clearer connection controls. Overall, this commit provides a more user-friendly and visually consistent debugging experience.
2025-12-22 15:21:45 +08:00
手瓜一十雪
c4f7107038 Refactor update dialog for new version notification
Replaces the old update confirmation and toast logic with a new dialog-based update flow, including detailed status feedback (idle, updating, success, error) and improved user guidance. Removes react-hot-toast dependency and introduces a dedicated UpdateDialogContent component for clearer update progress and error handling.
2025-12-22 14:10:23 +08:00
手瓜一十雪
7f81bf45ee Revert "Refactor UI components for consistent styling"
This reverts commit 7e6035d98b.
2025-12-22 14:04:26 +08:00
手瓜一十雪
7e6035d98b Refactor UI components for consistent styling
Unified card and component styles across the frontend by removing background image logic and related conditional classes. Updated color schemes, shadows, and spacing for a more consistent appearance. Improved error handling and response structure in the backend update handler.
2025-12-22 13:34:59 +08:00
手瓜一十雪
2405cb03d8 Improve background and text styling in NetworkItemDisplay
Adjusted background opacity and hover effects for better visual consistency. Updated text color logic to enhance readability based on background presence.
2025-12-22 13:07:08 +08:00
手瓜一十雪
32d3ff6998 Add showType prop and remove ScrollShadow usage
Added an optional showType prop to NetworkDisplayCardProps in common_card.tsx. Removed the ScrollShadow component and replaced it with a standard div in nav_list.tsx to simplify the layout.
2025-12-22 12:32:48 +08:00
手瓜一十雪
84f0e0f9a0 Refactor UI for network cards and improve theming
Redesigned network display cards and related components for a more modern, consistent look, including improved button styles, card layouts, and responsive design. Added support for background images and dynamic theming across cards, tables, and log views. Enhanced input and select components with unified styling. Improved file table responsiveness and log display usability. Refactored OneBot API debug and navigation UI for better usability and mobile support.
2025-12-22 12:27:56 +08:00
手瓜一十雪
8697061a90 Refactor UI styles for improved consistency and clarity
Unified card backgrounds, borders, and shadows across components for a more consistent look. Enhanced table, tab, and button styles for clarity and accessibility. Improved layout and modal structure in OneBot API debug, added modal for struct display, and optimized WebSocket debug connection logic. Updated file manager, logs, network, and terminal pages for visual consistency. Refactored interface definitions for stricter typing and readability.
2025-12-22 10:38:23 +08:00
手瓜一十雪
872a3e0100 Add @heroui/divider to frontend dependencies
Added the @heroui/divider package (version ^2.2.21) to the napcat-webui-frontend dependencies in package.json and updated pnpm-lock.yaml accordingly.
2025-12-20 18:10:32 +08:00
手瓜一十雪
4fcbdc4d89 Remove music player and related context/hooks
Deleted the audio player component, songs context, and use-music hook, along with all related code and configuration. Updated affected components and pages to remove music player dependencies and UI. Also improved sidebar, background, and about page UI, and refactored site config icons to use react-icons.
2025-12-20 18:07:16 +08:00
手瓜一十雪
176af14915 Add 42941 version mappings to external JSON files
Added new entries for version 42941 to appid.json, napi2native.json, and packet.json, including mappings for x64 and arm64 architectures. This update ensures support for the latest client versions and their corresponding identifiers and packet mappings.
2025-12-05 18:29:10 +08:00
手瓜一十雪
81cf1fd98e Update wording in usage instructions in README
Clarified the instructions regarding support for integration, basic, and underlying framework issues to improve user understanding.
2025-12-01 13:28:18 +08:00
手瓜一十雪
5189099146 Add pnpm-lock.yaml and update .gitignore
Added pnpm-lock.yaml to track dependencies and removed it from .gitignore in the napcat-webui-frontend package to enable version control of the lock file.
2025-11-30 18:08:22 +08:00
手瓜一十雪
7fc17d45ba Add support for 9.9.25-42905 and 6.9.86-42905 versions
Updated appid.json, napi2native.json, and packet.json to include entries for versions 9.9.25-42905 (x64/Win) and 6.9.86-42905 (arm64/Mac), adding corresponding appid, qua, send, and recv values.
2025-11-30 12:56:24 +08:00
手瓜一十雪
c54f74609e Update version keys from 9.9.23-42744 to 9.9.25-42744
Renamed version keys in appid.json, napi2native.json, and packet.json from 9.9.23-42744(-x64) to 9.9.25-42744(-x64) to reflect the new version. Associated values remain unchanged.
2025-11-28 17:25:28 +08:00
手瓜一十雪
a2d7ac4878 Add support for new app and protocol versions
Updated appid.json, napi2native.json, and packet.json to include entries for versions 9.9.23-42744 and 6.9.86-42744, as well as their corresponding protocol mappings for x64 and arm64 architectures.
2025-11-26 19:43:14 +08:00
手瓜一十雪
fd0afa3b25 Add support for 9.9.23-42430-x64 in napi2native and packet
Updated napi2native.json and packet.json to include send and recv addresses for version 9.9.23-42430-x64, enabling compatibility with this new version.
2025-11-26 19:15:02 +08:00
手瓜一十雪
7685cc3dfc Prefix version numbers with 'v' in system info
Updated the display of current and latest version numbers in the system info component to include a 'v' prefix for consistency and clarity.
2025-11-25 23:10:10 +08:00
手瓜一十雪
f9c0b9d106 Remove leading 'v' from latest tag in getLatestTag
Updated the getLatestTag function to strip a leading 'v' character from the latest tag before returning it. This ensures tag values are returned without the 'v' prefix.
2025-11-25 23:09:53 +08:00
手瓜一十雪
d31f0a45b4 Fix version tag formatting and error handling
Update packet.ts to prefix napCatVersion with 'v' in the error message link. In vite-plugin-version.js, improve formatting of catch blocks, ensure returned tags do not include a leading 'v', and standardize fallback version to '0.0.0'.
2025-11-25 23:03:55 +08:00
huan-yp
7c701781a1 Fix URL formatting in error message for QQ version (#1396) 2025-11-25 12:59:37 +08:00
时瑾
3c612e03ff feat: close #1394 2025-11-24 12:47:04 +08:00
手瓜一十雪
f27db01145 Update onMSFSsoError signature with code and desc
The onMSFSsoError method now accepts a numeric code and a string description as parameters instead of a single unknown argument. This change clarifies the expected input for error handling.
2025-11-22 20:30:02 +08:00
手瓜一十雪
ae97cfba03 Refine types in storage clean listener and service
Updated method signatures in NodeIKernelStorageCleanListener and NodeIKernelStorageCleanService to use more specific types and parameter names. This improves type safety and code clarity, particularly for cache scanning and listener methods.
2025-11-22 19:57:18 +08:00
手瓜一十雪
162ddc1bf5 fix: #1392 & Remove update functionality from VersionInfo component
Eliminated the update button and related logic from the VersionInfo component in the about page. This includes removing the useRequest hook for updating, the toast notifications, and the Button component, simplifying the component to only display version information.
2025-11-22 18:31:42 +08:00
手瓜一十雪
afb6ef421a Add Passkey (WebAuthn) authentication support
Introduces Passkey (WebAuthn) registration and authentication to both backend and frontend. Backend adds new API endpoints, middleware exceptions, and a PasskeyHelper for credential management using @simplewebauthn/server. Frontend integrates @simplewebauthn/browser, updates login and config pages for Passkey registration and login flows, and adds related UI and controller methods.
2025-11-22 16:00:32 +08:00
手瓜一十雪
173a165c4b Add latest version check and update prompt to UI
Introduces backend and frontend logic to fetch the latest NapCat version tag from multiple sources, exposes a new API endpoint, and adds a UI prompt to notify users of new versions with an update button. Also includes minor code style improvements in dialog context.
2025-11-22 13:52:49 +08:00
手瓜一十雪
d525f9b03d Refactor and standardize share and message history APIs
Standardized field names (e.g., 'reverseOrder' to 'reverse_order', 'phoneNumber' to 'phone_number') and added new action names and classes for sharing contacts and group cards (SendArkShare, SendGroupArkShare). Deprecated old action names, updated API schemas and routes, and ensured backward compatibility for legacy fields. Updated frontend API definitions to match backend changes.
2025-11-22 13:14:46 +08:00
zeus-x99
f2ba789cc0 fix(webui-backend): 仅在启用 WebUI 时检测/更新默认密码 (#1387)
在初始化 WebUI 时,先判断  并直接返回,确保禁用状态下不再检测或更新默认密码 token。
2025-11-20 14:38:59 +08:00
手瓜一十雪
2cdc9bdc09 Add backend and frontend support for NapCat auto-update
Introduces backend API and router for updating NapCat, including update logic and pending update application on startup. Adds frontend integration with update button and request handling. Refactors system info component to remove legacy new version tip. Updates types and runtime to track working environment for update selection. Implements lazy loading for pty in unixTerminal to avoid early initialization.
2025-11-19 21:05:08 +08:00
手瓜一十雪
c123b34d5f Update ffmpeg addon binary for Darwin ARM64
Replaces the ffmpegAddon.darwin.arm64.node binary with a new version, likely to include bug fixes or performance improvements for ARM64 macOS systems.
2025-11-18 20:16:03 +08:00
手瓜一十雪
d25b43ebf2 Update ffmpeg native binaries for Linux
Replaces ffmpegAddon.linux.arm64.node and ffmpegAddon.linux.x64.node with new versions. This may include bug fixes, performance improvements, or compatibility updates for native ffmpeg functionality.
2025-11-18 17:36:09 +08:00
时瑾
8fe4a9e6ac 更新 Bug 反馈模板,增强对不当内容的说明和合规性要求 2025-11-16 15:55:25 +08:00
手瓜一十雪
09da80aad5 Remove unused dependencies from package.json
Deleted the 'dependencies' section from napcat-qrcode/package.json, removing references to napcat-core, napcat-common, napcat-onebot, and napcat-webui-backend. This streamlines the package configuration and may indicate these dependencies are no longer required.
2025-11-16 11:02:21 +08:00
手瓜一十雪
3d3f718fd5 Refactor interfaces and decouple backend dependencies
Introduced new interface definitions in napcat-common for logging, status, and subscription. Refactored napcat-webui-backend to use these interfaces, decoupling it from napcat-core and napcat-onebot. Moved OneBot config schema to backend and updated imports. Updated framework and shell to pass subscriptions to InitWebUi. Improved type safety and modularity across backend and shared packages.
2025-11-16 10:58:30 +08:00
手瓜一十雪
6068abdec0 Update VSCode settings for formatting and file nesting
Enhanced .vscode/settings.json with file nesting patterns, formatting preferences, auto-save behavior, and import specifier options for JavaScript and TypeScript. Removed old debug source map overrides to streamline workspace configuration.
2025-11-15 17:17:24 +08:00
手瓜一十雪
3957d7af5a Use environment variables for secret keys in dev and backend
Set fixed secret keys for JWT and WebUI in development environment via environment variables. Updated backend to use NAPCAT_WEBUI_SECRET_KEY and NAPCAT_WEBUI_JWT_SECRET_KEY from environment if available, improving configurability and security.
2025-11-15 17:00:52 +08:00
手瓜一十雪
a2837974fe Add VSCode launch and TailwindCSS config files
Added .vscode/launch.json for Node.js debugging and .vscode/tailwindcss.json for TailwindCSS directive support in VSCode. These files improve development workflow and editor integration.
2025-11-15 16:51:46 +08:00
手瓜一十雪
6f8edfe570 清理重复的 Vitest configuration file
Deleted vitest.config.ts, which contained test environment and path alias settings. This may indicate a change in testing strategy or migration away from Vitest.
2025-11-15 16:42:50 +08:00
手瓜一十雪
0b655db4dd Add test step to build workflow
Inserts 'pnpm test' into both build jobs in the GitHub Actions workflow to ensure tests are run during CI before building artifacts.
2025-11-15 16:25:06 +08:00
手瓜一十雪
d800466a30 Move inversify and reflect-metadata to napcat-core
Transferred 'inversify' and 'reflect-metadata' dependencies from the root package.json to packages/napcat-core/package.json to better scope dependencies and improve project organization.
2025-11-15 16:23:03 +08:00
手瓜一十雪
fa80441e36 Add ESLint config and update code style
Introduced a new eslint.config.js using neostandard and added related devDependencies. Updated codebase for consistent formatting, spacing, and function declarations. Minor refactoring and cleanup across multiple files to improve readability and maintain code style compliance.
2025-11-15 16:21:59 +08:00
手瓜一十雪
1990761ad6 Update main entry and tsconfig for JS support
Changed package.json main and exports to use index.js instead of index.cjs. Updated tsconfig.json to allow JavaScript files and expanded include patterns to support both JS and TS files.
2025-11-15 16:09:26 +08:00
手瓜一十雪
ef63812391 Add napcat-test package and Vitest setup
Introduces the napcat-test package with initial SHA-1 stream tests, configuration files, and scripts for running tests. Updates root package.json to include test commands and Vitest dependencies, and adds Vitest configuration at the root and package level for test environment setup.
2025-11-15 16:05:09 +08:00
手瓜一十雪
0f033b0ac8 Remove performance monitor module
Deleted performance-monitor.ts from napcat-common, removing the function statistics and reporting utility. This may indicate a refactor or replacement of performance monitoring functionality.
2025-11-15 15:13:56 +08:00
手瓜一十雪
9fdef3cde9 Revert "Update default model in release workflow"
This reverts commit d32ccc6eb5.
2025-11-15 14:56:34 +08:00
手瓜一十雪
20e8643193 Fix import paths for require_dlopen module
Updated import statements in prebuild-loader.ts and windowsPtyAgent.ts to use relative path './index' for require_dlopen. This resolves incorrect module resolution issues.
2025-11-15 14:43:08 +08:00
手瓜一十雪
8645ed4d9d Update tsconfig to include typeRoots and format paths
Added 'typeRoots' to specify custom type definitions directory and reformatted the 'paths' property for better readability. This improves TypeScript type resolution and project maintainability.
2025-11-15 14:40:06 +08:00
手瓜一十雪
c0b9817ff5 Add type checking to build workflow
Incorporates 'pnpm run typecheck' into the build steps for both frontend and shell jobs to ensure type safety during CI builds.
2025-11-15 14:01:11 +08:00
手瓜一十雪
b147e57c1c Refactor TypeScript configs to use shared base
Introduced tsconfig.base.json for shared TypeScript configuration and updated all package tsconfig.json files to extend from it, reducing duplication and improving maintainability. Also updated typecheck script in package.json and fixed import in prebuild-loader.ts.
2025-11-15 14:00:27 +08:00
手瓜一十雪
ad4a108781 feat: 大规模去耦合
Moved various helper, event, and utility files from napcat-common to napcat-core/helper for better modularity and separation of concerns. Updated imports across packages to reflect new file locations. Removed unused dependencies from napcat-common and added them to napcat-core where needed. Also consolidated type definitions and cleaned up tsconfig settings for improved compatibility.
2025-11-15 13:36:33 +08:00
手瓜一十雪
df824d77ae feat: 所有的类型检查 2025-11-15 12:57:19 +08:00
手瓜一十雪
19888d52dc Remove unused test utility files
Deleted test.ts and test2.ts from utils as they are no longer needed for the project.
2025-11-15 12:02:07 +08:00
手瓜一十雪
4dc8b3ed3b Refactor FileApi usage to obContext in OneBot
Replaced references to `core.apis.FileApi` with `obContext.apis.FileApi` across actions and message API to ensure consistent context usage. Added FileApi to the ApiListType and initialized it in NapCatOneBot11Adapter. This improves maintainability and context handling for file-related operations.
2025-11-15 11:20:01 +08:00
手瓜一十雪
8df54d5cd3 feat: 去core耦合onebot
Moved file element creation methods (for file, picture, video, and ptt) from napcat-core/apis/file.ts to a new OneBotFileApi class in napcat-onebot/api/file.ts. Updated package.json dependencies to remove unused packages and fix workspace references. This improves separation of concerns and modularity between core and onebot-specific logic.
2025-11-15 11:17:57 +08:00
手瓜一十雪
aa982b3071 Improve version folder selection in loadNapCat.cjs
Enhanced logic to handle multiple or missing version folders by selecting the most recently modified folder if more than one exists, and providing clearer error messages. Also updated .vscode/settings.json to add source map path overrides for additional packages.
2025-11-15 10:58:33 +08:00
手瓜一十雪
8e71dec63a Bind TypedEventEmitter to DI container and update usage
Added TypedEventEmitter to the dependency injection container and exposed it in ServiceBase. Updated OlPushService to use the injected event emitter instead of directly importing appEvent. Also performed minor code style improvements for consistency.
2025-11-15 10:48:59 +08:00
手瓜一十雪
31bb1e5dee Add emoji like event handling to core and onebot
Introduces a typed event emitter for app events in napcat-core, specifically for emoji like events in groups. OlPushService now emits 'event:emoji_like' when a group reaction is detected. napcat-onebot listens for this event and emits corresponding OneBot events. Refactors and adds missing type definitions and improves method formatting for consistency.
2025-11-15 10:45:02 +08:00
手瓜一十雪
75e1e8dd79 Improve error handling in release workflow
Enhances the OpenRouter API call step by adding error handling for both curl and jq failures. If the API call or response parsing fails, the workflow now falls back to using a default release note template.
2025-11-14 23:00:20 +08:00
手瓜一十雪
d32ccc6eb5 Update default model in release workflow
Changed the OPENROUTER_MODEL environment variable from 'kimi-k2-0905-turbo' to 'glm-4.6-turbo' in the release workflow configuration.
2025-11-14 22:45:17 +08:00
手瓜一十雪
7b3e94d568 Add raw response output to release workflow
Added echo statements to display the raw API response in the release workflow for improved debugging and visibility.
2025-11-14 22:35:58 +08:00
手瓜一十雪
5cfe479044 Refactor createListenerFunction type and usage
Simplifies the generic type signature of createListenerFunction and updates the way createEventFunction is called, removing unnecessary type parameters and using a direct function call with TypeScript ignore for type checking.
2025-11-14 22:24:50 +08:00
手瓜一十雪
f04ffa5dc6 Add service handler registration and DI support
Introduces dependency injection via Inversify and reflect-metadata, adds a service handler registry for packet handling, and updates core initialization to auto-register and bind service handlers. Also updates Vite configs and auto-include logic to support protocol service files.
2025-11-14 22:20:33 +08:00
手瓜一十雪
a2a73ce2dd Add dev build script and improve Vite config
Introduces a 'build:shell:dev' script for development builds and updates napcat-shell's Vite config to conditionally enable source maps in development mode. This enhances build flexibility for development and production environments.
2025-11-14 21:25:29 +08:00
手瓜一十雪
66d02eeb6a Enable source maps and improve debugging support
This commit enables source maps in napcat-shell's Vite config for better debugging, adds source map path overrides to VSCode settings, and updates nodeTest.ps1 to launch with the Node.js inspector. The autoIncludeTSPlugin transform now returns a source map for improved breakpoint support in VSCode. Also adds a sources.txt file listing project and dependency sources.
2025-11-14 21:21:49 +08:00
手瓜一十雪
b99c0ca437 Set fetch-depth to 0 in release workflow
Configures the checkout step in the release workflow to use fetch-depth: 0, ensuring the full git history is available for subsequent steps.
2025-11-14 20:06:01 +08:00
手瓜一十雪
019b90984d Add napcat-vite dependency and plugin integration
Added 'napcat-vite' as a workspace dependency in package.json and integrated its version plugin into the Vite configuration. This enables version management features provided by napcat-vite for the framework.
2025-11-14 19:54:33 +08:00
手瓜一十雪
5043a49779 feat: 装饰器与装饰器路由注册 2025-11-14 19:49:13 +08:00
手瓜一十雪
36aa08a8f5 Replace nap-proto-core with napcat-protobuf package
Switched all imports from '@napneko/nap-proto-core' to the new 'napcat-protobuf' package across napcat-core and related packages. Updated dependencies and references to support the new package structure, improving maintainability and workspace integration.
2025-11-14 16:19:26 +08:00
手瓜一十雪
8bc8df32f9 Update type declarations and remove ts-ignore comments
Added 'types' field to package.json and updated tsconfig.json to include .d.ts files for better type support in napcat-pty. Removed unnecessary @ts-ignore comments from terminal_manager.ts to improve code clarity.
2025-11-14 15:00:40 +08:00
手瓜一十雪
bc183ae002 Remove unnecessary ts-ignore comments and improve typings
Removed redundant // @ts-ignore comments from converter.ts and http-server.ts. Enhanced type safety in event.ts by refining generic parameters for createListenerFunction and createEventFunction.
2025-11-14 14:53:21 +08:00
手瓜一十雪
b85f9197e3 Remove unused path aliases from tsconfig files
Deleted redundant or unused path alias entries from tsconfig.json in napcat-core, napcat-onebot, and napcat-plugin packages to simplify TypeScript configuration and avoid confusion.
2025-11-14 14:37:43 +08:00
手瓜一十雪
c8fd66fa9b Refactor imports to use package names instead of aliases
Replaced all path alias imports (e.g., '@/napcat-core') with direct package imports (e.g., 'napcat-core/index') across napcat-common, napcat-core, and napcat-webui-backend. This improves compatibility with tooling and workspace resolution, and aligns with standard TypeScript/Node.js import practices.
2025-11-14 14:34:27 +08:00
手瓜一十雪
6e9f448a0c Add feature request issue template
Introduces a new GitHub issue template for submitting feature requests and improvement suggestions for NapCat. The template guides users to provide relevant version information, detailed descriptions, background, and expected outcomes.
2025-11-14 13:15:01 +08:00
手瓜一十雪
142016778f Rename workflow job to node-shell-docker
Changed the job name from 'trigger-napcat-release' to 'node-shell-docker' in the trigger-docker-publish.yml workflow for improved clarity.
2025-11-14 13:12:21 +08:00
手瓜一十雪
159fb8cd3a Rename workflow job from Build-LiteLoader to Build-Framework
Updated workflow job names and dependencies in auto-release.yml, build.yml, and release.yml from 'Build-LiteLoader' to 'Build-Framework' for clarity and consistency. Also updated bug report template to reference version location in WebUI instead of settings page.
2025-11-14 13:05:29 +08:00
手瓜一十雪
01c911e178 Add workflows to trigger NapCat release builds
Introduces two new GitHub Actions jobs: one to trigger NapCat AppImage builds and another for NapCat Linux Node Loader releases. Both jobs fetch the latest NapCat version tag and use fixed QQ AppImage URLs for x86_64 and arm64 architectures.
2025-11-14 13:02:16 +08:00
手瓜一十雪
8b3ea8dcef Add workflow to trigger framework Docker publish
Introduces a new job in the GitHub Actions workflow to trigger the docker-image publish workflow for the NapCat.Docker.Framework repository. This enables automated publishing of the framework Docker image alongside the existing NapCat-Docker image.
2025-11-14 12:48:46 +08:00
手瓜一十雪
fe8b270ab3 Add workflow to trigger Docker publish on release
Introduces a GitHub Actions workflow that triggers the NapCat-Docker docker-publish workflow when a release is published. This automates Docker image publishing upon new releases.
2025-11-14 12:44:26 +08:00
手瓜一十雪
f02ae5894f Update QQ x64 download URL in auto-release workflow
Replaces dynamic retrieval of the QQ x64 download URL with a static direct link in the auto-release GitHub Actions workflow. This change improves reliability by avoiding proxy and parsing steps.
2025-11-14 12:39:15 +08:00
手瓜一十雪
a6a0b408af Update QQ x64 download URL in release workflow
Changed the QQ x64 config JS URL to use a proxy service instead of the direct CDN link in the auto-release workflow. This may help bypass access restrictions or improve reliability.
2025-11-14 12:37:32 +08:00
手瓜一十雪
e9856ac80f Refactor version API to GetNapCatVersion endpoint
Replaces the PackageInfo API and related frontend usage with a dedicated GetNapCatVersion endpoint that returns only the NapCat version string. Updates backend handler, helper, types, router, and frontend components to use the new API for improved clarity and separation of concerns.
2025-11-14 12:10:57 +08:00
手瓜一十雪
f553f9dc8d fix: webui version 2025-11-14 11:51:23 +08:00
手瓜一十雪
5608638e9a Update OpenRouter API URL and model in workflow
Changed the OpenRouter API endpoint and model in the auto-release GitHub Actions workflow to use new values. This may reflect a migration to a different service or model for automated releases.
2025-11-13 21:02:28 +08:00
手瓜一十雪
a53c20767a Update artifact zipping to exclude parent folder
Changed the zip commands in the auto-release workflow to zip the contents of each artifact directory rather than the directory itself. This ensures the resulting zip files do not include an extra parent folder.
2025-11-13 20:59:31 +08:00
手瓜一十雪
a92bef5b33 Update release note prompt and workflow wording
Clarified instructions in release_note_prompt.txt to use the provided version number and updated the example version. Modified auto-release.yml to specify '当前真正的版本' in the user content for improved clarity.
2025-11-13 20:54:19 +08:00
手瓜一十雪
a9a3b6ec6e Copy QQNT.dll in auto-release workflow
Adds a step to copy QQNT.dll from napcat-develop to the output directory in the auto-release workflow. Ensures the DLL is included in release artifacts.
2025-11-13 20:51:29 +08:00
手瓜一十雪
20d41fff9e Update output directory and file copy paths in release workflow
Changed output directory from 'napcat' to 'NapCat.Shell.Windows.Node' and updated all related file copy and artifact upload paths to match. This aligns the workflow with the new directory structure and ensures correct packaging of release artifacts.
2025-11-13 20:45:51 +08:00
手瓜一十雪
0b4d7e1346 Remove unused dependencies from package.json
Deleted workspace dependencies from napcat-vite/package.json, likely because they are no longer required or managed elsewhere.
2025-11-13 20:09:44 +08:00
手瓜一十雪
46b9049a24 Update user content format in auto-release workflow
Changed the user content string in the auto-release workflow to use '当前版本' instead of 'TAG' for improved clarity in release notes.
2025-11-13 20:04:31 +08:00
手瓜一十雪
521f4dc365 Include Windows Node shell in release workflow
Adds NapCat.Shell.Windows.Node to the release process by updating dependencies, packaging steps, and release artifacts in auto-release.yml.
2025-11-13 20:02:42 +08:00
手瓜一十雪
04b507d749 Add debug output for OpenRouter request body
Prints the OpenRouter API request body in the workflow using jq for easier debugging and inspection of outgoing requests.
2025-11-13 19:59:36 +08:00
手瓜一十雪
5638127813 Add default prompt and update release workflow
Added .github/prompt/default.md with deployment instructions and download links. Updated auto-release.yml to copy NapCat.Shell directory contents instead of unzipping the archive.
2025-11-13 19:58:10 +08:00
手瓜一十雪
30a7797ba9 Reduce aria2c parallelism in auto-release workflow
Changed aria2c download options from 16 connections to 1 in the auto-release workflow. This may help avoid issues with rate limiting or unstable downloads from the Node.js distribution server.
2025-11-13 19:53:05 +08:00
394 changed files with 28717 additions and 5110 deletions

View File

@@ -1,6 +1,6 @@
name: Bug 反馈
description: 报告可能的 NapCat 异常行为
title: '[BUG] '
title: "[BUG] "
labels: bug
body:
- type: markdown
@@ -10,6 +10,10 @@ body:
在提交新的 Bug 反馈前,请确保您:
* 已经搜索了现有的 issues并且没有找到可以解决您问题的方法
* 不与现有的某一 issue 重复
* **不接受因发送不当内容而导致的问题报告**
- 包括但不限于:多媒体发送失败、转发消息失败、消息被拦截等因 18+ 内容、违规内容或触发风控的问题
- 提交 issue 前,请确认您发送的多媒体内容、链接、文本等均为正常合规内容,不会触发平台风控机制
- 因违规内容导致的问题,一律不予受理
- type: input
id: system-version
attributes:
@@ -30,7 +34,7 @@ body:
id: napcat-version
attributes:
label: NapCat 版本
description: 可在 LiteLoaderQQNT 的设置页或是 QQNT 的设置页侧栏中找到
description: 可在 WebUI 的「系统信息」页中找到
placeholder: 1.0.0
validations:
required: true

60
.github/ISSUE_TEMPLATE/feat_request.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Feat 请求
description: 提交新的 NapCat 功能或改进建议
title: '[FEAT] '
labels: enhancement
body:
- type: markdown
attributes:
value: |
欢迎来到 NapCat 的 Issue Tracker请填写以下表格来提交功能请求。
在提交新的功能请求前,请确保您:
* 已经搜索了现有的 issues并且没有找到类似的建议
* 不与现有的某一 issue 重复
- type: input
id: system-version
attributes:
label: 系统版本
description: 运行 QQNT 的系统版本
placeholder: Windows 11 24H2
validations:
required: true
- type: input
id: qqnt-version
attributes:
label: QQNT 版本
description: 可在 QQNT 的「关于」的设置页中找到
placeholder: 9.9.16-29927
validations:
required: true
- type: input
id: napcat-version
attributes:
label: NapCat 版本
description: 可在 WebUI 的「系统信息」页中找到
placeholder: 1.0.0
validations:
required: true
- type: textarea
id: feature-description
attributes:
label: 功能描述
description: 请详细描述你希望添加的功能或改进
validations:
required: true
- type: textarea
id: feature-reason
attributes:
label: 需求背景与理由
description: 请说明为什么需要这个功能,解决了什么问题
validations:
required: true
- type: textarea
id: feature-expected
attributes:
label: 期望的实现方式或效果
description: 请描述你期望的功能实现方式或最终效果
- type: textarea
id: other-info
attributes:
label: 其他补充信息
description: 你还想补充什么?

43
.github/prompt/default.md vendored Normal file
View File

@@ -0,0 +1,43 @@
# {VERSION}
[使用文档](https://napneko.github.io/)
## Windows 一键包
我们为提供了的轻量化一键部署方案
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
你可以下载
NapCat.Shell.Windows.OneKey.zip (无头)
启动后可自动化部署一键包,教程参考使用文档安装部分
## 警告
**注意QQ版本推荐使用 40768+ 版本 最低可以使用40768版本**
**默认WebUi密钥为随机密码 控制台查看**
**[9.9.26-44343 X64 Win](https://dldir1.qq.com/qqfile/qq/QQNT/40d6045a/QQ9.9.26.44343_x64.exe)**
[LinuxX64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_amd64.deb)
[LinuxX64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.rpm)
[LinuxArm64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.deb)
[LinuxArm64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_aarch64.rpm)
[MAC DMG 40990 ](https://dldir1v6.qq.com/qqfile/qq/QQNT/c6cb0f5d/QQ_v6.9.82.40990.dmg)
## 如果WinX64缺少运行库或者xxx.dll
[安装运行库](https://aka.ms/vs/17/release/vc_redist.x64.exe)
## 更新
### 🐛 修复
1. 修复 WebUI 主题配置在有未保存更改时卸载组件导致字体重置的问题 (ae42eed6)
### ✨ 新增
1. 文件上传相关接口UploadGroupFile/UploadPrivateFile新增 `upload_file` 参数支持 (91e0839e)
2. 消息发送逻辑支持 PTT语音元素过滤确保语音消息正确独立发送 (47983e29)
### 🔧 优化
1. 优化合并转发消息GetForwardMsg的获取与解析逻辑提高兼容性 (334c4233)
2. 改进消息发送方法中发送者 UIN 的处理逻辑 (71bb4f68)
3. 增强 WebUI 系统信息界面中对构建产物的处理与展示 (cb061890)
---
**完整更新日志**: [v4.10.6...v4.10.7](https://github.com/NapNeko/NapCatQQ/compare/v4.10.6...v4.10.7)

View File

@@ -1,34 +1,33 @@
注意:输出必须严格使用 NapCat 的发布说明格式,严格保证示例格式,并用简体中文。
# NapCat Release Note Generator
格式规则:
1. 第一行:# V{TAG}
2. 第二行:[使用文档](https://napneko.github.io/)
3. 空行后,按下面的节顺序输出(存在则输出,不存在则省略该节):
你是 NapCat 项目的发布说明生成器。请根据提供的 commit 列表生成标准格式的发布说明。
## Windows 一键包
- 简短一句话介绍一键包用途
- 列出可下载的文件名(只列文件名,不写下载链接)
## 核心规则
## 警告
- 如果有需要特别提醒的兼容/运行库/版本要求,写成加粗警告句
1. **版本号**:第一行必须是 `# {VERSION}`,使用用户提供的版本号,如果版本号是小写 v 开头(如 v4.10.2),必须转换为大写 V如 V4.10.2
2. **语言**:全部使用简体中文
3. **格式**:严格按照下方模板输出,不要添加额外的 markdown 格式
## 如果WinX64缺少运行库或者xxx.dll
- 常见运行库建议
## Commit 分析规则
## 更新
按数字序列列出主要变更项,每条尽量一句话
- 前缀短 commit id例如1. 修复 get_essence_msg_list 崩溃 (a1b2c3d)
- 保持 4-18 条要点
将 commit 分类为以下类型:
- 🐛 **修复**bug fix、修复、fix 相关
- ✨ **新增**新功能、feat、add 相关
- 🔧 **优化**优化、重构、refactor、improve、perf 相关
- 📦 **依赖**deps、依赖更新通常可以忽略或合并
- 🔨 **构建**ci、build、workflow 相关(通常可以忽略)
## 开发者注意
- 列出迁移/接口断裂/配置变更;若无则省略
## 合并和筛选
额外约束:
- 语言简体中文,面向最终用户
- **合并相似项**:同一功能的多个 commit 合并为一条
- **忽略琐碎项**合并冲突、格式化、typo 等可忽略
- **控制数量**:最终保持 5-15 条更新要点
- **保留 commit hash**:每条末尾附上短 hash格式 `(a1b2c3d)`
下面为真实示例,请完全参考
## 输出模板 - 必须严格遵守以下格式
# V?.?.?
```
# {VERSION}
[使用文档](https://napneko.github.io/)
## Windows 一键包
@@ -37,7 +36,7 @@
你可以下载
NapCat.Shell.Windows.OneKey.zip (无头)
NapCat.Shell.Windows.OneKey.zip (无头)
启动后可自动化部署一键包,教程参考使用文档安装部分
@@ -45,16 +44,68 @@ NapCat.Shell.Windows.OneKey.zip (无头)
**注意QQ版本推荐使用 40768+ 版本 最低可以使用40768版本**
**默认WebUi密钥为随机密码 控制台查看**
**[9.9.22-40990 X64 Win](https://dldir1v6.qq.com/qqfile/qq/QQNT/2c9d3f6c/QQ9.9.22.40990_x64.exe)**
[LinuxX64 DEB 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_amd64.deb)
[LinuxX64 RPM 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_x86_64.rpm)
[LinuxArm64 DEB 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_arm64.deb)
[LinuxArm64 RPM 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_aarch64.rpm)
**[9.9.26-44343 X64 Win](https://dldir1.qq.com/qqfile/qq/QQNT/40d6045a/QQ9.9.26.44343_x64.exe)**
[LinuxX64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_amd64.deb)
[LinuxX64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.rpm)
[LinuxArm64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.deb)
[LinuxArm64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_aarch64.rpm)
[MAC DMG 40990 ](https://dldir1v6.qq.com/qqfile/qq/QQNT/c6cb0f5d/QQ_v6.9.82.40990.dmg)
## 如果WinX64缺少运行库或者xxx.dll
[安装运行库](https://aka.ms/vs/17/release/vc_redist.x64.exe)
## 更新
1. 修改了XXXXX
2. 新增了XXXX
3. 重构了XXXX
### 🐛 修复
1. 修复 xxx 问题 (a1b2c3d)
2. 修复 yyy 崩溃 (b2c3d4e)
### ✨ 新增
1. 新增 xxx 功能 (c3d4e5f)
2. 支持 yyy 特性 (d4e5f6g)
### 🔧 优化
1. 优化 xxx 性能 (e5f6g7h)
2. 重构 yyy 模块 (f6g7h8i)
---
**完整更新日志**: [{PREV_VERSION}...{VERSION}](https://github.com/NapNeko/NapCatQQ/compare/{PREV_VERSION}...{VERSION})
```
**格式要求 - 务必严格遵守:**
- "Windows 一键包"部分的文本必须完全一致,不要修改任何措辞
- "警告"部分必须包含所有 QQ 版本下载链接,保持原有格式
- "如果WinX64缺少运行库或者xxx.dll"这一行必须保持原样
- QQ 版本号和下载链接保持不变40990 版本)
- 只有"## 更新"部分下面的内容需要根据实际 commit 生成
## 重要约束
1. 如果某个分类没有内容,则完全省略该分类
2. 不要编造不存在的更新
3. 保持简洁,每条更新控制在一行内
4. 使用用户友好的语言,避免过于技术化的描述
5. 重大变更Breaking Changes需要在注意事项中加粗提示
## 文件变化分析
用户会提供文件变化统计和具体代码diff帮助你理解变更内容
### 目录含义
- `packages/napcat-core/` → 核心功能、消息处理、QQ接口
- `packages/napcat-onebot/` → OneBot 协议实现、API、事件
- `packages/napcat-webui-backend/` → WebUI 后端接口
- `packages/napcat-webui-frontend/` → WebUI 前端界面
- `packages/napcat-shell/` → Shell 启动器
### 代码diff阅读指南
- `+` 开头的行是新增代码
- `-` 开头的行是删除代码
- 关注函数名、类名的变化来理解功能变更
- 关注 `fix`、`bug`、`error` 等关键词识别修复项
- 关注 `add`、`new`、`feature` 等关键词识别新功能
- 忽略纯重构(代码移动但功能不变)和格式化变更
### 截断说明
- 如果看到 `[... 已截断 ...]`,表示内容过长被截断
- 根据已有信息推断完整变更意图即可

231
.github/scripts/lib/comment.ts vendored Normal file
View File

@@ -0,0 +1,231 @@
/**
* 构建状态评论模板
*/
export const COMMENT_MARKER = '<!-- napcat-pr-build -->';
export type BuildStatus = 'success' | 'failure' | 'cancelled' | 'pending' | 'unknown';
export interface BuildTarget {
name: string;
status: BuildStatus;
error?: string;
downloadUrl?: string; // Artifact 直接下载链接
}
// ============== 辅助函数 ==============
function formatSha (sha: string): string {
return sha && sha.length >= 7 ? sha.substring(0, 7) : sha || 'unknown';
}
function escapeCodeBlock (text: string): string {
// 替换 ``` 为转义形式,避免破坏 Markdown 代码块
return text.replace(/```/g, '\\`\\`\\`');
}
function getTimeString (): string {
return new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
}
// ============== 状态图标 ==============
export function getStatusIcon (status: BuildStatus): string {
switch (status) {
case 'success':
return '✅ 成功';
case 'pending':
return '⏳ 构建中...';
case 'cancelled':
return '⚪ 已取消';
case 'failure':
return '❌ 失败';
default:
return '❓ 未知';
}
}
function getStatusEmoji (status: BuildStatus): string {
switch (status) {
case 'success': return '✅';
case 'pending': return '⏳';
case 'cancelled': return '⚪';
case 'failure': return '❌';
default: return '❓';
}
}
// ============== 构建中评论 ==============
export function generateBuildingComment (prSha: string, targets: string[]): string {
const time = getTimeString();
const shortSha = formatSha(prSha);
const lines: string[] = [
COMMENT_MARKER,
'',
'<div align="center">',
'',
'# 🔨 NapCat 构建中',
'',
'![Building](https://img.shields.io/badge/状态-构建中-yellow?style=for-the-badge&logo=github-actions&logoColor=white)',
'',
'</div>',
'',
'---',
'',
'## 📦 构建目标',
'',
'| 包名 | 状态 | 说明 |',
'| :--- | :---: | :--- |',
...targets.map(name => `| \`${name}\` | ⏳ | 正在构建... |`),
'',
'---',
'',
'## 📋 构建信息',
'',
`| 项目 | 值 |`,
`| :--- | :--- |`,
`| 📝 提交 | \`${shortSha}\` |`,
`| 🕐 开始时间 | ${time} |`,
'',
'---',
'',
'<div align="center">',
'',
'> ⏳ **构建进行中,请稍候...**',
'>',
'> 构建完成后将自动更新此评论',
'',
'</div>',
];
return lines.join('\n');
}
// ============== 构建结果评论 ==============
export function generateResultComment (
targets: BuildTarget[],
prSha: string,
runId: string,
repository: string,
version?: string
): string {
const runUrl = `https://github.com/${repository}/actions/runs/${runId}`;
const shortSha = formatSha(prSha);
const time = getTimeString();
const allSuccess = targets.every(t => t.status === 'success');
const anyCancelled = targets.some(t => t.status === 'cancelled');
const anyFailure = targets.some(t => t.status === 'failure');
// 状态徽章
let statusBadge: string;
let headerTitle: string;
if (allSuccess) {
statusBadge = '![Success](https://img.shields.io/badge/状态-构建成功-success?style=for-the-badge&logo=github-actions&logoColor=white)';
headerTitle = '# ✅ NapCat 构建成功';
} else if (anyCancelled && !anyFailure) {
statusBadge = '![Cancelled](https://img.shields.io/badge/状态-已取消-lightgrey?style=for-the-badge&logo=github-actions&logoColor=white)';
headerTitle = '# ⚪ NapCat 构建已取消';
} else {
statusBadge = '![Failed](https://img.shields.io/badge/状态-构建失败-critical?style=for-the-badge&logo=github-actions&logoColor=white)';
headerTitle = '# ❌ NapCat 构建失败';
}
const downloadLink = (target: BuildTarget) => {
if (target.status !== 'success') return '—';
if (target.downloadUrl) {
return `[📥 下载](${target.downloadUrl})`;
}
return `[📥 下载](${runUrl}#artifacts)`;
};
const lines: string[] = [
COMMENT_MARKER,
'',
'<div align="center">',
'',
headerTitle,
'',
statusBadge,
'',
'</div>',
'',
'---',
'',
'## 📦 构建产物',
'',
'| 包名 | 状态 | 下载 |',
'| :--- | :---: | :---: |',
...targets.map(t => `| \`${t.name}\` | ${getStatusEmoji(t.status)} ${t.status === 'success' ? '成功' : t.status === 'failure' ? '失败' : t.status === 'cancelled' ? '已取消' : '未知'} | ${downloadLink(t)} |`),
'',
'---',
'',
'## 📋 构建信息',
'',
`| 项目 | 值 |`,
`| :--- | :--- |`,
...(version ? [`| 🏷️ 版本号 | \`${version}\` |`] : []),
`| 📝 提交 | \`${shortSha}\` |`,
`| 🔗 构建日志 | [查看详情](${runUrl}) |`,
`| 🕐 完成时间 | ${time} |`,
];
// 添加错误详情
const failedTargets = targets.filter(t => t.status === 'failure' && t.error);
if (failedTargets.length > 0) {
lines.push('', '---', '', '## ⚠️ 错误详情', '');
for (const target of failedTargets) {
lines.push(
`<details>`,
`<summary>🔴 <b>${target.name}</b> 构建错误</summary>`,
'',
'```',
escapeCodeBlock(target.error!),
'```',
'',
'</details>',
''
);
}
}
// 添加底部提示
lines.push('---', '');
if (allSuccess) {
lines.push(
'<div align="center">',
'',
'> 🎉 **所有构建均已成功完成!**',
'>',
'> 点击上方下载链接获取构建产物进行测试',
'',
'</div>'
);
} else if (anyCancelled && !anyFailure) {
lines.push(
'<div align="center">',
'',
'> ⚪ **构建已被取消**',
'>',
'> 可能是由于新的提交触发了新的构建',
'',
'</div>'
);
} else {
lines.push(
'<div align="center">',
'',
'> ⚠️ **部分构建失败**',
'>',
'> 请查看上方错误详情或点击构建日志查看完整输出',
'',
'</div>'
);
}
return lines.join('\n');
}

189
.github/scripts/lib/github.ts vendored Normal file
View File

@@ -0,0 +1,189 @@
/**
* GitHub API 工具库
*/
import { appendFileSync } from 'node:fs';
// ============== 类型定义 ==============
export interface PullRequest {
number: number;
state: string;
head: {
sha: string;
ref: string;
repo: {
full_name: string;
};
};
}
export interface Repository {
owner: {
type: string;
};
}
export interface Artifact {
id: number;
name: string;
size_in_bytes: number;
archive_download_url: string;
}
// ============== GitHub API Client ==========================
export class GitHubAPI {
private token: string;
private baseUrl = 'https://api.github.com';
constructor (token: string) {
this.token = token;
}
private async request<T> (endpoint: string, options: RequestInit = {}): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
Authorization: `Bearer ${this.token}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
...options.headers,
},
});
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
}
return response.json() as Promise<T>;
}
async getPullRequest (owner: string, repo: string, pullNumber: number): Promise<PullRequest> {
return this.request<PullRequest>(`/repos/${owner}/${repo}/pulls/${pullNumber}`);
}
async getCollaboratorPermission (owner: string, repo: string, username: string): Promise<string> {
const data = await this.request<{ permission: string; }>(
`/repos/${owner}/${repo}/collaborators/${username}/permission`
);
return data.permission;
}
async getRepository (owner: string, repo: string): Promise<Repository> {
return this.request(`/repos/${owner}/${repo}`);
}
async checkOrgMembership (org: string, username: string): Promise<boolean> {
try {
await this.request(`/orgs/${org}/members/${username}`);
return true;
} catch {
return false;
}
}
async getRunArtifacts (owner: string, repo: string, runId: string): Promise<Artifact[]> {
const data = await this.request<{ artifacts: Artifact[]; }>(
`/repos/${owner}/${repo}/actions/runs/${runId}/artifacts`
);
return data.artifacts;
}
async createComment (owner: string, repo: string, issueNumber: number, body: string): Promise<void> {
await this.request(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
method: 'POST',
body: JSON.stringify({ body }),
headers: { 'Content-Type': 'application/json' },
});
}
async findComment (owner: string, repo: string, issueNumber: number, marker: string): Promise<number | null> {
let page = 1;
const perPage = 100;
while (page <= 10) { // 最多检查 1000 条评论
const comments = await this.request<Array<{ id: number, body: string; }>>(
`/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=${perPage}&page=${page}`
);
if (comments.length === 0) {
return null;
}
const found = comments.find(c => c.body.includes(marker));
if (found) {
return found.id;
}
if (comments.length < perPage) {
return null;
}
page++;
}
return null;
}
async updateComment (owner: string, repo: string, commentId: number, body: string): Promise<void> {
await this.request(`/repos/${owner}/${repo}/issues/comments/${commentId}`, {
method: 'PATCH',
body: JSON.stringify({ body }),
headers: { 'Content-Type': 'application/json' },
});
}
async createOrUpdateComment (
owner: string,
repo: string,
issueNumber: number,
body: string,
marker: string
): Promise<void> {
const existingId = await this.findComment(owner, repo, issueNumber, marker);
if (existingId) {
await this.updateComment(owner, repo, existingId, body);
console.log(`✓ Updated comment #${existingId}`);
} else {
await this.createComment(owner, repo, issueNumber, body);
console.log('✓ Created new comment');
}
}
}
// ============== Output 工具 ==============
export function setOutput (name: string, value: string): void {
const outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
appendFileSync(outputFile, `${name}=${value}\n`);
}
console.log(` ${name}=${value}`);
}
export function setMultilineOutput (name: string, value: string): void {
const outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
const delimiter = `EOF_${Date.now()}`;
appendFileSync(outputFile, `${name}<<${delimiter}\n${value}\n${delimiter}\n`);
}
}
// ============== 环境变量工具 ==============
export function getEnv (name: string, required: true): string;
export function getEnv (name: string, required?: false): string | undefined;
export function getEnv (name: string, required = false): string | undefined {
const value = process.env[name];
if (required && !value) {
throw new Error(`Environment variable ${name} is required`);
}
return value;
}
export function getRepository (): { owner: string, repo: string; } {
const repository = getEnv('GITHUB_REPOSITORY', true);
const [owner, repo] = repository.split('/');
return { owner, repo };
}

36
.github/scripts/pr-build-building.ts vendored Normal file
View File

@@ -0,0 +1,36 @@
/**
* PR Build - 更新构建中状态评论
*
* 环境变量:
* - GITHUB_TOKEN: GitHub API Token
* - PR_NUMBER: PR 编号
* - PR_SHA: PR 提交 SHA
*/
import { GitHubAPI, getEnv, getRepository } from './lib/github.ts';
import { generateBuildingComment, COMMENT_MARKER } from './lib/comment.ts';
const BUILD_TARGETS = ['NapCat.Framework', 'NapCat.Shell'];
async function main (): Promise<void> {
console.log('🔨 Updating building status comment\n');
const token = getEnv('GITHUB_TOKEN', true);
const prNumber = parseInt(getEnv('PR_NUMBER', true), 10);
const prSha = getEnv('PR_SHA', true);
const { owner, repo } = getRepository();
console.log(`PR: #${prNumber}`);
console.log(`SHA: ${prSha}`);
console.log(`Repo: ${owner}/${repo}\n`);
const github = new GitHubAPI(token);
const comment = generateBuildingComment(prSha, BUILD_TARGETS);
await github.createOrUpdateComment(owner, repo, prNumber, comment, COMMENT_MARKER);
}
main().catch((error) => {
console.error('❌ Error:', error);
process.exit(1);
});

206
.github/scripts/pr-build-check.ts vendored Normal file
View File

@@ -0,0 +1,206 @@
/**
* PR Build Check Script
* 检查 PR 构建触发条件和用户权限
*
* 环境变量:
* - GITHUB_TOKEN: GitHub API Token
* - GITHUB_EVENT_NAME: 事件名称
* - GITHUB_EVENT_PATH: 事件 payload 文件路径
* - GITHUB_REPOSITORY: 仓库名称 (owner/repo)
* - GITHUB_OUTPUT: 输出文件路径
*/
import { readFileSync } from 'node:fs';
import { GitHubAPI, getEnv, getRepository, setOutput } from './lib/github.ts';
import type { PullRequest } from './lib/github.ts';
// ============== 类型定义 ==============
interface GitHubPayload {
pull_request?: PullRequest;
issue?: {
number: number;
pull_request?: object;
};
comment?: {
body: string;
user: { login: string; };
};
}
interface CheckResult {
should_build: boolean;
pr_number?: number;
pr_sha?: string;
pr_head_repo?: string;
pr_head_ref?: string;
}
// ============== 权限检查 ==============
async function checkUserPermission (
github: GitHubAPI,
owner: string,
repo: string,
username: string
): Promise<boolean> {
// 方法1检查仓库协作者权限
try {
const permission = await github.getCollaboratorPermission(owner, repo, username);
if (['admin', 'write', 'maintain'].includes(permission)) {
console.log(`✓ User ${username} has ${permission} permission`);
return true;
}
console.log(`✗ User ${username} has ${permission} permission (insufficient)`);
} catch (e) {
console.log(`✗ Failed to get collaborator permission: ${(e as Error).message}`);
}
// 方法2检查组织成员身份
try {
const repoInfo = await github.getRepository(owner, repo);
if (repoInfo.owner.type === 'Organization') {
const isMember = await github.checkOrgMembership(owner, username);
if (isMember) {
console.log(`✓ User ${username} is organization member`);
return true;
}
console.log(`✗ User ${username} is not organization member`);
}
} catch (e) {
console.log(`✗ Failed to check org membership: ${(e as Error).message}`);
}
return false;
}
// ============== 事件处理 ==============
function handlePullRequestTarget (payload: GitHubPayload): CheckResult {
const pr = payload.pull_request;
if (!pr) {
console.log('✗ No pull_request in payload');
return { should_build: false };
}
if (pr.state !== 'open') {
console.log(`✗ PR is not open (state: ${pr.state})`);
return { should_build: false };
}
console.log(`✓ PR #${pr.number} is open, triggering build`);
return {
should_build: true,
pr_number: pr.number,
pr_sha: pr.head.sha,
pr_head_repo: pr.head.repo.full_name,
pr_head_ref: pr.head.ref,
};
}
async function handleIssueComment (
payload: GitHubPayload,
github: GitHubAPI,
owner: string,
repo: string
): Promise<CheckResult> {
const { issue, comment } = payload;
if (!issue || !comment) {
console.log('✗ No issue or comment in payload');
return { should_build: false };
}
// 检查是否是 PR 的评论
if (!issue.pull_request) {
console.log('✗ Comment is not on a PR');
return { should_build: false };
}
// 检查是否是 /build 命令
if (!comment.body.trim().startsWith('/build')) {
console.log('✗ Comment is not a /build command');
return { should_build: false };
}
console.log(`→ /build command from @${comment.user.login}`);
// 获取 PR 详情
const pr = await github.getPullRequest(owner, repo, issue.number);
// 检查 PR 状态
if (pr.state !== 'open') {
console.log(`✗ PR is not open (state: ${pr.state})`);
await github.createComment(owner, repo, issue.number, '⚠️ 此 PR 已关闭,无法触发构建。');
return { should_build: false };
}
// 检查用户权限
const username = comment.user.login;
const hasPermission = await checkUserPermission(github, owner, repo, username);
if (!hasPermission) {
console.log(`✗ User ${username} has no permission`);
await github.createComment(
owner,
repo,
issue.number,
`⚠️ @${username} 您没有权限使用 \`/build\` 命令,仅仓库协作者或组织成员可使用。`
);
return { should_build: false };
}
console.log(`✓ Build triggered by @${username}`);
return {
should_build: true,
pr_number: issue.number,
pr_sha: pr.head.sha,
pr_head_repo: pr.head.repo.full_name,
pr_head_ref: pr.head.ref,
};
}
// ============== 主函数 ==============
async function main (): Promise<void> {
console.log('🔍 PR Build Check\n');
const token = getEnv('GITHUB_TOKEN', true);
const eventName = getEnv('GITHUB_EVENT_NAME', true);
const eventPath = getEnv('GITHUB_EVENT_PATH', true);
const { owner, repo } = getRepository();
console.log(`Event: ${eventName}`);
console.log(`Repository: ${owner}/${repo}\n`);
const payload = JSON.parse(readFileSync(eventPath, 'utf-8')) as GitHubPayload;
const github = new GitHubAPI(token);
let result: CheckResult;
switch (eventName) {
case 'pull_request_target':
result = handlePullRequestTarget(payload);
break;
case 'issue_comment':
result = await handleIssueComment(payload, github, owner, repo);
break;
default:
console.log(`✗ Unsupported event: ${eventName}`);
result = { should_build: false };
}
// 输出结果
console.log('\n=== Outputs ===');
setOutput('should_build', String(result.should_build));
setOutput('pr_number', String(result.pr_number ?? ''));
setOutput('pr_sha', result.pr_sha ?? '');
setOutput('pr_head_repo', result.pr_head_repo ?? '');
setOutput('pr_head_ref', result.pr_head_ref ?? '');
}
main().catch((error) => {
console.error('❌ Error:', error);
process.exit(1);
});

90
.github/scripts/pr-build-result.ts vendored Normal file
View File

@@ -0,0 +1,90 @@
/**
* PR Build - 更新构建结果评论
*
* 环境变量:
* - GITHUB_TOKEN: GitHub API Token
* - PR_NUMBER: PR 编号
* - PR_SHA: PR 提交 SHA
* - RUN_ID: GitHub Actions Run ID
* - NAPCAT_VERSION: 构建版本号
* - FRAMEWORK_STATUS: Framework 构建状态
* - FRAMEWORK_ERROR: Framework 构建错误信息
* - SHELL_STATUS: Shell 构建状态
* - SHELL_ERROR: Shell 构建错误信息
*/
import { GitHubAPI, getEnv, getRepository } from './lib/github.ts';
import { generateResultComment, COMMENT_MARKER } from './lib/comment.ts';
import type { BuildTarget, BuildStatus } from './lib/comment.ts';
function parseStatus (value: string | undefined): BuildStatus {
if (value === 'success' || value === 'failure' || value === 'cancelled') {
return value;
}
return 'unknown';
}
async function main (): Promise<void> {
console.log('📝 Updating build result comment\n');
const token = getEnv('GITHUB_TOKEN', true);
const prNumber = parseInt(getEnv('PR_NUMBER', true), 10);
const prSha = getEnv('PR_SHA') || 'unknown';
const runId = getEnv('RUN_ID', true);
const version = getEnv('NAPCAT_VERSION') || '';
const { owner, repo } = getRepository();
const frameworkStatus = parseStatus(getEnv('FRAMEWORK_STATUS'));
const frameworkError = getEnv('FRAMEWORK_ERROR');
const shellStatus = parseStatus(getEnv('SHELL_STATUS'));
const shellError = getEnv('SHELL_ERROR');
console.log(`PR: #${prNumber}`);
console.log(`SHA: ${prSha}`);
console.log(`Version: ${version}`);
console.log(`Run: ${runId}`);
console.log(`Framework: ${frameworkStatus}${frameworkError ? ` (${frameworkError})` : ''}`);
console.log(`Shell: ${shellStatus}${shellError ? ` (${shellError})` : ''}\n`);
const github = new GitHubAPI(token);
const repository = `${owner}/${repo}`;
// 获取 artifacts 列表,生成直接下载链接
const artifactMap: Record<string, string> = {};
try {
const artifacts = await github.getRunArtifacts(owner, repo, runId);
console.log(`Found ${artifacts.length} artifacts`);
for (const artifact of artifacts) {
// 生成直接下载链接https://github.com/{owner}/{repo}/actions/runs/{run_id}/artifacts/{artifact_id}
const downloadUrl = `https://github.com/${repository}/actions/runs/${runId}/artifacts/${artifact.id}`;
artifactMap[artifact.name] = downloadUrl;
console.log(` - ${artifact.name}: ${downloadUrl}`);
}
} catch (e) {
console.log(`Warning: Failed to get artifacts: ${(e as Error).message}`);
}
const targets: BuildTarget[] = [
{
name: 'NapCat.Framework',
status: frameworkStatus,
error: frameworkError,
downloadUrl: artifactMap['NapCat.Framework'],
},
{
name: 'NapCat.Shell',
status: shellStatus,
error: shellError,
downloadUrl: artifactMap['NapCat.Shell'],
},
];
const comment = generateResultComment(targets, prSha, runId, repository, version);
await github.createOrUpdateComment(owner, repo, prNumber, comment, COMMENT_MARKER);
}
main().catch((error) => {
console.error('❌ Error:', error);
process.exit(1);
});

149
.github/scripts/pr-build-run.ts vendored Normal file
View File

@@ -0,0 +1,149 @@
/**
* PR Build Runner
* 执行构建步骤
*
* 用法: node pr-build-run.ts <target>
* target: framework | shell
*/
import { execSync } from 'node:child_process';
import { existsSync, renameSync, unlinkSync } from 'node:fs';
import { setOutput } from './lib/github.ts';
type BuildTarget = 'framework' | 'shell';
interface BuildStep {
name: string;
command: string;
errorMessage: string;
}
// ============== 构建步骤 ==============
function getCommonSteps (): BuildStep[] {
return [
{
name: 'Install pnpm',
command: 'npm i -g pnpm',
errorMessage: 'Failed to install pnpm',
},
{
name: 'Install dependencies',
command: 'pnpm i',
errorMessage: 'Failed to install dependencies',
},
{
name: 'Type check',
command: 'pnpm run typecheck',
errorMessage: 'Type check failed',
},
{
name: 'Test',
command: 'pnpm test',
errorMessage: 'Tests failed',
},
{
name: 'Build WebUI',
command: 'pnpm --filter napcat-webui-frontend run build',
errorMessage: 'WebUI build failed',
},
];
}
function getTargetSteps (target: BuildTarget): BuildStep[] {
if (target === 'framework') {
return [
{
name: 'Build Framework',
command: 'pnpm run build:framework',
errorMessage: 'Framework build failed',
},
];
}
return [
{
name: 'Build Shell',
command: 'pnpm run build:shell',
errorMessage: 'Shell build failed',
},
];
}
// ============== 执行器 ==============
function runStep (step: BuildStep): boolean {
console.log(`\n::group::${step.name}`);
console.log(`> ${step.command}\n`);
try {
execSync(step.command, {
stdio: 'inherit',
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash',
});
console.log('::endgroup::');
console.log(`${step.name}`);
return true;
} catch (_error) {
console.log('::endgroup::');
console.log(`${step.name}`);
setOutput('error', step.errorMessage);
return false;
}
}
function postBuild (target: BuildTarget): void {
const srcDir = target === 'framework'
? 'packages/napcat-framework/dist'
: 'packages/napcat-shell/dist';
const destDir = target === 'framework' ? 'framework-dist' : 'shell-dist';
console.log(`\n→ Moving ${srcDir} to ${destDir}`);
if (!existsSync(srcDir)) {
throw new Error(`Build output not found: ${srcDir}`);
}
renameSync(srcDir, destDir);
// Install production dependencies
console.log('→ Installing production dependencies');
execSync('npm install --omit=dev', {
cwd: destDir,
stdio: 'inherit',
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash',
});
// Remove package-lock.json
const lockFile = `${destDir}/package-lock.json`;
if (existsSync(lockFile)) {
unlinkSync(lockFile);
}
console.log(`✓ Build output ready at ${destDir}`);
}
// ============== 主函数 ==============
function main (): void {
const target = process.argv[2] as BuildTarget;
if (!target || !['framework', 'shell'].includes(target)) {
console.error('Usage: node pr-build-run.ts <framework|shell>');
process.exit(1);
}
console.log(`🔨 Building NapCat.${target === 'framework' ? 'Framework' : 'Shell'}\n`);
const steps = [...getCommonSteps(), ...getTargetSteps(target)];
for (const step of steps) {
if (!runStep(step)) {
process.exit(1);
}
}
postBuild(target);
console.log('\n✅ Build completed successfully!');
}
main();

View File

@@ -1,268 +1,83 @@
name: AI RELEASE NapCat
name: Auto Release Docker
on:
workflow_dispatch:
push:
tags:
- '*'
permissions: write-all
env:
OPENROUTER_API_URL: https://openrouter.ai/api/v1/chat/completions
OPENROUTER_MODEL: "openrouter/auto"
RELEASE_NAME: "NapCat"
release:
types: [published]
jobs:
Build-LiteLoader:
shell-docker:
runs-on: ubuntu-latest
steps:
- name: Clone Main Repository
uses: actions/checkout@v4
- name: Use Node.js 20.X
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Build NapCat.Framework
run: |
npm i -g pnpm
pnpm i
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:framework
mv packages/napcat-framework/dist framework-dist
cd framework-dist
npm install --omit=dev
rm ./package-lock.json || exit 0
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: NapCat.Framework
path: framework-dist
Build-Shell:
runs-on: ubuntu-latest
steps:
- name: Clone Main Repository
uses: actions/checkout@v4
- name: Use Node.js 20.X
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Build NapCat.Shell
run: |
npm i -g pnpm
pnpm i
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:shell
mv packages/napcat-shell/dist shell-dist
cd shell-dist
npm install --omit=dev
rm ./package-lock.json || exit 0
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: NapCat.Shell
path: shell-dist
Download-QNX64:
needs: Build-Shell
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
path: ./artifacts
- name: Setup tools
run: |
sudo apt update
sudo apt install -y aria2 unzip zip p7zip-full curl jq
- name: Download QQ x64, Node.js and Assemble NapCat.Shell.Windows.Node.zip
run: |
set -euo pipefail
TMPDIR=$(mktemp -d)
cd "$TMPDIR"
# -----------------------------
# 1) 下载 QQ x64
# -----------------------------
JS_URL="https://cdn-go.cn/qq-web/im.qq.com_new/latest/rainbow/windowsConfig.js"
NT_URL=$(curl -fsSL "$JS_URL" | grep -oP '"ntDownloadX64Url"\s*:\s*"\K[^"]+')
QQ_ZIP="$(basename "$NT_URL")"
aria2c -x16 -s16 -k1M -o "$QQ_ZIP" "$NT_URL"
QQ_EXTRACT="$TMPDIR/qq_extracted"
mkdir -p "$QQ_EXTRACT"
7z x -y -o"$QQ_EXTRACT" "$QQ_ZIP" >/dev/null
# -----------------------------
# 2) 下载 Node.js Windows x64 zip 22.11.0
# -----------------------------
NODE_VER="22.11.0"
NODE_URL="https://nodejs.org/dist/v$NODE_VER/node-v$NODE_VER-win-x64.zip"
NODE_ZIP="node-v$NODE_VER-win-x64.zip"
aria2c -x16 -s16 -k1M -o "$NODE_ZIP" "$NODE_URL"
NODE_EXTRACT="$TMPDIR/node_extracted"
mkdir -p "$NODE_EXTRACT"
unzip -q "$NODE_ZIP" -d "$NODE_EXTRACT"
# -----------------------------
# 3) 创建输出目录
# -----------------------------
OUT_DIR="$GITHUB_WORKSPACE/napcat"
mkdir -p "$OUT_DIR/napcat"
# -----------------------------
# 4) 解压 NapCat.Shell.zip 到 napcat
# -----------------------------
unzip -q "$GITHUB_WORKSPACE/artifacts/NapCat.Shell.zip" -d "$OUT_DIR/napcat"
# -----------------------------
# 5) 拷贝 QQ 文件到 napcat
# -----------------------------
QQ_TARGETS=("avif_convert.dll" "broadcast_ipc.dll" "config.json" "libglib-2.0-0.dll" "libgobject-2.0-0.dll" "libvips-42.dll" "ncnn.dll" "opencv.dll" "package.json" "QBar.dll" "wrapper.node")
for name in "${QQ_TARGETS[@]}"; do
find "$QQ_EXTRACT" -iname "$name" -exec cp -a {} "$OUT_DIR/napcat" \; || true
done
# -----------------------------
# 6) 拷贝仓库文件 napcat.bat 和 index.js
# -----------------------------
cp -a "$GITHUB_WORKSPACE/packages/napcat-develop/napcat.bat" "$OUT_DIR/napcat/" || true
cp -a "$GITHUB_WORKSPACE/packages/napcat-develop/index.js" "$OUT_DIR/napcat/" || true
# -----------------------------
# 7) 拷贝 Node.exe 到 napcat
# -----------------------------
cp -a "$NODE_EXTRACT/node-v$NODE_VER-win-x64/node.exe" "$OUT_DIR/napcat/" || true
# -----------------------------
# 8) 生成 index.txt
# -----------------------------
echo "NapCat Files Collected on $(date)" > "$GITHUB_WORKSPACE/index.txt"
# -----------------------------
# 9) 打包 napcat命名为 NapCat.Shell.Windows.Node.zip
# -----------------------------
cd "$OUT_DIR"
zip -qr "$GITHUB_WORKSPACE/NapCat.Shell.Windows.Node.zip" napcat
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: NapCat.Shell.Windows.Node
path: NapCat.Shell.Windows.Node.zip
release-napcat:
needs: [Build-LiteLoader, Build-Shell]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
path: ./artifacts
- name: Zip Artifacts
run: |
cd artifacts
[ -d NapCat.Framework ] && zip -qr ../NapCat.Framework.zip -r NapCat.Framework
[ -d NapCat.Shell ] && zip -qr ../NapCat.Shell.zip -r NapCat.Shell
cd ..
- name: Generate release note via OpenRouter
- name: Trigger NapCat-Docker docker-publish workflow
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
OPENROUTER_API_URL: ${{ env.OPENROUTER_API_URL }}
OPENROUTER_MODEL: ${{ env.OPENROUTER_MODEL }}
GITHUB_OWNER: "NapNeKo" # 替换成你的 repo owner
GITHUB_REPO: "NapCatQQ" # 替换成你的 repo 名
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
run: |
set -euo pipefail
# 当前 tag
CURRENT_TAG="${GITHUB_REF#refs/tags/}"
echo "Current tag: $CURRENT_TAG"
# 从 GitHub API 获取 tag 列表
TAGS_JSON=$(curl -s "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/tags?per_page=100")
TAGS=( $(echo "$TAGS_JSON" | jq -r '.[].name' | sort -V) )
# 找到上一个 tag
PREV_TAG=""
for i in "${!TAGS[@]}"; do
if [ "${TAGS[$i]}" = "$CURRENT_TAG" ]; then
if [ $i -gt 0 ]; then
PREV_TAG="${TAGS[$((i-1))]}"
fi
break
fi
done
if [ -z "$PREV_TAG" ]; then
echo "❌ Could not find previous tag for $CURRENT_TAG, aborting."
exit 1
fi
echo "Previous tag: $PREV_TAG"
# 强制拉取上一个 tag 和当前 tag
git fetch origin "refs/tags/$PREV_TAG:refs/tags/$PREV_TAG" --force
git fetch origin "refs/tags/$CURRENT_TAG:refs/tags/$CURRENT_TAG" --force
# 获取 commit title + body + 作者,保留换行
COMMITS=$(git log --pretty=format:'%h %B (%an)' "$PREV_TAG".."$CURRENT_TAG" | sed 's/$/\\n/')
echo "Commit list from $PREV_TAG to $CURRENT_TAG:"
echo -e "$COMMITS"
# 读取 prompt
PROMPT_FILE=".github/prompt/release_note_prompt.txt"
SYSTEM_PROMPT=$(<"$PROMPT_FILE")
# 构建用户内容
USER_CONTENT="TAG: $CURRENT_TAG\n提交列表:\n$COMMITS"
# 构建请求 JSON
BODY=$(jq -n \
--arg system "$SYSTEM_PROMPT" \
--arg user "$USER_CONTENT" \
'{model: env.OPENROUTER_MODEL, messages:[{role:"system", content:$system},{role:"user", content:$user}], temperature:0.2, max_tokens:800}')
# 调用 OpenRouter
RESPONSE=$(curl -s -X POST "$OPENROUTER_API_URL" \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d "$BODY")
echo "=== OpenRouter raw response ==="
echo "$RESPONSE" | jq .
# 提取生成内容
RELEASE_BODY=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // .choices[0].text // ""')
if [ -z "$RELEASE_BODY" ]; then
echo "❌ OpenRouter failed to generate release note, terminating workflow."
exit 1
fi
# 输出到 CHANGELOG.md
echo -e "$RELEASE_BODY" > CHANGELOG.md
echo "=== generated release note ==="
cat CHANGELOG.md
- name: Create Release Draft and Upload Artifacts
uses: softprops/action-gh-release@v1
with:
name: NapCat ${{ github.ref_name }}
token: ${{ secrets.GITHUB_TOKEN }}
body_path: CHANGELOG.md
files: |
NapCat.Framework.zip
NapCat.Shell.zip
draft: true
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/NapNeko/NapCat-Docker/actions/workflows/docker-publish.yml/dispatches \
-d '{"ref":"main"}'
framework-docker:
runs-on: ubuntu-latest
steps:
- name: Trigger NapCat-Framework-Docker docker-publish workflow
env:
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
run: |
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/NapNeko/NapCat.Docker.Framework/actions/workflows/docker-image.yml/dispatches \
-d '{"ref":"main"}'
appimage-shell-docker:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Get Latest NapCat Version
id: get_version
run: |
# 获取当前仓库的最新 tag
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1))
# 输出调试信息
echo "Debug: Latest NapCat Version is ${latest_tag}"
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
- name: Trigger Release NapCat AppImage Workflow
env:
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
NAPCAT_VERSION: ${{ env.latest_tag }}
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
run: |
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_version_x86_64=${QQ_VERSION_X86_64}, qq_version_arm64=${QQ_VERSION_ARM64}"
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/NapNeko/NapCatAppImageBuild/actions/workflows/release.yml/dispatches \
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_version_x86_64\":\"${QQ_VERSION_X86_64}\",\"qq_version_arm64\":\"${QQ_VERSION_ARM64}\"}}"
node-shell-docker:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Get Latest NapCat Version
id: get_version
run: |
# 获取当前仓库的最新 tag
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1))
# 输出调试信息
echo "Debug: Latest NapCat Version is ${latest_tag}"
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
- name: Trigger Release NapCat AppImage Workflow
env:
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
NAPCAT_VERSION: ${{ env.latest_tag }}
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
run: |
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_url_amd64=${QQ_VERSION_X86_64}, qq_url_arm64=${QQ_VERSION_ARM64}"
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/NapNeko/NapCatLinuxNodeLoader/actions/workflows/release.yml/dispatches \
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_url_amd64\":\"${QQ_VERSION_X86_64}\",\"qq_url_arm64\":\"${QQ_VERSION_ARM64}\"}}"

View File

@@ -1,4 +1,4 @@
name: "Build Action"
name: Build NapCat Artifacts
on:
workflow_dispatch:
push:
@@ -8,21 +8,40 @@ on:
permissions: write-all
jobs:
Build-LiteLoader:
Build-Framework:
runs-on: ubuntu-latest
steps:
- name: Clone Main Repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # 需要完整历史来获取 tags
- name: Use Node.js 20.X
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Generate Version
run: |
# 获取最近的 release tag (格式: vX.X.X)
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
# 去掉 v 前缀
BASE_VERSION="${LATEST_TAG#v}"
SHORT_SHA="${GITHUB_SHA::7}"
VERSION="${BASE_VERSION}-main.${{ github.run_number }}+${SHORT_SHA}"
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
echo "Latest tag: ${LATEST_TAG}"
echo "Build version: ${VERSION}"
- name: Build NapCat.Framework
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
run: |
npm i -g pnpm
pnpm i
pnpm run typecheck || exit 1
pnpm test || exit 1
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:framework
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-framework/dist framework-dist
cd framework-dist
npm install --omit=dev
@@ -37,16 +56,35 @@ jobs:
steps:
- name: Clone Main Repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # 需要完整历史来获取 tags
- name: Use Node.js 20.X
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Generate Version
run: |
# 获取最近的 release tag (格式: vX.X.X)
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
# 去掉 v 前缀
BASE_VERSION="${LATEST_TAG#v}"
SHORT_SHA="${GITHUB_SHA::7}"
VERSION="${BASE_VERSION}-main.${{ github.run_number }}+${SHORT_SHA}"
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
echo "Latest tag: ${LATEST_TAG}"
echo "Build version: ${VERSION}"
- name: Build NapCat.Shell
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
run: |
npm i -g pnpm
pnpm i
pnpm run typecheck || exit 1
pnpm test || exit 1
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:shell
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-shell/dist shell-dist
cd shell-dist
npm install --omit=dev

303
.github/workflows/pr-build.yml vendored Normal file
View File

@@ -0,0 +1,303 @@
# =============================================================================
# PR 构建工作流
# =============================================================================
# 功能:
# 1. 在 PR 提交时自动构建 Framework 和 Shell 包
# 2. 支持通过 /build 命令手动触发构建(仅协作者/组织成员)
# 3. 在 PR 中发布构建状态评论,并持续更新(不会重复创建)
# 4. 支持 Fork PR 的构建(使用 pull_request_target 获取写权限)
#
# 安全说明:
# - 使用 pull_request_target 事件,在 base 分支上下文运行
# - 构建脚本始终从 base 分支 checkout避免恶意 PR 篡改脚本
# - PR 代码单独 checkout 到 workspace 目录
# =============================================================================
name: PR Build
# =============================================================================
# 触发条件
# =============================================================================
on:
# PR 事件:打开、同步(新推送)、重新打开时触发
# 注意:使用 pull_request_target 而非 pull_request以便对 Fork PR 有写权限
pull_request_target:
types: [opened, synchronize, reopened]
# Issue 评论事件:用于响应 /build 命令
issue_comment:
types: [created]
# =============================================================================
# 权限配置
# =============================================================================
permissions:
contents: read # 读取仓库内容
pull-requests: write # 写入 PR 评论
issues: write # 写入 Issue 评论(/build 命令响应)
actions: read # 读取 Actions 信息(获取构建日志链接)
# =============================================================================
# 并发控制
# =============================================================================
# 同一 PR 的多次构建会取消之前未完成的构建,避免资源浪费
# 注意:只有在 should_build=true 时才会进入实际构建流程,
# issue_comment 事件如果不是 /build 命令,会在 check-build 阶段快速退出,
# 不会取消正在进行的构建(因为 cancel-in-progress 只影响同 group 的后续任务)
concurrency:
# 使用不同的 group 策略:
# - pull_request_target: 使用 PR 号
# - issue_comment: 只有确认是 /build 命令时才使用 PR 号,否则使用 run_id不冲突
group: pr-build-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '/build') && github.event.issue.number || github.run_id }}
cancel-in-progress: true
# =============================================================================
# 任务定义
# =============================================================================
jobs:
# ---------------------------------------------------------------------------
# Job 1: 检查构建条件
# ---------------------------------------------------------------------------
# 判断是否应该触发构建:
# - pull_request_target 事件:总是触发
# - issue_comment 事件:检查是否为 /build 命令,且用户有权限
# ---------------------------------------------------------------------------
check-build:
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.check.outputs.should_build }} # 是否应该构建
pr_number: ${{ steps.check.outputs.pr_number }} # PR 编号
pr_sha: ${{ steps.check.outputs.pr_sha }} # PR 最新提交 SHA
pr_head_repo: ${{ steps.check.outputs.pr_head_repo }} # PR 源仓库(用于 Fork
pr_head_ref: ${{ steps.check.outputs.pr_head_ref }} # PR 源分支
steps:
# 仅 checkout 脚本目录,加快速度
- name: Checkout scripts
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
# 使用 Node.js 24 以支持原生 TypeScript 执行
- name: Setup Node.js 24
uses: actions/setup-node@v4
with:
node-version: 24
# 执行检查脚本,判断是否触发构建
- name: Check trigger condition
id: check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node --experimental-strip-types .github/scripts/pr-build-check.ts
# ---------------------------------------------------------------------------
# Job 2: 更新评论为"构建中"状态
# ---------------------------------------------------------------------------
# 在 PR 中创建或更新评论,显示构建正在进行中
# ---------------------------------------------------------------------------
update-comment-building:
needs: check-build
if: needs.check-build.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout scripts
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
- name: Setup Node.js 24
uses: actions/setup-node@v4
with:
node-version: 24
# 更新 PR 评论,显示构建中状态
- name: Update building comment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ needs.check-build.outputs.pr_number }}
PR_SHA: ${{ needs.check-build.outputs.pr_sha }}
run: node --experimental-strip-types .github/scripts/pr-build-building.ts
# ---------------------------------------------------------------------------
# Job 3: 构建 Framework 包
# ---------------------------------------------------------------------------
# 执行 napcat-framework 的构建流程
# ---------------------------------------------------------------------------
build-framework:
needs: [check-build, update-comment-building]
if: needs.check-build.outputs.should_build == 'true'
runs-on: ubuntu-latest
outputs:
status: ${{ steps.build.outcome }} # 构建结果success/failure
error: ${{ steps.build.outputs.error }} # 错误信息(如有)
version: ${{ steps.version.outputs.version }} # 构建版本号
steps:
# 【安全】先从 base 分支 checkout 构建脚本
# 这样即使 PR 中修改了脚本,也不会被执行
- name: Checkout scripts from base
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
path: _scripts
# 将 PR 代码 checkout 到单独的 workspace 目录
- name: Checkout PR code
uses: actions/checkout@v4
with:
repository: ${{ needs.check-build.outputs.pr_head_repo }}
ref: ${{ needs.check-build.outputs.pr_sha }}
path: workspace
fetch-depth: 0 # 需要完整历史来获取 tags
- name: Setup Node.js 24
uses: actions/setup-node@v4
with:
node-version: 24
# 获取最新 release tag 并生成版本号
- name: Generate Version
id: version
working-directory: workspace
run: |
# 获取最近的 release tag (格式: vX.X.X)
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
# 去掉 v 前缀
BASE_VERSION="${LATEST_TAG#v}"
SHORT_SHA="${{ needs.check-build.outputs.pr_sha }}"
SHORT_SHA="${SHORT_SHA::7}"
VERSION="${BASE_VERSION}-pr.${{ needs.check-build.outputs.pr_number }}.${{ github.run_number }}+${SHORT_SHA}"
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
echo "Latest tag: ${LATEST_TAG}"
echo "Build version: ${VERSION}"
# 执行构建,使用 base 分支的脚本处理 workspace 中的代码
- name: Build
id: build
working-directory: workspace
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
run: node --experimental-strip-types ../_scripts/.github/scripts/pr-build-run.ts framework
continue-on-error: true # 允许失败,后续更新评论时处理
# 构建成功时上传产物
- name: Upload Artifact
if: steps.build.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: NapCat.Framework
path: workspace/framework-dist
retention-days: 7 # 保留 7 天
# ---------------------------------------------------------------------------
# Job 4: 构建 Shell 包
# ---------------------------------------------------------------------------
# 执行 napcat-shell 的构建流程(与 Framework 并行执行)
# ---------------------------------------------------------------------------
build-shell:
needs: [check-build, update-comment-building]
if: needs.check-build.outputs.should_build == 'true'
runs-on: ubuntu-latest
outputs:
status: ${{ steps.build.outcome }} # 构建结果success/failure
error: ${{ steps.build.outputs.error }} # 错误信息(如有)
version: ${{ steps.version.outputs.version }} # 构建版本号
steps:
# 【安全】先从 base 分支 checkout 构建脚本
- name: Checkout scripts from base
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
path: _scripts
# 将 PR 代码 checkout 到单独的 workspace 目录
- name: Checkout PR code
uses: actions/checkout@v4
with:
repository: ${{ needs.check-build.outputs.pr_head_repo }}
ref: ${{ needs.check-build.outputs.pr_sha }}
path: workspace
fetch-depth: 0 # 需要完整历史来获取 tags
- name: Setup Node.js 24
uses: actions/setup-node@v4
with:
node-version: 24
# 获取最新 release tag 并生成版本号
- name: Generate Version
id: version
working-directory: workspace
run: |
# 获取最近的 release tag (格式: vX.X.X)
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
# 去掉 v 前缀
BASE_VERSION="${LATEST_TAG#v}"
SHORT_SHA="${{ needs.check-build.outputs.pr_sha }}"
SHORT_SHA="${SHORT_SHA::7}"
VERSION="${BASE_VERSION}-pr.${{ needs.check-build.outputs.pr_number }}.${{ github.run_number }}+${SHORT_SHA}"
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "Latest tag: ${LATEST_TAG}"
echo "Build version: ${VERSION}"
# 执行构建
- name: Build
id: build
working-directory: workspace
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
run: node --experimental-strip-types ../_scripts/.github/scripts/pr-build-run.ts shell
continue-on-error: true
# 构建成功时上传产物
- name: Upload Artifact
if: steps.build.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: NapCat.Shell
path: workspace/shell-dist
retention-days: 7 # 保留 7 天
# ---------------------------------------------------------------------------
# Job 5: 更新评论为构建结果
# ---------------------------------------------------------------------------
# 汇总所有构建结果,更新 PR 评论显示最终状态
# 使用 always() 确保即使构建失败/取消也会执行
# ---------------------------------------------------------------------------
update-comment-result:
needs: [check-build, update-comment-building, build-framework, build-shell]
if: always() && needs.check-build.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout scripts
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
- name: Setup Node.js 24
uses: actions/setup-node@v4
with:
node-version: 24
# 更新评论,显示构建结果和下载链接
- name: Update result comment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ needs.check-build.outputs.pr_number }}
PR_SHA: ${{ needs.check-build.outputs.pr_sha }}
RUN_ID: ${{ github.run_id }}
# 构建版本号
NAPCAT_VERSION: ${{ needs.build-framework.outputs.version || needs.build-shell.outputs.version || '' }}
# 获取构建状态,如果 job 被跳过则标记为 cancelled
FRAMEWORK_STATUS: ${{ needs.build-framework.outputs.status || 'cancelled' }}
FRAMEWORK_ERROR: ${{ needs.build-framework.outputs.error }}
SHELL_STATUS: ${{ needs.build-shell.outputs.status || 'cancelled' }}
SHELL_ERROR: ${{ needs.build-shell.outputs.error }}
run: node --experimental-strip-types .github/scripts/pr-build-result.ts

View File

@@ -1,12 +1,51 @@
name: "Build Release"
name: Release NapCat
on:
workflow_dispatch:
push:
tags:
- 'v*'
permissions: write-all
env:
OPENROUTER_API_URL: https://91vip.futureppo.top/v1/chat/completions
OPENROUTER_MODEL: "copilot/gemini-3-flash-preview"
RELEASE_NAME: "NapCat"
jobs:
Build-LiteLoader:
# 验证版本号格式
validate-version:
runs-on: ubuntu-latest
outputs:
valid: ${{ steps.check.outputs.valid }}
version: ${{ steps.check.outputs.version }}
steps:
- name: Validate semantic version
id: check
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "Checking tag: $TAG"
# 语义化版本正则表达式
# 支持: v1.0.0, v1.0.0-beta, v1.0.0-rc.1, v1.0.0-alpha.1+build.123
SEMVER_REGEX="^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?$"
if [[ "$TAG" =~ $SEMVER_REGEX ]]; then
echo "✅ Valid semantic version: $TAG"
echo "valid=true" >> $GITHUB_OUTPUT
echo "version=$TAG" >> $GITHUB_OUTPUT
else
echo "❌ Invalid version format: $TAG"
echo "Expected format: vX.Y.Z or vX.Y.Z-prerelease"
echo "Examples: v1.0.0, v1.2.3-beta, v2.0.0-rc.1"
echo "valid=false" >> $GITHUB_OUTPUT
exit 1
fi
Build-Framework:
needs: validate-version
if: needs.validate-version.outputs.valid == 'true'
runs-on: ubuntu-latest
steps:
- name: Clone Main Repository
@@ -16,11 +55,14 @@ jobs:
with:
node-version: 20.x
- name: Build NapCat.Framework
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npm i -g pnpm
pnpm i
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:framework
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-framework/dist framework-dist
cd framework-dist
npm install --omit=dev
@@ -32,6 +74,8 @@ jobs:
path: framework-dist
Build-Shell:
needs: validate-version
if: needs.validate-version.outputs.valid == 'true'
runs-on: ubuntu-latest
steps:
- name: Clone Main Repository
@@ -41,11 +85,14 @@ jobs:
with:
node-version: 20.x
- name: Build NapCat.Shell
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npm i -g pnpm
pnpm i
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:shell
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-shell/dist shell-dist
cd shell-dist
npm install --omit=dev
@@ -55,34 +102,343 @@ jobs:
with:
name: NapCat.Shell
path: shell-dist
release-napcat:
needs: [Build-LiteLoader, Build-Shell]
Download-QNX64:
needs: Build-Shell
runs-on: ubuntu-latest
steps:
- name: Download All Artifact
uses: actions/download-artifact@v4
- uses: actions/checkout@v4
- name: Compress subdirectories
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
path: ./artifacts
- name: Setup tools
run: |
cd ./NapCat.Shell/
zip -q -r NapCat.Shell.zip *
cd ..
cd ./NapCat.Framework/
zip -q -r NapCat.Framework.zip *
cd ..
rm ./NapCat.Shell.zip -rf
rm ./NapCat.Framework.zip -rf
mv ./NapCat.Shell/NapCat.Shell.zip ./
mv ./NapCat.Framework/NapCat.Framework.zip ./
sudo apt update
sudo apt install -y aria2 unzip zip p7zip-full curl jq
- name: Download QQ x64, Node.js and Assemble NapCat.Shell.Windows.Node.zip
run: |
set -euo pipefail
TMPDIR=$(mktemp -d)
cd "$TMPDIR"
# -----------------------------
# 1) 下载 QQ x64
# -----------------------------
# JS_URL="https://cdn-go.cn/qq-web/im.qq.com_new/latest/rainbow/windowsConfig.js"
# JS_URL="https://slave.docadan488.workers.dev/proxy?url=https://cdn-go.cn/qq-web/im.qq.com_new/latest/rainbow/windowsConfig.js"
# NT_URL=$(curl -fsSL "$JS_URL" | grep -oP '"ntDownloadX64Url"\s*:\s*"\K[^"]+')
NT_URL="https://dldir1v6.qq.com/qqfile/qq/QQNT/eb263b35/QQ9.9.23.42086_x64.exe"
QQ_ZIP="$(basename "$NT_URL")"
aria2c -x16 -s16 -k1M -o "$QQ_ZIP" "$NT_URL"
QQ_EXTRACT="$TMPDIR/qq_extracted"
mkdir -p "$QQ_EXTRACT"
7z x -y -o"$QQ_EXTRACT" "$QQ_ZIP" >/dev/null
# -----------------------------
# 2) 下载 Node.js Windows x64 zip 22.11.0
# -----------------------------
NODE_VER="22.11.0"
NODE_URL="https://nodejs.org/dist/v$NODE_VER/node-v$NODE_VER-win-x64.zip"
NODE_ZIP="node-v$NODE_VER-win-x64.zip"
aria2c -x1 -s1 -k1M -o "$NODE_ZIP" "$NODE_URL"
NODE_EXTRACT="$TMPDIR/node_extracted"
mkdir -p "$NODE_EXTRACT"
unzip -q "$NODE_ZIP" -d "$NODE_EXTRACT"
# -----------------------------
# 3) 创建输出目录
# -----------------------------
OUT_DIR="$GITHUB_WORKSPACE/NapCat.Shell.Windows.Node"
mkdir -p "$OUT_DIR/NapCat.Shell.Windows.Node"
# -----------------------------
# 4) 解压 NapCat.Shell.zip 到 napcat
# -----------------------------
cp -a "$GITHUB_WORKSPACE/artifacts/NapCat.Shell/." "$OUT_DIR/napcat/"
# -----------------------------
# 5) 拷贝 QQ 文件到 NapCat.Shell.Windows.Node
# -----------------------------
QQ_TARGETS=("avif_convert.dll" "broadcast_ipc.dll" "config.json" "libglib-2.0-0.dll" "libgobject-2.0-0.dll" "libvips-42.dll" "ncnn.dll" "opencv.dll" "package.json" "QBar.dll" "wrapper.node")
for name in "${QQ_TARGETS[@]}"; do
find "$QQ_EXTRACT" -iname "$name" -exec cp -a {} "$OUT_DIR" \; || true
done
# -----------------------------
# 6) 拷贝仓库文件 napcat.bat 和 index.js
# -----------------------------
cp -a "$GITHUB_WORKSPACE/packages/napcat-develop/napcat.bat" "$OUT_DIR/" || true
cp -a "$GITHUB_WORKSPACE/packages/napcat-develop/index.js" "$OUT_DIR/" || true
cp -a "$GITHUB_WORKSPACE/packages/napcat-develop/QQNT.dll" "$OUT_DIR/" || true
# -----------------------------
# 7) 拷贝 Node.exe 到 NapCat.Shell.Windows.Node
# -----------------------------
cp -a "$NODE_EXTRACT/node-v$NODE_VER-win-x64/node.exe" "$OUT_DIR/" || true
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: NapCat.Shell.Windows.Node
path: NapCat.Shell.Windows.Node
release-napcat:
needs: [Build-Framework, Build-Shell, Download-QNX64]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
path: ./artifacts
- name: Download NapCat.Shell.Windows.OneKey.zip
run: |
curl -L -o NapCat.Shell.Windows.OneKey.zip https://github.com/NapNeko/NapCatResource/raw/main/NapCat.Shell.Windows.OneKey.zip
- name: Zip Artifacts
run: |
cd artifacts
[ -d NapCat.Framework ] && (cd NapCat.Framework && zip -qr ../../NapCat.Framework.zip .)
[ -d NapCat.Shell ] && (cd NapCat.Shell && zip -qr ../../NapCat.Shell.zip .)
[ -d NapCat.Shell.Windows.Node ] && (cd NapCat.Shell.Windows.Node && zip -qr ../../NapCat.Shell.Windows.Node.zip .)
cd ..
- name: Generate release note via OpenRouter
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
OPENROUTER_API_URL: ${{ env.OPENROUTER_API_URL }}
OPENROUTER_MODEL: ${{ env.OPENROUTER_MODEL }}
GITHUB_OWNER: "NapNeko" # 替换成你的 repo owner
GITHUB_REPO: "NapCatQQ" # 替换成你的 repo 名
run: |
set -euo pipefail
# 当前 tag
CURRENT_TAG="${GITHUB_REF#refs/tags/}"
echo "Current tag: $CURRENT_TAG"
# 从 GitHub API 获取 tag 列表
TAGS_JSON=$(curl -s "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/tags?per_page=100")
TAGS=( $(echo "$TAGS_JSON" | jq -r '.[].name' | sort -V) )
# 找到上一个 tag
PREV_TAG=""
for i in "${!TAGS[@]}"; do
if [ "${TAGS[$i]}" = "$CURRENT_TAG" ]; then
if [ $i -gt 0 ]; then
PREV_TAG="${TAGS[$((i-1))]}"
fi
break
fi
done
if [ -z "$PREV_TAG" ]; then
echo "⚠️ Could not find previous tag for $CURRENT_TAG, using first commit"
PREV_TAG=$(git rev-list --max-parents=0 HEAD | head -1)
fi
echo "Previous tag: $PREV_TAG"
# 强制拉取上一个 tag 和当前 tag
git fetch origin "refs/tags/$PREV_TAG:refs/tags/$PREV_TAG" --force || true
git fetch origin "refs/tags/$CURRENT_TAG:refs/tags/$CURRENT_TAG" --force || true
# 获取 commit使用更清晰的格式
# 格式: <type>: <subject> (<hash>)
COMMITS=$(git log --pretty=format:'- %s (%h)' "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null || git log --pretty=format:'- %s (%h)' -20)
echo "Commit list from $PREV_TAG to $CURRENT_TAG:"
echo "$COMMITS"
# 获取文件变化统计
echo "Getting file change statistics..."
FILE_STATS=$(git diff --stat "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null || echo "")
# 获取总体统计(最后一行)
SUMMARY_LINE=$(echo "$FILE_STATS" | tail -1)
echo "Summary: $SUMMARY_LINE"
# 获取每个文件的变化(去掉最后一行汇总)
# 截断过长的输出最多50个文件每行最多80字符
FILE_CHANGES=$(echo "$FILE_STATS" | head -n -1 | head -50 | cut -c1-80)
# 如果文件变化太多,进一步精简:只保留主要目录的变化
FILE_COUNT=$(echo "$FILE_STATS" | head -n -1 | wc -l)
if [ "$FILE_COUNT" -gt 50 ]; then
echo "Too many files ($FILE_COUNT), grouping by directory..."
# 按目录分组统计
DIR_STATS=$(git diff --stat "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null | head -n -1 | \
sed 's/|.*//g' | \
awk -F'/' '{if(NF>1) print $1"/"$2; else print $1}' | \
sort | uniq -c | sort -rn | head -20)
FILE_CHANGES="[按目录分组统计 - 共 $FILE_COUNT 个文件变更]
$DIR_STATS"
fi
echo "File changes:"
echo "$FILE_CHANGES"
# 获取具体代码变化关键文件的diff
echo "Getting code diff for key files..."
# 定义关键目录(优先展示这些目录的变化)
KEY_DIRS="packages/napcat-core packages/napcat-onebot packages/napcat-webui-backend"
# 获取变更的关键文件列表(排除测试、配置等)
# 使用 || true 防止 grep 无匹配时返回非零退出码
KEY_FILES=$(git diff --name-only "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null | \
grep -E "^packages/napcat-(core|onebot|webui-backend|shell)/" || true | \
grep -E "\.(ts|js)$" || true | \
grep -v -E "(test|spec|\.d\.ts|config)" || true | \
head -15) || true
CODE_DIFF=""
DIFF_CHAR_LIMIT=6000 # 总diff字符限制
CURRENT_CHARS=0
if [ -n "$KEY_FILES" ]; then
for file in $KEY_FILES; do
if [ "$CURRENT_CHARS" -ge "$DIFF_CHAR_LIMIT" ]; then
CODE_DIFF="$CODE_DIFF
[... 更多文件变化已截断 ...]"
break
fi
# 获取单个文件的diff限制每个文件最多50行
FILE_DIFF=$(git diff "$PREV_TAG".."$CURRENT_TAG" -- "$file" 2>/dev/null | head -50) || true
FILE_DIFF_LEN=${#FILE_DIFF}
# 如果单个文件diff超过1500字符截断
if [ "$FILE_DIFF_LEN" -gt 1500 ]; then
FILE_DIFF=$(echo "$FILE_DIFF" | head -c 1500)
FILE_DIFF="$FILE_DIFF
[... 文件 $file 变化已截断 ...]"
fi
if [ -n "$FILE_DIFF" ]; then
CODE_DIFF="$CODE_DIFF
### $file
\`\`\`diff
$FILE_DIFF
\`\`\`"
CURRENT_CHARS=$((CURRENT_CHARS + FILE_DIFF_LEN))
fi
done
fi
# 如果没有关键文件变化获取前5个变更文件的diff
if [ -z "$CODE_DIFF" ]; then
echo "No key files changed, getting top changed files..."
TOP_FILES=$(git diff --name-only "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null | \
grep -E "\.(ts|js|yml|md)$" | head -5) || true
if [ -n "$TOP_FILES" ]; then
for file in $TOP_FILES; do
FILE_DIFF=$(git diff "$PREV_TAG".."$CURRENT_TAG" -- "$file" 2>/dev/null | head -30) || true
if [ -n "$FILE_DIFF" ] && [ ${#FILE_DIFF} -lt 1000 ]; then
CODE_DIFF="$CODE_DIFF
### $file
\`\`\`diff
$FILE_DIFF
\`\`\`"
fi
done
fi
fi
# 如果仍然没有代码变化,添加说明
if [ -z "$CODE_DIFF" ]; then
CODE_DIFF="[本次更新主要涉及配置文件和文档变更,无核心代码变化]"
fi
echo "Code diff preview:"
echo "$CODE_DIFF" | head -50
# 读取 prompt
PROMPT_FILE=".github/prompt/release_note_prompt.txt"
SYSTEM_PROMPT=$(<"$PROMPT_FILE")
# 构建用户内容传递更多上下文包含文件变化和代码diff
USER_CONTENT="当前版本: $CURRENT_TAG
上一版本: $PREV_TAG
## 提交列表
$COMMITS
## 文件变化统计
$SUMMARY_LINE
## 变更文件列表
$FILE_CHANGES
## 关键代码变化
$CODE_DIFF"
# 构建请求 JSON增加 max_tokens 以获取更完整的输出
BODY=$(jq -n \
--arg system "$SYSTEM_PROMPT" \
--arg user "$USER_CONTENT" \
--arg model "$OPENROUTER_MODEL" \
'{model: $model, messages:[{role:"system", content:$system},{role:"user", content:$user}], temperature:0.2, max_tokens:1500}')
echo "=== OpenRouter request body ==="
echo "$BODY" | jq .
# 调用 OpenRouter
if RESPONSE=$(curl -s -X POST "$OPENROUTER_API_URL" \
-H "Authorization: Bearer $OPENAI_KEY" \
-H "Content-Type: application/json" \
-d "$BODY"); then
echo "=== raw response ==="
echo "$RESPONSE"
echo "=== OpenRouter raw response ==="
if echo "$RESPONSE" | jq . >/dev/null 2>&1; then
echo "$RESPONSE" | jq .
else
echo "jq failed to parse response"
fi
# 提取生成内容
RELEASE_BODY=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // .choices[0].text // ""' 2>/dev/null || echo "")
if [ -z "$RELEASE_BODY" ]; then
echo "❌ OpenRouter failed to generate release note, using default.md"
# 替换默认模板中的版本占位符
sed "s/{VERSION}/$CURRENT_TAG/g" .github/prompt/default.md > CHANGELOG.md
else
# 后处理:确保版本号正确,并添加比较链接
echo -e "$RELEASE_BODY" > CHANGELOG.md
# 替换可能的占位符
sed -i "s/{VERSION}/$CURRENT_TAG/g" CHANGELOG.md
sed -i "s/{PREV_VERSION}/$PREV_TAG/g" CHANGELOG.md
fi
else
echo "❌ Curl failed, using default.md"
sed "s/{VERSION}/$CURRENT_TAG/g" .github/prompt/default.md > CHANGELOG.md
fi
echo "=== generated release note ==="
cat CHANGELOG.md
- name: Create Release Draft and Upload Artifacts
uses: softprops/action-gh-release@v1
with:
name: NapCat
name: NapCat ${{ github.ref_name }}
token: ${{ secrets.GITHUB_TOKEN }}
body: Automated release artifact (no version detection)
body_path: CHANGELOG.md
files: |
NapCat.Shell.Windows.Node.zip
NapCat.Framework.zip
NapCat.Shell.zip
draft: true
NapCat.Shell.Windows.OneKey.zip
draft: true

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

@@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node-terminal",
"request": "launch",
"name": "调试程序",
"command": "pnpm run dev:shell",
"cwd": "${workspaceFolder}"
}
]
}

35
.vscode/settings.json vendored
View File

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

55
.vscode/tailwindcss.json vendored Normal file
View File

@@ -0,0 +1,55 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that youd like to extract to a new component.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
}
]
},
{
"name": "@responsive",
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
}
]
},
{
"name": "@screen",
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
}
]
},
{
"name": "@variants",
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
}
]
}
]
}

View File

@@ -43,7 +43,7 @@ _Modern protocol-side framework implemented based on NTQQ._
**首次使用**请务必查看如下文档看使用教程
> 项目非盈利,对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
> 项目非盈利,涉及 对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
## Link

52
eslint.config.js Normal file
View File

@@ -0,0 +1,52 @@
import neostandard from 'neostandard';
/** 尾随逗号 */
const commaDangle = val => {
if (val?.rules?.['@stylistic/comma-dangle']?.[0] === 'warn') {
const rule = val?.rules?.['@stylistic/comma-dangle']?.[1];
Object.keys(rule).forEach(key => {
rule[key] = 'always-multiline';
});
val.rules['@stylistic/comma-dangle'][1] = rule;
}
/** 三元表达式 */
if (val?.rules?.['@stylistic/indent']) {
val.rules['@stylistic/indent'][2] = {
...val.rules?.['@stylistic/indent']?.[2],
flatTernaryExpressions: true,
offsetTernaryExpressions: false,
};
}
/** 支持下划线 - 禁用 camelcase 规则 */
if (val?.rules?.camelcase) {
val.rules.camelcase = 'off';
}
/** 未使用的变量强制报错 */
if (val?.rules?.['@typescript-eslint/no-unused-vars']) {
val.rules['@typescript-eslint/no-unused-vars'] = ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}];
}
return val;
};
/** 忽略的文件 */
const ignores = [
'node_modules',
'**/dist/**',
'launcher',
];
const options = neostandard({
ts: true,
ignores,
semi: true, // 强制使用分号
}).map(commaDangle);
export default options;

View File

@@ -5,17 +5,28 @@
"version": "0.0.1",
"scripts": {
"build:shell": "pnpm --filter napcat-shell run build || exit 1",
"build:shell:dev": "pnpm --filter napcat-shell run build:dev || exit 1",
"build:framework": "pnpm --filter napcat-framework run build || exit 1",
"build:webui": "pnpm --filter napcat-webui-frontend run build || exit 1",
"dev:shell": "pnpm --filter napcat-develop run dev || exit 1"
"dev:shell": "pnpm --filter napcat-develop run dev || exit 1",
"typecheck": "pnpm -r --if-present run typecheck",
"test": "pnpm --filter napcat-test run test",
"test:ui": "pnpm --filter napcat-test run test:ui",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.3",
"@vitejs/plugin-react-swc": "^4.2.2",
"@vitest/ui": "^4.0.9",
"eslint": "^9.39.1",
"neostandard": "^0.12.2",
"typescript": "^5.3.0",
"vite": "^6.4.1",
"vite-plugin-cp": "^6.0.3"
"vite-plugin-cp": "^6.0.3",
"vitest": "^4.0.9"
},
"dependencies": {
"silk-wasm": "^3.6.1",
"express": "^5.0.0",
"ws": "^8.18.3"
}

View File

@@ -4,6 +4,9 @@
"private": true,
"type": "module",
"main": "src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"exports": {
".": {
"import": "./src/index.ts"
@@ -13,14 +16,8 @@
}
},
"dependencies": {
"compressing": "^1.10.1",
"json5": "^2.2.3",
"ajv": "^8.13.0",
"file-type": "^21.0.0",
"napcat-image-size": "workspace:*",
"napcat-core": "workspace:*",
"silk-wasm": "^3.6.1",
"winston": "^3.17.0"
"file-type": "^21.0.0"
},
"devDependencies": {
"@types/node": "^22.0.1"

View File

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

View File

@@ -1,84 +0,0 @@
import fsPromise from 'fs/promises';
import path from 'node:path';
import { randomUUID } from 'crypto';
import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
import { LogWrapper } from '@/napcat-common/log';
import { EncodeArgs } from '@/napcat-common/audio-worker';
import { FFmpegService } from '@/napcat-common/ffmpeg';
import { runTask } from './worker';
import { fileURLToPath } from 'node:url';
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
function getWorkerPath () {
// return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'audio-worker.mjs');
}
async function guessDuration (pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath);
const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
logger.log('通过文件大小估算语音的时长:', duration);
return duration;
}
async function handleWavFile (
file: Buffer,
filePath: string,
pcmPath: string
): Promise<{ input: Buffer; sampleRate: number }> {
const { fmt } = getWavFileInfo(file);
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
const result = await FFmpegService.convert(filePath, pcmPath);
return { input: await fsPromise.readFile(pcmPath), sampleRate: result.sampleRate };
}
return { input: file, sampleRate: fmt.sampleRate };
}
export async function encodeSilk (filePath: string, TEMP_DIR: string, logger: LogWrapper) {
try {
const file = await fsPromise.readFile(filePath);
const pttPath = path.join(TEMP_DIR, randomUUID());
if (!isSilk(file)) {
logger.log(`语音文件${filePath}需要转换成silk`);
const pcmPath = `${pttPath}.pcm`;
// const { input, sampleRate } = isWav(file) ? await handleWavFile(file, filePath, pcmPath): { input: await FFmpegService.convert(filePath, pcmPath) ? await fsPromise.readFile(pcmPath) : Buffer.alloc(0), sampleRate: 24000 };
let input: Buffer;
let sampleRate: number;
if (isWav(file)) {
const result = await handleWavFile(file, filePath, pcmPath);
input = result.input;
sampleRate = result.sampleRate;
} else {
const result = await FFmpegService.convert(filePath, pcmPath);
input = await fsPromise.readFile(pcmPath);
sampleRate = result.sampleRate;
}
const silk = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { input, sampleRate });
fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e));
await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
return {
converted: true,
path: pttPath,
duration: silk.duration / 1000,
};
} else {
let duration = 0;
try {
duration = getDuration(file) / 1000;
} catch (e: unknown) {
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, (e as Error).stack);
duration = await guessDuration(filePath, logger);
}
return {
converted: false,
path: filePath,
duration,
};
}
} catch (error: unknown) {
logger.logError('convert silk failed', error);
return {};
}
}

View File

@@ -1,4 +1,4 @@
import { Peer } from '@/napcat-core';
import { Peer } from './types';
import { randomUUID } from 'crypto';
class TimeBasedCache<K, V> {

View File

@@ -2,7 +2,7 @@ import fs from 'fs';
import { stat } from 'fs/promises';
import crypto, { randomUUID } from 'crypto';
import path from 'node:path';
import { solveProblem } from '@/napcat-common/helper';
import { solveProblem } from '@/napcat-common/src/helper';
export interface HttpDownloadOptions {
url: string;

View File

@@ -1,8 +1,12 @@
import path from 'node:path';
import fs from 'fs';
import os from 'node:os';
import { QQLevel } from '@/napcat-core';
import { QQVersionConfigType } from './types';
import { QQVersionConfigType, QQLevel } from './types';
import { compareSemVer } from './version';
import { getAllGitHubTags as getAllTagsFromMirror } from './mirror';
// 导出 compareSemVer 供其他模块使用
export { compareSemVer } from './version';
export async function solveProblem<T extends (...arg: any[]) => any> (func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
return new Promise<ReturnType<T> | undefined>((resolve) => {
@@ -212,3 +216,25 @@ export function parseAppidFromMajor (nodeMajor: string): string | undefined {
return undefined;
}
// ============== GitHub Tags 获取 ==============
// 使用 mirror 模块统一管理镜像
export async function getAllTags (): Promise<{ tags: string[], mirror: string; }> {
return getAllTagsFromMirror('NapNeko', 'NapCatQQ');
}
export async function getLatestTag (): Promise<string> {
const { tags } = await getAllTags();
// 使用 SemVer 规范排序
tags.sort((a, b) => compareSemVer(a, b));
const latest = tags.at(-1);
if (!latest) {
throw new Error('No tags found');
}
// 去掉开头的 v
return latest.replace(/^v/, '');
}

View File

@@ -0,0 +1,24 @@
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
FATAL = 'fatal',
}
export interface ILogWrapper {
fileLogEnabled: boolean;
consoleLogEnabled: boolean;
cleanOldLogs (logDir: string): void;
setFileAndConsoleLogLevel (fileLogLevel: LogLevel, consoleLogLevel: LogLevel): void;
setLogSelfInfo (selfInfo: { nick: string; uid: string; }): void;
setFileLogEnabled (isEnabled: boolean): void;
setConsoleLogEnabled (isEnabled: boolean): void;
formatMsg (msg: any[]): string;
_log (level: LogLevel, ...args: any[]): void;
log (...args: any[]): void;
logDebug (...args: any[]): void;
logError (...args: any[]): void;
logWarn (...args: any[]): void;
logFatal (...args: any[]): void;
logMessage (msg: unknown, selfInfo: unknown): void;
}

View File

@@ -1,6 +1,5 @@
import { Peer } from '@/napcat-core';
import crypto from 'crypto';
import { Peer } from './types';
export class LimitedHashTable<K, V> {
private readonly keyToValue: Map<K, V> = new Map();
private readonly valueToKey: Map<V, K> = new Map();

File diff suppressed because it is too large Load Diff

View File

@@ -1,317 +0,0 @@
/**
* 性能监控器 - 用于统计函数调用次数、耗时等信息
*/
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

@@ -3,11 +3,11 @@ import http from 'node:http';
export class RequestUtil {
// 适用于获取服务器下发cookies时获取仅GET
static async HttpsGetCookies (url: string): Promise<{ [key: string]: string }> {
static async HttpsGetCookies (url: string): Promise<{ [key: string]: string; }> {
const client = url.startsWith('https') ? https : http;
return new Promise((resolve, reject) => {
const req = client.get(url, (res) => {
const cookies: { [key: string]: string } = {};
const cookies: { [key: string]: string; } = {};
res.on('data', () => { }); // Necessary to consume the stream
res.on('end', () => {
@@ -27,7 +27,7 @@ export class RequestUtil {
});
}
private static async handleRedirect (res: http.IncomingMessage, url: string, cookies: { [key: string]: string }): Promise<{ [key: string]: string }> {
private static async handleRedirect (res: http.IncomingMessage, url: string, cookies: { [key: string]: string; }): Promise<{ [key: string]: string; }> {
if (res.statusCode === 301 || res.statusCode === 302) {
if (res.headers.location) {
const redirectUrl = new URL(res.headers.location, url);
@@ -39,7 +39,7 @@ export class RequestUtil {
return cookies;
}
private static extractCookies (setCookieHeaders: string[], cookies: { [key: string]: string }) {
private static extractCookies (setCookieHeaders: string[], cookies: { [key: string]: string; }) {
setCookieHeaders.forEach((cookie) => {
const parts = cookie.split(';')[0]?.split('=');
if (parts) {
@@ -53,9 +53,10 @@ export class RequestUtil {
}
// 请求和回复都是JSON data传原始内容 自动编码json
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: {
[key: string]: string
} = {}, isJsonRet: boolean = true, isArgJson: boolean = true): Promise<T> {
// 支持 301/302 重定向(最多 5 次)
static async HttpGetJson<T> (url: string, method: string = 'GET', data?: any, headers: {
[key: string]: string;
} = {}, isJsonRet: boolean = true, isArgJson: boolean = true, maxRedirects: number = 5): Promise<T> {
const option = new URL(url);
const protocol = url.startsWith('https://') ? https : http;
const options = {
@@ -71,6 +72,20 @@ export class RequestUtil {
// },
return new Promise((resolve, reject) => {
const req = protocol.request(options, (res: http.IncomingMessage) => {
// 处理重定向
if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location) {
if (maxRedirects <= 0) {
reject(new Error('Too many redirects'));
return;
}
const redirectUrl = new URL(res.headers.location, url).href;
// 递归跟随重定向
this.HttpGetJson<T>(redirectUrl, method, data, headers, isJsonRet, isArgJson, maxRedirects - 1)
.then(resolve)
.catch(reject);
return;
}
let responseBody = '';
res.on('data', (chunk: string | Buffer) => {
responseBody += chunk.toString();
@@ -109,7 +124,7 @@ export class RequestUtil {
}
// 请求返回都是原始内容
static async HttpGetText (url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) {
static async HttpGetText (url: string, method: string = 'GET', data?: any, headers: { [key: string]: string; } = {}) {
return this.HttpGetJson<string>(url, method, data, headers, false, false);
}
}

View File

@@ -0,0 +1,24 @@
export interface SystemStatus {
cpu: {
model: string,
speed: string;
usage: {
system: string;
qq: string;
},
core: number;
},
memory: {
total: string;
usage: {
system: string;
qq: string;
};
},
arch: string;
}
export interface IStatusHelperSubscription {
on (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
off (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
emit (event: 'statusUpdate', status: SystemStatus): boolean;
}

View File

@@ -19,4 +19,4 @@ class Store {
const store = new Store();
export default store;
export default store;

View File

@@ -0,0 +1,6 @@
export type LogListener = (msg: string) => void;
export interface ISubscription {
subscribe (listener: LogListener): void;
unsubscribe (listener: LogListener): void;
notify (msg: string): void;
}

View File

@@ -15,3 +15,14 @@ export type QQVersionConfigType = {
export type QQAppidTableType = {
[key: string]: { appid: string, qua: string };
};
export interface Peer {
chatType: number; // 聊天类型
peerUid: string; // 对等方的唯一标识符
guildId?: string; // 可选的频道ID
}
export interface QQLevel {
crownNum: number;
sunNum: number;
moonNum: number;
starNum: number;
}

View File

@@ -1 +1,118 @@
export const napCatVersion = import.meta.env.VITE_NAPCAT_VERSION || 'alpha';
// @ts-ignore
export const napCatVersion = (typeof import.meta?.env !== 'undefined' && import.meta.env.VITE_NAPCAT_VERSION) || '1.0.0-dev';
/**
* SemVer 2.0 正则表达式
* 格式: 主版本号.次版本号.修订号[-先行版本号][+版本编译信息]
* 参考: https://semver.org/lang/zh-CN/
*/
const SEMVER_REGEX = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
export interface SemVerInfo {
valid: boolean;
normalized: string;
major: number;
minor: number;
patch: number;
prerelease: string | null;
buildmetadata: string | null;
}
/**
* 解析并验证版本号是否符合 SemVer 2.0 规范
* @param version - 版本字符串 (支持 v 前缀)
* @returns SemVer 解析结果
*/
export function parseSemVer (version: string | undefined | null): SemVerInfo {
if (!version || typeof version !== 'string') {
return { valid: false, normalized: '1.0.0-dev', major: 1, minor: 0, patch: 0, prerelease: 'dev', buildmetadata: null };
}
const match = version.trim().match(SEMVER_REGEX);
if (match) {
const major = parseInt(match[1]!, 10);
const minor = parseInt(match[2]!, 10);
const patch = parseInt(match[3]!, 10);
const prerelease = match[4] || null;
const buildmetadata = match[5] || null;
// 构建标准化版本号(不带 v 前缀)
let normalized = `${major}.${minor}.${patch}`;
if (prerelease) normalized += `-${prerelease}`;
if (buildmetadata) normalized += `+${buildmetadata}`;
return { valid: true, normalized, major, minor, patch, prerelease, buildmetadata };
}
return { valid: false, normalized: '1.0.0-dev', major: 1, minor: 0, patch: 0, prerelease: 'dev', buildmetadata: null };
}
/**
* 验证版本号是否符合 SemVer 2.0 规范
* @param version - 版本字符串
* @returns 是否有效
*/
export function isValidSemVer (version: string | undefined | null): boolean {
return parseSemVer(version).valid;
}
/**
* 比较两个 SemVer 版本号
* @param v1 - 版本号1
* @param v2 - 版本号2
* @returns -1 (v1 < v2), 0 (v1 == v2), 1 (v1 > v2)
*/
export function compareSemVer (v1: string, v2: string): -1 | 0 | 1 {
const a = parseSemVer(v1);
const b = parseSemVer(v2);
if (!a.valid || !b.valid) {
return 0;
}
// 比较主版本号
if (a.major !== b.major) return a.major > b.major ? 1 : -1;
// 比较次版本号
if (a.minor !== b.minor) return a.minor > b.minor ? 1 : -1;
// 比较修订号
if (a.patch !== b.patch) return a.patch > b.patch ? 1 : -1;
// 有先行版本号的版本优先级较低
if (a.prerelease && !b.prerelease) return -1;
if (!a.prerelease && b.prerelease) return 1;
// 两者都有先行版本号时,按字典序比较
if (a.prerelease && b.prerelease) {
const aParts = a.prerelease.split('.');
const bParts = b.prerelease.split('.');
const len = Math.max(aParts.length, bParts.length);
for (let i = 0; i < len; i++) {
const aPart = aParts[i];
const bPart = bParts[i];
if (aPart === undefined) return -1;
if (bPart === undefined) return 1;
const aNum = /^\d+$/.test(aPart) ? parseInt(aPart, 10) : NaN;
const bNum = /^\d+$/.test(bPart) ? parseInt(bPart, 10) : NaN;
// 数字 vs 数字
if (!isNaN(aNum) && !isNaN(bNum)) {
if (aNum !== bNum) return aNum > bNum ? 1 : -1;
continue;
}
// 数字优先级低于字符串
if (!isNaN(aNum)) return -1;
if (!isNaN(bNum)) return 1;
// 字符串 vs 字符串
if (aPart !== bPart) return aPart > bPart ? 1 : -1;
}
}
return 0;
}
/**
* 获取解析后的当前版本信息
*/
export const napCatVersionInfo = parseSemVer(napCatVersion);

View File

@@ -11,11 +11,11 @@
],
"esModuleInterop": true,
"outDir": "dist",
"rootDir": "src",
"rootDir": ".",
"noEmit": false,
"sourceMap": true,
"strict": true,
"noImplicitAny": true,
"noImplicitAny": false,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"alwaysStrict": true,
@@ -36,8 +36,8 @@
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@/napcat-common/*": [
"src/*"
"@/*": [
"../*"
]
},
"skipLibCheck": true,

View File

@@ -5,7 +5,7 @@ export class NodeIDependsAdapter {
}
onMSFSsoError (_args: unknown) {
onMSFSsoError (_code: number, _desc: string) {
}
@@ -24,4 +24,4 @@ export class NodeIDependsAdapter {
// console.log('[NodeIDependsAdapter] onSendMsfReply', _seq, _cmd, _uk1, _uk2, Buffer.from(_rsp.pbBuffer).toString('hex'));
// }
}
}

View File

@@ -5,12 +5,7 @@ import {
IMAGE_HTTP_HOST_NT,
Peer,
PicElement,
PicSubType,
RawMessage,
SendFileElement,
SendPicElement,
SendPttElement,
SendVideoElement,
} from '@/napcat-core/types';
import path from 'path';
import fs from 'fs';
@@ -19,16 +14,9 @@ import { InstanceContext, NapCatCore, SearchResultItem } from '@/napcat-core/ind
import { fileTypeFromFile } from 'file-type';
import { RkeyManager } from '@/napcat-core/helper/rkey';
import { calculateFileMD5 } from 'napcat-common/src/file';
import pathLib from 'node:path';
import { defaultVideoThumbB64 } from 'napcat-common/src/video';
import { encodeSilk } from 'napcat-common/src/audio';
import { SendMessageContext } from 'napcat-onebot/api/msg';
import { getFileTypeForSendType } from '../helper/msg';
import { FFmpegService } from 'napcat-common/src/ffmpeg';
import { rkeyDataType } from '../types/file';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { FileId } from '../packet/transformer/proto/misc/fileid';
import { imageSizeFallBack } from 'napcat-image-size';
export class NTQQFileApi {
context: InstanceContext;
@@ -150,7 +138,7 @@ export class NTQQFileApi {
})).urlResult.domainUrl;
}
async uploadFile (filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
async uploadFile (filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0, uploadGroupFile = true) {
const fileMd5 = await calculateFileMD5(filePath);
const extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(() => '');
const ext = extOrEmpty ? `.${extOrEmpty}` : '';
@@ -158,188 +146,38 @@ export class NTQQFileApi {
if (fileName.indexOf('.') === -1) {
fileName += ext;
}
const mediaPath = this.context.session.getMsgService().getRichMediaFilePathForGuild({
md5HexStr: fileMd5,
fileName,
elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: '',
});
await this.copyFile(filePath, mediaPath);
const fileSize = await this.getFileSize(filePath);
if (uploadGroupFile) {
const mediaPath = this.context.session.getMsgService().getRichMediaFilePathForGuild({
md5HexStr: fileMd5,
fileName,
elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: '',
});
await this.copyFile(filePath, mediaPath);
return {
md5: fileMd5,
fileName,
path: mediaPath,
fileSize,
ext,
};
}
return {
md5: fileMd5,
fileName,
path: mediaPath,
path: filePath,
fileSize,
ext,
};
}
async createValidSendFileElement (context: SendMessageContext, filePath: string, fileName: string = '', folderId: string = ''): Promise<SendFileElement> {
const {
fileName: _fileName,
path,
fileSize,
} = await this.core.apis.FileApi.uploadFile(filePath, ElementType.FILE);
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
context.deleteAfterSentFiles.push(path);
return {
elementType: ElementType.FILE,
elementId: '',
fileElement: {
fileName: fileName || _fileName,
folderId,
filePath: path,
fileSize: fileSize.toString(),
},
};
}
async createValidSendPicElement (context: SendMessageContext, picPath: string, summary: string = '', subType: PicSubType = 0): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
const imageSize = await imageSizeFallBack(picPath);
context.deleteAfterSentFiles.push(path);
return {
elementType: ElementType.PIC,
elementId: '',
picElement: {
md5HexStr: md5,
fileSize: fileSize.toString(),
picWidth: imageSize.width,
picHeight: imageSize.height,
fileName,
sourcePath: path,
original: true,
picType: await getFileTypeForSendType(picPath),
picSubType: subType,
fileUuid: '',
fileSubId: '',
thumbFileSize: 0,
summary,
} as PicElement,
};
}
async createValidSendVideoElement (context: SendMessageContext, filePath: string, fileName: string = '', _diyThumbPath: string = ''): Promise<SendVideoElement> {
let videoInfo = {
width: 1920,
height: 1080,
time: 15,
format: 'mp4',
size: 0,
filePath,
};
let fileExt = 'mp4';
try {
const tempExt = (await fileTypeFromFile(filePath))?.ext;
if (tempExt) fileExt = tempExt;
} catch (e) {
this.context.logger.logError('获取文件类型失败', e);
}
const newFilePath = `${filePath}.${fileExt}`;
fs.copyFileSync(filePath, newFilePath);
context.deleteAfterSentFiles.push(newFilePath);
filePath = newFilePath;
const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
context.deleteAfterSentFiles.push(path);
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
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);
}
}
context.deleteAfterSentFiles.push(thumbPath);
const thumbSize = (await fsPromises.stat(thumbPath)).size;
const thumbMd5 = await calculateFileMD5(thumbPath);
context.deleteAfterSentFiles.push(thumbPath);
const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith(`.${fileExt.toLocaleLowerCase()}`) ? (fileName || _fileName) : `${fileName || _fileName}.${fileExt}`;
return {
elementType: ElementType.VIDEO,
elementId: '',
videoElement: {
fileName: uploadName,
filePath: path,
videoMd5: md5,
thumbMd5,
fileTime: videoInfo.time,
thumbPath: new Map([[0, thumbPath]]),
thumbSize,
thumbWidth: videoInfo.width,
thumbHeight: videoInfo.height,
fileSize: fileSize.toString(),
},
};
}
async createValidSendPttElement (_context: SendMessageContext, pttPath: string): Promise<SendPttElement> {
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
if (!silkPath) {
throw new Error('语音转换失败, 请检查语音文件是否正常');
}
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(silkPath, ElementType.PTT);
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
if (converted) {
fsPromises.unlink(silkPath).then().catch((e) => this.context.logger.logError('删除临时文件失败', e));
}
return {
elementType: ElementType.PTT,
elementId: '',
pttElement: {
fileName,
filePath: path,
md5HexStr: md5,
fileSize: fileSize.toString(),
duration: duration ?? 1,
formatType: 1,
voiceType: 1,
voiceChangeType: 0,
canConvert2Text: true,
waveAmplitudes: [
0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17,
],
fileSubId: '',
playState: 1,
autoConvertText: 0,
storeID: 0,
otherBusinessInfo: {
aiVoiceType: 0,
},
},
};
}
async downloadFileForModelId (peer: Peer, modelId: string, unknown: string, timeout = 1000 * 60 * 2) {
const [, fileTransNotifyInfo] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelRichMediaService/downloadFileForModelId',

View File

@@ -15,9 +15,9 @@ import {
} from '@/napcat-core/index';
import { isNumeric, solveAsyncProblem } from 'napcat-common/src/helper';
import { LimitedHashTable } from 'napcat-common/src/message-unique';
import { NTEventWrapper } from 'napcat-common/src/event';
import { CancelableTask, TaskExecutor } from 'napcat-common/src/cancel-task';
import { createGroupDetailInfoV2Param, createGroupExtFilter, createGroupExtInfo } from '../data';
import { NTEventWrapper } from '../helper/event';
export class NTQQGroupApi {
context: InstanceContext;
@@ -395,7 +395,7 @@ export class NTQQGroupApi {
'NodeIKernelGroupListener/onMemberInfoChange',
[groupCode, [uid], forced],
(ret) => ret.result === 0,
(params, _, members) => params === GroupCode && members.size > 0 && members.has(uid),
(params: string, _: any, members: Map<string, GroupMember>) => params === GroupCode && members.size > 0 && members.has(uid),
1,
forced ? 2500 : 250
);

View File

@@ -1,9 +1,9 @@
import * as os from 'os';
import offset from '@/napcat-core/external/napi2native.json';
import { InstanceContext, NapCatCore } from '@/napcat-core/index';
import { LogWrapper } from 'napcat-common/src/log';
import { PacketClientSession } from '@/napcat-core/packet/clientSession';
import { napCatVersion } from 'napcat-common/src/version';
import { LogWrapper } from '../helper/log';
interface OffsetType {
[key: string]: {

View File

@@ -466,5 +466,57 @@
"6.9.85-42086": {
"appid": 537320237,
"qua": "V1_MAC_NQ_6.9.85_42086_GW_B"
},
"9.9.23-42430": {
"appid": 537320212,
"qua": "V1_WIN_NQ_9.9.23_42430_GW_B"
},
"9.9.25-42744": {
"appid": 537328470,
"qua": "V1_WIN_NQ_9.9.23_42744_GW_B"
},
"6.9.86-42744": {
"appid": 537328495,
"qua": "V1_MAC_NQ_6.9.85_42744_GW_B"
},
"9.9.25-42905": {
"appid": 537328521,
"qua": "V1_WIN_NQ_9.9.25_42905_GW_B"
},
"6.9.86-42905": {
"appid": 537328546,
"qua": "V1_MAC_NQ_6.9.86_42905_GW_B"
},
"3.2.22-42941": {
"appid": 537328659,
"qua": "V1_LNX_NQ_3.2.22_42941_GW_B"
},
"9.9.25-42941": {
"appid": 537328623,
"qua": "V1_WIN_NQ_9.9.25_42941_GW_B"
},
"6.9.86-42941": {
"appid": 537328648,
"qua": "V1_MAC_NQ_6.9.86_42941_GW_B"
},
"9.9.26-44175": {
"appid": 537336450,
"qua": "V1_WIN_NQ_9.9.26_44175_GW_B"
},
"9.9.26-44343": {
"appid": 537336603,
"qua": "V1_WIN_NQ_9.9.26_44343_GW_B"
},
"3.2.23-44343": {
"appid": 537336639,
"qua": "V1_LNX_NQ_3.2.23_44343_GW_B"
},
"9.9.26-44498": {
"appid": 537337416,
"qua": "V1_WIN_NQ_9.9.26_44498_GW_B"
},
"9.9.26-44725": {
"appid": 537337569,
"qua": "V1_WIN_NQ_9.9.26_44725_GW_B"
}
}

View File

@@ -87,8 +87,72 @@
"send": "23B0330",
"recv": "0957648"
},
"3.2.21-42086-arm64": {
"send": "3D6D98C",
"recv": "14797C8"
},
"3.2.21-42086-x64": {
"send": "5B42CF0",
"recv": "2FDA6F0"
},
"9.9.23-42430-x64": {
"send": "0A01A34",
"recv": "1D1CFF9"
},
"9.9.25-42744-x64": {
"send": "0A0D104",
"recv": "1D3E7F9"
},
"6.9.85-42744-arm64": {
"send": "23DFEF0",
"recv": "095FD80"
},
"9.9.25-42905-x64": {
"send": "0A12E74",
"recv": "1D450FD"
},
"6.9.86-42905-arm64": {
"send": "2342408",
"recv": "09639B8"
},
"3.2.22-42941-x64": {
"send": "5BC1630",
"recv": "3011E00"
},
"3.2.22-42941-arm64": {
"send": "3DC90AC",
"recv": "1497A70"
},
"9.9.25-42941-x64": {
"send": "0A131D4",
"recv": "1D4547D"
},
"6.9.86-42941-arm64": {
"send": "2346108",
"recv": "09675F0"
},
"9.9.26-44175-x64": {
"send": "0A0F2EC",
"recv": "1D3AD4D"
},
"9.9.26-44343-x64": {
"send": "0A0F7BC",
"recv": "1D3C3CD"
},
"3.2.23-44343-arm64": {
"send": "3C867DC",
"recv": "1404938"
},
"3.2.23-44343-x64": {
"send": "59A27B0",
"recv": "2FFBE90"
},
"9.9.26-44498-x64": {
"send": "0A1051C",
"recv": "1D3BC0D"
},
"9.9.26-44725-x64": {
"send": "0A18D0C",
"recv": "1D4BF0D"
}
}

View File

@@ -602,5 +602,65 @@
"3.2.21-42086-arm64": {
"send": "6B13038",
"recv": "6B169C8"
},
"9.9.23-42430-x64": {
"send": "2C9A4A0",
"recv": "2C9DA20"
},
"9.9.25-42744-x64": {
"send": "2CD8E40",
"recv": "2CDC3C0"
},
"6.9.86-42744-arm64": {
"send": "3DCC840",
"recv": "3DCF150"
},
"9.9.25-42905-x64": {
"send": "2CE46A0",
"recv": "2CE7C20"
},
"6.9.86-42905-arm64": {
"send": "3DD6098",
"recv": "3DD89A8"
},
"3.2.22-42941-x64": {
"send": "A8AD8A0",
"recv": "A8B1320"
},
"9.9.25-42941-x64": {
"send": "2CE4DA0",
"recv": "2CE8320"
},
"3.2.22-42941-arm64": {
"send": "6BC95E8",
"recv": "6BCCF78"
},
"6.9.86-42941-arm64": {
"send": "3DDDAD0",
"recv": "3DE03E0"
},
"9.9.26-44175-x64": {
"send": "2CD84A0",
"recv": "2CDBA20"
},
"3.2.23-44343-x64": {
"send": "A46F140",
"recv": "A472BE0"
},
"9.9.26-44343-x64": {
"send": "2CD8EE0",
"recv": "2CDC460"
},
"3.2.23-44343-arm64": {
"send": "6926F60",
"recv": "692A910"
},
"9.9.26-44498-x64": {
"send": "2CDAE40",
"recv": "2CDE3C0"
},
"9.9.26-44725-x64": {
"send": "2CEBB20",
"recv": "2CEF0A0"
}
}

View File

@@ -1,5 +1,5 @@
// TODO: further refactor in NapCat.Packet v2
import { NapProtoMsg, ProtoField, ScalarType } from '@napneko/nap-proto-core';
import { NapProtoMsg, ProtoField, ScalarType } from 'napcat-protobuf';
const LikeDetail = {
txt: ProtoField(1, ScalarType.STRING),

View File

@@ -0,0 +1,45 @@
import fsPromise from 'fs/promises';
import path from 'node:path';
import { randomUUID } from 'crypto';
import { LogWrapper } from '@/napcat-core/helper/log';
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
async function guessDuration (pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath);
const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
logger.log('通过文件大小估算语音的时长:', duration);
return duration;
}
export async function encodeSilk (filePath: string, TEMP_DIR: string, logger: LogWrapper) {
try {
const pttPath = path.join(TEMP_DIR, randomUUID());
if (!(await FFmpegService.isSilk(filePath))) {
logger.log(`语音文件${filePath}需要转换成silk`);
await FFmpegService.convertToNTSilkTct(filePath, pttPath);
const duration = await FFmpegService.getDuration(filePath);
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', duration);
return {
converted: true,
path: pttPath,
duration: duration,
};
} else {
let duration = 0;
try {
duration = await FFmpegService.getDuration(filePath);
} catch (e: unknown) {
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, (e as Error).stack);
duration = await guessDuration(filePath, logger);
}
return {
converted: false,
path: filePath,
duration,
};
}
} catch (error: unknown) {
logger.logError('convert silk failed', error);
return {};
}
}

View File

@@ -1,6 +1,6 @@
import path from 'node:path';
import fs from 'node:fs';
import type { NapCatCore } from 'napcat-core';
import type { NapCatCore } from '@/napcat-core';
import json5 from 'json5';
import Ajv, { AnySchema, ValidateFunction } from 'ajv';

View File

@@ -1,4 +1,4 @@
import { ConfigBase } from 'napcat-common/src/config-base';
import { ConfigBase } from '@/napcat-core/helper/config-base';
import { NapCatCore } from '@/napcat-core/index';
import { Type, Static } from '@sinclair/typebox';
import { AnySchema } from 'ajv';

View File

@@ -1,6 +1,6 @@
import { NodeIQQNTWrapperSession } from '@/napcat-core/wrapper';
import { randomUUID } from 'crypto';
import { ListenerNamingMapping, ServiceNamingMapping } from '@/napcat-core';
import { ListenerNamingMapping, ServiceNamingMapping } from '@/napcat-core/index';
interface InternalMapKey {
timeout: number;
@@ -54,7 +54,7 @@ export class NTEventWrapper {
Service extends keyof ServiceNamingMapping,
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
T extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>
> (eventName: `${Service}/${ServiceMethod}`): T | undefined {
>(eventName: `${Service}/${ServiceMethod}`): T | undefined {
const eventNameArr = eventName.split('/');
type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>>; };
@@ -112,7 +112,7 @@ export class NTEventWrapper {
Service extends keyof ServiceNamingMapping,
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>
> (
>(
serviceAndMethod: `${Service}/${ServiceMethod}`,
...args: Parameters<EventType>
): Promise<Awaited<ReturnType<EventType>>> {
@@ -123,7 +123,7 @@ export class NTEventWrapper {
Listener extends keyof ListenerNamingMapping,
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
> (
>(
listenerAndMethod: `${Listener}/${ListenerMethod}`,
checker: (...args: Parameters<ListenerType>) => boolean,
waitTimes = 1,
@@ -177,7 +177,7 @@ export class NTEventWrapper {
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
> (
>(
serviceAndMethod: `${Service}/${ServiceMethod}`,
listenerAndMethod: `${Listener}/${ListenerMethod}`,
args: Parameters<EventType>,

View File

@@ -6,7 +6,7 @@ import * as os from 'os';
import * as compressing from 'compressing'; // 修正导入方式
import { pipeline } from 'stream/promises';
import { fileURLToPath } from 'url';
import { LogWrapper } from './log';
import { LogWrapper } from '@/napcat-core/helper/log';
const downloadOri = 'https://github.com/NapNeko/ffmpeg-build/releases/download/v1.0.0/ffmpeg-7.1.1-win64.zip';
const urls = [

View File

@@ -3,7 +3,7 @@
* FFmpeg
*/
import { LogWrapper } from './log';
import { LogWrapper } from '@/napcat-core/helper/log';
import { FFmpegAddonAdapter } from './ffmpeg-addon-adapter';
import { FFmpegExecAdapter } from './ffmpeg-exec-adapter';
import type { IFFmpegAdapter } from './ffmpeg-adapter-interface';

View File

@@ -27,21 +27,27 @@ export interface IFFmpegAdapter {
readonly name: string;
/** 是否可用 */
isAvailable(): Promise<boolean>;
isAvailable (): Promise<boolean>;
/**
* ()
* @param videoPath
* @returns
*/
getVideoInfo(videoPath: string): Promise<VideoInfoResult>;
getVideoInfo (videoPath: string): Promise<VideoInfoResult>;
/**
*
* @param filePath
* @returns ()
*/
getDuration(filePath: string): Promise<number>;
getDuration (filePath: string): Promise<number>;
/**
* Silk
* @param filePath
*/
isSilk (filePath: string): Promise<boolean>;
/**
* PCM
@@ -49,7 +55,7 @@ export interface IFFmpegAdapter {
* @param pcmPath PCM
* @returns PCM Buffer
*/
convertToPCM(filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number }>;
convertToPCM (filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number; }>;
/**
*
@@ -57,12 +63,14 @@ export interface IFFmpegAdapter {
* @param outputFile
* @param format ('amr' | 'silk' )
*/
convertFile(inputFile: string, outputFile: string, format: string): Promise<void>;
convertFile (inputFile: string, outputFile: string, format: string): Promise<void>;
/**
*
* @param videoPath
* @param thumbnailPath
*/
extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void>;
extractThumbnail (videoPath: string, thumbnailPath: string): Promise<void>;
convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void>;
}

View File

@@ -5,7 +5,7 @@
import { platform, arch } from 'node:os';
import path from 'node:path';
import { existsSync } from 'node:fs';
import { existsSync, openSync, readSync, closeSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import type { FFmpeg } from './ffmpeg-addon';
import type { IFFmpegAdapter, VideoInfoResult } from './ffmpeg-adapter-interface';
@@ -68,13 +68,13 @@ export class FFmpegAddonAdapter implements IFFmpegAdapter {
const addon = this.ensureAddon();
const info = await addon.getVideoInfo(videoPath);
let format = info.format.includes(',') ? info.format.split(',')[0] ?? info.format : info.format;
const format = info.format.includes(',') ? info.format.split(',')[0] ?? info.format : info.format;
console.log('[FFmpegAddonAdapter] Detected format:', format);
return {
width: info.width,
height: info.height,
duration: info.duration,
format: format,
format,
thumbnail: info.image,
};
}
@@ -87,6 +87,22 @@ export class FFmpegAddonAdapter implements IFFmpegAdapter {
return addon.getDuration(filePath);
}
/**
* Silk
*/
async isSilk (filePath: string): Promise<boolean> {
try {
const fd = openSync(filePath, 'r');
const buffer = Buffer.alloc(10);
readSync(fd, buffer, 0, 10, 0);
closeSync(fd);
const header = buffer.toString();
return header.includes('#!SILK') || header.includes('\x02#!SILK');
} catch {
return false;
}
}
/**
* PCM
*/
@@ -106,6 +122,11 @@ export class FFmpegAddonAdapter implements IFFmpegAdapter {
await addon.decodeAudioToFmt(inputFile, outputFile, format);
}
async convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void> {
const addon = this.ensureAddon();
await addon.convertToNTSilkTct(inputFile, outputFile);
}
/**
*
*/

View File

@@ -70,4 +70,6 @@ export interface FFmpeg {
*/
decodeAudioToPCM (filePath: string, pcmPath: string, sampleRate?: number): Promise<{ result: boolean, sampleRate: number; }>;
decodeAudioToFmt (filePath: string, pcmPath: string, format: string): Promise<{ channels: number; sampleRate: number; format: string; }>;
convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void>;
}

View File

@@ -3,14 +3,14 @@
* 使 execFile FFmpeg
*/
import { readFileSync, existsSync, mkdirSync } from 'fs';
import { readFileSync, existsSync, mkdirSync, openSync, readSync, closeSync } from 'fs';
import { dirname, join } from 'path';
import { execFile } from 'child_process';
import { promisify } from 'util';
import { fileTypeFromFile } from 'file-type';
import { imageSizeFallBack } from 'napcat-image-size/src/index';
import { downloadFFmpegIfNotExists } from './download-ffmpeg';
import { LogWrapper } from './log';
import { LogWrapper } from '@/napcat-core/helper/log';
import type { IFFmpegAdapter, VideoInfoResult } from './ffmpeg-adapter-interface';
const execFileAsync = promisify(execFile);
@@ -154,6 +154,22 @@ export class FFmpegExecAdapter implements IFFmpegAdapter {
}
}
/**
* Silk
*/
async isSilk (filePath: string): Promise<boolean> {
try {
const fd = openSync(filePath, 'r');
const buffer = Buffer.alloc(10);
readSync(fd, buffer, 0, 10, 0);
closeSync(fd);
const header = buffer.toString();
return header.includes('#!SILK') || header.includes('\x02#!SILK');
} catch {
return false;
}
}
/**
* PCM
*/
@@ -241,4 +257,8 @@ export class FFmpegExecAdapter implements IFFmpegAdapter {
throw new Error(`提取缩略图失败: ${(error as Error).message}`);
}
}
async convertToNTSilkTct (_inputFile: string, _outputFile: string): Promise<void> {
throw new Error('convertToNTSilkTct is not implemented in FFmpegExecAdapter');
}
}

View File

@@ -3,7 +3,7 @@ import path from 'path';
import type { VideoInfo } from './video';
import { fileTypeFromFile } from 'file-type';
import { platform } from 'node:os';
import { LogWrapper } from './log';
import { LogWrapper } from '@/napcat-core/helper/log';
import { FFmpegAdapterFactory } from './ffmpeg-adapter-factory';
import type { IFFmpegAdapter } from './ffmpeg-adapter-interface';
@@ -53,7 +53,6 @@ export class FFmpegService {
throw new Error('FFmpeg service not initialized. Please call FFmpegService.init() first.');
}
return this.adapter.name;
}
/**
@@ -65,7 +64,10 @@ export class FFmpegService {
}
return this.adapter;
}
public static async convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void> {
const adapter = await this.getAdapter();
await adapter.convertToNTSilkTct(inputFile, outputFile);
}
/**
* FFmpeg
* @deprecated 使 init()
@@ -93,11 +95,27 @@ export class FFmpegService {
/**
*
*/
public static async convertFile (inputFile: string, outputFile: string, format: string): Promise<void> {
public static async convertAudioFmt (inputFile: string, outputFile: string, format: string): Promise<void> {
const adapter = await this.getAdapter();
await adapter.convertFile(inputFile, outputFile, format);
}
/**
*
*/
public static async getDuration (filePath: string): Promise<number> {
const adapter = await this.getAdapter();
return adapter.getDuration(filePath);
}
/**
* Silk
*/
public static async isSilk (filePath: string): Promise<boolean> {
const adapter = await this.getAdapter();
return adapter.isSilk(filePath);
}
/**
* PCM
*/

View File

@@ -1,8 +1,9 @@
import winston, { format, transports } from 'winston';
import { truncateString } from './helper';
import { truncateString } from 'napcat-common/src/helper';
import path from 'node:path';
import fs from 'node:fs/promises';
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from 'napcat-core/index';
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/napcat-core/index';
import { ILogWrapper } from 'napcat-common/src/log-interface';
import EventEmitter from 'node:events';
export enum LogLevel {
DEBUG = 'debug',
@@ -56,7 +57,7 @@ class Subscription {
export const logSubscription = new Subscription();
export class LogWrapper {
export class LogWrapper implements ILogWrapper {
fileLogEnabled = true;
consoleLogEnabled = true;
logger: winston.Logger;

View File

@@ -1,4 +1,4 @@
import { LogWrapper } from '@/napcat-common/log';
import { LogWrapper } from '@/napcat-core/helper/log';
export function proxyHandlerOf (logger: LogWrapper) {
return {

View File

@@ -1,10 +1,10 @@
import fs from 'node:fs';
import { systemPlatform } from '@/napcat-common/system';
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath, parseAppidFromMajor } from './helper';
import AppidTable from 'napcat-core/external/appid.json';
import { LogWrapper } from '@/napcat-common/log';
import { getMajorPath } from 'napcat-core';
import { QQAppidTableType, QQPackageInfoType, QQVersionConfigType } from './types';
import { systemPlatform } from 'napcat-common/src/system';
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath, parseAppidFromMajor } from 'napcat-common/src/helper';
import AppidTable from '@/napcat-core/external/appid.json';
import { LogWrapper } from './log';
import { getMajorPath } from '@/napcat-core/index';
import { QQAppidTableType, QQPackageInfoType, QQVersionConfigType } from 'napcat-common/src/types';
export class QQBasicInfoWrapper {
QQMainPath: string | undefined;

View File

@@ -1,5 +1,5 @@
import { LogWrapper } from 'napcat-common/src/log';
import { RequestUtil } from 'napcat-common/src/request';
import { LogWrapper } from './log';
interface ServerRkeyData {
group_rkey: string;

View File

@@ -1,24 +1,24 @@
import os from 'node:os';
import EventEmitter from 'node:events';
import { IStatusHelperSubscription } from 'napcat-common/src/status-interface';
export interface SystemStatus {
cpu: {
model: string,
speed: string
speed: string;
usage: {
system: string
qq: string
system: string;
qq: string;
},
core: number
core: number;
},
memory: {
total: string
total: string;
usage: {
system: string
qq: string
}
system: string;
qq: string;
};
},
arch: string
arch: string;
}
export class StatusHelper {
@@ -101,7 +101,7 @@ export class StatusHelper {
}
}
class StatusHelperSubscription extends EventEmitter {
class StatusHelperSubscription extends EventEmitter implements IStatusHelperSubscription {
private statusHelper: StatusHelper;
private interval: NodeJS.Timeout | null = null;

View File

@@ -16,21 +16,23 @@ import {
WrapperNodeApi,
WrapperSessionInitConfig,
} from '@/napcat-core/wrapper';
import { LogLevel, LogWrapper } from 'napcat-common/src/log';
import { NodeIKernelLoginService } from '@/napcat-core/services';
import { QQBasicInfoWrapper } from 'napcat-common/src/qq-basic-info';
import { LogLevel, LogWrapper } from '@/napcat-core/helper/log';
import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info';
import { NapCatPathWrapper } from 'napcat-common/src/path';
import path from 'node:path';
import fs from 'node:fs';
import { hostname, systemName, systemVersion } from 'napcat-common/src/system';
import { NTEventWrapper } from 'napcat-common/src/event';
import { NTEventWrapper } from '@/napcat-core/helper/event';
import { KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/napcat-core/types';
import { NapCatConfigLoader, NapcatConfigSchema } from '@/napcat-core/helper/config';
import os from 'node:os';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/napcat-core/listeners';
import { proxiedListenerOf } from 'napcat-common/src/proxy-handler';
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
import { NTQQPacketApi } from './apis/packet';
import { NativePacketHandler } from './packet/handler/client';
import { container, ReceiverServiceRegistry } from './packet/handler/serviceRegister';
import { appEvent } from './packet/handler/eventList';
import { TypedEventEmitter } from './packet/handler/typeEvent';
export * from './wrapper';
export * from './types/index';
export * from './services/index';
@@ -92,6 +94,7 @@ export function getMajorPath (QQVersion: string): string {
export class NapCatCore {
readonly context: InstanceContext;
readonly eventWrapper: NTEventWrapper;
event = appEvent;
NapCatDataPath: string = '';
NapCatTempPath: string = '';
apis: StableNTApiWrapper;
@@ -118,6 +121,16 @@ export class NapCatCore {
UserApi: new NTQQUserApi(this.context, this),
GroupApi: new NTQQGroupApi(this.context, this),
};
container.bind(NapCatCore).toConstantValue(this);
container.bind(TypedEventEmitter).toConstantValue(this.event);
ReceiverServiceRegistry.forEach((ServiceClass, serviceName) => {
container.bind(ServiceClass).toSelf();
// console.log(`Registering service handler for: ${serviceName}`);
this.context.packetHandler.onCmd(serviceName, ({ seq, hex_data }) => {
const serviceInstance = container.get(ServiceClass);
return serviceInstance.handler(seq, hex_data);
});
});
}
async initCore () {
@@ -164,8 +177,10 @@ export class NapCatCore {
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
const tips = `[KickedOffLine] [${Info.tipsTitle}] ${Info.tipsDesc}`;
this.context.logger.logError(tips);
this.selfInfo.online = false;
this.event.emit('KickedOffLine', tips);
};
msgListener.onRecvMsg = (msgs) => {
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
@@ -264,7 +279,6 @@ export interface InstanceContext {
readonly wrapper: WrapperNodeApi;
readonly session: NodeIQQNTWrapperSession;
readonly logger: LogWrapper;
readonly loginService: NodeIKernelLoginService;
readonly basicInfoWrapper: QQBasicInfoWrapper;
readonly pathWrapper: NapCatPathWrapper;
readonly packetHandler: NativePacketHandler;

View File

@@ -53,6 +53,8 @@ export class NodeIKernelLoginListener {
onLoginState (..._args: any[]): any {
}
onLoginRecordUpdate (..._args: any[]): any {
}
}
export interface QRCodeLoginSucceedResult {

View File

@@ -1,5 +1,5 @@
import { ChatType, KickedOffLineInfo, RawMessage } from '@/napcat-core/types';
import { CommonFileInfo } from '@/napcat-core';
import { CommonFileInfo } from '@/napcat-core/index';
export interface OnRichMediaDownloadCompleteParams {
fileModelId: string,

View File

@@ -3,7 +3,7 @@ export class NodeIKernelStorageCleanListener {
}
onScanCacheProgressChanged (_args: unknown): any {
onScanCacheProgressChanged (_current_progress: number, _total_progress: number): any {
}
@@ -11,7 +11,7 @@ export class NodeIKernelStorageCleanListener {
}
onFinishScan (_args: unknown): any {
onFinishScan (_sizes: Array<`${number}`>): any {
}

View File

@@ -4,6 +4,9 @@
"private": true,
"type": "module",
"main": "index.ts",
"scripts": {
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"exports": {
".": {
"import": "./index.ts"
@@ -13,14 +16,18 @@
}
},
"dependencies": {
"winston": "^3.17.0",
"json5": "^2.2.3",
"inversify": "^7.10.4",
"reflect-metadata": "^0.2.2",
"@protobuf-ts/runtime": "^2.11.1",
"@napneko/nap-proto-core": "^0.0.4",
"ajv": "^8.13.0",
"@sinclair/typebox": "^0.34.38",
"file-type": "^21.0.0",
"compressing": "^1.10.1",
"napcat-image-size": "workspace:*",
"napcat-common": "workspace:*",
"napcat-onebot": "workspace:*"
"napcat-protobuf": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.0.1"

View File

@@ -1,5 +1,5 @@
import { LogLevel, LogWrapper } from 'napcat-common/src/log';
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
import { LogWrapper, LogLevel } from '@/napcat-core/helper/log';
// TODO: check bind?
export class PacketLogger {

View File

@@ -9,10 +9,10 @@ import {
PacketMsgReplyElement,
PacketMsgVideoElement,
} from '@/napcat-core/packet/message/element';
import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/napcat-core';
import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/napcat-core/index';
import { MiniAppRawData, MiniAppReqParams } from '@/napcat-core/packet/entities/miniApp';
import { AIVoiceChatType } from '@/napcat-core/packet/entities/aiChat';
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from 'napcat-protobuf';
import { IndexNode, LongMsgResult, MsgInfo, PushMsgBody } from '@/napcat-core/packet/transformer/proto';
import { OidbPacket } from '@/napcat-core/packet/transformer/base';
import { ImageOcrResult } from '@/napcat-core/packet/entities/ocrResult';

View File

@@ -1,5 +1,5 @@
import { PacketHighwayContext } from '@/napcat-core/packet/highway/highwayContext';
import { NapCatCore } from '@/napcat-core';
import { NapCatCore } from '@/napcat-core/index';
import { PacketLogger } from '@/napcat-core/packet/context/loggerContext';
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
import { PacketClientContext } from '@/napcat-core/packet/context/clientContext';

View File

@@ -2,8 +2,8 @@ import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
import { constants } from 'node:os';
import { LogWrapper } from 'napcat-common/src/log';
import offset from '@/napcat-core/external/packet.json';
import { LogWrapper } from '../../helper/log';
interface OffsetType {
[key: string]: {
recv: string;
@@ -50,7 +50,6 @@ export class NativePacketHandler {
this.logger.logError('NativePacketClient 加载出错:', error);
this.loaded = false;
}
}
/**

View File

@@ -0,0 +1,7 @@
import { TypedEventEmitter } from './typeEvent';
export interface AppEvents {
'event:emoji_like': { groupId: string; senderUin: string; emojiId: string, msgSeq: string, isAdd: boolean, count: number; };
KickedOffLine: string;
}
export const appEvent = new TypedEventEmitter<AppEvents>();

View File

@@ -0,0 +1,28 @@
import 'reflect-metadata';
import { Container, injectable } from 'inversify';
import { NapCatCore } from '../..';
import { TypedEventEmitter } from './typeEvent';
export const container = new Container();
export const ReceiverServiceRegistry = new Map<string, new (...args: any[]) => ServiceBase>();
export abstract class ServiceBase {
get core (): NapCatCore {
return container.get(NapCatCore);
}
get event () {
return container.get(TypedEventEmitter);
}
abstract handler (seq: number, hex_data: string): Promise<void> | void;
}
export function ReceiveService (serviceName: string) {
return function <T extends new (...args: any[]) => ServiceBase>(constructor: T) {
injectable()(constructor);
ReceiverServiceRegistry.set(serviceName, constructor);
return constructor;
};
}

View File

@@ -0,0 +1,22 @@
import { EventEmitter } from 'node:events';
export class TypedEventEmitter<E extends Record<string, any>> {
private emitter = new EventEmitter();
on<K extends keyof E>(event: K, listener: (payload: E[K]) => void) {
this.emitter.on(event as string, listener);
return () => this.off(event, listener);
}
once<K extends keyof E>(event: K, listener: (payload: E[K]) => void) {
this.emitter.once(event as string, listener);
}
off<K extends keyof E>(event: K, listener: (payload: E[K]) => void) {
this.emitter.off(event as string, listener);
}
emit<K extends keyof E>(event: K, payload: E[K]) {
this.emitter.emit(event as string, payload);
}
}

View File

@@ -11,12 +11,16 @@ import {
import { ChatType, Peer } from '@/napcat-core/index';
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from '@/napcat-core/packet/utils/crypto/hash';
import UploadGroupImage from '@/napcat-core/packet/transformer/highway/UploadGroupImage';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import * as proto from '@/napcat-core/packet/transformer/proto';
import * as trans from '@/napcat-core/packet/transformer';
import fs from 'fs';
import path from 'path';
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
import { PacketClientContext } from '@/napcat-core/packet/context/clientContext';
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
import { defaultVideoThumbB64 } from '@/napcat-core/helper/ffmpeg/video';
import { calculateFileMD5 } from 'napcat-common/src/file';
export const BlockSize = 1024 * 1024;
@@ -105,13 +109,89 @@ export class PacketHighwayContext {
if (+(video.fileSize ?? 0) > 1024 * 1024 * 100) {
throw new Error(`[Highway] 视频文件过大: ${(+(video.fileSize ?? 0) / (1024 * 1024)).toFixed(2)} MB > 100 MB请使用文件上传`);
}
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
await this.uploadGroupVideo(+peer.peerUid, video);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CVideo(peer.peerUid, video);
} else {
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
// 如果缺少视频缩略图,自动生成一个
let tempThumbPath: string | null = null;
let thumbExists = false;
if (video.thumbPath) {
try {
await fs.promises.access(video.thumbPath, fs.constants.F_OK);
thumbExists = true;
} catch {
thumbExists = false;
}
}
if (!video.thumbPath || !thumbExists) {
tempThumbPath = await this.ensureVideoThumb(video);
}
try {
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
await this.uploadGroupVideo(+peer.peerUid, video);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CVideo(peer.peerUid, video);
} else {
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
}
} finally {
// 清理临时生成的缩略图文件
if (tempThumbPath) {
const thumbToClean = tempThumbPath;
fs.promises.unlink(thumbToClean)
.then(() => this.logger.debug(`[Highway] Cleaned up temp thumbnail: ${thumbToClean}`))
.catch((e) => {
// 文件不存在时忽略错误
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
this.logger.warn(`[Highway] Failed to clean up temp thumbnail: ${thumbToClean}, reason: ${e instanceof Error ? e.message : e}`);
}
});
}
}
}
/**
* 确保视频缩略图存在,如果不存在则自动生成
* @returns 生成的临时缩略图路径,用于后续清理
*/
private async ensureVideoThumb (video: PacketMsgVideoElement): Promise<string> {
if (!video.filePath) {
throw new Error('video.filePath is empty, cannot generate thumbnail');
}
// 生成缩略图路径
const videoDir = path.dirname(video.filePath);
const videoBasename = path.basename(video.filePath, path.extname(video.filePath));
const thumbPath = path.join(videoDir, `${videoBasename}_thumb.png`);
this.logger.debug(`[Highway] Video thumb missing, generating at: ${thumbPath}`);
try {
// 尝试使用 FFmpeg 提取视频缩略图
await FFmpegService.extractThumbnail(video.filePath, thumbPath);
try {
await fs.promises.access(thumbPath, fs.constants.F_OK);
this.logger.debug('[Highway] Video thumbnail generated successfully using FFmpeg');
} catch {
throw new Error('FFmpeg failed to generate thumbnail');
}
} catch (e) {
// FFmpeg 失败时(包括未初始化的情况)使用默认缩略图
this.logger.warn(`[Highway] Failed to extract thumbnail, using default. Reason: ${e instanceof Error ? e.message : e}`);
await fs.promises.writeFile(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
}
// 更新视频元素的缩略图信息
video.thumbPath = thumbPath;
const thumbStat = await fs.promises.stat(thumbPath);
video.thumbSize = thumbStat.size;
video.thumbMd5 = await calculateFileMD5(thumbPath);
// 默认缩略图尺寸(与 defaultVideoThumbB64 匹配的尺寸)
if (!video.thumbWidth) video.thumbWidth = 240;
if (!video.thumbHeight) video.thumbHeight = 383;
this.logger.debug(`[Highway] Video thumb info set: path=${thumbPath}, size=${video.thumbSize}, md5=${video.thumbMd5}`);
return thumbPath;
}
async uploadPtt (peer: Peer, ptt: PacketMsgPttElement): Promise<void> {

View File

@@ -1,6 +1,6 @@
import crypto from 'node:crypto';
import http from 'node:http';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { IHighwayUploader } from '@/napcat-core/packet/highway/uploader/highwayUploader';
import { Frame } from '@/napcat-core/packet/highway/frame';
import * as proto from '@/napcat-core/packet/transformer/proto';

View File

@@ -1,7 +1,7 @@
import net from 'node:net';
import stream from 'node:stream';
import crypto from 'node:crypto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { BlockSize } from '@/napcat-core/packet/highway/highwayContext';
import { Frame } from '@/napcat-core/packet/highway/frame';
import { IHighwayUploader } from '@/napcat-core/packet/highway/uploader/highwayUploader';

View File

@@ -1,5 +1,5 @@
// import * as tea from '@/napcat-core/packet/utils/crypto/tea';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { PacketHighwayTrans } from '@/napcat-core/packet/highway/client';
import { PacketLogger } from '@/napcat-core/packet/context/loggerContext';
import * as proto from '@/napcat-core/packet/transformer/proto';

View File

@@ -1,4 +1,4 @@
import { NapProtoEncodeStructType } from '@napneko/nap-proto-core';
import { NapProtoEncodeStructType } from 'napcat-protobuf';
import * as proto from '@/napcat-core/packet/transformer/proto';
export const int32ip2str = (ip: number) => {

View File

@@ -1,6 +1,6 @@
import * as crypto from 'crypto';
import { PushMsgBody } from '@/napcat-core/packet/transformer/proto';
import { NapProtoEncodeStructType } from '@napneko/nap-proto-core';
import { NapProtoEncodeStructType } from 'napcat-protobuf';
import { PacketMsg, PacketSendMsgElement } from '@/napcat-core/packet/message/message';
import { IPacketMsgElement, PacketMsgTextElement } from '@/napcat-core/packet/message/element';
import { SendTextElement } from '@/napcat-core/index';
@@ -14,7 +14,7 @@ export class PacketMsgBuilder {
buildFakeMsg (selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] {
return element.map((node): NapProtoEncodeStructType<typeof PushMsgBody> => {
const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`;
const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=0&img_type=jpg`;
const msgContent = node.msg.reduceRight((acc: undefined | Uint8Array, msg: IPacketMsgElement<PacketSendMsgElement>) => {
return acc ?? msg.buildContent();
}, undefined);

View File

@@ -32,7 +32,7 @@ import {
PacketMultiMsgElement,
} from '@/napcat-core/packet/message/element';
import { PacketMsg, PacketSendMsgElement } from '@/napcat-core/packet/message/message';
import { NapProtoDecodeStructType } from '@napneko/nap-proto-core';
import { NapProtoDecodeStructType } from 'napcat-protobuf';
import { Elem } from '@/napcat-core/packet/transformer/proto';
const SupportedElementTypes = [
@@ -132,7 +132,6 @@ export class PacketMsgConverter {
time: msg.time,
msg: msg.msg.map((element) => {
if (!this.isValidElementType(element.elementType)) return null;
// @ts-ignore
return this.rawToPacketMsgConverters[element.elementType](element as MessageElement);
}).filter((e) => e !== null),
};

View File

@@ -1,5 +1,5 @@
import * as zlib from 'node:zlib';
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from 'napcat-protobuf';
import {
CustomFace,
Elem,
@@ -32,8 +32,8 @@ import {
SendTextElement,
SendVideoElement,
Peer,
} from '@/napcat-core';
import { ForwardMsgBuilder } from 'napcat-common/src/forward-msg-builder';
} from '@/napcat-core/index';
import { ForwardMsgBuilder } from '@/napcat-core/helper/forward-msg-builder';
import { PacketMsg, PacketSendMsgElement } from '@/napcat-core/packet/message/message';
export type ParseElementFnR = [MessageElement, NapProtoDecodeStructType<typeof Elem> | null] | undefined;
@@ -510,15 +510,15 @@ export class PacketMsgPttElement extends IPacketMsgElement<SendPttElement> {
}
override buildElement (): NapProtoEncodeStructType<typeof Elem>[] {
return [];
// if (!this.msgInfo) return [];
// return [{
// commonElem: {
// serviceType: 48,
// pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
// businessType: 22,
// }
// }];
//return [];
if (!this.msgInfo) return [];
return [{
commonElem: {
serviceType: 48,
pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
businessType: 22,
}
}];
}
override toPreview (): string {

View File

@@ -1,5 +1,5 @@
import { IPacketMsgElement } from '@/napcat-core/packet/message/element';
import { SendMessageElement, SendMultiForwardMsgElement } from '@/napcat-core';
import { SendMessageElement, SendMultiForwardMsgElement } from '@/napcat-core/index';
export type PacketSendMsgElement = SendMessageElement | SendMultiForwardMsgElement;

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
import { AIVoiceChatType } from '@/napcat-core/packet/entities/aiChat';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
import { AIVoiceChatType } from '@/napcat-core/packet/entities/aiChat';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketBufBuilder, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import { MiniAppReqParams } from '@/napcat-core/packet/entities/miniApp';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';

View File

@@ -1,4 +1,4 @@
import { NapProtoDecodeStructType } from '@napneko/nap-proto-core';
import { NapProtoDecodeStructType } from 'napcat-protobuf';
import { PacketMsgBuilder } from '@/napcat-core/packet/message/builder';
export type PacketBuf = Buffer & { readonly hexNya: unique symbol; };

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoEncodeStructType, NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
import { IndexNode } from '@/napcat-core/packet/transformer/proto';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoEncodeStructType, NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';

View File

@@ -1,5 +1,5 @@
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { NapProtoEncodeStructType, NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
import { IndexNode } from '@/napcat-core/packet/transformer/proto';

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