Compare commits

..

13 Commits

Author SHA1 Message Date
dependabot[bot]
ff6d76a1a7 Bump ajv from 8.17.1 to 8.18.0
Bumps [ajv](https://github.com/ajv-validator/ajv) from 8.17.1 to 8.18.0.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v8.17.1...v8.18.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 8.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 09:02:16 +00:00
bietiaop
b42b6c3cf0 docs(README): update multi-language README.md
Some checks failed
Build NapCat Artifacts / Build-Framework (push) Has been cancelled
Build NapCat Artifacts / Build-Shell (push) Has been cancelled
2026-02-28 12:32:48 +08:00
手瓜一十雪
740d2118f1 Comment out plugin import UI and handlers
Some checks failed
Build NapCat Artifacts / Build-Framework (push) Has been cancelled
Build NapCat Artifacts / Build-Shell (push) Has been cancelled
Temporarily disable the local plugin import flow: remove the unused useRef import, comment out the FiUpload import, fileInputRef declaration, and the related handlers (handleImportClick and handleFileChange). Keeps the rest of the plugin page (loading, modal and plugin list logic) intact so the import feature can be re-enabled later.
2026-02-22 13:34:03 +08:00
手瓜一十雪
77bdcfd249 Disable local plugin import and upload handling
Commented out ImportLocalPluginHandler import and the multer upload configuration (50MB limit and .zip file filter). Temporarily disables local plugin .zip uploads, likely for debugging or while refactoring the upload/import flow; can be re-enabled when upload handling is restored.
2026-02-22 13:33:06 +08:00
手瓜一十雪
896e1c209a Disable plugin upload feature
Comment out the backend import route and corresponding frontend upload UI to disable plugin uploads. Backend: commented out POST /Import route in packages/napcat-webui-backend/src/router/Plugin.ts. Frontend: removed (commented) the upload Button and hidden file input in packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx. This temporarily prevents users from uploading plugins.
2026-02-22 13:15:42 +08:00
手瓜一十雪
cff07c7ce5 Revise README for new maintainer notice
Some checks failed
Build NapCat Artifacts / Build-Framework (push) Has been cancelled
Build NapCat Artifacts / Build-Shell (push) Has been cancelled
Updated the README to replace the 'New Feature' section with a 'Notice' about seeking new maintainers and removed outdated information.

Signed-off-by: 手瓜一十雪 <nanaeonn@outlook.com>
2026-02-21 19:22:07 +08:00
手瓜一十雪
5c04b799a6 feat: 提高超时检测等待 2026-02-21 16:21:31 +08:00
ud2
1fc4655ae1 refactor(onebot): 精简 WebSocket 适配器实现 (#1644)
* refactor(onebot): 移除 `async-mutex` 依赖

* fix(onebot): 避免重复发送 WebSocket pong
2026-02-21 15:59:12 +08:00
Eric-Terminal
eb07cdb715 feat: 自动登录失败后回退密码登录并补充独立配置 (#1638)
* feat: 自动登录失败后回退密码登录并补充独立配置

改动文件:
- packages/napcat-webui-backend/src/helper/config.ts
- packages/napcat-webui-backend/src/utils/auto_login.ts
- packages/napcat-webui-backend/src/utils/auto_login_config.ts
- packages/napcat-webui-backend/index.ts
- packages/napcat-webui-backend/src/api/QQLogin.ts
- packages/napcat-webui-backend/src/router/QQLogin.ts
- packages/napcat-webui-frontend/src/controllers/qq_manager.ts
- packages/napcat-webui-frontend/src/pages/dashboard/config/login.tsx
- packages/napcat-test/autoPasswordFallback.test.ts

目的:
- 在启动阶段将自动登录流程从“仅快速登录”扩展为“快速登录失败后自动回退密码登录”,并保持二维码兜底。
- 在 WebUI 登录配置页新增独立的自动回退账号/密码配置,密码仅提交与存储 MD5,不回显明文。

效果:
- 后端配置新增 autoPasswordLoginAccount 与 autoPasswordLoginPasswordMd5 字段,并提供读取、更新(空密码不覆盖)和清空能力。
- 新增 QQLogin API:GetAutoPasswordLoginConfig / SetAutoPasswordLoginConfig / ClearAutoPasswordLoginConfig。
- WebUI 登录配置页新增自动回退密码登录区块,支持保存、刷新、清空及“留空不修改密码”交互。
- 新增自动登录回退逻辑单测与配置补丁构造单测,覆盖快速成功、回退成功、回退失败、无密码兜底等场景。

* feat: 精简为环境变量驱动的快速登录失败密码回退

改动目的:
- 按维护者建议将方案收敛为后端环境变量驱动,不新增 WebUI 配置与路由
- 保留“快速登录失败 -> 密码回退 -> 二维码兜底”核心能力
- 兼容快速启动参数场景,降低评审复杂度

主要改动文件:
- packages/napcat-webui-backend/index.ts
- packages/napcat-shell/base.ts
- packages/napcat-webui-backend/src/api/QQLogin.ts
- packages/napcat-webui-backend/src/helper/config.ts
- packages/napcat-webui-backend/src/router/QQLogin.ts
- packages/napcat-webui-frontend/src/controllers/qq_manager.ts
- packages/napcat-webui-frontend/src/pages/dashboard/config/login.tsx
- 删除:packages/napcat-webui-backend/src/utils/auto_login.ts
- 删除:packages/napcat-webui-backend/src/utils/auto_login_config.ts
- 删除:packages/napcat-test/autoPasswordFallback.test.ts

实现细节:
1. WebUI 启动自动登录链路
- 保留 NAPCAT_QUICK_ACCOUNT 优先逻辑
- 快速登录失败后触发密码回退
- 回退密码来源优先级:
  a) NAPCAT_QUICK_PASSWORD_MD5(32 位 MD5)
  b) NAPCAT_QUICK_PASSWORD(运行时自动计算 MD5)
- 未配置回退密码时保持二维码兜底,并输出带 QQ 号的引导日志

2. Shell 快速登录链路
- quickLoginWithUin 失败判定统一基于 result 码 + errMsg
- 覆盖历史账号不存在、凭证失效、快速登录异常等场景
- 失败后统一进入同一密码回退逻辑,再兜底二维码

3. 文案与可运维性
- 日志明确推荐优先使用 ACCOUNT + NAPCAT_QUICK_PASSWORD
- NAPCAT_QUICK_PASSWORD_MD5 作为备用方式

效果:
- 满足自动回退登录需求,且改动面显著缩小
- 不修改 napcat-docker 仓库代码,直接兼容现有容器启动参数
- 便于上游快速审阅与合并

* fix: 修复 napcat-framework 未使用变量导致的 CI typecheck 失败

改动文件:
- packages/napcat-framework/napcat.ts

问题背景:
- 上游代码中声明了变量 bypassEnabled,但后续未使用
- 在 CI 的全量 TypeScript 检查中触发 TS6133(声明但未读取)
- 导致 PR Build 机器人评论显示构建失败(Type check failed)

具体修复:
- 将以下语句从“赋值后未使用”改为“直接调用”
- 原:const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions);
- 现:napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions);

影响与效果:
- 不改变运行时行为(仍会执行 enableAllBypasses)
- 消除 TS6133 报错,恢复 typecheck 可通过

本地验证:
- pnpm run typecheck:通过
- pnpm run build:framework:通过
- pnpm run build:shell:通过

---------

Co-authored-by: 手瓜一十雪 <nanaeonn@outlook.com>
2026-02-21 14:18:34 +08:00
手瓜一十雪
964fd98914 Add spinner during captcha verification
Show a loading spinner and message while captcha is being verified. Imported Spinner and added an optional captchaVerifying prop to PasswordLogin to toggle between the TencentCaptchaModal and a waiting state. In qq_login.tsx introduced captchaVerifying state, set it true before the captcha login request and reset it in finally, and passed the prop down to the PasswordLogin component.
2026-02-21 13:39:35 +08:00
手瓜一十雪
f9764c9559 Improve new-device QR handling and bypass init
Refactor new-device QR flow and streamline bypass init:

- napcat-shell: stop verbose logging and removed check of enableAllBypasses return value; just invoke native enableAllBypasses when not disabled by env.
- backend (QQLogin): simplify extraction of tokens from jumpUrl (use sig and uin-token), return an error if missing, and send oidbRequest directly (removed nested try/catch and regex fallback).
- frontend (new_device_verify): accept result.str_url without requiring bytes_token and pass an empty string to polling when bytes_token is absent.
- frontend (password_login): change render order to show captcha modal before new-device verification UI.
- frontend (qq_manager): normalize GetNewDeviceQRCode response — derive bytes_token from str_url's str_url query param (base64) when bytes_token is missing, and preserve extra status/error fields in the returned object.

These changes improve robustness when OIDB responses omit bytes_token, reduce noisy logs, and ensure the UI and polling still function.
2026-02-21 13:24:56 +08:00
手瓜一十雪
b71a4913eb Add captcha & new-device QQ login flows
Introduce multi-step QQ password login support (captcha and new-device verification) and related OIDB QR handling.

- Change login signature fields in NodeIKernelLoginService to binary (Uint8Array) and add unusualDeviceCheckSig.
- Update shell base to handle additional result codes (captcha required, new-device, abnormal-device), set login status on success, and register three callbacks: captcha, new-device, and password flows. Use TextEncoder for encoding ticket/randstr/sid and newDevicePullQrCodeSig.
- Extend backend WebUiDataRuntime (types and runtime) with set/request methods for captcha and new-device login calls and adjust LoginRuntime types to return richer metadata (needCaptcha, proofWaterUrl, needNewDevice, jumpUrl, newDevicePullQrCodeSig).
- Add backend API handlers: CaptchaLogin, NewDeviceLogin, GetNewDeviceQRCode and PollNewDeviceQR; add oidbRequest helper using https to query oidb.tim.qq.com for QR generation and polling.
- Wire new handlers into QQLogin router and return structured success responses when further steps are required.
- Add frontend components and pages for captcha and new-device verification (new files: 1.html, new_device_verify.tsx, tencent_captcha.tsx) and update existing frontend controllers/pages to integrate the new flows.
- Improve error logging and user-facing messages for the new flows.

This change enables handling of password-login scenarios requiring captcha or device attestation and provides endpoints to obtain and poll OIDB QR codes for new-device verification.
2026-02-21 13:03:40 +08:00
手瓜一十雪
f961830836 Inline plugin icon helper into PluginCard
Some checks failed
Build NapCat Artifacts / Build-Framework (push) Has been cancelled
Build NapCat Artifacts / Build-Shell (push) Has been cancelled
Move getPluginIconUrl from utils/plugin_icon.ts into plugin_card.tsx and remove the now-unused util file. The function behavior is unchanged: it reads webui_token from localStorage and appends it as a query parameter to plugin icon URLs so authenticated icon endpoints can be used as img src.
2026-02-20 23:36:45 +08:00
35 changed files with 1833 additions and 468 deletions

View File

@@ -5,47 +5,41 @@
_Modern protocol-side framework implemented based on NTQQ._
> 云起兮风生,心向远方兮路未曾至.
> Where clouds rise and winds are born, the heart yearns for distant lands yet untraveled.
English | [简体中文](./docs/README_zh-CN.md) | [繁體中文](./docs/README_zh-TW.md) | [日本語](./docs/README_ja.md) | [한국어](./docs/README_ko.md) | [Русский](./docs/README_ru.md) | [العربية](./docs/README_ar.md)
</div>
---
## New Feature
在 v4.8.115+ 版本开始
1. NapCatQQ 支持 [Stream Api](https://napneko.github.io/develop/file)
2. NapCatQQ 推荐 message_id/user_id/group_id 均使用字符串类型
- [1] 解决 Docker/跨设备/大文件 的多媒体上下传问题
- [2] 采用字符串可以解决扩展到int64的问题同时也可以解决部分语言如JavaScript对大整数支持不佳的问题增加极少成本。
## Notice
NapCat is currently looking for a new primary maintainer. Please email nanaeonn@outlook.com if you are interested. No public community groups will be established during this period, but NapCat will continue to receive regular updates.
## Welcome
- NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
- NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## Feature
## Features
- **Easy to Use**
- 作为初学者能够轻松使用.
- Beginners can get started effortlessly.
- **Quick and Efficient**
- 在低内存操作系统长时运行.
- Runs for extended periods on low-memory systems.
- **Rich API Interface**
- 完整实现了大部分标准接口.
- Fully implements most standard interfaces.
- **Stable and Reliable**
- 持续稳定的开发与维护.
- Continuous and stable development and maintenance.
## Quick Start
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
Download the latest version from the [Release](https://github.com/NapNeko/NapCatQQ/releases/) page.
**首次使用**请务必查看如下文档看使用教程
**First-time users** should make sure to read the documentation below for a usage guide.
> 项目非盈利,涉及 对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
> This is a non-profit project. For integration issues, basic questions, or underlying framework issues, please search for solutions on your own — this project's community does not provide such support.
## Link
## Links
| 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/) |
|:-:|:-:|:-:|:-:|
@@ -53,37 +47,31 @@ _Modern protocol-side framework implemented based on NTQQ._
| 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) |
|:-:|:-:|:-:|:-:|:-:|
| Telegram | [![Telegram](https://img.shields.io/badge/Telegram-napcatqq-blue)](https://t.me/napcatqq) |
|:-:|:-:|
| DeepWiki | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/NapNeko/NapCatQQ) |
|:-:|:-:|
> 请不要在其余社区提及本项目(包括其余协议端/相关应用端项目)引发争论,如有建议到达官方交流群讨论或PR
> Please do not mention this project in other communities (including other protocol-side or related application-side projects) to avoid disputes. If you have suggestions, please discuss them in the official group or submit a PR.
## Thanks
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) — Great support for this project; some code was referenced (with permission)
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) — An LLM Bot framework that perfectly integrates with this project
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) — A cyber companion Bot framework
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) 基于NapCat的消息导出工具 在此推荐一下
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) — A message export tool based on NapCat
- 不过最最重要的 还是需要感谢屏幕前的你哦~
- And most importantly, thank you — the person reading this right now~
---
## License
本项目采用 混合协议 开源,因此使用本项目时,你需要注意以下几点:
This project is open-sourced under a hybrid license. When using this project, please note the following:
1. 第三方库代码或修改部分遵循其原始开源许可.
2. 本项目获取部分项目授权而不受部分约束
2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE).
1. Third-party library code or modified portions follow their original open-source licenses.
2. This project has obtained authorization from certain projects and is not subject to some restrictions.
3. The remaining logic code of the project is governed by the [repository license](./LICENSE).
**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
**This repository is solely intended to improve usability and implement message push features. Furthermore, developing any project based on NapCat code without prior authorization from the repository's main author is prohibited. Please comply with local laws and regulations when using this project. Any issues arising from misuse are the responsibility of the user and anyone providing tutorials for unauthorized use.**

