Compare commits

...

160 Commits

Author SHA1 Message Date
手瓜一十雪
38f6dad118 Lower NTQQ build check from 41679 to 39038
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Update NTQQFriendApi to use a lower minimum NTQQ build threshold (39038) when deciding which buddyListV2 API path to call. The change replaces occurrences of '41679' with '39038' in packages/napcat-core/apis/friend.ts (affecting getBuddyV2SimpleInfoMap and the buddyListV2 retrieval logic) to broaden compatibility with older NTQQ builds.
2026-02-03 22:56:12 +08:00
手瓜一十雪
c3b29f1ee6 Improve extension tab UI and add /plugin proxy
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Update dashboard extension UI to prevent title overflow: add truncate and max-width to the tab title, include the plugin name in the title tooltip, and hide the plugin-name label on small screens (visible from md+). Also add a '/plugin' proxy entry to the Vite dev server config so plugin requests are forwarded to the backend debug URL.
2026-02-03 18:58:40 +08:00
手瓜一十雪
3e85d18ab5 refactor: serviceworker
重构sw.js,实现更智能的缓存,根据路由设计缓存
2026-02-03 18:09:12 +08:00
手瓜一十雪
df48c01ce4 Add externalVersion and refine API types
Add externalVersion boolean to LoginInitConfig and pass false at initialization; bump appid qua for 9.9.27 entry. Refine several IPC/wrapper typings to use Promise return types, adjust pathIsReadableAndWriteable signature to accept a type and return Promise<number>, and update NodeIKernelNodeMiscService method signatures (styling) while adding getQimei36WithNewSdk(). These changes improve type accuracy for async/native calls and add a new SDK helper.
2026-02-03 15:59:31 +08:00
手瓜一十雪
209776a9e8 Cache QQ installer and handle win64 files
Update GitHub Actions release workflow to use a cached QQ x64 installer (updated NT_URL), add cache key generation and actions/cache usage, and implement a download-and-extract step that reuses the cache. Adjust temporary directory usage (WORK_TMPDIR/NODE_EXTRACT), add LightQuic.dll to copy targets, and copy specific win64 files into an OUT_DIR/win64 folder.

