Compare commits

..

417 Commits

Author SHA1 Message Date
冷曦
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
手瓜一十雪
fbccf8be24 Make emoji_likes_list optional in OB11Message
Changed the OB11Message interface to make emoji_likes_list optional and updated GetMsg to initialize emoji_likes_list as an empty array before populating it. This prevents errors when the field is missing and improves type safety.
2026-01-13 17:08:31 +08:00
手瓜一十雪
37ae17b53f Remove unused imports and update method params
Removed the unused 'readFileSync' import from ffmpeg-addon-adapter.ts. Updated parameter names in convertToNTSilkTct method of ffmpeg-exec-adapter.ts to use underscores, indicating unused variables.
2026-01-13 17:01:00 +08:00
手瓜一十雪
35566970fd Update pnpm-lock.yaml 2026-01-13 16:57:00 +08:00
手瓜一十雪
e70cd1eff7 Update QQ download links to version 44343
Updated Windows and Linux QQ download links in default.md and release_note_prompt.txt to point to version 9.9.26-44343 and 3.2.23-44343, replacing previous 40990 links.
2026-01-13 16:54:57 +08:00
手瓜一十雪
fbd3241845 Improve version info UI and update model config
Refined the system info version comparison layout for better responsiveness and readability, especially on smaller screens. Updated the OpenRouter model name in the release workflow and improved dark mode text color handling in sidebar menu items.
2026-01-13 16:50:46 +08:00
手瓜一十雪
cf69ccdbc9 Add emoji likes list support to message types
Introduces the emojiLikesList property to RawMessage and maps it to the new emoji_likes_list field in OB11Message, which is populated in the GetMsg action. Also updates type definitions for stricter typing and consistency.
2026-01-13 16:43:00 +08:00
Makoto
f3de4d48d3 feat: add settings field to group notice API response (#1505)
* feat: add settings field to group notice API response

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

* refactor: make settings field optional for backward compatibility

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

* Update GetGroupNotice.ts

---------

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

Remove silk-wasm dependency and refactor audio handling

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

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

* fix: simplify logWarn to match upstream style

* fix: remove extra closing brace that broke class structure

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

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

* fix: 完善插件模板

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

* Refactor build and release workflows, update dependencies

Switch build scripts and workflows from npm to pnpm, update build and artifact paths, and simplify release workflow by removing version detection and changelog steps. Add new dependencies (silk-wasm, express, ws, node-pty-prebuilt-multiarch), update exports in package.json files, and add vite config for napcat-framework. Also, rename manifest.json for framework package and fix static asset copying in shell build config.
2025-11-13 15:39:42 +08:00
手瓜一十雪
e2486606f9 feat: 正式终止once支持 2025-11-13 11:22:46 +08:00
Mlikiowa
f8eb368cdb release: v4.9.42 2025-11-13 02:48:59 +00:00
手瓜一十雪
9ce51fb082 fix: #1380 2025-11-13 10:47:40 +08:00
Mlikiowa
89e50be1e9 release: v4.9.41 2025-11-13 01:28:44 +00:00
手瓜一十雪
1259dcea5b Add new app, packet, and native mappings for 42086 builds
Updated appid.json, packet.json, and napi2native.json to include mappings for new 42086 builds across multiple platforms. Added sendMsfRequest method to NodeIKernelMSFService interface and made minor formatting improvements. Commented out a debug method in NodeIDependsAdapter.
2025-11-13 09:28:17 +08:00
手瓜一十雪
b4900066b3 fix: #1367 2025-11-12 13:50:35 +08:00
Mlikiowa
28acd94cbd release: v4.9.40 2025-11-12 05:38:39 +00:00
手瓜一十雪
7aedacb27f feat: png video截图 2025-11-12 13:38:08 +08:00
手瓜一十雪
c9b45ec1a2 Fix busiId comparison in parsePrivateMsgEvent
Ensure busiId is compared as a string in parsePrivateMsgEvent to handle cases where busiId may not be a string type. This prevents potential mismatches due to type differences.
2025-11-12 12:54:09 +08:00
Mlikiowa
54cacc30e4 release: v4.9.38 2025-11-12 04:20:16 +00:00
手瓜一十雪
9b26fc99d3 fix 2025-11-12 12:19:39 +08:00
手瓜一十雪
204846b404 Add success log messages to packet handler
Added log statements to indicate successful loading and initialization in NativePacketHandler. This improves traceability and debugging by providing clear feedback in the logs.
2025-11-12 12:04:48 +08:00
手瓜一十雪
3d654791b9 Add platform and arch to native module path
Prepends process.platform and process.arch to the native MoeHoo module filename, ensuring the correct binary is loaded for the current environment.
2025-11-12 12:00:36 +08:00
手瓜一十雪
63f42f1592 Improve native packet handler loading and error checks
Adds a 'loaded' flag to track native module loading status and improves error handling during initialization. The constructor now attempts to load the native module and logs warnings or errors if loading fails, while the init method checks the loaded status before proceeding.
2025-11-12 11:59:06 +08:00
Mlikiowa
0ab0b939da release: v4.9.36 2025-11-11 12:30:58 +00:00
手瓜一十雪
522a123f9a Update native MoeHoo binaries for Linux
Replaces MoeHoo.linux.arm64.node and MoeHoo.linux.x64.node with new versions. This may include bug fixes, performance improvements, or compatibility updates for the native packet module.
2025-11-11 20:30:33 +08:00
Mlikiowa
ec9f8d6e12 release: v4.9.35 2025-11-10 04:04:07 +00:00
手瓜一十雪
3c750c75a9 Add Linux native modules for arm64 and x64
Added prebuilt native binaries MoeHoo.linux.arm64.node and MoeHoo.linux.x64.node to support Linux on arm64 and x64 architectures.
2025-11-10 12:03:42 +08:00
手瓜一十雪
5b2b1f499b Remove Linux native packet binaries
Deleted MoeHoo.linux.arm64.node and MoeHoo.linux.x64.node from src/native/packet. These binaries are no longer needed or are being replaced.
2025-11-10 12:02:58 +08:00
手瓜一十雪
531ffcd55d Update MoeHoo.linux.x64.node binary
Replaces the MoeHoo.linux.x64.node native module with a new version. Details of the changes depend on the updated binary implementation.
2025-11-10 12:02:45 +08:00
Mlikiowa
068e4d8bb5 release: v4.9.33 2025-11-09 04:54:38 +00:00
手瓜一十雪
5dc33e78ad Add napiloader scripts and binaries, update Vite config
Introduces napiloader batch scripts and binaries (napiloader.dll, napimain.exe) for Windows integration. Updates vite.config.ts to include napiloader files in the Framework build output and refactors plugin target lists for improved distribution packaging.
2025-11-09 12:54:15 +08:00
Mlikiowa
d76a2170a0 release: v4.9.32 2025-11-08 04:26:49 +00:00
手瓜一十雪
ec2af3120c Refactor FFmpeg file conversion logic and API
Unified file conversion in FFmpegAddonAdapter to use decodeAudioToFmt for all formats, updated FFmpeg interface and service to support new conversion method, and added adapter name checks in GetRecord and DownloadFileRecordStream for optimized conversion flow. Updated native addon binaries to support these changes.
2025-11-08 12:22:51 +08:00
Mlikiowa
8de49a3109 release: v4.9.30 2025-11-08 02:30:01 +00:00
手瓜一十雪
dbb5a0022e Update ffmpeg native binaries for all platforms
Replaces ffmpegAddon binaries for Darwin ARM64, Linux ARM64, Linux x64, and Windows x64 with new versions. Ensures compatibility and includes latest native changes.
2025-11-08 10:28:59 +08:00
Mlikiowa
cb8c8d6b57 release: v4.9.29 2025-11-07 12:01:08 +00:00
手瓜一十雪
93c140ed4e Improve error message for group notice image upload
Enhanced the error message when group notice image upload fails to include the error details from the upload API response.
2025-11-07 20:00:17 +08:00
手瓜一十雪
457b072f0e Update NTQQ build version check to 41679
Changed the minimum NTQQ build version required for buddy list operations from 40990 to 41679 in NTQQFriendApi. Ensures compatibility with newer NTQQ builds.
2025-11-07 19:57:07 +08:00
手瓜一十雪
1869493473 Update getBuddyV2ExWithCate for NTQQ build compatibility
Modified getBuddyV2ExWithCate to conditionally call getBuddyListV2 with an extra boolean argument for NTQQ builds >= 40990, ensuring compatibility with different API signatures.
2025-11-07 19:50:11 +08:00
Mlikiowa
f33c66ce15 release: v4.9.28 2025-11-07 11:35:58 +00:00
手瓜一十雪
e8d6f86458 feat: 修复一些问题 2025-11-07 19:35:35 +08:00
手瓜一十雪
a000ffdf0d fix: buddylist 2025-11-07 19:29:39 +08:00
Mlikiowa
202338a160 release: v4.9.27 2025-11-07 09:05:14 +00:00
手瓜一十雪
e3eb129a52 Update ffmpeg native binaries for all platforms
Replaces ffmpegAddon binaries for Darwin ARM64, Linux ARM64, Linux x64, and Windows x64 with new versions. Ensures compatibility and includes latest native changes.
2025-11-07 17:04:29 +08:00
手瓜一十雪
7654e9f2bb Update ffmpeg native binaries for all platforms
Replaces ffmpegAddon binaries for Darwin ARM64, Linux ARM64, Linux x64, and Windows x64. Ensures compatibility and includes latest native code updates.
2025-11-07 12:54:36 +08:00
Mlikiowa
a60c03f42f release: v4.9.26 2025-11-06 15:15:20 +00:00
手瓜一十雪
60aae228a1 feat: add Node NapCat Test 2025-11-06 17:57:57 +08:00
手瓜一十雪
b1417f9b56 feat: support node test 2025-11-06 10:57:54 +08:00
手瓜一十雪
eeeaddbb60 Remove development guide from README
Deleted the section detailing code checks and TypeScript error notes prior to code submission. This streamlines the README and removes internal development instructions.
2025-11-05 12:24:21 +08:00
Mlikiowa
b1109022bb release: v4.9.25 2025-11-04 13:55:32 +00:00
手瓜一十雪
1807789511 Add send and recv mappings for 3.2.21-41857-arm64
Updated napi2native.json to include the 'send' and 'recv' addresses for the 3.2.21-41857-arm64 version, which were previously missing.
2025-11-04 21:55:09 +08:00
手瓜一十雪
49a5b631c2 Add support for 41857 app versions and update mappings
Added new entries for version 41857 in appid.json, napi2native.json, and packet.json for Windows, Linux, and Mac platforms. Updated mappings for send/recv addresses to support the latest application versions.
2025-11-04 21:23:41 +08:00
Mlikiowa
e7aaec81e2 release: v4.9.24 2025-11-04 09:53:36 +00:00
phelogges
05f8e8f3c3 fix: 修复了Unix终端打开失败的bug (#1355)
Bug复现:
mlikiowa/napcat-docker:v4.9.23,登陆账号后,在WebUI中打开系统终端失败,查看容器日志报错如下
Failed to create terminal: TypeError: Cannot read properties of undefined (reading 'fork')
    at new UnixTerminal (file:///app/napcat/napcat.mjs:67721:22)
    at spawn (file:///app/napcat/napcat.mjs:67873:10)
    at TerminalManager.createTerminal (file:///app/napcat/napcat.mjs:67963:17)
    at CreateTerminalHandler (file:///app/napcat/napcat.mjs:68069:36)
    at Layer.handleRequest (/app/napcat/node_modules/router/lib/layer.js:152:17)
    at next (/app/napcat/node_modules/router/lib/route.js:157:13)
    at Route.dispatch (/app/napcat/node_modules/router/lib/route.js:117:3)
    at handle (/app/napcat/node_modules/router/index.js:435:11)
    at Layer.handleRequest (/app/napcat/node_modules/router/lib/layer.js:152:17)
    at /app/napcat/node_modules/router/index.js:295:15

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

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

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

* lint: napcat.webui

* lint: napcat.webui

* lint: napcat.core

* build: fix

* lint: napcat.webui

* refactor: 重构eslint

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

二者任选其一
2025-10-12 21:13:56 +08:00
手瓜一十雪
32e886e53b Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-10-12 20:38:38 +08:00
风小七
315d847f06 Update helper.ts (#1311)
修复256级以后等级清零的问题。
2025-10-12 20:36:31 +08:00
手瓜一十雪
381d320967 feat: Add image and record stream download actions
Introduces BaseDownloadStream as a shared base class for streaming file downloads. Adds DownloadFileImageStream and DownloadFileRecordStream for image and audio file streaming with support for format conversion. Refactors DownloadFileStream to use the new base class, and updates action registration and router to include the new actions.
2025-10-12 15:50:34 +08:00
Clansty
2f5b62decb fix: get_group_info ownerUin is "0" 2025-10-09 02:30:08 +08:00
Mlikiowa
2afdb2a0da release: v4.8.119 2025-10-03 04:36:00 +00:00
手瓜一十雪
5bfbf92c21 Update key for 9.9.22-40362 offsets in offset.json
Changed the key from '9.9.22-40362' to '9.9.22-40362-x64' to clarify architecture specificity in the offsets mapping.
2025-10-03 12:35:28 +08:00
Mlikiowa
a775a0dde9 release: v4.8.118 2025-10-03 04:34:56 +00:00
手瓜一十雪
d7f00c0594 Fix batch variable quoting and case consistency
Updated batch scripts to use proper variable quoting and consistent casing for 'QQPath'. This improves reliability when handling paths with spaces and ensures environment variable names are used consistently.
2025-10-03 12:34:29 +08:00
Mlikiowa
77c8f874b6 release: v4.8.117 2025-10-03 04:21:22 +00:00
手瓜一十雪
fb0a20919b Add support for version 9.9.22-40362
Updated appid.json and offset.json to include entries for version 9.9.22-40362, specifying the new appid, qua, and offset values for send and recv.
2025-10-03 12:20:21 +08:00
手瓜一十雪
0300ba4648 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-10-03 12:20:15 +08:00
时瑾
d472eee777 fix: Reset pagination when navigating between directories in file manager
Fix: Reset pagination when navigating between directories in file manager
2025-10-02 09:40:37 +08:00
copilot-swe-agent[bot]
41bd06e50a Fix: Reset pagination to page 1 when navigating directories
Co-authored-by: sj817 <74231782+sj817@users.noreply.github.com>
2025-10-02 01:16:14 +00:00
copilot-swe-agent[bot]
97334dfbf5 Initial plan 2025-10-02 01:10:22 +00:00
手瓜一十雪
e3d8c8e940 fix: #1260 2025-09-29 16:39:37 +08:00
1484 changed files with 92636 additions and 80903 deletions

View File

@@ -15,10 +15,10 @@ charset = utf-8
# 4 space indentation
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
indent_style = space
indent_size = 4
indent_size = 2
[*.bat]
charset = latin1
# Unfortunately, EditorConfig doesn't support space configuration inside import braces directly.
# You'll need to rely on your linter/formatter like ESLint or Prettier for that.
# You'll need to rely on your linter/formatter like ESLint or Prettier for that.

View File

@@ -1,2 +0,0 @@
VITE_BUILD_TYPE = Production
VITE_BUILD_PLATFORM = Framework

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
VITE_BUILD_TYPE = Production
VITE_BUILD_PLATFORM = Universal

View File

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

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

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

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

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

111
.github/prompt/release_note_prompt.txt vendored Normal file
View File

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

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

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

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

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

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

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

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

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

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

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

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

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

153
.github/workflows/auto-release.yml vendored Normal file
View File

@@ -0,0 +1,153 @@
name: Auto Release Docker
on:
release:
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:
- name: Trigger NapCat-Docker docker-publish workflow
env:
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
run: |
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/NapNeko/NapCat-Docker/actions/workflows/docker-publish.yml/dispatches \
-d '{"ref":"main"}'
framework-docker:
runs-on: ubuntu-latest
steps:
- name: Trigger NapCat-Framework-Docker docker-publish workflow
env:
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
run: |
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/NapNeko/NapCat.Docker.Framework/actions/workflows/docker-image.yml/dispatches \
-d '{"ref":"main"}'
appimage-shell-docker:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Get Latest NapCat Version
id: get_version
run: |
# 获取当前仓库的最新 tag
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1))
# 输出调试信息
echo "Debug: Latest NapCat Version is ${latest_tag}"
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
- name: Trigger Release NapCat AppImage Workflow
env:
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
NAPCAT_VERSION: ${{ env.latest_tag }}
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
run: |
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_version_x86_64=${QQ_VERSION_X86_64}, qq_version_arm64=${QQ_VERSION_ARM64}"
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/NapNeko/NapCatAppImageBuild/actions/workflows/release.yml/dispatches \
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_version_x86_64\":\"${QQ_VERSION_X86_64}\",\"qq_version_arm64\":\"${QQ_VERSION_ARM64}\"}}"
node-shell-docker:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Get Latest NapCat Version
id: get_version
run: |
# 获取当前仓库的最新 tag
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1))
# 输出调试信息
echo "Debug: Latest NapCat Version is ${latest_tag}"
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
- name: Trigger Release NapCat AppImage Workflow
env:
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
NAPCAT_VERSION: ${{ env.latest_tag }}
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
run: |
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_url_amd64=${QQ_VERSION_X86_64}, qq_url_arm64=${QQ_VERSION_ARM64}"
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/NapNeko/NapCatLinuxNodeLoader/actions/workflows/release.yml/dispatches \
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_url_amd64\":\"${QQ_VERSION_X86_64}\",\"qq_url_arm64\":\"${QQ_VERSION_ARM64}\"}}"
- 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

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

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

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

View File

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

3
.gitignore vendored
View File

@@ -14,3 +14,6 @@ devconfig/*
*.db
checkVersion.sh
bun.lockb
tests/run/
guild1.db-wal
guild1.db-shm

View File

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

111
.vscode/launch.json vendored
View File

@@ -2,114 +2,11 @@
"version": "0.2.0",
"configurations": [
{
"type": "node",
"type": "node-terminal",
"request": "launch",
"name": "dev:shell",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:shell"
]
},
{
"type": "node",
"request": "launch",
"name": "build:shell",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:shell"
]
},
{
"type": "node",
"request": "launch",
"name": "build:universal",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:universal"
]
},
{
"type": "node",
"request": "launch",
"name": "build:framework",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:framework"
]
},
{
"type": "node",
"request": "launch",
"name": "build:webui",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:webui"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:universal",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:universal"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:framework",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:framework"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:webui",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:webui"
]
},
{
"type": "node",
"request": "launch",
"name": "lint",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"lint"
]
},
{
"type": "node",
"request": "launch",
"name": "depend",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"depend"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:depend",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:depend"
]
"name": "调试程序",
"command": "pnpm run dev:shell",
"cwd": "${workspaceFolder}"
}
]
}

52
.vscode/settings.json vendored
View File

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

View File

@@ -3,8 +3,6 @@
# NapCat
_Modern protocol-side framework implemented based on NTQQ._
> 云起兮风生,心向远方兮路未曾至.
@@ -14,6 +12,7 @@ _Modern protocol-side framework implemented based on NTQQ._
---
## New Feature
在 v4.8.115+ 版本开始
1. NapCatQQ 支持 [Stream Api](https://napneko.github.io/develop/file)
@@ -23,32 +22,35 @@ _Modern protocol-side framework implemented based on NTQQ._
- [2] 采用字符串可以解决扩展到int64的问题同时也可以解决部分语言如JavaScript对大整数支持不佳的问题增加极少成本。
## Welcome
+ NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
- NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
- NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## Feature
+ **Easy to Use**
- **Easy to Use**
- 作为初学者能够轻松使用.
+ **Quick and Efficient**
- **Quick and Efficient**
- 在低内存操作系统长时运行.
+ **Rich API Interface**
- **Rich API Interface**
- 完整实现了大部分标准接口.
+ **Stable and Reliable**
- **Stable and Reliable**
- 持续稳定的开发与维护.
## Quick Start
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
**首次使用**请务必查看如下文档看使用教程
> 项目非盈利,对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
> 项目非盈利,涉及 对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
## Link
| Docs | [![Github.IO](https://img.shields.io/badge/docs%20on-Github.IO-orange)](https://napneko.github.io/) | [![Cloudflare.Worker](https://img.shields.io/badge/docs%20on-Cloudflare.Worker-black)](https://doc.napneko.icu/) | [![Cloudflare.HKServer](https://img.shields.io/badge/docs%20on-Cloudflare.HKServer-informational)](https://napcat.napneko.icu/) |
|:-:|:-:|:-:|:-:|
| Docs | [![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) |
@@ -64,20 +66,22 @@ _Modern protocol-side framework implemented based on NTQQ._
## Thanks
+ [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
+ [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
+ [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下
+ [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) 基于NapCat的消息导出工具 在此推荐一下
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) 基于NapCat的消息导出工具 在此推荐一下
+ 不过最最重要的 还是需要感谢屏幕前的你哦~
- 不过最最重要的 还是需要感谢屏幕前的你哦~
---
## License
本项目采用 混合协议 开源,因此使用本项目时,你需要注意以下几点:
1. 第三方库代码或修改部分遵循其原始开源许可.
2. 本项目获取部分项目授权而不受部分约束
2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE).

52
eslint.config.js Normal file
View File

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

View File

@@ -1,32 +0,0 @@
import eslint from '@eslint/js';
import tsEslintPlugin from '@typescript-eslint/eslint-plugin';
import tsEslintParser from '@typescript-eslint/parser';
import globals from "globals";
const customTsFlatConfig = [
{
name: 'typescript-eslint/base',
languageOptions: {
parser: tsEslintParser,
sourceType: 'module',
globals: {
...globals.browser,
...globals.node,
NodeJS: 'readonly', // 添加 NodeJS 全局变量
},
},
files: ['**/*.{ts,tsx}'],
rules: {
...tsEslintPlugin.configs.recommended.rules,
'quotes': ['error', 'single'], // 使用单引号
'semi': ['error', 'always'], // 强制使用分号
'indent': ['error', 4], // 使用 4 空格缩进
},
plugins: {
'@typescript-eslint': tsEslintPlugin,
},
ignores: ['src/webui/**'], // 忽略 src/webui/ 目录所有文件
},
];
export default [eslint.configs.recommended, ...customTsFlatConfig];

Binary file not shown.

BIN
external/logo.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

View File

@@ -1,4 +0,0 @@
@echo off
REM ./launcher.bat 123456
REM ./launcher-win10.bat 123456
REM 带有REM的为注释 删掉你需要的系统的那行REM这三个单词 修改QQ本脚本启动即可

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 KiB

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -1 +0,0 @@
VITE_DEBUG_BACKEND_URL="http://127.0.0.1:6099"

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,59 +0,0 @@
import { Button } from '@heroui/button'
import clsx from 'clsx'
import toast from 'react-hot-toast'
import { IoMdRefresh } from 'react-icons/io'
export interface SaveButtonsProps {
onSubmit: () => void
reset: () => void
refresh?: () => void
isSubmitting: boolean
className?: string
}
const SaveButtons: React.FC<SaveButtonsProps> = ({
onSubmit,
reset,
isSubmitting,
refresh,
className
}) => (
<div
className={clsx(
'max-w-full mx-3 w-96 flex flex-col justify-center gap-3',
className
)}
>
<div className="flex items-center justify-center gap-2 mt-5">
<Button
color="default"
onPress={() => {
reset()
toast.success('重置成功')
}}
>
</Button>
<Button
color="primary"
isLoading={isSubmitting}
onPress={() => onSubmit()}
>
</Button>
{refresh && (
<Button
isIconOnly
color="secondary"
radius="full"
variant="flat"
onPress={() => refresh()}
>
<IoMdRefresh size={24} />
</Button>
)}
</div>
</div>
)
export default SaveButtons

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,58 +0,0 @@
import { Card, CardBody } from '@heroui/card'
import clsx from 'clsx'
import { title } from '@/components/primitives'
export interface NetworkItemDisplayProps {
count: number
label: string
size?: 'sm' | 'md'
}
const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
count,
label,
size = 'md'
}) => {
return (
<Card
className={clsx(
'bg-opacity-60 shadow-sm md:rounded-3xl',
size === 'md'
? 'col-span-8 md:col-span-2 bg-primary-50 shadow-primary-100'
: 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200'
)}
shadow="sm"
>
<CardBody className="items-center md:gap-1 p-1 md:p-2">
<div
className={clsx(
'flex-1',
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
title({
color: size === 'md' ? 'pink' : 'yellow',
size
})
)}
>
{count}
</div>
<div
className={clsx(
'whitespace-nowrap text-nowrap flex-shrink-0',
size === 'md' ? 'text-sm md:text-base' : 'text-xs md:text-sm',
title({
color: size === 'md' ? 'pink' : 'yellow',
shadow: true,
size: 'xxs'
})
)}
>
{label}
</div>
</CardBody>
</Card>
)
}
export default NetworkItemDisplay

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,97 +0,0 @@
import { Button } from '@heroui/button'
import {
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Modal as NextUIModal,
useDisclosure
} from '@heroui/modal'
import React from 'react'
export interface ModalProps {
content: React.ReactNode
title?: React.ReactNode
size?: React.ComponentProps<typeof NextUIModal>['size']
scrollBehavior?: React.ComponentProps<typeof NextUIModal>['scrollBehavior']
onClose?: () => void
onConfirm?: () => void
onCancel?: () => void
backdrop?: 'opaque' | 'blur' | 'transparent'
showCancel?: boolean
dismissible?: boolean
confirmText?: string
cancelText?: string
}
const Modal: React.FC<ModalProps> = React.memo((props) => {
const {
backdrop = 'blur',
title,
content,
showCancel = true,
dismissible,
confirmText = '确定',
cancelText = '取消',
onClose,
onConfirm,
onCancel,
...rest
} = props
const { onClose: onNativeClose } = useDisclosure()
return (
<NextUIModal
defaultOpen
backdrop={backdrop}
isDismissable={dismissible}
onClose={() => {
onClose?.()
onNativeClose()
}}
classNames={{
backdrop: 'z-[99]',
wrapper: 'z-[99]'
}}
{...rest}
>
<ModalContent>
{(nativeClose) => (
<>
{title && (
<ModalHeader className="flex flex-col gap-1">{title}</ModalHeader>
)}
<ModalBody className="break-all">{content}</ModalBody>
<ModalFooter>
{showCancel && (
<Button
color="primary"
variant="light"
onPress={() => {
onCancel?.()
nativeClose()
}}
>
{cancelText}
</Button>
)}
<Button
color="primary"
onPress={() => {
onConfirm?.()
nativeClose()
}}
>
{confirmText}
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</NextUIModal>
)
})
Modal.displayName = 'Modal'
export default Modal

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,61 +0,0 @@
import { Card, CardBody } from '@heroui/card'
import { Image } from '@heroui/image'
import clsx from 'clsx'
import { BsTencentQq } from 'react-icons/bs'
import { SelfInfo } from '@/types/user'
import PageLoading from './page_loading'
export interface QQInfoCardProps {
data?: SelfInfo
error?: Error
loading?: boolean
}
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
return (
<Card
className="relative bg-primary-100 bg-opacity-60 overflow-hidden flex-shrink-0 shadow-md shadow-primary-300 dark:shadow-primary-50"
shadow="none"
radius="lg"
>
<PageLoading loading={loading} />
{error ? (
<CardBody className="items-center gap-1 justify-center">
<div className="flex-1 text-content1-foreground">Error</div>
<div className="whitespace-nowrap text-nowrap flex-shrink-0">
{error.message}
</div>
</CardBody>
) : (
<CardBody className="flex-row items-center gap-2 overflow-hidden relative">
<div className="absolute right-0 bottom-0 text-5xl text-primary-400">
<BsTencentQq />
</div>
<div className="relative flex-shrink-0 z-10">
<Image
src={
data?.avatarUrl ??
`https://q1.qlogo.cn/g?b=qq&nk=${data?.uin}&s=1`
}
className="shadow-md rounded-full w-12 aspect-square"
/>
<div
className={clsx(
'w-4 h-4 rounded-full absolute right-0.5 bottom-0 border-2 border-primary-100 z-10',
data?.online ? 'bg-green-500' : 'bg-gray-500'
)}
></div>
</div>
<div className="flex-col justify-center">
<div className="text-lg truncate">{data?.nick}</div>
<div className="text-primary-500 text-sm">{data?.uin}</div>
</div>
</CardBody>
)}
</Card>
)
}
export default QQInfoCard

View File

@@ -1,24 +0,0 @@
import { Spinner } from '@heroui/spinner'
import { QRCodeSVG } from 'qrcode.react'
interface QrCodeLoginProps {
qrcode: string
}
const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode }) => {
return (
<div className="flex flex-col items-center">
<div className="bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden">
{!qrcode && (
<div className="absolute left-2 top-2 right-2 bottom-2 bg-white bg-opacity-50 backdrop-blur flex items-center justify-center">
<Spinner color="primary" />
</div>
)}
<QRCodeSVG size={180} value={qrcode} />
</div>
<div className="mt-5 text-center">使QQ或者TIM扫描上方二维码</div>
</div>
)
}
export default QrCodeLogin