81
docs/README_ar.md Normal file
View File

@@ -0,0 +1,81 @@
<div dir="rtl">
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=left />
<div align="center">
# NapCat
_إطار عمل حديث على جانب البروتوكول مبني على NTQQ._
> حيث تنهض السحب وتولد الرياح، يتوق القلب إلى أراضٍ بعيدة لم تُسلك بعد.
[English](../README.md) | [简体中文](./README_zh-CN.md) | [繁體中文](./README_zh-TW.md) | [日本語](./README_ja.md) | [한국어](./README_ko.md) | [Русский](./README_ru.md) | العربية
</div>
---
## إعلان
يبحث NapCat حالياً عن مشرف رئيسي جديد. يرجى التواصل عبر البريد الإلكتروني nanaeonn@outlook.com. لن يتم إنشاء مجموعات مجتمع عامة خلال هذه الفترة، لكن NapCat يضمن استمرار التحديثات المنتظمة.
## مرحباً
- NapCatQQ هو تطبيق حديث لبروتوكول Bot مبني على NTQQ
## المميزات
- **سهل الاستخدام**
- يمكن للمبتدئين البدء بسهولة.
- **سريع وفعال**
- يعمل لفترات طويلة على أنظمة ذات ذاكرة منخفضة.
- **واجهة API غنية**
- تنفيذ كامل لمعظم الواجهات القياسية.
- **مستقر وموثوق**
- تطوير وصيانة مستمرة ومستقرة.
## البداية السريعة
قم بتنزيل أحدث إصدار من صفحة [Release](https://github.com/NapNeko/NapCatQQ/releases/)
**للمستخدمين لأول مرة** تأكد من قراءة الوثائق أدناه للحصول على دليل الاستخدام
> هذا مشروع غير ربحي. للمشاكل المتعلقة بالتكامل والأسئلة الأساسية ومشاكل إطار العمل الأساسي، يرجى البحث عن الحلول بنفسك — مجتمع هذا المشروع لا يقدم مثل هذا الدعم.
## الروابط
| الوثائق | [![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/) |
|:-:|:-:|:-:|:-:|
| الوثائق | [![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/) |
|:-:|:-:|:-:|:-:|
| DeepWiki | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/NapNeko/NapCatQQ) |
|:-:|:-:|
> يرجى عدم ذكر هذا المشروع في مجتمعات أخرى (بما في ذلك مشاريع البروتوكول الأخرى / مشاريع التطبيقات ذات الصلة) لتجنب الخلافات. إذا كانت لديك اقتراحات، يرجى مناقشتها في المجموعة الرسمية أو تقديم PR.
## شكر وتقدير
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) — دعم كبير لهذا المشروع؛ تمت الإشارة إلى بعض الأكواد (بإذن)
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) — إطار عمل LLM Bot متوافق تماماً مع هذا المشروع
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) — إطار عمل Bot رفيق سيبراني
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) — أداة تصدير الرسائل مبنية على NapCat
- والأهم من ذلك كله، شكراً لك — أنت الذي تقرأ هذا الآن~
---
## الرخصة
هذا المشروع مفتوح المصدر بموجب رخصة هجينة. عند استخدام هذا المشروع، يرجى ملاحظة ما يلي:
1. كود المكتبات الخارجية أو الأجزاء المعدلة تخضع لرخصها مفتوحة المصدر الأصلية.
2. حصل هذا المشروع على إذن من بعض المشاريع ولا يخضع لبعض القيود.
3. باقي الكود المنطقي للمشروع يخضع [لرخصة هذا المستودع](../LICENSE).
**هذا المستودع مخصص فقط لتحسين سهولة الاستخدام وتنفيذ ميزات دفع الرسائل. علاوة على ذلك، يُحظر تطوير أي مشروع بناءً على كود NapCat دون إذن مسبق من المؤلف الرئيسي للمستودع. يرجى الامتثال للقوانين واللوائح المحلية عند الاستخدام. أي مشاكل ناتجة عن سوء الاستخدام تقع على عاتق المستخدم ومن يقدم تعليمات الاستخدام غير المصرح به.**
</div>

77
docs/README_ja.md Normal file
View File

@@ -0,0 +1,77 @@
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
<div align="center">
# NapCat
_NTQQをベースにした最新のプロトコル側フレームワーク._
> 雲起きて風生じ、心は遠方を向けど路は未だ至らず.
[English](../README.md) | [简体中文](./README_zh-CN.md) | [繁體中文](./README_zh-TW.md) | 日本語 | [한국어](./README_ko.md) | [Русский](./README_ru.md) | [العربية](./README_ar.md)
</div>
---
## お知らせ
NapCat は現在、新しいメインメンテナーを募集しています。nanaeonn@outlook.com までメールでご連絡ください。この期間中、公開コミュニティグループは設立しませんが、NapCat は通常通りの更新を保証します。
## ようこそ
- NapCatQQ は NTQQ をベースにした最新の Bot プロトコル側実装です
## 特徴
- **簡単に使える**
- 初心者でも簡単に使用できます.
- **高速で効率的**
- 低メモリ環境のOSで長時間稼働できます.
- **豊富な API インターフェース**
- ほとんどの標準インターフェースを完全に実装しています.
- **安定して信頼できる**
- 継続的で安定した開発とメンテナンスを行っています.
## クイックスタート
最新バージョンは [Release](https://github.com/NapNeko/NapCatQQ/releases/) ページからダウンロードできます
**初めてご使用の方**は、以下のドキュメントで使用ガイドを必ずご確認ください
> このプロジェクトは非営利です。連携に関する問題/基本的な問題/下位フレームワークの問題については、ご自身で検索して解決してください。本プロジェクトのコミュニティではそのような回答は提供していません。
## リンク
| ドキュメント | [![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/) |
|:-:|:-:|:-:|:-:|
| ドキュメント | [![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/) |
|:-:|:-:|:-:|:-:|
| DeepWiki | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/NapNeko/NapCatQQ) |
|:-:|:-:|
> 他のコミュニティでこのプロジェクト(他のプロトコル側/関連アプリケーション側のプロジェクトを含むに言及して論争を引き起こさないでください。提案がある場合は、公式交流グループで議論するか、PRを提出してください。
## 謝辞
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 本プロジェクトへの多大なサポート、一部コードを参考にしています(許可済み)
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) 本プロジェクトに完璧に対応した LLM Bot フレームワークです
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) サイバー仲間の 麦麦 Bot フレームワークです
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) NapCat ベースのメッセージエクスポートツールです
- そして何より大切なのは、画面の前のあなたに感謝することです~
---
## ライセンス
本プロジェクトはハイブリッドライセンスの下でオープンソース化されています。本プロジェクトを使用する際は、以下の点にご注意ください:
1. サードパーティライブラリのコードまたは修正部分は、元のオープンソースライセンスに従います.
2. 本プロジェクトは一部のプロジェクトから許可を得ており、一部の制約を受けません.
3. プロジェクトのその他のロジックコードは[本リポジトリのオープンソースライセンス](../LICENSE)に従います.
**本リポジトリは使いやすさの향上とメッセージプッシュ機能の実装のみを目的としています。また、リポジトリの主要作者の事前許可なく NapCat のコードに基づいてプロジェクトを開発することは禁止されています。使用する際は現地の法律と規制を遵守してください。これに起因する問題は、使用者および違反使用チュートリアルの提供者の責任となります。**

77
docs/README_ko.md Normal file
View File

@@ -0,0 +1,77 @@
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
<div align="center">
# NapCat
_NTQQ 기반으로 구현된 최신 프로토콜 사이드 프레임워크._
> 구름이 일고 바람이 불어, 마음은 먼 곳을 향하나 길은 아직 닿지 못했네.
[English](../README.md) | [简体中文](./README_zh-CN.md) | [繁體中文](./README_zh-TW.md) | [日本語](./README_ja.md) | 한국어 | [Русский](./README_ru.md) | [العربية](./README_ar.md)
</div>
---
## 공지
NapCat은 현재 새로운 주요 메인테이너를 찾고 있습니다. nanaeonn@outlook.com 으로 이메일을 보내주세요. 이 기간 동안 공개 커뮤니티 그룹은 만들어지지 않지만, NapCat은 정기적인 업데이트를 보장합니다.
## 환영합니다
- NapCatQQ는 NTQQ를 기반으로 한 최신 Bot 프로토콜 구현체입니다
## 특징
- **사용하기 쉬움**
- 초보자도 쉽게 사용할 수 있습니다.
- **빠르고 효율적**
- 저메모리 시스템에서도 장시간 안정적으로 실행됩니다.
- **풍부한 API 인터페이스**
- 대부분의 표준 인터페이스를 완전히 구현했습니다.
- **안정적이고 신뢰할 수 있음**
- 지속적이고 안정적인 개발과 유지보수를 하고 있습니다.
## 빠른 시작
[Release](https://github.com/NapNeko/NapCatQQ/releases/) 페이지에서 최신 버전을 다운로드할 수 있습니다
**처음 사용하시는 분**은 아래 문서에서 사용 가이드를 반드시 확인해 주세요
> 이 프로젝트는 비영리입니다. 연동 문제/기본 문제/하위 프레임워크 문제는 직접 검색하여 해결해 주세요. 본 프로젝트 커뮤니티에서는 이러한 지원을 제공하지 않습니다.
## 링크
| 문서 | [![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/) |
|:-:|:-:|:-:|:-:|
| 문서 | [![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/) |
|:-:|:-:|:-:|:-:|
| DeepWiki | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/NapNeko/NapCatQQ) |
|:-:|:-:|
> 다른 커뮤니티에서 이 프로젝트(다른 프로토콜 측/관련 애플리케이션 측 프로젝트 포함)를 언급하여 논쟁을 일으키지 마세요. 제안이 있으시면 공식 그룹에서 토론하거나 PR을 제출해 주세요.
## 감사의 말
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) — 본 프로젝트에 대한 큰 지원, 일부 코드 참고 (허가 완료)
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) — 본 프로젝트와 완벽하게 호환되는 LLM Bot 프레임워크
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) — 사이버 친구 麦麦 Bot 프레임워크
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) — NapCat 기반의 메시지 내보내기 도구
- 그리고 가장 중요한 것은, 지금 이 글을 읽고 있는 당신에게 감사드립니다~
---
## 라이선스
이 프로젝트는 하이브리드 라이선스로 오픈소스화되어 있습니다. 이 프로젝트를 사용할 때 다음 사항에 유의해 주세요:
1. 서드파티 라이브러리 코드 또는 수정된 부분은 원래의 오픈소스 라이선스를 따릅니다.
2. 이 프로젝트는 일부 프로젝트로부터 허가를 받아 일부 제한을 받지 않습니다.
3. 프로젝트의 나머지 로직 코드는 [본 리포지토리 라이선스](../LICENSE)를 따릅니다.
**이 리포지토리는 사용 편의성 향상과 메시지 푸시 기능 구현만을 목적으로 합니다. 또한, 리포지토리 주요 작성자의 사전 승인 없이 NapCat 코드를 기반으로 프로젝트를 개발하는 것은 금지되어 있습니다. 사용 시 현지 법률 및 규정을 준수해 주세요. 이로 인해 발생하는 문제는 사용자 및 위반 사용 튜토리얼 제공자의 책임입니다.**

77
docs/README_ru.md Normal file
View File

@@ -0,0 +1,77 @@
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
<div align="center">
# NapCat
_Современный фреймворк на стороне протокола, реализованный на базе NTQQ._
> Облака поднимаются, ветер рождается, сердце стремится вдаль, но путь ещё не пройден.
[English](../README.md) | [简体中文](./README_zh-CN.md) | [繁體中文](./README_zh-TW.md) | [日本語](./README_ja.md) | [한국어](./README_ko.md) | Русский | [العربية](./README_ar.md)
</div>
---
## Объявление
NapCat в настоящее время ищет нового основного мейнтейнера. Пожалуйста, свяжитесь с нами по электронной почте nanaeonn@outlook.com. В этот период публичные группы сообщества создаваться не будут, но NapCat гарантирует регулярные обновления.
## Добро пожаловать
- NapCatQQ — это современная реализация протокола Bot на базе NTQQ
## Особенности
- **Простота использования**
- Начинающие пользователи смогут легко освоить работу.
- **Скорость и эффективность**
- Длительная работа на системах с ограниченным объёмом памяти.
- **Богатый API-интерфейс**
- Полная реализация большинства стандартных интерфейсов.
- **Стабильность и надёжность**
- Постоянная стабильная разработка и поддержка.
## Быстрый старт
Скачайте последнюю версию на странице [Release](https://github.com/NapNeko/NapCatQQ/releases/)
**При первом использовании** обязательно ознакомьтесь с руководством в документации ниже
> Проект является некоммерческим. Вопросы интеграции, базовые вопросы и вопросы по нижележащему фреймворку решайте самостоятельно — сообщество данного проекта не предоставляет такой поддержки.
## Ссылки
| Документация | [![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/) |
|:-:|:-:|:-:|:-:|
| Документация | [![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/) |
|:-:|:-:|:-:|:-:|
| DeepWiki | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/NapNeko/NapCatQQ) |
|:-:|:-:|
> Пожалуйста, не упоминайте данный проект (включая другие протокольные/прикладные проекты) в других сообществах во избежание споров. Если у вас есть предложения, обсуждайте их в официальной группе или отправляйте PR.
## Благодарности
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) — большая поддержка данного проекта, частично использован код (с разрешения)
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) — фреймворк LLM Bot, идеально совместимый с данным проектом
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) — кибер-друг 麦麦, Bot-фреймворк
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) — инструмент экспорта сообщений на базе NapCat
- И самое главное — спасибо вам, читающему это~
---
## Лицензия
Данный проект распространяется с открытым исходным кодом под гибридной лицензией. При использовании данного проекта обратите внимание на следующее:
1. Код сторонних библиотек или модифицированные части подчиняются их исходным лицензиям с открытым исходным кодом.
2. Данный проект получил разрешение от ряда проектов и не подчиняется некоторым ограничениям.
3. Остальной логический код проекта распространяется по [лицензии данного репозитория](../LICENSE).
**Данный репозиторий предназначен исключительно для повышения удобства использования и реализации функций отправки сообщений. Кроме того, запрещается разработка проектов на основе кода NapCat без предварительного разрешения главного автора репозитория. При использовании соблюдайте местное законодательство. За любые проблемы, вызванные нарушением, несут ответственность пользователь и лицо, предоставившее инструкции по незаконному использованию.**

