Compare commits

...

92 Commits

Author SHA1 Message Date
手瓜一十雪
d9297c1e10 Bump napcat-types & add plugin static/memory tests
Upgrade napcat-types to v0.0.15 and update the built-in plugin UI to test both filesystem and in-memory static resources. dashboard.html: clarify which plugin endpoints require auth, add buttons and a testMemoryResource() function that fetches an in-memory JSON resource, and add staticBase/memBase variables for non-auth static routes. napcat-webui-backend: return after 404 for missing memory files to stop further handling. (Lockfile updated accordingly.)
2026-02-02 15:35:26 +08:00
手瓜一十雪
94f07ab98b Support memory static files and plugin APIs
Introduce in-memory static file support and inter-plugin exports. Add MemoryStaticFile/MemoryFileGenerator types and expose staticOnMem in PluginRouterRegistry; router registry now tracks memory routes and exposes getters. Add getPluginExports to plugin manager adapters to allow plugins to call each other's exported modules. WebUI backend gains routes to serve /plugin/:pluginId/mem/* (memory files) and /plugin/:pluginId/files/* (plugin filesystem static) without auth. Update builtin plugin to demonstrate staticOnMem and inter-plugin call, and add frontend UI to open extension pages in a new window. Note: API router no longer mounts static filesystem routes — those are handled by webui-backend.
2026-02-02 15:01:26 +08:00
手瓜一十雪
01a6594707 Add image-size alias to napcat-schema vite config
Add a new Vite path alias '@/napcat-image-size' in packages/napcat-schema/vite.config.ts pointing to ../napcat-image-size. This enables cleaner imports of the napcat-image-size package from within napcat-schema source files.
2026-02-02 14:15:27 +08:00
手瓜一十雪
82a7154b92 fix: #1574 & Clear CJS cache and reload plugins on install
Add support for clearing CommonJS module cache when unloading plugins and reload plugins on install. PluginLoader now uses createRequire to access require.cache and exposes clearCache(pluginPath) to remove cached modules under the plugin directory; plugin manager calls this when unloading. Web UI backend install handlers now reload an existing plugin (with progress updates) instead of skipping registration, ensuring updated code/metadata take effect.
2026-02-02 14:14:06 +08:00
手瓜一十雪
9b385ac9c9 Ignore env file and db-shm in .gitignore
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Add packages/napcat-develop/config/.env to .gitignore to prevent committing environment configuration, and restore the guild1.db-shm entry (fix EOF/newline).
2026-02-02 13:22:29 +08:00
手瓜一十雪
e3d4cee416 Rename .env to .env.example
Rename the tracked dotenv file to .env.example (identical content) to avoid committing environment secrets and provide a template/example for project configuration.
2026-02-02 13:21:34 +08:00
手瓜一十雪
f6c79370cb Replace Type.Void() with Type.Object({})
Replace Type.Void() payload/return schemas with Type.Object({}) in several OneBot action extensions to represent empty object schemas and avoid void-type validation issues. Updated files: BotExit (payload & return), GetClientkey, GetFriendWithCategory, GetGroupAddRequest, GetRkey, GetRobotUinRange, GetUnidirectionalFriendList.
2026-02-02 13:17:06 +08:00
手瓜一十雪
39460e4acb Rename image-size alias to napcat-image-size
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Update Vite path alias from '@/image-size' to '@/napcat-image-size' in napcat-framework and napcat-shell vite.config.ts files to match the package directory ../napcat-image-size and ensure consistent import paths.
2026-02-01 17:46:40 +08:00
手瓜一十雪
f971c312b9 Add TIFF parser and buffer-based image size APIs
Introduce a TIFF parser and integrate it into the image detection/size parsing pipeline. Add buffer-based APIs (detectImageTypeFromBuffer, imageSizeFromBuffer, imageSizeFromBufferFallBack) and a helper to convert Buffer to a Readable stream; refactor parser registry into a type-to-parser map and a first-byte fast-path map for quicker detection. Harden WebP parsing with safer length checks. Add sample image resources and a comprehensive Vitest test suite (packages/napcat-test) with updated package dependency and resolve aliases. pnpm-lock updated to link the new package.
2026-02-01 17:42:58 +08:00
手瓜一十雪
2c3a304440 Split image parsers into separate files
Move PNG/JPEG/BMP/GIF/WEBP parser implementations out of index.ts into dedicated files under packages/napcat-image-size/src/parser. Export ImageParser and matchMagic from index.ts, import the new BmpParser, GifParser, JpegParser, PngParser and WebpParser classes, and update the parsers array to instantiate those classes. Keeps existing parsing logic and stream-based size detection while cleaning up index.ts for modularity.
2026-02-01 17:13:19 +08:00
手瓜一十雪
286b0e03f7 Add node types to package tsconfig; drop base typeRoots
Enable Node typings for packages/napcat-image-size by adding "compilerOptions.types": ["node"] to its tsconfig.json. Remove the restrictive "typeRoots" entry from tsconfig.base.json so type resolution can be controlled per-package and won't be forced globally.
2026-02-01 17:08:46 +08:00
手瓜一十雪
447f86e2b5 Use tabs, redesign theme settings UI
Replace the Accordion-based layout with a Tabs component and overhaul the Theme settings page UI. Rework the top header to a compact title/status area showing current theme, font and unsaved state; restyle action buttons and refresh icon. Convert font settings into a Card with improved FileInput flow (attempt delete before upload, better success/error toasts and page reload), and present theme previews and custom color editors as Cards per light/dark mode with updated ColorPicker handling. Update imports accordingly and apply various layout / class refinements.
2026-02-01 14:53:00 +08:00
手瓜一十雪
0592f1a99a Replace react-color with custom HSL ColorPicker
Remove the react-color dependency and add a custom ColorPicker implementation. The new component uses HSL parsing and hex<->HSL conversion, provides SatLightPanel and HueSlider subcomponents with mouse/touch drag support, and integrates @heroui/input for inline HEX/HSL editing. The ColorPicker onChange now emits an "hsl(...)" string; theme config parsing was updated to convert that string into the existing "h s% l%" format. Also update package.json to drop react-color.
2026-02-01 14:22:52 +08:00
手瓜一十雪
90e3936204 Support custom WebUI fonts and UI additions
Backend: add CheckWebUIFontExist API and route; set --font-family-mono CSS variable in InitWebUi for aacute/custom/default. Improve webui font uploader: force saved filename to CustomFont, robustly clean old webui/CustomFont files, and log failures.