View File

@@ -1,93 +0,0 @@
import { Button } from '@heroui/button'
import { Image } from '@heroui/image'
import clsx from 'clsx'
import { motion } from 'motion/react'
import React from 'react'
import { IoMdLogOut } from 'react-icons/io'
import { MdDarkMode, MdLightMode } from 'react-icons/md'
import useAuth from '@/hooks/auth'
import useDialog from '@/hooks/use-dialog'
import { useTheme } from '@/hooks/use-theme'
import logo from '@/assets/images/logo.png'
import type { MenuItem } from '@/config/site'
import Menus from './menus'
interface SideBarProps {
open: boolean
items: MenuItem[]
}
const SideBar: React.FC<SideBarProps> = (props) => {
const { open, items } = props
const { toggleTheme, isDark } = useTheme()
const { revokeAuth } = useAuth()
const dialog = useDialog()
const onRevokeAuth = () => {
dialog.confirm({
title: '退出登录',
content: '确定要退出登录吗?',
onConfirm: revokeAuth
})
}
return (
<motion.div
className={clsx(
'overflow-hidden fixed top-0 left-0 h-full z-50 bg-background md:bg-transparent md:static shadow-md md:shadow-none rounded-r-md md:rounded-none'
)}
initial={{ width: 0 }}
animate={{ width: open ? '16rem' : 0 }}
transition={{
type: open ? 'spring' : 'tween',
stiffness: 150,
damping: open ? 15 : 10
}}
style={{ overflow: 'hidden' }}
>
<motion.div className="w-64 flex flex-col items-stretch h-full transition-transform duration-300 ease-in-out z-30 relative float-right">
<div className="flex justify-center items-center my-2 gap-2">
<Image radius="none" height={40} src={logo} className="mb-2" />
<div
className={clsx(
'flex items-center font-bold',
'!text-2xl shiny-text'
)}
>
NapCat
</div>
</div>
<div className="overflow-y-auto flex flex-col flex-1 px-4">
<Menus items={items} />
<div className="mt-auto mb-10 md:mb-0">
<Button
className="w-full"
color="primary"
radius="full"
variant="light"
onPress={toggleTheme}
startContent={
!isDark ? <MdLightMode size={16} /> : <MdDarkMode size={16} />
}
>
</Button>
<Button
className="w-full mb-2"
color="primary"
radius="full"
variant="light"
onPress={onRevokeAuth}
startContent={<IoMdLogOut size={16} />}
>
退
</Button>
</div>
</div>
</motion.div>
</motion.div>
)
}
export default SideBar