77
docs/README_zh-CN.md Normal file
View File

@@ -0,0 +1,77 @@
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
<div align="center">
# NapCat
_基于 NTQQ 实现的现代化协议端框架._
> 云起兮风生,心向远方兮路未曾至.
[English](../README.md) | 简体中文 | [繁體中文](./README_zh-TW.md) | [日本語](./README_ja.md) | [한국어](./README_ko.md) | [Русский](./README_ru.md) | [العربية](./README_ar.md)
</div>
---
## 公告
NapCat 当前正在寻找新的主要维护者欢迎email到 nanaeonn@outlook.com 在此期不会建立任何公开社区交流群Napcat会保证此期间的正常更新。
## 欢迎
- NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 特性
- **易于使用**
- 作为初学者能够轻松使用.
- **快速高效**
- 在低内存操作系统长时运行.
- **丰富的 API 接口**
- 完整实现了大部分标准接口.
- **稳定可靠**
- 持续稳定的开发与维护.
## 快速开始
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
**首次使用**请务必查看如下文档看使用教程
> 项目非盈利,涉及 对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
## 链接
| 文档 | [![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/) |
|:-:|:-:|:-:|:-:|
| 文档 | [![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/) |
|:-:|:-:|:-:|:-:|
| DeepWiki | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/NapNeko/NapCatQQ) |
|:-:|:-:|
> 请不要在其余社区提及本项目(包括其余协议端/相关应用端项目)引发争论如有建议到达官方交流群讨论或PR。
## 致谢
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) 基于NapCat的消息导出工具 在此推荐一下
- 不过最最重要的 还是需要感谢屏幕前的你哦~
---
## 许可证
本项目采用 混合协议 开源,因此使用本项目时,你需要注意以下几点:
1. 第三方库代码或修改部分遵循其原始开源许可.
2. 本项目获取部分项目授权而不受部分约束
2. 项目其余逻辑代码采用[本仓库开源许可](../LICENSE).
**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**

77
docs/README_zh-TW.md Normal file
View File

@@ -0,0 +1,77 @@
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
<div align="center">
# NapCat
_基於 NTQQ 實現的現代化協議端框架._
> 雲起兮風生,心向遠方兮路未曾至.
[English](../README.md) | [简体中文](./README_zh-CN.md) | 繁體中文 | [日本語](./README_ja.md) | [한국어](./README_ko.md) | [Русский](./README_ru.md) | [العربية](./README_ar.md)
</div>
---
## 公告
NapCat 目前正在尋找新的主要維護者,歡迎透過 email 聯繫 nanaeonn@outlook.com。在此期間不會建立任何公開社群交流群NapCat 會保證此期間的正常更新。
## 歡迎
- NapCatQQ 是現代化的基於 NTQQ 的 Bot 協議端實現
## 特性
- **易於使用**
- 初學者也能輕鬆上手.
- **快速高效**
- 在低記憶體系統長時間穩定運行.
- **豐富的 API 介面**
- 完整實現了大部分標準介面.
- **穩定可靠**
- 持續穩定的開發與維護.
## 快速開始
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 頁面下載最新版本
**首次使用**請務必查看以下文件中的使用教學
> 專案為非營利性質,涉及 對接問題/基礎問題/底層框架問題 請自行搜尋解決,本專案社群不提供此類解答。
## 連結
| 文件 | [![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/) |
|:-:|:-:|:-:|:-:|
| 文件 | [![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/) |
|:-:|:-:|:-:|:-:|
| DeepWiki | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/NapNeko/NapCatQQ) |
|:-:|:-:|
> 請不要在其他社群提及本專案(包括其他協議端/相關應用端專案)以免引發爭論。如有建議,請到官方交流群討論或提交 PR。
## 致謝
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 對本專案的大力支持,參考部分程式碼(已獲授權)
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美適配本專案的 LLM Bot 框架,在此推薦
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一隻賽博群友 麥麥 Bot 框架,在此推薦
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) 基於 NapCat 的訊息匯出工具,在此推薦
- 不過最最重要的,還是要感謝螢幕前的你哦~
---
## 授權條款
本專案採用混合協議開源,因此使用本專案時,您需要注意以下幾點:
1. 第三方函式庫程式碼或修改部分遵循其原始開源授權.
2. 本專案已獲取部分專案授權而不受部分約束.
3. 專案其餘邏輯程式碼採用[本倉庫開源授權](../LICENSE).
**本倉庫僅用於提高易用性,實現訊息推送類功能,此外,禁止任何專案未經倉庫主作者授權基於 NapCat 程式碼開發。使用請遵守當地法律法規,由此造成的問題由使用者和提供違規使用教學者負責。**

View File

@@ -16,7 +16,7 @@
}
},
"dependencies": {
"ajv": "^8.13.0",
"ajv": "^8.18.0",
"file-type": "^21.0.0"
},
"devDependencies": {

View File

@@ -68,7 +68,7 @@ export class NTQQPacketApi {
this.pkt = new PacketClientSession(this.core);
await this.pkt.init(process.pid, table.recv, table.send);
try {
await this.pkt.operation.FetchRkey(1500);
await this.pkt.operation.FetchRkey(3000);
} catch (error) {
this.logger.logError('测试Packet状态异常', error);
return false;

View File

@@ -21,7 +21,7 @@
"inversify": "^7.10.4",
"reflect-metadata": "^0.2.2",
"@protobuf-ts/runtime": "^2.11.1",
"ajv": "^8.13.0",
"ajv": "^8.18.0",
"@sinclair/typebox": "^0.34.38",
"file-type": "^21.0.0",
"compressing": "^1.10.1",

View File

@@ -29,10 +29,11 @@ export interface PasswordLoginArgType {
uin: string;
passwordMd5: string;// passwMD5
step: number;// 猜测是需要二次认证 参数 一次为0
newDeviceLoginSig: string;
proofWaterSig: string;
proofWaterRand: string;
proofWaterSid: string;
newDeviceLoginSig: Uint8Array;
proofWaterSig: Uint8Array;
proofWaterRand: Uint8Array;
proofWaterSid: Uint8Array;
unusualDeviceCheckSig: Uint8Array;
}
export interface LoginListItem {

View File

@@ -13,14 +13,12 @@ import { URL } from 'url';
import { ActionName } from '@/napcat-onebot/action/router';
import { OB11HeartbeatEvent } from '@/napcat-onebot/event/meta/OB11HeartbeatEvent';
import { OB11LifeCycleEvent, LifeCycleSubType } from '@/napcat-onebot/event/meta/OB11LifeCycleEvent';
import { Mutex } from 'async-mutex';
export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig> {
private app: Express | undefined;
private server: http.Server | undefined;
private wsServer?: WebSocketServer;
private wsClients: WebSocket[] = [];
private wsClientsMutex = new Mutex();
private heartbeatIntervalId: NodeJS.Timeout | null = null;
private wsClientWithEvent: WebSocket[] = [];
@@ -30,19 +28,17 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
override async onEvent<T extends OB11EmitEventContent> (event: T) {
// http server is passive, no need to emit event
this.wsClientsMutex.runExclusive(async () => {
const promises = this.wsClientWithEvent.map((wsClient) => {
return new Promise<void>((resolve, reject) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(event));
resolve();
} else {
reject(new Error('WebSocket is not open'));
}
});
const promises = this.wsClientWithEvent.map((wsClient) => {
return new Promise<void>((resolve, reject) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(event));
resolve();
} else {
reject(new Error('WebSocket is not open'));
}
});
await Promise.allSettled(promises);
});
await Promise.allSettled(promises);
}
open () {
@@ -65,13 +61,9 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
this.server?.close();
this.app = undefined;
this.stopHeartbeat();
await this.wsClientsMutex.runExclusive(async () => {
this.wsClients.forEach((wsClient) => {
wsClient.close();
});
this.wsClients = [];
this.wsClientWithEvent = [];
});
this.wsClients.forEach((wsClient) => wsClient.close());
this.wsClients = [];
this.wsClientWithEvent = [];
this.wsServer?.close();
}
@@ -153,36 +145,29 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
wsClient.on('message', (message) => {
this.handleWSMessage(wsClient, message).then().catch(e => this.logger.logError(e));
});
wsClient.on('ping', () => {
wsClient.pong();
});
wsClient.on('pong', () => {
// this.logger.logDebug('[OneBot] [HTTP WebSocket] Pong received');
});
wsClient.once('close', () => {
this.wsClientsMutex.runExclusive(async () => {
const NormolIndex = this.wsClients.indexOf(wsClient);
if (NormolIndex !== -1) {
this.wsClients.splice(NormolIndex, 1);
}
const EventIndex = this.wsClientWithEvent.indexOf(wsClient);
if (EventIndex !== -1) {
this.wsClientWithEvent.splice(EventIndex, 1);
}
if (this.wsClientWithEvent.length === 0) {
this.stopHeartbeat();
}
});
});
await this.wsClientsMutex.runExclusive(async () => {
if (!isApiConnect) {
this.wsClientWithEvent.push(wsClient);
const NormolIndex = this.wsClients.indexOf(wsClient);
if (NormolIndex !== -1) {
this.wsClients.splice(NormolIndex, 1);
}
this.wsClients.push(wsClient);
if (this.wsClientWithEvent.length > 0) {
this.startHeartbeat();
const EventIndex = this.wsClientWithEvent.indexOf(wsClient);
if (EventIndex !== -1) {
this.wsClientWithEvent.splice(EventIndex, 1);
}
if (this.wsClientWithEvent.length === 0) {
this.stopHeartbeat();
}
});
if (!isApiConnect) {
this.wsClientWithEvent.push(wsClient);
}
this.wsClients.push(wsClient);
if (this.wsClientWithEvent.length > 0) {
this.startHeartbeat();
}
}).on('error', (err) => this.logger.log('[OneBot] [HTTP WebSocket] Server Error:', err.message));
}
@@ -197,12 +182,10 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
private startHeartbeat () {
if (this.heartbeatIntervalId) return;
this.heartbeatIntervalId = setInterval(() => {
this.wsClientsMutex.runExclusive(async () => {
this.wsClientWithEvent.forEach((wsClient) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, 30000, this.core.selfInfo.online ?? true, true)));
}
});
this.wsClientWithEvent.forEach((wsClient) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, 30000, this.core.selfInfo.online ?? true, true)));
}
});
}, 30000);
}

View File

@@ -85,9 +85,6 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
},
});
this.connection.on('ping', () => {
this.connection?.pong();
});
this.connection.on('pong', () => {
// this.logger.logDebug('[OneBot] [WebSocket Client] 收到pong');
});

View File

@@ -1,7 +1,6 @@
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
import { URL } from 'url';
import { RawData, WebSocket, WebSocketServer } from 'ws';
import { Mutex } from 'async-mutex';
import { OB11Response } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { NapCatCore } from 'napcat-core';
@@ -17,7 +16,6 @@ import json5 from 'json5';
export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> {
wsServer?: WebSocketServer;
wsClients: WebSocket[] = [];
wsClientsMutex = new Mutex();
private heartbeatIntervalId: NodeJS.Timeout | null = null;
wsClientWithEvent: WebSocket[] = [];
@@ -58,36 +56,29 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
wsClient.on('message', (message) => {
this.handleMessage(wsClient, message).then().catch(e => this.logger.logError(e));
});
wsClient.on('ping', () => {
wsClient.pong();
});
wsClient.on('pong', () => {
// this.logger.logDebug('[OneBot] [WebSocket Server] Pong received');
});
wsClient.once('close', () => {
this.wsClientsMutex.runExclusive(async () => {
const NormolIndex = this.wsClients.indexOf(wsClient);
if (NormolIndex !== -1) {
this.wsClients.splice(NormolIndex, 1);
}
const EventIndex = this.wsClientWithEvent.indexOf(wsClient);
if (EventIndex !== -1) {
this.wsClientWithEvent.splice(EventIndex, 1);
}
if (this.wsClientWithEvent.length === 0) {
this.stopHeartbeat();
}
});
});
await this.wsClientsMutex.runExclusive(async () => {
if (!isApiConnect) {
this.wsClientWithEvent.push(wsClient);
const NormolIndex = this.wsClients.indexOf(wsClient);
if (NormolIndex !== -1) {
this.wsClients.splice(NormolIndex, 1);
}
this.wsClients.push(wsClient);
if (this.wsClientWithEvent.length > 0) {
this.startHeartbeat();
const EventIndex = this.wsClientWithEvent.indexOf(wsClient);
if (EventIndex !== -1) {
this.wsClientWithEvent.splice(EventIndex, 1);
}
if (this.wsClientWithEvent.length === 0) {
this.stopHeartbeat();
}
});
if (!isApiConnect) {
this.wsClientWithEvent.push(wsClient);
}
this.wsClients.push(wsClient);
if (this.wsClientWithEvent.length > 0) {
this.startHeartbeat();
}
}).on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Server Error:', err.message));
}
@@ -100,19 +91,17 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
}
async onEvent<T extends OB11EmitEventContent> (event: T) {
this.wsClientsMutex.runExclusive(async () => {
const promises = this.wsClientWithEvent.map((wsClient) => {
return new Promise<void>((resolve, reject) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(event));
resolve();
} else {
reject(new Error('WebSocket is not open'));
}
});
const promises = this.wsClientWithEvent.map((wsClient) => {
return new Promise<void>((resolve, reject) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(event));
resolve();
} else {
reject(new Error('WebSocket is not open'));
}
});
await Promise.allSettled(promises);
});
await Promise.allSettled(promises);
}
open () {
@@ -136,24 +125,18 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
}
});
this.stopHeartbeat();
await this.wsClientsMutex.runExclusive(async () => {
this.wsClients.forEach((wsClient) => {
wsClient.close();
});
this.wsClients = [];
this.wsClientWithEvent = [];
});
this.wsClients.forEach((wsClient) => wsClient.close());
this.wsClients = [];
this.wsClientWithEvent = [];
}
private startHeartbeat () {
if (this.heartbeatIntervalId || this.config.heartInterval <= 0) return;
this.heartbeatIntervalId = setInterval(() => {
this.wsClientsMutex.runExclusive(async () => {
this.wsClientWithEvent.forEach((wsClient) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.config.heartInterval, this.core.selfInfo.online ?? true, true)));
}
});
this.wsClientWithEvent.forEach((wsClient) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.config.heartInterval, this.core.selfInfo.online ?? true, true)));
}
});
}, this.config.heartInterval);
}