Frontend: add FileManager.checkWebUIFontExists; update theme settings to show upload UI only when 'custom' is selected, display uploaded status, attempt delete-before-upload, reload after actions, and adjust Accordion props. ColorPicker: enable pointer events on PopoverContent to allow dragging. applyFont now sets --font-family-mono for all modes.
2026-02-01 14:00:27 +08:00
Qiao
1239f622d2 feat(webui): 插件卡片添加仓库主页跳转功能 (#1569)
* 为插件接口添加主页字段并优化展示组件

本次更新在 PluginPackageJson 接口及相关类型中新增了一个可选的 `homepage` 字段,允许插件指定其主页 URL。插件展示组件已更新,新增了一个指向主页的 GitHub 链接按钮,以提升用户对插件资源的访问便捷性。此外,PluginConfigModal 中新增了一个问题反馈按钮,该按钮直接链接到插件的主页,从而优化了用户支持与反馈机制。

* 优化标题区域样式,确保长标题正确截断显示省略号

* 移除插件相关接口中的可选主页字段,并优化展示组件以简化代码结构。更新了插件展示卡片的样式,确保更好的用户体验。

* 修改 PluginStoreCard 组件,新增 displayId 优化包名展示,并调整卡片样式以提升响应式表现。更新不同屏幕尺寸的最大宽度设置,确保包名截断显示且悬停可查看完整内容。

* Revert "修改 PluginStoreCard 组件,新增 displayId 优化包名展示,并调整卡片样式以提升响应式表现。更新不同屏幕尺寸的最大宽度设置,确保包名截断显示且悬停可查看完整内容。"

This reverts commit 0301421bc8.

* Revert "移除插件相关接口中的可选主页字段,并优化展示组件以简化代码结构。更新了插件展示卡片的样式,确保更好的用户体验。"

This reverts commit 1d22f19fa6.

* Revert "优化标题区域样式,确保长标题正确截断显示省略号"

This reverts commit 8a0912b5b9.

* Revert "为插件接口添加主页字段并优化展示组件"

This reverts commit 4e5dddde90.

* 再说丑我打死你

---------

Co-authored-by: 手瓜一十雪 <nanaeonn@outlook.com>
2026-02-01 13:47:18 +08:00
手瓜一十雪
d511e2bb3f Load .env, prefer WEBUI secret, add build script
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Load local .env in napcat-shell and prioritize NAPCAT_WEBUI_SECRET_KEY across the app. Changes include:
- package.json: add build:shell:config script to build shell and copy env for dev builds.
- packages/napcat-develop/config/.env and loadNapCat.cjs: update default WEBUI secret to `napcatqq` and set the same env in the loader.
- packages/napcat-shell/napcat.ts: read config/.env into process.env at startup, rework pathWrapper/logger initialization, and minor formatting cleanups.
- packages/napcat-webui-backend/index.ts: InitWebUi now prefers NAPCAT_WEBUI_SECRET_KEY from the environment (updates config and logs the change); only generates a random token if no env override and current token is default.
These changes make it easier to override the WebUI token for development and ensure the token is propagated when building the shell for dev workflows.
2026-02-01 11:24:25 +08:00
手瓜一十雪
ff93aa3dc7 Add dev config and copy-env build script
Add development config files and wiring to include them in the shell build. Creates packages/napcat-develop/config/.env and packages/napcat-develop/config/onebot11.json, adds a root script `build:shell:config` that builds napcat-shell and then runs napcat-develop's `copy-env` script. Update packages/napcat-develop/package.json to add the `copy-env` script (uses xcopy to copy config into the napcat-shell dist), tidy exports and metadata. This ensures dev configuration is packaged into napcat-shell during the build process.
2026-02-01 11:01:53 +08:00
手瓜一十雪
cc8891b6a1 fix: #1575 2026-02-01 10:33:58 +08:00
手瓜一十雪
7c65b1eaf1 Revert "增加个网络配置导出导入 (#1567)"
This reverts commit c0bcced5fb.
2026-02-01 10:22:15 +08:00
香草味的纳西妲喵
ebe3e9c63c feat(webui): 新增配置全量备份与恢复功能。 (#1571)
* feat(webui): 新增配置全量备份与恢复功能。

* chore: Remove dependencies "archiver"

* feat(webui): 增加上传文件大小限制配置并优化上传处理

* Use memory-based zip import/export and multer

Replace disk-based zip handling with in-memory streaming to avoid temp files: remove unzipper/@types(unzipper) deps from package.json; update BackupConfig to stream-export configs with compressing.zip.Stream and to import by extracting uploaded zip buffer via compressing.zip.UncompressStream into in-memory Buffers. Backup of existing config is kept in-memory instead of copying to tmp, and imported files are written with path normalization checks. Router changed to use multer.memoryStorage() for uploads (remove dynamic tmp/disk upload logic and uploadSizeLimit usage). Also remove uploadSizeLimit from config schema.

* Revert "chore: Remove dependencies "archiver""

This reverts commit 890736d3c7.

* Regenerate pnpm-lock.yaml (prune entries)

Regenerated pnpm-lock.yaml to reflect the current dependency resolution. This update prunes many removed/unused lock entries (notably archiver, unzipper and related @types, older/deprecated packages such as rimraf v2/fstream/bluebird, etc.) and removes platform 'libc' metadata from several platform-specific packages. There are no package.json changes; run `pnpm install` to sync your local node_modules with the updated lockfile.

---------

Co-authored-by: 手瓜一十雪 <nanaeonn@outlook.com>
2026-02-01 10:21:19 +08:00
冷曦
d33a872c42 修改合并消息上传资源日志 (#1573)
当上传资源有失败时为warn
全部成功则不输出日志
2026-02-01 09:53:40 +08:00
手瓜一十雪
9377dc3d52 Update version keys to 9.9.27-45627
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Rename release keys for build 45627 to 9.9.27-45627 across external metadata. Updated keys in packages/napcat-core/external/appid.json, napi2native.json, and packet.json (including x64 entries). No other payload values were modified.
2026-01-31 22:04:08 +08:00
手瓜一十雪
17322bb5a4 Add mappings for 9.9.26-45627 and 6.9.88-44725
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Add support for new client builds by updating external mappings. appid.json: add 9.9.26-45627 (appid 537340060) and 6.9.88-44725 (appid 537337594) with QUA strings. napi2native.json: add send/recv entries for 9.9.26-45627-x64, 6.9.88-44725-x64 and 6.9.88-44725-arm64. packet.json: add corresponding send/recv offsets for the same builds/architectures. These additions enable handling of the new versions in napcat-core.
2026-01-31 15:55:36 +08:00
冷曦
c0bcced5fb 增加个网络配置导出导入 (#1567)
* 增加个网络配置导出导入

重装容器时可以直接导出导入

* Remove unused import for useRef in network.tsx
2026-01-31 15:28:18 +08:00
手瓜一十雪
805c1d5ea2 Default plugins disabled; skip loading disabled
Change plugin loader to treat plugins as disabled by default (unless the id/dir is 'napcat-plugin-builtin') by using nullish coalescing when reading statusConfig. Add an early-return guard in the plugin manager/adapter that logs and skips loading when entry.enable is false. This prevents disabled plugins from being loaded automatically and provides a clear log message when skipped.
2026-01-31 15:26:56 +08:00
手瓜一十雪
b3399b07ad Silence update log; change update UI colors
Comment out the noisy '[NapCat Update] No pending updates found' log in UpdateNapCat.ts. Update frontend color choices: switch the plugin store action color from 'success' to 'default', and change the NewVersion chip and spinner from 'danger' to 'primary' in system_info.tsx. These tweaks reduce alarming red styling and quiet an unnecessary backend log.
2026-01-31 15:15:01 +08:00
手瓜一十雪
71f8504849 Refactor extension page layout and tab handling
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Improves the layout of the extension page by adjusting container heights and restructuring the header to better support responsive design. Moves the tab navigation to the header and displays only the selected extension page in the main content area, simplifying the rendering logic and improving user experience.
2026-01-30 19:41:27 +08:00
手瓜一十雪
3b7ca1a08f Remove flex-wrap from tabList class in ExtensionPage
The 'flex-wrap' class was removed from the tabList classNames in the ExtensionPage component, likely to prevent tab items from wrapping onto multiple lines and to maintain a single-line tab layout.
2026-01-30 19:35:46 +08:00
手瓜一十雪
57f3c4dd31 Support nested innerPacketMsg in SendMsgBase
Adds handling for innerPacketMsg arrays within uploadReturnData, allowing nested packet messages to be included in the result. This change ensures that all relevant inner messages are processed and returned.
2026-01-30 19:25:01 +08:00
时瑾
5b20ebb7b0 fix: webui 随机token仅生成不会被url编码的随机字符 (#1565)
* fix: webui 随机token仅生成不会被url编码的随机字符

* fix: 移除调试模块中的encodeURIComponent
2026-01-30 18:51:13 +08:00
手瓜一十雪
3a3eaeec7c Add UploadForwardMsgV2 support for multi-message forwarding
Introduces UploadForwardMsgV2 transformer and integrates it into the message sending flow to support forwarding multiple messages with custom action commands. Updates related interfaces and logic to handle UUIDs and nested forwarded messages, improving flexibility and extensibility for message forwarding operations.
2026-01-30 18:47:45 +08:00
手瓜一十雪
b0cc7b6ee5 Update Vite aliases in napcat-schema config
Expanded the alias configuration in vite.config.ts to include specific paths for napcat-onebot, napcat-common, napcat-schema, and napcat-core. This improves module resolution and import clarity within the project.
2026-01-30 14:41:46 +08:00
冷曦
e5108c0427 增加判断插件启用状态显示配置提示 (#1562) 2026-01-30 14:31:56 +08:00
手瓜一十雪
927797f3d5 Add SSL certificate management to WebUI config
Introduces backend API endpoints and frontend UI for managing SSL certificates, including viewing status, uploading, and deleting cert/key files. Adds a new SSL configuration tab in the dashboard, allowing users to enable HTTPS by providing PEM-formatted certificate and key, with changes taking effect after restart.
2026-01-30 14:28:47 +08:00
手瓜一十雪
72e01f8c84 Change default host to IPv6 (::) in config schema
Updated the default value of the 'host' field in WebUiConfigSchema from '0.0.0.0' (IPv4) to '::' (IPv6) to support IPv6 by default.
2026-01-30 14:11:53 +08:00
手瓜一十雪
c38b98a0c4 Add plugin WebUI extension page and API routing support
Introduces a plugin router registry for registering plugin-specific API routes, static resources, and extension pages. Updates the plugin manager and context to expose the router, and implements backend and frontend support for serving and displaying plugin extension pages in the WebUI. Also adds a demo extension page and static resource to the builtin plugin.
2026-01-30 12:48:24 +08:00
手瓜一十雪
05d27e86ce Add local plugin import functionality
Implemented backend API and frontend UI for importing local plugin zip files. The backend now supports file uploads via a new /Plugin/Import endpoint using multer, and the frontend provides a button to upload and import plugins directly from the dashboard.

Prompt to register plugin manager if not loaded

Renames plugin_develop.ts to plugin-develop.ts for consistency. Updates the plugin import handler to prompt the user to register the plugin manager if it is not loaded, improving user experience and error handling.
2026-01-30 11:58:43 +08:00
手瓜一十雪
40409a3841 Refactor plugin manager with modular loader and types
Refactors the plugin manager by extracting configuration, loader, and type definitions into separate modules under the 'plugin' directory. Introduces a new PluginLoader class for scanning and loading plugins, and updates the main manager to use modularized logic and improved type safety. This change improves maintainability, separation of concerns, and extensibility for plugin management.
2026-01-30 11:50:22 +08:00
手瓜一十雪
65bae6b57a Introduce NapCat Protocol and adapter management
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Added the new napcat-protocol package with protocol config, event, API, and network management modules. Introduced napcat-adapter package to unify protocol adapter management, replacing direct OneBot usage in framework and shell. Updated napcat-framework and napcat-shell to use NapCatAdapterManager for protocol initialization and registration. Adjusted dependencies and Vite configs to include new packages.
2026-01-29 22:14:55 +08:00
手瓜一十雪
0b6afb66d9 Add session proxy with event wrapper integration
Introduces a session proxy mechanism in napcat-core that intercepts service method calls and routes them through an event wrapper when enabled via the NAPCAT_SESSION_PROXY environment variable. Adds helper functions for creating proxied sessions and updates NapCatCore to support the new proxy integration.
2026-01-29 21:58:27 +08:00
手瓜一十雪
52be000fdd Update napcat-types to 0.0.11 and improve config API
Upgraded napcat-types dependency from 0.0.10 to 0.0.11. Refactored the API URL config option to use the new signature supporting reactivity directly, improving code clarity and maintainability.
2026-01-29 21:01:52 +08:00
手瓜一十雪
55ce5bcfd3 Bump napcat-types version to 0.0.11 2026-01-29 21:00:11 +08:00
手瓜一十雪
29888cb38b Remove undici dependency from lockfile
The undici package and its references have been removed from pnpm-lock.yaml, indicating it is no longer required as a dependency.
2026-01-29 20:58:14 +08:00
手瓜一十雪
6ea4c9ec65 Remove undici dependency and implement proxy download
Replaces the use of the undici library for HTTP downloads with proxy support by implementing a custom httpDownloadWithProxy function using Node.js http and tls modules. The undici dependency is removed from package.json, reducing external dependencies and improving compatibility.
2026-01-29 20:54:48 +08:00
手瓜一十雪
4bec3aa597 Reapply "Add image download proxy support to OneBot"
This reverts commit 38c320d2c9.
2026-01-29 20:40:19 +08:00
手瓜一十雪
38c320d2c9 Revert "Add image download proxy support to OneBot"
This reverts commit 0779628be5.
2026-01-29 20:39:07 +08:00
手瓜一十雪
76cbd8a1c1 Add crash protection for worker process restarts
Implements a mechanism to track recent worker process crashes and prevent excessive restarts. If the worker crashes more than 3 times within 10 seconds, the main process will exit to avoid crash loops.
2026-01-29 20:38:35 +08:00
手瓜一十雪
0779628be5 Add image download proxy support to OneBot
Introduces an 'imageDownloadProxy' config option to OneBot, allowing image downloads via a specified HTTP proxy. Updates the file download logic in napcat-common to use the undici library for proxy support, and propagates the new config through backend, frontend, and type definitions. Also adds undici as a dependency.
2026-01-29 20:32:01 +08:00
手瓜一十雪
34ca919c4d Add reactive plugin config UI with SSE support
Introduces a reactive plugin configuration system with dynamic schema updates via server-sent events (SSE). Adds new fields and controller interfaces to the plugin manager, updates the built-in plugin to demonstrate dynamic config fields, and implements backend and frontend logic for real-time config UI updates. Also updates napcat-types to 0.0.10.
2026-01-29 20:18:34 +08:00
手瓜一十雪
b1b357347b Remove unused installedVersion prop from PluginStoreCard
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
The installedVersion prop was declared but not used in PluginStoreCard. This commit removes it to clean up the component's props.
2026-01-29 17:13:30 +08:00
手瓜一十雪
129d63f66e Add mirror management and selection UI
Introduces backend API and router for mirror management, including latency testing and custom mirror setting. Adds frontend components and controllers for mirror selection, speed testing, and integration into system info and plugin store pages, allowing users to select and test download/list mirrors interactively.
2026-01-29 17:11:59 +08:00
手瓜一十雪
699b46acbd Improve plugin status handling and dirname lookup
Enhanced setPluginStatus to support enabling/disabling plugins by both package name and dirname, improving robustness when plugins are not loaded. Also removed redundant directory name matching logic from findDirnameById in the web UI backend.

Register plugin after installation in PluginStore

Adds logic to immediately register a plugin with the plugin manager after installation, both in the standard and SSE install handlers. This ensures newly installed plugins are available without requiring a restart or manual reload.

Refactor plugin path handling in plugin manager

Simplifies plugin directory and data path resolution by using pluginPath from the plugin context instead of fileId. Streamlines plugin uninstall and reload logic, removing redundant file system scans and improving code clarity.

Refactor plugin API to use package id and improve UX

Standardized plugin management APIs and frontend to use 'id' (package name) instead of ambiguous 'name' or 'filename'. Added support for a 'plugin' display field in package.json and improved plugin store UI to show install/update status. Refactored backend and frontend logic for enabling, disabling, uninstalling, and configuring plugins to use consistent identifiers, and enhanced type definitions and documentation for better maintainability.
2026-01-29 16:42:15 +08:00
手瓜一十雪
7f05aee11d Add manual plugin manager registration support
Introduces backend and frontend logic to manually register the plugin manager if not already loaded. Adds a new API endpoint and frontend UI prompt to guide users through registration after plugin installation when necessary.
2026-01-29 15:44:26 +08:00
手瓜一十雪
542036f46e Refactor type build: inline external types, simplify scripts
Removed custom build scripts for copying and inlining types, consolidating all post-build logic into a single enhanced post-build.mjs script. The new script processes .d.ts files, inlines external module types, updates imports, and copies necessary files to dist, eliminating the need for external-shims and simplifying the build process. Updated package.json scripts accordingly.

Refactor type inlining: remove shims, auto-extract types

Removed external-shims.d.ts and its references, replacing manual shims with an automated script that extracts type definitions from node_modules. Updated build scripts, dependencies, and test files to support the new inlining process. The inline-types.mjs script now scans for external imports, generates inline type files, and rewrites imports as import type, eliminating the need for hand-written shims.

Add type inlining script and update build process

Introduced a new script (inline-types.mjs) to inline external type dependencies into the dist directory, updated the build process to use this script, and removed the now-unnecessary external-shims.d.ts from the copy-dist script. Added a test file to verify inlined types, updated dependencies to include ts-morph, and adjusted package.json and pnpm-lock.yaml accordingly.
2026-01-29 15:27:46 +08:00
pohgxz
b958e9e803 修复 OpenAPI 导出的相应接口缺失 stream 字段 2026-01-29 14:14:11 +08:00
冷曦
73fcfb5900 修复下载插件后插件列表显示开启 (#1560)
下载后插件应该为禁用状态,但是前端显示启用状态
2026-01-29 13:12:54 +08:00
手瓜一十雪
adabc4da46 Improve schema parsing and error handling in API debug tools
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Enhances the TypeBox schema parser to better handle deep nesting, circular references, and union truncation, and adds error handling for schema parsing and default value generation in the OneBot API debug UI. Updates the display component to show clear messages for circular or truncated schemas, and improves robustness in HTTP debug command execution. Also synchronizes the ParsedSchema type in the Zod utility for consistency.
2026-01-28 16:07:23 +08:00
手瓜一十雪
bf073b544b Refactor schema ID handling and reduce parse depth
Changed MAX_PARSE_DEPTH from 10 to 6 to limit nesting. Improved schema ID retrieval to only use $id if present, and added a utility to collect all $id values in a schema for better circular reference detection. Updated font file AaCute.woff.
2026-01-28 16:01:43 +08:00
手瓜一十雪
a71219062a Enhance TypeBox schema parsing with circular ref detection
Added detection and handling for circular references and excessive nesting in TypeBox schema parsing and default value generation. Introduced depth limits and a visited set to prevent infinite recursion, and updated the parseTypeBox and generateDefaultFromTypeBox functions accordingly.
2026-01-28 15:59:27 +08:00
手瓜一十雪
001fe01ace Add plugin logger interface and update builtin plugin
Introduces a PluginLogger interface and injects a plugin-specific logger into the plugin context for consistent logging. Refactors the builtin plugin to use the new logger instead of direct console calls. Updates napcat-types to version 0.0.9 in dependencies and lock files.
2026-01-28 15:07:06 +08:00
手瓜一十雪
0aa0c44634 Refactor plugin identification to use package name and dirname
Updated plugin manager and API to distinguish between plugin package name and directory name (dirname) for more robust plugin identification and path resolution. Adjusted context creation, status management, and API handlers to use package name for identification and dirname for filesystem operations. Also replaced console.error with console.log in builtin plugin for consistency.
2026-01-28 15:02:47 +08:00
手瓜一十雪
93126e514e Refactor builtin plugin for improved type safety
Replaced generic 'any' types with 'NetworkAdapterConfig' for better type safety in getVersionInfo and sendMessage functions. Removed redundant comments and improved code clarity. Changed a warning log to a standard log for config load failures.
2026-01-28 14:54:43 +08:00
手瓜一十雪
1ae10ae0c6 Refactor plugin to use context actions and update deps
Refactored the builtin plugin to pass actions and adapterName explicitly from context instead of relying on a global variable. Updated napcat-types dependency to version 0.0.8.
2026-01-28 14:42:44 +08:00
手瓜一十雪
4b693bf6e2 Refactor plugin manager to support only directory plugins
Removed support for single-file plugins in OB11PluginMangerAdapter, simplifying plugin identification to use directory names as unique IDs. Updated related logic in the backend API to align with this change, ensuring consistent plugin management and status handling.
2026-01-28 14:38:11 +08:00
手瓜一十雪
574c257591 Refactor plugin manager to support only directory plugins
Removed support for single-file plugins in OB11PluginMangerAdapter, simplifying plugin identification to use directory names as unique IDs. Updated related logic in the backend API to align with this change, ensuring consistent plugin management and status handling.
2026-01-28 14:18:44 +08:00
手瓜一十雪
d680328762 Add config UI and persistence to builtin plugin
Introduces a configuration UI schema and persistent config storage for the napcat-plugin-builtin. The plugin now loads and saves its configuration, supports dynamic prefix and reply toggling, and updates dependencies to napcat-types v0.0.6.
2026-01-28 14:13:48 +08:00
手瓜一十雪
d711cdecaf Add plugin config management to backend and frontend
Introduces a unified plugin configuration schema and API in the backend, with endpoints for getting and setting plugin config. Updates the frontend to support plugin config modals, including a UI for editing plugin settings. Also adds support for uninstalling plugins with optional data cleanup, and updates dependencies to use napcat-types@0.0.5.
2026-01-28 13:56:40 +08:00
手瓜一十雪
c5f1792009 Add plugin data management API and frontend support
Introduced backend API endpoints for managing plugin configuration data, including listing, reading, saving, and deleting plugin data files. Added a new PluginData API module and registered related routes. Updated the frontend plugin manager controller to support these new API methods and corresponding TypeScript interfaces. Also fixed minor typos in documentation prompts.
2026-01-28 13:39:36 +08:00
手瓜一十雪
a5e705e6a4 Fix typo in Windows deployment instructions
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Corrected a grammatical error in the Windows one-click deployment section in both the default prompt and release note prompt files.
2026-01-27 23:01:16 +08:00
手瓜一十雪
007f1db339 Update OpenAPI schema copy path in release workflow
The workflow now copies openapi.json from the dist directory instead of the package root, ensuring the built schema is used during the release process.
2026-01-27 23:00:14 +08:00
手瓜一十雪
008fb39f8f Reduce WebSocket maxPayload to 50 MB
Lowered the maxPayload limit from 1 GB to 50 MB in both the WebSocket client and server adapters to improve resource management and prevent excessively large payloads.
2026-01-27 22:56:27 +08:00
手瓜一十雪
6b8cc6756d Add plugin install SSE API and mirror selection UI
Introduces a new SSE-based plugin installation API for real-time progress updates and adds frontend support for selecting download mirrors, especially for GitHub-based plugins. Refactors backend plugin directory handling, improves logging, and updates the frontend to use the new API with user-selectable mirrors and progress feedback.
2026-01-27 22:51:45 +08:00
手瓜一十雪
24623f18d8 Implement real plugin store backend and install logic
Replaces mock plugin store data with live fetching from a remote source using mirrors and caching. Implements actual plugin download, extraction, and installation logic in the backend. Updates frontend to call the new install API and display installation results, and removes unused icon/rating display from plugin cards.
2026-01-27 22:03:47 +08:00
手瓜一十雪
8b676ed693 Refactor plugin store types and update category logic
Simplified PluginStoreItem types by removing unused fields and standardizing the minimum version property. Updated frontend plugin store page to use new categories ('官方', '工具', '娱乐', '其它') instead of download-based sorting, aligning with the revised type definitions.
2026-01-27 21:47:44 +08:00
手瓜一十雪
ab2dfcfd8f Update dependencies in pnpm-lock.yaml
Added @sinclair/typebox to packages/napcat-types dependencies.
2026-01-27 21:31:31 +08:00
手瓜一十雪
2998e04435 Add @sinclair/typebox as dependency and remove shim
Removed the manual TypeBox type definitions from external-shims.d.ts and added @sinclair/typebox as a dependency in package.json and package.public.json. Bumped package version to 0.0.4 to reflect this change.
2026-01-27 21:24:50 +08:00
手瓜一十雪
613690f5af Fix build:openapi script to use sequential commands
Replaces '&' with '&&' in the build:openapi script to ensure that 'node ./dist/schemas.mjs' runs only after 'vite build' completes successfully.
2026-01-27 20:34:21 +08:00
手瓜一十雪
a60d8d109c Update napcat-types usage and config in plugin
Changed napcat-plugin-builtin to use workspace reference for napcat-types instead of a fixed version. Updated tsconfig to use bundler module resolution and improved Vite config plugin resolution. Enhanced napcat-types package.json exports for better type support.
2026-01-27 20:22:40 +08:00
时瑾
28c9761e3d fix(napcat-plugin-builtin): 修复vite.config.ts hook 2026-01-27 20:07:53 +08:00
手瓜一十雪
805cc32d7f Update napcat-types dependency to version 0.0.2
Changed import paths in index.ts to match new napcat-types structure and updated package.json to use napcat-types@0.0.2 instead of workspace reference. Updated pnpm-lock.yaml accordingly.
2026-01-27 19:12:04 +08:00
手瓜一十雪
de9d5180fe Add payload and return schemas to OneBot actions (#1549)
* Add payload and return schemas to OneBot actions

Introduced explicit payloadSchema and returnSchema definitions for all OneBotAction classes using @sinclair/typebox. This improves type safety, API documentation, and validation for action payloads and return values. Also refactored method signatures and types for consistency across the codebase.

* Refactor payload schemas to use string IDs

Replaced Type.Union([Type.Number(), Type.String()]) with Type.String for group_id, user_id, and similar fields across all action payload schemas to standardize input types. Also made minor improvements to error handling, return types, and removed unused imports for better code clarity and consistency.

* Refactor type definitions and payload schemas in actions

Standardized type usage and improved type safety across multiple OneBot action files. Updated payload schemas to use string types for IDs and flags, refined return types, and enhanced message content typing. Added error handling for missing parameters in SetGroupTodo.

* Refactor type handling and improve message parsing

Updated several actions to use more precise type casting and type guards, improving type safety and clarity. Enhanced message parsing logic for forward messages and group/friend message history. Standardized return schemas and error handling for avatar and group portrait actions.

* Add napcat-schema package for OpenAPI generation

Introduces the napcat-schema package with scripts and configuration to auto-generate OpenAPI schemas for NapCat OneBot 11 actions. Refactors action handler export in napcat-onebot to support schema extraction.

* Add action examples and enhance action metadata

Introduced a centralized examples.ts file providing payload and return examples for all actions. Updated numerous action classes to include actionDescription, actionTags, payloadExample, and returnExample fields, improving API documentation and discoverability.

* Refactor action example imports and add example files

Moved action example data to dedicated 'examples.ts' files for each action category (extends, file, go-cqhttp, group, msg, system, user). Updated all action classes to import and use the new example modules, improving code organization and maintainability. Also added missing actionTags and actionDescription where appropriate.

* Update GetGroupMemberList.ts

* Add actionSummary and improve action metadata

Introduces the actionSummary property to OneBotAction and updates all action classes to provide concise summaries and improved descriptions. Refactors example imports for better modularity, adds new example files for guild and packet actions, and updates the OpenAPI schema generator to use the new summary and improved descriptions. This enhances API documentation clarity and consistency.

* Enhance action metadata and add examples for new actions

Added actionSummary, actionDescription, and actionTags to multiple OneBot actions for improved API documentation. Introduced payload and response examples for new actions (GetDoubtFriendsAddRequest, SetDoubtFriendsAddRequest) in a new examples.ts file. Also removed unused imports from several files for code clarity.

* Refactor action examples and enhance metadata

Replaced generic ActionExamples imports with more specific examples modules (FileActionsExamples, GroupActionsExamples, GoCQHTTPActionsExamples) across file, group, and go-cqhttp actions. Added or updated actionSummary, actionDescription, actionTags, payloadExample, and returnExample properties for improved API documentation and clarity.

* Refactor extends actions to use new examples module

Replaced imports of ActionExamples with ExtendsActionsExamples in all extends actions. Updated action summary, description, tags, and example references for consistency and clarity across actions. This improves maintainability and aligns with the new examples structure.

* Add action metadata to OneBot action classes

Added or updated actionSummary, actionTags, payloadExample, and returnExample properties for all OneBot action classes in the napcat-onebot package. This improves API documentation and discoverability by providing concise summaries, categorization tags, and usage examples for each action.

* Refactor OpenAPI schema generation to 3.0.1 format

Updated the OpenAPI schema output to use version 3.0.1, restructured tags, responses, and examples for better clarity and compatibility, and simplified output file locations. Also removed unused scripts from package.json.

* Fix SendPokePayloadSchema type definitions

Corrected the type definitions for user_id and target_id to only allow strings, and fixed a syntax error in group_id. This ensures payload validation is consistent and accurate.

Refactor fileset ID API response and schema handling

Updated GetFilesetId action to return a structured object with fileset_id and adjusted its return schema accordingly. Improved frontend TypeBox schema parsing to support allOf (intersection) merging and updated API debug component to construct response schemas in a more robust way for object recognition.

Refactor OneBot API schema handling to use TypeBox

Replaces Zod-based static API schema definitions with dynamic fetching of schemas from the backend using TypeBox. Removes legacy static schema files, updates frontend API debug components to use TypeBox utilities, and adds @sinclair/typebox as a dependency. Backend now exposes a /schemas endpoint for all OneBot actions. Various schema and description fields are updated for clarity and consistency.

* Remove OneBot API navigation list component

Deleted nav_list.tsx from the onebot/api components, removing the OneBotApiNavList React component and its related logic. This may be part of a refactor or cleanup to eliminate unused or redundant UI code.

* Add action tags to OneBot API schema and update tag name

Included the 'tags' property in the OneBot API schema for both backend and frontend, allowing actions to be categorized. Also updated the action tag from '群扩展' to '群组扩展' in SetGroupSign for consistency.

* Add napcat-types package for unified type exports

Introduced the napcat-types package to aggregate and re-export all types, enums, and classes from napcat-core and napcat-onebot. Added external module shims, test files, and configuration for type-only distribution. Updated core and onebot packages to improve export granularity and fixed import paths for better modularity.

* Move external-shims.d.ts to files in tsconfig

external-shims.d.ts was moved from the include array to the files array in tsconfig.json to ensure it is always included explicitly. This change clarifies the intent and may help with TypeScript's file resolution.

Refactor napcat-types package and update plugin deps

Refactored napcat-types to provide more accurate shims, added real type dependencies, and improved build/test scripts. Updated napcat-plugin and napcat-plugin-builtin to depend on napcat-types instead of napcat-onebot. Adjusted imports in affected packages to use napcat-types, and updated pnpm-lock.yaml accordingly.

Add build and test scripts to napcat-types package

Introduced 'build' and 'test' scripts in the napcat-types package.json for easier development and testing. Also updated dependencies in the lockfile.

* 完善部分api描述

* Remove unused statusText constant

Deleted the unused statusText constant from FetchCustomFace.ts to clean up the code.

* Bump napcat-types version to 0.0.2

Updated the package version in package.public.json from 0.0.1 to 0.0.2.

Update napcat-types package metadata and dependencies

Set package as public by changing 'private' to false. Move 'napcat-core' and 'napcat-onebot' from dependencies to devDependencies, and remove 'compressing' from dependencies.

Add public packaging and build script for napcat-types

Introduces package.public.json and a copy-dist.mjs script to automate copying metadata and README into the dist folder for publishing. Updates build script in package.json to use the new copy step. Adds initial package.json and README for napcat-types.

Update scripts in napcat-types package configs

Added a publish script to package.json and removed scripts from package.public.json to streamline configuration and avoid duplication.

* Update publish script to use npm in dist directory

Changed the publish script to run 'npm publish' from the 'dist' directory instead of using 'pnpm publish --filter napcat-types'. This ensures the published package uses the built output.

* Update pnpm-lock.yaml dependencies

Removed 'compressing' from dependencies and cleaned up libc fields for various platform-specific packages. This streamlines the lock file and may improve cross-platform compatibility.

* Add workflow to publish OpenAPI schema to NapCatDocs

Introduces a new 'publish-schema' job in the auto-release workflow. This job builds the napcat-schema package, copies the generated OpenAPI schema to the NapCatDocs repository under a versioned path, and commits the update. Automates schema publishing on release events.

* AI修正部分api文档

* Update OpenAPI version and use dynamic version from napcat-common

Changed OpenAPI spec version to 3.1.0 and replaced the hardcoded API version with napCatVersion from napcat-common. Added napcat-common as a dependency in package.json.

* Update napcat-schema build and OpenAPI version

Renamed the build script from build:schema to build:openapi in napcat-schema and updated the workflow to use the new script. Changed OpenAPI version from 3.1.0 to 3.0.1 in the schema generator. Added napcat-vite as a dependency and integrated its version plugin into the Vite config.

* 暂时OK

* Refactor action examples structure and imports

Moved action example files into a new 'example' directory and updated all imports accordingly. Removed the monolithic 'examples.ts' and redefined ActionExamples in OneBotAction.ts to only include common error codes. This improves code organization and maintainability.

* Fix type for rate limiter middleware in router

Casts the rate limiter middleware to RequestHandler to resolve type compatibility issues with Express router middleware.

* Add OB11 message segment schemas and update SendMsg

Introduces a comprehensive message segment schema (OB11) in a new file, refactors SendMsg payload to use the new OB11MessageMixTypeSchema, and updates related type definitions for improved type safety and extensibility in message handling.

* Refactor OB11 message types to use TypeBox schemas

Migrates all OB11 message segment and message type definitions from interface/enums to TypeBox schemas in types/message.ts. Removes the now-redundant message-segment-schema.ts file and updates imports to use the new schema-based types. This unifies type validation and TypeScript types, improving maintainability and consistency.

---------

Co-authored-by: pohgxz <Ttjonne@outlook.com>
2026-01-27 19:07:17 +08:00
手瓜一十雪
791e359199 Update documentation links in README
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Replaced outdated Server.Other and NapCat.Wiki links with updated napcat.top URLs and labels in the documentation section of the README.
2026-01-27 12:59:18 +08:00
手瓜一十雪
63a9d571f3 Add flexible IP access control to WebUI config
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Replaces the 'disableNonLANAccess' option with a more flexible access control system supporting 'none', 'whitelist', and 'blacklist' modes, along with IP list and X-Forwarded-For support. Updates backend API, config schema, middleware, and frontend UI to allow configuration of access control mode, IP whitelist/blacklist, and X-Forwarded-For handling. Removes legacy LAN-only access logic and updates types accordingly.
2026-01-26 19:46:15 +08:00
手瓜一十雪
59d4b08982 Fix typo in delBuddy method name & fix #1550
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Renamed the delBuudy method to delBuddy in NTQQFriendApi and updated its usage in GoCQHTTPDeleteFriend to ensure correct method invocation.
2026-01-25 21:09:12 +08:00
手瓜一十雪
81e4e54f25 Optimize emoji likes fetching logic
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Adjusts the pagination logic to fetch only the required number of pages based on the requested count. Trims the result list to the specified count if provided, improving efficiency and accuracy.
2026-01-25 13:21:27 +08:00
Hans155922
b75b733bb0 增加/fetch_emoji_likes_all (#1548)
* Update FetchEmojiLike.ts

* 减少getMsgEmojiLikesList参数,一次性全部拉取

* Update FetchEmojiLike.ts

* Refactor API message schema and update descriptions

* Update and rename FetchEmojiLike.ts to FetchEmojiLikesAll.ts

* Create FetchEmojiLike.ts

* Update router.ts

* Update index.ts

* Update index.ts

* Update FetchEmojiLikesAll.ts

* Update FetchEmojiLikesAll.ts

* Refactor emoji likes API and update related logic

Replaces FetchEmojiLikesAll with GetEmojiLikes, updating the API to use a new payload and return schema. Adjusts action registration, router action names, and frontend API mapping accordingly. Adds isShortId utility to MessageUnique for improved message ID handling.

---------

Co-authored-by: 手瓜一十雪 <nanaeonn@outlook.com>
2026-01-25 13:18:16 +08:00
H3CoF6
246269b519 feat: Support custom filename and cover image for Flash Transfer (#1544)
* feat: support thumbnail for flash-transfer

* fix: fix get thumbnail path unknown type error

* Refactor flash module types and enums

Standardized TypeScript interface property formatting in flash.ts, flash data, and wrapper files. Introduced the UploadSceneType enum for upload scene types, replacing hardcoded numeric values. Improved type annotations and consistency across the flash API and related data structures.

* Update arg type in NodeQQNTWrapperUtil interface

Changed the type of the 'arg' parameter in the NodeQQNTWrapperUtil interface from optional number to 'number | null | undefined' for improved type clarity.

* Refactor flash scene type and update method params

Introduced BusiScene enum for sceneType in FileListInfoRequests to improve type safety. Renamed parameters in getFileThumbSavePathForSend for better clarity.

* Refactor downloadSceneType to use enum type

Replaced numeric downloadSceneType fields with the DownloadSceneType enum in relevant interfaces. Updated NodeIKernelFlashTransferService method signatures to use DownloadSceneType for download operations, improving type safety and code clarity.

* refactor: remove thumbnail dependency for QQ resource icons

* fix: remove useless console.log

---------

Co-authored-by: 手瓜一十雪 <nanaeonn@outlook.com>
2026-01-25 09:51:43 +08:00
手瓜一十雪
3c24d6b700 Refactor markdownElement flash transfer check
Some checks failed
Build NapCat Artifacts / Build-Framework (push) Has been cancelled
Build NapCat Artifacts / Build-Shell (push) Has been cancelled
Simplifies the condition for detecting flash transfer info in markdownElement by removing redundant undefined check.
2026-01-24 12:31:03 +08:00
手瓜一十雪
679c980683 Refine markdown element handling in message parsing
Simplified the condition for returning markdown summaries in log.ts and improved the check for flash transfer info in msg.ts to ensure filesetId exists. This enhances message parsing reliability for markdown and flash transfer messages.
2026-01-24 12:28:52 +08:00
手瓜一十雪
19766002ae Add token check exception for localhost servers
Updated the network form modal to allow missing tokens only for servers with host '127.0.0.1'. This enhances security by prompting a warning when a token is missing for non-localhost servers.
2026-01-24 12:24:57 +08:00
手瓜一十雪
c2d3a8034d Add plugin store feature to backend and frontend
Implemented plugin store API endpoints and types in the backend, including mock data and handlers for listing, detail, and install actions. Added plugin store page, card component, and related logic to the frontend, with navigation and categorized browsing. Updated plugin manager controller and site config to support the new plugin store functionality.
2026-01-24 12:00:26 +08:00
手瓜一十雪
58220d3fbc fix #1515 & Add cookie parameter to getMsgEmojiLikesList API
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Introduces an optional 'cookie' parameter to the getMsgEmojiLikesList method in NTQQMsgApi and updates FetchEmojiLike to support passing this parameter. This allows for more flexible pagination or state management when fetching emoji likes.
2026-01-23 21:41:28 +08:00
351 changed files with 17619 additions and 6511 deletions

View File

@@ -2,7 +2,7 @@
[使用文档](https://napneko.github.io/)
## Windows 一键包
我们提供了轻量化一键部署方案
我们提供了轻量化一键部署方案
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
你可以下载

View File

@@ -31,7 +31,7 @@
[使用文档](https://napneko.github.io/)
## Windows 一键包
我们提供了轻量化一键部署方案
我们提供了轻量化一键部署方案
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
你可以下载

View File

@@ -5,6 +5,63 @@ on:
types: [published]
jobs:
publish-schema:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Get Version
id: get_version
run: |
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1))
version=${latest_tag#v}
echo "version=${version}" >> $GITHUB_ENV
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
echo "Debug: Version is ${version}"
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Build napcat-schema
run: |
cd packages/napcat-schema
pnpm run build:openapi
- name: Checkout NapCatDocs
uses: actions/checkout@v4
with:
repository: NapNeko/NapCatDocs
token: ${{ secrets.NAPCAT_BUILD }}
path: napcat-docs
- name: Copy OpenAPI Schema
run: |
mkdir -p napcat-docs/src/api/${{ env.version }}
cp packages/napcat-schema/dist/openapi.json napcat-docs/src/api/${{ env.version }}/openapi.json
echo "OpenAPI schema copied to napcat-docs/src/api/${{ env.version }}/openapi.json"
- name: Commit and Push
run: |
cd napcat-docs
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add src/api/${{ env.version }}/openapi.json
git commit -m "chore: update OpenAPI schema for version ${{ env.version }}" || echo "No changes to commit"
git push
shell-docker:
runs-on: ubuntu-latest
steps:

3
.gitignore vendored
View File

@@ -16,4 +16,5 @@ checkVersion.sh
bun.lockb
tests/run/
guild1.db-wal
guild1.db-shm
guild1.db-shm
packages/napcat-develop/config/.env

View File

@@ -50,7 +50,7 @@ _Modern protocol-side framework implemented based on NTQQ._
| Docs | [![Github.IO](https://img.shields.io/badge/docs%20on-Github.IO-orange)](https://napneko.github.io/) | [![Cloudflare.Worker](https://img.shields.io/badge/docs%20on-Cloudflare.Worker-black)](https://doc.napneko.icu/) | [![Cloudflare.HKServer](https://img.shields.io/badge/docs%20on-Cloudflare.HKServer-informational)](https://napcat.napneko.icu/) |
|:-:|:-:|:-:|:-:|
| Docs | [![Cloudflare.Pages](https://img.shields.io/badge/docs%20on-Cloudflare.Pages-blue)](https://napneko.pages.dev/) | [![Server.Other](https://img.shields.io/badge/docs%20on-Server.Other-green)](https://napcat.cyou/) | [![NapCat.Wiki](https://img.shields.io/badge/docs%20on-NapCat.Wiki-red)](https://www.napcat.wiki) |
| Docs | [![Cloudflare.Pages](https://img.shields.io/badge/docs%20on-Cloudflare.Pages-blue)](https://napneko.pages.dev/) | [![Server.Other](https://img.shields.io/badge/docs%20on-Server.Other-green)](https://napcat.top/) | [![NapCat.Top](https://img.shields.io/badge/docs%20on-NapCat.Top-red)](https://napcat.top/) |
|:-:|:-:|:-:|:-:|
| QQ Group | [![QQ Group#4](https://img.shields.io/badge/QQ%20Group%234-Join-blue)](https://qm.qq.com/q/CMmPbGw0jA) | [![QQ Group#3](https://img.shields.io/badge/QQ%20Group%233-Join-blue)](https://qm.qq.com/q/8zJMLjqy2Y) | [![QQ Group#2](https://img.shields.io/badge/QQ%20Group%232-Join-blue)](https://qm.qq.com/q/CMmPbGw0jA) | [![QQ Group#1](https://img.shields.io/badge/QQ%20Group%231-Join-blue)](https://qm.qq.com/q/I6LU87a0Yq) |

View File

@@ -6,6 +6,7 @@
"scripts": {
"build:shell": "pnpm --filter napcat-shell run build || exit 1",
"build:shell:dev": "pnpm --filter napcat-shell run build:dev || exit 1",
"build:shell:config": "pnpm --filter napcat-shell run build && pnpm --filter napcat-develop run copy-env",
"build:framework": "pnpm --filter napcat-framework run build || exit 1",
"build:webui": "pnpm --filter napcat-webui-frontend run build || exit 1",
"build:plugin-builtin": "pnpm --filter napcat-plugin-builtin run build || exit 1",

View File

@@ -0,0 +1,176 @@
import { InstanceContext, NapCatCore } from 'napcat-core';
import { NapCatPathWrapper } from 'napcat-common/src/path';
import { NapCatOneBot11Adapter } from 'napcat-onebot';
import { NapCatProtocolAdapter } from 'napcat-protocol';
// 协议适配器类型
export type ProtocolAdapterType = 'onebot11' | 'napcat-protocol';
// 协议适配器接口
export interface IProtocolAdapter {
readonly name: string;
readonly enabled: boolean;
init (): Promise<void>;
close (): Promise<void>;
}
// 协议适配器包装器
class OneBotAdapterWrapper implements IProtocolAdapter {
readonly name = 'onebot11';
private adapter: NapCatOneBot11Adapter;
constructor (adapter: NapCatOneBot11Adapter) {
this.adapter = adapter;
}
get enabled (): boolean {
return true; // OneBot11 默认启用
}
async init (): Promise<void> {
await this.adapter.InitOneBot();
}
async close (): Promise<void> {
await this.adapter.networkManager.closeAllAdapters();
}
getAdapter (): NapCatOneBot11Adapter {
return this.adapter;
}
}
// NapCat Protocol 适配器包装器
class NapCatProtocolAdapterWrapper implements IProtocolAdapter {
readonly name = 'napcat-protocol';
private adapter: NapCatProtocolAdapter;
constructor (adapter: NapCatProtocolAdapter) {
this.adapter = adapter;
}
get enabled (): boolean {
return this.adapter.isEnabled();
}
async init (): Promise<void> {
await this.adapter.initProtocol();
}
async close (): Promise<void> {
await this.adapter.close();
}
getAdapter (): NapCatProtocolAdapter {
return this.adapter;
}
}
// 协议适配器管理器
export class NapCatAdapterManager {
private core: NapCatCore;
private context: InstanceContext;
private pathWrapper: NapCatPathWrapper;
// 协议适配器实例
private onebotAdapter: OneBotAdapterWrapper | null = null;
private napcatProtocolAdapter: NapCatProtocolAdapterWrapper | null = null;
// 所有已注册的适配器
private adapters: Map<string, IProtocolAdapter> = new Map();
constructor (core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) {
this.core = core;
this.context = context;
this.pathWrapper = pathWrapper;
}
// 初始化所有协议适配器
async initAdapters (): Promise<void> {
this.context.logger.log('[AdapterManager] 开始初始化协议适配器...');
// 初始化 OneBot11 适配器 (默认启用)
try {
const onebot = new NapCatOneBot11Adapter(this.core, this.context, this.pathWrapper);
this.onebotAdapter = new OneBotAdapterWrapper(onebot);
this.adapters.set('onebot11', this.onebotAdapter);
await this.onebotAdapter.init();
this.context.logger.log('[AdapterManager] OneBot11 适配器初始化完成');
} catch (e) {
this.context.logger.logError('[AdapterManager] OneBot11 适配器初始化失败:', e);
}
// 初始化 NapCat Protocol 适配器 (默认关闭,需要配置启用)
try {
const napcatProtocol = new NapCatProtocolAdapter(this.core, this.context, this.pathWrapper);
this.napcatProtocolAdapter = new NapCatProtocolAdapterWrapper(napcatProtocol);
this.adapters.set('napcat-protocol', this.napcatProtocolAdapter);
if (this.napcatProtocolAdapter.enabled) {
await this.napcatProtocolAdapter.init();
this.context.logger.log('[AdapterManager] NapCat Protocol 适配器初始化完成');
} else {
this.context.logger.log('[AdapterManager] NapCat Protocol 适配器未启用,跳过初始化');
}
} catch (e) {
this.context.logger.logError('[AdapterManager] NapCat Protocol 适配器初始化失败:', e);
}
this.context.logger.log(`[AdapterManager] 协议适配器初始化完成,已加载 ${this.adapters.size} 个适配器`);
}
// 获取 OneBot11 适配器
getOneBotAdapter (): NapCatOneBot11Adapter | null {
return this.onebotAdapter?.getAdapter() ?? null;
}
// 获取 NapCat Protocol 适配器
getNapCatProtocolAdapter (): NapCatProtocolAdapter | null {
return this.napcatProtocolAdapter?.getAdapter() ?? null;
}
// 获取指定适配器
getAdapter (name: ProtocolAdapterType): IProtocolAdapter | undefined {
return this.adapters.get(name);
}
// 获取所有已启用的适配器
getEnabledAdapters (): IProtocolAdapter[] {
return Array.from(this.adapters.values()).filter(adapter => adapter.enabled);
}
// 获取所有适配器
getAllAdapters (): IProtocolAdapter[] {
return Array.from(this.adapters.values());
}
// 关闭所有适配器
async closeAllAdapters (): Promise<void> {
this.context.logger.log('[AdapterManager] 开始关闭所有协议适配器...');
for (const [name, adapter] of this.adapters) {
try {
await adapter.close();
this.context.logger.log(`[AdapterManager] ${name} 适配器已关闭`);
} catch (e) {
this.context.logger.logError(`[AdapterManager] 关闭 ${name} 适配器失败:`, e);
}
}
this.adapters.clear();
this.context.logger.log('[AdapterManager] 所有协议适配器已关闭');
}
// 重新加载指定适配器
async reloadAdapter (name: ProtocolAdapterType): Promise<void> {
const adapter = this.adapters.get(name);
if (adapter) {
await adapter.close();
await adapter.init();
this.context.logger.log(`[AdapterManager] ${name} 适配器已重新加载`);
}
}
}
export { NapCatOneBot11Adapter } from 'napcat-onebot';
export { NapCatProtocolAdapter } from 'napcat-protocol';

View File

@@ -0,0 +1,30 @@
{
"name": "napcat-adapter",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.ts",
"scripts": {
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"exports": {
".": {
"import": "./index.ts"
},
"./*": {
"import": "./*"
}
},
"dependencies": {
"napcat-core": "workspace:*",
"napcat-common": "workspace:*",
"napcat-onebot": "workspace:*",
"napcat-protocol": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@@ -1,28 +1,28 @@
{
"name": "napcat-common",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
"name": "napcat-common",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"exports": {
".": {
"import": "./src/index.ts"
},
"exports": {
".": {
"import": "./src/index.ts"
},
"./src/*": {
"import": "./src/*"
}
},
"dependencies": {
"ajv": "^8.13.0",
"file-type": "^21.0.0"
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
"./src/*": {
"import": "./src/*"
}
},
"dependencies": {
"ajv": "^8.13.0",
"file-type": "^21.0.0"
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@@ -2,11 +2,14 @@ import fs from 'fs';
import { stat } from 'fs/promises';
import crypto, { randomUUID } from 'crypto';
import path from 'node:path';
import http from 'node:http';
import tls from 'node:tls';
import { solveProblem } from '@/napcat-common/src/helper';
export interface HttpDownloadOptions {
url: string;
headers?: Record<string, string> | string;
proxy?: string;
}
type Uri2LocalRes = {
@@ -96,6 +99,7 @@ export function calculateFileMD5 (filePath: string): Promise<string> {
async function tryDownload (options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
let url: string;
let proxy: string | undefined;
let headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
};
@@ -104,6 +108,7 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
headers['Host'] = new URL(url).hostname;
} else {
url = options.url;
proxy = options.proxy;
if (options.headers) {
if (typeof options.headers === 'string') {
headers = JSON.parse(options.headers);
@@ -115,6 +120,18 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
if (useReferer && !headers['Referer']) {
headers['Referer'] = url;
}
// 如果配置了代理,使用代理下载
if (proxy) {
try {
const response = await httpDownloadWithProxy(url, headers, proxy);
return new Response(response, { status: 200, statusText: 'OK' });
} catch (proxyError) {
// 如果代理失败,记录错误并尝试直接下载
console.error('代理下载失败,尝试直接下载:', proxyError);
}
}
const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
if (err.cause) {
throw err.cause;
@@ -124,6 +141,220 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
return fetchRes;
}
/**
* 使用 HTTP/HTTPS 代理下载文件
*/
function httpDownloadWithProxy (url: string, headers: Record<string, string>, proxy: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
const targetUrl = new URL(url);
const proxyUrl = new URL(proxy);
const isTargetHttps = targetUrl.protocol === 'https:';
const proxyPort = parseInt(proxyUrl.port) || (proxyUrl.protocol === 'https:' ? 443 : 80);
// 代理认证头
const proxyAuthHeader = proxyUrl.username && proxyUrl.password
? { 'Proxy-Authorization': 'Basic ' + Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString('base64') }
: {};
if (isTargetHttps) {
// HTTPS 目标:需要通过 CONNECT 建立隧道
const connectReq = http.request({
host: proxyUrl.hostname,
port: proxyPort,
method: 'CONNECT',
path: `${targetUrl.hostname}:${targetUrl.port || 443}`,
headers: {
'Host': `${targetUrl.hostname}:${targetUrl.port || 443}`,
...proxyAuthHeader,
},
});
connectReq.on('connect', (res, socket) => {
if (res.statusCode !== 200) {
socket.destroy();
reject(new Error(`代理 CONNECT 失败: ${res.statusCode} ${res.statusMessage}`));
return;
}
// 在隧道上建立 TLS 连接
const tlsSocket = tls.connect({
socket: socket,
servername: targetUrl.hostname,
rejectUnauthorized: true,
}, () => {
// TLS 握手成功,发送 HTTP 请求
const requestPath = targetUrl.pathname + targetUrl.search;
const requestHeaders = {
...headers,
'Host': targetUrl.hostname,
'Connection': 'close',
};
const headerLines = Object.entries(requestHeaders)
.map(([key, value]) => `${key}: ${value}`)
.join('\r\n');
const httpRequest = `GET ${requestPath} HTTP/1.1\r\n${headerLines}\r\n\r\n`;
tlsSocket.write(httpRequest);
});
// 解析 HTTP 响应
let responseData = Buffer.alloc(0);
let headersParsed = false;
let statusCode = 0;
let isChunked = false;
let bodyData = Buffer.alloc(0);
let redirectLocation: string | null = null;
tlsSocket.on('data', (chunk: Buffer) => {
responseData = Buffer.concat([responseData, chunk]);
if (!headersParsed) {
const headerEndIndex = responseData.indexOf('\r\n\r\n');
if (headerEndIndex !== -1) {
headersParsed = true;
const headerStr = responseData.subarray(0, headerEndIndex).toString();
const headerLines = headerStr.split('\r\n');
// 解析状态码
const statusLine = headerLines[0];
const statusMatch = statusLine?.match(/HTTP\/\d\.\d\s+(\d+)/);
statusCode = statusMatch ? parseInt(statusMatch[1]!) : 0;
// 解析响应头
for (const line of headerLines.slice(1)) {
const [key, ...valueParts] = line.split(':');
const value = valueParts.join(':').trim();
if (key?.toLowerCase() === 'transfer-encoding' && value.toLowerCase() === 'chunked') {
isChunked = true;
} else if (key?.toLowerCase() === 'location') {
redirectLocation = value;
}
}
bodyData = responseData.subarray(headerEndIndex + 4);
}
} else {
bodyData = Buffer.concat([bodyData, chunk]);
}
});
tlsSocket.on('end', () => {
// 处理重定向
if (statusCode >= 300 && statusCode < 400 && redirectLocation) {
const redirectUrl = redirectLocation.startsWith('http')
? redirectLocation
: `${targetUrl.protocol}//${targetUrl.host}${redirectLocation}`;
httpDownloadWithProxy(redirectUrl, headers, proxy).then(resolve).catch(reject);
return;
}
if (statusCode !== 200) {
reject(new Error(`下载失败: ${statusCode}`));
return;
}
// 处理 chunked 编码
if (isChunked) {
resolve(parseChunkedBody(bodyData));
} else {
resolve(bodyData);
}
});
tlsSocket.on('error', (err) => {
reject(new Error(`TLS 连接错误: ${err.message}`));
});
});
connectReq.on('error', (err) => {
reject(new Error(`代理连接错误: ${err.message}`));
});
connectReq.end();
} else {
// HTTP 目标:直接通过代理请求
const req = http.request({
host: proxyUrl.hostname,
port: proxyPort,
method: 'GET',
path: url, // 完整 URL
headers: {
...headers,
'Host': targetUrl.hostname,
...proxyAuthHeader,
},
}, (response) => {
handleResponse(response, resolve, reject, url, headers, proxy);
});
req.on('error', (err) => {
reject(new Error(`代理请求错误: ${err.message}`));
});
req.end();
}
});
}
/**
* 解析 chunked 编码的响应体
*/
function parseChunkedBody (data: Buffer): Buffer {
const chunks: Buffer[] = [];
let offset = 0;
while (offset < data.length) {
// 查找 chunk 大小行的结束
const lineEnd = data.indexOf('\r\n', offset);
if (lineEnd === -1) break;
const sizeStr = data.subarray(offset, lineEnd).toString().split(';')[0]; // 忽略 chunk 扩展
const chunkSize = parseInt(sizeStr!, 16);
if (chunkSize === 0) break; // 最后一个 chunk
const chunkStart = lineEnd + 2;
const chunkEnd = chunkStart + chunkSize;
if (chunkEnd > data.length) break;
chunks.push(data.subarray(chunkStart, chunkEnd));
offset = chunkEnd + 2; // 跳过 chunk 数据后的 \r\n
}
return Buffer.concat(chunks);
}
/**
* 处理 HTTP 响应
*/
function handleResponse (
response: http.IncomingMessage,
resolve: (value: Buffer) => void,
reject: (reason: Error) => void,
_url: string,
headers: Record<string, string>,
proxy: string
): void {
// 处理重定向
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
httpDownloadWithProxy(response.headers.location, headers, proxy).then(resolve).catch(reject);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`下载失败: ${response.statusCode} ${response.statusMessage}`));
return;
}
const chunks: Buffer[] = [];
response.on('data', (chunk: Buffer) => chunks.push(chunk));
response.on('end', () => resolve(Buffer.concat(chunks)));
response.on('error', reject);
}
export async function httpDownload (options: string | HttpDownloadOptions): Promise<Buffer> {
const useReferer = typeof options === 'string';
let resp = await tryDownload(options);
@@ -176,7 +407,7 @@ export async function checkUriType (Uri: string) {
return { Uri, Type: FileUriType.Unknown };
}
export async function uriToLocalFile (dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>): Promise<Uri2LocalRes> {
export async function uriToLocalFile (dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>, proxy?: string): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
const filePath = path.join(dir, filename);
@@ -191,7 +422,7 @@ export async function uriToLocalFile (dir: string, uri: string, filename: string
}
case FileUriType.Remote: {
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {} });
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {}, proxy });
fs.writeFileSync(filePath, buffer);
return { success: true, errMsg: '', fileName: filename, path: filePath };
}

View File

@@ -58,12 +58,12 @@ export class LimitedHashTable<K, V> {
}
// 获取最近刚写入的几个值
getHeads (size: number): { key: K; value: V }[] | undefined {
getHeads (size: number): { key: K; value: V; }[] | undefined {
const keyList = this.getKeyList();
if (keyList.length === 0) {
return undefined;
}
const result: { key: K; value: V }[] = [];
const result: { key: K; value: V; }[] = [];
const listSize = Math.min(size, keyList.length);
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i];
@@ -108,7 +108,7 @@ class MessageUniqueWrapper {
return shortId;
}
getMsgIdAndPeerByShortId (shortId: number): { MsgId: string; Peer: Peer } | undefined {
getMsgIdAndPeerByShortId (shortId: number): { MsgId: string; Peer: Peer; } | undefined {
const data = this.msgDataMap.getKey(shortId);
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|');
@@ -136,6 +136,12 @@ class MessageUniqueWrapper {
this.msgIdMap.resize(maxSize);
this.msgDataMap.resize(maxSize);
}
isShortId (message_id: string): boolean {
const num = Number(message_id);
// 判断是否是整数并且在 INT32 的范围内
return Number.isInteger(num) && num >= -2147483648 && num <= 2147483647;
}
}
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper();

View File

@@ -383,17 +383,14 @@ export async function testUrlHead (url: string, timeout: number = 5000): Promise
}, (res) => {
const statusCode = res.statusCode || 0;
const contentType = (res.headers['content-type'] as string) || '';
const contentLength = parseInt((res.headers['content-length'] as string) || '0', 10);
// 验证条件:
// 简化验证条件:
// 1. 状态码 2xx 或 3xx
// 2. Content-Type 不应该是 text/html表示错误页面
// 3. 对于 .zip 文件Content-Length 应该 > 1MB避免获取到错误页面
const isValidStatus = statusCode >= 200 && statusCode < 400;
const isNotHtmlError = !contentType.includes('text/html');
const isValidSize = url.endsWith('.zip') ? contentLength > 1024 * 1024 : true;
resolve(isValidStatus && isNotHtmlError && isValidSize);
resolve(isValidStatus && isNotHtmlError);
});
req.on('error', () => resolve(false));
@@ -437,10 +434,9 @@ export async function validateUrl (url: string, timeout: number = 5000): Promise
const contentType = (res.headers['content-type'] as string) || '';
const contentLength = parseInt((res.headers['content-length'] as string) || '0', 10);
// 验证条件
// 简化验证条件
const isValidStatus = statusCode >= 200 && statusCode < 400;
const isNotHtmlError = !contentType.includes('text/html');
const isValidSize = url.endsWith('.zip') ? contentLength > 1024 * 1024 : true;
if (!isValidStatus) {
resolve({
@@ -458,14 +454,6 @@ export async function validateUrl (url: string, timeout: number = 5000): Promise
contentLength,
error: '返回了 HTML 页面而非文件',
});
} else if (!isValidSize) {
resolve({
valid: false,
statusCode,
contentType,
contentLength,
error: `文件过小 (${contentLength} bytes),可能是错误页面`,
});
} else {
resolve({
valid: true,
@@ -542,21 +530,21 @@ export async function findAvailableDownloadUrl (
const testWithValidation = async (url: string): Promise<boolean> => {
if (validateContent) {
const result = await validateUrl(url, timeout);
// 额外检查文件大小
// 额外检查文件大小(仅当指定了 minFileSize 时)
if (result.valid && minFileSize && result.contentLength && result.contentLength < minFileSize) {
return false;
}
return result.valid;
}
return testMethod === 'head' ? testUrlHead(url, timeout) : testUrl(url, timeout);
// 不验证内容,只检查状态码
const isValid = testMethod === 'head' ? await testUrlHead(url, timeout) : await testUrl(url, timeout);
return isValid;
};
// 1. 如果设置了自定义镜像,优先使用
// 1. 如果设置了自定义镜像,直接使用(不测试,信任用户选择)
if (customMirror) {
const customUrl = buildMirrorUrl(originalUrl, customMirror);
if (await testWithValidation(customUrl)) {
return customUrl;
}
return customUrl;
}
// 2. 先测试原始 URL

View File

@@ -4,6 +4,7 @@ import {
FileListResponse,
FlashFileSetInfo,
SendStatus,
UploadSceneType,
} from '@/napcat-core/data/flash';
import { Peer } from '@/napcat-core/types';
@@ -19,25 +20,44 @@ export class NTQQFlashApi {
/**
* 发起闪传上传任务
* @param fileListToUpload 上传文件绝对路径的列表,可以是文件夹!!
* @param thumbnailPath
* @param filesetName
*/
async createFlashTransferUploadTask (fileListToUpload: string[]): Promise < GeneralCallResult & {
async createFlashTransferUploadTask (fileListToUpload: string[], thumbnailPath: string, filesetName: string): Promise<GeneralCallResult & {
createFlashTransferResult: createFlashTransferResult;
seq: number;
} > {
}> {
const flashService = this.context.session.getFlashTransferService();
const timestamp : number = Date.now();
const timestamp: number = Date.now();
const selfInfo = this.core.selfInfo;
const fileUploadArg = {
screen: 1, // 1
name: filesetName,
uploaders: [{
uin: selfInfo.uin,
uid: selfInfo.uid,
sendEntrance: '',
nickname: selfInfo.nick,
}],
coverPath: thumbnailPath,
paths: fileListToUpload,
excludePaths: [],
expireLeftTime: 0,
isNeedDelDeviceInfo: false,
isNeedDelLocation: false,
coverOriginalInfos: [
{
path: fileListToUpload[0] || '',
thumbnailPath,
},
],
uploadSceneType: UploadSceneType.KUPLOADSCENEAIOFILESELECTOR, // 不知道怎么枚举 先硬编码吧 (PC QQ 10)
detectPrivacyInfoResult: {
exists: false,
allDetectResults: new Map(),
},
};
const uploadResult = await flashService.createFlashTransferUploadTask(timestamp, fileUploadArg);
@@ -54,9 +74,9 @@ export class NTQQFlashApi {
* 下载闪传文件集
* @param fileSetId
*/
async downloadFileSetBySetId (fileSetId: string): Promise < GeneralCallResult & {
extraInfo: unknown
} > {
async downloadFileSetBySetId (fileSetId: string): Promise<GeneralCallResult & {
extraInfo: unknown;
}> {
const flashService = this.context.session.getFlashTransferService();
const result = await flashService.startFileSetDownload(fileSetId, 1, { isIncludeCompressInnerFiles: false }); // 为了方便,暂时硬编码
@@ -72,7 +92,7 @@ export class NTQQFlashApi {
* 获取闪传的外链分享
* @param fileSetId
*/
async getShareLinkBySetId (fileSetId: string): Promise < GeneralCallResult & {
async getShareLinkBySetId (fileSetId: string): Promise<GeneralCallResult & {
shareLink: string;
expireTimestamp: string;
}> {
@@ -91,9 +111,9 @@ export class NTQQFlashApi {
* 从分享外链获取文件集id
* @param shareCode
*/
async fromShareLinkFindSetId (shareCode: string): Promise < GeneralCallResult & {
async fromShareLinkFindSetId (shareCode: string): Promise<GeneralCallResult & {
fileSetId: string;
} > {
}> {
const flashService = this.context.session.getFlashTransferService();
const result = await flashService.getFileSetIdByCode(shareCode);
@@ -110,7 +130,7 @@ export class NTQQFlashApi {
* == 注意返回结构和其它的不同没有GeneralCallResult!!! ==
* @param fileSetId
*/
async getFileListBySetId (fileSetId: string): Promise < FileListResponse > {
async getFileListBySetId (fileSetId: string): Promise<FileListResponse> {
const flashService = this.context.session.getFlashTransferService();
const requestArg = {
@@ -153,11 +173,11 @@ export class NTQQFlashApi {
* 获取闪传文件集合信息
* @param fileSetId
*/
async getFileSetIndoBySetId (fileSetId: string): Promise < GeneralCallResult & {
async getFileSetIndoBySetId (fileSetId: string): Promise<GeneralCallResult & {
seq: number;
isCache: boolean;
fileSet: FlashFileSetInfo;
} > {
}> {
const flashService = this.context.session.getFlashTransferService();
const requestArg = {
@@ -178,13 +198,13 @@ export class NTQQFlashApi {
* @param fileSetId
* @param peer
*/
async sendFlashMessage (fileSetId: string, peer:Peer): Promise < {
async sendFlashMessage (fileSetId: string, peer: Peer): Promise<{
errCode: number,
errMsg: string,
rsp: {
sendStatus: SendStatus[]
}
} > {
sendStatus: SendStatus[];
};
}> {
const flashService = this.context.session.getFlashTransferService();
const target = {
@@ -212,9 +232,9 @@ export class NTQQFlashApi {
* @param fileSetId
* @param options
*/
async getFileTransUrl (fileSetId: string, options: { fileName?: string; fileIndex?: number }): Promise < GeneralCallResult & {
async getFileTransUrl (fileSetId: string, options: { fileName?: string; fileIndex?: number; }): Promise<GeneralCallResult & {
transferUrl: string;
} > {
}> {
const flashService = this.context.session.getFlashTransferService();
const result = await this.getFileListBySetId(fileSetId);
@@ -261,4 +281,27 @@ export class NTQQFlashApi {
};
}
}
async createFileThumbnail (filePath: string): Promise<any> {
const msgService = this.context.session.getMsgService();
const savePath = msgService.getFileThumbSavePathForSend(750, true);
const result = await this.core.util.createThumbnailImage(
'flashtransfer',
filePath,
savePath,
{
width: 520,
height: 520,
},
'jpeg',
null
);
if (result.result === 0) {
this.context.logger.log('获取缩略图成功!!');
result.targetPath = savePath;
return result;
}
return result;
}
}

View File

@@ -43,7 +43,7 @@ export class NTQQFriendApi {
return retMap;
}
async delBuudy (uid: string, tempBlock = false, tempBothDel = false) {
async delBuddy (uid: string, tempBlock = false, tempBothDel = false) {
return this.context.session.getBuddyService().delBuddy({
friendUid: uid,
tempBlock,

View File

@@ -32,9 +32,9 @@ export class NTQQMsgApi {
return this.context.session.getMsgService().getSourceOfReplyMsgV2(peer, clientSeq, time);
}
async getMsgEmojiLikesList (peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
async getMsgEmojiLikesList (peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string = '', count: number = 20) {
// 注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, cookie, false, count);
}
async setEmojiLike (peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {

View File

@@ -114,8 +114,9 @@ export class NTQQOnlineApi {
fileElement: {
fileName: actualFolderName,
filePath: folderPath,
fileSize: "",
},
} as any;
};
const msgService = this.context.session.getMsgService();
const startTime = Math.floor(Date.now() / 1000) - 2;
@@ -173,7 +174,7 @@ export class NTQQOnlineApi {
* 获取好友的在线文件消息
* @param peer
*/
async getOnlineFileMsg (peer: Peer) : Promise<any> {
async getOnlineFileMsg (peer: Peer): Promise<any> {
const msgService = this.context.session.getMsgService();
return await msgService.getOnlineFileMsgs(peer);
}
@@ -183,7 +184,7 @@ export class NTQQOnlineApi {
* @param peer
* @param msgId
*/
async cancelMyOnlineFileMsg (peer: Peer, msgId: string) : Promise<void> {
async cancelMyOnlineFileMsg (peer: Peer, msgId: string): Promise<void> {
const msgService = this.context.session.getMsgService();
await msgService.cancelSendMsg(peer, msgId);
}
@@ -194,7 +195,7 @@ export class NTQQOnlineApi {
* @param msgId
* @param elementId
*/
async refuseOnlineFileMsg (peer: Peer, msgId: string, elementId: string) : Promise<void> {
async refuseOnlineFileMsg (peer: Peer, msgId: string, elementId: string): Promise<void> {
const msgService = this.context.session.getMsgService();
const arrToSend = {
msgId,
@@ -215,7 +216,7 @@ export class NTQQOnlineApi {
* @param elementId
* @constructor
*/
async receiveOnlineFileOrFolder (peer: Peer, msgId: string, elementId: string) : Promise<any> {
async receiveOnlineFileOrFolder (peer: Peer, msgId: string, elementId: string): Promise<any> {
const msgService = this.context.session.getMsgService();
const arrToSend = {
msgId,
@@ -233,7 +234,7 @@ export class NTQQOnlineApi {
* @param peer
* @param msgId
*/
async switchFileToOffline (peer: Peer, msgId: string) : Promise<void> {
async switchFileToOffline (peer: Peer, msgId: string): Promise<void> {
const msgService = this.context.session.getMsgService();
await msgService.switchToOfflineSendMsg(peer, msgId);
}

View File

@@ -1,5 +1,5 @@
export interface FlashBaseRequest {
fileSetId: string
fileSetId: string;
}
export interface UploaderInfo {
@@ -19,14 +19,14 @@ export interface thumbnailInfo {
}
export interface SendTarget {
destType: number // 1私聊
destType: number; // 1私聊
destUin?: string,
destUid: string,
}
export interface SendTargetRequests {
fileSetId: string
targets: SendTarget[]
fileSetId: string;
targets: SendTarget[];
}
export interface DownloadStatusInfo {
@@ -53,13 +53,13 @@ export interface DownloadStatusInfo {
isAllFileAlreadyDownloaded: boolean,
saveFileSetDir: string,
allWaitingStatusTask: boolean,
downloadSceneType: number,
downloadSceneType: DownloadSceneType,
retryCount: number,
statisticInfo: {
downloadTaskId: string,
downloadFilesetName: string,
downloadFileTypeDistribution: string,
downloadFileSizeDistribution: string
downloadFileSizeDistribution: string;
},
albumStorageFailImageNum: number,
albumStorageFailVideoNum: number,
@@ -67,8 +67,8 @@ export interface DownloadStatusInfo {
albumStorageSucImageNum: number,
albumStorageSucVideoNum: number,
albumStorageSucFileIdList: [],
albumStorageFileNum: number
}
albumStorageFileNum: number;
};
}
export interface physicalInfo {
@@ -95,94 +95,94 @@ export interface uploadInfo {
svrRrrCode: number,
errMsg: string,
isNeedDelDeviceInfo: boolean,
thumbnailUploadState: number
thumbnailUploadState: number;
isSecondHit: boolean,
hasModifiedErr: boolean,
}
export interface folderUploadInfo {
totalUploadedFileSize: string
successCount: number
failedCount: number
totalUploadedFileSize: string;
successCount: number;
failedCount: number;
}
export interface folderDownloadInfo {
totalDownloadedFileSize: string
totalFileSize: string
totalDownloadFileCount: number
successCount: number
failedCount: number
pausedCount: number
cancelCount: number
downloadingCount: number
partialDownloadCount: number
curLevelDownloadedFileCount: number
curLevelUnDownloadedFileCount: number
totalDownloadedFileSize: string;
totalFileSize: string;
totalDownloadFileCount: number;
successCount: number;
failedCount: number;
pausedCount: number;
cancelCount: number;
downloadingCount: number;
partialDownloadCount: number;
curLevelDownloadedFileCount: number;
curLevelUnDownloadedFileCount: number;
}
export interface compressFileFolderInfo {
downloadStatus: number
saveFileDirPath: string
totalFileCount: string
totalFileSize: string
downloadStatus: number;
saveFileDirPath: string;
totalFileCount: string;
totalFileSize: string;
}
export interface albumStorgeInfo {
status: number
localIdentifier: string
errorCode: number
timeCost: number
status: number;
localIdentifier: string;
errorCode: number;
timeCost: number;
}
export interface FlashOneFileInfo {
fileSetId: string
cliFileId: string // client?? 或许可以换取url
compressedFileFolderId: string
archiveIndex: 0
indexPath: string
isDir: boolean // 文件或者文件夹!!
parentId: string
depth: number // 1
cliFileIndex: number
fileType: number // 枚举!! 已完成枚举!!
name: string
namePinyin: string
isCover: boolean
isCoverOriginal: boolean
fileSize: string
fileCount: number
thumbnail: thumbnailInfo
physical: physicalInfo
srvFileId: string // service?? 服务器上面的id吗
srvParentFileId: string
svrLastUpdateTimestamp: string
downloadInfo: downloadInfo
saveFilePath: string
search_relative_path: string
disk_relative_path: string
uploadInfo: uploadInfo
status: number
uploadStatus: number // 3已上传成功
downloadStatus: number // 0未下载
folderUploadInfo: folderUploadInfo
folderDownloadInfo: folderDownloadInfo
sha1: string
bookmark: string
compressFileFolderInfo: compressFileFolderInfo
uploadPauseReason: string
downloadPauseReason: string
filePhysicalSize: string
thumbnail_sha1: string | null
thumbnail_size: string | null
needAlbumStorage: boolean
albumStorageInfo: albumStorgeInfo
fileSetId: string;
cliFileId: string; // client?? 或许可以换取url
compressedFileFolderId: string;
archiveIndex: 0;
indexPath: string;
isDir: boolean; // 文件或者文件夹!!
parentId: string;
depth: number; // 1
cliFileIndex: number;
fileType: number; // 枚举!! 已完成枚举!!
name: string;
namePinyin: string;
isCover: boolean;
isCoverOriginal: boolean;
fileSize: string;
fileCount: number;
thumbnail: thumbnailInfo;
physical: physicalInfo;
srvFileId: string; // service?? 服务器上面的id吗
srvParentFileId: string;
svrLastUpdateTimestamp: string;
downloadInfo: downloadInfo;
saveFilePath: string;
search_relative_path: string;
disk_relative_path: string;
uploadInfo: uploadInfo;
status: number;
uploadStatus: number; // 3已上传成功
downloadStatus: number; // 0未下载
folderUploadInfo: folderUploadInfo;
folderDownloadInfo: folderDownloadInfo;
sha1: string;
bookmark: string;
compressFileFolderInfo: compressFileFolderInfo;
uploadPauseReason: string;
downloadPauseReason: string;
filePhysicalSize: string;
thumbnail_sha1: string | null;
thumbnail_size: string | null;
needAlbumStorage: boolean;
albumStorageInfo: albumStorgeInfo;
}
export interface fileListsInfo {
parentId: string,
depth: number, // 1
fileList: FlashOneFileInfo[],
paginationInfo: {}
paginationInfo: {};
isEnd: boolean,
isCache: boolean,
}
@@ -200,30 +200,50 @@ export interface createFlashTransferResult {
expireTime: string,
expireLeftTime: string,
}
export enum UploadSceneType {
KUPLOADSCENEUNKNOWN,
KUPLOADSCENEFLOATWINDOWRIGHTCLICKMENU,
KUPLOADSCENEFLOATWINDOWDRAG,
KUPLOADSCENEFLOATWINDOWFILESELECTOR,
KUPLOADSCENEFLOATWINDOWSHORTCUTKEYCTRLCV,
KUPLOADSCENEH5LAUNCHCLIENTRIGHTCLICKMENU,
KUPLOADSCENEH5LAUNCHCLIENTDRAG,
KUPLOADSCENEH5LAUNCHCLIENTFILESELECTOR,
KUPLOADSCENEH5LAUNCHCLIENTSHORTCUTKEYCTRLCV,
KUPLOADSCENEAIODRAG,
KUPLOADSCENEAIOFILESELECTOR,
KUPLOADSCENEAIOSHORTCUTKEYCTRLCV
}
export interface StartFlashTaskRequests {
screen?: number; // 1 PC-QQ
screen: number; // 1 PC-QQ
name?: string;
uploaders: UploaderInfo[];
permission?: {};
coverPath?: string;
paths: string[]; // 文件的绝对路径,可以是文件夹
// excludePaths: [];
// expireLeftTime: 0,
// isNeedDelDeviceInfo: boolean,
// isNeedDelLocation: boolean,
// coverOriginalInfos: [],
// uploadSceneType: 10, // 不知道怎么枚举 先硬编码吧
// detectPrivacyInfoResult: {
// exists: boolean,
// allDetectResults: {}
// }
excludePaths?: string[];
expireLeftTime?: number, // 0
isNeedDelDeviceInfo: boolean,
isNeedDelLocation: boolean,
coverOriginalInfos?: {
path: string,
thumbnailPath: string,
}[],
uploadSceneType: UploadSceneType, // 不知道怎么枚举 先硬编码吧 (PC QQ 10)
detectPrivacyInfoResult: {
exists: boolean,
allDetectResults: {};
};
}
export enum BusiScene {
KBUSISCENEINVALID,
KBUSISCENEFLASHSCENE
}
export interface FileListInfoRequests {
seq: number, // 0
fileSetId: string,
isUseCache: boolean,
sceneType: number, // 1
sceneType: BusiScene, // 1
reqInfos: {
count: number, // 18 ?? 硬编码吧 不懂
paginationInfo: {},
@@ -238,10 +258,24 @@ export interface FileListInfoRequests {
sortField: number,
sortOrder: number,
}[],
isNeedPhysicalInfoReady: boolean
}[]
isNeedPhysicalInfoReady: boolean;
}[];
}
export enum DownloadSceneType {
KDOWNLOADSCENEUNKNOWN,
KDOWNLOADSCENEARKC2C,
KDOWNLOADSCENEARKC2CDETAILPAGE,
KDOWNLOADSCENEARKGROUP,
KDOWNLOADSCENEARKGROUPDETAILPAGE,
KDOWNLOADSCENELINKC2C,
KDOWNLOADSCENELINKGROUP,
KDOWNLOADSCENELINKCHANNEL,
KDOWNLOADSCENELINKTEMPCHAT,
KDOWNLOADSCENELINKOTHERINQQ,
KDOWNLOADSCENESCANQRCODE,
KDWONLOADSCENEFLASHTRANSFERCENTERCLIENT,
KDWONLOADSCENEFLASHTRANSFERCENTERSCHEMA
}
export interface FlashFileSetInfo {
fileSetId: string,
name: string,
@@ -258,23 +292,23 @@ export interface FlashFileSetInfo {
urls: [
{
spec: number, // 2
url: string
url: string;
}
],
localCachePath: string
localCachePath: string;
},
uploaders: [
{
uin: string,
nickname: string,
uid: string,
sendEntrance: string
sendEntrance: string;
}
],
expireLeftTime: number,
aiClusteringStatus: {
firstClusteringList: [],
shouldPull: boolean
shouldPull: boolean;
},
createTime: number,
expireTime: number,
@@ -284,7 +318,7 @@ export interface FlashFileSetInfo {
uploadInfo: {
totalUploadedFileSize: number,
successCount: number,
failedCount: number
failedCount: number;
},
downloadInfo: {
totalDownloadedFileSize: 0,
@@ -296,7 +330,7 @@ export interface FlashFileSetInfo {
cancelCount: 0,
status: 0,
curLevelDownloadedFileCount: number,
curLevelUnDownloadedFileCount: 0
curLevelUnDownloadedFileCount: 0;
},
transferType: number,
isLocalCreate: true,
@@ -306,12 +340,12 @@ export interface FlashFileSetInfo {
downloadStatus: 0,
downloadPauseReason: 0,
saveFileSetDir: string,
uploadSceneType: 10,
downloadSceneType: 0, // 0 PC-QQ 103 web
uploadSceneType: UploadSceneType,
downloadSceneType: DownloadSceneType, // 0 PC-QQ 103 web
retryCount: number,
isMergeShareUpload: 0,
isRemoveDeviceInfo: boolean,
isRemoveLocation: boolean
isRemoveLocation: boolean;
}
export interface SendStatus {
@@ -320,5 +354,5 @@ export interface SendStatus {
target: {
destType: number,
destUid: string,
}
};
}

View File

@@ -518,5 +518,13 @@
"9.9.26-44725": {
"appid": 537337569,
"qua": "V1_WIN_NQ_9.9.26_44725_GW_B"
},
"9.9.27-45627": {
"appid": 537340060,
"qua": "V1_WIN_NQ_9.9.26_45627_GW_B"
},
"6.9.88-44725": {
"appid": 537337594,
"qua": "V1_MAC_NQ_6.9.88_44725_GW_B"
}
}

View File

@@ -154,5 +154,17 @@
"9.9.26-44725-x64": {
"send": "0A18D0C",
"recv": "1D4BF0D"
},
"9.9.27-45627-x64": {
"send": "0A697CC",
"recv": "1E86AC1"
},
"6.9.88-44725-x64": {
"send": "2756EF6",
"recv": "0A36152"
},
"6.9.88-44725-arm64": {
"send": "2313C68",
"recv": "09693E4"
}
}

View File

@@ -662,5 +662,17 @@
"9.9.26-44725-x64": {
"send": "2CEBB20",
"recv": "2CEF0A0"
},
"9.9.27-45627-x64": {
"send": "2E59CC0",
"recv": "2E5D240"
},
"6.9.88-44725-x64": {
"send": "451FE90",
"recv": "4522A40"
},
"6.9.88-44725-arm64": {
"send": "3D79168",
"recv": "3D7BA78"
}
}

View File

@@ -2,14 +2,14 @@ import * as crypto from 'node:crypto';
import { PacketMsg } from '@/napcat-core/packet/message/message';
interface ForwardMsgJson {
app: string
app: string;
config: ForwardMsgJsonConfig,
desc: string,
extra: ForwardMsgJsonExtra,
meta: ForwardMsgJsonMeta,
prompt: string,
ver: string,
view: string
view: string;
}
interface ForwardMsgJsonConfig {
@@ -17,7 +17,7 @@ interface ForwardMsgJsonConfig {
forward: number,
round: number,
type: string,
width: number
width: number;
}
interface ForwardMsgJsonExtra {
@@ -26,17 +26,17 @@ interface ForwardMsgJsonExtra {
}
interface ForwardMsgJsonMeta {
detail: ForwardMsgJsonMetaDetail
detail: ForwardMsgJsonMetaDetail;
}
interface ForwardMsgJsonMetaDetail {
news: {
text: string
text: string;
}[],
resid: string,
source: string,
summary: string,
uniseq: string
uniseq: string;
}
interface ForwardAdaptMsg {
@@ -50,8 +50,8 @@ interface ForwardAdaptMsgElement {
}
export class ForwardMsgBuilder {
private static build (resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
const id = crypto.randomUUID();
private static build (resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string, uuid?: string): ForwardMsgJson {
const id = uuid ?? crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg);
if (!source) {
source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录');
@@ -104,13 +104,19 @@ export class ForwardMsgBuilder {
return this.build(resId, []);
}
static fromPacketMsg (resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
static fromPacketMsg (resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string, uuid?: string): ForwardMsgJson {
return this.build(resId, packetMsg.map(msg => ({
senderName: msg.senderName,
isGroupMsg: msg.groupId !== undefined,
msg: msg.msg.map(m => ({
preview: m.valid ? m.toPreview() : '[该消息类型暂不支持查看]',
})),
})), source, news, summary, prompt);
})),
source,
news,
summary,
prompt,
uuid,
);
}
}

View File

@@ -294,8 +294,7 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
}
if (element.markdownElement) {
// console.log(element.markdownElement);
if (element.markdownElement.mdSummary !== undefined && element.markdownElement.mdExtInfo !== undefined && element.markdownElement.mdExtInfo.flashTransferInfo) {
if (element.markdownElement?.mdSummary) {
return element.markdownElement.mdSummary;
} else {
return '[Markdown 消息]';

View File

@@ -0,0 +1,140 @@
import { NodeIQQNTWrapperSession } from '@/napcat-core/wrapper';
import { ServiceNamingMapping } from '@/napcat-core/services/index';
import { NTEventWrapper } from './event';
/**
* 创建 Service 方法的代理
* 拦截所有方法调用,通过 EventWrapper 进行调用
*/
function createServiceMethodProxy<S extends keyof ServiceNamingMapping>(
serviceName: S,
originalService: ServiceNamingMapping[S],
eventWrapper: NTEventWrapper
): ServiceNamingMapping[S] {
return new Proxy(originalService as object, {
get(target, prop, receiver) {
const originalValue = Reflect.get(target, prop, receiver);
// 如果不是函数,直接返回原始值
if (typeof originalValue !== 'function') {
return originalValue;
}
const methodName = prop as string;
// 返回一个包装函数,通过 EventWrapper 调用
return function (this: unknown, ...args: unknown[]) {
// 构造 EventWrapper 需要的路径格式: ServiceName/MethodName
const eventPath = `${serviceName}/${methodName}`;
// 尝试通过 EventWrapper 调用
try {
// 使用 callNoListenerEvent 的底层实现逻辑
const eventFunc = (eventWrapper as any).createEventFunction(eventPath);
if (eventFunc) {
return eventFunc(...args);
}
} catch {
// 如果 EventWrapper 调用失败,回退到原始调用
}
// 回退到原始方法调用
return originalValue.apply(originalService, args);
};
},
}) as ServiceNamingMapping[S];
}
/**
* 创建 Session 的双层代理
* 第一层:拦截 getXXXService 方法
* 第二层:拦截 Service 上的具体方法调用
*/
export function createSessionProxy(
session: NodeIQQNTWrapperSession,
eventWrapper: NTEventWrapper
): NodeIQQNTWrapperSession {
// 缓存已代理的 Service避免重复创建
const serviceProxyCache = new Map<string, unknown>();
return new Proxy(session, {
get(target, prop, receiver) {
const propName = prop as string;
// 检查是否是 getXXXService 方法
if (typeof propName === 'string' && propName.startsWith('get') && propName.endsWith('Service')) {
// 提取 Service 名称: getMsgService -> NodeIKernelMsgService
const servicePart = propName.slice(3); // 移除 'get' 前缀
const serviceName = `NodeIKernel${servicePart}` as keyof ServiceNamingMapping;
// 返回一个函数,该函数返回代理后的 Service
return function () {
// 检查缓存
if (serviceProxyCache.has(serviceName)) {
return serviceProxyCache.get(serviceName);
}
// 获取原始 Service
const originalGetter = Reflect.get(target, prop, receiver) as () => unknown;
const originalService = originalGetter.call(target);
// 检查是否在 ServiceNamingMapping 中定义
if (isKnownService(serviceName)) {
// 创建 Service 方法代理
const proxiedService = createServiceMethodProxy(
serviceName,
originalService as ServiceNamingMapping[typeof serviceName],
eventWrapper
);
serviceProxyCache.set(serviceName, proxiedService);
return proxiedService;
}
// 未知的 Service直接返回原始对象
serviceProxyCache.set(serviceName, originalService);
return originalService;
};
}
// 非 getXXXService 方法,直接返回原始值
return Reflect.get(target, prop, receiver);
},
});
}
/**
* 检查 Service 名称是否在已知的映射中
*/
function isKnownService(serviceName: string): serviceName is keyof ServiceNamingMapping {
const knownServices: string[] = [
'NodeIKernelAvatarService',
'NodeIKernelBuddyService',
'NodeIKernelFileAssistantService',
'NodeIKernelGroupService',
'NodeIKernelLoginService',
'NodeIKernelMsgService',
'NodeIKernelOnlineStatusService',
'NodeIKernelProfileLikeService',
'NodeIKernelProfileService',
'NodeIKernelTicketService',
'NodeIKernelStorageCleanService',
'NodeIKernelRobotService',
'NodeIKernelRichMediaService',
'NodeIKernelDbToolsService',
'NodeIKernelTipOffService',
'NodeIKernelSearchService',
'NodeIKernelCollectionService',
];
return knownServices.includes(serviceName);
}
/**
* 创建带有 EventWrapper 集成的 InstanceContext
* 这是推荐的使用方式,在创建 context 时自动代理 session
*/
export function createProxiedSession(
session: NodeIQQNTWrapperSession,
eventWrapper: NTEventWrapper
): NodeIQQNTWrapperSession {
return createSessionProxy(session, eventWrapper);
}

View File

@@ -25,6 +25,7 @@ import path from 'node:path';
import fs from 'node:fs';
import { hostname, systemName, systemVersion } from 'napcat-common/src/system';
import { NTEventWrapper } from '@/napcat-core/helper/event';
import { createSessionProxy } from '@/napcat-core/helper/session-proxy';
import { KickedOffLineInfo, RawMessage, SelfInfo, SelfStatusInfo } from '@/napcat-core/types';
import { NapCatConfigLoader, NapcatConfigSchema } from '@/napcat-core/helper/config';
import os from 'node:os';
@@ -39,6 +40,14 @@ export * from './wrapper';
export * from './types/index';
export * from './services/index';
export * from './listeners/index';
export * from './apis/index';
export * from './helper/log';
export * from './helper/qq-basic-info';
export * from './helper/event';
export * from './helper/config';
export * from './helper/config-base';
export * from './helper/proxy-handler';
export * from './helper/session-proxy';
export enum NapCatCoreWorkingEnv {
Unknown = 0,
@@ -74,6 +83,7 @@ export function loadQQWrapper (execPath: string | undefined, QQVersion: string):
}
const nativemodule: { exports: WrapperNodeApi; } = { exports: {} as WrapperNodeApi };
process.dlopen(nativemodule, wrapperNodePath);
process.env['NAPCAT_WRAPPER_PATH'] = wrapperNodePath;
return nativemodule.exports;
}
export function getMajorPath (execPath: string, QQVersion: string): string {
@@ -111,9 +121,19 @@ export class NapCatCore {
// 通过构造器递过去的 runtime info 应该尽量少
constructor (context: InstanceContext, selfInfo: SelfInfo) {
this.selfInfo = selfInfo;
this.context = context;
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
// 先用原始 session 创建 eventWrapper
this.eventWrapper = new NTEventWrapper(context.session);
// 通过环境变量 NAPCAT_SESSION_PROXY 开启 session 代理
if (process.env['NAPCAT_SESSION_PROXY'] === '1') {
const proxiedSession = createSessionProxy(context.session, this.eventWrapper);
this.context = {
...context,
session: proxiedSession,
};
} else {
this.context = context;
}
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath, NapcatConfigSchema);
this.apis = {
FileApi: new NTQQFileApi(this.context, this),

View File

@@ -18,6 +18,7 @@ import { OidbPacket } from '@/napcat-core/packet/transformer/base';
import { ImageOcrResult } from '@/napcat-core/packet/entities/ocrResult';
import { gunzipSync } from 'zlib';
import { PacketMsgConverter } from '@/napcat-core/packet/message/converter';
import { UploadForwardMsgParams } from '@/napcat-core/packet/transformer/message/UploadForwardMsgV2';
export class PacketOperationContext {
private readonly context: PacketContext;
@@ -26,7 +27,7 @@ export class PacketOperationContext {
this.context = context;
}
async sendPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
async sendPacket<T extends boolean = false> (pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
return await this.context.client.sendOidbPacket(pkt, rsp);
}
@@ -94,12 +95,15 @@ export class PacketOperationContext {
.filter(Boolean)
);
const res = await Promise.allSettled(reqList);
this.context.logger.info(`上传资源${res.length}个,失败${res.filter((r) => r.status === 'rejected').length}`);
res.forEach((result, index) => {
if (result.status === 'rejected') {
this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`);
}
});
const failedCount = res.filter((r) => r.status === 'rejected').length;
if (failedCount > 0) {
this.context.logger.warn(`上传资源${res.length}个,失败${failedCount}`);
res.forEach((result, index) => {
if (result.status === 'rejected') {
this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`);
}
});
}
}
async UploadImage (img: PacketMsgPicElement) {
@@ -224,7 +228,15 @@ export class PacketOperationContext {
const res = trans.UploadForwardMsg.parse(resp);
return res.result.resId;
}
async UploadForwardMsgV2 (msg: UploadForwardMsgParams[], groupUin: number = 0) {
//await this.SendPreprocess(msg, groupUin);
// 遍历上传资源
await Promise.allSettled(msg.map(async (item) => { return await this.SendPreprocess(item.actionMsg, groupUin); }));
const req = trans.UploadForwardMsgV2.build(this.context.napcore.basicInfo.uid, msg, groupUin);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.UploadForwardMsg.parse(resp);
return res.result.resId;
}
async MoveGroupFile (
groupUin: number,
fileUUID: string,

View File

@@ -0,0 +1,51 @@
import zlib from 'node:zlib';
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketBufBuilder, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import { PacketMsg } from '@/napcat-core/packet/message/message';
export interface UploadForwardMsgParams {
actionCommand: string;
actionMsg: PacketMsg[];
}
class UploadForwardMsgV2 extends PacketTransformer<typeof proto.SendLongMsgResp> {
build (selfUid: string, msg: UploadForwardMsgParams[], groupUin: number = 0): OidbPacket {
const reqdata = msg.map((item) => ({
actionCommand: item.actionCommand,
actionData: {
msgBody: this.msgBuilder.buildFakeMsg(selfUid, item.actionMsg),
}
}));
const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode(
{
action: reqdata,
}
);
const payload = zlib.gzipSync(Buffer.from(longMsgResultData));
const req = new NapProtoMsg(proto.SendLongMsgReq).encode(
{
info: {
type: groupUin === 0 ? 1 : 3,
uid: {
uid: groupUin === 0 ? selfUid : groupUin.toString(),
},
groupUin,
payload,
},
settings: {
field1: 4, field2: 1, field3: 7, field4: 0,
},
}
);
return {
cmd: 'trpc.group.long_msg_interface.MsgService.SsoSendLongMsg',
data: PacketBufBuilder(req),
};
}
parse (data: Buffer) {
return new NapProtoMsg(proto.SendLongMsgResp).decode(data);
}
}
export default new UploadForwardMsgV2();

View File

@@ -2,3 +2,4 @@ export { default as UploadForwardMsg } from './UploadForwardMsg';
export { default as FetchGroupMessage } from './FetchGroupMessage';
export { default as FetchC2CMessage } from './FetchC2CMessage';
export { default as DownloadForwardMsg } from './DownloadForwardMsg';
export { default as UploadForwardMsgV2 } from './UploadForwardMsgV2';

View File

@@ -10,6 +10,7 @@ import {
DownloadStatusInfo,
SendTargetRequests,
FlashOneFileInfo,
DownloadSceneType,
} from '../data/flash';
export interface NodeIKernelFlashTransferService {
@@ -18,48 +19,48 @@ export interface NodeIKernelFlashTransferService {
* @param timestamp
* @param fileInfo
*/
createFlashTransferUploadTask(timestamp: number, fileInfo: StartFlashTaskRequests): Promise < GeneralCallResult & {
createFlashTransferUploadTask (timestamp: number, fileInfo: StartFlashTaskRequests): Promise<GeneralCallResult & {
createFlashTransferResult: createFlashTransferResult;
seq: number;
} >; // 2 arg 重点 // 自动上传
}>; // 2 arg 重点 // 自动上传
createMergeShareTask(...args: unknown[]): unknown; // 2 arg
createMergeShareTask (...args: unknown[]): unknown; // 2 arg
updateFlashTransfer(...args: unknown[]): unknown; // 2 arg
updateFlashTransfer (...args: unknown[]): unknown; // 2 arg
getFileSetList(...args: unknown[]): unknown; // 1 arg
getFileSetList (...args: unknown[]): unknown; // 1 arg
getFileSetListCount(...args: unknown[]): unknown; // 1 arg
getFileSetListCount (...args: unknown[]): unknown; // 1 arg
/**
* 获取file set 的信息
* @param fileSetIdDict
*/
getFileSet(fileSetIdDict: FlashBaseRequest): Promise < GeneralCallResult & {
getFileSet (fileSetIdDict: FlashBaseRequest): Promise<GeneralCallResult & {
seq: number;
isCache: boolean;
fileSet: FlashFileSetInfo;
} >; // 1 arg
}>; // 1 arg
/**
* 获取file set 里面的文件信息(文件夹结构)
* @param requestArgs
*/
getFileList(requestArgs: FileListInfoRequests): Promise < {
getFileList (requestArgs: FileListInfoRequests): Promise<{
rsp: FileListResponse;
} > ; // 1 arg 这个方法QQ有bug 并没有,是我参数有问题
}>; // 1 arg 这个方法QQ有bug 并没有,是我参数有问题
getDownloadedFileCount(...args: unknown[]): unknown; // 1 arg
getDownloadedFileCount (...args: unknown[]): unknown; // 1 arg
getLocalFileList(...args: unknown[]): unknown; // 3 arg
getLocalFileList (...args: unknown[]): unknown; // 3 arg
batchRemoveUserFileSetHistory(...args: unknown[]): unknown; // 1 arg
batchRemoveUserFileSetHistory (...args: unknown[]): unknown; // 1 arg
/**
* 获取分享链接
* @param fileSetId
*/
getShareLinkReq(fileSetId:string): Promise< GeneralCallResult & {
getShareLinkReq (fileSetId: string): Promise<GeneralCallResult & {
shareLink: string;
expireTimestamp: string;
}>;
@@ -68,235 +69,235 @@ export interface NodeIKernelFlashTransferService {
* 由分享链接到fileSetId
* @param shareCode
*/
getFileSetIdByCode(shareCode: string): Promise < GeneralCallResult & {
getFileSetIdByCode (shareCode: string): Promise<GeneralCallResult & {
fileSetId: string;
} > ; // 1 arg code == share code
}>; // 1 arg code == share code
batchRemoveFile(...args: unknown[]): unknown; // 1 arg
batchRemoveFile (...args: unknown[]): unknown; // 1 arg
checkUploadPathValid(...args: unknown[]): unknown; // 1 arg
checkUploadPathValid (...args: unknown[]): unknown; // 1 arg
cleanFailedFiles(...args: unknown[]): unknown; // 2 arg
cleanFailedFiles (...args: unknown[]): unknown; // 2 arg
/**
* 暂停所有的任务
*/
resumeAllUnfinishedTasks(): unknown; // 0 arg !!
resumeAllUnfinishedTasks (): unknown; // 0 arg !!
addFileSetUploadListener(...args: unknown[]): unknown; // 1 arg
addFileSetUploadListener (...args: unknown[]): unknown; // 1 arg
removeFileSetUploadListener(...args: unknown[]): unknown; // 1 arg
removeFileSetUploadListener (...args: unknown[]): unknown; // 1 arg
/**
* 开始上传任务 适用于已暂停的
* @param fileSetId
*/
startFileSetUpload(fileSetId: string): void; // 1 arg 并不是新建任务,应该是暂停后的启动
startFileSetUpload (fileSetId: string): void; // 1 arg 并不是新建任务,应该是暂停后的启动
/**
* 结束,无法再次启动
* @param fileSetId
*/
stopFileSetUpload(fileSetId: string): void; // 1 arg stop 后start无效
stopFileSetUpload (fileSetId: string): void; // 1 arg stop 后start无效
/**
* 暂停上传
* @param fileSetId
*/
pauseFileSetUpload(fileSetId: string): void; // 1 arg 暂停上传
pauseFileSetUpload (fileSetId: string): void; // 1 arg 暂停上传
/**
* 继续上传
* @param args
*/
resumeFileSetUpload(...args: unknown[]): unknown; // 1 arg 继续
resumeFileSetUpload (...args: unknown[]): unknown; // 1 arg 继续
pauseFileUpload(...args: unknown[]): unknown; // 1 arg
pauseFileUpload (...args: unknown[]): unknown; // 1 arg
resumeFileUpload(...args: unknown[]): unknown; // 1 arg
resumeFileUpload (...args: unknown[]): unknown; // 1 arg
stopFileUpload(...args: unknown[]): unknown; // 1 arg
stopFileUpload (...args: unknown[]): unknown; // 1 arg
asyncGetThumbnailPath(...args: unknown[]): unknown; // 2 arg
asyncGetThumbnailPath (...args: unknown[]): unknown; // 2 arg
setDownLoadDefaultFileDir(...args: unknown[]): unknown; // 1 arg
setDownLoadDefaultFileDir (...args: unknown[]): unknown; // 1 arg
setFileSetDownloadDir(...args: unknown[]): unknown; // 2 arg
setFileSetDownloadDir (...args: unknown[]): unknown; // 2 arg
getFileSetDownloadDir(...args: unknown[]): unknown; // 1 arg
getFileSetDownloadDir (...args: unknown[]): unknown; // 1 arg
setFlashTransferDir(...args: unknown[]): unknown; // 2 arg
setFlashTransferDir (...args: unknown[]): unknown; // 2 arg
addFileSetDownloadListener(...args: unknown[]): unknown; // 1 arg
addFileSetDownloadListener (...args: unknown[]): unknown; // 1 arg
removeFileSetDownloadListener(...args: unknown[]): unknown; // 1 arg
removeFileSetDownloadListener (...args: unknown[]): unknown; // 1 arg
/**
* 开始下载file set的函数 同开始上传
* @param fileSetId
* @param chatType 聊天类型 //因为没有peer其实可以硬编码为1 (好友私聊)
* @param downloadSceneType 下载类型 //因为没有peer其实可以硬编码为1 (好友私聊)
* @param arg // 默认为false
*/
startFileSetDownload(fileSetId:string, chatType: number, arg: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & {
extraInfo: 0
} >; // 3 arg
startFileSetDownload (fileSetId: string, downloadSceneType: DownloadSceneType, downloadOptionParams: { isIncludeCompressInnerFiles: boolean; }): Promise<GeneralCallResult & {
extraInfo: 0;
}>; // 3 arg
stopFileSetDownload(fileSetId: string, arg1: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & {
extraInfo: 0
} > ; // 2 arg 结束不可重启!!
stopFileSetDownload (fileSetId: string, downloadOptionParams: { isIncludeCompressInnerFiles: boolean; }): Promise<GeneralCallResult & {
extraInfo: 0;
}>; // 2 arg 结束不可重启!!
pauseFileSetDownload(fileSetId: string, arg1: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & {
extraInfo: 0
} > ; // 2 arg
pauseFileSetDownload (fileSetId: string, downloadOptionParams: { isIncludeCompressInnerFiles: boolean; }): Promise<GeneralCallResult & {
extraInfo: 0;
}>; // 2 arg
resumeFileSetDownload(fileSetId: string, arg1: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & {
extraInfo: 0
} > ; // 2 arg
resumeFileSetDownload (fileSetId: string, downloadOptionParams: { isIncludeCompressInnerFiles: boolean; }): Promise<GeneralCallResult & {
extraInfo: 0;
}>; // 2 arg
startFileListDownLoad(...args: unknown[]): unknown; // 4 arg // 大概率是选择set里面的部分文件进行下载没必要不想写
startFileListDownLoad (...args: unknown[]): unknown; // 4 arg // 大概率是选择set里面的部分文件进行下载没必要不想写
pauseFileListDownLoad(...args: unknown[]): unknown; // 2 arg
pauseFileListDownLoad (...args: unknown[]): unknown; // 2 arg
resumeFileListDownLoad(...args: unknown[]): unknown; // 2 arg
resumeFileListDownLoad (...args: unknown[]): unknown; // 2 arg
stopFileListDownLoad(...args: unknown[]): unknown; // 2 arg
stopFileListDownLoad (...args: unknown[]): unknown; // 2 arg
startThumbnailListDownload(fileSetId: string): Promise < GeneralCallResult >; // 1 arg // 缩略图下载
startThumbnailListDownload (fileSetId: string): Promise<GeneralCallResult>; // 1 arg // 缩略图下载
stopThumbnailListDownload(fileSetId: string): Promise < GeneralCallResult >; // 1 arg
stopThumbnailListDownload (fileSetId: string): Promise<GeneralCallResult>; // 1 arg
asyncRequestDownLoadStatus(fileSetId: string): Promise < DownloadStatusInfo >; // 1 arg
asyncRequestDownLoadStatus (fileSetId: string): Promise<DownloadStatusInfo>; // 1 arg
startFileTransferUrl(fileInfo: FlashOneFileInfo): Promise < {
startFileTransferUrl (fileInfo: FlashOneFileInfo): Promise<{
ret: number,
url: string,
expireTimestampSeconds: string
} >; // 1 arg
expireTimestampSeconds: string;
}>; // 1 arg
startFileListDownLoadBySessionId(...args: unknown[]): unknown; // 2 arg
startFileListDownLoadBySessionId (...args: unknown[]): unknown; // 2 arg
addFileSetSimpleStatusListener(...args: unknown[]): unknown; // 2 arg
addFileSetSimpleStatusListener (...args: unknown[]): unknown; // 2 arg
addFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 2 arg
addFileSetSimpleStatusMonitoring (...args: unknown[]): unknown; // 2 arg
removeFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 2 arg
removeFileSetSimpleStatusMonitoring (...args: unknown[]): unknown; // 2 arg
removeFileSetSimpleStatusListener(...args: unknown[]): unknown; // 1 arg
removeFileSetSimpleStatusListener (...args: unknown[]): unknown; // 1 arg
addDesktopFileSetSimpleStatusListener(...args: unknown[]): unknown; // 1 arg
addDesktopFileSetSimpleStatusListener (...args: unknown[]): unknown; // 1 arg
addDesktopFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 1 arg
addDesktopFileSetSimpleStatusMonitoring (...args: unknown[]): unknown; // 1 arg
removeDesktopFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 1 arg
removeDesktopFileSetSimpleStatusMonitoring (...args: unknown[]): unknown; // 1 arg
removeDesktopFileSetSimpleStatusListener(...args: unknown[]): unknown; // 1 arg
removeDesktopFileSetSimpleStatusListener (...args: unknown[]): unknown; // 1 arg
addFileSetSimpleUploadInfoListener(...args: unknown[]): unknown; // 1 arg
addFileSetSimpleUploadInfoListener (...args: unknown[]): unknown; // 1 arg
addFileSetSimpleUploadInfoMonitoring(...args: unknown[]): unknown; // 1 arg
addFileSetSimpleUploadInfoMonitoring (...args: unknown[]): unknown; // 1 arg
removeFileSetSimpleUploadInfoMonitoring(...args: unknown[]): unknown; // 1 arg
removeFileSetSimpleUploadInfoMonitoring (...args: unknown[]): unknown; // 1 arg
removeFileSetSimpleUploadInfoListener(...args: unknown[]): unknown; // 1 arg
removeFileSetSimpleUploadInfoListener (...args: unknown[]): unknown; // 1 arg
/**
* 发送闪传消息
* @param sendArgs
*/
sendFlashTransferMsg(sendArgs: SendTargetRequests): Promise < {
sendFlashTransferMsg (sendArgs: SendTargetRequests): Promise<{
errCode: number,
errMsg: string,
rsp: {
sendStatus: SendStatus[]
}
} >; // 1 arg 估计是file set id
sendStatus: SendStatus[];
};
}>; // 1 arg 估计是file set id
addFlashTransferTaskInfoListener(...args: unknown[]): unknown; // 1 arg
addFlashTransferTaskInfoListener (...args: unknown[]): unknown; // 1 arg
removeFlashTransferTaskInfoListener(...args: unknown[]): unknown; // 1 arg
removeFlashTransferTaskInfoListener (...args: unknown[]): unknown; // 1 arg
retrieveLocalLastFailedSetTasksInfo(): unknown; // 0 arg
retrieveLocalLastFailedSetTasksInfo (): unknown; // 0 arg
getFailedFileList(fileSetId: string): Promise < {
getFailedFileList (fileSetId: string): Promise<{
rsp: {
seq: number;
result: number;
errMs: string;
fileSetId: string;
fileList: []
}
} >; // 1 arg
fileList: [];
};
}>; // 1 arg
getLocalFileListByStatuses(...args: unknown[]): unknown; // 1 arg
getLocalFileListByStatuses (...args: unknown[]): unknown; // 1 arg
addTransferStateListener(...args: unknown[]): unknown; // 1 arg
addTransferStateListener (...args: unknown[]): unknown; // 1 arg
removeTransferStateListener(...args: unknown[]): unknown; // 1 arg
removeTransferStateListener (...args: unknown[]): unknown; // 1 arg
getFileSetFirstClusteringList(...args: unknown[]): unknown; // 3 arg
getFileSetFirstClusteringList (...args: unknown[]): unknown; // 3 arg
getFileSetClusteringList(...args: unknown[]): unknown; // 1 arg
getFileSetClusteringList (...args: unknown[]): unknown; // 1 arg
addFileSetClusteringListListener(...args: unknown[]): unknown; // 1 arg
addFileSetClusteringListListener (...args: unknown[]): unknown; // 1 arg
removeFileSetClusteringListListener(...args: unknown[]): unknown; // 1 arg
removeFileSetClusteringListListener (...args: unknown[]): unknown; // 1 arg
getFileSetClusteringDetail(...args: unknown[]): unknown; // 1 arg
getFileSetClusteringDetail (...args: unknown[]): unknown; // 1 arg
doAIOFlashTransferBubbleActionWithStatus(...args: unknown[]): unknown; // 4 arg
doAIOFlashTransferBubbleActionWithStatus (...args: unknown[]): unknown; // 4 arg
getFilesTransferProgress(...args: unknown[]): unknown; // 1 arg
getFilesTransferProgress (...args: unknown[]): unknown; // 1 arg
pollFilesTransferProgress(...args: unknown[]): unknown; // 1 arg
pollFilesTransferProgress (...args: unknown[]): unknown; // 1 arg
cancelPollFilesTransferProgress(...args: unknown[]): unknown; // 1 arg
cancelPollFilesTransferProgress (...args: unknown[]): unknown; // 1 arg
checkDownloadStatusBeforeLocalFileOper(...args: unknown[]): unknown; // 3 arg
checkDownloadStatusBeforeLocalFileOper (...args: unknown[]): unknown; // 3 arg
getCompressedFileFolder(...args: unknown[]): unknown; // 1 arg
getCompressedFileFolder (...args: unknown[]): unknown; // 1 arg
addFolderListener(...args: unknown[]): unknown; // 1 arg
addFolderListener (...args: unknown[]): unknown; // 1 arg
removeFolderListener(...args: unknown[]): unknown;
removeFolderListener (...args: unknown[]): unknown;
addCompressedFileListener(...args: unknown[]): unknown;
addCompressedFileListener (...args: unknown[]): unknown;
removeCompressedFileListener(...args: unknown[]): unknown;
removeCompressedFileListener (...args: unknown[]): unknown;
getFileCategoryList(...args: unknown[]): unknown;
getFileCategoryList (...args: unknown[]): unknown;
addDeviceStatusListener(...args: unknown[]): unknown;
addDeviceStatusListener (...args: unknown[]): unknown;
removeDeviceStatusListener(...args: unknown[]): unknown;
removeDeviceStatusListener (...args: unknown[]): unknown;
checkDeviceStatus(...args: unknown[]): unknown;
checkDeviceStatus (...args: unknown[]): unknown;
pauseAllTasks(...args: unknown[]): unknown; // 2 arg
pauseAllTasks (...args: unknown[]): unknown; // 2 arg
resumePausedTasksAfterDeviceStatus(...args: unknown[]): unknown;
resumePausedTasksAfterDeviceStatus (...args: unknown[]): unknown;
onSystemGoingToSleep(...args: unknown[]): unknown;
onSystemGoingToSleep (...args: unknown[]): unknown;
onSystemWokeUp(...args: unknown[]): unknown;
onSystemWokeUp (...args: unknown[]): unknown;
getFileMetas(...args: unknown[]): unknown;
getFileMetas (...args: unknown[]): unknown;
addDownloadCntStatisticsListener(...args: unknown[]): unknown;
addDownloadCntStatisticsListener (...args: unknown[]): unknown;
removeDownloadCntStatisticsListener(...args: unknown[]): unknown;
removeDownloadCntStatisticsListener (...args: unknown[]): unknown;
detectPrivacyInfoInPaths(...args: unknown[]): unknown;
detectPrivacyInfoInPaths (...args: unknown[]): unknown;
getFileThumbnailUrl(...args: unknown[]): unknown;
getFileThumbnailUrl (...args: unknown[]): unknown;
handleDownloadFinishAfterSaveToAlbum(...args: unknown[]): unknown;
handleDownloadFinishAfterSaveToAlbum (...args: unknown[]): unknown;
checkBatchFilesDownloadStatus(...args: unknown[]): unknown;
checkBatchFilesDownloadStatus (...args: unknown[]): unknown;
onCheckAlbumStorageStatusResult(...args: unknown[]): unknown;
onCheckAlbumStorageStatusResult (...args: unknown[]): unknown;
addFileAlbumStorageListener(...args: unknown[]): unknown;
addFileAlbumStorageListener (...args: unknown[]): unknown;
removeFileAlbumStorageListener(...args: unknown[]): unknown;
removeFileAlbumStorageListener (...args: unknown[]): unknown;
refreshFolderStatus(...args: unknown[]): unknown;
refreshFolderStatus (...args: unknown[]): unknown;
}

View File

@@ -336,7 +336,7 @@ export interface NodeIKernelMsgService {
assembleMobileQQRichMediaFilePath (...args: unknown[]): unknown;
getFileThumbSavePathForSend (...args: unknown[]): unknown;
getFileThumbSavePathForSend (thumbSize: number, createNeed: boolean): string;
getFileThumbSavePath (...args: unknown[]): unknown;

View File

@@ -11,3 +11,7 @@ export * from './constant';
export * from './graytip';
export * from './emoji';
export * from './service';
export * from './adapter';
export * from './contact';
export * from './file';
export * from './flashfile';

View File

@@ -1,4 +1,4 @@
import { NTGroupMemberRole } from '@/napcat-core/index';
import { NTGroupMemberRole } from './group';
import { ActionBarElement, ArkElement, AvRecordElement, CalendarElement, FaceBubbleElement, FaceElement, FileElement, GiphyElement, GrayTipElement, MarketFaceElement, PicElement, PttElement, RecommendedMsgElement, ReplyElement, ShareLocationElement, StructLongMsgElement, TaskTopMsgElement, TextElement, TofuRecordElement, VideoElement, YoloGameResultElement } from './element';
/*

View File

@@ -1,5 +1,6 @@
import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from './adapters';
import {
GeneralCallResult,
NodeIKernelAvatarService,
NodeIKernelBuddyService,
NodeIKernelGroupService,
@@ -27,78 +28,78 @@ import { NodeIKernelMSFService } from './services/NodeIKernelMSFService';
import { NodeIkernelTestPerformanceService } from './services/NodeIkernelTestPerformanceService';
import { NodeIKernelECDHService } from './services/NodeIKernelECDHService';
import { NodeIO3MiscService } from './services/NodeIO3MiscService';
import { NodeIKernelFlashTransferService } from "./services/NodeIKernelFlashTransferService";
import { NodeIKernelFlashTransferService } from './services/NodeIKernelFlashTransferService';
export interface NodeQQNTWrapperUtil {
get(): NodeQQNTWrapperUtil;
get (): NodeQQNTWrapperUtil;
getNTUserDataInfoConfig(): string;
getNTUserDataInfoConfig (): string;
emptyWorkingSet(n: number): void;
emptyWorkingSet (n: number): void;
getSsoCmdOfOidbReq(arg1: number, arg2: number): unknown;
getSsoCmdOfOidbReq (arg1: number, arg2: number): unknown;
getSsoBufferOfOidbReq(...args: unknown[]): unknown; // 有点看不懂参数定义 待补充 好像是三个参数
getSsoBufferOfOidbReq (...args: unknown[]): unknown; // 有点看不懂参数定义 待补充 好像是三个参数
getOidbRspInfo(arg: string): unknown; // 可能是错的
getOidbRspInfo (arg: string): unknown; // 可能是错的
getFileSize(path: string): Promise<number>; // 直接的猜测
getFileSize (path: string): Promise<number>; // 直接的猜测
genFileMd5Buf(arg: string): unknown; // 可能是错的
genFileMd5Buf (arg: string): unknown; // 可能是错的
genFileMd5Hex(path: string): unknown; // 直接的猜测
genFileMd5Hex (path: string): unknown; // 直接的猜测
genFileShaBuf(path: string): unknown; // 直接的猜测
genFileShaBuf (path: string): unknown; // 直接的猜测
genFileCumulateSha1(path: string): unknown; // 直接的猜测
genFileCumulateSha1 (path: string): unknown; // 直接的猜测
genFileShaHex(path: string): unknown; // 直接的猜测
genFileShaHex (path: string): unknown; // 直接的猜测
fileIsExist(path: string): unknown;
fileIsExist (path: string): unknown;
startTrace(path: string): unknown; // 可能是错的
startTrace (path: string): unknown; // 可能是错的
copyFile(src: string, dst: string): unknown;
copyFile (src: string, dst: string): unknown;
genFileShaAndMd5Hex(path: string, unknown: number): unknown; // 可能是错的
genFileShaAndMd5Hex (path: string, unknown: number): unknown; // 可能是错的
setTraceInfo(unknown: unknown): unknown;
setTraceInfo (unknown: unknown): unknown;
encodeOffLine(unknown: unknown): unknown;
encodeOffLine (unknown: unknown): unknown;
decodeOffLine(arg: string): unknown; // 可能是错的 传递hex
decodeOffLine (arg: string): unknown; // 可能是错的 传递hex
DecoderRecentInfo(arg: string): unknown; // 可能是错的 传递hex
DecoderRecentInfo (arg: string): unknown; // 可能是错的 传递hex
getPinyin(arg0: string, arg1: boolean): unknown;
getPinyin (arg0: string, arg1: boolean): unknown;
matchInPinyin(arg0: unknown[], arg1: string): unknown; // 参数特复杂 arg0是个复杂数据类型
matchInPinyin (arg0: unknown[], arg1: string): unknown; // 参数特复杂 arg0是个复杂数据类型
makeDirByPath(arg0: string): unknown;
makeDirByPath (arg0: string): unknown;
emptyWorkingSet(arg0: number): unknown; // 参数是UINT32
emptyWorkingSet (arg0: number): unknown; // 参数是UINT32
runProcess(arg0: string, arg1: boolean): unknown;
runProcess (arg0: string, arg1: boolean): unknown;
runProcessArgs(arg0: string, arg1: { [key: string]: string }, arg2: boolean): unknown;
runProcessArgs (arg0: string, arg1: { [key: string]: string; }, arg2: boolean): unknown;
calcThumbSize(arg0: number, arg1: number, arg2: unknown): unknown;
calcThumbSize (arg0: number, arg1: number, arg2: unknown): unknown;
fullWordToHalfWord(word: string): unknown;
fullWordToHalfWord (word: string): unknown;
getNTUserDataInfoConfig(): unknown;
getNTUserDataInfoConfig (): unknown;
pathIsReadableAndWriteable(path: string): unknown; // 直接的猜测
pathIsReadableAndWriteable (path: string): unknown; // 直接的猜测
resetUserDataSavePathToDocument(): unknown;
resetUserDataSavePathToDocument (): unknown;
getSoBuildInfo(): unknown; // 例如 0[0]_d491dc01e0a_0
getSoBuildInfo (): unknown; // 例如 0[0]_d491dc01e0a_0
registerCountInstruments(arg0: string, arg1: string[], arg2: number, arg3: number): unknown;
registerCountInstruments (arg0: string, arg1: string[], arg2: number, arg3: number): unknown;
registerValueInstruments(arg0: string, arg1: string[], arg2: number, arg3: number): unknown;
registerValueInstruments (arg0: string, arg1: string[], arg2: number, arg3: number): unknown;
registerValueInstrumentsWithBoundary(
registerValueInstrumentsWithBoundary (
arg0: string,
arg1: unknown,
arg2: unknown,
@@ -106,7 +107,7 @@ export interface NodeQQNTWrapperUtil {
arg4: number,
): unknown;
reportCountIndicators(
reportCountIndicators (
arg0: string,
arg1: Map<unknown, unknown>,
arg2: string,
@@ -114,7 +115,7 @@ export interface NodeQQNTWrapperUtil {
arg4: boolean,
): unknown;
reportValueIndicators(
reportValueIndicators (
arg0: string,
arg1: Map<unknown, unknown>,
arg2: string,
@@ -122,142 +123,154 @@ export interface NodeQQNTWrapperUtil {
arg4: number,
): unknown;
checkNewUserDataSaveDirAvailable(arg0: string): unknown;
checkNewUserDataSaveDirAvailable (arg0: string): unknown;
copyUserData(arg0: string, arg1: string): Promise<unknown>;
copyUserData (arg0: string, arg1: string): Promise<unknown>;
setUserDataSaveDirectory(arg0: string): Promise<unknown>;
setUserDataSaveDirectory (arg0: string): Promise<unknown>;
hasOtherRunningQQProcess(): boolean;
hasOtherRunningQQProcess (): boolean;
quitAllRunningQQProcess(arg: boolean): unknown;
quitAllRunningQQProcess (arg: boolean): unknown;
checkNvidiaConfig(): unknown;
checkNvidiaConfig (): unknown;
repairNvidiaConfig(): unknown;
repairNvidiaConfig (): unknown;
getNvidiaDriverVersion(): unknown;
getNvidiaDriverVersion (): unknown;
isNull(): unknown;
isNull (): unknown;
createThumbnailImage (
serviceName: string,
filePath: string,
targetPath: string,
imgSize: {
width: number,
height: number;
},
fileFormat: string,
arg: number | null | undefined, // null undefined都行
): Promise<GeneralCallResult & { targetPath?: string; }>;
}
export interface NodeIQQNTStartupSessionWrapper {
create(): NodeIQQNTStartupSessionWrapper;
stop(): void;
start(): void;
createWithModuleList(uk: unknown): unknown;
getSessionIdList(): unknown;
create (): NodeIQQNTStartupSessionWrapper;
stop (): void;
start (): void;
createWithModuleList (uk: unknown): unknown;
getSessionIdList (): unknown;
}
export interface NodeIQQNTWrapperSession {
getNTWrapperSession(str: string): NodeIQQNTWrapperSession;
getNTWrapperSession (str: string): NodeIQQNTWrapperSession;
get(): NodeIQQNTWrapperSession;
get (): NodeIQQNTWrapperSession;
new(): NodeIQQNTWrapperSession;
create(): NodeIQQNTWrapperSession;
create (): NodeIQQNTWrapperSession;
init(
init (
wrapperSessionInitConfig: WrapperSessionInitConfig,
nodeIDependsAdapter: NodeIDependsAdapter,
nodeIDispatcherAdapter: NodeIDispatcherAdapter,
nodeIKernelSessionListener: NodeIKernelSessionListener,
): void;
startNT(session: number): void;
startNT (session: number): void;
startNT(): void;
startNT (): void;
getBdhUploadService(): unknown;
getBdhUploadService (): unknown;
getECDHService(): NodeIKernelECDHService;
getECDHService (): NodeIKernelECDHService;
getMsgService(): NodeIKernelMsgService;
getMsgService (): NodeIKernelMsgService;
getProfileService(): NodeIKernelProfileService;
getProfileService (): NodeIKernelProfileService;
getProfileLikeService(): NodeIKernelProfileLikeService;
getProfileLikeService (): NodeIKernelProfileLikeService;
getGroupService(): NodeIKernelGroupService;
getGroupService (): NodeIKernelGroupService;
getStorageCleanService(): NodeIKernelStorageCleanService;
getStorageCleanService (): NodeIKernelStorageCleanService;
getBuddyService(): NodeIKernelBuddyService;
getBuddyService (): NodeIKernelBuddyService;
getRobotService(): NodeIKernelRobotService;
getRobotService (): NodeIKernelRobotService;
getTicketService(): NodeIKernelTicketService;
getTicketService (): NodeIKernelTicketService;
getTipOffService(): NodeIKernelTipOffService;
getTipOffService (): NodeIKernelTipOffService;
getNodeMiscService(): NodeIKernelNodeMiscService;
getNodeMiscService (): NodeIKernelNodeMiscService;
getRichMediaService(): NodeIKernelRichMediaService;
getRichMediaService (): NodeIKernelRichMediaService;
getMsgBackupService(): NodeIKernelMsgBackupService;
getMsgBackupService (): NodeIKernelMsgBackupService;
getAlbumService(): NodeIKernelAlbumService;
getAlbumService (): NodeIKernelAlbumService;
getTianShuService(): NodeIKernelTianShuService;
getTianShuService (): NodeIKernelTianShuService;
getUnitedConfigService(): NodeIKernelUnitedConfigService;
getUnitedConfigService (): NodeIKernelUnitedConfigService;
getSearchService(): NodeIKernelSearchService;
getSearchService (): NodeIKernelSearchService;
getFlashTransferService(): NodeIKernelFlashTransferService;
getFlashTransferService (): NodeIKernelFlashTransferService;
getDirectSessionService(): unknown;
getDirectSessionService (): unknown;
getRDeliveryService(): unknown;
getRDeliveryService (): unknown;
getAvatarService(): NodeIKernelAvatarService;
getAvatarService (): NodeIKernelAvatarService;
getFeedChannelService(): unknown;
getFeedChannelService (): unknown;
getYellowFaceService(): unknown;
getYellowFaceService (): unknown;
getCollectionService(): NodeIKernelCollectionService;
getCollectionService (): NodeIKernelCollectionService;
getSettingService(): unknown;
getSettingService (): unknown;
getQiDianService(): unknown;
getQiDianService (): unknown;
getFileAssistantService(): unknown;
getFileAssistantService (): unknown;
getGuildService(): unknown;
getGuildService (): unknown;
getSkinService(): unknown;
getSkinService (): unknown;
getTestPerformanceService(): NodeIkernelTestPerformanceService;
getTestPerformanceService (): NodeIkernelTestPerformanceService;
getQQPlayService(): unknown;
getQQPlayService (): unknown;
getDbToolsService(): unknown;
getDbToolsService (): unknown;
getUixConvertService(): NodeIKernelUixConvertService;
getUixConvertService (): NodeIKernelUixConvertService;
getOnlineStatusService(): unknown;
getOnlineStatusService (): unknown;
getRemotingService(): unknown;
getRemotingService (): unknown;
getGroupTabService(): unknown;
getGroupTabService (): unknown;
getGroupSchoolService(): unknown;
getGroupSchoolService (): unknown;
getLiteBusinessService(): unknown;
getLiteBusinessService (): unknown;
getGuildMsgService(): unknown;
getGuildMsgService (): unknown;
getLockService(): unknown;
getLockService (): unknown;
getMSFService(): NodeIKernelMSFService;
getMSFService (): NodeIKernelMSFService;
getGuildHotUpdateService(): unknown;
getGuildHotUpdateService (): unknown;
getAVSDKService(): unknown;
getAVSDKService (): unknown;
getRecentContactService(): NodeIKernelRecentContactService;
getRecentContactService (): NodeIKernelRecentContactService;
getConfigMgrService(): unknown;
getConfigMgrService (): unknown;
}
export interface EnginInitDesktopConfig {
@@ -271,20 +284,20 @@ export interface EnginInitDesktopConfig {
global_path_config: {
desktopGlobalPath: string;
};
thumb_config: { maxSide: 324; minSide: 48; longLimit: 6; density: 2 };
thumb_config: { maxSide: 324; minSide: 48; longLimit: 6; density: 2; };
}
export interface NodeIQQNTWrapperEngine {
get(): NodeIQQNTWrapperEngine;
get (): NodeIQQNTWrapperEngine;
initWithDeskTopConfig(config: EnginInitDesktopConfig, nodeIGlobalAdapter: NodeIGlobalAdapter): void;
initWithDeskTopConfig (config: EnginInitDesktopConfig, nodeIGlobalAdapter: NodeIGlobalAdapter): void;
}
export interface WrapperNodeApi {
NodeIO3MiscService: NodeIO3MiscService;
NodeQQNTWrapperUtil: NodeQQNTWrapperUtil;
NodeIQQNTWrapperSession: NodeIQQNTWrapperSession;
NodeIQQNTStartupSessionWrapper: NodeIQQNTStartupSessionWrapper
NodeIQQNTStartupSessionWrapper: NodeIQQNTStartupSessionWrapper;
NodeIQQNTWrapperEngine: NodeIQQNTWrapperEngine;
NodeIKernelLoginService: NodeIKernelLoginService;

View File

@@ -0,0 +1,4 @@
NAPCAT_DISABLE_PIPE=1
NAPCAT_DISABLE_MULTI_PROCESS=1
NAPCAT_WEBUI_JWT_SECRET_KEY=napcat_dev_secret_key
NAPCAT_WEBUI_SECRET_KEY=napcatqq

View File

@@ -0,0 +1,39 @@
{
"network": {
"httpServers": [
{
"enable": true,
"name": "HTTP",
"host": "127.0.0.1",
"port": 3000,
"enableCors": true,
"enableWebsocket": false,
"messagePostFormat": "array",
"token": "",
"debug": false
}
],
"httpSseServers": [],
"httpClients": [],
"websocketServers": [
{
"enable": true,
"name": "WebSocket",
"host": "127.0.0.1",
"port": 3001,
"reportSelfMessage": false,
"enableForcePushEvent": true,
"messagePostFormat": "array",
"token": "",
"debug": false,
"heartInterval": 30000
}
],
"websocketClients": [],
"plugins": []
},
"musicSignUrl": "",
"enableLocalFile2Url": false,
"parseMultMsg": false,
"imageDownloadProxy": ""
}

View File

@@ -78,7 +78,7 @@ async function copyAll () {
process.env.NAPCAT_WORKDIR = TARGET_DIR;
// 开发环境使用固定密钥
process.env.NAPCAT_WEBUI_JWT_SECRET_KEY = 'napcat_dev_secret_key';
process.env.NAPCAT_WEBUI_SECRET_KEY = 'napcat';
process.env.NAPCAT_WEBUI_SECRET_KEY = 'napcatqq';
console.log('Loading NapCat module...');
await import(pathToFileURL(NAPCAT_MJS_PATH).href);
}

View File

@@ -1,27 +1,28 @@
{
"name": "napcat-develop",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.js",
"scripts": {
"dev": "powershell ./nodeTest.ps1"
"name": "napcat-develop",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.js",
"scripts": {
"dev": "powershell ./nodeTest.ps1",
"copy-env": "xcopy config ..\\napcat-shell\\dist\\config /E /I /Y"
},
"exports": {
".": {
"require": "./index.js"
},
"exports": {
".": {
"require": "./index.js"
},
"./*": {
"require": "./*"
}
},
"dependencies": {
"fs-extra": "^11.3.2"
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
"./*": {
"require": "./*"
}
},
"dependencies": {
"fs-extra": "^11.3.2"
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@@ -1,6 +1,6 @@
import { NapCatPathWrapper } from 'napcat-common/src/path';
import { InitWebUi, WebUiConfig, webUiRuntimePort } from 'napcat-webui-backend/index';
import { NapCatOneBot11Adapter } from 'napcat-onebot/index';
import { NapCatAdapterManager } from 'napcat-adapter';
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
import { FFmpegService } from 'napcat-core/helper/ffmpeg/ffmpeg';
import { logSubscription, LogWrapper } from 'napcat-core/helper/log';
@@ -79,11 +79,14 @@ export async function NCoreInitFramework (
// 启动WebUi
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework);
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
// 初始化LLNC的Onebot实现
const oneBotAdapter = new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper);
// 注册到 WebUiDataRuntime供调试功能使用
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
await oneBotAdapter.InitOneBot();
// 使用 NapCatAdapterManager 统一管理协议适配器
const adapterManager = new NapCatAdapterManager(loaderObject.core, loaderObject.context, pathWrapper);
await adapterManager.initAdapters();
// 注册 OneBot 适配器到 WebUiDataRuntime供调试功能使用
const oneBotAdapter = adapterManager.getOneBotAdapter();
if (oneBotAdapter) {
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
}
}
export class NapCatFramework {

View File

@@ -1,33 +1,33 @@
{
"name": "napcat-framework",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.ts",
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
"name": "napcat-framework",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.ts",
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"exports": {
".": {
"import": "./index.ts"
},
"exports": {
".": {
"import": "./index.ts"
},
"./*": {
"import": "./*"
}
},
"dependencies": {
"napcat-core": "workspace:*",
"napcat-common": "workspace:*",
"napcat-onebot": "workspace:*",
"napcat-webui-backend": "workspace:*",
"napcat-vite": "workspace:*",
"napcat-qrcode": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
"./*": {
"import": "./*"
}
},
"dependencies": {
"napcat-core": "workspace:*",
"napcat-common": "workspace:*",
"napcat-adapter": "workspace:*",
"napcat-webui-backend": "workspace:*",
"napcat-vite": "workspace:*",
"napcat-qrcode": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@@ -49,7 +49,9 @@ const FrameworkBaseConfig = () =>
'@/napcat-onebot': resolve(__dirname, '../napcat-onebot'),
'@/napcat-pty': resolve(__dirname, '../napcat-pty'),
'@/napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'),
'@/image-size': resolve(__dirname, '../image-size'),
'@/napcat-image-size': resolve(__dirname, '../napcat-image-size'),
'@/napcat-protocol': resolve(__dirname, '../napcat-protocol'),
'@/napcat-adapter': resolve(__dirname, '../napcat-adapter'),
},
},
build: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,5 +1,12 @@
import { BmpParser } from '@/napcat-image-size/src/parser/BmpParser';
import { GifParser } from '@/napcat-image-size/src/parser/GifParser';
import { JpegParser } from '@/napcat-image-size/src/parser/JpegParser';
import { PngParser } from '@/napcat-image-size/src/parser/PngParser';
import { TiffParser } from '@/napcat-image-size/src/parser/TiffParser';
import { WebpParser } from '@/napcat-image-size/src/parser/WebpParser';
import * as fs from 'fs';
import { ReadStream } from 'fs';
import { Readable } from 'stream';
export interface ImageSize {
width: number;
@@ -12,17 +19,18 @@ export enum ImageType {
BMP = 'bmp',
GIF = 'gif',
WEBP = 'webp',
TIFF = 'tiff',
UNKNOWN = 'unknown',
}
interface ImageParser {
export interface ImageParser {
readonly type: ImageType;
canParse(buffer: Buffer): boolean;
parseSize(stream: ReadStream): Promise<ImageSize | undefined>;
canParse (buffer: Buffer): boolean;
parseSize (stream: ReadStream): Promise<ImageSize | undefined>;
}
// 魔术匹配
function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolean {
export function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolean {
if (buffer.length < offset + magic.length) {
return false;
}
@@ -35,316 +43,39 @@ function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolean {
return true;
}
// PNG解析器
class PngParser implements ImageParser {
readonly type = ImageType.PNG;
// PNG 魔术头89 50 4E 47 0D 0A 1A 0A
private readonly PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
// 所有解析器实例
const parserInstances = {
png: new PngParser(),
jpeg: new JpegParser(),
bmp: new BmpParser(),
gif: new GifParser(),
webp: new WebpParser(),
tiff: new TiffParser(),
};
canParse (buffer: Buffer): boolean {
return matchMagic(buffer, this.PNG_SIGNATURE);
}
// 首字节到可能的图片类型映射,用于快速筛选
const firstByteMap = new Map<number, ImageType[]>([
[0x42, [ImageType.BMP]], // 'B' - BMP
[0x47, [ImageType.GIF]], // 'G' - GIF
[0x49, [ImageType.TIFF]], // 'I' - TIFF (II - little endian)
[0x4D, [ImageType.TIFF]], // 'M' - TIFF (MM - big endian)
[0x52, [ImageType.WEBP]], // 'R' - RIFF (WebP)
[0x89, [ImageType.PNG]], // PNG signature
[0xFF, [ImageType.JPEG]], // JPEG SOI
]);
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(24) as Buffer;
if (!buf || buf.length < 24) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt32BE(16);
const height = buf.readUInt32BE(20);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}
// 类型到解析器的映射
const typeToParser = new Map<ImageType, ImageParser>([
[ImageType.PNG, parserInstances.png],
[ImageType.JPEG, parserInstances.jpeg],
[ImageType.BMP, parserInstances.bmp],
[ImageType.GIF, parserInstances.gif],
[ImageType.WEBP, parserInstances.webp],
[ImageType.TIFF, parserInstances.tiff],
]);
// JPEG解析器
class JpegParser implements ImageParser {
readonly type = ImageType.JPEG;
// JPEG 魔术头FF D8
private readonly JPEG_SIGNATURE = [0xFF, 0xD8];
// JPEG标记常量
private readonly SOF_MARKERS = {
SOF0: 0xC0, // 基线DCT
SOF1: 0xC1, // 扩展顺序DCT
SOF2: 0xC2, // 渐进式DCT
SOF3: 0xC3, // 无损
} as const;
// 非SOF标记
private readonly NON_SOF_MARKERS: number[] = [
0xC4, // DHT
0xC8, // JPEG扩展
0xCC, // DAC
] as const;
canParse (buffer: Buffer): boolean {
return matchMagic(buffer, this.JPEG_SIGNATURE);
}
isSOFMarker (marker: number): boolean {
return (
marker === this.SOF_MARKERS.SOF0 ||
marker === this.SOF_MARKERS.SOF1 ||
marker === this.SOF_MARKERS.SOF2 ||
marker === this.SOF_MARKERS.SOF3
);
}
isNonSOFMarker (marker: number): boolean {
return this.NON_SOF_MARKERS.includes(marker);
}
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise<ImageSize | undefined>((resolve, reject) => {
const BUFFER_SIZE = 1024; // 读取块大小,可以根据需要调整
let buffer = Buffer.alloc(0);
let offset = 0;
let found = false;
// 处理错误
stream.on('error', (err) => {
stream.destroy();
reject(err);
});
// 处理数据块
stream.on('data', (chunk: Buffer | string) => {
// 追加新数据到缓冲区
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
buffer = Buffer.concat([buffer.subarray(offset), chunkBuffer]);
offset = 0;
// 保持缓冲区在合理大小内,只保留最后的部分用于跨块匹配
const bufferSize = buffer.length;
const MIN_REQUIRED_BYTES = 10; // SOF段最低字节数
// 从JPEG头部后开始扫描
while (offset < bufferSize - MIN_REQUIRED_BYTES) {
// 寻找FF标记
if (buffer[offset] === 0xFF && buffer[offset + 1]! >= 0xC0 && buffer[offset + 1]! <= 0xCF) {
const marker = buffer[offset + 1];
if (!marker) {
break;
}
// 跳过非SOF标记
if (this.isNonSOFMarker(marker)) {
offset += 2;
continue;
}
// 处理SOF标记 (包含尺寸信息)
if (this.isSOFMarker(marker)) {
// 确保缓冲区中有足够数据读取尺寸
if (offset + 9 < bufferSize) {
// 解析尺寸: FF XX YY YY PP HH HH WW WW ...
// XX = 标记, YY YY = 段长度, PP = 精度, HH HH = 高, WW WW = 宽
const height = buffer.readUInt16BE(offset + 5);
const width = buffer.readUInt16BE(offset + 7);
found = true;
stream.destroy();
resolve({ width, height });
return;
} else {
// 如果缓冲区内数据不够,保留当前位置等待更多数据
break;
}
}
}
offset++;
}
// 缓冲区管理: 如果处理了许多数据但没找到标记,
// 保留最后N字节用于跨块匹配丢弃之前的数据
if (offset > BUFFER_SIZE) {
const KEEP_BYTES = 20; // 保留足够数据以处理跨块边界的情况
if (offset > KEEP_BYTES) {
buffer = buffer.subarray(offset - KEEP_BYTES);
offset = KEEP_BYTES;
}
}
});
// 处理流结束
stream.on('end', () => {
if (!found) {
resolve(undefined);
}
});
});
}
}
// BMP解析器
class BmpParser implements ImageParser {
readonly type = ImageType.BMP;
// BMP 魔术头42 4D (BM)
private readonly BMP_SIGNATURE = [0x42, 0x4D];
canParse (buffer: Buffer): boolean {
return matchMagic(buffer, this.BMP_SIGNATURE);
}
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(26) as Buffer;
if (!buf || buf.length < 26) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt32LE(18);
const height = buf.readUInt32LE(22);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}
// GIF解析器
class GifParser implements ImageParser {
readonly type = ImageType.GIF;
// GIF87a 魔术头47 49 46 38 37 61
private readonly GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61];
// GIF89a 魔术头47 49 46 38 39 61
private readonly GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61];
canParse (buffer: Buffer): boolean {
return (
matchMagic(buffer, this.GIF87A_SIGNATURE) ||
matchMagic(buffer, this.GIF89A_SIGNATURE)
);
}
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(10) as Buffer;
if (!buf || buf.length < 10) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt16LE(6);
const height = buf.readUInt16LE(8);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}
// WEBP解析器 - 完整支持VP8, VP8L, VP8X格式
class WebpParser implements ImageParser {
readonly type = ImageType.WEBP;
// WEBP RIFF 头52 49 46 46 (RIFF)
private readonly RIFF_SIGNATURE = [0x52, 0x49, 0x46, 0x46];
// WEBP 魔术头57 45 42 50 (WEBP)
private readonly WEBP_SIGNATURE = [0x57, 0x45, 0x42, 0x50];
// WEBP 块头
private readonly CHUNK_VP8 = [0x56, 0x50, 0x38, 0x20]; // "VP8 "
private readonly CHUNK_VP8L = [0x56, 0x50, 0x38, 0x4C]; // "VP8L"
private readonly CHUNK_VP8X = [0x56, 0x50, 0x38, 0x58]; // "VP8X"
canParse (buffer: Buffer): boolean {
return (
buffer.length >= 12 &&
matchMagic(buffer, this.RIFF_SIGNATURE, 0) &&
matchMagic(buffer, this.WEBP_SIGNATURE, 8)
);
}
isChunkType (buffer: Buffer, offset: number, chunkType: number[]): boolean {
return buffer.length >= offset + 4 && matchMagic(buffer, chunkType, offset);
}
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
// 需要读取足够的字节来检测所有三种格式
const MAX_HEADER_SIZE = 32;
let totalBytes = 0;
let buffer = Buffer.alloc(0);
stream.on('error', reject);
stream.on('data', (chunk: Buffer | string) => {
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
buffer = Buffer.concat([buffer, chunkBuffer]);
totalBytes += chunk.length;
// 检查是否有足够的字节进行格式检测
if (totalBytes >= MAX_HEADER_SIZE) {
stream.destroy();
// 检查基本的WEBP签名
if (!this.canParse(buffer)) {
return resolve(undefined);
}
// 检查chunk头部位于字节12-15
if (this.isChunkType(buffer, 12, this.CHUNK_VP8)) {
// VP8格式 - 标准WebP
// 宽度和高度在帧头中
const width = buffer.readUInt16LE(26) & 0x3FFF;
const height = buffer.readUInt16LE(28) & 0x3FFF;
return resolve({ width, height });
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8L)) {
// VP8L格式 - 无损WebP
// 1字节标记后是14位宽度和14位高度
const bits = buffer.readUInt32LE(21);
const width = 1 + (bits & 0x3FFF);
const height = 1 + ((bits >> 14) & 0x3FFF);
return resolve({ width, height });
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8X)) {
// VP8X格式 - 扩展WebP
// 24位宽度和高度(减去1)
if (!buffer[24] || !buffer[25] || !buffer[26] || !buffer[27] || !buffer[28] || !buffer[29]) {
return resolve(undefined);
}
const width = 1 + ((buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) & 0xFFFFFF);
const height = 1 + ((buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) & 0xFFFFFF);
return resolve({ width, height });
} else {
// 未知的WebP子格式
return resolve(undefined);
}
}
});
stream.on('end', () => {
// 如果没有读到足够的字节
if (totalBytes < MAX_HEADER_SIZE) {
resolve(undefined);
}
});
});
}
}
const parsers: ReadonlyArray<ImageParser> = [
new PngParser(),
new JpegParser(),
new BmpParser(),
new GifParser(),
new WebpParser(),
];
// 所有解析器列表(用于回退)
const parsers: ReadonlyArray<ImageParser> = Object.values(parserInstances);
export async function detectImageType (filePath: string): Promise<ImageType> {
return new Promise((resolve, reject) => {
@@ -354,18 +85,22 @@ export async function detectImageType (filePath: string): Promise<ImageType> {
end: 63,
});
let buffer: Buffer | null = null;
const chunks: Buffer[] = [];
stream.once('error', (err) => {
stream.on('error', (err) => {
stream.destroy();
reject(err);
});
stream.once('readable', () => {
buffer = stream.read(64) as Buffer;
stream.destroy();
stream.on('data', (chunk: Buffer | string) => {
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
chunks.push(chunkBuffer);
});
if (!buffer) {
stream.on('end', () => {
const buffer = Buffer.concat(chunks);
if (buffer.length === 0) {
return resolve(ImageType.UNKNOWN);
}
@@ -377,12 +112,6 @@ export async function detectImageType (filePath: string): Promise<ImageType> {
resolve(ImageType.UNKNOWN);
});
stream.once('end', () => {
if (!buffer) {
resolve(ImageType.UNKNOWN);
}
});
});
}
@@ -390,7 +119,7 @@ export async function imageSizeFromFile (filePath: string): Promise<ImageSize |
try {
// 先检测类型
const type = await detectImageType(filePath);
const parser = parsers.find(p => p.type === type);
const parser = typeToParser.get(type);
if (!parser) {
return undefined;
}
@@ -422,3 +151,71 @@ export async function imageSizeFallBack (
): Promise<ImageSize> {
return await imageSizeFromFile(filePath) ?? fallback;
}
// 从 Buffer 创建可读流
function bufferToReadStream (buffer: Buffer): ReadStream {
const readable = new Readable({
read () {
this.push(buffer);
this.push(null);
}
});
return readable as unknown as ReadStream;
}
// 从 Buffer 检测图片类型(使用首字节快速筛选)
export function detectImageTypeFromBuffer (buffer: Buffer): ImageType {
if (buffer.length === 0) {
return ImageType.UNKNOWN;
}
const firstByte = buffer[0]!;
const possibleTypes = firstByteMap.get(firstByte);
if (possibleTypes) {
// 根据首字节快速筛选可能的类型
for (const type of possibleTypes) {
const parser = typeToParser.get(type);
if (parser && parser.canParse(buffer)) {
return parser.type;
}
}
}
// 回退:遍历所有解析器
for (const parser of parsers) {
if (parser.canParse(buffer)) {
return parser.type;
}
}
return ImageType.UNKNOWN;
}
// 从 Buffer 解析图片尺寸
export async function imageSizeFromBuffer (buffer: Buffer): Promise<ImageSize | undefined> {
const type = detectImageTypeFromBuffer(buffer);
const parser = typeToParser.get(type);
if (!parser) {
return undefined;
}
try {
const stream = bufferToReadStream(buffer);
return await parser.parseSize(stream);
} catch (err) {
console.error(`解析图片尺寸出错: ${err}`);
return undefined;
}
}
// 从 Buffer 解析图片尺寸,带回退值
export async function imageSizeFromBufferFallBack (
buffer: Buffer,
fallback: ImageSize = {
width: 1024,
height: 1024,
}
): Promise<ImageSize> {
return await imageSizeFromBuffer(buffer) ?? fallback;
}

View File

@@ -0,0 +1,32 @@
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
import { ReadStream } from 'fs';
// BMP解析器
export class BmpParser implements ImageParser {
readonly type = ImageType.BMP;
// BMP 魔术头42 4D (BM)
private readonly BMP_SIGNATURE = [0x42, 0x4D];
canParse (buffer: Buffer): boolean {
return matchMagic(buffer, this.BMP_SIGNATURE);
}
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(26) as Buffer;
if (!buf || buf.length < 26) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt32LE(18);
const height = buf.readUInt32LE(22);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}

View File

@@ -0,0 +1,37 @@
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
import { ReadStream } from 'fs';
// GIF解析器
export class GifParser implements ImageParser {
readonly type = ImageType.GIF;
// GIF87a 魔术头47 49 46 38 37 61
private readonly GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61];
// GIF89a 魔术头47 49 46 38 39 61
private readonly GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61];
canParse (buffer: Buffer): boolean {
return (
matchMagic(buffer, this.GIF87A_SIGNATURE) ||
matchMagic(buffer, this.GIF89A_SIGNATURE)
);
}
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(10) as Buffer;
if (!buf || buf.length < 10) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt16LE(6);
const height = buf.readUInt16LE(8);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}

View File

@@ -0,0 +1,123 @@
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
import { ReadStream } from 'fs';
// JPEG解析器
export class JpegParser implements ImageParser {
readonly type = ImageType.JPEG;
// JPEG 魔术头FF D8
private readonly JPEG_SIGNATURE = [0xFF, 0xD8];
// JPEG标记常量
private readonly SOF_MARKERS = {
SOF0: 0xC0, // 基线DCT
SOF1: 0xC1, // 扩展顺序DCT
SOF2: 0xC2, // 渐进式DCT
SOF3: 0xC3, // 无损
} as const;
// 非SOF标记
private readonly NON_SOF_MARKERS: number[] = [
0xC4, // DHT
0xC8, // JPEG扩展
0xCC, // DAC
] as const;
canParse (buffer: Buffer): boolean {
return matchMagic(buffer, this.JPEG_SIGNATURE);
}
isSOFMarker (marker: number): boolean {
return (
marker === this.SOF_MARKERS.SOF0 ||
marker === this.SOF_MARKERS.SOF1 ||
marker === this.SOF_MARKERS.SOF2 ||
marker === this.SOF_MARKERS.SOF3
);
}
isNonSOFMarker (marker: number): boolean {
return this.NON_SOF_MARKERS.includes(marker);
}
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise<ImageSize | undefined>((resolve, reject) => {
const BUFFER_SIZE = 1024; // 读取块大小,可以根据需要调整
let buffer = Buffer.alloc(0);
let offset = 0;
let found = false;
// 处理错误
stream.on('error', (err) => {
stream.destroy();
reject(err);
});
// 处理数据块
stream.on('data', (chunk: Buffer | string) => {
// 追加新数据到缓冲区
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
buffer = Buffer.concat([buffer.subarray(offset), chunkBuffer]);
offset = 0;
// 保持缓冲区在合理大小内,只保留最后的部分用于跨块匹配
const bufferSize = buffer.length;
const MIN_REQUIRED_BYTES = 10; // SOF段最低字节数
// 从JPEG头部后开始扫描
while (offset < bufferSize - MIN_REQUIRED_BYTES) {
// 寻找FF标记
if (buffer[offset] === 0xFF && buffer[offset + 1]! >= 0xC0 && buffer[offset + 1]! <= 0xCF) {
const marker = buffer[offset + 1];
if (!marker) {
break;
}
// 跳过非SOF标记
if (this.isNonSOFMarker(marker)) {
offset += 2;
continue;
}
// 处理SOF标记 (包含尺寸信息)
if (this.isSOFMarker(marker)) {
// 确保缓冲区中有足够数据读取尺寸
if (offset + 9 < bufferSize) {
// 解析尺寸: FF XX YY YY PP HH HH WW WW ...
// XX = 标记, YY YY = 段长度, PP = 精度, HH HH = 高, WW WW = 宽
const height = buffer.readUInt16BE(offset + 5);
const width = buffer.readUInt16BE(offset + 7);
found = true;
stream.destroy();
resolve({ width, height });
return;
} else {
// 如果缓冲区内数据不够,保留当前位置等待更多数据
break;
}
}
}
offset++;
}
// 缓冲区管理: 如果处理了许多数据但没找到标记,
// 保留最后N字节用于跨块匹配丢弃之前的数据
if (offset > BUFFER_SIZE) {
const KEEP_BYTES = 20; // 保留足够数据以处理跨块边界的情况
if (offset > KEEP_BYTES) {
buffer = buffer.subarray(offset - KEEP_BYTES);
offset = KEEP_BYTES;
}
}
});
// 处理流结束
stream.on('end', () => {
if (!found) {
resolve(undefined);
}
});
});
}
}

View File

@@ -0,0 +1,32 @@
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
import { ReadStream } from 'fs';
// PNG解析器
export class PngParser implements ImageParser {
readonly type = ImageType.PNG;
// PNG 魔术头89 50 4E 47 0D 0A 1A 0A
private readonly PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
canParse (buffer: Buffer): boolean {
return matchMagic(buffer, this.PNG_SIGNATURE);
}
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(24) as Buffer;
if (!buf || buf.length < 24) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt32BE(16);
const height = buf.readUInt32BE(20);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}

View File

@@ -0,0 +1,124 @@
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
import { ReadStream } from 'fs';
// TIFF解析器
export class TiffParser implements ImageParser {
readonly type = ImageType.TIFF;
// TIFF Little Endian 魔术头49 49 2A 00 (II)
private readonly TIFF_LE_SIGNATURE = [0x49, 0x49, 0x2A, 0x00];
// TIFF Big Endian 魔术头4D 4D 00 2A (MM)
private readonly TIFF_BE_SIGNATURE = [0x4D, 0x4D, 0x00, 0x2A];
canParse (buffer: Buffer): boolean {
return (
matchMagic(buffer, this.TIFF_LE_SIGNATURE) ||
matchMagic(buffer, this.TIFF_BE_SIGNATURE)
);
}
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
let totalBytes = 0;
const MAX_BYTES = 64 * 1024; // 最多读取 64KB
stream.on('error', reject);
stream.on('data', (chunk: Buffer | string) => {
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
chunks.push(chunkBuffer);
totalBytes += chunkBuffer.length;
if (totalBytes >= MAX_BYTES) {
stream.destroy();
}
});
stream.on('end', () => {
const buffer = Buffer.concat(chunks);
const size = this.parseTiffSize(buffer);
resolve(size);
});
stream.on('close', () => {
if (chunks.length > 0) {
const buffer = Buffer.concat(chunks);
const size = this.parseTiffSize(buffer);
resolve(size);
}
});
});
}
private parseTiffSize (buffer: Buffer): ImageSize | undefined {
if (buffer.length < 8) {
return undefined;
}
// 判断字节序
const isLittleEndian = buffer[0] === 0x49; // 'I'
const readUInt16 = isLittleEndian
? (offset: number) => buffer.readUInt16LE(offset)
: (offset: number) => buffer.readUInt16BE(offset);
const readUInt32 = isLittleEndian
? (offset: number) => buffer.readUInt32LE(offset)
: (offset: number) => buffer.readUInt32BE(offset);
// 获取第一个 IFD 的偏移量
const ifdOffset = readUInt32(4);
if (ifdOffset + 2 > buffer.length) {
return undefined;
}
// 读取 IFD 条目数量
const numEntries = readUInt16(ifdOffset);
let width: number | undefined;
let height: number | undefined;
// TIFF 标签
const TAG_IMAGE_WIDTH = 0x0100;
const TAG_IMAGE_HEIGHT = 0x0101;
// 遍历 IFD 条目
for (let i = 0; i < numEntries; i++) {
const entryOffset = ifdOffset + 2 + i * 12;
if (entryOffset + 12 > buffer.length) {
break;
}
const tag = readUInt16(entryOffset);
const type = readUInt16(entryOffset + 2);
// const count = readUInt32(entryOffset + 4);
// 根据类型读取值
let value: number;
if (type === 3) {
// SHORT (2 bytes)
value = readUInt16(entryOffset + 8);
} else if (type === 4) {
// LONG (4 bytes)
value = readUInt32(entryOffset + 8);
} else {
continue;
}
if (tag === TAG_IMAGE_WIDTH) {
width = value;
} else if (tag === TAG_IMAGE_HEIGHT) {
height = value;
}
if (width !== undefined && height !== undefined) {
return { width, height };
}
}
if (width !== undefined && height !== undefined) {
return { width, height };
}
return undefined;
}
}

View File

@@ -0,0 +1,90 @@
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
import { ReadStream } from 'fs';
// WEBP解析器 - 完整支持VP8, VP8L, VP8X格式
export class WebpParser implements ImageParser {
readonly type = ImageType.WEBP;
// WEBP RIFF 头52 49 46 46 (RIFF)
private readonly RIFF_SIGNATURE = [0x52, 0x49, 0x46, 0x46];
// WEBP 魔术头57 45 42 50 (WEBP)
private readonly WEBP_SIGNATURE = [0x57, 0x45, 0x42, 0x50];
// WEBP 块头
private readonly CHUNK_VP8 = [0x56, 0x50, 0x38, 0x20]; // "VP8 "
private readonly CHUNK_VP8L = [0x56, 0x50, 0x38, 0x4C]; // "VP8L"
private readonly CHUNK_VP8X = [0x56, 0x50, 0x38, 0x58]; // "VP8X"
canParse (buffer: Buffer): boolean {
return (
buffer.length >= 12 &&
matchMagic(buffer, this.RIFF_SIGNATURE, 0) &&
matchMagic(buffer, this.WEBP_SIGNATURE, 8)
);
}
isChunkType (buffer: Buffer, offset: number, chunkType: number[]): boolean {
return buffer.length >= offset + 4 && matchMagic(buffer, chunkType, offset);
}
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
// 需要读取足够的字节来检测所有三种格式
const MAX_HEADER_SIZE = 32;
let totalBytes = 0;
let buffer = Buffer.alloc(0);
stream.on('error', reject);
stream.on('data', (chunk: Buffer | string) => {
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
buffer = Buffer.concat([buffer, chunkBuffer]);
totalBytes += chunk.length;
// 检查是否有足够的字节进行格式检测
if (totalBytes >= MAX_HEADER_SIZE) {
stream.destroy();
// 检查基本的WEBP签名
if (!this.canParse(buffer)) {
return resolve(undefined);
}
// 检查chunk头部位于字节12-15
if (this.isChunkType(buffer, 12, this.CHUNK_VP8)) {
// VP8格式 - 标准WebP
// 宽度和高度在帧头中
const width = buffer.readUInt16LE(26) & 0x3FFF;
const height = buffer.readUInt16LE(28) & 0x3FFF;
return resolve({ width, height });
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8L)) {
// VP8L格式 - 无损WebP
// 1字节标记后是14位宽度和14位高度
const bits = buffer.readUInt32LE(21);
const width = 1 + (bits & 0x3FFF);
const height = 1 + ((bits >> 14) & 0x3FFF);
return resolve({ width, height });
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8X)) {
// VP8X格式 - 扩展WebP
// 24位宽度和高度(减去1)
if (buffer.length < 30) {
return resolve(undefined);
}
const width = 1 + ((buffer[24]! | (buffer[25]! << 8) | (buffer[26]! << 16)) & 0xFFFFFF);
const height = 1 + ((buffer[27]! | (buffer[28]! << 8) | (buffer[29]! << 16)) & 0xFFFFFF);
return resolve({ width, height });
} else {
// 未知的WebP子格式
return resolve(undefined);
}
}
});
stream.on('end', () => {
// 如果没有读到足够的字节
if (totalBytes < MAX_HEADER_SIZE) {
resolve(undefined);
}
});
});
}
}

View File

@@ -5,9 +5,18 @@ import { NapCatOneBot11Adapter, OB11Return } from '@/napcat-onebot/index';
import { NetworkAdapterConfig } from '../config/config';
import { TSchema } from '@sinclair/typebox';
import { StreamPacket, StreamPacketBasic, StreamStatus } from './stream/StreamBasic';
export const ActionExamples = {
Common: {
errors: [
{ code: 1400, description: '请求参数错误或业务逻辑执行失败' },
{ code: 1401, description: '权限不足' },
{ code: 1404, description: '资源不存在' }
]
}
};
export class OB11Response {
private static createResponse<T>(data: T, status: string, retcode: number, message: string = '', echo: unknown = null, useStream: boolean = false): OB11Return<T> {
private static createResponse<T> (data: T, status: string, retcode: number, message: string = '', echo: unknown = null, useStream: boolean = false): OB11Return<T> {
return {
status,
retcode,
@@ -19,11 +28,11 @@ export class OB11Response {
};
}
static res<T>(data: T, status: string, retcode: number, message: string = '', echo: unknown = null, useStream: boolean = false): OB11Return<T> {
static res<T> (data: T, status: string, retcode: number, message: string = '', echo: unknown = null, useStream: boolean = false): OB11Return<T> {
return this.createResponse(data, status, retcode, message, echo, useStream);
}
static ok<T>(data: T, echo: unknown = null, useStream: boolean = false): OB11Return<T> {
static ok<T> (data: T, echo: unknown = null, useStream: boolean = false): OB11Return<T> {
return this.createResponse(data, 'ok', 0, '', echo, useStream);
}
@@ -32,15 +41,22 @@ export class OB11Response {
}
}
export abstract class OneBotRequestToolkit {
abstract send<T>(packet: StreamPacket<T>): Promise<void>;
abstract send<T> (packet: StreamPacket<T>): Promise<void>;
}
export abstract class OneBotAction<PayloadType, ReturnDataType> {
actionName: typeof ActionName[keyof typeof ActionName] = ActionName.Unknown;
core: NapCatCore;
private validate?: ValidateFunction<unknown> = undefined;
payloadSchema?: TSchema = undefined;
returnSchema?: TSchema = undefined;
payloadExample?: unknown = undefined;
returnExample?: unknown = undefined;
actionSummary: string = '';
actionDescription: string = '';
actionTags: string[] = [];
obContext: NapCatOneBot11Adapter;
useStream: boolean = false;
errorExamples: Array<{ code: number, description: string; }> = ActionExamples.Common.errors;
constructor (obContext: NapCatOneBot11Adapter, core: NapCatCore) {
this.obContext = obContext;

View File

@@ -0,0 +1,45 @@
export const ExtendsActionsExamples = {
OCRImage: {
payload: { image: 'image_id_123' },
response: { texts: [{ text: '识别内容', coordinates: [] }] },
},
GetAiCharacters: {
payload: { group_id: '123456' },
response: [
{
type: 'string',
characters: [
{ character_id: 'id', character_name: 'name', preview_url: 'url' }
]
}
],
},
GetClientkey: {
payload: {},
response: { clientkey: 'abcdef123456' },
},
SetQQAvatar: {
payload: { file: 'base64://...' },
response: null,
},
SetGroupKickMembers: {
payload: { group_id: '123456', user_id: ['123456789'], reject_add_request: false },
response: null,
},
TranslateEnWordToZn: {
payload: { words: ['hello'] },
response: { words: ['你好'] },
},
GetRkey: {
payload: {},
response: { rkey: '...' },
},
SetLongNick: {
payload: { longNick: '个性签名' },
response: null,
},
SetSpecialTitle: {
payload: { group_id: '123456', user_id: '123456789', special_title: '头衔' },
response: null,
},
};

View File

@@ -0,0 +1,22 @@
export const FileActionsExamples = {
GetFile: {
payload: { file: 'file_id_123' },
response: { file: '/path/to/file', url: 'http://...', file_size: 1024, file_name: 'test.jpg' },
},
GetGroupFileUrl: {
payload: { group_id: '123456', file_id: 'file_id_123', busid: 102 },
response: { url: 'http://...' },
},
GetImage: {
payload: { file: 'image_id_123' },
response: { file: '/path/to/image', url: 'http://...' },
},
GetPrivateFileUrl: {
payload: { user_id: '123456789', file_id: 'file_id_123' },
response: { url: 'http://...' },
},
GetRecord: {
payload: { file: 'record_id_123', out_format: 'mp3' },
response: { file: '/path/to/record', url: 'http://...' },
},
};

View File

@@ -0,0 +1,102 @@
export const GoCQHTTPActionsExamples = {
GetStrangerInfo: {
payload: { user_id: '123456789' },
response: { user_id: 123456789, nickname: '昵称', sex: 'unknown' },
},
GetGroupHonorInfo: {
payload: { group_id: '123456', type: 'all' },
response: { group_id: 123456, current_talkative: {}, talkative_list: [] },
},
GetForwardMsg: {
payload: { message_id: '123456' },
response: { messages: [] },
},
SendForwardMsg: {
payload: { group_id: '123456', messages: [] },
response: { message_id: 123456 },
},
GetGroupAtAllRemain: {
payload: { group_id: '123456' },
response: { can_at_all: true, remain_at_all_count_for_group: 10, remain_at_all_count_for_self: 10 },
},
CreateGroupFileFolder: {
payload: { group_id: '123456', name: '测试目录' },
response: { result: {}, groupItem: {} },
},
DeleteGroupFile: {
payload: { group_id: '123456', file_id: 'file_uuid_123' },
response: {},
},
DeleteGroupFileFolder: {
payload: { group_id: '123456', folder_id: 'folder_uuid_123' },
response: {},
},
DownloadFile: {
payload: { url: 'https://example.com/file.png', thread_count: 1, headers: 'User-Agent: NapCat' },
response: { file: '/path/to/downloaded/file' },
},
GetFriendMsgHistory: {
payload: { user_id: '123456789', message_seq: 0, count: 20 },
response: { messages: [] },
},
GetGroupFilesByFolder: {
payload: { group_id: '123456', folder_id: 'folder_id' },
response: { files: [], folders: [] },
},
GetGroupFileSystemInfo: {
payload: { group_id: '123456' },
response: { file_count: 10, limit_count: 10000, used_space: 1024, total_space: 10737418240 },
},
GetGroupMsgHistory: {
payload: { group_id: '123456', message_seq: 0, count: 20 },
response: { messages: [] },
},
GetGroupRootFiles: {
payload: { group_id: '123456' },
response: { files: [], folders: [] },
},
GetOnlineClient: {
payload: { no_cache: false },
response: [],
},
GoCQHTTPCheckUrlSafely: {
payload: { url: 'https://example.com' },
response: { level: 1 },
},
GoCQHTTPDeleteFriend: {
payload: { user_id: '123456789' },
response: {},
},
GoCQHTTPGetModelShow: {
payload: { model: 'iPhone 13' },
response: { variants: [] },
},
GoCQHTTPSetModelShow: {
payload: { model: 'iPhone 13', model_show: 'iPhone 13' },
response: {},
},
QuickAction: {
payload: { context: {}, operation: {} },
response: {},
},
SendGroupNotice: {
payload: { group_id: '123456', content: '公告内容', image: 'base64://...' },
response: {},
},
SetGroupPortrait: {
payload: { group_id: '123456', file: 'base64://...' },
response: { result: 0, errMsg: '' },
},
SetQQProfile: {
payload: { nickname: '新昵称', personal_note: '个性签名' },
response: {},
},
UploadGroupFile: {
payload: { group_id: '123456', file: '/path/to/file', name: 'test.txt' },
response: { file_id: 'file_uuid_123' },
},
UploadPrivateFile: {
payload: { user_id: '123456789', file: '/path/to/file', name: 'test.txt' },
response: { file_id: 'file_uuid_123' },
},
};

View File

@@ -0,0 +1,79 @@
export const GroupActionsExamples = {
DelEssenceMsg: {
payload: { message_id: 123456 },
response: null,
},
DelGroupNotice: {
payload: { group_id: '123456', notice_id: 'notice_123' },
response: null,
},
GetGroupDetailInfo: {
payload: { group_id: '123456' },
response: { group_id: 123456, group_name: '测试群', member_count: 100, max_member_count: 500 },
},
GetGroupEssence: {
payload: { group_id: '123456' },
response: [{ message_id: 123456, sender_id: 123456, sender_nick: '昵称', operator_id: 123456, operator_nick: '昵称', operator_time: 1710000000, content: '精华内容' }],
},
GetGroupInfo: {
payload: { group_id: '123456' },
response: { group_id: 123456, group_name: '测试群', member_count: 100, max_member_count: 500 },
},
GetGroupList: {
payload: {},
response: [{ group_id: 123456, group_name: '测试群', member_count: 100, max_member_count: 500 }],
},
GetGroupMemberInfo: {
payload: { group_id: '123456', user_id: '123456789' },
response: { group_id: 123456, user_id: 123456789, nickname: '昵称', card: '名片', role: 'member' },
},
GetGroupMemberList: {
payload: { group_id: '123456' },
response: [{ group_id: 123456, user_id: 123456789, nickname: '昵称', card: '名片', role: 'member' }],
},
GetGroupNotice: {
payload: { group_id: '123456' },
response: [{ notice_id: 'notice_123', sender_id: 123456, publish_time: 1710000000, message: { text: '公告内容', image: [] } }],
},
SendGroupMsg: {
payload: { group_id: '123456', message: 'hello' },
response: { message_id: 123456 },
},
SetEssenceMsg: {
payload: { message_id: 123456 },
response: null,
},
SetGroupAddRequest: {
payload: { flag: 'flag_123', sub_type: 'add', approve: true },
response: null,
},
SetGroupAdmin: {
payload: { group_id: '123456', user_id: '123456789', enable: true },
response: null,
},
SetGroupBan: {
payload: { group_id: '123456', user_id: '123456789', duration: 1800 },
response: null,
},
SetGroupCard: {
payload: { group_id: '123456', user_id: '123456789', card: '新名片' },
response: null,
},
SetGroupKick: {
payload: { group_id: '123456', user_id: '123456789', reject_add_request: false },
response: null,
},
SetGroupLeave: {
payload: { group_id: '123456', is_dismiss: false },
response: null,
},
SetGroupName: {
payload: { group_id: '123456', group_name: '新群名' },
response: null,
},
SetGroupWholeBan: {
payload: { group_id: '123456', enable: true },
response: null,
},
};

View File

@@ -0,0 +1,10 @@
export const GuildActionsExamples = {
GetGuildList: {
payload: {},
response: [{ guild_id: '123456', guild_name: '测试频道' }],
},
GetGuildProfile: {
payload: { guild_id: '123456' },
response: { guild_id: '123456', guild_name: '测试频道', guild_display_id: '123' },
},
};

View File

@@ -0,0 +1,10 @@
export const NewActionsExamples = {
GetDoubtFriendsAddRequest: {
payload: { count: 10 },
response: [{ user_id: 123456789, nickname: '昵称', age: 20, sex: 'male', reason: '申请理由', flag: 'flag_123' }],
},
SetDoubtFriendsAddRequest: {
payload: { flag: 'flag_123', approve: true },
response: {},
},
};

View File

@@ -0,0 +1,14 @@
export const PacketActionsExamples = {
GetPacketStatus: {
payload: {},
response: { status: 'ok' },
},
SendPoke: {
payload: { user_id: '123456789' },
response: {},
},
SetGroupTodo: {
payload: { group_id: '123456', message_id: '123456789' },
response: {},
},
};

View File

@@ -0,0 +1,42 @@
export const SystemActionsExamples = {
CanSendImage: {
payload: {},
response: { yes: true },
},
CanSendRecord: {
payload: {},
response: { yes: true },
},
CleanCache: {
payload: {},
response: {},
},
GetCredentials: {
payload: {},
response: { cookies: '...', csrf_token: 123456789 },
},
GetCSRF: {
payload: {},
response: { token: 123456789 },
},
GetLoginInfo: {
payload: {},
response: { user_id: 123456789, nickname: '机器人' },
},
GetStatus: {
payload: {},
response: { online: true, good: true },
},
GetSystemMsg: {
payload: {},
response: { invited_requests: [], join_requests: [] },
},
GetVersionInfo: {
payload: {},
response: { app_name: 'NapCatQQ', app_version: '1.0.0', protocol_version: 'v11' },
},
SetRestart: {
payload: { delay: 0 },
response: {},
},
};

View File

@@ -0,0 +1,38 @@
export const UserActionsExamples = {
GetCookies: {
payload: { domain: 'qun.qq.com' },
response: { cookies: 'p_skey=xxx; p_uin=o0123456789;' },
},
GetFriendList: {
payload: {},
response: [{ user_id: 123456789, nickname: '昵称', remark: '备注' }],
},
GetRecentContact: {
payload: { count: 10 },
response: [
{
lastestMsg: 'hello',
peerUin: '123456789',
remark: 'remark',
msgTime: '1710000000',
chatType: 1,
msgId: '12345',
sendNickName: 'nick',
sendMemberName: 'card',
peerName: 'name',
},
],
},
SendLike: {
payload: { user_id: '123456789', times: 10 },
response: {},
},
SetFriendAddRequest: {
payload: { flag: 'flag_123', approve: true, remark: '好友' },
response: {},
},
SetFriendRemark: {
payload: { user_id: '123456789', remark: '新备注' },
response: {},
},
};

View File

@@ -1,8 +1,15 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { OneBotAction } from '../OneBotAction';
import { Type } from '@sinclair/typebox';
export class BotExit extends OneBotAction<void, void> {
override actionName = ActionName.Exit;
override payloadSchema = Type.Object({});
override returnSchema = Type.Object({});
override actionSummary = '退出登录';
override actionTags = ['系统扩展'];
override payloadExample = {};
override returnExample = null;
async _handle () {
process.exit(0);

View File

@@ -2,21 +2,36 @@ import { ActionName } from '@/napcat-onebot/action/router';
import { OneBotAction } from '../OneBotAction';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
bot_appid: Type.String(),
button_id: Type.String({ default: '' }),
callback_data: Type.String({ default: '' }),
msg_seq: Type.String({ default: '10086' }),
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
bot_appid: Type.String({ description: '机器人AppID' }),
button_id: Type.String({ default: '', description: '按钮ID' }),
callback_data: Type.String({ default: '', description: '回调数据' }),
msg_seq: Type.String({ default: '10086', description: '消息序列号' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class ClickInlineKeyboardButton extends OneBotAction<Payload, unknown> {
const ReturnSchema = Type.Any({ description: '点击结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class ClickInlineKeyboardButton extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.ClickInlineKeyboardButton;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '点击内联键盘按钮';
override actionTags = ['消息扩展'];
override payloadExample = {
group_id: '123456',
bot_appid: '1234567890',
button_id: 'btn_1',
callback_data: '',
msg_seq: '10086'
};
override returnExample = {
};
async _handle (payload: PayloadType) {
return await this.core.apis.MsgApi.clickInlineKeyboardButton({
buttonId: payload.button_id,
peerId: payload.group_id.toString(),

View File

@@ -2,18 +2,33 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
const SchemaData = Type.Object({
rawData: Type.String(),
brief: Type.String(),
const PayloadSchema = Type.Object({
rawData: Type.String({ description: '原始数据' }),
brief: Type.String({ description: '简要描述' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class CreateCollection extends OneBotAction<Payload, unknown> {
const ReturnSchema = Type.Any({ description: '创建结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class CreateCollection extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.CreateCollection;
override payloadSchema = SchemaData;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '创建收藏';
override actionTags = ['扩展接口'];
override payloadExample = {
rawData: '收藏内容',
brief: '收藏标题'
};
override returnExample = {
result: 0,
errMsg: ''
};
async _handle (payload: Payload) {
async _handle (payload: PayloadType) {
return await this.core.apis.CollectionApi.createCollection(
this.core.selfInfo.uin,
this.core.selfInfo.uid,

View File

@@ -2,19 +2,34 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
lloc: Type.String(),
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
album_id: Type.String({ description: '相册ID' }),
lloc: Type.String({ description: '媒体ID (lloc)' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class DelGroupAlbumMedia extends OneBotAction<Payload, unknown> {
const ReturnSchema = Type.Any({ description: '删除结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class DelGroupAlbumMedia extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.DelGroupAlbumMedia;
override payloadSchema = SchemaData;
override actionSummary = '删除群相册媒体';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456',
album_id: 'album_id_1',
lloc: 'media_id_1',
};
override returnExample = {
result: {}
};
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: Payload) {
async _handle (payload: PayloadType) {
return await this.core.apis.WebApi.deleteAlbumMediaByNTQQ(
payload.group_id,
payload.album_id,

View File

@@ -2,20 +2,32 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
lloc: Type.String(),
content: Type.String(),
export const DoGroupAlbumCommentPayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
album_id: Type.String({ description: '相册 ID' }),
lloc: Type.String({ description: '图片 ID' }),
content: Type.String({ description: '评论内容' }),
});
type Payload = Static<typeof SchemaData>;
export type DoGroupAlbumCommentPayload = Static<typeof DoGroupAlbumCommentPayloadSchema>;
export class DoGroupAlbumComment extends OneBotAction<Payload, unknown> {
export class DoGroupAlbumComment extends OneBotAction<DoGroupAlbumCommentPayload, any> {
override actionName = ActionName.DoGroupAlbumComment;
override payloadSchema = SchemaData;
override actionSummary = '发表群相册评论';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456',
album_id: 'album_id_1',
lloc: 'media_id_1',
content: '很有意思'
};
override returnExample = {
result: {}
};
override payloadSchema = DoGroupAlbumCommentPayloadSchema;
override returnSchema = Type.Any({ description: '评论结果' });
async _handle (payload: Payload) {
async _handle (payload: DoGroupAlbumCommentPayload) {
return await this.core.apis.WebApi.doAlbumMediaPlainCommentByNTQQ(
payload.group_id,
payload.album_id,

View File

@@ -2,18 +2,32 @@ import { Type, Static } from '@sinclair/typebox';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
const SchemaData = Type.Object({
count: Type.Union([Type.Number(), Type.String()], { default: 48 }),
const PayloadSchema = Type.Object({
count: Type.Union([Type.Number(), Type.String()], { default: 48, description: '获取数量' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class FetchCustomFace extends OneBotAction<Payload, string[]> {
const ReturnSchema = Type.Array(Type.String(), { description: '表情URL列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class FetchCustomFace extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.FetchCustomFace;
override payloadSchema = SchemaData;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取自定义表情';
override actionTags = ['系统扩展'];
override payloadExample = {
count: 10
};
override returnExample = [
'http://example.com/face1.png'
];
async _handle (payload: Payload) {
async _handle (payload: PayloadType) {
const ret = await this.core.apis.MsgApi.fetchFavEmojiList(+payload.count);
return ret.emojiInfoList.map(e => e.url);
}
}

View File

@@ -2,28 +2,68 @@ import { Type, Static } from '@sinclair/typebox';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { MessageUnique } from 'napcat-common/src/message-unique';
import { type NTQQMsgApi } from 'napcat-core/apis';
const SchemaData = Type.Object({
message_id: Type.Union([Type.Number(), Type.String()]),
emojiId: Type.Union([Type.Number(), Type.String()]),
emojiType: Type.Union([Type.Number(), Type.String()]),
count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
const PayloadSchema = Type.Object({
message_id: Type.Union([Type.Number(), Type.String()], { description: '消息ID' }),
emojiId: Type.Union([Type.Number(), Type.String()], { description: '表情ID' }),
emojiType: Type.Union([Type.Number(), Type.String()], { description: '表情类型' }),
count: Type.Union([Type.Number(), Type.String()], { default: 20, description: '获取数量' }),
cookie: Type.String({ default: '', description: '分页Cookie' })
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class FetchEmojiLike extends OneBotAction<Payload, Awaited<ReturnType<NTQQMsgApi['getMsgEmojiLikesList']>>> {
const ReturnSchema = Type.Object({
emojiLikesList: Type.Array(Type.Object({
tinyId: Type.String({ description: 'TinyID' }),
nickName: Type.String({ description: '昵称' }),
headUrl: Type.String({ description: '头像URL' }),
}), { description: '表情回应列表' }),
cookie: Type.String({ description: '分页Cookie' }),
isLastPage: Type.Boolean({ description: '是否最后一页' }),
isFirstPage: Type.Boolean({ description: '是否第一页' }),
result: Type.Number({ description: '结果状态码' }),
errMsg: Type.String({ description: '错 误信息' }),
}, { description: '表情回应详情' });
type ReturnType = Static<typeof ReturnSchema>;
export class FetchEmojiLike extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.FetchEmojiLike;
override payloadSchema = SchemaData;
override actionSummary = '获取表情点赞详情';
override actionTags = ['消息扩展'];
override payloadExample = {
message_id: 12345,
emojiId: '123',
emojiType: 1,
count: 10,
cookie: ''
};
override returnExample = {
emojiLikesList: [
{
tinyId: '123456',
nickName: '测试用户',
headUrl: 'http://example.com/avatar.png'
}
],
cookie: '',
isLastPage: true,
isFirstPage: true,
result: 0,
errMsg: ''
};
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: Payload) {
async _handle (payload: PayloadType): Promise<ReturnType> {
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
if (!msgIdPeer) throw new Error('消息不存在');
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(msgIdPeer.Peer, [msgIdPeer.MsgId])).msgList[0];
if (!msg) throw new Error('消息不存在');
return await this.core.apis.MsgApi.getMsgEmojiLikesList(
msgIdPeer.Peer, msg.msgSeq, payload.emojiId.toString(), payload.emojiType.toString(), +payload.count
const res = await this.core.apis.MsgApi.getMsgEmojiLikesList(
msgIdPeer.Peer, msg.msgSeq, payload.emojiId.toString(), payload.emojiType.toString(), payload.cookie, +payload.count
);
return res;
}
}

View File

@@ -1,30 +1,46 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { AIVoiceChatType } from 'napcat-core/packet/entities/aiChat';
import { Type, Static } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
chat_type: Type.Union([Type.Union([Type.Number(), Type.String()])], { default: 1 }),
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
chat_type: Type.Union([Type.Number(), Type.String()], { default: 1, description: '聊天类型' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
interface GetAiCharactersResponse {
type: string;
characters: {
character_id: string;
character_name: string;
preview_url: string;
}[];
}
const ReturnSchema = Type.Array(
Type.Object({
type: Type.String({ description: '角色类型' }),
characters: Type.Array(
Type.Object({
character_id: Type.String({ description: '角色ID' }),
character_name: Type.String({ description: '角色名称' }),
preview_url: Type.String({ description: '预览URL' }),
}),
{ description: '角色列表' }
),
}),
{ description: 'AI角色列表' }
);
export class GetAiCharacters extends GetPacketStatusDepends<Payload, GetAiCharactersResponse[]> {
type ReturnType = Static<typeof ReturnSchema>;
export class GetAiCharacters extends GetPacketStatusDepends<PayloadType, ReturnType> {
override actionName = ActionName.GetAiCharacters;
override payloadSchema = SchemaData;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取AI角色列表';
override actionDescription = '获取群聊中的AI角色列表';
override actionTags = ['扩展接口'];
override payloadExample = ExtendsActionsExamples.GetAiCharacters.payload;
override returnExample = ExtendsActionsExamples.GetAiCharacters.response;
async _handle (payload: Payload) {
const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, +payload.chat_type as AIVoiceChatType);
async _handle (payload: PayloadType) {
const chatTypeNum = Number(payload.chat_type);
const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, chatTypeNum);
return rawList?.map((item) => ({
type: item.category,
characters: item.voices.map((voice) => ({

View File

@@ -1,12 +1,24 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { OneBotAction } from '../OneBotAction';
import { Type, Static } from '@sinclair/typebox';
interface GetClientkeyResponse {
clientkey?: string;
}
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
export class GetClientkey extends OneBotAction<void, GetClientkeyResponse> {
const ReturnSchema = Type.Object({
clientkey: Type.Optional(Type.String({ description: '客户端Key' })),
}, { description: '获取ClientKey结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetClientkey extends OneBotAction<void, ReturnType> {
override actionName = ActionName.GetClientkey;
override payloadSchema = Type.Object({});
override returnSchema = ReturnSchema;
override actionSummary = '获取ClientKey';
override actionDescription = '获取当前登录帐号的ClientKey';
override actionTags = ['扩展接口'];
override payloadExample = ExtendsActionsExamples.GetClientkey.payload;
override returnExample = ExtendsActionsExamples.GetClientkey.response;
async _handle () {
return { clientkey: (await this.core.apis.UserApi.forceFetchClientKey()).clientKey };

View File

@@ -1,20 +1,81 @@
import { type NTQQCollectionApi } from 'napcat-core/apis/collection';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
const SchemaData = Type.Object({
category: Type.Union([Type.Number(), Type.String()]),
count: Type.Union([Type.Union([Type.Number(), Type.String()])], { default: 1 }),
const PayloadSchema = Type.Object({
category: Type.String({ description: '分类ID' }),
count: Type.String({ default: '50', description: '获取数量' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class GetCollectionList extends OneBotAction<Payload, Awaited<ReturnType<NTQQCollectionApi['getAllCollection']>>> {
const ReturnSchema = Type.Any({ description: '收藏列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetCollectionList extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GetCollectionList;
override payloadSchema = SchemaData;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取收藏列表';
override actionTags = ['系统扩展'];
override payloadExample = {
category: '0',
count: '50'
};
override returnExample = {
errCode: 0,
errMsg: "",
collectionSearchList: {
collectionItemList: [
{
cid: "123456",
type: 8,
status: 1,
author: {
type: 2,
numId: "123456",
strId: "昵称",
groupId: "123456",
groupName: "群名",
uid: "123456"
},
bid: 1,
category: 1,
createTime: "1769169157000",
collectTime: "1769413477691",
modifyTime: "1769413477691",
sequence: "1769413476735",
shareUrl: "",
customGroupId: 0,
securityBeat: false,
summary: {
textSummary: null,
linkSummary: null,
gallerySummary: null,
audioSummary: null,
videoSummary: null,
fileSummary: null,
locationSummary: null,
richMediaSummary: {
title: "",
subTitle: "",
brief: "text",
picList: [],
contentType: 1,
originalUri: "",
publisher: "",
richMediaVersion: 0
}
}
}
],
hasMore: false,
bottomTimeStamp: "1769413477691"
}
};
async _handle (payload: Payload) {
async _handle (payload: PayloadType) {
return await this.core.apis.CollectionApi.getAllCollection(+payload.category, +payload.count);
}
}

View File

@@ -0,0 +1,88 @@
import { Type, Static } from '@sinclair/typebox';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { MessageUnique } from 'napcat-common/src/message-unique';
import { Peer, ChatType } from '@/napcat-core';
const PayloadSchema = Type.Object({
group_id: Type.Optional(Type.String({ description: '群号短ID可不传' })),
message_id: Type.String({ description: '消息ID可以传递长ID或短ID' }),
emoji_id: Type.String({ description: '表情ID' }),
emoji_type: Type.Optional(Type.String({ description: '表情类型' })),
count: Type.Number({ default: 0, description: '数量0代表全部' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
emoji_like_list: Type.Array(
Type.Object({
user_id: Type.String({ description: '点击者QQ号' }),
nick_name: Type.String({ description: '昵称?' }),
}),
{ description: '表情回应列表' }
),
});
type ReturnType = Static<typeof ReturnSchema>;
export class GetEmojiLikes extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GetEmojiLikes;
override actionSummary = '获取消息表情点赞列表';
override actionTags = ['消息扩展'];
override payloadExample = {
message_id: '12345',
emoji_id: '123'
};
override returnExample = {
emoji_like_list: [
{
user_id: '654321',
nick_name: '测试用户'
}
]
};
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: PayloadType) {
let peer: Peer;
let msgId: string;
if (MessageUnique.isShortId(payload.message_id)) {
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
if (!msgIdPeer) throw new Error('消息不存在');
peer = msgIdPeer.Peer;
msgId = msgIdPeer.MsgId;
} else {
if (!payload.group_id) throw new Error('长ID模式下必须提供群号');
peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id };
msgId = payload.message_id;
}
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId])).msgList[0];
if (!msg) throw new Error('消息不存在');
const emojiType = payload.emoji_type ?? (payload.emoji_id.length > 3 ? '2' : '1');
const emojiLikeList: Array<{ user_id: string; nick_name: string; }> = [];
let cookie = '';
let needFetchCount = payload.count == 0 ? 200 : Math.ceil(payload.count / 15);
for (let page = 0; page < needFetchCount; page++) {
const res = await this.core.apis.MsgApi.getMsgEmojiLikesList(
peer, msg.msgSeq, payload.emoji_id.toString(), emojiType, cookie, 15
);
if (Array.isArray(res.emojiLikesList)) {
for (const like of res.emojiLikesList) {
emojiLikeList.push({ user_id: like.tinyId, nick_name: like.nickName });
}
}
if (res.isLastPage || !res.cookie) break;
cookie = res.cookie;
}
// 切断多余部分
if (payload.count > 0) {
emojiLikeList.splice(payload.count);
}
return { emoji_like_list: emojiLikeList };
}
}

View File

@@ -1,12 +1,40 @@
import { OB11Construct } from '@/napcat-onebot/helper/data';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
import { OB11UserSchema } from '../schemas';
export class GetFriendWithCategory extends OneBotAction<void, unknown> {
const ReturnSchema = Type.Array(
Type.Object({
categoryId: Type.Number({ description: '分组ID' }),
categoryName: Type.String({ description: '分组名称' }),
categoryMbCount: Type.Number({ description: '分组内好友数量' }),
buddyList: Type.Array(OB11UserSchema, { description: '好友列表' }),
}),
{ description: '带分组的好友列表' }
);
type ReturnType = Static<typeof ReturnSchema>;
export class GetFriendWithCategory extends OneBotAction<void, ReturnType> {
override actionName = ActionName.GetFriendsWithCategory;
override payloadSchema = Type.Object({});
override returnSchema = ReturnSchema;
override actionSummary = '获取带分组的好友列表';
override actionTags = ['用户扩展'];
override payloadExample = {};
override returnExample = [
{
categoryId: 1,
categoryName: '我的好友',
categoryMbCount: 1,
buddyList: []
}
];
async _handle () {
return (await this.core.apis.FriendApi.getBuddyV2ExWithCate()).map(category => ({
const categories = await this.core.apis.FriendApi.getBuddyV2ExWithCate();
return categories.map(category => ({
...category,
buddyList: OB11Construct.friends(category.buddyList),
}));

View File

@@ -1,16 +1,51 @@
import { GroupNotifyMsgStatus } from 'napcat-core';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Notify } from '@/napcat-onebot/types';
import { Type, Static } from '@sinclair/typebox';
export default class GetGroupAddRequest extends OneBotAction<null, Notify[] | null> {
const ReturnSchema = Type.Array(
Type.Object({
request_id: Type.Number({ description: '请求ID' }),
invitor_uin: Type.Number({ description: '邀请者QQ' }),
invitor_nick: Type.Optional(Type.String({ description: '邀请者昵称' })),
group_id: Type.Number({ description: '群号' }),
message: Type.Optional(Type.String({ description: '验证信息' })),
group_name: Type.Optional(Type.String({ description: '群名称' })),
checked: Type.Boolean({ description: '是否已处理' }),
actor: Type.Number({ description: '处理者QQ' }),
requester_nick: Type.Optional(Type.String({ description: '请求者昵称' })),
}),
{ description: '群通知列表' }
);
type ReturnType = Static<typeof ReturnSchema>;
export default class GetGroupAddRequest extends OneBotAction<void, ReturnType> {
override actionName = ActionName.GetGroupIgnoreAddRequest;
override payloadSchema = Type.Object({});
override returnSchema = ReturnSchema;
override actionSummary = '获取群被忽略的加群请求';
override actionTags = ['群组接口'];
override payloadExample = {};
override returnExample = [
{
request_id: 12345,
invitor_uin: 123456789,
invitor_nick: '邀请者',
group_id: 123456789,
message: '加群请求',
group_name: '群名称',
checked: false,
actor: 0,
requester_nick: '请求者'
}
];
async _handle (): Promise<Notify[] | null> {
async _handle (): Promise<ReturnType> {
const NTQQUserApi = this.core.apis.UserApi;
const NTQQGroupApi = this.core.apis.GroupApi;
const ignoredNotifies = await NTQQGroupApi.getSingleScreenNotifies(true, 10);
const retData: Notify[] = [];
const retData: ReturnType = [];
const notifyPromises = ignoredNotifies
.filter(notify => notify.type === 7)

View File

@@ -2,19 +2,35 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
attach_info: Type.String({ default: '' }),
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
album_id: Type.String({ description: '相册ID' }),
attach_info: Type.String({ default: '', description: '附加信息(用于分页)' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class GetGroupAlbumMediaList extends OneBotAction<Payload, unknown> {
const ReturnSchema = Type.Any({ description: '相册媒体列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetGroupAlbumMediaList extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GetGroupAlbumMediaList;
override payloadSchema = SchemaData;
override actionSummary = '获取群相册媒体列表';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456',
album_id: 'album_id_1',
};
override returnExample = {
media_list: [
{ media_id: 'media_id_1', url: 'http://example.com/1.jpg' }
]
};
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: Payload) {
async _handle (payload: PayloadType) {
return await this.core.apis.WebApi.getAlbumMediaListByNTQQ(
payload.group_id,
payload.album_id,

View File

@@ -1,17 +1,30 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class GetGroupInfoEx extends OneBotAction<Payload, unknown> {
const ReturnSchema = Type.Any({ description: '群扩展信息' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetGroupInfoEx extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GetGroupInfoEx;
override payloadSchema = SchemaData;
override actionSummary = '获取群详细信息 (扩展)';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456'
};
override returnExample = {
};
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: Payload) {
async _handle (payload: PayloadType) {
return (await this.core.apis.GroupApi.getGroupExtFE0Info([payload.group_id.toString()])).result.groupExtInfos.get(payload.group_id.toString());
}
}

View File

@@ -1,57 +1,79 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { MiniAppInfo, MiniAppInfoHelper } from 'napcat-core/packet/utils/helper/miniAppHelper';
import { MiniAppData, MiniAppRawData, MiniAppReqCustomParams, MiniAppReqParams } from 'napcat-core/packet/entities/miniApp';
import { MiniAppReqCustomParams, MiniAppReqParams } from 'napcat-core/packet/entities/miniApp';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Union([
const PayloadSchema = Type.Union([
Type.Object({
type: Type.Union([Type.Literal('bili'), Type.Literal('weibo')]),
title: Type.String(),
desc: Type.String(),
picUrl: Type.String(),
jumpUrl: Type.String(),
webUrl: Type.Optional(Type.String()),
rawArkData: Type.Optional(Type.Union([Type.String()])),
type: Type.Union([Type.Literal('bili'), Type.Literal('weibo')], { description: '模板类型' }),
title: Type.String({ description: '标题' }),
desc: Type.String({ description: '描述' }),
picUrl: Type.String({ description: '图片URL' }),
jumpUrl: Type.String({ description: '跳转URL' }),
webUrl: Type.Optional(Type.String({ description: '网页URL' })),
rawArkData: Type.Optional(Type.Union([Type.String()], { description: '是否返回原始Ark数据' })),
}),
Type.Object({
title: Type.String(),
desc: Type.String(),
picUrl: Type.String(),
jumpUrl: Type.String(),
iconUrl: Type.String(),
webUrl: Type.Optional(Type.String()),
appId: Type.String(),
scene: Type.Union([Type.Number(), Type.String()]),
templateType: Type.Union([Type.Number(), Type.String()]),
businessType: Type.Union([Type.Number(), Type.String()]),
verType: Type.Union([Type.Number(), Type.String()]),
shareType: Type.Union([Type.Number(), Type.String()]),
versionId: Type.String(),
sdkId: Type.String(),
withShareTicket: Type.Union([Type.Number(), Type.String()]),
rawArkData: Type.Optional(Type.Union([Type.String()])),
title: Type.String({ description: '标题' }),
desc: Type.String({ description: '描述' }),
picUrl: Type.String({ description: '图片URL' }),
jumpUrl: Type.String({ description: '跳转URL' }),
iconUrl: Type.String({ description: '图标URL' }),
webUrl: Type.Optional(Type.String({ description: '网页URL' })),
appId: Type.String({ description: '小程序AppID' }),
scene: Type.String({ description: '场景ID' }),
templateType: Type.String({ description: '模板类型' }),
businessType: Type.String({ description: '业务类型' }),
verType: Type.String({ description: '版本类型' }),
shareType: Type.String({ description: '分享类型' }),
versionId: Type.String({ description: '版本ID' }),
sdkId: Type.String({ description: 'SDK ID' }),
withShareTicket: Type.String({ description: '是否携带分享票据' }),
rawArkData: Type.Optional(Type.String({ description: '是否返回原始Ark数据' })),
}),
]);
type Payload = Static<typeof SchemaData>;
], { description: '小程序Ark参数' });
export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
data: MiniAppData | MiniAppRawData
}> {
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
data: Type.Any({ description: 'Ark数据' }),
}, { description: '获取小程序Ark结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetMiniAppArk extends GetPacketStatusDepends<PayloadType, ReturnType> {
override actionName = ActionName.GetMiniAppArk;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema; override actionSummary = '获取小程序 Ark';
override actionTags = ['系统扩展'];
override payloadExample = {
type: 'bili',
title: '测试标题',
desc: '测试描述',
picUrl: 'http://example.com/pic.jpg',
jumpUrl: 'http://example.com'
};
override returnExample = {
data: {
ark: 'ark_content'
}
};
async _handle (payload: PayloadType) {
let reqParam: MiniAppReqParams;
const customParams = {
const customParams: MiniAppReqCustomParams = {
title: payload.title,
desc: payload.desc,
picUrl: payload.picUrl,
jumpUrl: payload.jumpUrl,
webUrl: payload.webUrl,
} as MiniAppReqCustomParams;
webUrl: payload.webUrl ?? '',
};
if ('type' in payload) {
reqParam = MiniAppInfoHelper.generateReq(customParams, MiniAppInfo.get(payload.type)!.template);
const template = MiniAppInfo.get(payload.type)?.template;
if (!template) {
throw new Error('未知的模板类型');
}
reqParam = MiniAppInfoHelper.generateReq(customParams, template);
} else {
const { appId, scene, iconUrl, templateType, businessType, verType, shareType, versionId, withShareTicket } = payload;
reqParam = MiniAppInfoHelper.generateReq(

View File

@@ -1,36 +1,65 @@
import { NTVoteInfo } from 'napcat-core';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
const SchemaData = Type.Object({
user_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
start: Type.Union([Type.Number(), Type.String()], { default: 0 }),
count: Type.Union([Type.Number(), Type.String()], { default: 10 }),
const PayloadSchema = Type.Object({
user_id: Type.Optional(Type.String({ description: 'QQ号' })),
start: Type.Union([Type.Number(), Type.String()], { default: 0, description: '起始位置' }),
count: Type.Union([Type.Number(), Type.String()], { default: 10, description: '获取数量' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class GetProfileLike extends OneBotAction<Payload, {
uid: string;
time: string;
favoriteInfo: {
userInfos: Array<NTVoteInfo>;
total_count: number;
last_time: number;
today_count: number;
};
voteInfo: {
total_count: number;
new_count: number;
new_nearby_count: number;
last_visit_time: number;
userInfos: Array<NTVoteInfo>;
};
}> {
const ReturnSchema = Type.Object({
uid: Type.String({ description: '用户UID' }),
time: Type.String({ description: '时间' }),
favoriteInfo: Type.Object({
userInfos: Type.Array(Type.Any(), { description: '点赞用户信息' }),
total_count: Type.Number({ description: '总点赞数' }),
last_time: Type.Number({ description: '最后点赞时间' }),
today_count: Type.Number({ description: '今日点赞数' }),
}),
voteInfo: Type.Object({
total_count: Type.Number({ description: '总点赞数' }),
new_count: Type.Number({ description: '新增点赞数' }),
new_nearby_count: Type.Number({ description: '新增附近点赞数' }),
last_visit_time: Type.Number({ description: '最后访问时间' }),
userInfos: Type.Array(Type.Any(), { description: '点赞用户信息' }),
}),
}, { description: '点赞详情' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetProfileLike extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GetProfileLike;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取资料点赞';
override actionTags = ['用户扩展'];
override payloadExample = {
user_id: '123456789',
start: 0,
count: 10
};
override returnExample = {
uid: 'u_123',
time: '1734567890',
favoriteInfo: {
userInfos: [],
total_count: 10,
last_time: 1734567890,
today_count: 5
},
voteInfo: {
total_count: 100,
new_count: 2,
new_nearby_count: 0,
last_visit_time: 1734567890,
userInfos: []
}
};
async _handle (payload: PayloadType): Promise<ReturnType> {
const isSelf = this.core.selfInfo.uin === payload.user_id || !payload.user_id;
const userUid = isSelf || !payload.user_id ? this.core.selfInfo.uid : await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
const type = isSelf ? 2 : 1;

View File

@@ -1,18 +1,37 @@
import { NTQQWebApi } from 'napcat-core/apis';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
import { NTQQWebApi } from 'napcat-core/apis';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class GetQunAlbumList extends OneBotAction<Payload, Awaited<ReturnType<NTQQWebApi['getAlbumListByNTQQ']>>['response']['album_list']> {
const ReturnSchema = Type.Array(Type.Any(), { description: '群相册列表' });
type GetQunAlbumListReturn = Awaited<globalThis.ReturnType<NTQQWebApi['getAlbumListByNTQQ']>>['response']['album_list'];
export class GetQunAlbumList extends OneBotAction<PayloadType, GetQunAlbumListReturn> {
override actionName = ActionName.GetQunAlbumList;
override payloadSchema = SchemaData;
override actionSummary = '获取群相册列表';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456',
};
override returnExample = [
{
album_id: 'album_1',
album_name: '测试相册',
cover_url: 'http://example.com/cover.jpg',
create_time: 1734567890
}
];
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: Payload) {
async _handle (payload: PayloadType): Promise<GetQunAlbumListReturn> {
return (await this.core.apis.WebApi.getAlbumListByNTQQ(payload.group_id)).response.album_list;
}
}

View File

@@ -1,8 +1,24 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { Type, Static } from '@sinclair/typebox';
export class GetRkey extends GetPacketStatusDepends<void, Array<unknown>> {
const ReturnSchema = Type.Array(Type.Any(), { description: 'Rkey列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetRkey extends GetPacketStatusDepends<void, ReturnType> {
override actionName = ActionName.GetRkey;
override payloadSchema = Type.Object({});
override returnSchema = ReturnSchema;
override actionSummary = '获取 RKey';
override actionTags = ['系统扩展'];
override payloadExample = {};
override returnExample = [
{
"key": "rkey_value",
"expired": 1734567890
}
];
async _handle () {
return await this.core.apis.PacketApi.pkt.operation.FetchRkey();

View File

@@ -1,8 +1,21 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
export class GetRobotUinRange extends OneBotAction<void, Array<unknown>> {
const ReturnSchema = Type.Array(Type.Any(), { description: '机器人Uin范围列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetRobotUinRange extends OneBotAction<void, ReturnType> {
override actionName = ActionName.GetRobotUinRange;
override actionSummary = '获取机器人 UIN 范围';
override actionTags = ['系统扩展'];
override payloadExample = {};
override returnExample = [
{ minUin: '12345678', maxUin: '87654321' }
];
override payloadSchema = Type.Object({});
override returnSchema = ReturnSchema;
async _handle () {
return await this.core.apis.UserApi.getRobotUinRange();

View File

@@ -2,26 +2,37 @@ import { PacketBuf } from 'napcat-core/packet/transformer/base';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { ProtoBuf, ProtoBufBase, PBUint32, PBString } from 'napcat.protobuf';
import { Type, Static } from '@sinclair/typebox';
interface Friend {
uin: number;
uid: string;
nick_name: string;
age: number;
source: string;
}
const ReturnSchema = Type.Array(
Type.Object({
uin: Type.Number({ description: 'QQ号' }),
uid: Type.String({ description: '用户UID' }),
nick_name: Type.String({ description: '昵称' }),
age: Type.Number({ description: '年龄' }),
source: Type.String({ description: '来源' }),
}),
{ description: '单向好友列表' }
);
interface Block {
str_uid: string;
bytes_source: string;
uint32_sex: number;
uint32_age: number;
bytes_nick: string;
uint64_uin: number;
}
type ReturnType = Static<typeof ReturnSchema>;
export class GetUnidirectionalFriendList extends OneBotAction<void, Friend[]> {
export class GetUnidirectionalFriendList extends OneBotAction<void, ReturnType> {
override actionName = ActionName.GetUnidirectionalFriendList;
override payloadSchema = Type.Object({});
override returnSchema = ReturnSchema;
override actionSummary = '获取单向好友列表';
override actionTags = ['用户扩展'];
override payloadExample = {};
override returnExample = [
{
uin: 123456789,
uid: 'u_123',
nick_name: '单向好友',
age: 20,
source: '来源'
}
];
async pack_data (data: string): Promise<Uint8Array> {
return ProtoBuf(class extends ProtoBufBase {
@@ -30,7 +41,7 @@ export class GetUnidirectionalFriendList extends OneBotAction<void, Friend[]> {
}).encode();
}
async _handle (): Promise<Friend[]> {
async _handle (): Promise<ReturnType> {
const self_id = this.core.selfInfo.uin;
const req_json = {
uint64_uin: self_id,
@@ -40,10 +51,18 @@ export class GetUnidirectionalFriendList extends OneBotAction<void, Friend[]> {
};
const packed_data = await this.pack_data(JSON.stringify(req_json));
const data = Buffer.from(packed_data);
const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as PacketBuf };
const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as unknown as PacketBuf };
const rsp_data = await this.core.apis.PacketApi.pkt.operation.sendPacket(rsq, true);
const block_json = ProtoBuf(class extends ProtoBufBase { data = PBString(4); }).decode(rsp_data);
const block_list: Block[] = JSON.parse(block_json.data).rpt_block_list;
interface BlockItem {
uint64_uin: number;
str_uid: string;
bytes_nick: string;
uint32_age: number;
bytes_source: string;
}
const block_data: { rpt_block_list: BlockItem[]; } = JSON.parse(block_json.data);
const block_list = block_data.rpt_block_list;
return block_list.map((block) => ({
uin: block.uint64_uin,

View File

@@ -2,17 +2,38 @@ import { ActionName } from '@/napcat-onebot/action/router';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
const PayloadSchema = Type.Object({
user_id: Type.String({ description: 'QQ号' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class GetUserStatus extends GetPacketStatusDepends<Payload, { status: number; ext_status: number; } | undefined> {
const ReturnSchema = Type.Object({
status: Type.Number({ description: '在线状态' }),
ext_status: Type.Number({ description: '扩展状态' }),
}, { description: '用户状态' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetUserStatus extends GetPacketStatusDepends<PayloadType, ReturnType> {
override actionName = ActionName.GetUserStatus;
override payloadSchema = SchemaData;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取用户在线状态';
override actionTags = ['系统扩展'];
override payloadExample = {
user_id: '123456789'
};
override returnExample = {
status: 10,
ext_status: 0
};
async _handle (payload: Payload) {
return await this.core.apis.PacketApi.pkt.operation.GetStrangerStatus(+payload.user_id);
async _handle (payload: PayloadType) {
const res = await this.core.apis.PacketApi.pkt.operation.GetStrangerStatus(+payload.user_id);
if (!res) {
throw new Error('无法获取用户状态');
}
return res;
}
}

View File

@@ -3,24 +3,38 @@ import { FileNapCatOneBotUUID } from 'napcat-common/src/file-uuid';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
file_id: Type.String(),
current_parent_directory: Type.String(),
target_parent_directory: Type.String(),
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
file_id: Type.String({ description: '文件ID' }),
current_parent_directory: Type.String({ description: '当前父目录' }),
target_parent_directory: Type.String({ description: '目标父目录' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
interface MoveGroupFileResponse {
ok: boolean;
}
const ReturnSchema = Type.Object({
ok: Type.Boolean({ description: '是否成功' }),
}, { description: '移动文件结果' });
export class MoveGroupFile extends GetPacketStatusDepends<Payload, MoveGroupFileResponse> {
type ReturnType = Static<typeof ReturnSchema>;
export class MoveGroupFile extends GetPacketStatusDepends<PayloadType, ReturnType> {
override actionName = ActionName.MoveGroupFile;
override payloadSchema = SchemaData;
override actionSummary = '移动群文件';
override actionTags = ['文件扩展'];
override payloadExample = {
group_id: '123456',
file_id: '/file_id',
current_parent_directory: '/current_folder_id',
target_parent_directory: '/target_folder_id',
};
override returnExample = {
ok: true
};
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: Payload) {
async _handle (payload: PayloadType) {
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (contextMsgFile?.fileUUID) {
await this.core.apis.PacketApi.pkt.operation.MoveGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.target_parent_directory);

View File

@@ -3,18 +3,29 @@ import { ActionName } from '@/napcat-onebot/action/router';
import { checkFileExist, uriToLocalFile } from 'napcat-common/src/file';
import fs from 'fs';
import { Static, Type } from '@sinclair/typebox';
import { GeneralCallResultStatus } from 'napcat-core';
const SchemaData = Type.Object({
image: Type.String(),
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
const PayloadSchema = Type.Object({
image: Type.String({ description: '图片路径、URL或Base64' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
class OCRImageBase extends OneBotAction<Payload, GeneralCallResultStatus> {
override payloadSchema = SchemaData;
const ReturnSchema = Type.Any({ description: 'OCR结果' });
async _handle (payload: Payload) {
type ReturnType = Static<typeof ReturnSchema>;
class OCRImageBase extends OneBotAction<PayloadType, ReturnType> {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '图片 OCR 识别';
override actionDescription = '识别图片中的文字内容(仅Windows端支持)';
override actionTags = ['扩展接口'];
override payloadExample = ExtendsActionsExamples.OCRImage.payload;
override returnExample = ExtendsActionsExamples.OCRImage.response;
async _handle (payload: PayloadType): Promise<ReturnType> {
const { path, success } = await uriToLocalFile(this.core.NapCatTempPath, payload.image);
if (!success) {
throw new Error(`OCR ${payload.image}失败, image字段可能格式不正确`);
@@ -37,8 +48,10 @@ class OCRImageBase extends OneBotAction<Payload, GeneralCallResultStatus> {
export class OCRImage extends OCRImageBase {
override actionName = ActionName.OCRImage;
override actionSummary = '图片 OCR 识别';
}
export class IOCRImage extends OCRImageBase {
override actionName = ActionName.IOCRImage;
override actionSummary = '图片 OCR 识别 (内部)';
}

View File

@@ -3,24 +3,38 @@ import { FileNapCatOneBotUUID } from 'napcat-common/src/file-uuid';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
file_id: Type.String(),
current_parent_directory: Type.String(),
new_name: Type.String(),
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
file_id: Type.String({ description: '文件ID' }),
current_parent_directory: Type.String({ description: '当前父目录' }),
new_name: Type.String({ description: '新文件名' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
interface RenameGroupFileResponse {
ok: boolean;
}
const ReturnSchema = Type.Object({
ok: Type.Boolean({ description: '是否成功' }),
}, { description: '重命名文件结果' });
export class RenameGroupFile extends GetPacketStatusDepends<Payload, RenameGroupFileResponse> {
type ReturnType = Static<typeof ReturnSchema>;
export class RenameGroupFile extends GetPacketStatusDepends<PayloadType, ReturnType> {
override actionName = ActionName.RenameGroupFile;
override payloadSchema = SchemaData;
override actionSummary = '重命名群文件';
override actionTags = ['文件扩展'];
override payloadExample = {
group_id: '123456',
file_id: '/file_id',
current_parent_directory: '/',
new_name: 'new_name.jpg'
};
override returnExample = {
ok: true
};
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: Payload) {
async _handle (payload: PayloadType) {
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (contextMsgFile?.fileUUID) {
await this.core.apis.PacketApi.pkt.operation.RenameGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.new_name);

View File

@@ -3,20 +3,35 @@ import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketS
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
cmd: Type.String(),
data: Type.String(),
rsp: Type.Union([Type.String(), Type.Boolean()], { default: true }),
const PayloadSchema = Type.Object({
cmd: Type.String({ description: '命令字' }),
data: Type.String({ description: '十六进制数据' }),
rsp: Type.Union([Type.String(), Type.Boolean()], { default: true, description: '是否等待响应' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class SendPacket extends GetPacketStatusDepends<Payload, string | undefined> {
override payloadSchema = SchemaData;
const ReturnSchema = Type.Union([Type.String({ description: '响应十六进制数据' }), Type.Undefined()], { description: '发包结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class SendPacket extends GetPacketStatusDepends<PayloadType, ReturnType> {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionName = ActionName.SendPacket;
async _handle (payload: Payload) {
override actionSummary = '发送原始数据包';
override actionTags = ['系统扩展'];
override payloadExample = {
cmd: 'Example.Cmd',
data: '123456',
rsp: true
};
override returnExample = '123456';
async _handle (payload: PayloadType) {
const rsp = typeof payload.rsp === 'boolean' ? payload.rsp : payload.rsp === 'true';
const data = await this.core.apis.PacketApi.pkt.operation.sendPacket({ cmd: payload.cmd, data: Buffer.from(payload.data, 'hex') as PacketBuf }, rsp);
const packetData = Buffer.from(payload.data, 'hex') as unknown as PacketBuf;
const data = await this.core.apis.PacketApi.pkt.operation.sendPacket({ cmd: payload.cmd, data: packetData }, rsp);
return typeof data === 'object' ? data.toString('hex') : undefined;
}
}

View File

@@ -2,19 +2,31 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
face_id: Type.Union([Type.Number(), Type.String()]), // 参考 face_config.json 的 QSid
face_type: Type.Union([Type.Number(), Type.String()], { default: '1' }),
wording: Type.String({ default: ' ' }),
const PayloadSchema = Type.Object({
face_id: Type.Union([Type.Number(), Type.String()], { description: '图标ID' }), // 参考 face_config.json 的 QSid
face_type: Type.Union([Type.Number(), Type.String()], { default: '1', description: '图标类型' }),
wording: Type.String({ default: ' ', description: '状态文字内容' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class SetDiyOnlineStatus extends OneBotAction<Payload, string> {
const ReturnSchema = Type.String({ description: '错误信息(如果有)' });
type ReturnType = Static<typeof ReturnSchema>;
export class SetDiyOnlineStatus extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetDiyOnlineStatus;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema; override actionSummary = '设置自定义在线状态';
override actionDescription = '设置自定义在线状态';
override actionTags = ['用户扩展'];
override payloadExample = {
face_id: '123',
face_type: '1',
wording: '自定义状态'
};
override returnExample = '';
async _handle (payload: PayloadType) {
const ret = await this.core.apis.UserApi.setDiySelfOnlineStatus(
payload.face_id.toString(),
payload.wording,

View File

@@ -2,19 +2,31 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
add_type: Type.Number(),
group_question: Type.Optional(Type.String()),
group_answer: Type.Optional(Type.String()),
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
add_type: Type.Number({ description: '加群方式' }),
group_question: Type.Optional(Type.String({ description: '加群问题' })),
group_answer: Type.Optional(Type.String({ description: '加群答案' })),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export default class SetGroupAddOption extends OneBotAction<Payload, null> {
const ReturnSchema = Type.Null({ description: '返回结果' });
type ReturnType = Static<typeof ReturnSchema>;
export default class SetGroupAddOption extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetGroupAddOption;
override payloadSchema = SchemaData;
async _handle (payload: Payload): Promise<null> {
override actionSummary = '设置群加群选项';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456',
add_type: 1,
};
override returnExample = null;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: PayloadType): Promise<ReturnType> {
const ret = await this.core.apis.GroupApi.setGroupAddOption(payload.group_id, {
addOption: payload.add_type,
groupQuestion: payload.group_question,

View File

@@ -2,21 +2,37 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
lloc: Type.String(),
id: Type.String(), // 421_1_0_1012959257|V61Yiali4PELg90bThrH4Bo2iI1M5Kab|V5bCgAxMDEyOTU5MjU3.PyqaPndPxg!^||^421_1_0_1012959257|V61Yiali4PELg90bThrH4Bo2iI1M5Kab|17560363448^||^1
set: Type.Boolean({ default: true }), // true=点赞 false=取消点赞 未实现
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
album_id: Type.String({ description: '相册ID' }),
lloc: Type.String({ description: '媒体ID (lloc)' }),
id: Type.String({ description: '点赞ID' }), // 421_1_0_1012959257|V61Yiali4PELg90bThrH4Bo2iI1M5Kab|V5bCgAxMDEyOTU5MjU3.PyqaPndPxg!^||^421_1_0_1012959257|V61Yiali4PELg90bThrH4Bo2iI1M5Kab|17560363448^||^1
set: Type.Boolean({ default: true, description: '是否点赞' }), // true=点赞 false=取消点赞 未实现
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class SetGroupAlbumMediaLike extends OneBotAction<Payload, unknown> {
const ReturnSchema = Type.Any({ description: '操作结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class SetGroupAlbumMediaLike extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetGroupAlbumMediaLike;
override payloadSchema = SchemaData;
override actionSummary = '点赞群相册媒体';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456',
album_id: 'album_id_1',
lloc: 'media_id_1',
id: '123456',
};
override returnExample = {
result: {}
};
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: Payload) {
async _handle (payload: PayloadType) {
return await this.core.apis.WebApi.doAlbumMediaLikeByNTQQ(
payload.group_id,
payload.album_id,

View File

@@ -2,19 +2,31 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
user_id: Type.Array(Type.String()),
reject_add_request: Type.Optional(Type.Union([Type.Boolean(), Type.String()])),
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
user_id: Type.Array(Type.String(), { description: 'QQ号列表' }),
reject_add_request: Type.Optional(Type.Union([Type.Boolean(), Type.String()], { description: '是否拒绝加群请求' })),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export default class SetGroupKickMembers extends OneBotAction<Payload, null> {
const ReturnSchema = Type.Null({ description: '返回结果' });
type ReturnType = Static<typeof ReturnSchema>;
export default class SetGroupKickMembers extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetGroupKickMembers;
override payloadSchema = SchemaData;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '批量踢出群成员';
override actionDescription = '从指定群聊中批量踢出多个成员';
override actionTags = ['扩展接口'];
override payloadExample = ExtendsActionsExamples.SetGroupKickMembers.payload;
override returnExample = ExtendsActionsExamples.SetGroupKickMembers.response;
async _handle (payload: Payload): Promise<null> {
async _handle (payload: PayloadType): Promise<ReturnType> {
const rejectReq = payload.reject_add_request?.toString() === 'true';
const uids: string[] = await Promise.all(payload.user_id.map(async uin => await this.core.apis.UserApi.getUidByUinV2(uin)));
await this.core.apis.GroupApi.kickMember(payload.group_id.toString(), uids.filter(uid => !!uid), rejectReq);

View File

@@ -2,17 +2,31 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
remark: Type.String(),
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
remark: Type.String({ description: '备注' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export default class SetGroupRemark extends OneBotAction<Payload, null> {
const ReturnSchema = Type.Null({ description: '返回结果' });
type ReturnType = Static<typeof ReturnSchema>;
export default class SetGroupRemark extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetGroupRemark;
override payloadSchema = SchemaData;
async _handle (payload: Payload): Promise<null> {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '设置群备注';
override actionDescription = '设置群备注';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456',
remark: '测试群备注'
};
override returnExample = null;
async _handle (payload: PayloadType): Promise<ReturnType> {
const ret = await this.core.apis.GroupApi.setGroupRemark(payload.group_id, payload.remark);
if (ret.result !== 0) {
throw new Error(`设置群备注失败, ${ret.result}:${ret.errMsg}`);

View File

@@ -2,18 +2,29 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
robot_member_switch: Type.Optional(Type.Number()),
robot_member_examine: Type.Optional(Type.Number()),
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
robot_member_switch: Type.Optional(Type.Number({ description: '机器人成员开关' })),
robot_member_examine: Type.Optional(Type.Number({ description: '机器人成员审核' })),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export default class SetGroupRobotAddOption extends OneBotAction<Payload, null> {
const ReturnSchema = Type.Null({ description: '返回结果' });
type ReturnType = Static<typeof ReturnSchema>;
export default class SetGroupRobotAddOption extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetGroupRobotAddOption;
override payloadSchema = SchemaData;
async _handle (payload: Payload): Promise<null> {
override actionSummary = '设置群机器人加群选项';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456'
};
override returnExample = null;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: PayloadType): Promise<ReturnType> {
const ret = await this.core.apis.GroupApi.setGroupRobotAddOption(
payload.group_id,
payload.robot_member_switch,

View File

@@ -2,18 +2,29 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
no_code_finger_open: Type.Optional(Type.Number()),
no_finger_open: Type.Optional(Type.Number()),
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
no_code_finger_open: Type.Optional(Type.Number({ description: '未知' })),
no_finger_open: Type.Optional(Type.Number({ description: '未知' })),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export default class SetGroupSearch extends OneBotAction<Payload, null> {
const ReturnSchema = Type.Null({ description: '返回结果' });
type ReturnType = Static<typeof ReturnSchema>;
export default class SetGroupSearch extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetGroupSearch;
override payloadSchema = SchemaData;
async _handle (payload: Payload): Promise<null> {
override actionSummary = '设置群搜索选项';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456'
};
override returnExample = null;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: PayloadType): Promise<ReturnType> {
const ret = await this.core.apis.GroupApi.setGroupSearch(payload.group_id, {
noCodeFingerOpenFlag: payload.no_code_finger_open,
noFingerOpenFlag: payload.no_finger_open,

View File

@@ -2,16 +2,27 @@ import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketS
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
class SetGroupSignBase extends GetPacketStatusDepends<Payload, void> {
override payloadSchema = SchemaData;
const ReturnSchema = Type.Void({ description: '打卡结果' });
async _handle (payload: Payload) {
type ReturnType = Static<typeof ReturnSchema>;
class SetGroupSignBase extends GetPacketStatusDepends<PayloadType, ReturnType> {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '群打卡';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456789'
};
override returnExample = null;
async _handle (payload: PayloadType) {
return await this.core.apis.PacketApi.pkt.operation.GroupSign(+payload.group_id);
}
}

View File

@@ -3,17 +3,30 @@ import { ActionName } from '@/napcat-onebot/action/router';
import { ChatType } from 'napcat-core';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
event_type: Type.Number(),
const PayloadSchema = Type.Object({
user_id: Type.String({ description: 'QQ号' }),
event_type: Type.Number({ description: '事件类型' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class SetInputStatus extends OneBotAction<Payload, unknown> {
const ReturnSchema = Type.Any({ description: '设置结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class SetInputStatus extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetInputStatus;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '设置输入状态';
override actionTags = ['系统扩展'];
override payloadExample = {
user_id: '123456789',
event_type: 1
};
override returnExample = null;
async _handle (payload: PayloadType) {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error('uid is empty');
const peer = {

View File

@@ -2,17 +2,29 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
longNick: Type.String(),
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
const PayloadSchema = Type.Object({
longNick: Type.String({ description: '签名内容' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class SetLongNick extends OneBotAction<Payload, unknown> {
const ReturnSchema = Type.Any({ description: '设置结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class SetLongNick extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetLongNick;
override payloadSchema = SchemaData;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '设置个性签名';
override actionDescription = '修改当前登录帐号的个性签名';
override actionTags = ['扩展接口'];
override payloadExample = ExtendsActionsExamples.SetLongNick.payload;
override returnExample = ExtendsActionsExamples.SetLongNick.response;
async _handle (payload: Payload) {
async _handle (payload: PayloadType) {
return await this.core.apis.UserApi.setLongNick(payload.longNick);
}
}

View File

@@ -2,19 +2,33 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
status: Type.Union([Type.Number(), Type.String()]),
ext_status: Type.Union([Type.Number(), Type.String()]),
battery_status: Type.Union([Type.Number(), Type.String()]),
const PayloadSchema = Type.Object({
status: Type.Union([Type.Number(), Type.String()], { description: '在线状态' }),
ext_status: Type.Union([Type.Number(), Type.String()], { description: '扩展状态' }),
battery_status: Type.Union([Type.Number(), Type.String()], { description: '电量状态' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export class SetOnlineStatus extends OneBotAction<Payload, null> {
const ReturnSchema = Type.Null({ description: '设置结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class SetOnlineStatus extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetOnlineStatus;
override payloadSchema = SchemaData;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '设置在线状态';
override actionDescription = statusText;
override actionTags = ['系统扩展'];
override payloadExample = {
status: 11,
ext_status: 0,
battery_status: 100
};
override returnExample = null;
async _handle (payload: Payload) {
async _handle (payload: PayloadType) {
const ret = await this.core.apis.UserApi.setSelfOnlineStatus(
+payload.status,
+payload.ext_status,
@@ -26,3 +40,216 @@ export class SetOnlineStatus extends OneBotAction<Payload, null> {
return null;
}
}
const statusText = `
## 状态列表
### 在线
\`\`\`json5;
{ "status": 10, "ext_status": 0, "battery_status": 0; }
\`\`\`
### Q我吧
\`\`\`json5;
{ "status": 60, "ext_status": 0, "battery_status": 0; }
\`\`\`
### 离开
\`\`\`json5;
{ "status": 30, "ext_status": 0, "battery_status": 0; }
\`\`\`
### 忙碌
\`\`\`json5;
{ "status": 50, "ext_status": 0, "battery_status": 0; }
\`\`\`
### 请勿打扰
\`\`\`json5;
{ "status": 70, "ext_status": 0, "battery_status": 0; }
\`\`\`
### 隐身
\`\`\`json5;
{ "status": 40, "ext_status": 0, "battery_status": 0; }
\`\`\`
### 听歌中
\`\`\`json5;
{ "status": 10, "ext_status": 1028, "battery_status": 0; }
\`\`\`
### 春日限定
\`\`\`json5;
{ "status": 10, "ext_status": 2037, "battery_status": 0; }
\`\`\`
### 一起元梦
\`\`\`json5;
{ "status": 10, "ext_status": 2025, "battery_status": 0; }
\`\`\`
### 求星搭子
\`\`\`json5;
{ "status": 10, "ext_status": 2026, "battery_status": 0; }
\`\`\`
### 被掏空
\`\`\`json5;
{ "status": 10, "ext_status": 2014, "battery_status": 0; }
\`\`\`
### 今日天气
\`\`\`json5;
{ "status": 10, "ext_status": 1030, "battery_status": 0; }
\`\`\`
### 我crash了
\`\`\`json5;
{ "status": 10, "ext_status": 2019, "battery_status": 0; }
\`\`\`
### 爱你
\`\`\`json5;
{ "status": 10, "ext_status": 2006, "battery_status": 0; }
\`\`\`
### 恋爱中
\`\`\`json5;
{ "status": 10, "ext_status": 1051, "battery_status": 0; }
\`\`\`
### 好运锦鲤
\`\`\`json5;
{ "status": 10, "ext_status": 1071, "battery_status": 0; }
\`\`\`
### 水逆退散
\`\`\`json5;
{ "status": 10, "ext_status": 1201, "battery_status": 0; }
\`\`\`
### 嗨到飞起
\`\`\`json5;
{ "status": 10, "ext_status": 1056, "battery_status": 0; }
\`\`\`
### 元气满满
\`\`\`json5;
{ "status": 10, "ext_status": 1058, "battery_status": 0; }
\`\`\`
### 宝宝认证
\`\`\`json5;
{ "status": 10, "ext_status": 1070, "battery_status": 0; }
\`\`\`
### 一言难尽
\`\`\`json5;
{ "status": 10, "ext_status": 1063, "battery_status": 0; }
\`\`\`
### 难得糊涂
\`\`\`json5;
{ "status": 10, "ext_status": 2001, "battery_status": 0; }
\`\`\`
### emo中
\`\`\`json5;
{ "status": 10, "ext_status": 1401, "battery_status": 0; }
\`\`\`
### 我太难了
\`\`\`json5;
{ "status": 10, "ext_status": 1062, "battery_status": 0; }
\`\`\`
### 我想开了
\`\`\`json5;
{ "status": 10, "ext_status": 2013, "battery_status": 0; }
\`\`\`
### 我没事
\`\`\`json5;
{ "status": 10, "ext_status": 1052, "battery_status": 0; }
\`\`\`
### 想静静
\`\`\`json5;
{ "status": 10, "ext_status": 1061, "battery_status": 0; }
\`\`\`
### 悠哉哉
\`\`\`json5;
{ "status": 10, "ext_status": 1059, "battery_status": 0; }
\`\`\`
### 去旅行
\`\`\`json5;
{ "status": 10, "ext_status": 2015, "battery_status": 0; }
\`\`\`
### 信号弱
\`\`\`json5;
{ "status": 10, "ext_status": 1011, "battery_status": 0; }
\`\`\`
### 出去浪
\`\`\`json5;
{ "status": 10, "ext_status": 2003, "battery_status": 0; }
\`\`\`
### 肝作业
\`\`\`json5;
{ "status": 10, "ext_status": 2012, "battery_status": 0; }
\`\`\`
### 学习中
\`\`\`json5;
{ "status": 10, "ext_status": 1018, "battery_status": 0; }
\`\`\`
### 搬砖中
\`\`\`json5;
{ "status": 10, "ext_status": 2023, "battery_status": 0; }
\`\`\`
### 摸鱼中
\`\`\`json5;
{ "status": 10, "ext_status": 1300, "battery_status": 0; }
\`\`\`
### 无聊中
\`\`\`json5;
{ "status": 10, "ext_status": 1060, "battery_status": 0; }
\`\`\`
### timi中
\`\`\`json5;
{ "status": 10, "ext_status": 1027, "battery_status": 0; }
\`\`\`
### 睡觉中
\`\`\`json5;
{ "status": 10, "ext_status": 1016, "battery_status": 0; }
\`\`\`
### 熬夜中
\`\`\`json5;
{ "status": 10, "ext_status": 1032, "battery_status": 0; }
\`\`\`
### 追剧中
\`\`\`json5;
{ "status": 10, "ext_status": 1021, "battery_status": 0; }
\`\`\`
### 我的电量
\`\`\`json5;
{
"status": 10,
"ext_status": 1000,
"battery_status": 0;
}
\`\`\`
`;

View File

@@ -4,16 +4,29 @@ import fs from 'node:fs/promises';
import { checkFileExist, uriToLocalFile } from 'napcat-common/src/file';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
file: Type.String(),
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
const PayloadSchema = Type.Object({
file: Type.String({ description: '图片路径、URL或Base64' }),
});
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
export default class SetAvatar extends OneBotAction<Payload, null> {
const ReturnSchema = Type.Null({ description: '设置结果' });
type ReturnType = Static<typeof ReturnSchema>;
export default class SetAvatar extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetQQAvatar;
override payloadSchema = SchemaData;
async _handle (payload: Payload): Promise<null> {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '设置QQ头像';
override actionDescription = '修改当前账号的QQ头像';
override actionTags = ['扩展接口'];
override payloadExample = ExtendsActionsExamples.SetQQAvatar.payload;
override returnExample = ExtendsActionsExamples.SetQQAvatar.response;
async _handle (payload: PayloadType): Promise<ReturnType> {
const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.file));
if (!success) {
throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`);
@@ -26,7 +39,7 @@ export default class SetAvatar extends OneBotAction<Payload, null> {
throw new Error(`头像${payload.file}设置失败,api无返回`);
}
// log(`头像设置返回:${JSON.stringify(ret)}`)
if (ret.result as number === 1004022) {
if (Number(ret.result) === 1004022) {
throw new Error(`头像${payload.file}设置失败,文件可能不是图片格式`);
} else if (ret.result !== 0) {
throw new Error(`头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`);

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