View File

@@ -1,158 +0,0 @@
import { Button } from '@heroui/button'
import { Image } from '@heroui/image'
import { useLocalStorage } from '@uidotdev/usehooks'
import clsx from 'clsx'
import React from 'react'
import { matchPath, useLocation, useNavigate } from 'react-router-dom'
import key from '@/const/key'
import type { MenuItem } from '@/config/site'
const renderItems = (items: MenuItem[], children = false) => {
return items?.map((item) => {
const navigate = useNavigate()
const locate = useLocation()
const [open, setOpen] = React.useState(!!item.autoOpen)
const canOpen = React.useMemo(
() => item.items && item.items.length > 0,
[item.items]
)
const [b64img] = useLocalStorage(key.backgroundImage, '')
const [customIcons] = useLocalStorage<Record<string, string>>(
key.customIcons,
{}
)
const isActive = React.useMemo(() => {
if (item.href) {
return !!matchPath(item.href, locate.pathname)
}
return false
}, [item.href, locate.pathname])
const goTo = (href: string) => {
navigate(href)
}
React.useEffect(() => {
if (item.items) {
const shouldOpen = item.items.some(
(item) => item?.href && !!matchPath(item.href, locate.pathname)
)
if (shouldOpen) setOpen(true)
}
}, [item.items, locate.pathname])
const panelRef = React.useRef<HTMLDivElement>(null)
return (
<div key={item.href + item.label}>
<Button
className={clsx(
'flex items-center w-full text-left justify-start dark:text-white',
// children && 'rounded-l-lg',
isActive && 'bg-opacity-60',
b64img && 'backdrop-blur-md text-white'
)}
color="primary"
endContent={
canOpen ? (
<div
className={clsx(
'ml-auto relative w-3 h-3 transition-transform',
open && 'transform rotate-180',
isActive
? 'text-primary-500'
: 'text-primary-200 dark:text-white',
'before:rounded-full',
'before:content-[""]',
'before:block',
'before:absolute',
'before:w-3',
'before:h-[4.5px]',
'before:bg-current',
'before:top-1/2',
'before:-left-[3px]',
'before:transform',
'before:-translate-y-1/2',
'before:rotate-45',
'after:rounded-full',
'after:content-[""]',
'after:block',
'after:absolute',
'after:w-3',
'after:h-[4.5px]',
'after:bg-current',
'after:top-1/2',
'after:left-[3px]',
'after:transform',
'after:-translate-y-1/2',
'after:-rotate-45'
)}
/>
) : (
<div
className={clsx(
'w-3 h-1.5 rounded-full ml-auto shadow-lg',
isActive
? 'bg-primary-500 animate-spinner-ease-spin'
: 'bg-primary-200 dark:bg-white'
)}
/>
)
}
radius="full"
startContent={
customIcons[item.label] ? (
<Image
radius="none"
src={customIcons[item.label]}
alt={item.label}
className="w-5 h-5"
/>
) : (
item.icon
)
}
variant={isActive ? (children ? 'solid' : 'shadow') : 'light'}
onPress={() => {
if (item.href) {
if (!isActive) {
goTo(item.href)
}
} else if (canOpen) {
setOpen(!open)
}
}}
>
{item.label}
</Button>
<div
ref={panelRef}
className="ml-4 overflow-hidden transition-all duration-300"
style={{
height: open ? panelRef.current?.scrollHeight : 0
}}
>
{item.items && renderItems(item.items, true)}
</div>
</div>
)
})
}
interface MenusProps {
items: MenuItem[]
}
const Menus: React.FC<MenusProps> = (props) => {
const { items } = props
return (
<div className="flex flex-col justify-content-center flex-1 gap-2">
{renderItems(items)}
</div>
)
}
export default Menus

View File

@@ -1,48 +0,0 @@
import { Switch } from '@heroui/switch'
import clsx from 'clsx'
import React, { forwardRef } from 'react'
export interface SwitchCardProps {
label?: string
description?: string
value?: boolean
onValueChange?: (value: boolean) => void
name?: string
onBlur?: React.FocusEventHandler
disabled?: boolean
onChange?: React.ChangeEventHandler<HTMLInputElement>
}
const SwitchCard = forwardRef<HTMLInputElement, SwitchCardProps>(
(props, ref) => {
const { label, description, value, onValueChange, disabled } = props
const selectString = value ? 'true' : 'false'
return (
<Switch
classNames={{
base: clsx(
'inline-flex flex-row-reverse w-full max-w-md bg-content1 hover:bg-content2 items-center',
'justify-between cursor-pointer rounded-lg gap-2 p-3 border-2 border-transparent',
'data-[selected=true]:border-primary bg-opacity-50 backdrop-blur-sm'
)
}}
{...props}
ref={ref}
isDisabled={disabled}
isSelected={value}
value={selectString}
onValueChange={onValueChange}
>
<div className="flex flex-col gap-1">
<p className="text-medium">{label}</p>
<p className="text-tiny text-default-400">{description}</p>
</div>
</Switch>
)
}
)
SwitchCard.displayName = 'SwitchCard'
export default SwitchCard

View File