Update packages/napcat-develop/loadNapCat.cjs to recognize and copy win64 artifacts: add TARGET_WIN64_DIR, include LightQuic.dll, define win64ItemsToCopy (SSOShareInfoHelper64.dll, parent-ipc-core-x64.dll), validate their presence, ensure target directories, and copy win64 files into dist/win64. These changes speed CI by caching the installer and ensure required win64 DLLs are packaged alongside the main distribution.
2026-02-03 15:28:13 +08:00
Qiao
09dae7269a style(webui): 优化插件商店样式,使用固定头部 (#1585)
* fix: 修复 qq_login.tsx 类型错误

- onSelectionChange 的 key 参数可能为 null,添加空值检查

* style(webui): refactor plugin store layout with sticky header
2026-02-03 14:23:13 +08:00
Qiao
2dcf8004ab fix: 插件更新时保留 data 文件夹 (#1584)
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
* fix: 修复 qq_login.tsx 类型错误

- onSelectionChange 的 key 参数可能为 null,添加空值检查

* fix: 插件更新时保留 data 文件夹

- 更新插件时备份 data 文件夹,解压后恢复

- 添加异常处理,确保解压失败时也能恢复 data 文件夹
2026-02-03 09:55:21 +08:00
手瓜一十雪
74b1da67d8 Add password login support to web UI and backend
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Implement password-based QQ login across the stack: add a PasswordLogin React component, integrate it into the QQ login page, and add a frontend controller method to call a new /QQLogin/PasswordLogin API. On the backend, add QQPasswordLoginHandler, router entry, and WebUiDataRuntime hooks (setPasswordLoginCall / requestPasswordLogin) plus a default handler. Register a password login callback in the shell (base.ts) that calls the kernel login service, handles common error cases and falls back to QR code when needed. Update types to include onPasswordLoginRequested and adjust NodeIKernelLoginService method signatures (including passwordLogin return type changed to Promise<QuickLoginResult>) and minor formatting fixes.
2026-02-02 19:48:31 +08:00
手瓜一十雪
78ac36f670 Add plugin no-auth API routes and WebUI handling
Introduce support for plugin API routes that do not require WebUI authentication. Updates include:

- napcat-onebot: Add apiNoAuth route storage and helpers (apiNoAuth, getNoAuth, postNoAuth, putNoAuth, deleteNoAuth), hasApiNoAuthRoutes, buildApiNoAuthRouter, and clear handling in PluginRouterRegistryImpl.
- napcat-onebot types: Extend PluginRouterRegistry interface with no-auth API methods and document that authenticated APIs remain separate.
- napcat-webui-backend: Mount a new unauthenticated plugin route handler at /plugin/:pluginId/api that looks up the plugin router and dispatches requests to the plugin's no-auth router, returning appropriate errors when context or routes are missing.
- napcat-plugin-builtin: Add example no-auth endpoints (public/info and health) and update logger messages to reflect both auth and no-auth API paths.
- Bump napcat-types version to 0.0.16 and update napcat-plugin-builtin dependency accordingly.

These changes enable plugins to expose public endpoints (e.g. health checks or public metadata) under /plugin/{pluginId}/api/ while keeping existing authenticated APIs under /api/Plugin/ext/{pluginId}/.
2026-02-02 19:13:01 +08:00
手瓜一十雪
74781fda0a Cache object properties to avoid extra RPC
Serialize non-function properties of server-side objects as cachedProps so simple property reads don't require additional RPCs. Added cachedProps to SerializedValue, have RpcServer.storeObjectRef serialize and attach cachedProps (skipping functions), updated serializer to deserialize cachedProps and pass them to proxyCreator, and updated client proxy creation to accept cachedProps and return cached top-level properties directly. Tests updated to expect direct property access for serialized/simple objects and arrays.
2026-02-02 18:59:23 +08:00
手瓜一十雪
3bead89d46 Support object references and deep proxying
Introduce remote object references (refId) and deep proxy support across client, server, serializer and types. Key changes:

- Add refId propagation in client proxies so child proxies inherit and include refId on RPC requests.
- Extend serializer to handle a new SerializedValueType.OBJECT_REF, add refResolver and pass refId to proxyCreator.
- Server: store object references in a Map with generated ref IDs, resolve paths with optional refId, serialize results to OBJECT_REF when shouldProxyResult returns true, and release cleans up references. Add defaultShouldProxyResult heuristic to decide which return values should remain proxied (class instances and objects with methods).
- Types: add refId fields and ObjectRef shape, expose shouldProxyResult option on RpcServerOptions, and include refId in ProxyMeta and serialized values.
- Tests updated across the suite to expect proxied return values (arrays/objects/class instances) and to await property access or method calls; add comprehensive tests for deep return value proxying, chained calls, callbacks, constructors on returned proxies, and lifecycle of remote object proxies.

These changes enable returning live/proxied remote objects (including class instances and objects with methods) from RPC calls, preserving remote behavior and allowing subsequent operations to target the same server-side object.
2026-02-02 18:43:37 +08:00
手瓜一十雪
a4527fd8ca Add napcat-rpc package with deep RPC
Introduce a new napcat-rpc package implementing a deep-proxy RPC system. Adds client (createDeepProxy, proxy helpers), server (RpcServer, createRpcServer), serializer (serialize/deserialize, callback registry), and transport layers (LocalTransport, MessageTransport, message server handler), plus an easy API (createRpcPair, mockRemote, createServer, createClient). Includes TypeScript types, tsconfig and package.json. Wire-up: add package alias in napcat-schema vite config and add napcat-rpc dependency to napcat-test along with comprehensive rpc tests.
2026-02-02 17:12:05 +08:00
手瓜一十雪
52b6627ebd Validate pluginId and use localStorage token
Return a 400 error when the /call-plugin/:pluginId route is requested without a pluginId to avoid calling getPluginExports with an undefined id (packages/napcat-plugin-builtin/index.ts).

Update the dashboard UI to read the auth token from localStorage (same-origin) instead of relying on a URL parameter; a comment about legacy webui_token in the URL was added while the implementation currently prefers localStorage.getItem('token') (packages/napcat-plugin-builtin/webui/dashboard.html).
2026-02-02 16:17:03 +08:00
手瓜一十雪
a5769b6a62 Expose plugin pages at /plugin/:id/page/:path
Add a public route to serve plugin extension pages without auth and update related pieces accordingly. Backend: register GET /plugin/:pluginId/page/:pagePath to locate the plugin router, validate page and HTML file existence, and send the file (returns appropriate 4xx/5xx errors). Frontend: switch iframe and new-window URLs to the new unauthenticated route (remove webui_token usage). Builtin plugin: clarify page registration comment and add a log line for the extension page URL. Minor formatting whitespace tweaks in plugin manager type annotations.
2026-02-02 15:40:18 +08:00
手瓜一十雪
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
手瓜一十雪
2daddbb030 Refactor message API types and add elementId to file element
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Removed unnecessary type casting in NTQQMsgApi, added missing elementId property to fileElement in NTQQOnlineApi, and updated NodeIKernelMsgService to use SendMessageElement for sendMsg. Also standardized method signatures and formatting for improved type safety and consistency.
2026-01-22 17:59:11 +08:00
手瓜一十雪
6ec5bbeddf Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2026-01-22 17:45:13 +08:00
H3CoF6
75236dd50c Feat/Implement QQ Online File/Folder and Flash Transfer support (#1541)
* feat: implement QQ online file transfer and flash transfer support

* fix: change OnlineFile OB11Message data

* fix: add fileSize and isDir to OB11MessageOnlineFile

* fix: resolve typescript strict mode errors
2026-01-22 17:44:09 +08:00
手瓜一十雪
01958d47a4 Refactor type annotations and router initialization
Standardized type annotations for interfaces in user.ts and improved type safety in webapi.ts. Updated all Express router initializations to explicitly declare the Router type. Added missing RequestHandler typings in uploader modules for better type checking.
2026-01-22 17:35:54 +08:00
手瓜一十雪
772f07c58b Refactor DebugAdapter to extend IOB11NetworkAdapter
Refactored DebugAdapter to inherit from IOB11NetworkAdapter, improving integration with the OneBot network manager. Enhanced WebSocket client management, error handling, and adapter lifecycle. Updated API and WebSocket handlers for better type safety and reliability. This change prepares the debug adapter for more robust and maintainable debugging sessions.
2026-01-22 16:22:18 +08:00
手瓜一十雪
0f9647bf64 Update ffmpegAddon binary for darwin arm64
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Replaces the ffmpegAddon.darwin.arm64.node binary with a new version, likely to include bug fixes or performance improvements for Apple Silicon platforms.
2026-01-22 14:53:58 +08:00
手瓜一十雪
8197ebcbcf Add build script for plugin-builtin package
Introduces a new npm script 'build:plugin-builtin' to build the napcat-plugin-builtin package using pnpm.
2026-01-22 14:51:21 +08:00
手瓜一十雪
d0519feb4f Update loadQQWrapper to accept QQMainPath argument
Modified the call to loadQQWrapper to pass both QQMainPath and the full QQ version from basicInfoWrapper. This change likely aligns with an updated function signature for loadQQWrapper requiring the main path as an additional parameter.
2026-01-22 14:49:17 +08:00
手瓜一十雪
d43c6b10a3 feat: 修复mac问题 2026-01-22 14:42:42 +08:00
手瓜一十雪
857be5ee49 Add uptime display to version info message
Introduced a function to format and display the application's uptime in the version information message. This provides users with additional context about how long the application has been running.
2026-01-22 14:22:04 +08:00
手瓜一十雪
af8005dd6f feat: wavf32le 2026-01-22 14:04:17 +08:00
手瓜一十雪
6e8adad7ca Improve layout and styling of NewVersionTip component
Added flexbox classes to center the update tip, adjusted Chip component styles for better alignment, and set a minimum width. Spinner size and alignment were also refined for consistency.
2026-01-22 13:41:01 +08:00
手瓜一十雪
0f8584b8e1 Refine update check logic and UI styling
Updated the shell's named pipe connection logic to better handle environment variables. Improved the system info component's update notification UI for better alignment and spinner sizing.
2026-01-22 13:39:44 +08:00
时瑾
37f40a2635 feat: support msg_seq parameter in reply message construction (#1529)
* feat: support msg_seq parameter in reply message construction

- Add optional 'seq' parameter to OB11MessageReply for using msg_seq
- Prioritize seq over id for querying reply messages
- Maintain backward compatibility with existing id parameter
- Update type definitions across backend and frontend
- Update validation schemas for message nodes

close #1523

* Update debug button label in NetworkDisplayCard

Changed the button label from '关闭调试'/'开启调试' to '默认'/'调试' based on the debug state for improved clarity.

---------

Co-authored-by: 手瓜一十雪 <nanaeonn@outlook.com>
2026-01-22 13:20:32 +08:00
手瓜一十雪
1b4d604e32 Add support for preloading Node Addons via env var
Introduces logic to preload a Node Addon in the worker process if the NAPCAT_PRELOAD_NODE_ADDON_PATH environment variable is set. Logs success or failure of the preload operation for better debugging and flexibility.
2026-01-22 12:55:34 +08:00
手瓜一十雪
81a0c07922 Revert "Add manual trigger to auto-release workflow"
This reverts commit d25bd65b2d.
2026-01-22 12:53:52 +08:00
手瓜一十雪
a8cb6b5865 Revert "Add support for CJS environment loader in main entry"
This reverts commit 711a060dd9.
2026-01-22 12:53:11 +08:00
手瓜一十雪
d25bd65b2d Add manual trigger to auto-release workflow
Enables the auto-release workflow to be triggered manually using workflow_dispatch, in addition to running on published releases.
2026-01-22 12:13:14 +08:00
手瓜一十雪
e510a75f0c Add workflow to trigger NapCat AppImage release
Introduces a new GitHub Actions step to trigger the Release NapCat AppImage workflow. This step uses hardcoded QQ AppImage URLs for both x86_64 and arm64 architectures and passes the latest NapCat version as input.
2026-01-22 12:12:38 +08:00
吴天一
e3c6048a7f Rename OB11MessageContext to OB11MessageContact (#1540) 2026-01-22 12:06:59 +08:00
手瓜一十雪
789c72d4cf Trigger docker-publish workflow in auto-release
Added a POST request to dispatch the docker-publish workflow alongside the release workflow in the auto-release GitHub Actions workflow. This ensures both workflows are triggered with the same input parameters.

Fix indentation for curl command in workflow

Corrected the indentation of a curl command in the auto-release GitHub Actions workflow to ensure proper execution of the job steps.
2026-01-22 12:05:40 +08:00
手瓜一十雪
711a060dd9 Add support for CJS environment loader in main entry
Checks for the NAPCAT_NODE_CJS_ENV_LOADER_PATH environment variable and loads the specified CommonJS environment loader if present. Logs success or failure and exits on error. This allows for custom environment setup before continuing with the main process.
2026-01-22 11:58:13 +08:00
手瓜一十雪
6268923f01 Fix import path for connectToNamedPipe in base.ts
Some checks failed
Build NapCat Artifacts / Build-Framework (push) Has been cancelled
Build NapCat Artifacts / Build-Shell (push) Has been cancelled
Updated the import statement for connectToNamedPipe to use a relative path instead of an alias. This resolves issues with module resolution in the napcat-shell package.
2026-01-18 14:50:34 +08:00
手瓜一十雪
f6b9017429 Enable named pipe connection with multi-process check
Restores the named pipe connection in NCoreInitShell, now gated by both NAPCAT_DISABLE_PIPE and NAPCAT_DISABLE_MULTI_PROCESS environment variables. This ensures the pipe is only connected when multi-process is enabled.
2026-01-18 14:42:00 +08:00
手瓜一十雪
178e51bbb8 Reduce mirror timeouts and use fast mirror cache
Decreased default and test timeouts for mirrors to improve responsiveness. Updated logic in getAllGitHubTags and getWorkflowRunsFromHtml to use cached fast mirror lists instead of static lists for better performance.
2026-01-18 14:34:08 +08:00
手瓜一十雪
8a232d8c68 Support preferred WebUI port via environment variable
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Adds support for specifying a preferred WebUI port using the NAPCAT_WEBUI_PREFERRED_PORT environment variable. The shell and backend now coordinate to pass and honor this port during worker restarts, falling back to the default port if the preferred one is unavailable.
2026-01-18 12:19:03 +08:00
手瓜一十雪
7216755430 Refactor process management and improve shutdown logic
Removed excessive logging and streamlined process restart and shutdown flows in napcat.ts. Added isShuttingDown flag to prevent unintended worker restarts during shutdown. Improved forceKillProcess to handle Windows-specific process termination. Updated IWorkerProcess interface and implementations to include the 'off' event method for better event management.
2026-01-18 12:10:14 +08:00
手瓜一十雪
0c91f9c66b Remove retry logic from tryUsePort function
Simplified the tryUsePort function by removing the retryCurrentCount parameter and associated retry logic. Now, if a port is in use, the function increments the port number and retries up to MAX_PORT_TRY times without waiting between attempts.
2026-01-18 11:45:40 +08:00
手瓜一十雪
e8855a59b0 Improve alignment in system info and status components
Adjusted flex alignment and added 'items-baseline' and 'self-center' classes to enhance vertical alignment of icons and content in SystemInfoItem and SystemStatusItem components for better UI consistency.
2026-01-18 11:25:06 +08:00
手瓜一十雪
5de2664af4 Support passing JWT secret key on worker restart
Added the ability to pass a JWT secret key when restarting the worker process by updating environment variable handling and message passing. Improved port retry logic in the backend to allow multiple attempts on the same port before incrementing. Also refactored process API to use getter for pid property.

Ensure Electron app is ready before creating process manager

Adds a check to await electron.app.whenReady() if the Electron app is not yet ready before instantiating the ElectronProcessManager. This prevents potential issues when accessing Electron APIs before the app is fully initialized.

Add mirror selection support for version updates

Introduces the ability to specify and select GitHub mirror sources for fetching tags, releases, and action artifacts throughout the backend and frontend. Updates API endpoints, internal helper functions, and UI components to allow users to choose a mirror for version queries and updates, improving reliability in regions with limited GitHub access. Also enhances version comparison logic and improves artifact metadata display.

Refactor artifact fetching to use HTML parsing only

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

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

* feat: 新增看门狗汪汪汪

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

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

* fix: 刷新二维码清楚错误信息
2026-01-17 15:38:24 +08:00
手瓜一十雪
822f683a14 Disable multi-process in development environment
Set NAPCAT_DISABLE_MULTI_PROCESS environment variable to '1' to disable restart and multi-process features during development.
2026-01-17 15:12:30 +08:00
手瓜一十雪
f4d3d33954 Remove explicit status code from sendError calls
Eliminated the explicit 500 status code parameter from sendError calls in RestartProcessHandler, allowing sendError to use its default behavior.
2026-01-17 15:10:21 +08:00
手瓜一十雪
d1abf788a5 Remove redundant comments in worker process handler
Cleaned up unnecessary comments in the message handler for process restart and shutdown signals to improve code readability.
2026-01-17 15:08:39 +08:00
手瓜一十雪
9ba6b2ed40 Remove redundant worker creation log statements
Deleted duplicate and unnecessary log messages related to worker process creation and spawning to reduce log clutter.
2026-01-17 15:06:44 +08:00
手瓜一十雪
3a880e389b Refactor process management with unified API
Introduces a new process-api.ts module to abstract process management for both Electron and Node.js environments. Refactors napcat.ts to use this unified API, improving clarity and maintainability of worker/master process logic, restart handling, and environment detection. Removes unused import from base.ts.
2026-01-17 15:02:54 +08:00
手瓜一十雪
1c7ac42a46 Add support to disable multi-process and named pipe via env
Introduces NAPCAT_DISABLE_MULTI_PROCESS and NAPCAT_DISABLE_PIPE environment variables to allow disabling multi-process mode and named pipe connection, respectively. Also simplifies process termination logic by always using SIGKILL.
2026-01-17 14:46:57 +08:00
手瓜一十雪
3e8b575015 Add process restart feature via WebUI
Introduces backend and frontend support for restarting the worker process from the WebUI. Adds API endpoint, controller, and UI button for process management. Refactors napcat-shell to support master/worker process lifecycle and restart logic.
2026-01-17 14:42:07 +08:00
手瓜一十雪
7c22170e1e Add support for version 9.9.26-44725
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Updated appid.json, napi2native.json, and packet.json to include entries for version 9.9.26-44725, adding relevant appid, qua, send, and recv values.
2026-01-16 17:25:29 +08:00
手瓜一十雪
f143da6ba8 Revert "Update pnpm install to use --no-frozen-lockfile"
Some checks failed
Build NapCat Artifacts / Build-Framework (push) Has been cancelled
Build NapCat Artifacts / Build-Shell (push) Has been cancelled
This reverts commit d0d3934869.
2026-01-15 11:19:15 +08:00
手瓜一十雪
d0d3934869 Update pnpm install to use --no-frozen-lockfile
Replaces 'pnpm i' with 'pnpm i --no-frozen-lockfile' in build and release GitHub workflows to allow installation even if lockfile changes are detected. This helps prevent CI failures due to lockfile mismatches.
2026-01-15 11:15:38 +08:00
手瓜一十雪
808165b008 Add napi2native mapping for 3.2.21-42086-arm64
Introduced native address mappings for the 3.2.21-42086-arm64 version, including 'send' and 'recv' function offsets.
2026-01-15 10:53:58 +08:00
手瓜一十雪
d23785f34d Add isActive property to plugin adapters
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Introduces an isActive getter to OB11PluginAdapter and OB11PluginMangerAdapter, which returns true only if the adapter is enabled and has loaded plugins. Updates event emission logic to use isActive instead of isEnable, ensuring events are only sent to active adapters.
2026-01-14 18:53:32 +08:00
手瓜一十雪
31daf41135 Add onLoginRecordUpdate method to listener
Introduces the onLoginRecordUpdate method to NodeIKernelLoginListener, preparing for future handling of login record updates.
2026-01-14 18:53:31 +08:00
手瓜一十雪
a2450b72be Refactor network adapter activation and message handling
Introduces isActive property to network adapters for more accurate activation checks, refactors message dispatch logic to use only active adapters, and improves heartbeat management for WebSocket adapters. Also sets default enableWebsocket to false in config and frontend forms, and adds a security dialog for missing tokens in the web UI.
2026-01-14 18:53:31 +08:00
418 changed files with 26572 additions and 7092 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:
@@ -80,4 +137,17 @@ jobs:
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/NapNeko/NapCatLinuxNodeLoader/actions/workflows/release.yml/dispatches \
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_url_amd64\":\"${QQ_VERSION_X86_64}\",\"qq_url_arm64\":\"${QQ_VERSION_ARM64}\"}}"
- name: Trigger Release NapCat AppImage Workflow
env:
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
NAPCAT_VERSION: ${{ env.latest_tag }}
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
run: |
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_url_amd64=${QQ_VERSION_X86_64}, qq_url_arm64=${QQ_VERSION_ARM64}"
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/NapNeko/NapCatLinuxNodeLoader/actions/workflows/docker-publish.yml/dispatches \
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_url_amd64\":\"${QQ_VERSION_X86_64}\",\"qq_url_arm64\":\"${QQ_VERSION_ARM64}\"}}"

View File

@@ -41,6 +41,7 @@ jobs:
pnpm test || exit 1
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:framework
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-framework/dist framework-dist
cd framework-dist
npm install --omit=dev
@@ -83,6 +84,7 @@ jobs:
pnpm test || exit 1
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:shell
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-shell/dist shell-dist
cd shell-dist
npm install --omit=dev

View File

@@ -10,7 +10,7 @@ permissions: write-all
env:
OPENROUTER_API_URL: https://91vip.futureppo.top/v1/chat/completions
OPENROUTER_MODEL: "copilot/ant/gemini-3-flash-preview"
OPENROUTER_MODEL: "copilot/gemini-3-flash-preview"
RELEASE_NAME: "NapCat"
jobs:
@@ -62,6 +62,7 @@ jobs:
pnpm i
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:framework
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-framework/dist framework-dist
cd framework-dist
npm install --omit=dev
@@ -91,6 +92,7 @@ jobs:
pnpm i
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:shell
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-shell/dist shell-dist
cd shell-dist
npm install --omit=dev
@@ -123,18 +125,54 @@ jobs:
cd "$TMPDIR"
# -----------------------------
# 1) 下载 QQ x64
# 1) 下载 QQ x64 (使用缓存)
# -----------------------------
# JS_URL="https://cdn-go.cn/qq-web/im.qq.com_new/latest/rainbow/windowsConfig.js"
# JS_URL="https://slave.docadan488.workers.dev/proxy?url=https://cdn-go.cn/qq-web/im.qq.com_new/latest/rainbow/windowsConfig.js"
# NT_URL=$(curl -fsSL "$JS_URL" | grep -oP '"ntDownloadX64Url"\s*:\s*"\K[^"]+')
NT_URL="https://dldir1v6.qq.com/qqfile/qq/QQNT/eb263b35/QQ9.9.23.42086_x64.exe"
NT_URL="https://dldir1v6.qq.com/qqfile/qq/QQNT/32876254/QQ9.9.27.45627_x64.exe"
QQ_ZIP="$(basename "$NT_URL")"
aria2c -x16 -s16 -k1M -o "$QQ_ZIP" "$NT_URL"
# 根据 URL 生成缓存键
QQ_CACHE_KEY="qq-x64-$(echo "$NT_URL" | md5sum | cut -d' ' -f1)"
echo "QQ_CACHE_KEY=$QQ_CACHE_KEY" >> $GITHUB_ENV
echo "QQ_ZIP=$QQ_ZIP" >> $GITHUB_ENV
echo "NT_URL=$NT_URL" >> $GITHUB_ENV
- name: Cache QQ x64 Installer
id: cache-qq
uses: actions/cache@v4
with:
path: ~/qq-cache
key: ${{ env.QQ_CACHE_KEY }}
- name: Download and Extract QQ x64
run: |
set -euo pipefail
TMPDIR=$(mktemp -d)
cd "$TMPDIR"
QQ_CACHE_DIR="$HOME/qq-cache"
mkdir -p "$QQ_CACHE_DIR"
if [ -f "$QQ_CACHE_DIR/$QQ_ZIP" ]; then
echo "Using cached QQ installer: $QQ_ZIP"
cp "$QQ_CACHE_DIR/$QQ_ZIP" "$QQ_ZIP"
else
echo "Downloading QQ installer: $QQ_ZIP"
aria2c -x16 -s16 -k1M -o "$QQ_ZIP" "$NT_URL"
cp "$QQ_ZIP" "$QQ_CACHE_DIR/$QQ_ZIP"
fi
QQ_EXTRACT="$TMPDIR/qq_extracted"
mkdir -p "$QQ_EXTRACT"
7z x -y -o"$QQ_EXTRACT" "$QQ_ZIP" >/dev/null
echo "QQ_EXTRACT=$QQ_EXTRACT" >> $GITHUB_ENV
echo "WORK_TMPDIR=$TMPDIR" >> $GITHUB_ENV
- name: Download Node.js and Assemble NapCat.Shell.Windows.Node.zip
run: |
set -euo pipefail
cd "$WORK_TMPDIR"
# -----------------------------
# 2) 下载 Node.js Windows x64 zip 22.11.0
@@ -144,7 +182,7 @@ jobs:
NODE_ZIP="node-v$NODE_VER-win-x64.zip"
aria2c -x1 -s1 -k1M -o "$NODE_ZIP" "$NODE_URL"
NODE_EXTRACT="$TMPDIR/node_extracted"
NODE_EXTRACT="$WORK_TMPDIR/node_extracted"
mkdir -p "$NODE_EXTRACT"
unzip -q "$NODE_ZIP" -d "$NODE_EXTRACT"
@@ -162,11 +200,18 @@ jobs:
# -----------------------------
# 5) 拷贝 QQ 文件到 NapCat.Shell.Windows.Node
# -----------------------------
QQ_TARGETS=("avif_convert.dll" "broadcast_ipc.dll" "config.json" "libglib-2.0-0.dll" "libgobject-2.0-0.dll" "libvips-42.dll" "ncnn.dll" "opencv.dll" "package.json" "QBar.dll" "wrapper.node")
QQ_TARGETS=("avif_convert.dll" "broadcast_ipc.dll" "config.json" "libglib-2.0-0.dll" "libgobject-2.0-0.dll" "libvips-42.dll" "ncnn.dll" "opencv.dll" "package.json" "QBar.dll" "wrapper.node" "LightQuic.dll")
for name in "${QQ_TARGETS[@]}"; do
find "$QQ_EXTRACT" -iname "$name" -exec cp -a {} "$OUT_DIR" \; || true
done
# -----------------------------
# 5.1) 拷贝 win64 目录下的文件
# -----------------------------
mkdir -p "$OUT_DIR/win64"
find "$QQ_EXTRACT" -ipath "*/win64/SSOShareInfoHelper64.dll" -exec cp -a {} "$OUT_DIR/win64/" \; || true
find "$QQ_EXTRACT" -ipath "*/win64/parent-ipc-core-x64.dll" -exec cp -a {} "$OUT_DIR/win64/" \; || true
# -----------------------------
# 6) 拷贝仓库文件 napcat.bat 和 index.js
# -----------------------------
@@ -176,6 +221,7 @@ jobs:
# -----------------------------
# 7) 拷贝 Node.exe 到 NapCat.Shell.Windows.Node
# -----------------------------
NODE_VER="22.11.0"
cp -a "$NODE_EXTRACT/node-v$NODE_VER-win-x64/node.exe" "$OUT_DIR/" || true
- name: Upload Artifact

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,8 +6,10 @@
"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",
"dev:shell": "pnpm --filter napcat-develop run dev || exit 1",
"typecheck": "pnpm -r --if-present run typecheck",
"test": "pnpm --filter napcat-test run test",

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

@@ -220,13 +220,13 @@ export function parseAppidFromMajor (nodeMajor: string): string | undefined {
// ============== GitHub Tags 获取 ==============
// 使用 mirror 模块统一管理镜像
export async function getAllTags (): Promise<{ tags: string[], mirror: string; }> {
return getAllTagsFromMirror('NapNeko', 'NapCatQQ');
export async function getAllTags (mirror?: string): Promise<{ tags: string[], mirror: string; }> {
return getAllTagsFromMirror('NapNeko', 'NapCatQQ', mirror);
}
export async function getLatestTag (): Promise<string> {
const { tags } = await getAllTags();
export async function getLatestTag (mirror?: string): Promise<string> {
const { tags } = await getAllTags(mirror);
// 使用 SemVer 规范排序
tags.sort((a, b) => compareSemVer(a, b));

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

@@ -23,66 +23,50 @@ import { PromiseTimer } from './helper';
* 懒加载测速:首次使用时自动测速,缓存 30 分钟
*/
export const GITHUB_FILE_MIRRORS = [
// 延迟 < 800ms 的最快镜像
'https://github.chenc.dev/', // 666ms
'https://ghproxy.cfd/', // 719ms - 支持重定向
'https://github.tbedu.top/', // 760ms
'https://ghps.cc/', // 768ms
'https://gh.llkk.cc/', // 774ms
'https://ghproxy.cc/', // 777ms
'https://gh.monlor.com/', // 779ms
'https://cdn.akaere.online/', // 784ms
// 延迟 800-1000ms 的快速镜像
'https://gh.idayer.com/', // 869ms
'https://gh-proxy.net/', // 885ms
'https://ghpxy.hwinzniej.top/', // 890ms
'https://github-proxy.memory-echoes.cn/', // 896ms
'https://git.yylx.win/', // 917ms
'https://gitproxy.mrhjx.cn/', // 950ms
'https://jiashu.1win.eu.org/', // 954ms
'https://ghproxy.cn/', // 981ms
// 延迟 1000-1500ms 的中速镜像
'https://gh.fhjhy.top/', // 1014ms
'https://gp.zkitefly.eu.org/', // 1015ms
'https://gh-proxy.com/', // 1022ms
'https://hub.gitmirror.com/', // 1027ms
'https://ghfile.geekertao.top/', // 1029ms
'https://j.1lin.dpdns.org/', // 1037ms
'https://ghproxy.imciel.com/', // 1047ms
'https://github-proxy.teach-english.tech/', // 1047ms
'https://gh.927223.xyz/', // 1071ms
'https://github.ednovas.xyz/', // 1099ms
'https://ghf.xn--eqrr82bzpe.top/',// 1122ms
'https://gh.dpik.top/', // 1131ms
'https://gh.jasonzeng.dev/', // 1139ms
'https://gh.xxooo.cf/', // 1157ms
'https://gh.bugdey.us.kg/', // 1228ms
'https://ghm.078465.xyz/', // 1289ms
'https://j.1win.ggff.net/', // 1329ms
'https://tvv.tw/', // 1393ms
'https://gh.chjina.com/', // 1446ms
'https://gitproxy.127731.xyz/', // 1458ms
// 延迟 1500-2500ms 的较慢镜像
'https://gh.inkchills.cn/', // 1617ms
'https://ghproxy.cxkpro.top/', // 1651ms
'https://gh.sixyin.com/', // 1686ms
'https://github.geekery.cn/', // 1734ms
'https://git.669966.xyz/', // 1824ms
'https://gh.5050net.cn/', // 1858ms
'https://gh.felicity.ac.cn/', // 1903ms
'https://gh.ddlc.top/', // 2056ms
'https://cf.ghproxy.cc/', // 2058ms
'https://gitproxy.click/', // 2068ms
'https://github.dpik.top/', // 2313ms
'https://gh.zwnes.xyz/', // 2434ms
'https://ghp.keleyaa.com/', // 2440ms
'https://gh.wsmdn.dpdns.org/', // 2744ms
// 延迟 > 2500ms 的慢速镜像(作为备用)
'https://ghproxy.monkeyray.net/', // 3023ms
'https://fastgit.cc/', // 3369ms
'https://cdn.gh-proxy.com/', // 3394ms
'https://gh.catmak.name/', // 4119ms
'https://gh.noki.icu/', // 5990ms
'https://github.chenc.dev/',
'https://ghproxy.cfd/',
'https://github.tbedu.top/',
'https://ghproxy.cc/',
'https://gh.monlor.com/',
'https://cdn.akaere.online/',
'https://gh.idayer.com/',
'https://gh.llkk.cc/',
'https://ghpxy.hwinzniej.top/',
'https://github-proxy.memory-echoes.cn/',
'https://git.yylx.win/',
'https://gitproxy.mrhjx.cn/',
'https://gh.fhjhy.top/',
'https://gp.zkitefly.eu.org/',
'https://gh-proxy.com/',
'https://ghfile.geekertao.top/',
'https://j.1lin.dpdns.org/',
'https://ghproxy.imciel.com/',
'https://github-proxy.teach-english.tech/',
'https://gh.927223.xyz/',
'https://github.ednovas.xyz/',
'https://ghf.xn--eqrr82bzpe.top/',
'https://gh.dpik.top/',
'https://gh.jasonzeng.dev/',
'https://gh.xxooo.cf/',
'https://gh.bugdey.us.kg/',
'https://ghm.078465.xyz/',
'https://j.1win.ggff.net/',
'https://tvv.tw/',
'https://gitproxy.127731.xyz/',
'https://gh.inkchills.cn/',
'https://ghproxy.cxkpro.top/',
'https://gh.sixyin.com/',
'https://github.geekery.cn/',
'https://git.669966.xyz/',
'https://gh.5050net.cn/',
'https://gh.felicity.ac.cn/',
'https://github.dpik.top/',
'https://ghp.keleyaa.com/',
'https://gh.wsmdn.dpdns.org/',
'https://ghproxy.monkeyray.net/',
'https://fastgit.cc/',
'https://gh.catmak.name/',
'https://gh.noki.icu/',
'', // 原始 URL无镜像
];
@@ -109,7 +93,6 @@ export const GITHUB_RAW_MIRRORS = [
// 测试确认支持 raw 文件的镜像
'https://github.chenc.dev/https://raw.githubusercontent.com',
'https://ghproxy.cfd/https://raw.githubusercontent.com',
'https://gh.llkk.cc/https://raw.githubusercontent.com',
'https://ghproxy.cc/https://raw.githubusercontent.com',
'https://gh-proxy.net/https://raw.githubusercontent.com',
];
@@ -150,7 +133,7 @@ const defaultConfig: MirrorConfig = {
apiMirrors: GITHUB_API_MIRRORS,
rawMirrors: GITHUB_RAW_MIRRORS,
nightlyLinkMirrors: NIGHTLY_LINK_MIRRORS,
timeout: 10000, // 10秒超时,平衡速度和可靠性
timeout: 5000, // 5秒超时,平衡速度和可靠性
enabled: true,
customMirror: undefined,
};
@@ -274,7 +257,7 @@ export async function getFastMirrors (forceRefresh: boolean = false): Promise<st
async function performMirrorTest (): Promise<string[]> {
// 开始镜像测速
const timeout = 8000; // 测速超时 8
const timeout = 3000; // 测速超时 3
// 并行测试所有镜像
const mirrors = currentConfig.fileMirrors.filter(m => m);
@@ -400,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));
@@ -454,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({
@@ -475,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,
@@ -559,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
@@ -585,7 +556,7 @@ export async function findAvailableDownloadUrl (
let testedCount = 0;
for (const mirror of mirrors) {
if (!mirror) continue; // 跳过空字符串
// 特殊处理 nightly.link URL
let mirrorUrl: string;
if (originalUrl.includes('nightly.link')) {
@@ -594,7 +565,7 @@ export async function findAvailableDownloadUrl (
} else {
mirrorUrl = buildMirrorUrl(originalUrl, mirror);
}
testedCount++;
if (await testWithValidation(mirrorUrl)) {
return mirrorUrl;
@@ -650,8 +621,15 @@ function compareSemVerSimple (a: string, b: string): number {
* 从 tags 列表中获取最新的 release tag
* 不依赖 GitHub API
*/
export async function getLatestReleaseTag (owner: string, repo: string): Promise<string> {
const result = await getAllGitHubTags(owner, repo);
// Update definitions validation locally first if possible.
// I'll assume valid typescript.
// I will split this into two tool calls to avoid complexity.
// 1. Update mirror.ts
// 2. Update UpdateNapCat.ts
// This tool call: Update mirror.ts
export async function getLatestReleaseTag (owner: string, repo: string, mirror?: string): Promise<string> {
const result = await getAllGitHubTags(owner, repo, mirror);
// 过滤出符合 semver 的 tags
const releaseTags = result.tags.filter(tag => SEMVER_REGEX.test(tag));
@@ -701,6 +679,8 @@ export async function getGitHubRelease (
assetNames?: string[];
/** 是否需要获取 changelog需要调用 API */
fetchChangelog?: boolean;
/** 指定镜像 */
mirror?: string;
} = {}
): Promise<{
tag_name: string;
@@ -710,15 +690,16 @@ export async function getGitHubRelease (
}>;
body?: string;
}> {
const { assetNames = [], fetchChangelog = false } = options;
const { assetNames = [], fetchChangelog = false, mirror } = options;
// 1. 获取实际的 tag 名称
let actualTag: string;
if (tag === 'latest') {
actualTag = await getLatestReleaseTag(owner, repo);
actualTag = await getLatestReleaseTag(owner, repo, mirror);
} else {
actualTag = tag;
}
// ...
// 2. 构建 assets 列表(不需要 API
const assets = assetNames.map(name => ({
@@ -782,8 +763,8 @@ const tagsCache: Map<string, TagsCache> = new Map();
* 获取所有 GitHub tags带缓存
* 优化:并行请求多个镜像,使用第一个成功返回的结果
*/
export async function getAllGitHubTags (owner: string, repo: string): Promise<{ tags: string[], mirror: string; }> {
const cacheKey = `${owner}/${repo}`;
export async function getAllGitHubTags (owner: string, repo: string, mirror?: string): Promise<{ tags: string[], mirror: string; }> {
const cacheKey = `${owner}/${repo}/${mirror || 'auto'}`;
// 检查缓存
const cached = tagsCache.get(cacheKey);
@@ -805,7 +786,7 @@ export async function getAllGitHubTags (owner: string, repo: string): Promise<{
};
// 尝试从 URL 获取 tags
const fetchFromUrl = async (url: string, mirror: string): Promise<{ tags: string[], mirror: string; } | null> => {
const fetchFromUrl = async (url: string, usedMirror: string): Promise<{ tags: string[], mirror: string; } | null> => {
try {
const raw = await PromiseTimer(
RequestUtil.HttpGetText(url),
@@ -813,79 +794,55 @@ export async function getAllGitHubTags (owner: string, repo: string): Promise<{
);
// 检查返回内容是否有效(不是 HTML 错误页面)
if (raw.includes('<!DOCTYPE') || raw.includes('<html')) {
return null;
if (raw.includes('refs/tags')) {
return { tags: parseTags(raw), mirror: usedMirror };
}
const tags = parseTags(raw);
if (tags.length > 0) {
return { tags, mirror };
}
return null;
} catch {
return null;
// 忽略错误
}
return null;
};
// 获取快速镜像列表
let fastMirrors: string[] = [];
try {
fastMirrors = await getFastMirrors();
} catch {
// 忽略错误
// 准备镜像列表
let mirrors: string[] = [];
if (mirror) {
// 如果指定了镜像,只使用该镜像
mirrors = [mirror];
} else {
// 否则使用 auto 逻辑,利用缓存的快速镜像列表
mirrors = await getFastMirrors();
}
// 构建 URL 列表(取前 5 个快速镜像 + 原始 URL 并行请求
const topMirrors = fastMirrors.slice(0, 5);
const mirrorUrls = [
{ url: baseUrl, mirror: 'github.com' }, // 原始 URL
...topMirrors.filter(m => m).map(m => ({ url: buildMirrorUrl(baseUrl, m), mirror: m })),
];
// 并行请求
const promises = mirrors.map(m => {
const url = m ? buildMirrorUrl(baseUrl, m) : baseUrl;
return fetchFromUrl(url, m || 'https://github.com');
});
// 并行请求所有镜像,使用 Promise.any 获取第一个成功的结果
try {
const result = await Promise.any(
mirrorUrls.map(async ({ url, mirror }) => {
const res = await fetchFromUrl(url, mirror);
if (res) return res;
throw new Error('Failed');
})
);
// 缓存结果
tagsCache.set(cacheKey, { tags: result.tags, mirror: result.mirror, timestamp: Date.now() });
return result;
} catch {
// Promise.any 全部失败,回退到顺序尝试剩余镜像
}
// 回退:顺序尝试剩余镜像
const remainingMirrors = fastMirrors.slice(5).filter(m => m);
for (const mirror of remainingMirrors) {
const url = buildMirrorUrl(baseUrl, mirror);
const result = await fetchFromUrl(url, mirror);
const result = await Promise.any(promises.filter(p => p !== null) as Promise<{ tags: string[], mirror: string; } | null>[]);
if (result) {
tagsCache.set(cacheKey, { tags: result.tags, mirror: result.mirror, timestamp: Date.now() });
tagsCache.set(cacheKey, {
tags: result.tags,
mirror: result.mirror,
timestamp: Date.now(),
});
return result;
}
} catch {
// all failed
}
// 最后尝试所有镜像
const allMirrors = currentConfig.fileMirrors.filter(m => m && !fastMirrors.includes(m));
for (const mirror of allMirrors) {
const url = buildMirrorUrl(baseUrl, mirror);
const result = await fetchFromUrl(url, mirror);
if (result) {
tagsCache.set(cacheKey, { tags: result.tags, mirror: result.mirror, timestamp: Date.now() });
return result;
}
if (mirror) {
throw new Error(`指定镜像 ${mirror} 获取 tags 失败`);
}
throw new Error('无法获取 tags所有源都不可用');
throw new Error('无法获取 tags所有镜像源都不可用');
}
// ============== Action Artifacts 支持 ==============
// ActionArtifact 接口定义
export interface ActionArtifact {
id: number;
name: string;
@@ -895,12 +852,14 @@ export interface ActionArtifact {
archive_download_url: string;
workflow_run_id?: number;
head_sha?: string;
workflow_title?: string;
}
// ============== Action Artifacts 缓存 ==============
interface ArtifactsCache {
artifacts: ActionArtifact[];
mirror: string;
timestamp: number;
}
@@ -920,68 +879,116 @@ export function clearArtifactsCache (): void {
* 当 api.github.com 不可用时使用
* 页面格式: https://github.com/{owner}/{repo}/actions/workflows/{workflow}
*/
async function getWorkflowRunsFromHtml (
owner: string,
repo: string,
workflow: string = 'build.yml',
maxRuns: number = 10
): Promise<Array<{ id: number; created_at: string; }>> {
maxRuns: number = 10,
mirror?: string
): Promise<{ runs: Array<{ id: number; created_at: string; title: string; }>; mirror: string; }> {
const baseUrl = `https://github.com/${owner}/${repo}/actions/workflows/${workflow}`;
// 尝试使用镜像获取 HTML
const mirrors = ['', ...currentConfig.fileMirrors.filter(m => m)];
for (const mirror of mirrors) {
// 如果指定了 mirror则只使用该 mirror
let mirrors: string[] = [];
if (mirror) {
mirrors = [mirror];
} else {
// 使用缓存的快速镜像列表
mirrors = await getFastMirrors();
}
for (const mirrorItem of mirrors) {
try {
const url = mirror ? buildMirrorUrl(baseUrl, mirror) : baseUrl;
const html = await PromiseTimer(
RequestUtil.HttpGetText(url),
10000
);
// 从 HTML 中提取 run IDs 和时间
// 格式: href="/NapNeko/NapCatQQ/actions/runs/20676123968"
// 时间格式: <relative-time datetime="2026-01-03T10:37:29Z"
const runPattern = new RegExp(`href="/${owner}/${repo}/actions/runs/(\\d+)"`, 'gi');
const timePattern = /<relative-time\s+datetime="([^"]+)"/gi;
// 提取所有时间
const times: string[] = [];
let timeMatch;
while ((timeMatch = timePattern.exec(html)) !== null) {
times.push(timeMatch[1]);
}
const runs: Array<{ id: number; created_at: string; }> = [];
const allRuns: Array<{ id: number; created_at: string; title: string; }> = [];
const foundIds = new Set<number>();
let timeIndex = 0;
let match;
while ((match = runPattern.exec(html)) !== null && runs.length < maxRuns) {
const id = parseInt(match[1]);
if (!foundIds.has(id)) {
foundIds.add(id);
// 尝试获取对应的时间,每个 run 通常有两个时间(桌面和移动端显示)
// 所以每找到一个 run跳过两个时间
const created_at = times[timeIndex] || new Date().toISOString();
timeIndex += 2; // 跳过两个时间(桌面端和移动端各一个)
runs.push({
id,
created_at,
});
let page = 1;
const maxPages = 10; // 防止无限请求最多翻10页约250个条目
while (allRuns.length < maxRuns && page <= maxPages) {
const pageUrl = page > 1 ? `${baseUrl}?page=${page}` : baseUrl;
const url = mirrorItem ? buildMirrorUrl(pageUrl, mirrorItem) : pageUrl;
const html = await PromiseTimer(
RequestUtil.HttpGetText(url),
10000
);
// 使用 Block 分割策略,更稳健地关联 ID 和时间
const rows = html.split('<div class="Box-row');
let foundOnThisPage = 0;
for (const row of rows) {
// 提取 Run ID 和 Status
// <a href="/NapNeko/NapCatQQ/actions/runs/20799940346" ... aria-label="completed successfully: ...">
const runMatch = new RegExp(`href="/${owner}/${repo}/actions/runs/(\\d+)"[^>]*aria-label="([^"]*)"`, 'i').exec(row);
if (!runMatch || !runMatch[1] || !runMatch[2]) continue;
const id = parseInt(runMatch[1]);
const ariaLabel = runMatch[2];
const ariaLabelLower = ariaLabel.toLowerCase();
// 只需要判断 completed
if (ariaLabelLower.includes('completed')) {
if (!foundIds.has(id)) {
// 提取时间 (取 Block 内的第一个 relative-time)
const timeMatch = /<relative-time\s+datetime="([^"]+)"/.exec(row);
if (timeMatch && timeMatch[1]) {
foundIds.add(id);
foundOnThisPage++;
// 优先从 markdown-title class 提取标题
let title = '';
const titleMatch = /class="[^"]*markdown-title[^"]*"[^>]*>([\s\S]*?)<\/span>/i.exec(row);
if (titleMatch && titleMatch[1]) {
title = titleMatch[1].trim();
}
// 如果没找到,回退到 aria-label 逻辑
if (!title) {
title = ariaLabel;
const prefixMatch = /^(completed successfully:\s*)/i.exec(title);
if (prefixMatch) {
title = title.substring(prefixMatch[0].length);
}
}
allRuns.push({
id,
created_at: timeMatch[1],
title: title.trim()
});
}
}
}
}
// 如果本页没有找到任何 completed 的 run但页面可能不为空或者页面内容太少可能是最后一页或错误
// 这里简化判断: 如果本页没提取到任何有效数据,就认为没有更多数据了
if (foundOnThisPage === 0) {
// 也要考虑到可能是页面解析失败或者全是 failed 状态
// 检查是否有翻页按钮可能更复杂,暂时假设如果一整页都没有 successful run可能后面也没有了或者我们已经获取够多了
// 为了稳健,如果本页没找到,且 allRuns 还没满,尝试下一页 (除非页面很小说明是空页)
if (rows.length < 2) { // 只有 split 的第一个空元素
break;
}
}
// 分页逻辑:总是尝试下一页,直到满足 maxRuns
page++;
}
if (runs.length > 0) {
return runs;
if (allRuns.length > 0) {
return { runs: allRuns, mirror: mirrorItem || 'https://github.com' };
}
} catch {
continue;
}
}
return [];
return { runs: [], mirror: '' };
}
/**
@@ -996,122 +1003,49 @@ async function getArtifactsFromNightlyLink (
owner: string,
repo: string,
workflow: string = 'build.yml',
branch: string = 'main',
maxRuns: number = 10
): Promise<ActionArtifact[]> {
let workflowRuns: Array<{ id: number; head_sha?: string; created_at: string; }> = [];
// 策略1: 优先尝试 GitHub API
_branch: string = 'main',
maxRuns: number = 10,
mirror?: string
): Promise<{ artifacts: ActionArtifact[], mirror: string; }> {
// 策略: 优先使用 nightly.link更稳定无需认证+ HTML 解析
try {
const endpoint = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflow}/runs?branch=${branch}&status=success&per_page=${maxRuns}`;
const runsResponse = await PromiseTimer(
RequestUtil.HttpGetJson<{
workflow_runs: Array<{ id: number; head_sha: string; created_at: string; }>;
}>(endpoint, 'GET', undefined, {
'User-Agent': 'NapCat',
'Accept': 'application/vnd.github.v3+json',
}),
10000
);
// 以前尝试使用 GitHub API现在弃用完全使用 HTML 解析逻辑
// 并获取 workflow // 直接从 HTML 页面解析
const { runs: workflowRuns, mirror: runsMirror } = await getWorkflowRunsFromHtml(owner, repo, workflow, maxRuns, mirror);
if (runsResponse.workflow_runs && runsResponse.workflow_runs.length > 0) {
workflowRuns = runsResponse.workflow_runs;
if (workflowRuns.length === 0) {
return { artifacts: [], mirror: runsMirror };
}
} catch {
// API 请求失败,继续尝试 HTML 解析
}
// 策略2: API 失败时,从 HTML 页面解析
if (workflowRuns.length === 0) {
workflowRuns = await getWorkflowRunsFromHtml(owner, repo, workflow, maxRuns);
}
// 直接拼接 nightly.link URL
// 格式: https://nightly.link/{owner}/{repo}/actions/runs/{run_id}/{artifact_name}.zip
const artifacts: ActionArtifact[] = [];
const artifactNames = ['NapCat.Framework', 'NapCat.Shell']; // 已知的 artifact 名称
if (workflowRuns.length === 0) {
return [];
}
// 如果 HTML 解析使用的 mirror 是 github.com则 nightly.link 使用默认配置
// 如果使用了镜像,可能需要特殊的 nightly.link 镜像,或者这里仅记录 HTML 来源镜像
// 实际上 nightly.link 本身就是一个服务,我们使用配置中的 nightlyLinkMirrors
const baseNightlyMirror = currentConfig.nightlyLinkMirrors[0] || 'https://nightly.link';
// 直接拼接 nightly.link URL
// 格式: https://nightly.link/{owner}/{repo}/actions/runs/{run_id}/{artifact_name}.zip
const artifacts: ActionArtifact[] = [];
const artifactNames = ['NapCat.Framework', 'NapCat.Shell']; // 已知的 artifact 名称
for (const run of workflowRuns) {
for (const artifactName of artifactNames) {
const mirror = currentConfig.nightlyLinkMirrors[0] || 'https://nightly.link';
artifacts.push({
id: run.id,
name: artifactName,
size_in_bytes: 0,
created_at: run.created_at,
expires_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
archive_download_url: `${mirror}/${owner}/${repo}/actions/runs/${run.id}/${artifactName}.zip`,
workflow_run_id: run.id,
head_sha: run.head_sha,
});
}
}
return artifacts;
}
/**
* 通过 GitHub API 获取 artifacts主要方案
*/
async function getArtifactsFromAPI (
owner: string,
repo: string,
workflow: string = 'build.yml',
branch: string = 'main',
maxRuns: number = 10
): Promise<ActionArtifact[]> {
const endpoint = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflow}/runs?branch=${branch}&status=success&per_page=${maxRuns}`;
const runsResponse = await PromiseTimer(
RequestUtil.HttpGetJson<{
workflow_runs: Array<{ id: number; head_sha: string; created_at: string; }>;
}>(endpoint, 'GET', undefined, {
'User-Agent': 'NapCat',
'Accept': 'application/vnd.github.v3+json',
}),
10000
);
const workflowRuns = runsResponse.workflow_runs;
if (!workflowRuns || workflowRuns.length === 0) {
throw new Error('No successful workflow runs found');
}
// 获取所有 runs 的 artifacts
const allArtifacts: ActionArtifact[] = [];
for (const run of workflowRuns) {
try {
const artifactsEndpoint = `https://api.github.com/repos/${owner}/${repo}/actions/runs/${run.id}/artifacts`;
const artifactsResponse = await PromiseTimer(
RequestUtil.HttpGetJson<{
artifacts: ActionArtifact[];
}>(artifactsEndpoint, 'GET', undefined, {
'User-Agent': 'NapCat',
'Accept': 'application/vnd.github.v3+json',
}),
10000
);
if (artifactsResponse.artifacts) {
// 为每个 artifact 添加 run 信息
for (const artifact of artifactsResponse.artifacts) {
artifact.workflow_run_id = run.id;
artifact.head_sha = run.head_sha;
allArtifacts.push(artifact);
}
for (const run of workflowRuns) {
for (const artifactName of artifactNames) {
artifacts.push({
id: run.id,
name: artifactName,
size_in_bytes: 0,
created_at: run.created_at,
expires_at: new Date(new Date(run.created_at).getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
archive_download_url: `${baseNightlyMirror}/${owner}/${repo}/actions/runs/${run.id}/${artifactName}.zip`,
workflow_run_id: run.id,
workflow_title: run.title,
});
}
} catch {
// 单个 run 获取失败,继续下一个
}
}
return { artifacts, mirror: runsMirror };
return allArtifacts;
} catch {
return { artifacts: [], mirror: '' };
}
}
/**
@@ -1121,48 +1055,41 @@ async function getArtifactsFromAPI (
* 策略:
* 1. 检查缓存10分钟有效
* 2. 优先尝试从 nightly.link 获取(无需认证,更稳定)
* 3. 如果失败,回退到 GitHub API
* 3. 这里的实现已经完全移除了对 GitHub API 的依赖,直接解析 HTML
*/
export async function getLatestActionArtifacts (
owner: string,
repo: string,
workflow: string = 'build.yml',
branch: string = 'main',
maxRuns: number = 10
): Promise<ActionArtifact[]> {
const cacheKey = `${owner}/${repo}/${workflow}/${branch}`;
maxRuns: number = 10,
mirror?: string
): Promise<{ artifacts: ActionArtifact[], mirror: string; }> {
const cacheKey = `${owner}/${repo}/${workflow}/${branch}/${mirror || 'auto'}`;
// 检查缓存
const cached = artifactsCache.get(cacheKey);
if (cached && (Date.now() - cached.timestamp) < ARTIFACTS_CACHE_TTL) {
return cached.artifacts;
return { artifacts: cached.artifacts, mirror: cached.mirror };
}
let artifacts: ActionArtifact[] = [];
let result: { artifacts: ActionArtifact[], mirror: string; } = { artifacts: [], mirror: '' };
// 策略1: 优先使用 nightly.link更稳定无需认证
// 策略: 优先使用 nightly.link更稳定无需认证+ HTML 解析
try {
artifacts = await getArtifactsFromNightlyLink(owner, repo, workflow, branch, maxRuns);
result = await getArtifactsFromNightlyLink(owner, repo, workflow, branch, maxRuns, mirror);
} catch {
// nightly.link 获取失败
}
// 策略2: 回退到 GitHub API
if (artifacts.length === 0) {
try {
artifacts = await getArtifactsFromAPI(owner, repo, workflow, branch, maxRuns);
} catch {
// API 获取失败
}
// 获取失败
}
// 缓存结果(即使为空也缓存,避免频繁请求)
if (artifacts.length > 0) {
if (result.artifacts.length > 0) {
artifactsCache.set(cacheKey, {
artifacts,
artifacts: result.artifacts,
mirror: result.mirror,
timestamp: Date.now(),
});
}
return artifacts;
return result;
}

View File

@@ -21,4 +21,4 @@ export interface IStatusHelperSubscription {
on (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
off (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
emit (event: 'statusUpdate', status: SystemStatus): boolean;
}
}

View File

@@ -65,9 +65,11 @@ export function compareSemVer (v1: string, v2: string): -1 | 0 | 1 {
const a = parseSemVer(v1);
const b = parseSemVer(v2);
if (!a.valid || !b.valid) {
return 0;
if (!a.valid && !b.valid) {
return v1.localeCompare(v2) as -1 | 0 | 1;
}
if (!a.valid) return -1;
if (!b.valid) return 1;
// 比较主版本号
if (a.major !== b.major) return a.major > b.major ? 1 : -1;

View File

@@ -0,0 +1,307 @@
import { GeneralCallResult, InstanceContext, NapCatCore } from '@/napcat-core';
import {
createFlashTransferResult,
FileListResponse,
FlashFileSetInfo,
SendStatus,
UploadSceneType,
} from '@/napcat-core/data/flash';
import { Peer } from '@/napcat-core/types';
export class NTQQFlashApi {
context: InstanceContext;
core: NapCatCore;
constructor (context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
}
/**
* 发起闪传上传任务
* @param fileListToUpload 上传文件绝对路径的列表,可以是文件夹!!
* @param thumbnailPath
* @param filesetName
*/
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 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);
if (uploadResult.result === 0) {
this.context.logger.log('[Flash] 发起闪传任务成功');
return uploadResult;
} else {
this.context.logger.logError('[Flash] 发起闪传上传任务失败!!');
return uploadResult;
}
}
/**
* 下载闪传文件集
* @param fileSetId
*/
async downloadFileSetBySetId (fileSetId: string): Promise<GeneralCallResult & {
extraInfo: unknown;
}> {
const flashService = this.context.session.getFlashTransferService();
const result = await flashService.startFileSetDownload(fileSetId, 1, { isIncludeCompressInnerFiles: false }); // 为了方便,暂时硬编码
if (result.result === 0) {
this.context.logger.log('[Flash] 成功开始下载文件集');
} else {
this.context.logger.logError('[Flash] 尝试下载文件集失败!');
}
return result;
}
/**
* 获取闪传的外链分享
* @param fileSetId
*/
async getShareLinkBySetId (fileSetId: string): Promise<GeneralCallResult & {
shareLink: string;
expireTimestamp: string;
}> {
const flashService = this.context.session.getFlashTransferService();
const result = await flashService.getShareLinkReq(fileSetId);
if (result.result === 0) {
this.context.logger.log('[Flash] 获取闪传外链分享成功:', result.shareLink);
} else {
this.context.logger.logError('[Flash] 获取闪传外链失败!!');
}
return result;
}
/**
* 从分享外链获取文件集id
* @param shareCode
*/
async fromShareLinkFindSetId (shareCode: string): Promise<GeneralCallResult & {
fileSetId: string;
}> {
const flashService = this.context.session.getFlashTransferService();
const result = await flashService.getFileSetIdByCode(shareCode);
if (result.result === 0) {
this.context.logger.log('[Flash] 获取shareCode的文件集Id成功');
} else {
this.context.logger.logError('[Flash] 获取文件集ID失败');
}
return result;
}
/**
* 获取fileSet的文件结构信息 (未来可能需要深度遍历)
* == 注意返回结构和其它的不同没有GeneralCallResult!!! ==
* @param fileSetId
*/
async getFileListBySetId (fileSetId: string): Promise<FileListResponse> {
const flashService = this.context.session.getFlashTransferService();
const requestArg = {
seq: 0,
fileSetId,
isUseCache: false,
sceneType: 1, // 硬编码
reqInfos: [
{
count: 18, // 18 ??
paginationInfo: {},
parentId: '',
reqIndexPath: '',
reqDepth: 1,
filterCondition: {
fileCategory: 0,
filterType: 0,
},
sortConditions: [
{
sortField: 0,
sortOrder: 0,
},
],
isNeedPhysicalInfoReady: false,
},
],
};
const result = await flashService.getFileList(requestArg);
if (result.rsp.result === 0) {
this.context.logger.log('[Flash] 获取fileSet文件信息成功');
return result.rsp;
} else {
this.context.logger.logError(`[Flash] 获取文件信息失败ErrMsg: ${result.rsp.errMs}`);
return result.rsp;
}
}
/**
* 获取闪传文件集合信息
* @param fileSetId
*/
async getFileSetIndoBySetId (fileSetId: string): Promise<GeneralCallResult & {
seq: number;
isCache: boolean;
fileSet: FlashFileSetInfo;
}> {
const flashService = this.context.session.getFlashTransferService();
const requestArg = {
fileSetId,
};
const result = await flashService.getFileSet(requestArg);
if (result.result === 0) {
this.context.logger.log('[Flash] 获取闪传文件集信息成功!');
} else {
this.context.logger.logError('[Flash] 获取闪传文件信息失败!!');
}
return result;
}
/**
* 发送闪传消息(私聊/群聊)
* @param fileSetId
* @param peer
*/
async sendFlashMessage (fileSetId: string, peer: Peer): Promise<{
errCode: number,
errMsg: string,
rsp: {
sendStatus: SendStatus[];
};
}> {
const flashService = this.context.session.getFlashTransferService();
const target = {
destUid: peer.peerUid,
destType: peer.chatType,
// destUin: peer.peerUin,
};
const requestsArg = {
fileSetId,
targets: [target],
};
const result = await flashService.sendFlashTransferMsg(requestsArg);
if (result.errCode === 0) {
this.context.logger.log('[Flash] 消息发送成功');
} else {
this.context.logger.logError(`[Flash] 消息发送失败!!原因:${result.errMsg}`);
}
return result;
}
/**
* 获取闪传文件集中某个文件的下载URL外链
* @param fileSetId
* @param options
*/
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);
const { fileName, fileIndex } = options;
let targetFile: any;
let file: any;
const allFolder = result.fileLists;
// eslint-disable-next-line no-labels
searchLoop: for (const folder of allFolder) {
const fileList = folder.fileList;
for (let i = 0; i < fileList.length; i++) {
file = fileList[i];
if (fileName !== undefined && file.name === fileName) {
targetFile = file;
// eslint-disable-next-line no-labels
break searchLoop;
}
if (fileIndex !== undefined && i === fileIndex) {
targetFile = file;
// eslint-disable-next-line no-labels
break searchLoop;
}
}
}
if (targetFile === undefined) {
this.context.logger.logError('[Flash] 未找到对应文件!!');
return {
result: -1,
errMsg: '未找到对应文件',
transferUrl: '',
};
} else {
this.context.logger.log('[Flash] 找到对应文件,准备尝试获取传输链接');
const res = await flashService.startFileTransferUrl(targetFile);
return {
result: 0,
errMsg: '',
transferUrl: res.url,
};
}
}
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

@@ -18,7 +18,7 @@ export class NTQQFriendApi {
async getBuddyV2SimpleInfoMap () {
const buddyService = this.context.session.getBuddyService();
let uids: string[] = [];
if (this.core.context.basicInfoWrapper.requireMinNTQQBuild('41679')) {
if (this.core.context.basicInfoWrapper.requireMinNTQQBuild('39038')) {
const buddyListV2NT = await buddyService.getBuddyListV2('0', true, BuddyListReqType.KNOMAL);
uids = buddyListV2NT.data.flatMap(item => item.buddyUids);
} else {
@@ -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,
@@ -55,7 +55,7 @@ export class NTQQFriendApi {
const buddyService = this.context.session.getBuddyService();
let uids: string[] = [];
let buddyListV2: Awaited<ReturnType<typeof buddyService.getBuddyListV2>>['data'];
if (this.core.context.basicInfoWrapper.requireMinNTQQBuild('41679')) {
if (this.core.context.basicInfoWrapper.requireMinNTQQBuild('39038')) {
buddyListV2 = (await buddyService.getBuddyListV2('0', true, BuddyListReqType.KNOMAL)).data;
uids = buddyListV2.flatMap(item => item.buddyUids);
} else {

View File

@@ -7,3 +7,5 @@ export * from './webapi';
export * from './system';
export * from './packet';
export * from './file';
export * from './online';
export * from './flash';

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

@@ -0,0 +1,241 @@
import { InstanceContext, NapCatCore } from '@/napcat-core';
import { Peer } from '@/napcat-core/types';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { GeneralCallResultStatus } from '@/napcat-core/services/common';
import { sleep } from '@/napcat-common/src/helper';
const normalizePath = (p: string) => path.normalize(p).toLowerCase();
export class NTQQOnlineApi {
context: InstanceContext;
core: NapCatCore;
constructor (context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
}
/**
* 这里不等待node返回因为the fuck wrapper.node 根本不返回(会卡死不知道为什么)!!! 只能手动查询判断死活
* @param peer
* @param filePath
* @param fileName
*/
async sendOnlineFile (peer: Peer, filePath: string, fileName: string): Promise<any> {
if (!fs.existsSync(filePath)) {
throw new Error(`[NapCat] 文件不存在: ${filePath}`);
}
const actualFileName = fileName || path.basename(filePath);
const fileSize = fs.statSync(filePath).size.toString();
const fileElementToSend = [{
elementType: 23,
elementId: '',
fileElement: {
fileName: actualFileName,
filePath,
fileSize,
},
}];
const msgService = this.context.session.getMsgService();
const startTime = Math.floor(Date.now() / 1000) - 2; // 容错时间窗口
msgService.sendMsg('0', peer, fileElementToSend, new Map()).catch((_e: any) => {
});
const maxRetries = 10;
let retryCount = 0;
while (retryCount < maxRetries) {
await sleep(1000);
retryCount++;
try {
const msgListResult = await msgService.getOnlineFileMsgs(peer);
const msgs = msgListResult?.msgList || [];
const foundMsg = msgs.find((msg: any) => {
if (parseInt(msg.msgTime) < startTime) return false;
const validElement = msg.elements.find((el: any) => {
if (el.elementType !== 23 || !el.fileElement) return false;
const isNameMatch = el.fileElement.fileName === actualFileName;
const isPathMatch = normalizePath(el.fileElement.filePath) === normalizePath(filePath);
return isNameMatch && isPathMatch;
});
return !!validElement;
});
if (foundMsg) {
const targetElement = foundMsg.elements.find((el: any) => el.elementType === 23);
this.context.logger.log('[OnlineFile] 在线文件发送成功!');
return {
result: GeneralCallResultStatus.OK,
errMsg: '',
msgId: foundMsg.msgId,
elementId: targetElement?.elementId || '',
};
}
} catch (_e) {
}
}
this.context.logger.logError('[OnlineFile] 在线文件发送失败!!!');
return {
result: GeneralCallResultStatus.ERROR,
errMsg: '[NapCat] Send Online File Timeout: Message not found in history.',
};
}
/**
* 发送在线文件夹
* @param peer
* @param folderPath
* @param folderName
*/
async sendOnlineFolder (peer: Peer, folderPath: string, folderName?: string): Promise<any> {
const actualFolderName = folderName || path.basename(folderPath);
if (!fs.existsSync(folderPath)) {
return { result: GeneralCallResultStatus.ERROR, errMsg: `Folder not found: ${folderPath}` };
}
if (!fs.statSync(folderPath).isDirectory()) {
return { result: GeneralCallResultStatus.ERROR, errMsg: `Path is not a directory: ${folderPath}` };
}
const folderElementItem = {
elementType: 30,
elementId: '',
fileElement: {
fileName: actualFolderName,
filePath: folderPath,
fileSize: "",
},
};
const msgService = this.context.session.getMsgService();
const startTime = Math.floor(Date.now() / 1000) - 2;
msgService.sendMsg('0', peer, [folderElementItem], new Map()).catch((_e: any) => {
});
const maxRetries = 10;
let retryCount = 0;
while (retryCount < maxRetries) {
await sleep(1000);
retryCount++;
try {
const msgListResult = await msgService.getOnlineFileMsgs(peer);
const msgs = msgListResult?.msgList || [];
const foundMsg = msgs.find((msg: any) => {
if (parseInt(msg.msgTime) < startTime) return false;
const validElement = msg.elements.find((el: any) => {
if (el.elementType !== 30 || !el.fileElement) return false;
const isNameMatch = el.fileElement.fileName === actualFolderName;
const isPathMatch = normalizePath(el.fileElement.filePath) === normalizePath(folderPath);
return isNameMatch && isPathMatch;
});
return !!validElement;
});
if (foundMsg) {
const targetElement = foundMsg.elements.find((el: any) => el.elementType === 30);
this.context.logger.log('[OnlineFile] 在线文件夹发送成功!');
return {
result: GeneralCallResultStatus.OK,
errMsg: '',
msgId: foundMsg.msgId,
elementId: targetElement?.elementId || '',
};
}
} catch (_e) {
}
}
this.context.logger.logError('[OnlineFile] 在线文件发送失败!!!');
return {
result: GeneralCallResultStatus.ERROR,
errMsg: '[NapCat] Send Online Folder Timeout: Message not found in history.',
};
}
/**
* 获取好友的在线文件消息
* @param peer
*/
async getOnlineFileMsg (peer: Peer): Promise<any> {
const msgService = this.context.session.getMsgService();
return await msgService.getOnlineFileMsgs(peer);
}
/**
* 取消在线文件的发送
* @param peer
* @param msgId
*/
async cancelMyOnlineFileMsg (peer: Peer, msgId: string): Promise<void> {
const msgService = this.context.session.getMsgService();
await msgService.cancelSendMsg(peer, msgId);
}
/**
* 拒绝接收在线文件
* @param peer
* @param msgId
* @param elementId
*/
async refuseOnlineFileMsg (peer: Peer, msgId: string, elementId: string): Promise<void> {
const msgService = this.context.session.getMsgService();
const arrToSend = {
msgId,
peerUid: peer.peerUid,
chatType: 1,
elementId,
downloadType: 1,
downSourceType: 1,
};
await msgService.refuseGetRichMediaElement(arrToSend);
}
/**
* 接收在线文件/文件夹
* @param peer
* @param msgId
* @param elementId
* @constructor
*/
async receiveOnlineFileOrFolder (peer: Peer, msgId: string, elementId: string): Promise<any> {
const msgService = this.context.session.getMsgService();
const arrToSend = {
msgId,
peerUid: peer.peerUid,
chatType: 1,
elementId,
downSourceType: 1,
downloadType: 1,
};
return await msgService.getRichMediaElement(arrToSend);
}
/**
* 在线文件/文件夹转离线
* @param peer
* @param msgId
*/
async switchFileToOffline (peer: Peer, msgId: string): Promise<void> {
const msgService = this.context.session.getMsgService();
await msgService.switchToOfflineSendMsg(peer, msgId);
}
}

View File

@@ -13,6 +13,17 @@ import { createHash } from 'node:crypto';
import { basename } from 'node:path';
import { qunAlbumControl } from '../data/webapi';
import { createAlbumCommentRequest, createAlbumFeedPublish, createAlbumMediaFeed } from '../data/album';
export interface SetNoticeRetSuccess {
ec: number;
em: string;
id: number;
ltsm: number;
new_fid: string;
read_only: number;
role: number;
srv_code: number;
}
export class NTQQWebApi {
context: InstanceContext;
core: NapCatCore;
@@ -25,12 +36,12 @@ export class NTQQWebApi {
async shareDigest (groupCode: string, msgSeq: string, msgRandom: string, targetGroupCode: string) {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
const url = `https://qun.qq.com/cgi-bin/group_digest/share_digest?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
group_code: groupCode,
msg_seq: msgSeq,
msg_random: msgRandom,
target_group_code: targetGroupCode,
}).toString()}`;
bkn: this.getBknFromCookie(cookieObject),
group_code: groupCode,
msg_seq: msgSeq,
msg_random: msgRandom,
target_group_code: targetGroupCode,
}).toString()}`;
try {
return RequestUtil.HttpGetText(url, 'GET', '', { Cookie: this.cookieToString(cookieObject) });
} catch {
@@ -52,11 +63,11 @@ export class NTQQWebApi {
async getGroupEssenceMsg (GroupCode: string, page_start: number = 0, page_limit: number = 50) {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
page_start: page_start.toString(),
page_limit: page_limit.toString(),
group_code: GroupCode,
}).toString()}`;
bkn: this.getBknFromCookie(cookieObject),
page_start: page_start.toString(),
page_limit: page_limit.toString(),
group_code: GroupCode,
}).toString()}`;
try {
const ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(
url,
@@ -76,16 +87,16 @@ export class NTQQWebApi {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
const retList: Promise<WebApiGroupMemberRet>[] = [];
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: '0',
end: '40',
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: '0',
end: '40',
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
return [];
@@ -101,16 +112,16 @@ export class NTQQWebApi {
// 遍历批量请求
for (let i = 2; i <= PageNum; i++) {
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: ((i - 1) * 40).toString(),
end: (i * 40).toString(),
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: ((i - 1) * 40).toString(),
end: (i * 40).toString(),
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
retList.push(ret);
}
@@ -153,16 +164,7 @@ export class NTQQWebApi {
imgWidth: number = 540,
imgHeight: number = 300
) {
interface SetNoticeRetSuccess {
ec: number;
em: string;
id: number;
ltsm: number;
new_fid: string;
read_only: number;
role: number;
srv_code: number;
}
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
@@ -178,18 +180,18 @@ export class NTQQWebApi {
imgHeight: imgHeight.toString(),
};
const ret: SetNoticeRetSuccess = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
text: Content,
pinned: pinned.toString(),
type: type.toString(),
settings,
...(picId === '' ? {} : externalParam),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
text: Content,
pinned: pinned.toString(),
type: type.toString(),
settings,
...(picId === '' ? {} : externalParam),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
return ret;
} catch {
@@ -201,20 +203,20 @@ export class NTQQWebApi {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
try {
const ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(
`https://web.qun.qq.com/cgi-bin/announce/get_t_list?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
ft: '23',
ni: '1',
n: '1',
i: '1',
log_read: '1',
platform: '1',
s: '-1',
}).toString()}&n=20`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://web.qun.qq.com/cgi-bin/announce/get_t_list?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
ft: '23',
ni: '1',
n: '1',
i: '1',
log_read: '1',
platform: '1',
s: '-1',
}).toString()}&n=20`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
return ret?.ec === 0 ? ret : undefined;
} catch {
@@ -222,17 +224,17 @@ export class NTQQWebApi {
}
}
private async getDataInternal (cookieObject: { [key: string]: string }, groupCode: string, type: number) {
private async getDataInternal (cookieObject: { [key: string]: string; }, groupCode: string, type: number) {
let resJson;
try {
const res = await RequestUtil.HttpGetText(
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
gc: groupCode,
type: type.toString(),
}).toString()}`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
gc: groupCode,
type: type.toString(),
}).toString()}`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
if (match?.[1]) {
@@ -245,7 +247,7 @@ export class NTQQWebApi {
}
}
private async getHonorList (cookieObject: { [key: string]: string }, groupCode: string, type: number) {
private async getHonorList (cookieObject: { [key: string]: string; }, groupCode: string, type: number) {
const data = await this.getDataInternal(cookieObject, groupCode, type);
if (!data) {
this.context.logger.logError(`获取类型 ${type} 的荣誉信息失败`);
@@ -304,11 +306,11 @@ export class NTQQWebApi {
return HonorInfo;
}
private cookieToString (cookieObject: { [key: string]: string }) {
private cookieToString (cookieObject: { [key: string]: string; }) {
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ');
}
public getBknFromCookie (cookieObject: { [key: string]: string }) {
public getBknFromCookie (cookieObject: { [key: string]: string; }) {
const sKey = cookieObject['skey'] as string;
let hash = 5381;
@@ -361,7 +363,7 @@ export class NTQQWebApi {
uin,
getMemberRole: '0',
});
const response = await RequestUtil.HttpGetJson<{ data: { album: Array<{ id: string, title: string }> } }>(api + params.toString(), 'GET', '', {
const response = await RequestUtil.HttpGetJson<{ data: { album: Array<{ id: string, title: string; }>; }; }>(api + params.toString(), 'GET', '', {
Cookie: cookies,
});
return response.data.album;
@@ -384,7 +386,7 @@ export class NTQQWebApi {
sAlbumID,
});
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`;
const post = await RequestUtil.HttpGetJson<{ data: { session: string }, ret: number, msg: string }>(api, 'POST', body, {
const post = await RequestUtil.HttpGetJson<{ data: { session: string; }, ret: number, msg: string; }>(api, 'POST', body, {
Cookie: cookie,
'Content-Type': 'application/json',
});
@@ -430,7 +432,7 @@ export class NTQQWebApi {
throw new Error(`HTTP error! status: ${response.status}`);
}
const post = await response.json() as { ret: number, msg: string }; if (post.ret !== 0) {
const post = await response.json() as { ret: number, msg: string; }; if (post.ret !== 0) {
throw new Error(`分片 ${seq} 上传失败: ${post.msg}`);
}
offset += chunk.length;
@@ -475,10 +477,10 @@ export class NTQQWebApi {
const client_key = Date.now() * 1000;
return await this.context.session.getAlbumService().doQunComment(
random_seq, {
map_info: [],
map_bytes_info: [],
map_user_account: [],
},
map_info: [],
map_bytes_info: [],
map_user_account: [],
},
qunId,
2,
createAlbumMediaFeed(uin, albumId, lloc),
@@ -509,13 +511,13 @@ export class NTQQWebApi {
const uin = this.core.selfInfo.uin || '10001';
return await this.context.session.getAlbumService().doQunLike(
random_seq, {
map_info: [],
map_bytes_info: [],
map_user_account: [],
}, {
id,
status: 1,
},
map_info: [],
map_bytes_info: [],
map_user_account: [],
}, {
id,
status: 1,
},
createAlbumFeedPublish(qunId, uin, albumId, lloc)
);
}

View File

@@ -0,0 +1,358 @@
export interface FlashBaseRequest {
fileSetId: string;
}
export interface UploaderInfo {
uin: string,
nickname: string,
uid: string,
sendEntrance: string, // ""
}
export interface thumbnailInfo {
id: string,
url: {
spec: number,
uri: string,
}[],
localCachePath: string,
}
export interface SendTarget {
destType: number; // 1私聊
destUin?: string,
destUid: string,
}
export interface SendTargetRequests {
fileSetId: string;
targets: SendTarget[];
}
export interface DownloadStatusInfo {
result: number; // 0
fileSetId: string;
status: number;
info: {
curDownLoadFailFileNum: number,
curDownLoadedPauseFileNum: number,
curDownLoadedFileNum: number,
curRealDownLoadedFileNum: number,
curDownloadingFileNum: number,
totalDownLoadedFileNum: number,
curDownLoadedBytes: string, // "0"
totalDownLoadedBytes: string,
curSpeedBps: number,
avgSpeedBps: number,
maxSpeedBps: number,
remainDownLoadSeconds: number,
failFileIdList: [],
allFileIdList: [],
hasNormalFileDownloading: boolean,
onlyCompressInnerFileDownloading: boolean,
isAllFileAlreadyDownloaded: boolean,
saveFileSetDir: string,
allWaitingStatusTask: boolean,
downloadSceneType: DownloadSceneType,
retryCount: number,
statisticInfo: {
downloadTaskId: string,
downloadFilesetName: string,
downloadFileTypeDistribution: string,
downloadFileSizeDistribution: string;
},
albumStorageFailImageNum: number,
albumStorageFailVideoNum: number,
albumStorageFailFileIdList: [],
albumStorageSucImageNum: number,
albumStorageSucVideoNum: number,
albumStorageSucFileIdList: [],
albumStorageFileNum: number;
};
}
export interface physicalInfo {
id: string,
url: string,
status: number, // 2 已下载
processing: string,
localPath: string,
width: 0,
height: 0,
time: number,
}
export interface downloadInfo {
status: number,
curDownLoadBytes: string,
totalFileBytes: string,
errorCode: number,
}
export interface uploadInfo {
uploadedBytes: string,
errorCode: number,
svrRrrCode: number,
errMsg: string,
isNeedDelDeviceInfo: boolean,
thumbnailUploadState: number;
isSecondHit: boolean,
hasModifiedErr: boolean,
}
export interface folderUploadInfo {
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;
}
export interface compressFileFolderInfo {
downloadStatus: number;
saveFileDirPath: string;
totalFileCount: string;
totalFileSize: string;
}
export interface albumStorgeInfo {
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;
}
export interface fileListsInfo {
parentId: string,
depth: number, // 1
fileList: FlashOneFileInfo[],
paginationInfo: {};
isEnd: boolean,
isCache: boolean,
}
export interface FileListResponse {
seq: number,
result: number,
errMs: string,
fileLists: fileListsInfo[],
}
export interface createFlashTransferResult {
fileSetId: string,
shareLink: string,
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
name?: string;
uploaders: UploaderInfo[];
permission?: {};
coverPath?: string;
paths: string[]; // 文件的绝对路径,可以是文件夹
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: BusiScene, // 1
reqInfos: {
count: number, // 18 ?? 硬编码吧 不懂
paginationInfo: {},
parentId: string,
reqIndexPath: string,
reqDepth: number, // 1
filterCondition: {
fileCategory: number,
filterType: number,
}, // 0
sortConditions: {
sortField: number,
sortOrder: number,
}[],
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,
namePinyin: string,
totalFileCount: number,
totalFileSize: number,
permission: {},
shareInfo: {
shareLink: string,
extractionCode: string,
},
cover: {
id: string,
urls: [
{
spec: number, // 2
url: string;
}
],
localCachePath: string;
},
uploaders: [
{
uin: string,
nickname: string,
uid: string,
sendEntrance: string;
}
],
expireLeftTime: number,
aiClusteringStatus: {
firstClusteringList: [],
shouldPull: boolean;
},
createTime: number,
expireTime: number,
firstLevelItemCount: 1,
svrLastUpdateTimestamp: 0,
taskId: string, // 同 fileSetId
uploadInfo: {
totalUploadedFileSize: number,
successCount: number,
failedCount: number;
},
downloadInfo: {
totalDownloadedFileSize: 0,
totalFileSize: 0,
totalDownloadFileCount: 0,
successCount: 0,
failedCount: 0,
pausedCount: 0,
cancelCount: 0,
status: 0,
curLevelDownloadedFileCount: number,
curLevelUnDownloadedFileCount: 0;
},
transferType: number,
isLocalCreate: true,
status: number, // todo 枚举全部状态
uploadStatus: number, // todo 同上
uploadPauseReason: 0,
downloadStatus: 0,
downloadPauseReason: 0,
saveFileSetDir: string,
uploadSceneType: UploadSceneType,
downloadSceneType: DownloadSceneType, // 0 PC-QQ 103 web
retryCount: number,
isMergeShareUpload: 0,
isRemoveDeviceInfo: boolean,
isRemoveLocation: boolean;
}
export interface SendStatus {
result: number,
msg: string,
target: {
destType: number,
destUid: string,
};
}

View File

@@ -513,7 +513,18 @@
},
"9.9.26-44498": {
"appid": 537337416,
"offset": "0x1809C2810",
"qua": "V1_WIN_NQ_9.9.26_44498_GW_B"
},
"9.9.26-44725": {
"appid": 537337569,
"qua": "V1_WIN_NQ_9.9.26_44725_GW_B"
},
"9.9.27-45627": {
"appid": 537340060,
"qua": "V1_WIN_NQ_9.9.27_45627_GW_B"
},
"6.9.88-44725": {
"appid": 537337594,
"qua": "V1_MAC_NQ_6.9.88_44725_GW_B"
}
}

View File

@@ -87,6 +87,10 @@
"send": "23B0330",
"recv": "0957648"
},
"3.2.21-42086-arm64": {
"send": "3D6D98C",
"recv": "14797C8"
},
"3.2.21-42086-x64": {
"send": "5B42CF0",
"recv": "2FDA6F0"
@@ -146,5 +150,21 @@
"9.9.26-44498-x64": {
"send": "0A1051C",
"recv": "1D3BC0D"
},
"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

@@ -658,5 +658,21 @@
"9.9.26-44498-x64": {
"send": "2CDAE40",
"recv": "2CDE3C0"
},
"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

@@ -5,6 +5,7 @@ import fs from 'node:fs/promises';
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/napcat-core/index';
import { ILogWrapper } from 'napcat-common/src/log-interface';
import EventEmitter from 'node:events';
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
@@ -263,7 +264,13 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
}
if (element.fileElement) {
return `[文件 ${element.fileElement.fileName}]`;
if (element.fileElement.fileUuid) {
return `[文件 ${element.fileElement.fileName}]`;
} else if (element.elementType === ElementType.TOFURECORD) {
return `[在线文件 ${element.fileElement.fileName}]`;
} else if (element.elementType === ElementType.ONLINEFOLDER) {
return `[在线文件夹 ${element.fileElement.fileName}/]`;
}
}
if (element.videoElement) {
@@ -287,7 +294,11 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
}
if (element.markdownElement) {
return '[Markdown 消息]';
if (element.markdownElement?.mdSummary) {
return element.markdownElement.mdSummary;
} else {
return '[Markdown 消息]';
}
}
if (element.multiForwardMsgElement) {
@@ -296,6 +307,8 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
if (element.elementType === ElementType.GreyTip) {
return '[灰条消息]';
} else if (element.elementType === ElementType.FILE) {
return '[文件发送中]';
}
return `[未实现 (ElementType = ${element.elementType})]`;

View File

@@ -5,6 +5,7 @@ import AppidTable from '@/napcat-core/external/appid.json';
import { LogWrapper } from './log';
import { getMajorPath } from '@/napcat-core/index';
import { QQAppidTableType, QQPackageInfoType, QQVersionConfigType } from 'napcat-common/src/types';
import path from 'node:path';
export class QQBasicInfoWrapper {
QQMainPath: string | undefined;
@@ -21,6 +22,10 @@ export class QQBasicInfoWrapper {
// 基础目录获取
this.context = context;
this.QQMainPath = process.execPath;
if (process.platform === 'darwin' && path.basename(this.QQMainPath) === 'QQ Helper') {
// 实用进程特殊处理 实用进程目录和QQ差远了
this.QQMainPath = path.resolve(path.dirname(this.QQMainPath), '../../../../', 'MacOS', 'QQ');
}
this.QQVersionConfigPath = getQQVersionConfigPath(this.QQMainPath);
// 基础信息获取 无快更则启用默认模板填充
@@ -99,7 +104,10 @@ export class QQBasicInfoWrapper {
}
getAppidV2ByMajor (QQVersion: string) {
const majorPath = getMajorPath(QQVersion);
if (!this.QQMainPath) {
throw new Error('QQMainPath未定义 无法通过Major获取Appid');
}
const majorPath = getMajorPath(QQVersion, this.QQMainPath);
const appid = parseAppidFromMajor(majorPath);
return appid;
}

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

@@ -6,6 +6,8 @@ import {
NTQQSystemApi,
NTQQUserApi,
NTQQWebApi,
NTQQFlashApi,
NTQQOnlineApi,
} from '@/napcat-core/apis';
import { NTQQCollectionApi } from '@/napcat-core/apis/collection';
import {
@@ -23,7 +25,8 @@ 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 { KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/napcat-core/types';
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';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/napcat-core/listeners';
@@ -37,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,
@@ -44,20 +55,23 @@ export enum NapCatCoreWorkingEnv {
Framework = 2,
}
export function loadQQWrapper (QQVersion: string): WrapperNodeApi {
export function loadQQWrapper (execPath: string | undefined, QQVersion: string): WrapperNodeApi {
if (process.env['NAPCAT_WRAPPER_PATH']) {
const wrapperPath = process.env['NAPCAT_WRAPPER_PATH'];
const nativemodule: { exports: WrapperNodeApi; } = { exports: {} as WrapperNodeApi };
process.dlopen(nativemodule, wrapperPath);
return nativemodule.exports;
}
if (!execPath) {
throw new Error('无法加载WrapperexecPath未定义');
}
let appPath;
if (os.platform() === 'darwin') {
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
appPath = path.resolve(path.dirname(execPath), '../Resources/app');
} else if (os.platform() === 'linux') {
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
appPath = path.resolve(path.dirname(execPath), './resources/app');
} else {
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
appPath = path.resolve(path.dirname(execPath), `./versions/${QQVersion}/`);
}
let wrapperNodePath = path.resolve(appPath, 'wrapper.node');
if (!fs.existsSync(wrapperNodePath)) {
@@ -65,21 +79,22 @@ export function loadQQWrapper (QQVersion: string): WrapperNodeApi {
}
// 老版本兼容 未来去掉
if (!fs.existsSync(wrapperNodePath)) {
wrapperNodePath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/wrapper.node`);
wrapperNodePath = path.join(path.dirname(execPath), `./resources/app/versions/${QQVersion}/wrapper.node`);
}
const nativemodule: { exports: WrapperNodeApi; } = { exports: {} as WrapperNodeApi };
process.dlopen(nativemodule, wrapperNodePath);
process.env['NAPCAT_WRAPPER_PATH'] = wrapperNodePath;
return nativemodule.exports;
}
export function getMajorPath (QQVersion: string): string {
export function getMajorPath (execPath: string, QQVersion: string): string {
// major.node
let appPath;
if (os.platform() === 'darwin') {
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
appPath = path.resolve(path.dirname(execPath), '../Resources/app');
} else if (os.platform() === 'linux') {
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
appPath = path.resolve(path.dirname(execPath), './resources/app');
} else {
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
appPath = path.resolve(path.dirname(execPath), `./versions/${QQVersion}/`);
}
let majorPath = path.resolve(appPath, 'major.node');
if (!fs.existsSync(majorPath)) {
@@ -87,7 +102,7 @@ export function getMajorPath (QQVersion: string): string {
}
// 老版本兼容 未来去掉
if (!fs.existsSync(majorPath)) {
majorPath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/major.node`);
majorPath = path.join(path.dirname(execPath), `./resources/app/versions/${QQVersion}/major.node`);
}
return majorPath;
}
@@ -106,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),
@@ -120,12 +145,14 @@ export class NapCatCore {
MsgApi: new NTQQMsgApi(this.context, this),
UserApi: new NTQQUserApi(this.context, this),
GroupApi: new NTQQGroupApi(this.context, this),
FlashApi: new NTQQFlashApi(this.context, this),
OnlineApi: new NTQQOnlineApi(this.context, this),
};
container.bind(NapCatCore).toConstantValue(this);
container.bind(TypedEventEmitter).toConstantValue(this.event);
ReceiverServiceRegistry.forEach((ServiceClass, serviceName) => {
container.bind(ServiceClass).toSelf();
//console.log(`Registering service handler for: ${serviceName}`);
// console.log(`Registering service handler for: ${serviceName}`);
this.context.packetHandler.onCmd(serviceName, ({ seq, hex_data }) => {
const serviceInstance = container.get(ServiceClass);
return serviceInstance.handler(seq, hex_data);
@@ -175,10 +202,17 @@ export class NapCatCore {
async initNapCatCoreListeners () {
const msgListener = new NodeIKernelMsgListener();
// 在线文件/文件夹消息
msgListener.onRecvOnlineFileMsg = (msgs: RawMessage[]) => {
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
};
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
const tips = `[KickedOffLine] [${Info.tipsTitle}] ${Info.tipsDesc}`;
this.context.logger.logError(tips);
this.selfInfo.online = false;
this.event.emit('KickedOffLine', tips);
};
msgListener.onRecvMsg = (msgs) => {
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
@@ -292,4 +326,6 @@ export interface StableNTApiWrapper {
MsgApi: NTQQMsgApi,
UserApi: NTQQUserApi,
GroupApi: NTQQGroupApi;
FlashApi: NTQQFlashApi,
OnlineApi: NTQQOnlineApi,
}

View File

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

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

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

View File

@@ -0,0 +1,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

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

View File

@@ -8,6 +8,7 @@ export interface LoginInitConfig {
commonPath: string;
clientVer: string;
hostName: string;
externalVersion: boolean;
}
export interface PasswordLoginRetType {
@@ -21,7 +22,7 @@ export interface PasswordLoginRetType {
jumpWord: string;
tipsTitle: string;
tipsContent: string;
}
};
}
export interface PasswordLoginArgType {
@@ -55,37 +56,37 @@ export interface QuickLoginResult {
jumpUrl: string,
jumpWord: string,
tipsTitle: string,
tipsContent: string
tipsContent: string;
};
}
export interface NodeIKernelLoginService {
getMsfStatus: () => number;
setLoginMiscData(arg0: string, value: string): unknown;
setLoginMiscData (arg0: string, value: string): unknown;
getMachineGuid(): string;
getMachineGuid (): string;
get(): NodeIKernelLoginService;
get (): NodeIKernelLoginService;
connect(): boolean;
connect (): boolean;
addKernelLoginListener(listener: NodeIKernelLoginListener): number;
addKernelLoginListener (listener: NodeIKernelLoginListener): number;
removeKernelLoginListener(listener: number): void;
removeKernelLoginListener (listener: number): void;
initConfig(config: LoginInitConfig): void;
initConfig (config: LoginInitConfig): void;
getLoginMiscData(data: string): Promise<GeneralCallResult & { value: string }>;
getLoginMiscData (data: string): Promise<GeneralCallResult & { value: string; }>;
getLoginList(): Promise<{
getLoginList (): Promise<{
result: number, // 0是ok
LocalLoginInfoList: LoginListItem[]
LocalLoginInfoList: LoginListItem[];
}>;
quickLoginWithUin(uin: string): Promise<QuickLoginResult>;
quickLoginWithUin (uin: string): Promise<QuickLoginResult>;
passwordLogin(param: PasswordLoginArgType): Promise<unknown>;
passwordLogin (param: PasswordLoginArgType): Promise<QuickLoginResult>;
getQRCodePicture(): boolean;
getQRCodePicture (): boolean;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,17 @@
import { GeneralCallResult } from './common';
export interface NodeIKernelNodeMiscService {
writeVersionToRegistry(version: string): void;
writeVersionToRegistry (version: string): void;
getMiniAppPath(): unknown;
getMiniAppPath (): unknown;
setMiniAppVersion(version: string): unknown;
setMiniAppVersion (version: string): unknown;
wantWinScreenOCR(imagepath: string): Promise<GeneralCallResult>;
wantWinScreenOCR (imagepath: string): Promise<GeneralCallResult>;
SendMiniAppMsg(arg1: string, arg2: string, arg3: string): unknown;
SendMiniAppMsg (arg1: string, arg2: string, arg3: string): unknown;
startNewMiniApp(appfile: string, params: string): unknown;
startNewMiniApp (appfile: string, params: string): unknown;
getQimei36WithNewSdk (): Promise<string>;
}

View File

@@ -1,5 +1,6 @@
export enum GeneralCallResultStatus {
OK = 0,
ERROR = -1,
}
export interface GeneralCallResult {

View File

@@ -0,0 +1,21 @@
export enum fileType {
MP3 = 1,
VIDEO = 2,
DOC = 3,
ZIP = 4,
XLS = 6,
PPT = 7,
CODE = 8,
PDF = 9,
TXT = 10,
UNKNOW = 11,
FOLDER = 25,
IMG = 26,
}
export enum FileStatus {
UPLOADING = 0,
// DOWNLOADED = 1, ??? 不太清楚
OK = 2,
STOP = 3,
}

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';
/*
@@ -66,13 +66,14 @@ export enum ElementType {
YOLOGAMERESULT = 20,
AVRECORD = 21,
FEED = 22,
TOFURECORD = 23,
TOFURECORD = 23, // tofu record?? 在线文件的id是这个
ACEBUBBLE = 24,
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
ONLINEFOLDER = 30, // 在线文件夹
RECOMMENDEDMSG = 43,
ACTIONBAR = 44,
}
@@ -303,11 +304,40 @@ export enum NTVideoType {
VIDEO_FORMAT_WMV = 3,
}
/**
* 闪传图标
*/
export interface FlashTransferIcon {
spec: number;
url: string;
}
/**
* 闪传文件信息
*/
export interface FlashTransferInfo {
filesetId: string;
name: string;
fileSize: string;
thnumbnail: {
id: string;
urls: FlashTransferIcon[];
localCachePath: string;
}
}
/**
* Markdown元素接口
*/
export interface MarkdownElement {
content: string;
style?: {};
processMsg?: string;
mdSummary?: string;
mdExtType?: number;
mdExtInfo?: {
flashTransferInfo: FlashTransferInfo;
}
}
/**

View File

@@ -57,24 +57,24 @@ export interface BaseInfo {
}
// 音乐信息
interface MusicInfo {
export interface MusicInfo {
buf: string;
}
// 视频业务信息
interface VideoBizInfo {
export interface VideoBizInfo {
cid: string;
tvUrl: string;
synchType: string;
}
// 视频信息
interface VideoInfo {
export interface VideoInfo {
name: string;
}
// 扩展在线业务信息
interface ExtOnlineBusinessInfo {
export interface ExtOnlineBusinessInfo {
buf: string;
customStatus: unknown;
videoBizInfo: VideoBizInfo;
@@ -82,12 +82,12 @@ interface ExtOnlineBusinessInfo {
}
// 扩展缓冲区
interface ExtBuffer {
export interface ExtBuffer {
buf: string;
}
// 用户状态
interface UserStatus {
export interface UserStatus {
uid: string;
uin: string;
status: number;
@@ -109,14 +109,14 @@ interface UserStatus {
}
// 特权图标
interface PrivilegeIcon {
export interface PrivilegeIcon {
jumpUrl: string;
openIconList: unknown[];
closeIconList: unknown[];
}
// 增值服务信息
interface VasInfo {
export interface VasInfo {
vipFlag: boolean;
yearVipFlag: boolean;
svipFlag: boolean;
@@ -149,7 +149,7 @@ interface VasInfo {
}
// 关系标志
interface RelationFlags {
export interface RelationFlags {
topTime: string;
isBlock: boolean;
isMsgDisturb: boolean;
@@ -167,7 +167,7 @@ interface RelationFlags {
}
// 通用扩展信息
interface CommonExt {
export interface CommonExt {
constellation: number;
shengXiao: number;
kBloodType: number;
@@ -193,14 +193,14 @@ export enum BuddyListReqType {
}
// 图片信息
interface Pic {
export interface Pic {
picId: string;
picTime: number;
picUrlMap: Record<string, string>;
}
// 照片墙
interface PhotoWall {
export interface PhotoWall {
picList: Pic[];
}
@@ -247,7 +247,7 @@ export interface ModifyProfileParams {
nick: string;
longNick: string;
sex: NTSex;
birthday: { birthday_year: string, birthday_month: string, birthday_day: string };
birthday: { birthday_year: string, birthday_month: string, birthday_day: string; };
location: unknown;
}

View File

@@ -1,5 +1,6 @@
import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from './adapters';
import {
GeneralCallResult,
NodeIKernelAvatarService,
NodeIKernelBuddyService,
NodeIKernelGroupService,
@@ -27,77 +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';
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 (): Promise<string>;
pathIsReadableAndWriteable(path: string): unknown; // 直接的猜测
pathIsReadableAndWriteable (path: string, type: number): Promise<number>; // type 2 , result 0 成功
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,
@@ -105,7 +107,7 @@ export interface NodeQQNTWrapperUtil {
arg4: number,
): unknown;
reportCountIndicators(
reportCountIndicators (
arg0: string,
arg1: Map<unknown, unknown>,
arg2: string,
@@ -113,7 +115,7 @@ export interface NodeQQNTWrapperUtil {
arg4: boolean,
): unknown;
reportValueIndicators(
reportValueIndicators (
arg0: string,
arg1: Map<unknown, unknown>,
arg2: string,
@@ -121,140 +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 (): Promise<Map<unknown, 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;
getDirectSessionService(): unknown;
getFlashTransferService (): NodeIKernelFlashTransferService;
getRDeliveryService(): unknown;
getDirectSessionService (): unknown;
getAvatarService(): NodeIKernelAvatarService;
getRDeliveryService (): unknown;
getFeedChannelService(): unknown;
getAvatarService (): NodeIKernelAvatarService;
getYellowFaceService(): unknown;
getFeedChannelService (): unknown;
getCollectionService(): NodeIKernelCollectionService;
getYellowFaceService (): unknown;
getSettingService(): unknown;
getCollectionService (): NodeIKernelCollectionService;
getQiDianService(): unknown;
getSettingService (): unknown;
getFileAssistantService(): unknown;
getQiDianService (): unknown;
getGuildService(): unknown;
getFileAssistantService (): unknown;
getSkinService(): unknown;
getGuildService (): unknown;
getTestPerformanceService(): NodeIkernelTestPerformanceService;
getSkinService (): unknown;
getQQPlayService(): unknown;
getTestPerformanceService (): NodeIkernelTestPerformanceService;
getDbToolsService(): unknown;
getQQPlayService (): unknown;
getUixConvertService(): NodeIKernelUixConvertService;
getDbToolsService (): unknown;
getOnlineStatusService(): unknown;
getUixConvertService (): NodeIKernelUixConvertService;
getRemotingService(): unknown;
getOnlineStatusService (): unknown;
getGroupTabService(): unknown;
getRemotingService (): unknown;
getGroupSchoolService(): unknown;
getGroupTabService (): unknown;
getLiteBusinessService(): unknown;
getGroupSchoolService (): unknown;
getGuildMsgService(): unknown;
getLiteBusinessService (): unknown;
getLockService(): unknown;
getGuildMsgService (): unknown;
getMSFService(): NodeIKernelMSFService;
getLockService (): unknown;
getGuildHotUpdateService(): unknown;
getMSFService (): NodeIKernelMSFService;
getAVSDKService(): unknown;
getGuildHotUpdateService (): unknown;
getRecentContactService(): NodeIKernelRecentContactService;
getAVSDKService (): unknown;
getConfigMgrService(): unknown;
getRecentContactService (): NodeIKernelRecentContactService;
getConfigMgrService (): unknown;
}
export interface EnginInitDesktopConfig {
@@ -268,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

@@ -32,6 +32,7 @@ if (versionFolders.length === 0) {
const BASE_DIR = path.join(versionsDir, selectedFolder, 'resources', 'app');
console.log(`BASE_DIR: ${BASE_DIR}`);
const TARGET_DIR = path.join(__dirname, 'dist');
const TARGET_WIN64_DIR = path.join(__dirname, 'dist', 'win64');
const QQNT_FILE = path.join(__dirname, 'QQNT.dll');
const NAPCAT_MJS_PATH = path.join(__dirname, '..', 'napcat-shell', 'dist', 'napcat.mjs');
@@ -46,6 +47,12 @@ const itemsToCopy = [
'package.json',
'QBar.dll',
'wrapper.node',
'LightQuic.dll'
];
const win64ItemsToCopy = [
'SSOShareInfoHelper64.dll',
'parent-ipc-core-x64.dll'
];
async function copyAll () {
@@ -53,13 +60,23 @@ async function copyAll () {
const configPath = path.join(TARGET_DIR, 'config.json');
const allItemsExist = await fs.pathExists(qqntDllPath) &&
await fs.pathExists(configPath) &&
(await Promise.all(itemsToCopy.map(item => fs.pathExists(path.join(TARGET_DIR, item))))).every(exists => exists);
(await Promise.all(itemsToCopy.map(item => fs.pathExists(path.join(TARGET_DIR, item))))).every(exists => exists) &&
(await Promise.all(win64ItemsToCopy.map(item => fs.pathExists(path.join(TARGET_WIN64_DIR, item))))).every(exists => exists);
if (!allItemsExist) {
console.log('Copying required files...');
await fs.ensureDir(TARGET_DIR);
await fs.ensureDir(TARGET_WIN64_DIR);
await fs.copy(QQNT_FILE, qqntDllPath, { overwrite: true });
await fs.copy(path.join(versionsDir, 'config.json'), configPath, { overwrite: true });
// 复制 win64 目录下的文件
await Promise.all(win64ItemsToCopy.map(async (item) => {
await fs.copy(path.join(BASE_DIR, 'win64', item), path.join(TARGET_WIN64_DIR, item), { overwrite: true });
console.log(`Copied ${item} to win64`);
}));
// 复制根目录下的文件
await Promise.all(itemsToCopy.map(async (item) => {
await fs.copy(path.join(BASE_DIR, item), path.join(TARGET_DIR, item), { overwrite: true });
console.log(`Copied ${item}`);
@@ -73,10 +90,12 @@ async function copyAll () {
process.env.NAPCAT_QQ_PACKAGE_INFO_PATH = path.join(TARGET_DIR, 'package.json');
process.env.NAPCAT_QQ_VERSION_CONFIG_PATH = path.join(TARGET_DIR, 'config.json');
process.env.NAPCAT_DISABLE_PIPE = '1';
// 禁用重启和多进程功能
process.env.NAPCAT_DISABLE_MULTI_PROCESS = '1';
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';
@@ -38,7 +38,7 @@ export async function NCoreInitFramework (
const logger = new LogWrapper(pathWrapper.logsPath);
await applyPendingUpdates(pathWrapper, logger);
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
const wrapper = loadQQWrapper(basicInfoWrapper.QQMainPath, basicInfoWrapper.getFullQQVersion());
const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用
// nativePacketHandler.onAll((packet) => {
// console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data);
@@ -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();

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