View File

@@ -20,13 +20,12 @@
"fast-xml-parser": "^4.3.6",
"type-is": "^1.6.7",
"on-finished": "^2.3.5",
"ajv": "^8.13.0",
"ajv": "^8.18.0",
"@sinclair/typebox": "^0.34.38",
"cors": "^2.8.5",
"express": "^5.0.0",
"ws": "^8.18.3",
"file-type": "^21.0.0",
"async-mutex": "^0.5.0",
"napcat-protobuf": "workspace:*",
"json5": "^2.2.3",
"napcat-core": "workspace:*",

View File

@@ -16,7 +16,7 @@
}
},
"dependencies": {
"ajv": "^8.13.0",
"ajv": "^8.18.0",
"@sinclair/typebox": "^0.34.38",
"cors": "^2.8.5",
"express": "^5.0.0",

View File

@@ -20,6 +20,7 @@ import { hostname, systemVersion } from 'napcat-common/src/system';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { createHash } from 'node:crypto';
import { LoginListItem, NodeIKernelLoginService } from 'napcat-core/services';
import qrcode from 'napcat-qrcode/lib/main';
import { NapCatAdapterManager } from 'napcat-adapter';
@@ -194,6 +195,24 @@ async function handleLogin (
return await selfInfo;
}
async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWrapper, loginService: NodeIKernelLoginService, quickLoginUin: string | undefined, historyLoginList: LoginListItem[]) {
const resolveQuickPasswordMd5 = (): string | undefined => {
const quickPasswordMd5 = process.env['NAPCAT_QUICK_PASSWORD_MD5']?.trim();
if (quickPasswordMd5) {
if (/^[a-fA-F0-9]{32}$/.test(quickPasswordMd5)) {
return quickPasswordMd5.toLowerCase();
}
logger.logError('NAPCAT_QUICK_PASSWORD_MD5 格式无效(需为 32 位 MD5');
}
const quickPassword = process.env['NAPCAT_QUICK_PASSWORD'];
if (typeof quickPassword === 'string' && quickPassword.length > 0) {
logger.log('检测到 NAPCAT_QUICK_PASSWORD已在内存中计算 MD5 用于回退登录');
return createHash('md5').update(quickPassword, 'utf8').digest('hex');
}
return undefined;
};
// 注册刷新二维码回调
WebUiDataRuntime.setRefreshQRCodeCallback(async () => {
loginService.getQRCodePicture();
@@ -204,10 +223,12 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr
if (uin) {
logger.log('正在快速登录 ', uin);
loginService.quickLoginWithUin(uin).then(res => {
if (res.loginErrorInfo.errMsg) {
WebUiDataRuntime.setQQLoginError(res.loginErrorInfo.errMsg);
const quickLoginSuccess = res.result === '0' && !res.loginErrorInfo?.errMsg;
if (!quickLoginSuccess) {
const errMsg = res.loginErrorInfo?.errMsg || `快速登录失败,错误码: ${res.result}`;
WebUiDataRuntime.setQQLoginError(errMsg);
loginService.getQRCodePicture();
resolve({ result: false, message: res.loginErrorInfo.errMsg });
resolve({ result: false, message: errMsg });
} else {
WebUiDataRuntime.setQQLoginStatus(true);
WebUiDataRuntime.setQQLoginError('');
@@ -234,21 +255,43 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr
uin,
passwordMd5,
step: 0,
newDeviceLoginSig: '',
proofWaterSig: '',
proofWaterRand: '',
proofWaterSid: '',
newDeviceLoginSig: new Uint8Array(),
proofWaterSig: new Uint8Array(),
proofWaterRand: new Uint8Array(),
proofWaterSid: new Uint8Array(),
unusualDeviceCheckSig: new Uint8Array(),
}).then(res => {
if (res.result === '140022008') {
const errMsg = '需要验证码,暂不支持';
WebUiDataRuntime.setQQLoginError(errMsg);
loginService.getQRCodePicture();
resolve({ result: false, message: errMsg });
const proofWaterUrl = res.loginErrorInfo?.proofWaterUrl || '';
logger.log('需要验证码, proofWaterUrl: ', proofWaterUrl);
resolve({
result: false,
message: '需要验证码',
needCaptcha: true,
proofWaterUrl,
});
} else if (res.result === '140022010') {
const errMsg = '新设备需要扫码登录,暂不支持';
WebUiDataRuntime.setQQLoginError(errMsg);
loginService.getQRCodePicture();
resolve({ result: false, message: errMsg });
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
logger.log('新设备需要扫码验证, jumpUrl: ', jumpUrl);
resolve({
result: false,
message: '新设备需要扫码验证',
needNewDevice: true,
jumpUrl,
newDevicePullQrCodeSig,
});
} else if (res.result === '140022011') {
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
logger.log('异常设备需要验证, jumpUrl: ', jumpUrl);
resolve({
result: false,
message: '异常设备需要验证',
needNewDevice: true,
jumpUrl,
newDevicePullQrCodeSig,
});
} else if (res.result !== '0') {
const errMsg = res.loginErrorInfo?.errMsg || '密码登录失败';
WebUiDataRuntime.setQQLoginError(errMsg);
@@ -270,21 +313,170 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr
}
});
});
const tryPasswordFallbackLogin = async (uin: string): Promise<{ success: boolean, attempted: boolean; }> => {
const quickPasswordMd5 = resolveQuickPasswordMd5();
if (!quickPasswordMd5) {
logger.log(`QQ ${uin} 未配置回退密码环境变量,建议优先使用 ACCOUNT + NAPCAT_QUICK_PASSWORDNAPCAT_QUICK_PASSWORD_MD5 作为备用),将使用二维码登录方式`);
return { success: false, attempted: false };
}
logger.log('正在尝试密码回退登录 ', uin);
const fallbackResult = await WebUiDataRuntime.requestPasswordLogin(uin, quickPasswordMd5);
if (fallbackResult.result) {
logger.log('密码回退登录成功 ', uin);
return { success: true, attempted: true };
}
if (fallbackResult.needCaptcha) {
const captchaTip = fallbackResult.proofWaterUrl
? `密码回退需要验证码,请在 WebUi 中继续完成验证:${fallbackResult.proofWaterUrl}`
: '密码回退需要验证码,请在 WebUi 中继续完成验证';
logger.logWarn(captchaTip);
WebUiDataRuntime.setQQLoginError('密码回退需要验证码,请在 WebUi 中继续完成验证');
return { success: false, attempted: true };
}
if (fallbackResult.needNewDevice) {
const newDeviceTip = fallbackResult.jumpUrl
? `密码回退需要新设备验证,请在 WebUi 中继续完成验证:${fallbackResult.jumpUrl}`
: '密码回退需要新设备验证,请在 WebUi 中继续完成验证';
logger.logWarn(newDeviceTip);
WebUiDataRuntime.setQQLoginError('密码回退需要新设备验证,请在 WebUi 中继续完成验证');
return { success: false, attempted: true };
}
logger.logError('密码回退登录失败:', fallbackResult.message);
return { success: false, attempted: true };
};
// 注册验证码登录回调(密码登录需要验证码时的第二步)
WebUiDataRuntime.setCaptchaLoginCall(async (uin: string, passwordMd5: string, ticket: string, randstr: string, sid: string) => {
return await new Promise((resolve) => {
if (uin && passwordMd5 && ticket) {
logger.log('正在验证码登录 ', uin);
loginService.passwordLogin({
uin,
passwordMd5,
step: 1,
newDeviceLoginSig: new Uint8Array(),
proofWaterSig: new TextEncoder().encode(ticket),
proofWaterRand: new TextEncoder().encode(randstr),
proofWaterSid: new TextEncoder().encode(sid),
unusualDeviceCheckSig: new Uint8Array(),
}).then(res => {
console.log('验证码登录结果: ', res);
if (res.result === '140022010') {
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
logger.log('验证码登录后需要新设备验证, jumpUrl: ', jumpUrl);
resolve({
result: false,
message: '新设备需要扫码验证',
needNewDevice: true,
jumpUrl,
newDevicePullQrCodeSig,
});
} else if (res.result === '140022011') {
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
logger.log('验证码登录后需要异常设备验证, jumpUrl: ', jumpUrl);
resolve({
result: false,
message: '异常设备需要验证',
needNewDevice: true,
jumpUrl,
newDevicePullQrCodeSig,
});
} else if (res.result !== '0') {
const errMsg = res.loginErrorInfo?.errMsg || '验证码登录失败';
WebUiDataRuntime.setQQLoginError(errMsg);
loginService.getQRCodePicture();
resolve({ result: false, message: errMsg });
} else {
WebUiDataRuntime.setQQLoginStatus(true);
WebUiDataRuntime.setQQLoginError('');
resolve({ result: true, message: '' });
}
}).catch((e) => {
logger.logError(e);
WebUiDataRuntime.setQQLoginError('验证码登录发生错误');
loginService.getQRCodePicture();
resolve({ result: false, message: '验证码登录发生错误' });
});
} else {
resolve({ result: false, message: '验证码登录失败:参数不完整' });
}
});
});
// 注册新设备登录回调(密码登录需要新设备验证时的第二步)
WebUiDataRuntime.setNewDeviceLoginCall(async (uin: string, passwordMd5: string, newDevicePullQrCodeSig: string) => {
return await new Promise((resolve) => {
if (uin && passwordMd5 && newDevicePullQrCodeSig) {
logger.log('正在新设备验证登录 ', uin);
loginService.passwordLogin({
uin,
passwordMd5,
step: 2,
newDeviceLoginSig: new TextEncoder().encode(newDevicePullQrCodeSig),
proofWaterSig: new Uint8Array(),
proofWaterRand: new Uint8Array(),
proofWaterSid: new Uint8Array(),
unusualDeviceCheckSig: new Uint8Array(),
}).then(res => {
if (res.result === '140022011') {
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
logger.log('新设备验证后需要异常设备验证, jumpUrl: ', jumpUrl);
resolve({
result: false,
message: '异常设备需要验证',
needNewDevice: true,
jumpUrl,
newDevicePullQrCodeSig,
});
} else if (res.result !== '0') {
const errMsg = res.loginErrorInfo?.errMsg || '新设备验证登录失败';
WebUiDataRuntime.setQQLoginError(errMsg);
loginService.getQRCodePicture();
resolve({ result: false, message: errMsg });
} else {
WebUiDataRuntime.setQQLoginStatus(true);
WebUiDataRuntime.setQQLoginError('');
resolve({ result: true, message: '' });
}
}).catch((e) => {
logger.logError(e);
WebUiDataRuntime.setQQLoginError('新设备验证登录发生错误');
loginService.getQRCodePicture();
resolve({ result: false, message: '新设备验证登录发生错误' });
});
} else {
resolve({ result: false, message: '新设备验证登录失败:参数不完整' });
}
});
});
if (quickLoginUin) {
if (historyLoginList.some(u => u.uin === quickLoginUin)) {
logger.log('正在快速登录 ', quickLoginUin);
loginService.quickLoginWithUin(quickLoginUin)
.then(result => {
if (result.loginErrorInfo.errMsg) {
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
WebUiDataRuntime.setQQLoginError(result.loginErrorInfo.errMsg);
if (!context.isLogined) loginService.getQRCodePicture();
.then(async result => {
const quickLoginSuccess = result.result === '0' && !result.loginErrorInfo?.errMsg;
if (!quickLoginSuccess) {
const errMsg = result.loginErrorInfo?.errMsg || `快速登录失败,错误码: ${result.result}`;
logger.logError('快速登录错误:', errMsg);
WebUiDataRuntime.setQQLoginError(errMsg);
const { success, attempted } = await tryPasswordFallbackLogin(quickLoginUin);
if (!success && !attempted && !context.isLogined) loginService.getQRCodePicture();
}
})
.catch();
.catch(async (error) => {
logger.logError('快速登录异常:', error);
WebUiDataRuntime.setQQLoginError('快速登录发生错误');
const { success, attempted } = await tryPasswordFallbackLogin(quickLoginUin);
if (!success && !attempted && !context.isLogined) loginService.getQRCodePicture();
});
} else {
logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将使用二维码登录方式');
if (!context.isLogined) loginService.getQRCodePicture();
logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将尝试密码回退登录');
const { success, attempted } = await tryPasswordFallbackLogin(quickLoginUin);
if (!success && !attempted && !context.isLogined) loginService.getQRCodePicture();
}
} else {
logger.log('没有 -q 指令指定快速登录,将使用二维码登录方式');
@@ -411,11 +603,7 @@ export async function NCoreInitShell () {
// wrapper.node 加载后立刻启用 Bypass可通过环境变量禁用
if (process.env['NAPCAT_DISABLE_BYPASS'] !== '1') {
const bypassOptions = napcatConfig.bypass ?? {};
logger.logDebug('[NapCat] Bypass 配置:', bypassOptions);
const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions);
if (bypassEnabled) {
logger.log('[NapCat] Napi2NativeLoader: 已启用Bypass');
}
napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions);
} else {
logger.log('[NapCat] Napi2NativeLoader: Bypass已通过环境变量禁用');
}

View File

@@ -61,20 +61,6 @@ declare module 'yaml' {
export const stringify: (...args: any[]) => any;
}
declare module 'async-mutex' {
export class Mutex {
acquire (): Promise<() => void>;
runExclusive<T> (callback: () => T | Promise<T>): Promise<T>;
}
export class Semaphore {
acquire (): Promise<[() => void, number]>;
runExclusive<T> (callback: () => T | Promise<T>): Promise<T>;
release (): void;
}
const _async_mutex_default: { Mutex: typeof Mutex; Semaphore: typeof Semaphore; };
export default _async_mutex_default;
}
declare module 'napcat-protobuf' {
export class NapProtoMsg<T = any> {
constructor (schema: any);

View File

@@ -35,9 +35,6 @@ const EXTERNAL_TYPE_REPLACEMENTS = {
'ValidateFunction<T>': 'any',
// inversify
'Container': 'any',
// async-mutex
'Mutex': 'any',
'Semaphore': 'any',
// napcat-protobuf
'NapProtoDecodeStructType': 'any',
'NapProtoEncodeStructType': 'any',
@@ -90,15 +87,15 @@ function replaceExternalTypes (content) {
// 使用类型上下文的模式匹配
const typeContextPatterns = [
// : Type
/:\s*(WebSocket|WebSocketServer|RawData|Ajv|AnySchema|ValidateFunction|Container|Mutex|Semaphore|NapProtoDecodeStructType|NapProtoEncodeStructType|Express|Request|Response|NextFunction)(?=\s*[;,)\]\}|&]|$)/g,
/:\s*(WebSocket|WebSocketServer|RawData|Ajv|AnySchema|ValidateFunction|Container|NapProtoDecodeStructType|NapProtoEncodeStructType|Express|Request|Response|NextFunction)(?=\s*[;,)\]\}|&]|$)/g,
// <Type>
/<(WebSocket|WebSocketServer|RawData|Ajv|AnySchema|ValidateFunction|Container|Mutex|Semaphore|NapProtoDecodeStructType|NapProtoEncodeStructType|Express|Request|Response|NextFunction)>/g,
/<(WebSocket|WebSocketServer|RawData|Ajv|AnySchema|ValidateFunction|Container|NapProtoDecodeStructType|NapProtoEncodeStructType|Express|Request|Response|NextFunction)>/g,
// Type[]
/(WebSocket|WebSocketServer|RawData|Ajv|AnySchema|ValidateFunction|Container|Mutex|Semaphore|NapProtoDecodeStructType|NapProtoEncodeStructType|Express|Request|Response|NextFunction)\[\]/g,
/(WebSocket|WebSocketServer|RawData|Ajv|AnySchema|ValidateFunction|Container|NapProtoDecodeStructType|NapProtoEncodeStructType|Express|Request|Response|NextFunction)\[\]/g,
// extends Type
/extends\s+(WebSocket|WebSocketServer|RawData|Ajv|AnySchema|ValidateFunction|Container|Mutex|Semaphore|NapProtoDecodeStructType|NapProtoEncodeStructType|Express|Request|Response|NextFunction)(?=\s*[{,])/g,
/extends\s+(WebSocket|WebSocketServer|RawData|Ajv|AnySchema|ValidateFunction|Container|NapProtoDecodeStructType|NapProtoEncodeStructType|Express|Request|Response|NextFunction)(?=\s*[{,])/g,
// implements Type
/implements\s+(WebSocket|WebSocketServer|RawData|Ajv|AnySchema|ValidateFunction|Container|Mutex|Semaphore|NapProtoDecodeStructType|NapProtoEncodeStructType|Express|Request|Response|NextFunction)(?=\s*[{,])/g,
/implements\s+(WebSocket|WebSocketServer|RawData|Ajv|AnySchema|ValidateFunction|Container|NapProtoDecodeStructType|NapProtoEncodeStructType|Express|Request|Response|NextFunction)(?=\s*[{,])/g,
];
for (const pattern of typeContextPatterns) {

View File

@@ -5,7 +5,7 @@
import express from 'express';
import type { WebUiConfigType } from './src/types';
import { createServer } from 'http';
import { randomUUID } from 'node:crypto';
import { createHash, randomUUID } from 'node:crypto';
import { createServer as createHttpsServer } from 'https';
import { NapCatPathWrapper } from 'napcat-common/src/path';
import { WebUiConfigWrapper } from '@/napcat-webui-backend/src/helper/config';
@@ -156,16 +156,60 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra
WebUiDataRuntime.setWebUiConfigQuickFunction(
async () => {
const autoLoginAccount = process.env['NAPCAT_QUICK_ACCOUNT'] || WebUiConfig.getAutoLoginAccount();
if (autoLoginAccount) {
try {
const { result, message } = await WebUiDataRuntime.requestQuickLogin(autoLoginAccount);
if (!result) {
throw new Error(message);
const resolveQuickPasswordMd5 = (): string | undefined => {
const quickPasswordMd5FromEnv = process.env['NAPCAT_QUICK_PASSWORD_MD5']?.trim();
if (quickPasswordMd5FromEnv) {
if (/^[a-fA-F0-9]{32}$/.test(quickPasswordMd5FromEnv)) {
return quickPasswordMd5FromEnv.toLowerCase();
}
console.log(`[NapCat] [WebUi] Auto login account: ${autoLoginAccount}`);
} catch (error) {
console.log('[NapCat] [WebUi] Auto login account failed.' + error);
console.log('[NapCat] [WebUi] NAPCAT_QUICK_PASSWORD_MD5 格式无效(需为 32 位 MD5');
}
const quickPassword = process.env['NAPCAT_QUICK_PASSWORD'];
if (typeof quickPassword === 'string' && quickPassword.length > 0) {
console.log('[NapCat] [WebUi] 检测到 NAPCAT_QUICK_PASSWORD已在内存中计算 MD5 用于回退登录');
return createHash('md5').update(quickPassword, 'utf8').digest('hex');
}
return undefined;
};
if (!autoLoginAccount) {
return;
}
const quickPasswordMd5 = resolveQuickPasswordMd5();
try {
const { result, message } = await WebUiDataRuntime.requestQuickLogin(autoLoginAccount);
if (result) {
console.log(`[NapCat] [WebUi] 自动快速登录成功: ${autoLoginAccount}`);
return;
}
console.log(`[NapCat] [WebUi] 自动快速登录失败: ${message || '未知错误'}`);
} catch (error) {
console.log('[NapCat] [WebUi] 自动快速登录异常:' + error);
}
if (!quickPasswordMd5) {
console.log(`[NapCat] [WebUi] QQ ${autoLoginAccount} 未配置回退密码环境变量,建议优先使用 ACCOUNT + NAPCAT_QUICK_PASSWORDNAPCAT_QUICK_PASSWORD_MD5 作为备用),保持二维码登录兜底`);
return;
}
try {
const { result, message, needCaptcha, needNewDevice } = await WebUiDataRuntime.requestPasswordLogin(autoLoginAccount, quickPasswordMd5);
if (result) {
console.log(`[NapCat] [WebUi] 自动密码回退登录成功: ${autoLoginAccount}`);
return;
}
if (needCaptcha) {
console.log(`[NapCat] [WebUi] 自动密码回退登录需要验证码,请在登录页面继续完成: ${autoLoginAccount}`);
return;
}
if (needNewDevice) {
console.log(`[NapCat] [WebUi] 自动密码回退登录需要新设备验证,请在登录页面继续完成: ${autoLoginAccount}`);
return;
}
console.log(`[NapCat] [WebUi] 自动密码回退登录失败: ${message || '未知错误'}`);
} catch (error) {
console.log('[NapCat] [WebUi] 自动密码回退登录异常:' + error);
}
});
// ------------注册中间件------------

View File

@@ -18,7 +18,7 @@
"dependencies": {
"@simplewebauthn/server": "^13.2.2",
"@sinclair/typebox": "^0.34.38",
"ajv": "^8.13.0",
"ajv": "^8.18.0",
"compressing": "^1.10.3",
"compression": "^1.8.1",
"express": "^5.0.0",

View File

@@ -1,4 +1,5 @@
import { RequestHandler } from 'express';
import https from 'https';
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
import { WebUiConfig } from '@/napcat-webui-backend/index';
@@ -7,6 +8,37 @@ import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/respons
import { Registry20Utils, MachineInfoUtils } from '@/napcat-webui-backend/src/utils/guid';
import os from 'node:os';
// oidb 新设备验证请求辅助函数
function oidbRequest (uid: string, body: Record<string, unknown>): Promise<Record<string, unknown>> {
return new Promise((resolve, reject) => {
const postData = JSON.stringify(body);
const req = https.request({
hostname: 'oidb.tim.qq.com',
path: `/v3/oidbinterface/oidb_0xc9e_8?uid=${encodeURIComponent(uid)}&getqrcode=1&sdkappid=39998&actype=2`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData),
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
'Accept': 'application/json, text/plain, */*',
},
}, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch {
reject(new Error('Failed to parse oidb response'));
}
});
});
req.on('error', reject);
req.write(postData);
req.end();
});
}
// 获取 Registry20 路径的辅助函数
const getRegistryPath = () => {
// 优先从 WebUiDataRuntime 获取早期设置的 dataPath
@@ -171,8 +203,62 @@ export const QQPasswordLoginHandler: RequestHandler = async (req, res) => {
}
// 执行密码登录
const { result, message } = await WebUiDataRuntime.requestPasswordLogin(uin, passwordMd5);
const { result, message, needCaptcha, proofWaterUrl, needNewDevice, jumpUrl, newDevicePullQrCodeSig } = await WebUiDataRuntime.requestPasswordLogin(uin, passwordMd5);
if (!result) {
if (needCaptcha && proofWaterUrl) {
return sendSuccess(res, { needCaptcha: true, proofWaterUrl });
}
if (needNewDevice && jumpUrl) {
return sendSuccess(res, { needNewDevice: true, jumpUrl, newDevicePullQrCodeSig });
}
return sendError(res, message);
}
return sendSuccess(res, null);
};
// 验证码登录(密码登录需要验证码时的第二步)
export const QQCaptchaLoginHandler: RequestHandler = async (req, res) => {
const { uin, passwordMd5, ticket, randstr, sid } = req.body;
const isLogin = WebUiDataRuntime.getQQLoginStatus();
if (isLogin) {
return sendError(res, 'QQ Is Logined');
}
if (isEmpty(uin) || isEmpty(passwordMd5)) {
return sendError(res, 'uin or passwordMd5 is empty');
}
if (isEmpty(ticket) || isEmpty(randstr)) {
return sendError(res, 'captcha ticket or randstr is empty');
}
const { result, message, needNewDevice, jumpUrl, newDevicePullQrCodeSig: sig } = await WebUiDataRuntime.requestCaptchaLogin(uin, passwordMd5, ticket, randstr, sid || '');
if (!result) {
if (needNewDevice && jumpUrl) {
return sendSuccess(res, { needNewDevice: true, jumpUrl, newDevicePullQrCodeSig: sig });
}
return sendError(res, message);
}
return sendSuccess(res, null);
};
// 新设备验证登录(密码登录需要新设备验证时的第二步)
export const QQNewDeviceLoginHandler: RequestHandler = async (req, res) => {
const { uin, passwordMd5, newDevicePullQrCodeSig } = req.body;
const isLogin = WebUiDataRuntime.getQQLoginStatus();
if (isLogin) {
return sendError(res, 'QQ Is Logined');
}
if (isEmpty(uin) || isEmpty(passwordMd5)) {
return sendError(res, 'uin or passwordMd5 is empty');
}
if (isEmpty(newDevicePullQrCodeSig)) {
return sendError(res, 'newDevicePullQrCodeSig is empty');
}
const { result, message, needNewDevice, jumpUrl, newDevicePullQrCodeSig: sig } = await WebUiDataRuntime.requestNewDeviceLogin(uin, passwordMd5, newDevicePullQrCodeSig);
if (!result) {
if (needNewDevice && jumpUrl) {
return sendSuccess(res, { needNewDevice: true, jumpUrl, newDevicePullQrCodeSig: sig });
}
return sendError(res, message);
}
return sendSuccess(res, null);
@@ -412,4 +498,61 @@ export const QQResetLinuxDeviceIDHandler: RequestHandler = async (_, res) => {
}
};
// ============================================================
// OIDB 新设备 QR 验证
// ============================================================
// 获取新设备验证二维码 (通过 OIDB 接口)
export const QQGetNewDeviceQRCodeHandler: RequestHandler = async (req, res) => {
const { uin, jumpUrl } = req.body;
if (!uin || !jumpUrl) {
return sendError(res, 'uin and jumpUrl are required');
}
// 从 jumpUrl 中提取参数
// jumpUrl 格式: https://accounts.qq.com/safe/verify?...&uin-token=xxx&sig=yyy
// sig -> str_dev_auth_token, uin-token -> str_uin_token
const url = new URL(jumpUrl);
const strDevAuthToken = url.searchParams.get('sig') || '';
const strUinToken = url.searchParams.get('uin-token') || '';
if (!strDevAuthToken || !strUinToken) {
return sendError(res, 'Failed to get new device QR code: unable to extract sig/uin-token from jumpUrl');
}
const body = {
str_dev_auth_token: strDevAuthToken,
uint32_flag: 1,
uint32_url_type: 0,
str_uin_token: strUinToken,
str_dev_type: 'Windows',
str_dev_name: os.hostname() || 'DESKTOP-NAPCAT',
};
const result = await oidbRequest(uin, body);
return sendSuccess(res, result);
};
// 轮询新设备验证二维码状态
export const QQPollNewDeviceQRHandler: RequestHandler = async (req, res) => {
const { uin, bytesToken } = req.body;
if (!uin || !bytesToken) {
return sendError(res, 'uin and bytesToken are required');
}
try {
const body = {
uint32_flag: 0,
bytes_token: bytesToken, // base64 编码的 token
};
const result = await oidbRequest(uin, body);
// result 应包含 uint32_guarantee_status:
// 0 = 等待扫码, 3 = 已扫码, 1 = 已确认 (包含 str_nt_succ_token)
return sendSuccess(res, result);
} catch (e) {
return sendError(res, `Failed to poll QR status: ${(e as Error).message}`);
}
};

View File

@@ -37,6 +37,12 @@ const LoginRuntime: LoginRuntimeType = {
onPasswordLoginRequested: async () => {
return { result: false, message: '密码登录功能未初始化' };
},
onCaptchaLoginRequested: async () => {
return { result: false, message: '验证码登录功能未初始化' };
},
onNewDeviceLoginRequested: async () => {
return { result: false, message: '新设备登录功能未初始化' };
},
onRestartProcessRequested: async () => {
return { result: false, message: '重启功能未初始化' };
},
@@ -148,6 +154,22 @@ export const WebUiDataRuntime = {
return LoginRuntime.NapCatHelper.onPasswordLoginRequested(uin, passwordMd5);
} as LoginRuntimeType['NapCatHelper']['onPasswordLoginRequested'],
setCaptchaLoginCall (func: LoginRuntimeType['NapCatHelper']['onCaptchaLoginRequested']): void {
LoginRuntime.NapCatHelper.onCaptchaLoginRequested = func;
},
requestCaptchaLogin: function (uin: string, passwordMd5: string, ticket: string, randstr: string, sid: string) {
return LoginRuntime.NapCatHelper.onCaptchaLoginRequested(uin, passwordMd5, ticket, randstr, sid);
} as LoginRuntimeType['NapCatHelper']['onCaptchaLoginRequested'],
setNewDeviceLoginCall (func: LoginRuntimeType['NapCatHelper']['onNewDeviceLoginRequested']): void {
LoginRuntime.NapCatHelper.onNewDeviceLoginRequested = func;
},
requestNewDeviceLogin: function (uin: string, passwordMd5: string, newDevicePullQrCodeSig: string) {
return LoginRuntime.NapCatHelper.onNewDeviceLoginRequested(uin, passwordMd5, newDevicePullQrCodeSig);
} as LoginRuntimeType['NapCatHelper']['onNewDeviceLoginRequested'],
setOnOB11ConfigChanged (func: LoginRuntimeType['NapCatHelper']['onOB11ConfigChanged']): void {
LoginRuntime.NapCatHelper.onOB11ConfigChanged = func;
},

View File

@@ -1,5 +1,5 @@
import { Router } from 'express';
import multer from 'multer';
// import multer from 'multer';
import path from 'path';
import fs from 'fs';
import os from 'os';
@@ -12,7 +12,7 @@ import {
RegisterPluginManagerHandler,
PluginConfigSSEHandler,
PluginConfigChangeHandler,
ImportLocalPluginHandler,
// ImportLocalPluginHandler,
GetPluginIconHandler
} from '@/napcat-webui-backend/src/api/Plugin';
import {
@@ -31,32 +31,32 @@ if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const storage = multer.diskStorage({
destination: (_req, _file, cb) => {
cb(null, uploadDir);
},
filename: (_req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + '-' + file.originalname);
}
});
// const storage = multer.diskStorage({
// destination: (_req, _file, cb) => {
// cb(null, uploadDir);
// },
// filename: (_req, file, cb) => {
// const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
// cb(null, uniqueSuffix + '-' + file.originalname);
// }
// });
const upload = multer({
storage,
limits: {
fileSize: 50 * 1024 * 1024, // 50MB 限制
},
fileFilter: (_req, file, cb) => {
// 只允许 .zip 文件
if (file.mimetype === 'application/zip' ||
file.mimetype === 'application/x-zip-compressed' ||
file.originalname.endsWith('.zip')) {
cb(null, true);
} else {
cb(new Error('Only .zip files are allowed'));
}
}
});
// const upload = multer({
// storage,
// limits: {
// fileSize: 50 * 1024 * 1024, // 50MB 限制
// },
// fileFilter: (_req, file, cb) => {
// // 只允许 .zip 文件
// if (file.mimetype === 'application/zip' ||
// file.mimetype === 'application/x-zip-compressed' ||
// file.originalname.endsWith('.zip')) {
// cb(null, true);
// } else {
// cb(new Error('Only .zip files are allowed'));
// }
// }
// });
const router: Router = Router();
@@ -68,7 +68,7 @@ router.post('/Config', SetPluginConfigHandler);
router.get('/Config/SSE', PluginConfigSSEHandler);
router.post('/Config/Change', PluginConfigChangeHandler);
router.post('/RegisterManager', RegisterPluginManagerHandler);
router.post('/Import', upload.single('plugin'), ImportLocalPluginHandler);
// router.post('/Import', upload.single('plugin'), ImportLocalPluginHandler); // 禁用插件上传
router.get('/Icon/:pluginId', GetPluginIconHandler);
// 插件商店相关路由

View File

@@ -11,6 +11,10 @@ import {
setAutoLoginAccountHandler,
QQRefreshQRcodeHandler,
QQPasswordLoginHandler,
QQCaptchaLoginHandler,
QQNewDeviceLoginHandler,
QQGetNewDeviceQRCodeHandler,
QQPollNewDeviceQRHandler,
QQResetDeviceIDHandler,
QQRestartNapCatHandler,
QQGetDeviceGUIDHandler,
@@ -50,6 +54,14 @@ router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
router.post('/RefreshQRcode', QQRefreshQRcodeHandler);
// router:密码登录
router.post('/PasswordLogin', QQPasswordLoginHandler);
// router:验证码登录(密码登录需要验证码时的第二步)
router.post('/CaptchaLogin', QQCaptchaLoginHandler);
// router:新设备验证登录(密码登录需要新设备验证时的第二步)
router.post('/NewDeviceLogin', QQNewDeviceLoginHandler);
// router:获取新设备验证二维码 (OIDB)
router.post('/GetNewDeviceQRCode', QQGetNewDeviceQRCodeHandler);
// router:轮询新设备验证二维码状态 (OIDB)
router.post('/PollNewDeviceQR', QQPollNewDeviceQRHandler);
// router:重置设备信息
router.post('/ResetDeviceID', QQResetDeviceIDHandler);
// router:重启NapCat

View File

@@ -57,7 +57,9 @@ export interface LoginRuntimeType {
OneBotContext: any | null; // OneBot 上下文,用于调试功能
NapCatHelper: {
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>;
onPasswordLoginRequested: (uin: string, passwordMd5: string) => Promise<{ result: boolean; message: string; }>;
onPasswordLoginRequested: (uin: string, passwordMd5: string) => Promise<{ result: boolean; message: string; needCaptcha?: boolean; proofWaterUrl?: string; needNewDevice?: boolean; jumpUrl?: string; newDevicePullQrCodeSig?: string; }>;
onCaptchaLoginRequested: (uin: string, passwordMd5: string, ticket: string, randstr: string, sid: string) => Promise<{ result: boolean; message: string; needNewDevice?: boolean; jumpUrl?: string; newDevicePullQrCodeSig?: string; }>;
onNewDeviceLoginRequested: (uin: string, passwordMd5: string, newDevicePullQrCodeSig: string) => Promise<{ result: boolean; message: string; needNewDevice?: boolean; jumpUrl?: string; newDevicePullQrCodeSig?: string; }>;
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
onRestartProcessRequested: () => Promise<{ result: boolean; message: string; }>;
QQLoginList: string[];

View File

@@ -11,7 +11,20 @@ import { useState } from 'react';
import key from '@/const/key';
import { PluginItem } from '@/controllers/plugin_manager';
import { getPluginIconUrl } from '@/utils/plugin_icon';
function getPluginIconUrl (iconPath?: string): string | undefined {
if (!iconPath) return undefined;
try {
const raw = localStorage.getItem(key.token);
if (!raw) return iconPath;
const token = JSON.parse(raw);
const url = new URL(iconPath, window.location.origin);
url.searchParams.set('webui_token', token);
return url.pathname + url.search;
} catch {
return iconPath;
}
}
export interface PluginDisplayCardProps {
data: PluginItem;

View File

@@ -0,0 +1,158 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { Button } from '@heroui/button';
import { Spinner } from '@heroui/spinner';
import { QRCodeSVG } from 'qrcode.react';
import QQManager from '@/controllers/qq_manager';
interface NewDeviceVerifyProps {
/** jumpUrl from loginErrorInfo */
jumpUrl: string;
/** QQ uin for OIDB requests */
uin: string;
/** Called when QR verification is confirmed, passes str_nt_succ_token */
onVerified: (token: string) => void;
/** Called when user cancels */
onCancel?: () => void;
}
type QRStatus = 'loading' | 'waiting' | 'scanned' | 'confirmed' | 'error';
const NewDeviceVerify: React.FC<NewDeviceVerifyProps> = ({
jumpUrl,
uin,
onVerified,
onCancel,
}) => {
const [qrUrl, setQrUrl] = useState<string>('');
const [status, setStatus] = useState<QRStatus>('loading');
const [errorMsg, setErrorMsg] = useState<string>('');
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const mountedRef = useRef(true);
const stopPolling = useCallback(() => {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
pollTimerRef.current = null;
}
}, []);
const startPolling = useCallback((token: string) => {
stopPolling();
pollTimerRef.current = setInterval(async () => {
if (!mountedRef.current) return;
try {
const result = await QQManager.pollNewDeviceQR(uin, token);
if (!mountedRef.current) return;
const s = result?.uint32_guarantee_status;
if (s === 3) {
setStatus('scanned');
} else if (s === 1) {
stopPolling();
setStatus('confirmed');
const ntToken = result?.str_nt_succ_token || '';
onVerified(ntToken);
}
// s === 0 means still waiting, do nothing
} catch {
// Ignore poll errors, keep polling
}
}, 2500);
}, [uin, onVerified, stopPolling]);
const fetchQRCode = useCallback(async () => {
setStatus('loading');
setErrorMsg('');
try {
const result = await QQManager.getNewDeviceQRCode(uin, jumpUrl);
if (!mountedRef.current) return;
if (result?.str_url) {
setQrUrl(result.str_url);
setStatus('waiting');
// bytes_token 用于轮询,如果 OIDB 未返回则用空字符串
startPolling(result.bytes_token || '');
} else {
setStatus('error');
setErrorMsg('获取二维码失败,请重试');
}
} catch (e) {
if (!mountedRef.current) return;
setStatus('error');
setErrorMsg((e as Error).message || '获取二维码失败');
}
}, [uin, jumpUrl, startPolling]);
useEffect(() => {
mountedRef.current = true;
fetchQRCode();
return () => {
mountedRef.current = false;
stopPolling();
};
}, [fetchQRCode, stopPolling]);
const statusText: Record<QRStatus, string> = {
loading: '正在获取二维码...',
waiting: '请使用手机QQ扫描二维码完成验证',
scanned: '已扫描,请在手机上确认',
confirmed: '验证成功,正在登录...',
error: errorMsg || '获取二维码失败',
};
const statusColor: Record<QRStatus, string> = {
loading: 'text-default-500',
waiting: 'text-warning',
scanned: 'text-primary',
confirmed: 'text-success',
error: 'text-danger',
};
return (
<div className='flex flex-col gap-4 items-center'>
<p className='text-warning text-sm'>
使QQ扫描下方二维码完成验证
</p>
<div className='flex flex-col items-center gap-3' style={{ minHeight: 280 }}>
{status === 'loading' ? (
<div className='flex items-center justify-center' style={{ height: 240 }}>
<Spinner size='lg' />
</div>
) : status === 'error' ? (
<div className='flex flex-col items-center justify-center gap-3' style={{ height: 240 }}>
<p className='text-danger text-sm'>{errorMsg}</p>
<Button color='primary' variant='flat' onPress={fetchQRCode}>
</Button>
</div>
) : (
<div className='p-3 bg-white rounded-lg'>
<QRCodeSVG value={qrUrl} size={220} />
</div>
)}
<p className={`text-sm ${statusColor[status]}`}>
{statusText[status]}
</p>
</div>
<div className='flex gap-3'>
{status === 'waiting' && (
<Button color='default' variant='flat' size='sm' onPress={fetchQRCode}>
</Button>
)}
<Button
variant='light'
color='danger'
size='sm'
onPress={onCancel}
>
</Button>
</div>
</div>
);
};
export default NewDeviceVerify;

View File

@@ -6,17 +6,37 @@ import { Input } from '@heroui/input';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
import { IoChevronDown } from 'react-icons/io5';
import { Spinner } from '@heroui/spinner';
import type { QQItem } from '@/components/quick_login';
import { isQQQuickNewItem } from '@/utils/qq';
import TencentCaptchaModal from '@/components/tencent_captcha';
import type { CaptchaCallbackData } from '@/components/tencent_captcha';
import NewDeviceVerify from '@/components/new_device_verify';
interface PasswordLoginProps {
onSubmit: (uin: string, password: string) => void;
onCaptchaSubmit?: (uin: string, password: string, captchaData: CaptchaCallbackData) => void;
onNewDeviceVerified?: (token: string) => void;
isLoading: boolean;
qqList: (QQItem | LoginListItem)[];
captchaState?: {
needCaptcha: boolean;
proofWaterUrl: string;
uin: string;
password: string;
} | null;
captchaVerifying?: boolean;
newDeviceState?: {
needNewDevice: boolean;
jumpUrl: string;
uin: string;
} | null;
onCaptchaCancel?: () => void;
onNewDeviceCancel?: () => void;
}
const PasswordLogin: React.FC<PasswordLoginProps> = ({ onSubmit, isLoading, qqList }) => {
const PasswordLogin: React.FC<PasswordLoginProps> = ({ onSubmit, onCaptchaSubmit, onNewDeviceVerified, isLoading, qqList, captchaState, captchaVerifying, newDeviceState, onCaptchaCancel, onNewDeviceCancel }) => {
const [uin, setUin] = useState('');
const [password, setPassword] = useState('');
@@ -34,87 +54,128 @@ const PasswordLogin: React.FC<PasswordLoginProps> = ({ onSubmit, isLoading, qqLi
return (
<div className='flex flex-col gap-8'>
<div className='flex justify-center'>
<Image
className='shadow-lg'
height={100}
radius='full'
src={`https://q1.qlogo.cn/g?b=qq&nk=${uin || '0'}&s=100`}
width={100}
alt="QQ Avatar"
{captchaState?.needCaptcha && captchaState.proofWaterUrl ? (
<div className='flex flex-col gap-4 items-center'>
{captchaVerifying ? (
<>
<p className='text-primary text-sm'>...</p>
<div className='flex items-center justify-center py-8 gap-3'>
<Spinner size='lg' />
</div>
</>
) : (
<>
<p className='text-warning text-sm'></p>
<TencentCaptchaModal
proofWaterUrl={captchaState.proofWaterUrl}
onSuccess={(data) => {
onCaptchaSubmit?.(captchaState.uin, captchaState.password, data);
}}
onCancel={onCaptchaCancel}
/>
</>
)}
<Button
variant='light'
color='danger'
size='sm'
onPress={onCaptchaCancel}
>
</Button>
</div>
) : newDeviceState?.needNewDevice && newDeviceState.jumpUrl ? (
<NewDeviceVerify
jumpUrl={newDeviceState.jumpUrl}
uin={newDeviceState.uin}
onVerified={(token) => onNewDeviceVerified?.(token)}
onCancel={onNewDeviceCancel}
/>
</div>
<div className='flex flex-col gap-4'>
<Input
type="text"
label="QQ账号"
placeholder="请输入QQ号"
value={uin}
onValueChange={setUin}
variant="bordered"
size='lg'
autoComplete="off"
endContent={
<Dropdown>
<DropdownTrigger>
<Button isIconOnly variant="light" size="sm" radius="full">
<IoChevronDown size={16} />
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="QQ Login History"
items={qqList}
onAction={(key) => setUin(key.toString())}
>
{(item) => (
<DropdownItem key={item.uin} textValue={item.uin}>
<div className='flex items-center gap-2'>
<Avatar
alt={item.uin}
className='flex-shrink-0'
size='sm'
src={
isQQQuickNewItem(item)
? item.faceUrl
: `https://q1.qlogo.cn/g?b=qq&nk=${item.uin}&s=1`
}
/>
<div className='flex flex-col'>
{isQQQuickNewItem(item)
? `${item.nickName}(${item.uin})`
: item.uin}
</div>
</div>
</DropdownItem>
)}
</DropdownMenu>
</Dropdown>
}
/>
<Input
type="password"
label="密码"
placeholder="请输入密码"
value={password}
onValueChange={setPassword}
variant="bordered"
size='lg'
autoComplete="new-password"
/>
</div>
<div className='flex justify-center mt-5'>
<Button
className='w-64 max-w-full'
color='primary'
isLoading={isLoading}
radius='full'
size='lg'
variant='shadow'
onPress={handleSubmit}
>
</Button>
</div>
) : (
<>
<div className='flex justify-center'>
<Image
className='shadow-lg'
height={100}
radius='full'
src={`https://q1.qlogo.cn/g?b=qq&nk=${uin || '0'}&s=100`}
width={100}
alt="QQ Avatar"
/>
</div>
<div className='flex flex-col gap-4'>
<Input
type="text"
label="QQ账号"
placeholder="请输入QQ号"
value={uin}
onValueChange={setUin}
variant="bordered"
size='lg'
autoComplete="off"
endContent={
<Dropdown>
<DropdownTrigger>
<Button isIconOnly variant="light" size="sm" radius="full">
<IoChevronDown size={16} />
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="QQ Login History"
items={qqList}
onAction={(key) => setUin(key.toString())}
>
{(item) => (
<DropdownItem key={item.uin} textValue={item.uin}>
<div className='flex items-center gap-2'>
<Avatar
alt={item.uin}
className='flex-shrink-0'
size='sm'
src={
isQQQuickNewItem(item)
? item.faceUrl
: `https://q1.qlogo.cn/g?b=qq&nk=${item.uin}&s=1`
}
/>
<div className='flex flex-col'>
{isQQQuickNewItem(item)
? `${item.nickName}(${item.uin})`
: item.uin}
</div>
</div>
</DropdownItem>
)}
</DropdownMenu>
</Dropdown>
}
/>
<Input
type="password"
label="密码"
placeholder="请输入密码"
value={password}
onValueChange={setPassword}
variant="bordered"
size='lg'
autoComplete="new-password"
/>
</div>
<div className='flex justify-center mt-5'>
<Button
className='w-64 max-w-full'
color='primary'
isLoading={isLoading}
radius='full'
size='lg'
variant='shadow'
onPress={handleSubmit}
>
</Button>
</div>
</>
)}
</div>
);
};

View File

@@ -0,0 +1,166 @@
import { useEffect, useRef, useCallback } from 'react';
import { Spinner } from '@heroui/spinner';
declare global {
interface Window {
TencentCaptcha: new (
appid: string,
callback: (res: TencentCaptchaResult) => void,
options?: Record<string, unknown>
) => { show: () => void; destroy: () => void; };
}
}
export interface TencentCaptchaResult {
ret: number;
appid?: string;
ticket?: string;
randstr?: string;
errorCode?: number;
errorMessage?: string;
}
export interface CaptchaCallbackData {
ticket: string;
randstr: string;
appid: string;
sid: string;
}
interface TencentCaptchaProps {
/** proofWaterUrl returned from login error, contains uin/sid/aid params */
proofWaterUrl: string;
/** Called when captcha verification succeeds */
onSuccess: (data: CaptchaCallbackData) => void;
/** Called when captcha is cancelled or fails */
onCancel?: () => void;
}
function parseUrlParams (url: string): Record<string, string> {
const params: Record<string, string> = {};
try {
const u = new URL(url);
u.searchParams.forEach((v, k) => { params[k] = v; });
} catch {
const match = url.match(/[?&]([^#]+)/);
if (match) {
match[1].split('&').forEach(pair => {
const [k, v] = pair.split('=');
if (k) params[k] = decodeURIComponent(v || '');
});
}
}
return params;
}
function loadScript (src: string): Promise<void> {
return new Promise((resolve, reject) => {
if (window.TencentCaptcha) {
resolve();
return;
}
const tag = document.createElement('script');
tag.src = src;
tag.onload = () => resolve();
tag.onerror = () => reject(new Error(`Failed to load ${src}`));
document.head.appendChild(tag);
});
}
const TencentCaptchaModal: React.FC<TencentCaptchaProps> = ({
proofWaterUrl,
onSuccess,
onCancel,
}) => {
const captchaRef = useRef<{ destroy: () => void; } | null>(null);
const mountedRef = useRef(true);
const handleResult = useCallback((res: TencentCaptchaResult, sid: string) => {
if (!mountedRef.current) return;
if (res.ret === 0 && res.ticket && res.randstr) {
onSuccess({
ticket: res.ticket,
randstr: res.randstr,
appid: res.appid || '',
sid,
});
} else {
onCancel?.();
}
}, [onSuccess, onCancel]);
useEffect(() => {
mountedRef.current = true;
const params = parseUrlParams(proofWaterUrl);
const appid = params.aid || '2081081773';
const sid = params.sid || '';
const init = async () => {
try {
await loadScript('https://captcha.gtimg.com/TCaptcha.js');
} catch {
try {
await loadScript('https://ssl.captcha.qq.com/TCaptcha.js');
} catch {
// Both CDN failed, generate fallback ticket
if (mountedRef.current) {
handleResult({
ret: 0,
ticket: `terror_1001_${appid}_${Math.floor(Date.now() / 1000)}`,
randstr: '@' + Math.random().toString(36).substring(2),
errorCode: 1001,
errorMessage: 'jsload_error',
}, sid);
}
return;
}
}
if (!mountedRef.current) return;
try {
const captcha = new window.TencentCaptcha(
appid,
(res) => handleResult(res, sid),
{
type: 'popup',
showHeader: false,
login_appid: params.login_appid,
uin: params.uin,
sid: params.sid,
enableAged: true,
}
);
captchaRef.current = captcha;
captcha.show();
} catch {
if (mountedRef.current) {
handleResult({
ret: 0,
ticket: `terror_1001_${appid}_${Math.floor(Date.now() / 1000)}`,
randstr: '@' + Math.random().toString(36).substring(2),
errorCode: 1001,
errorMessage: 'init_error',
}, sid);
}
}
};
init();
return () => {
mountedRef.current = false;
captchaRef.current?.destroy();
captchaRef.current = null;
};
}, [proofWaterUrl, handleResult]);
return (
<div className="flex items-center justify-center py-8 gap-3">
<Spinner size="lg" />
<span className="text-default-500">...</span>
</div>
);
};
export default TencentCaptchaModal;

View File

@@ -96,10 +96,93 @@ export default class QQManager {
}
public static async passwordLogin (uin: string, passwordMd5: string) {
await serverRequest.post<ServerResponse<null>>('/QQLogin/PasswordLogin', {
const data = await serverRequest.post<ServerResponse<{
needCaptcha?: boolean;
proofWaterUrl?: string;
needNewDevice?: boolean;
jumpUrl?: string;
newDevicePullQrCodeSig?: string;
} | null>>('/QQLogin/PasswordLogin', {
uin,
passwordMd5,
});
return data.data.data;
}
public static async captchaLogin (uin: string, passwordMd5: string, ticket: string, randstr: string, sid: string) {
const data = await serverRequest.post<ServerResponse<{
needNewDevice?: boolean;
jumpUrl?: string;
newDevicePullQrCodeSig?: string;
} | null>>('/QQLogin/CaptchaLogin', {
uin,
passwordMd5,
ticket,
randstr,
sid,
});
return data.data.data;
}
public static async newDeviceLogin (uin: string, passwordMd5: string, newDevicePullQrCodeSig: string) {
const data = await serverRequest.post<ServerResponse<{
needNewDevice?: boolean;
jumpUrl?: string;
newDevicePullQrCodeSig?: string;
} | null>>('/QQLogin/NewDeviceLogin', {
uin,
passwordMd5,
newDevicePullQrCodeSig,
});
return data.data.data;
}
public static async getNewDeviceQRCode (uin: string, jumpUrl: string) {
const data = await serverRequest.post<ServerResponse<{
str_url?: string;
bytes_token?: string;
uint32_guarantee_status?: number;
ActionStatus?: string;
ErrorCode?: number;
ErrorInfo?: string;
}>>('/QQLogin/GetNewDeviceQRCode', {
uin,
jumpUrl,
});
const result = data.data.data;
if (result?.str_url) {
let bytesToken = result.bytes_token || '';
if (!bytesToken && result.str_url) {
// 只对 str_url 参数值做 base64 编码
try {
const urlObj = new URL(result.str_url);
const strUrlParam = urlObj.searchParams.get('str_url') || '';
bytesToken = strUrlParam ? btoa(strUrlParam) : '';
} catch {
bytesToken = '';
}
}
return {
str_url: result.str_url,
bytes_token: bytesToken,
uint32_guarantee_status: result.uint32_guarantee_status,
ActionStatus: result.ActionStatus,
ErrorCode: result.ErrorCode,
ErrorInfo: result.ErrorInfo,
};
}
return result;
}
public static async pollNewDeviceQR (uin: string, bytesToken: string) {
const data = await serverRequest.post<ServerResponse<{
uint32_guarantee_status?: number;
str_nt_succ_token?: string;
}>>('/QQLogin/PollNewDeviceQR', {
uin,
bytesToken,
});
return data.data.data;
}
public static async resetDeviceID () {

View File

@@ -1,8 +1,8 @@
import { Button } from '@heroui/button';
import { useEffect, useState, useRef } from 'react';
import { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
import { IoMdRefresh } from 'react-icons/io';
import { FiUpload } from 'react-icons/fi';
// import { FiUpload } from 'react-icons/fi';
import { useDisclosure } from '@heroui/modal';
import PageLoading from '@/components/page_loading';
@@ -19,7 +19,7 @@ export default function PluginPage () {
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const [currentPluginId, setCurrentPluginId] = useState<string>('');
const fileInputRef = useRef<HTMLInputElement>(null);
// const fileInputRef = useRef<HTMLInputElement>(null);
const loadPlugins = async () => {
setLoading(true);
@@ -106,60 +106,60 @@ export default function PluginPage () {
onOpen();
};
const handleImportClick = () => {
if (pluginManagerNotFound) {
dialog.confirm({
title: '插件管理器未加载',
content: (
<div className="space-y-2">
<p className="text-sm text-default-600">
</p>
<p className="text-sm text-default-600">
</p>
</div>
),
confirmText: '注册插件管理器',
cancelText: '取消',
onConfirm: async () => {
try {
await PluginManager.registerPluginManager();
toast.success('插件管理器注册成功');
setPluginManagerNotFound(false);
// 注册成功后打开文件选择器
fileInputRef.current?.click();
} catch (e: any) {
toast.error('注册失败: ' + e.message);
}
},
});
return;
}
fileInputRef.current?.click();
};
// const handleImportClick = () => {
// if (pluginManagerNotFound) {
// dialog.confirm({
// title: '插件管理器未加载',
// content: (
// <div className="space-y-2">
// <p className="text-sm text-default-600">
// 插件管理器尚未加载,无法导入插件。
// </p>
// <p className="text-sm text-default-600">
// 是否立即注册插件管理器?
// </p>
// </div>
// ),
// confirmText: '注册插件管理器',
// cancelText: '取消',
// onConfirm: async () => {
// try {
// await PluginManager.registerPluginManager();
// toast.success('插件管理器注册成功');
// setPluginManagerNotFound(false);
// // 注册成功后打开文件选择器
// fileInputRef.current?.click();
// } catch (e: any) {
// toast.error('注册失败: ' + e.message);
// }
// },
// });
// return;
// }
// fileInputRef.current?.click();
// };
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
// const file = e.target.files?.[0];
// if (!file) return;
// 重置 input允许重复选择同一文件
e.target.value = '';
// // 重置 input允许重复选择同一文件
// e.target.value = '';
if (!file.name.endsWith('.zip')) {
toast.error('请选择 .zip 格式的插件包');
return;
}
// if (!file.name.endsWith('.zip')) {
// toast.error('请选择 .zip 格式的插件包');
// return;
// }
const loadingToast = toast.loading('正在导入插件...');
try {
const result = await PluginManager.importLocalPlugin(file);
toast.success(result.message, { id: loadingToast });
loadPlugins();
} catch (err: any) {
toast.error(err.message || '导入失败', { id: loadingToast });
}
};
// const loadingToast = toast.loading('正在导入插件...');
// try {
// const result = await PluginManager.importLocalPlugin(file);
// toast.success(result.message, { id: loadingToast });
// loadPlugins();
// } catch (err: any) {
// toast.error(err.message || '导入失败', { id: loadingToast });
// }
// };
return (
<>
@@ -182,6 +182,7 @@ export default function PluginPage () {
>
<IoMdRefresh size={24} />
</Button>
{/* 禁用插件上传
<Button
className="bg-primary-100/50 hover:bg-primary-200/50 text-primary-700 backdrop-blur-md"
radius='full'
@@ -197,6 +198,7 @@ export default function PluginPage () {
className="hidden"
onChange={handleFileChange}
/>
*/}
</div>
{pluginManagerNotFound ? (

View File

@@ -19,6 +19,7 @@ import QrCodeLogin from '@/components/qr_code_login';
import QuickLogin from '@/components/quick_login';
import type { QQItem } from '@/components/quick_login';
import { ThemeSwitch } from '@/components/theme-switch';
import type { CaptchaCallbackData } from '@/components/tencent_captcha';
import QQManager from '@/controllers/qq_manager';
import useDialog from '@/hooks/use-dialog';
@@ -58,6 +59,21 @@ export default function QQLoginPage () {
const [refresh, setRefresh] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>('shortcut');
const firstLoad = useRef<boolean>(true);
const [captchaState, setCaptchaState] = useState<{
needCaptcha: boolean;
proofWaterUrl: string;
uin: string;
password: string;
} | null>(null);
const [captchaVerifying, setCaptchaVerifying] = useState(false);
const [newDeviceState, setNewDeviceState] = useState<{
needNewDevice: boolean;
jumpUrl: string;
newDevicePullQrCodeSig: string;
uin: string;
password: string;
} | null>(null);
// newDevicePullQrCodeSig is kept for step:2 login after QR verification
const onSubmit = async () => {
if (!uinValue) {
toast.error('请选择快捷登录的QQ');
@@ -83,8 +99,28 @@ export default function QQLoginPage () {
try {
// 计算密码的MD5值
const passwordMd5 = CryptoJS.MD5(password).toString();
await QQManager.passwordLogin(uin, passwordMd5);
toast.success('密码登录请求已发送');
const result = await QQManager.passwordLogin(uin, passwordMd5);
if (result?.needCaptcha && result.proofWaterUrl) {
// 需要验证码,显示验证码组件
setCaptchaState({
needCaptcha: true,
proofWaterUrl: result.proofWaterUrl,
uin,
password,
});
toast('需要安全验证,请完成验证码', { icon: '🔒' });
} else if (result?.needNewDevice && result.jumpUrl) {
setNewDeviceState({
needNewDevice: true,
jumpUrl: result.jumpUrl,
newDevicePullQrCodeSig: result.newDevicePullQrCodeSig || '',
uin,
password,
});
toast('检测到新设备,请扫码验证', { icon: '📱' });
} else {
toast.success('密码登录请求已发送');
}
} catch (error) {
const msg = (error as Error).message;
toast.error(`密码登录失败: ${msg}`);
@@ -93,6 +129,75 @@ export default function QQLoginPage () {
}
};
const onCaptchaSubmit = async (uin: string, password: string, captchaData: CaptchaCallbackData) => {
setIsLoading(true);
setCaptchaVerifying(true);
try {
const passwordMd5 = CryptoJS.MD5(password).toString();
const result = await QQManager.captchaLogin(uin, passwordMd5, captchaData.ticket, captchaData.randstr, captchaData.sid);
if (result?.needNewDevice && result.jumpUrl) {
setCaptchaState(null);
setNewDeviceState({
needNewDevice: true,
jumpUrl: result.jumpUrl,
newDevicePullQrCodeSig: result.newDevicePullQrCodeSig || '',
uin,
password,
});
toast('检测到异常设备,请扫码验证', { icon: '📱' });
} else {
toast.success('验证码登录请求已发送');
setCaptchaState(null);
}
} catch (error) {
const msg = (error as Error).message;
toast.error(`验证码登录失败: ${msg}`);
setCaptchaState(null);
} finally {
setIsLoading(false);
setCaptchaVerifying(false);
}
};
const onCaptchaCancel = () => {
setCaptchaState(null);
};
const onNewDeviceVerified = async (token: string) => {
if (!newDeviceState) return;
setIsLoading(true);
try {
const passwordMd5 = CryptoJS.MD5(newDeviceState.password).toString();
// Use the str_nt_succ_token from QR verification as newDevicePullQrCodeSig for step:2
const sig = token || newDeviceState.newDevicePullQrCodeSig;
const result = await QQManager.newDeviceLogin(newDeviceState.uin, passwordMd5, sig);
if (result?.needNewDevice && result.jumpUrl) {
// 新设备验证后又触发了异常设备验证,更新 jumpUrl
setNewDeviceState({
needNewDevice: true,
jumpUrl: result.jumpUrl,
newDevicePullQrCodeSig: result.newDevicePullQrCodeSig || '',
uin: newDeviceState.uin,
password: newDeviceState.password,
});
toast('检测到异常设备,请继续扫码验证', { icon: '📱' });
} else {
toast.success('新设备验证登录请求已发送');
setNewDeviceState(null);
}
} catch (error) {
const msg = (error as Error).message;
toast.error(`新设备验证登录失败: ${msg}`);
setNewDeviceState(null);
} finally {
setIsLoading(false);
}
};
const onNewDeviceCancel = () => {
setNewDeviceState(null);
};
const onUpdateQrCode = async () => {
if (firstLoad.current) setIsLoading(true);
try {
@@ -249,7 +354,14 @@ export default function QQLoginPage () {
<PasswordLogin
isLoading={isLoading}
onSubmit={onPasswordSubmit}
onCaptchaSubmit={onCaptchaSubmit}
onNewDeviceVerified={onNewDeviceVerified}
qqList={qqList}
captchaState={captchaState}
captchaVerifying={captchaVerifying}
newDeviceState={newDeviceState}
onCaptchaCancel={onCaptchaCancel}
onNewDeviceCancel={onNewDeviceCancel}
/>
</Tab>
<Tab key='qrcode' title='扫码登录'>

View File

@@ -1,20 +0,0 @@
import key from '@/const/key';
/**
* 将后端返回的插件 icon 路径拼接上 webui_token 查询参数
* 后端 /api/Plugin/Icon/:pluginId 需要鉴权img src 无法携带 Authorization header
* 所以通过 query 参数传递 token
*/
export function getPluginIconUrl (iconPath?: string): string | undefined {
if (!iconPath) return undefined;
try {
const raw = localStorage.getItem(key.token);
if (!raw) return iconPath;
const token = JSON.parse(raw);
const url = new URL(iconPath, window.location.origin);
url.searchParams.set('webui_token', token);
return url.pathname + url.search;
} catch {
return iconPath;
}
}

123
pnpm-lock.yaml generated
View File

@@ -65,8 +65,8 @@ importers:
packages/napcat-common:
dependencies:
ajv:
specifier: ^8.13.0
version: 8.17.1
specifier: ^8.18.0
version: 8.18.0
file-type:
specifier: ^21.0.0
version: 21.1.0
@@ -84,8 +84,8 @@ importers:
specifier: ^0.34.38
version: 0.34.41
ajv:
specifier: ^8.13.0
version: 8.17.1
specifier: ^8.18.0
version: 8.18.0
compressing:
specifier: ^1.10.1
version: 1.10.3
@@ -174,11 +174,8 @@ importers:
specifier: ^0.34.38
version: 0.34.41
ajv:
specifier: ^8.13.0
version: 8.17.1
async-mutex:
specifier: ^0.5.0
version: 0.5.0
specifier: ^8.18.0
version: 8.18.0
cors:
specifier: ^2.8.5
version: 2.8.5
@@ -264,8 +261,8 @@ importers:
specifier: ^0.34.38
version: 0.34.41
ajv:
specifier: ^8.13.0
version: 8.17.1
specifier: ^8.18.0
version: 8.18.0
cors:
specifier: ^2.8.5
version: 2.8.5
@@ -430,8 +427,8 @@ importers:
specifier: ^0.34.38
version: 0.34.41
ajv:
specifier: ^8.13.0
version: 8.17.1
specifier: ^8.18.0
version: 8.18.0
compressing:
specifier: ^1.10.3
version: 1.10.3
@@ -900,6 +897,9 @@ packages:
'@codemirror/commands@6.10.1':
resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==}
'@codemirror/commands@6.10.2':
resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==}
'@codemirror/lang-css@6.3.1':
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
@@ -924,6 +924,9 @@ packages:
'@codemirror/theme-one-dark@6.1.3':
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
'@codemirror/view@6.39.16':
resolution: {integrity: sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==}
'@codemirror/view@6.39.6':
resolution: {integrity: sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==}
@@ -1913,105 +1916,89 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -2107,6 +2094,9 @@ packages:
'@lezer/common@1.5.0':
resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==}
'@lezer/common@1.5.1':
resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==}
'@lezer/css@1.3.0':
resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==}
@@ -2762,67 +2752,56 @@ packages:
resolution: {integrity: sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.53.2':
resolution: {integrity: sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.53.2':
resolution: {integrity: sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.53.2':
resolution: {integrity: sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.53.2':
resolution: {integrity: sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.53.2':
resolution: {integrity: sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.53.2':
resolution: {integrity: sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.53.2':
resolution: {integrity: sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.53.2':
resolution: {integrity: sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.53.2':
resolution: {integrity: sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.53.2':
resolution: {integrity: sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.53.2':
resolution: {integrity: sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==}
@@ -2897,28 +2876,24 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@swc/core-linux-arm64-musl@1.15.1':
resolution: {integrity: sha512-fKzP9mRQGbhc5QhJPIsqKNNX/jyWrZgBxmo3Nz1SPaepfCUc7RFmtcJQI5q8xAun3XabXjh90wqcY/OVyg2+Kg==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@swc/core-linux-x64-gnu@1.15.1':
resolution: {integrity: sha512-ZLjMi138uTJxb+1wzo4cB8mIbJbAsSLWRNeHc1g1pMvkERPWOGlem+LEYkkzaFzCNv1J8aKcL653Vtw8INHQeg==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@swc/core-linux-x64-musl@1.15.1':
resolution: {integrity: sha512-jvSI1IdsIYey5kOITzyajjofXOOySVitmLxb45OPUjoNojql4sDojvlW5zoHXXFePdA6qAX4Y6KbzAOV3T3ctA==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@swc/core-win32-arm64-msvc@1.15.1':
resolution: {integrity: sha512-X/FcDtNrDdY9r4FcXHt9QxUqC/2FbQdvZobCKHlHe8vTSKhUHOilWl5EBtkFVfsEs4D5/yAri9e3bJbwyBhhBw==}
@@ -3283,49 +3258,41 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -3450,11 +3417,11 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ajv@6.14.0:
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
@@ -3563,9 +3530,6 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
async-mutex@0.5.0:
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
@@ -4654,16 +4618,17 @@ packages:
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
deprecated: Glob versions prior to v9 are no longer supported
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
@@ -5808,6 +5773,7 @@ packages:
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
hasBin: true
prelude-ls@1.2.1:
@@ -6507,6 +6473,7 @@ packages:
tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
text-hex@1.0.0:
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
@@ -7171,6 +7138,13 @@ snapshots:
'@codemirror/view': 6.39.6
'@lezer/common': 1.5.0
'@codemirror/commands@6.10.2':
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.3
'@codemirror/view': 6.39.16
'@lezer/common': 1.5.1
'@codemirror/lang-css@6.3.1':
dependencies:
'@codemirror/autocomplete': 6.20.0
@@ -7212,7 +7186,7 @@ snapshots:
'@codemirror/search@6.5.11':
dependencies:
'@codemirror/state': 6.5.3
'@codemirror/view': 6.39.6
'@codemirror/view': 6.39.16
crelt: 1.0.6
'@codemirror/state@6.5.3':
@@ -7226,6 +7200,13 @@ snapshots:
'@codemirror/view': 6.39.6
'@lezer/highlight': 1.2.3
'@codemirror/view@6.39.16':
dependencies:
'@codemirror/state': 6.5.3
crelt: 1.0.6
style-mod: 4.1.3
w3c-keyname: 2.2.8
'@codemirror/view@6.39.6':
dependencies:
'@codemirror/state': 6.5.3
@@ -7468,7 +7449,7 @@ snapshots:
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
ajv: 6.14.0
debug: 4.4.3
espree: 10.4.0
globals: 14.0.0
@@ -8607,6 +8588,8 @@ snapshots:
'@lezer/common@1.5.0': {}
'@lezer/common@1.5.1': {}
'@lezer/css@1.3.0':
dependencies:
'@lezer/common': 1.5.0
@@ -10308,14 +10291,14 @@ snapshots:
screenfull: 5.2.0
tslib: 2.8.1
ajv@6.12.6:
ajv@6.14.0:
dependencies:
fast-deep-equal: 3.1.3
fast-json-stable-stringify: 2.1.0
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ajv@8.17.1:
ajv@8.18.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.0
@@ -10443,10 +10426,6 @@ snapshots:
async-function@1.0.0: {}
async-mutex@0.5.0:
dependencies:
tslib: 2.8.1
async@3.2.6: {}
asynckit@0.4.0: {}
@@ -10717,12 +10696,12 @@ snapshots:
codemirror@6.0.2:
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/commands': 6.10.1
'@codemirror/commands': 6.10.2
'@codemirror/language': 6.12.1
'@codemirror/lint': 6.9.2
'@codemirror/search': 6.5.11
'@codemirror/state': 6.5.3
'@codemirror/view': 6.39.6
'@codemirror/view': 6.39.16
color-convert@2.0.1:
dependencies:
@@ -11389,7 +11368,7 @@ snapshots:
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
ajv: 6.12.6
ajv: 6.14.0
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.3