@@ -1,274 +0,0 @@
import { Button } from '@heroui/button'
import { Card, CardBody, CardHeader } from '@heroui/card'
import { Chip } from '@heroui/chip'
import { Spinner } from '@heroui/spinner'
import { Tooltip } from '@heroui/tooltip'
import { useRequest } from 'ahooks'
import { useEffect } from 'react'
import { BsStars } from 'react-icons/bs'
import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6'
import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io'
import { RiMacFill } from 'react-icons/ri'
import useDialog from '@/hooks/use-dialog'
import { request } from '@/utils/request'
import { compareVersion } from '@/utils/version'
import WebUIManager from '@/controllers/webui_manager'
import { GithubRelease } from '@/types/github'
import TailwindMarkdown from './tailwind_markdown'
export interface SystemInfoItemProps {
title: string
icon?: React.ReactNode
value?: React.ReactNode
endContent?: React.ReactNode
}
const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
title,
value = '--',
icon,
endContent
}) => {
return (
<div className="flex text-sm gap-1 p-2 items-center shadow-sm shadow-primary-100 dark:shadow-primary-100 rounded text-primary-400">
{icon}
<div className="w-24">{title}</div>
<div className="text-primary-200">{value}</div>
<div className="ml-auto">{endContent}</div>
</div>
)
}
export interface NewVersionTipProps {
currentVersion?: string
}
const NewVersionTip = (props: NewVersionTipProps) => {
const { currentVersion } = props
const dialog = useDialog()
const { data: releaseData, error } = useRequest(() =>
request.get<GithubRelease[]>(
'https://api.github.com/repos/NapNeko/NapCatQQ/releases'
)
)
if (error) {
return (
<Tooltip content="检查新版本失败">
<Button
isIconOnly
radius="full"
color="primary"
variant="shadow"
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
onPress={() => {
dialog.alert({
title: '检查新版本失败',
content: error.message
})
}}
>
<FaInfo />
</Button>
</Tooltip>
)
}
const latestVersion = releaseData?.data?.[0]?.tag_name
if (!latestVersion || !currentVersion) {
return null
}
if (compareVersion(latestVersion, currentVersion) <= 0) {
return null
}
const middleVersions: GithubRelease[] = []
for (let i = 0; i < releaseData.data.length; i++) {
const versionInfo = releaseData.data[i]
if (compareVersion(versionInfo.tag_name, currentVersion) > 0) {
middleVersions.push(versionInfo)
} else {
break
}
}
const AISummaryComponent = () => {
const {
data: aiSummaryData,
loading: aiSummaryLoading,
error: aiSummaryError,
run: runAiSummary
} = useRequest(
(version) =>
request.get<ServerResponse<string | null>>(
`https://release.nc.152710.xyz/?version=${version}`,
{
timeout: 30000
}
),
{
manual: true
}
)
useEffect(() => {
runAiSummary(currentVersion)
}, [currentVersion, runAiSummary])
if (aiSummaryLoading) {
return (
<div className="flex justify-center py-1">
<Spinner size="sm" />
</div>
)
}
if (aiSummaryError) {
return <div className="text-center text-primary-500">AI </div>
}
return <span className="text-default-700">{aiSummaryData?.data.data}</span>
}
return (
<Tooltip content="有新版本可用">
<Button
isIconOnly
radius="full"
color="primary"
variant="shadow"
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
onPress={() => {
dialog.confirm({
title: '有新版本可用',
content: (
<div className="space-y-2">
<div className="text-sm space-x-2">
<span></span>
<Chip color="primary" variant="flat">
v{currentVersion}
</Chip>
</div>
<div className="text-sm space-x-2">
<span></span>
<Chip color="primary">{latestVersion}</Chip>
</div>
<div className="p-2 rounded-md bg-content2 text-sm">
<div className="text-primary-400 font-bold flex items-center gap-1 mb-1">
<BsStars />
<span>AI总结</span>
</div>
{<AISummaryComponent />}
</div>
<div className="text-sm space-y-2 !mt-4">
{middleVersions.map((versionInfo) => (
<div
key={versionInfo.tag_name}
className="p-4 bg-content1 rounded-md shadow-small"
>
<TailwindMarkdown content={versionInfo.body} />
</div>
))}
</div>
</div>
),
scrollBehavior: 'inside',
size: '3xl',
confirmText: '前往下载',
onConfirm() {
window.open(
'https://github.com/NapNeko/NapCatQQ/releases',
'_blank',
'noopener'
)
}
})
}}
>
<FaInfo />
</Button>
</Tooltip>
)
}
const NapCatVersion = () => {
const {
data: packageData,
loading: packageLoading,
error: packageError
} = useRequest(WebUIManager.getPackageInfo)
const currentVersion = packageData?.version
return (
<SystemInfoItem
title="NapCat 版本"
icon={<IoLogoOctocat className="text-xl" />}
value={
packageError ? (
`错误:${packageError.message}`
) : packageLoading ? (
<Spinner size="sm" />
) : (
currentVersion
)
}
endContent={<NewVersionTip currentVersion={currentVersion} />}
/>
)
}
export interface SystemInfoProps {
archInfo?: string
}
const SystemInfo: React.FC<SystemInfoProps> = (props) => {
const { archInfo } = props
const {
data: qqVersionData,
loading: qqVersionLoading,
error: qqVersionError
} = useRequest(WebUIManager.getQQVersion)
return (
<Card className="bg-opacity-60 shadow-sm shadow-primary-100 dark:shadow-primary-100 overflow-visible flex-1">
<CardHeader className="pb-0 items-center gap-1 text-primary-500 font-extrabold">
<FaCircleInfo className="text-lg" />
<span></span>
</CardHeader>
<CardBody className="flex-1">
<div className="flex flex-col justify-between h-full">
<NapCatVersion />
<SystemInfoItem
title="QQ 版本"
icon={<FaQq className="text-lg" />}
value={
qqVersionError ? (
`错误:${qqVersionError.message}`
) : qqVersionLoading ? (
<Spinner size="sm" />
) : (
qqVersionData
)
}
/>
<SystemInfoItem
title="WebUI 版本"
icon={<IoLogoChrome className="text-xl" />}
value="Next"
/>
<SystemInfoItem
title="系统版本"
icon={<RiMacFill className="text-xl" />}
value={archInfo}
/>
</div>
</CardBody>
</Card>
)
}
export default SystemInfo

View File

@@ -1,130 +0,0 @@
import { Card, CardBody } from '@heroui/card'
import { Image } from '@heroui/image'
import clsx from 'clsx'
import { BiSolidMemoryCard } from 'react-icons/bi'
import { GiCpu } from 'react-icons/gi'
import bkg from '@/assets/images/bg/1AD934174C0107F14BAD8776D29C5F90.png'
import UsagePie from './usage_pie'
export interface SystemStatusItemProps {
title: string
value?: string | number
size?: 'md' | 'lg'
unit?: string
}
const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
title,
value = '-',
size = 'md',
unit
}) => {
return (
<div
className={clsx(
'shadow-sm shadow-primary-100 p-2 rounded-md text-sm bg-content1 bg-opacity-30',
size === 'lg' ? 'col-span-2' : 'col-span-1 flex justify-between'
)}
>
<div className="w-24">{title}</div>
<div className="text-default-400">
{value}
{unit}
</div>
</div>
)
}
export interface SystemStatusDisplayProps {
data?: SystemStatus
}
const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
const memoryUsage = {
system: 0,
qq: 0
}
if (data) {
const system = Number(data.memory.total) || 1
const systemUsage = Number(data.memory.usage.system)
const qqUsage = Number(data.memory.usage.qq)
memoryUsage.system = (systemUsage / system) * 100
memoryUsage.qq = (qqUsage / system) * 100
}
return (
<Card className="bg-opacity-60 shadow-sm shadow-primary-100 col-span-1 lg:col-span-2 relative overflow-hidden">
<div className="absolute h-full right-0 top-0">
<Image
src={bkg}
alt="background"
className="select-none pointer-events-none !opacity-30 w-full h-full"
classNames={{
wrapper: 'w-full h-full',
img: 'object-contain w-full h-full'
}}
/>
</div>
<CardBody className="overflow-visible md:flex-row gap-4 items-center justify-stretch z-10">
<div className="flex-1 w-full md:max-w-96">
<h2 className="text-lg font-semibold flex items-center gap-1 text-primary-400">
<GiCpu className="text-xl" />
<span>CPU</span>
</h2>
<div className="grid grid-cols-2 gap-2">
<SystemStatusItem title="型号" value={data?.cpu.model} size="lg" />
<SystemStatusItem title="内核数" value={data?.cpu.core} />
<SystemStatusItem title="主频" value={data?.cpu.speed} unit="GHz" />
<SystemStatusItem
title="使用率"
value={data?.cpu.usage.system}
unit="%"
/>
<SystemStatusItem
title="QQ主线程"
value={data?.cpu.usage.qq}
unit="%"
/>
</div>
<h2 className="text-lg font-semibold flex items-center gap-1 text-primary-400 mt-2">
<BiSolidMemoryCard className="text-xl" />
<span></span>
</h2>
<div className="grid grid-cols-2 gap-2">
<SystemStatusItem
title="总量"
value={data?.memory.total}
size="lg"
unit="MB"
/>
<SystemStatusItem
title="使用量"
value={data?.memory.usage.system}
unit="MB"
/>
<SystemStatusItem
title="QQ主线程"
value={data?.memory.usage.qq}
unit="MB"
/>
</div>
</div>
<div className="flex flex-row md:flex-col gap-2 flex-shrink-0 w-full justify-center md:w-40 min-h-40 mt-4 md:mt-0 md:mx-auto">
<UsagePie
systemUsage={Number(data?.cpu.usage.system) || 0}
processUsage={Number(data?.cpu.usage.qq) || 0}
title="CPU占用"
/>
<UsagePie
systemUsage={memoryUsage.system}
processUsage={memoryUsage.qq}
title="内存占用"
/>
</div>
</CardBody>
</Card>
)
}
export default SystemStatusDisplay

View File

@@ -1,49 +0,0 @@
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
const TailwindMarkdown: React.FC<{ content: string }> = ({ content }) => {
return (
<Markdown
className="prose prose-sm sm:prose lg:prose-lg xl:prose-xl"
remarkPlugins={[remarkGfm]}
components={{
h1: ({ node, ...props }) => (
<h1 className="text-2xl font-bold" {...props} />
),
h2: ({ node, ...props }) => (
<h2 className="text-xl font-bold" {...props} />
),
h3: ({ node, ...props }) => (
<h3 className="text-lg font-bold" {...props} />
),
p: ({ node, ...props }) => <p className="m-0" {...props} />,
a: ({ node, ...props }) => (
<a
className="text-primary-500 inline-block hover:underline"
target="_blank"
{...props}
/>
),
ul: ({ node, ...props }) => (
<ul className="list-disc list-inside" {...props} />
),
ol: ({ node, ...props }) => (
<ol className="list-decimal list-inside" {...props} />
),
blockquote: ({ node, ...props }) => (
<blockquote
className="border-l-4 border-default-300 pl-4 italic"
{...props}
/>
),
code: ({ node, ...props }) => (
<code className="bg-default-100 p-1 rounded text-xs" {...props} />
)
}}
>
{content}
</Markdown>
)
}
export default TailwindMarkdown

View File

@@ -1,56 +0,0 @@
import { useEffect, useRef } from 'react'
import TerminalManager from '@/controllers/terminal_manager'
import XTerm, { XTermRef } from '../xterm'
interface TerminalInstanceProps {
id: string
}
export function TerminalInstance({ id }: TerminalInstanceProps) {
const termRef = useRef<XTermRef>(null)
const connected = useRef(false)
const handleData = (data: string) => {
try {
const parsed = JSON.parse(data)
if (parsed.data) {
termRef.current?.write(parsed.data)
}
} catch (e) {
termRef.current?.write(data)
}
}
useEffect(() => {
return () => {
if (connected.current) {
TerminalManager.disconnectTerminal(id, handleData)
}
}
}, [id])
const handleInput = (data: string) => {
TerminalManager.sendInput(id, data)
}
const handleResize = (cols: number, rows: number) => {
if (!connected.current) {
connected.current = true
console.log('instance', rows, cols)
TerminalManager.connectTerminal(id, handleData, { rows, cols })
} else {
TerminalManager.sendResize(id, cols, rows)
}
}
return (
<XTerm
ref={termRef}
onInput={handleInput}
onResize={handleResize} // 使用 fitAddon 改变后触发的 resize 回调
className="w-full h-full"
/>
)
}

View File

@@ -1,21 +0,0 @@
import { Toaster as HotToaster } from 'react-hot-toast'
import { useTheme } from '@/hooks/use-theme'
export const Toaster = () => {
const { isDark } = useTheme()
return (
<HotToaster
toastOptions={{
style: {
borderRadius: '20px',
background: isDark ? '#333' : '#fff',
color: isDark ? '#fff' : '#333'
}
}}
/>
)
}
export default Toaster

View File

@@ -1,12 +0,0 @@
export default function UnderConstruction() {
return (
<div className="flex flex-col items-center justify-center h-full pt-4">
<div className="flex flex-col items-center justify-center space-y-4">
<div className="text-6xl font-bold text-gray-500">🚧</div>
<div className="text-2xl font-bold text-gray-500">
Under Construction
</div>
</div>
</div>
)
}

View File

@@ -1,143 +0,0 @@
import * as echarts from 'echarts'
import React, { useEffect, useRef } from 'react'
import { useTheme } from '@/hooks/use-theme'
interface UsagePieProps {
systemUsage: number
processUsage: number
title?: string
}
const defaultOption: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '<center>{b}<br/><b>{d}%</b></center>',
borderRadius: 10,
extraCssText: 'backdrop-filter: blur(10px);'
},
series: [
{
name: '系统占用',
type: 'pie',
radius: ['70%', '90%'],
avoidLabelOverlap: false,
label: {
show: true,
position: 'center',
formatter: '系统占用',
fontSize: 14
},
itemStyle: {
borderWidth: 1,
borderRadius: 10
},
labelLine: {
show: false
},
data: [
{
value: 100,
name: '系统总量'
}
]
}
]
}
const UsagePie: React.FC<UsagePieProps> = ({
systemUsage,
processUsage,
title
}) => {
const chartRef = useRef<HTMLDivElement>(null)
const chartInstance = useRef<echarts.ECharts | null>(null)
const { theme } = useTheme()
useEffect(() => {
if (chartRef.current) {
chartInstance.current = echarts.init(chartRef.current)
const option = defaultOption
chartInstance.current.setOption(option)
const observer = new ResizeObserver(() => {
chartInstance.current?.resize()
})
observer.observe(chartRef.current)
return () => {
chartInstance.current?.dispose()
observer.disconnect()
}
}
}, [])
useEffect(() => {
if (chartInstance.current) {
chartInstance.current.setOption({
series: [
{
label: {
formatter: title
}
}
]
})
}
}, [title])
useEffect(() => {
if (chartInstance.current) {
chartInstance.current.setOption({
darkMode: theme === 'dark',
tooltip: {
backgroundColor:
theme === 'dark'
? 'rgba(0, 0, 0, 0.8)'
: 'rgba(255, 255, 255, 0.8)',
textStyle: {
color: theme === 'dark' ? '#fff' : '#333'
}
},
color:
theme === 'dark'
? ['#D33FF0', '#EF8664', '#E25180']
: ['#D33FF0', '#EA7D9B', '#FFC107'],
series: [
{
itemStyle: {
borderColor: theme === 'dark' ? '#333' : '#F0A9A7'
}
}
]
})
}
}, [theme])
useEffect(() => {
if (chartInstance.current) {
chartInstance.current.setOption({
series: [
{
data: [
{
value: processUsage,
name: 'QQ占用'
},
{
value: systemUsage - processUsage,
name: '其他进程占用'
},
{
value: 100 - systemUsage,
name: '剩余系统总量'
}
]
}
]
})
}
}, [systemUsage, processUsage])
return <div ref={chartRef} className="w-36 h-36 flex-shrink-0" />
}
export default UsagePie

View File

@@ -1,112 +0,0 @@
import {
BugIcon2,
FileIcon,
InfoIcon,
LogIcon,
RouteIcon,
SettingsIcon,
SignalTowerIcon,
TerminalIcon
} from '@/components/icons'
export type SiteConfig = typeof siteConfig
export interface MenuItem {
label: string
icon?: React.ReactNode
autoOpen?: boolean
href?: string
items?: MenuItem[]
customIcon?: string
}
export const siteConfig = {
name: 'NapCat WebUI',
description: 'NapCat WebUI.',
navItems: [
{
label: '基础信息',
icon: (
<div className="w-5 h-5">
<RouteIcon />
</div>
),
href: '/'
},
{
label: '网络配置',
icon: (
<div className="w-5 h-5">
<SignalTowerIcon />
</div>
),
href: '/network'
},
{
label: '其他配置',
icon: (
<div className="w-5 h-5">
<SettingsIcon />
</div>
),
href: '/config'
},
{
label: '猫猫日志',
icon: (
<div className="w-5 h-5">
<LogIcon />
</div>
),
href: '/logs'
},
{
label: '接口调试',
icon: (
<div className="w-5 h-5">
<BugIcon2 />
</div>
),
items: [
{
label: 'HTTP',
href: '/debug/http'
},
{
label: 'Websocket',
href: '/debug/ws'
}
]
},
{
label: '文件管理',
icon: (
<div className="w-5 h-5">
<FileIcon />
</div>
),
href: '/file_manager'
},
{
label: '系统终端',
icon: (
<div className="w-5 h-5">
<TerminalIcon />
</div>
),
href: '/terminal'
},
{
label: '关于我们',
icon: (
<div className="w-5 h-5">
<InfoIcon />
</div>
),
href: '/about'
}
] as MenuItem[],
links: {
github: 'https://github.com/NapNeko/NapCatQQ',
docs: 'https://napcat.napneko.icu/'
}
}

View File

@@ -1,744 +0,0 @@
import { z } from 'zod'
import messageNodeSchema from './message/node'
import { baseResponseSchema, commonResponseDataSchema } from './response'
const oneBotHttpApiGroup = {
'/set_group_kick': {
description: '群踢人',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
reject_add_request: z.boolean().describe('拒绝此人的加群请求')
}),
response: baseResponseSchema
},
'/set_group_ban': {
description: '群禁言',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
duration: z.number()
}),
response: baseResponseSchema
},
'/get_group_system_msg': {
description: '获取群系统消息',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
InvitedRequest: z
.array(
z
.object({
request_id: z.string().describe('请求 ID'),
invitor_uin: z.string().describe('邀请人 QQ 号'),
invitor_nick: z.string().describe('邀请人昵称'),
group_id: z.string().describe('群号'),
message: z.string().describe('入群回答'),
group_name: z.string().describe('群名称'),
checked: z.boolean().describe('是否已处理'),
actor: z.string().describe('处理人 QQ 号')
})
.describe('邀请入群请求')
)
.describe('邀请入群请求列表'),
join_requests: z.array(
z.object({
request_id: z.string().describe('请求 ID'),
requester_uin: z.string().describe('请求人 QQ 号'),
requester_nick: z.string().describe('请求人昵称'),
group_id: z.string().describe('群号'),
message: z.string().describe('入群回答'),
group_name: z.string().describe('群名称'),
checked: z.boolean().describe('是否已处理'),
actor: z.string().describe('处理人 QQ 号')
})
)
})
})
},
'/get_essence_msg_list': {
description: '获取精华消息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema.extend({
data: z
.array(
z
.object({
msg_seq: z.number().describe('消息序号'),
msg_random: z.number().describe('消息随机数'),
sender_id: z.number().describe('发送人 QQ 号'),
sender_nick: z.string().describe('发送人昵称'),
operator_id: z.number().describe('操作人 QQ 号'),
operator_nick: z.string().describe('操作人昵称'),
message_id: z.string().describe('消息 ID'),
operator_time: z.string().describe('操作时间'),
content: z.array(messageNodeSchema)
})
.describe('精华消息')
)
.describe('精华消息列表')
})
},
'/set_group_whole_ban': {
description: '全员禁言',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
enable: z.boolean().describe('是否开启')
}),
response: baseResponseSchema
},
'/set_group_portrait': {
description: '设置群头像',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
file: z.string().describe('图片文件路径,服务器本地路径或远程 URL')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/set_group_admin': {
description: '设置群管理',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
enable: z.boolean().describe('是否设置为管理员')
}),
response: baseResponseSchema
},
'/set_essence_msg': {
description: '设置群精华消息',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息 ID')
}),
response: baseResponseSchema.extend({
data: z.object({
errCode: z.number().describe('错误码'),
errMsg: z.string().describe('错误信息'),
result: z
.object({
wording: z.string().describe('?'),
digestUin: z.string().describe('?QQ号'),
digestTime: z.number().describe('设置时间?'),
msg: z
.object({
groupCode: z.string().describe('群号'),
msgSeq: z.number().describe('消息序号'),
msgRandom: z.number().describe('消息随机数'),
msgContent: z.array(messageNodeSchema).describe('消息内容'),
textSize: z.string().describe('文本大小'),
picSize: z.string().describe('图片大小'),
videoSize: z.string().describe('视频大小'),
senderUin: z.string().describe('发送人 QQ 号'),
senderTime: z.number().describe('发送时间'),
addDigestUin: z.string().describe('添加精华消息人 QQ 号'),
addDigestTime: z.number().describe('添加精华消息时间'),
startTime: z.number().describe('开始时间'),
latestMsgSeq: z.number().describe('最新消息序号'),
opType: z.number().describe('操作类型')
})
.describe('消息内容'),
errorCode: z.number().describe('错误码')
})
.describe('结果')
})
})
},
'/set_group_card': {
description: '设置群成员名片',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
card: z.string().describe('名片')
}),
response: baseResponseSchema
},
'/delete_essence_msg': {
description: '删除群精华消息',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息 ID')
}),
response: baseResponseSchema.extend({
data: z.object({
errCode: z.number().describe('错误码'),
errMsg: z.string().describe('错误信息'),
result: z
.object({
wording: z.string().describe('?'),
digestUin: z.string().describe('?QQ号'),
digestTime: z.number().describe('设置时间?'),
msg: z.object({
groupCode: z.string().describe('群号'),
msgSeq: z.number().describe('消息序号'),
msgRandom: z.number().describe('消息随机数'),
msgContent: z.array(messageNodeSchema).describe('消息内容'),
textSize: z.string().describe('文本大小'),
picSize: z.string().describe('图片大小'),
videoSize: z.string().describe('视频大小'),
senderUin: z.string().describe('发送人 QQ 号'),
senderTime: z.number().describe('发送时间'),
addDigestUin: z.string().describe('添加精华消息人 QQ 号'),
addDigestTime: z.number().describe('添加精华消息时间'),
startTime: z.number().describe('开始时间'),
latestMsgSeq: z.number().describe('最新消息序号'),
opType: z.number().describe('操作类型')
}),
errorCode: z.number().describe('错误码')
})
.describe('结果')
})
})
},
'/set_group_name': {
description: '设置群名称',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
group_name: z.string().describe('群名称')
}),
response: baseResponseSchema
},
'/set_group_leave': {
description: '退出群聊',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema
},
'/_send_group_notice': {
description: '发送群公告',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
content: z.string().describe('公告内容'),
image: z.string().optional().describe('图片地址')
}),
response: baseResponseSchema
},
'/_get_group_notice': {
description: '获取群公告',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
notice_id: z.string().describe('公告 ID'),
sender_id: z.number().describe('发送人 QQ 号'),
publish_time: z.number().describe('发布时间'),
message: z.object({
text: z.string().describe('文本内容'),
image: z
.array(
z
.object({
id: z.string().describe('图片 ID'),
height: z.string().describe('高度'),
width: z.string().describe('宽度')
})
.describe('图片信息')
)
.describe('图片内容列表')
})
})
)
})
},
'/set_group_special_title': {
description: '设置群成员专属头衔',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
special_title: z.string().describe('专属头衔内容')
}),
response: baseResponseSchema
},
'/upload_group_file': {
description: '上传群文件',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
file: z.string().describe('文件路径'),
name: z.string().describe('文件名'),
folder_id: z.string().describe('文件夹 ID')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/set_group_add_request': {
description: '处理加群请求',
request: z.object({
flag: z.string().describe('请求ID'),
approve: z.boolean().describe('是否同意'),
reason: z.string().optional().describe('拒绝理由')
}),
response: baseResponseSchema
},
'/get_group_info': {
description: '获取群信息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema.extend({
data: z.object({})
})
},
'/get_group_info_ex': {
description: '获取群信息扩展',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema.extend({
data: z
.object({
groupCode: z.string().describe('群号'),
resultCode: z.number().describe('结果码'),
extInfo: z
.object({
groupInfoExtSeq: z.number().describe('群信息序列号'),
reserve: z.number().describe('?'),
luckyWordId: z.string().describe('幸运字符ID'),
lightCharNum: z.number().describe('?'),
luckyWord: z.string().describe('幸运字符'),
starId: z.number().describe('?'),
essentialMsgSwitch: z.number().describe('精华消息开关'),
todoSeq: z.number().describe('?'),
blacklistExpireTime: z.number().describe('黑名单过期时间'),
isLimitGroupRtc: z.number().describe('是否限制群视频通话'),
companyId: z.number().describe('公司ID'),
hasGroupCustomPortrait: z.number().describe('是否有群自定义头像'),
bindGuildId: z.string().describe('绑定频道ID'),
groupOwnerId: z
.object({
memberUin: z.string().describe('群主QQ号'),
memberUid: z.string().describe('群主ID'),
memberQid: z.string().describe('群主QID')
})
.describe('群主信息'),
essentialMsgPrivilege: z.number().describe('精华消息权限'),
msgEventSeq: z.string().describe('消息事件序列号'),
inviteRobotSwitch: z.number().describe('邀请机器人开关'),
gangUpId: z.string().describe('?'),
qqMusicMedalSwitch: z.number().describe('QQ音乐勋章开关'),
showPlayTogetherSwitch: z.number().describe('显示一起玩开关'),
groupFlagPro1: z.string()?.describe('群标识1'),
groupBindGuildIds: z
.object({
guildIds: z.array(z.string())
})
.describe('绑定频道ID列表?'),
viewedMsgDisappearTime: z.string().describe('消息消失时间'),
groupExtFlameData: z.object({
switchState: z.number().describe('开关状态'),
state: z.number().describe('状态'),
dayNums: z.array(z.number()).describe('天数列表'),
version: z.number().describe('版本号'),
updateTime: z.string().describe('更新时间'),
isDisplayDayNum: z.boolean().describe('是否显示天数')
}),
groupBindGuildSwitch: z.number().describe('绑定频道开关'),
groupAioBindGuildId: z.string().describe('AIO绑定频道ID'),
groupExcludeGuildIds: z
.object({
guildIds: z.array(z.string()).describe('排除频道ID')
})
.describe('排除频道ID列表?'),
fullGroupExpansionSwitch: z.number().describe('全员群扩容开关'),
fullGroupExpansionSeq: z.string().describe('全员群扩容序列号'),
inviteRobotMemberSwitch: z
.number()
.describe('邀请机器人成员开关'),
inviteRobotMemberExamine: z
.number()
.describe('邀请机器人成员审核'),
groupSquareSwitch: z.number().describe('群广场开关')
})
.describe('扩展信息')
})
.describe('结果')
})
},
'/create_group_file_folder': {
description: '创建群文件夹',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
folder_name: z.string().describe('文件夹名称')
}),
response: baseResponseSchema.extend({
data: z
.object({
result: z
.object({
retCode: z.number().describe('结果码'),
retMsg: z.string().describe('结果信息'),
clientWording: z.string().describe('客户端提示')
})
.describe('结果'),
groupItem: z
.object({
peerId: z.string().describe('?'),
type: z.string().describe('类型'),
folderInfo: z
.object({
folderId: z.string().describe('文件夹 ID'),
parentFolderId: z.string().describe('父文件夹 ID'),
folderName: z.string().describe('文件夹名称'),
createTime: z.number().describe('创建时间'),
modifyTime: z.number().describe('修改时间'),
createUin: z.string().describe('创建人 QQ 号'),
creatorName: z.string().describe('创建人昵称'),
totalFileCount: z.string().describe('文件总数'),
modifyUin: z.string().describe('修改人 QQ 号'),
modifyName: z.string().describe('修改人昵称'),
usedSpace: z.string().describe('已使用空间')
})
.describe('文件夹信息')
})
.describe('群文件夹信息')
})
.describe('数据')
})
},
'/delete_group_file': {
description: '删除群文件',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
file_id: z.string().describe('文件 ID')
}),
response: baseResponseSchema.extend({
data: z
.object({
result: z.number().describe('结果码'),
errMsg: z.string().describe('错误信息'),
transGroupFileResult: z
.object({
result: z
.object({
retCode: z.number().describe('结果码'),
retMsg: z.string().describe('结果信息'),
clientWording: z.string().describe('客户端提示')
})
.describe('结果'),
successFileIdList: z
.array(z.string())
.describe('成功文件 ID 列表'),
failFileIdList: z.array(z.string()).describe('失败文件 ID 列表')
})
.describe('删除群文件结果')
})
.describe('结果')
})
},
'/delete_group_folder': {
description: '删除群文件夹',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
folder_id: z.string().describe('文件夹 ID')
}),
response: baseResponseSchema.extend({
data: z.object({
retCode: z.number().describe('结果码'),
retMsg: z.string().describe('结果信息'),
clientWording: z.string().describe('客户端提示')
})
})
},
'/get_group_file_system_info': {
description: '获取群文件系统信息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema.extend({
data: z.object({
file_count: z.number().describe('文件总数'),
limit_count: z.number().describe('文件总数限制'),
used_space: z.number().describe('已使用空间'),
total_space: z.number().describe('总空间')
})
})
},
'/get_group_root_files': {
description: '获取群根目录文件列表',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
files: z
.array(
z
.object({
group_id: z.number().describe('群号'),
file_id: z.string().describe('文件 ID'),
file_name: z.string().describe('文件名'),
busid: z.number().describe('?'),
size: z.number().describe('文件大小'),
upload_time: z.number().describe('上传时间'),
dead_time: z.number().describe('过期时间'),
modify_time: z.number().describe('修改时间'),
download_times: z.number().describe('下载次数'),
uploader: z.number().describe('上传人 QQ 号'),
uploader_name: z.string().describe('上传人昵称')
})
.describe('文件信息')
)
.describe('文件列表'),
folders: z
.array(
z
.object({
group_id: z.number().describe('群号'),
folder_id: z.string().describe('文件夹 ID'),
folder: z.string().describe('文件夹?'),
folder_name: z.string().describe('文件夹名称'),
create_time: z.string().describe('创建时间'),
creator: z.string().describe('创建人 QQ 号'),
creator_name: z.string().describe('创建人昵称'),
total_file_count: z.string().describe('文件总数')
})
.describe('文件夹信息')
)
.describe('文件夹列表')
})
)
})
},
'/get_group_files_by_folder': {
description: '获取群子目录文件列表',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
folder_id: z.string().describe('文件夹 ID'),
file_count: z.number().describe('文件数量')
}),
response: baseResponseSchema.extend({
data: z.object({
files: z
.array(
z
.object({
group_id: z.number().describe('群号'),
file_id: z.string().describe('文件 ID'),
file_name: z.string().describe('文件名'),
busid: z.number().describe('?'),
size: z.number().describe('文件大小'),
upload_time: z.number().describe('上传时间'),
dead_time: z.number().describe('过期时间'),
modify_time: z.number().describe('修改时间'),
download_times: z.number().describe('下载次数'),
uploader: z.number().describe('上传人 QQ 号'),
uploader_name: z.string().describe('上传人昵称')
})
.describe('文件信息')
)
.describe('文件列表'),
folders: z
.array(
z
.object({
group_id: z.number().describe('群号'),
folder_id: z.string().describe('文件夹 ID'),
folder: z.string().describe('文件夹?'),
folder_name: z.string().describe('文件夹名称'),
create_time: z.string().describe('创建时间'),
creator: z.string().describe('创建人 QQ 号'),
creator_name: z.string().describe('创建人昵称'),
total_file_count: z.string().describe('文件总数')
})
.describe('文件夹信息')
)
.describe('文件夹列表')
})
})
},
'/get_group_file_url': {
description: '获取群文件下载链接',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
file_id: z.string().describe('文件 ID')
}),
response: baseResponseSchema.extend({
data: z.object({
url: z.string().describe('下载链接')
})
})
},
'/get_group_list': {
description: '获取群列表',
request: z.object({
next_token: z.string().optional().describe('下一页标识')
}),
response: baseResponseSchema.extend({
data: z.array(z.object({}))
})
},
'/get_group_member_info': {
description: '获取群成员信息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
no_cache: z.boolean().describe('是否不使用缓存')
}),
response: baseResponseSchema.extend({
data: z.object({})
})
},
'/get_group_member_list': {
description: '获取群成员列表',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
no_cache: z.boolean().describe('是否不使用缓存')
}),
response: baseResponseSchema.extend({
data: z.array(z.object({}))
})
},
'/get_group_honor_info': {
description: '获取群荣誉',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema.extend({
data: z
.object({
group_id: z.number().describe('群号'),
current_talkative: z
.object({
user_id: z.number().describe('QQ 号'),
avatar: z.string().describe('头像 URL'),
nickname: z.string().describe('昵称'),
day_count: z.number().describe('天数'),
description: z.string().describe('描述')
})
.describe('当前龙王'),
talkative_list: z
.array(
z.object({
user_id: z.number().describe('QQ 号'),
avatar: z.string().describe('头像 URL'),
nickname: z.string().describe('昵称'),
day_count: z.number().describe('天数'),
description: z.string().describe('描述')
})
)
.describe('龙王榜'),
performer_list: z
.array(
z.object({
user_id: z.number().describe('QQ 号'),
avatar: z.string().describe('头像 URL'),
nickname: z.string().describe('昵称'),
description: z.string().describe('描述')
})
)
.describe('?'),
legend_list: z.array(z.string()).describe('?'),
emotion_list: z.array(z.string()).describe('?'),
strong_newbie_list: z.array(z.string()).describe('?')
})
.describe('群荣誉信息')
})
},
'/get_group_at_all_remain': {
description: '获取群 @全体成员 剩余次数',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema.extend({
data: z.object({
can_at_all: z.boolean().describe('是否可以 @全体成员'),
remain_at_all_count_for_group: z.number().describe('剩余次数(group?)'),
remain_at_all_count_for_uin: z.number().describe('剩余次数(qq?)')
})
})
},
'/get_group_ignored_notifies': {
description: '获取群过滤系统消息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema.extend({
data: z.object({
join_requests: z
.array(
z.object({
request_id: z.string().describe('请求 ID'),
requester_uin: z.string().describe('请求人 QQ 号'),
requester_nick: z.string().describe('请求人昵称'),
group_id: z.string().describe('群号'),
group_name: z.string().describe('群名称'),
checked: z.boolean().describe('是否已处理'),
actor: z.string().describe('处理人 QQ 号')
})
)
.describe('入群请求列表')
})
})
},
'/set_group_sign': {
description: '设置群打卡',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema
},
'/send_group_sign': {
description: '发送群打卡',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema
},
'/get_ai_characters': {
description: '获取AI语音人物',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
chat_type: z.union([z.string(), z.number()]).describe('聊天类型')
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
type: z.string().describe('类型'),
characters: z.array(
z
.object({
character_id: z.string().describe('人物 ID'),
character_name: z.string().describe('人物名称'),
preview_url: z.string().describe('预览音频地址')
})
.describe('人物信息')
)
})
)
})
},
'/send_group_ai_record': {
description: '发送群AI语音',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
character: z.string().describe('人物ID'),
text: z.string().describe('文本内容')
}),
response: baseResponseSchema.extend({
data: z.object({
message_id: z.string().describe('消息 ID')
})
})
},
'/get_ai_record': {
description: '获取AI语音',
request: z.object({
group_id: z.string().describe('群号'),
character: z.string().describe('人物ID'),
text: z.string().describe('文本内容')
}),
response: baseResponseSchema.extend({
data: z.string()
})
}
} as const
export default oneBotHttpApiGroup

View File

@@ -1,34 +0,0 @@
import { ZodSchema } from 'zod'
import oneBotHttpApiGroup from './group'
import oneBotHttpApiMessage from './message'
import oneBotHttpApiSystem from './system'
import oneBotHttpApiUser from './user'
type AllKey =
| keyof typeof oneBotHttpApiUser
| keyof typeof oneBotHttpApiMessage
| keyof typeof oneBotHttpApiGroup
| keyof typeof oneBotHttpApiSystem
export type OneBotHttpApi = Record<
AllKey,
{
description?: string
request: ZodSchema
response: ZodSchema
}
>
const oneBotHttpApi: OneBotHttpApi = {
...oneBotHttpApiUser,
...oneBotHttpApiMessage,
...oneBotHttpApiGroup,
...oneBotHttpApiSystem
} as const
export type OneBotHttpApiPath = keyof OneBotHttpApi
export type OneBotHttpApiContent = OneBotHttpApi[OneBotHttpApiPath]
export default oneBotHttpApi

View File

@@ -1,84 +0,0 @@
import { z } from 'zod'
import type { ZodSchema } from 'zod'
import { baseResponseSchema, commonResponseDataSchema } from '../response'
import messageNodeSchema, { nodeMessage } from './node'
const oneBotHttpApiMessageGroup: Record<
string,
{
description?: string
request: ZodSchema
response: ZodSchema
}
> = {
'/send_group_msg': {
description: '发送群消息',
request: z
.object({
group_id: z
.union([z.string(), z.number()])
.describe('群号')
.describe('群号'),
message: z.array(messageNodeSchema).describe('消息内容')
})
.refine(
(data) => {
const hasReply = data.message.some((item) => item.type === 'reply')
if (hasReply) {
return data.message[0].type === 'reply'
}
return true
},
{
message:
'如果 message 包含 reply 类型的消息,那么只能包含一个,而且排在最前面'
}
),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/send_group_forward_msg': {
description: '发送群合并转发消息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
messages: z.array(nodeMessage).describe('消息内容'),
news: z
.array(
z.object({
text: z.string()
})
)
.describe('?'),
prompt: z.string().describe('外显'),
summary: z.string().describe('底下文本'),
source: z.string().describe('内容')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/forward_group_single_msg': {
description: '消息转发到群',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
message_id: z.union([z.string(), z.number()]).describe('消息 ID')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/group_poke': {
description: '发送戳一戳',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('对方QQ号')
}),
response: baseResponseSchema
}
}
export default oneBotHttpApiMessageGroup

View File

@@ -1,269 +0,0 @@
import { z } from 'zod'
import { baseResponseSchema, commonResponseDataSchema } from '../response'
import oneBotHttpApiMessageGroup from './group'
import messageNodeSchema from './node'
import oneBotHttpApiMessagePrivate from './private'
const fileSchema = z
.object({
file: z.string().describe('路径或链接'),
url: z.string().describe('路径或链接'),
file_size: z.string().describe('文件大小'),
file_name: z.string().describe('文件名'),
base64: z.string().describe('文件base64编码')
})
.describe('文件')
const messageSchema = z
.object({
self_id: z.number().describe('自己QQ号'),
user_id: z.number().describe('发送人QQ号'),
time: z.number().describe('发送时间'),
message_id: z.number().describe('消息ID'),
message_seq: z.number().describe('消息序号'),
real_id: z.number().describe('?ID'),
message_type: z.string().describe('消息类型'),
sender: z
.object({
user_id: z.number().describe('发送人QQ号'),
nickname: z.string().describe('昵称'),
sex: z.enum(['male', 'female', 'unknown']).describe('性别'),
age: z.number().describe('年龄'),
card: z.string().describe('名片'),
role: z.enum(['owner', 'admin', 'member']).describe('角色')
})
.describe('发送人信息'),
raw_message: z.string().describe('原始消息'),
font: z.number().describe('字体'),
sub_type: z.string().describe('子类型'),
message: z.array(messageNodeSchema).describe('消息内容'),
message_format: z.string().describe('消息格式'),
post_type: z.string().describe('?'),
message_sent_type: z.string().describe('消息发送类型'),
group_id: z.number().describe('群号')
})
.describe('消息')
const oneBotHttpApiMessage = {
...oneBotHttpApiMessagePrivate,
...oneBotHttpApiMessageGroup,
'/mark_msg_as_read': {
description: '标记消息已读',
request: z
.object({
group_id: z
.union([z.string(), z.number()])
.optional()
.describe('群号,与 user_id 二选一'),
user_id: z
.union([z.string(), z.number()])
.optional()
.describe('用户QQ号与 group_id 二选一')
})
.refine(
(data) =>
(data.group_id && !data.user_id) || (!data.group_id && data.user_id),
{
message: 'group_id 和 user_id 必须二选一,且不能同时存在或同时为空',
path: ['group_id', 'user_id']
}
),
response: baseResponseSchema
},
'/mark_group_msg_as_read': {
description: '标记群消息已读',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
response: baseResponseSchema
},
'/mark_private_msg_as_read': {
description: '标记私聊消息已读',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('用户QQ号')
}),
response: baseResponseSchema
},
'/_mark_all_as_read': {
description: '标记所有消息已读',
request: z.object({}),
response: baseResponseSchema
},
'/delete_msg': {
description: '撤回消息',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息ID')
}),
response: baseResponseSchema
},
'/get_msg': {
description: '获取消息',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息ID')
}),
response: baseResponseSchema.extend({
data: z.object({})
})
},
'/get_image': {
description: '获取图片',
request: z.object({
file_id: z.string().describe('文件ID')
}),
response: baseResponseSchema.extend({
data: fileSchema
})
},
'/get_record': {
description: '获取语音',
request: z.object({
file_id: z.string().describe('文件ID'),
out_format: z
.enum(['mp3', 'amr', 'wma', 'm4a', 'spx', 'ogg', 'wav', 'flac'])
.describe('输出格式')
}),
response: baseResponseSchema.extend({
data: fileSchema
})
},
'/get_file': {
description: '获取文件',
request: z.object({
file_id: z.string().describe('文件ID')
}),
response: baseResponseSchema.extend({
data: fileSchema
})
},
'/get_group_msg_history': {
description: '获取群消息历史',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
message_seq: z.union([z.string(), z.number()]).describe('消息序号'),
count: z.number().int().positive().describe('获取数量'),
reverseOrder: z.boolean().describe('是否倒序')
}),
response: baseResponseSchema.extend({
data: z.object({
messages: z.array(messageSchema).describe('消息列表')
})
})
},
'/set_msg_emoji_like': {
description: '贴表情',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
emoji_id: z.number().describe('表情ID'),
set: z.boolean().describe('?')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/get_friend_msg_history': {
description: '获取好友消息历史',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('用户QQ号'),
message_seq: z.union([z.string(), z.number()]).describe('消息序号'),
count: z.number().int().positive().describe('获取数量'),
reverseOrder: z.boolean().describe('是否倒序')
}),
response: baseResponseSchema.extend({
data: z.object({
messages: z.array(messageSchema)
})
})
},
'/get_recent_contact': {
description: '最近消息列表',
request: z.object({
count: z.number().int().positive().describe('获取数量')
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
lastestMsg: messageSchema,
peerUin: z.string().describe('对方QQ号'),
remark: z.string().describe('备注'),
msgTime: z.string().describe('消息时间'),
chatType: z.number().describe('聊天类型'),
msgId: z.string().describe('消息ID'),
sendNickName: z.string().describe('发送人昵称'),
sendMemberName: z.string().describe('发送人?昵称'),
peerName: z.string().describe('对方昵称')
})
)
})
},
'/fetch_emoji_like': {
description: '获取贴表情详情',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
emojiId: z.string().describe('表情ID'),
emojiType: z.string().describe('表情类型'),
group_id: z.union([z.string(), z.number()]).optional().describe('群号'),
user_id: z
.union([z.string(), z.number()])
.optional()
.describe('用户QQ号'),
count: z.number().int().positive().optional().describe('获取数量')
}),
response: baseResponseSchema.extend({
data: z.object({
result: z.number().describe('结果'),
errMsg: z.string().describe('错误信息'),
emojiLikesList: z
.array(
z
.object({
tinyId: z.string().describe('表情ID'),
nickName: z.string().describe('昵称?'),
headUrl: z.string().describe('头像?')
})
.describe('表情点赞列表')
)
.describe('表情点赞列表'),
cookie: z.string().describe('cookie'),
isLastPage: z.boolean().describe('是否最后一页'),
isFirstPage: z.boolean().describe('是否第一页')
})
})
},
'/get_forward_msg': {
description: '获取合并转发消息',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息ID')
}),
response: baseResponseSchema.extend({
data: z.object({})
})
},
'/send_forward_msg': {
description: '发送合并转发消息',
request: z.object({
group_id: z.union([z.string(), z.number()]).optional().describe('群号'),
user_id: z
.union([z.string(), z.number()])
.optional()
.describe('用户QQ号'),
messages: z.array(messageNodeSchema).describe('消息内容'),
news: z
.array(
z.object({
text: z.string()
})
)
.describe('?'),
prompt: z.string().describe('外显'),
summary: z.string().describe('底下文字'),
source: z.string().describe('内容')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema.extend({
data: z.object({})
})
})
}
} as const
export default oneBotHttpApiMessage

View File

@@ -1,119 +0,0 @@
import { z } from 'zod'
const messageNode = z.union([
z
.object({
type: z.literal('text'),
data: z.object({
text: z.string()
})
})
.describe('文本消息'),
z
.object({
type: z.literal('at'),
data: z.object({
qq: z.string()
})
})
.describe('@某人'),
z
.object({
type: z.literal('image'),
data: z.object({
file: z.string()
})
})
.describe('图片消息'),
z
.object({
type: z.literal('face'),
data: z.object({
id: z.number()
})
})
.describe('表情消息'),
z
.object({
type: z.literal('json'),
data: z.object({
data: z.string()
})
})
.describe('json 卡片消息'),
z
.object({
type: z.literal('record'),
data: z.object({
file: z.string()
})
})
.describe('语音消息'),
z
.object({
type: z.literal('video'),
data: z.object({
file: z.string()
})
})
.describe('视频消息'),
z
.object({
type: z.literal('reply'),
data: z.object({
id: z.number()
})
})
.describe('回复消息'),
z
.object({
type: z.literal('music'),
data: z.union([
z.object({
type: z.enum(['qq', '163']),
id: z.string()
}),
z.object({
type: z.literal('custom'),
url: z.string(),
audio: z.string(),
title: z.string(),
image: z.string()
})
])
})
.describe('音乐消息'),
z
.object({
type: z.literal('dice')
})
.describe('掷骰子'),
z
.object({
type: z.literal('rps')
})
.describe('猜拳'),
z
.object({
type: z.literal('file'),
data: z.object({
file: z.string().describe('文件路径,服务器本地或者网络文件均可')
})
})
.describe('发送消息')
])
export const nodeMessage = z
.object({
type: z.literal('node'),
data: z.object({
user_id: z.string(),
nickname: z.string(),
content: z.array(messageNode)
})
})
.describe('消息节点')
const messageNodeSchema = z.union([messageNode, nodeMessage])
export default messageNodeSchema

View File

@@ -1,80 +0,0 @@
import { z } from 'zod'
import type { ZodSchema } from 'zod'
import { baseResponseSchema, commonResponseDataSchema } from '../response'
import messageNodeSchema, { nodeMessage } from './node'
const oneBotHttpApiMessagePrivate: Record<
string,
{
description?: string
request: ZodSchema
response: ZodSchema
}
> = {
'/send_private_msg': {
description: '发送私聊消息',
request: z
.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
message: z.array(messageNodeSchema).describe('消息内容')
})
.refine(
(data) => {
const hasReply = data.message.some((item) => item.type === 'reply')
if (hasReply) {
return data.message[0].type === 'reply'
}
return true
},
{
message:
'如果 message 包含 reply 类型的消息,那么只能包含一个,而且排在最前面'
}
),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/send_private_forward_msg': {
description: '发送私聊合并转发消息',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
messages: z.array(nodeMessage).describe('消息内容'),
news: z
.array(
z.object({
text: z.string()
})
)
.describe('?'),
prompt: z.string().describe('外显'),
summary: z.string().describe('底下文本'),
source: z.string().describe('内容')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/forward_friend_single_msg': {
description: '消息转发到私聊',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
message_id: z.union([z.string(), z.number()]).describe('消息ID')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/group_poke': {
description: '发送私聊戳一戳',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号')
}),
response: baseResponseSchema
}
}
export default oneBotHttpApiMessagePrivate

View File

@@ -1,335 +0,0 @@
import { z } from 'zod'
// 定义 set_online_status 的 data 格式
const onlineStatusDataSchema = z.union([
// 在线
z
.object({
status: z.literal(10),
ext_status: z.literal(0),
battery_status: z.literal(0)
})
.describe('在线'),
// Q我吧
z
.object({
status: z.literal(60),
ext_status: z.literal(0),
battery_status: z.literal(0)
})
.describe('Q我吧'),
// 离开
z
.object({
status: z.literal(30),
ext_status: z.literal(0),
battery_status: z.literal(0)
})
.describe('离开'),
// 忙碌
z
.object({
status: z.literal(50),
ext_status: z.literal(0),
battery_status: z.literal(0)
})
.describe('忙碌'),
// 请勿打扰
z
.object({
status: z.literal(70),
ext_status: z.literal(0),
battery_status: z.literal(0)
})
.describe('请勿打扰'),
// 隐身
z
.object({
status: z.literal(40),
ext_status: z.literal(0),
battery_status: z.literal(0)
})
.describe('隐身'),
// 听歌中
z
.object({
status: z.literal(10),
ext_status: z.literal(1028),
battery_status: z.literal(0)
})
.describe('听歌中'),
// 春日限定
z
.object({
status: z.literal(10),
ext_status: z.literal(2037),
battery_status: z.literal(0)
})
.describe('春日限定'),
// 一起元梦
z
.object({
status: z.literal(10),
ext_status: z.literal(2025),
battery_status: z.literal(0)
})
.describe('一起元梦'),
// 求星搭子
z
.object({
status: z.literal(10),
ext_status: z.literal(2026),
battery_status: z.literal(0)
})
.describe('求星搭子'),
// 被掏空
z
.object({
status: z.literal(10),
ext_status: z.literal(2014),
battery_status: z.literal(0)
})
.describe('被掏空'),
// 今日天气
z
.object({
status: z.literal(10),
ext_status: z.literal(1030),
battery_status: z.literal(0)
})
.describe('今日天气'),
// 我crash了
z
.object({
status: z.literal(10),
ext_status: z.literal(2019),
battery_status: z.literal(0)
})
.describe('我crash了'),
// 爱你
z
.object({
status: z.literal(10),
ext_status: z.literal(2006),
battery_status: z.literal(0)
})
.describe('爱你'),
// 恋爱中
z
.object({
status: z.literal(10),
ext_status: z.literal(1051),
battery_status: z.literal(0)
})
.describe('恋爱中'),
// 好运锦鲤
z
.object({
status: z.literal(10),
ext_status: z.literal(1071),
battery_status: z.literal(0)
})
.describe('好运锦鲤'),
// 水逆退散
z
.object({
status: z.literal(10),
ext_status: z.literal(1201),
battery_status: z.literal(0)
})
.describe('水逆退散'),
// 嗨到飞起
z
.object({
status: z.literal(10),
ext_status: z.literal(1056),
battery_status: z.literal(0)
})
.describe('嗨到飞起'),
// 元气满满
z
.object({
status: z.literal(10),
ext_status: z.literal(1058),
battery_status: z.literal(0)
})
.describe('元气满满'),
// 宝宝认证
z
.object({
status: z.literal(10),
ext_status: z.literal(1070),
battery_status: z.literal(0)
})
.describe('宝宝认证'),
// 一言难尽
z
.object({
status: z.literal(10),
ext_status: z.literal(1063),
battery_status: z.literal(0)
})
.describe('一言难尽'),
// 难得糊涂
z
.object({
status: z.literal(10),
ext_status: z.literal(2001),
battery_status: z.literal(0)
})
.describe('难得糊涂'),
// emo中
z
.object({
status: z.literal(10),
ext_status: z.literal(1401),
battery_status: z.literal(0)
})
.describe('emo中'),
// 我太难了
z
.object({
status: z.literal(10),
ext_status: z.literal(1062),
battery_status: z.literal(0)
})
.describe('我太难了'),
// 我想开了
z
.object({
status: z.literal(10),
ext_status: z.literal(2013),
battery_status: z.literal(0)
})
.describe('我想开了'),
// 我没事
z
.object({
status: z.literal(10),
ext_status: z.literal(1052),
battery_status: z.literal(0)
})
.describe('我没事'),
// 想静静
z
.object({
status: z.literal(10),
ext_status: z.literal(1061),
battery_status: z.literal(0)
})
.describe('想静静'),
// 悠哉哉
z
.object({
status: z.literal(10),
ext_status: z.literal(1059),
battery_status: z.literal(0)
})
.describe('悠哉哉'),
// 去旅行
z
.object({
status: z.literal(10),
ext_status: z.literal(2015),
battery_status: z.literal(0)
})
.describe('去旅行'),
// 信号弱
z
.object({
status: z.literal(10),
ext_status: z.literal(1011),
battery_status: z.literal(0)
})
.describe('信号弱'),
// 出去浪
z
.object({
status: z.literal(10),
ext_status: z.literal(2003),
battery_status: z.literal(0)
})
.describe('出去浪'),
// 肝作业
z
.object({
status: z.literal(10),
ext_status: z.literal(2012),
battery_status: z.literal(0)
})
.describe('肝作业'),
// 学习中
z
.object({
status: z.literal(10),
ext_status: z.literal(1018),
battery_status: z.literal(0)
})
.describe('学习中'),
// 搬砖中
z
.object({
status: z.literal(10),
ext_status: z.literal(2023),
battery_status: z.literal(0)
})
.describe('搬砖中'),
// 摸鱼中
z
.object({
status: z.literal(10),
ext_status: z.literal(1300),
battery_status: z.literal(0)
})
.describe('摸鱼中'),
// 无聊中
z
.object({
status: z.literal(10),
ext_status: z.literal(1060),
battery_status: z.literal(0)
})
.describe('无聊中'),
// timi中
z
.object({
status: z.literal(10),
ext_status: z.literal(1027),
battery_status: z.literal(0)
})
.describe('timi中'),
// 睡觉中
z
.object({
status: z.literal(10),
ext_status: z.literal(1016),
battery_status: z.literal(0)
})
.describe('睡觉中'),
// 熬夜中
z
.object({
status: z.literal(10),
ext_status: z.literal(1032),
battery_status: z.literal(0)
})
.describe('熬夜中'),
// 追剧中
z
.object({
status: z.literal(10),
ext_status: z.literal(1021),
battery_status: z.literal(0)
})
.describe('追剧中'),
// 我的电量
z
.object({
status: z.literal(10),
ext_status: z.literal(1000),
battery_status: z.literal(0)
})
.describe('我的电量')
])
export default onlineStatusDataSchema

View File

@@ -1,16 +0,0 @@
import { z } from 'zod'
// 通用响应格式
export const baseResponseSchema = z.object({
status: z.enum(['ok', 'error']).describe('请求状态'), // 状态
retcode: z.number().describe('响应🐎'), // 返回码
data: z.null(),
message: z.string().describe('提示信息'), // 提示信息
wording: z.string().describe('提示信息(人性化)'), // 人性化提示
echo: z.string().describe('回显') // 请求回显内容
})
export const commonResponseDataSchema = z.object({
result: z.number(),
errMsg: z.string()
})

View File

@@ -1,366 +0,0 @@
import { z } from 'zod'
import { baseResponseSchema, commonResponseDataSchema } from './response'
const oneBotHttpApiSystem = {
'/get_online_clients': {
description: '获取当前账号在线客户端列表',
request: z.object({
no_cache: z.boolean().optional().describe('是否不使用缓存')
}),
response: baseResponseSchema.extend({
data: z.object({
clients: z.object({})
})
})
},
'/get_robot_uin_range': {
description: '获取机器人账号范围',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
minUin: z.string(),
maxUin: z.string()
})
)
})
},
'/ocr_image': {
description: 'OCR图片识别',
request: z.object({
image: z.string()
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
text: z.string(),
pt1: z.object({
x: z.string(),
y: z.string()
}),
pt2: z.object({
x: z.string(),
y: z.string()
}),
pt3: z.object({
x: z.string(),
y: z.string()
}),
pt4: z.object({
x: z.string(),
y: z.string()
}),
charBox: z.array(
z.object({
charText: z.string(),
charBox: z.object({
pt1: z.object({
x: z.string(),
y: z.string()
}),
pt2: z.object({
x: z.string(),
y: z.string()
}),
pt3: z.object({
x: z.string(),
y: z.string()
}),
pt4: z.object({
x: z.string(),
y: z.string()
})
})
})
),
score: z.string()
})
)
})
},
'/.ocr_image': {
description: '.OCR图片识别',
request: z.object({
image: z.string()
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
text: z.string(),
pt1: z.object({
x: z.string(),
y: z.string()
}),
pt2: z.object({
x: z.string(),
y: z.string()
}),
pt3: z.object({
x: z.string(),
y: z.string()
}),
pt4: z.object({
x: z.string(),
y: z.string()
}),
charBox: z.array(
z.object({
charText: z.string(),
charBox: z.object({
pt1: z.object({
x: z.string(),
y: z.string()
}),
pt2: z.object({
x: z.string(),
y: z.string()
}),
pt3: z.object({
x: z.string(),
y: z.string()
}),
pt4: z.object({
x: z.string(),
y: z.string()
})
})
})
),
score: z.string()
})
)
})
},
'/translate_en2zh': {
description: '英文翻译为中文',
request: z.object({
words: z.array(z.string())
}),
response: baseResponseSchema.extend({
data: z.array(z.string())
})
},
'/get_login_info': {
description: '获取登录号信息',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
user_id: z.number(),
nickname: z.string()
})
})
},
'/set_input_status': {
description: '设置输入状态',
request: z.object({
eventType: z.union([z.literal(0), z.literal(1)]),
user_id: z.union([z.number(), z.string()])
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/download_file': {
description: '下载文件到缓存目录',
request: z
.object({
base64: z.string().optional(),
url: z.string().optional(),
thread_count: z.number(),
headers: z.union([z.string(), z.array(z.string())]),
name: z.string().optional()
})
.refine(
(data) => (data.base64 && !data.url) || (!data.base64 && data.url),
{
message: 'base64 和 url 必须二选一,且不能同时存在或同时为空',
path: ['base64', 'url']
}
),
response: baseResponseSchema.extend({
data: z.object({
file: z.string()
})
})
},
'/get_cookies': {
description: '获取cookies',
request: z.object({
domain: z.string()
}),
response: baseResponseSchema.extend({
data: z.object({
cookies: z.string(),
bkn: z.string()
})
})
},
'/.handle_quick_operation': {
description: '.对事件执行快速操作',
request: z.object({
context: z.object({}),
operation: z.object({})
}),
response: baseResponseSchema
},
'/get_csrf_token': {
description: '获取CSRF Token',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
token: z.number()
})
})
},
'/_del_group_notice': {
description: '_删除群公告',
request: z.object({
group_id: z.union([z.number(), z.string()]),
notice_id: z.number()
}),
response: baseResponseSchema
},
'/get_credentials': {
description: '获取 QQ 相关接口凭证',
request: z.object({
domain: z.string()
}),
response: baseResponseSchema.extend({
data: z.object({
cookies: z.string(),
token: z.number()
})
})
},
'/_get_model_show': {
description: '_获取在线机型',
request: z.object({
model: z.string()
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
variants: z.object({
model_show: z.string(),
need_pay: z.boolean()
})
})
)
})
},
'/_set_model_show': {
description: '_设置在线机型',
request: z.object({
model: z.string(),
model_show: z.string()
}),
response: baseResponseSchema
},
'/can_send_image': {
description: '检查是否可以发送图片',
request: z.object({}),
response: baseResponseSchema.extend({
yes: z.boolean()
})
},
'/nc_get_packet_status': {
description: '获取packet状态',
request: z.object({}),
response: baseResponseSchema
},
'/can_send_record': {
description: '检查是否可以发送语音',
request: z.object({}),
response: baseResponseSchema.extend({
yes: z.boolean()
})
},
'/get_status': {
description: '获取状态',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
online: z.boolean(),
good: z.boolean(),
stat: z.object({})
})
})
},
'/nc_get_rkey': {
description: '获取rkey',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
rkey: z.string(),
ttl: z.string(),
time: z.number(),
type: z.number()
})
)
})
},
'/get_version_info': {
description: '获取版本信息',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
app_name: z.string(),
protocol_version: z.string(),
app_version: z.string()
})
})
},
'/get_group_shut_list': {
description: '获取群禁言列表',
request: z.object({
group_id: z.union([z.number(), z.string()])
}),
response: baseResponseSchema.extend({
data: z.object({
uid: z.string(),
qid: z.string(),
uin: z.string(),
nick: z.string(),
remark: z.string(),
cardType: z.number(),
cardName: z.string(),
role: z.number(),
avatarPath: z.string(),
shutUpTime: z.number(),
isDelete: z.boolean(),
isSpecialConcerned: z.boolean(),
isSpecialShield: z.boolean(),
isRobot: z.boolean(),
groupHonor: z.record(z.number()),
memberRealLevel: z.number(),
memberLevel: z.number(),
globalGroupLevel: z.number(),
globalGroupPoint: z.number(),
memberTitleId: z.number(),
memberSpecialTitle: z.string(),
specialTitleExpireTime: z.string(),
userShowFlag: z.number(),
userShowFlagNew: z.number(),
richFlag: z.number(),
mssVipType: z.number(),
bigClubLevel: z.number(),
bigClubFlag: z.number(),
autoRemark: z.string(),
creditLevel: z.number(),
joinTime: z.number(),
lastSpeakTime: z.number(),
memberFlag: z.number(),
memberFlagExt: z.number(),
memberMobileFlag: z.number(),
memberFlagExt2: z.number(),
isSpecialShielded: z.boolean(),
cardNameId: z.number()
})
})
}
} as const
export default oneBotHttpApiSystem

View File

@@ -1,278 +0,0 @@
import { z } from 'zod'
import onlineStatusDataSchema from './online_status'
import { baseResponseSchema, commonResponseDataSchema } from './response'
const oneBotHttpApiUser = {
'/set_qq_profile': {
description: '设置账号信息',
request: z.object({
nickname: z.string().describe('昵称'),
personal_note: z.string().describe('个性签名'),
sex: z.string().optional().describe('性别')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/ArkSharePeer': {
description: '获取推荐好友/群聊卡片',
request: z
.object({
group_id: z
.union([z.string(), z.number()])
.optional()
.describe('群聊ID与 user_id 二选一'),
user_id: z
.union([z.string(), z.number()])
.optional()
.describe('用户ID与 group_id 二选一'),
phoneNumber: z.string().optional().describe('对方手机号码')
})
.refine(
(data) =>
(data.group_id && !data.user_id) || (!data.group_id && data.user_id),
{
message: 'group_id 和 user_id 必须二选一,且不能同时存在或同时为空',
path: ['group_id', 'user_id'] // 错误路径
}
),
response: baseResponseSchema.extend({
data: z.object({
errCode: z.number(),
errMsg: z.string(),
arkJson: z.string()
})
})
},
'/ArkShareGroup': {
description: '获取推荐群聊卡片',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群聊ID')
}),
response: baseResponseSchema.extend({
data: z.string()
})
},
'/set_online_status': {
description: '设置在线状态',
request: z.object({
data: onlineStatusDataSchema
}),
response: baseResponseSchema
},
'/get_friends_with_category': {
description: '获取好友分组列表',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
categoryId: z.number(),
categorySortId: z.number(),
categoryName: z.string(),
categoryMbCount: z.number(),
onlineCount: z.number(),
buddyList: z.array(
z.object({
qid: z.string(),
longNick: z.string(),
birthday_year: z.number(),
birthday_month: z.number(),
birthday_day: z.number(),
age: z.number(),
sex: z.string(),
eMail: z.string(),
phoneNum: z.string(),
categoryId: z.number(),
richTime: z.number(),
richBuffer: z.object({}),
uid: z.string(),
uin: z.string(),
nick: z.string(),
remark: z.string(),
user_id: z.number(),
nickname: z.string(),
level: z.number()
})
)
})
)
})
},
'/set_qq_avatar': {
description: '设置头像',
request: z.object({
file: z.string().describe('图片文件路径(服务器本地或者远程均可)')
}),
response: baseResponseSchema
},
'/send_like': {
description: '点赞',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
times: z.number().describe('点赞次数')
}),
response: baseResponseSchema
},
'/create_collection': {
description: '创建收藏',
request: z.object({
rawData: z.string().describe('收藏内容'),
brief: z.string().describe('收藏简介')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/set_friend_add_request': {
description: '处理好友请求',
request: z.object({
flag: z.string().describe('请求ID'),
approve: z.boolean().describe('是否同意'),
remark: z.string().describe('好友备注')
}),
response: baseResponseSchema
},
'/set_self_longnick': {
description: '设置个性签名',
request: z.object({
longNick: z.string().describe('签名内容')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/get_stranger_info': {
description: '获取账号信息',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号')
}),
response: baseResponseSchema.extend({
data: z.object({
user_id: z.number(),
uid: z.string(),
uin: z.string(),
nickname: z.string(),
age: z.number(),
qid: z.string(),
qqLevel: z.number(),
sex: z.string(),
long_nick: z.string(),
reg_time: z.number(),
is_vip: z.boolean(),
is_years_vip: z.boolean(),
vip_level: z.number(),
remark: z.string(),
status: z.number(),
login_days: z.number()
})
})
},
'/get_friend_list': {
description: '获取好友列表',
request: z.object({
no_cache: z.boolean().describe('是否不使用缓存')
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
qid: z.string(),
longNick: z.string(),
birthday_year: z.number(),
birthday_month: z.number(),
birthday_day: z.number(),
age: z.number(),
sex: z.string(),
eMail: z.string(),
phoneNum: z.string(),
categoryId: z.number(),
richTime: z.number(),
richBuffer: z.object({}),
uid: z.string(),
uin: z.string(),
nick: z.string(),
remark: z.string(),
user_id: z.number(),
nickname: z.string(),
level: z.number()
})
)
})
},
'/get_profile_like': {
description: '获取点赞列表',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
total_count: z.number(),
new_count: z.number(),
new_nearby_count: z.number(),
last_visit_time: z.number(),
userInfos: z.array(
z.object({
uid: z.string(),
src: z.number(),
latestTime: z.number(),
count: z.number(),
giftCount: z.number(),
customId: z.number(),
lastCharged: z.number(),
bAvailableCnt: z.number(),
bTodayVotedCnt: z.number(),
nick: z.string(),
gender: z.number(),
age: z.number(),
isFriend: z.boolean(),
isvip: z.boolean(),
isSvip: z.boolean(),
uin: z.number()
})
)
})
})
},
'/fetch_custom_face': {
description: '获取收藏表情',
request: z.object({
count: z.number().optional().describe('获取数量')
}),
response: baseResponseSchema.extend({
data: z.array(z.string())
})
},
'/upload_private_file': {
description: '上传私聊文件',
request: z.object({
user_id: z.union([z.string(), z.number()]),
file: z.string(),
name: z.string()
}),
response: baseResponseSchema
},
'/delete_friend': {
description: '删除好友',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('自己QQ号'),
friend_id: z.union([z.string(), z.number()]).describe('好友QQ号'),
temp_block: z.boolean().describe('是否加入黑名单'),
temp_both_del: z.boolean().describe('是否双向删除')
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema
})
},
'/nc_get_user_status': {
description: '获取用户在线状态',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号')
}),
response: baseResponseSchema.extend({
data: z.object({
status: z.number(),
ext_status: z.number()
})
})
}
} as const
export default oneBotHttpApiUser

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