diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..25369ec --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Yui: Debug TS via nodestart", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/tools/yui/nodestart.cjs", + "runtimeExecutable": "node", + "runtimeArgs": ["--enable-source-maps"], + "preLaunchTask": "yui:build", + "autoAttachChildProcesses": true, + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/program/**/*.js"], + "resolveSourceMapLocations": [ + "${workspaceFolder}/program/**/*.js.map", + "${workspaceFolder}/src/**/*.ts", + "!**/node_modules/**" + ], + "skipFiles": ["/**"], + "console": "integratedTerminal" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..8898c94 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "yui:build", + "type": "shell", + "command": "pnpm run yui:build", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + } + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..06f4227 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,122 @@ +# AGENTS + +This file gives coding agents and contributors a fast, project-specific map of the Yui repository. + +## Project Summary + +- Yui is a TypeScript-based bot framework around NTQQ/QQ integration. +- The runtime entry is `src/index.ts`, which initializes OneBot actions, starts HTTP/WebSocket servers, and loads native pieces on Linux. +- The main build output is bundled with Vite into `program/index.js` (CommonJS format). +- The project expects the Node.js version to match the official client version. The current documented version is `v20.18.1` in `README.MD`. + +## Package Manager And Toolchain + +- Package manager: `pnpm` +- Language: TypeScript with ESM source files +- Bundler: Vite +- Test runner: Vitest +- Native/runtime coupling: Electron/NTQQ-related native modules and preload hooks are part of normal development flow + +## Common Commands + +Use the commands defined in `package.json` rather than older names in some docs. + +- Install dependencies: `pnpm install` +- Install Yui runtime assets: `pnpm run yui:install` +- Start Vite watch build: `pnpm run yui:dev` +- Build once: `pnpm run yui:build` +- Start node-mode runtime: `pnpm run yui:nodestart` +- Install UI assets: `pnpm run ui:install` +- Start UI mode: `pnpm run ui:start` +- Start UI mode with log file: `pnpm run ui:start-log-file` +- Regenerate protobuf TS files: `pnpm run proto:generate` +- Watch protobuf changes: `pnpm run proto:watch` +- Compile JS to bytecode helper: `pnpm run compile-jsc` +- Run tests directly: `pnpm exec vitest run` + +## Important Documentation + +- Overview: `README.MD` +- Dev workflow: `docs/dev.md` +- Manual environment setup: `docs/dev-manual.md` +- Patch notes / validation removal: `docs/patch.md` +- Exported function inventory: `docs/function.md` + +Note: `docs/dev.md` still uses older command names such as `pnpm run dev` and `pnpm run nodestart`. Prefer the `yui:*` and `ui:*` scripts from `package.json`. + +## Repository Map + +### Source directories + +- `src/index.ts`: main runtime entry +- `src/bootstrap`: startup/runtime orchestration +- `src/adapters`: protocol and runtime adapters for HTTP, WebSocket, OneBot, and server startup +- `src/app`: application services for higher-level use cases such as login and message flows +- `src/server`: legacy-compatible server entrypoints plus HTTP/WebSocket implementations +- `src/http`: legacy-compatible HTTP route and handler area; newer route assembly now lives under `src/adapters/http` +- `src/onebot`: legacy-compatible OneBot entrypoints plus protocol-specific handlers +- `src/ntqq`: NTQQ-related types, protobuf code, helpers, and store logic +- `src/wrapper`: wrapper hook logic and exported wrapper service shims +- `src/native`: native initialization +- `src/common`: shared utilities, logging, locks, file helpers +- `src/store`: config and persistence helpers +- `src/models`: contact and model abstractions +- `src/transfer`: login/message conversion flow +- `src/test`: in-repo runtime test hooks used by the entrypoint + +### Build/runtime/support directories + +- `tools/yui`: install/build/dev/nodestart scripts +- `tools/ui`: UI startup/install helpers +- `tools/wrapper`: wrapper-related code generation/helpers +- `program`: runtime/program assets and generated launcher output +- `dist`: additional compiled artifacts such as the UI hook bundle +- `resources`: static assets +- `docs`: project documentation and reverse-engineering notes +- `test`: ad hoc scripts and artifacts, not the main Vitest suite + +## Change Guidelines + +### 1. Respect generated or semi-generated areas + +- `src/ntqq/protobuf/*.ts` is generated from `.proto` files. If behavior depends on protobuf schema changes, update the `.proto` files first and regenerate with `pnpm run proto:generate`. +- Large wrapper export files under `src/wrapper/exports` are repetitive and may be generated or scaffolded by helper scripts in `tools/wrapper`. When making broad changes, inspect the generator path before hand-editing many files. + +### 2. Be careful around runtime-coupled code + +- `src/index.ts`, `src/wrapper/hook.ts`, `src/native`, and `tools/yui/nodestart.cjs` are tightly coupled to local NTQQ/native runtime behavior. +- Linux runtime depends on `YUI_PRELOAD` and `YUI_NATIVE` paths. Windows runtime has different hard-coded development assumptions. +- Avoid “cleanup” changes in these paths unless they are necessary for the task and validated against the existing startup flow. + +### 3. Follow existing implementation patterns + +- Prefer placing new orchestration and registration logic in `src/adapters/**` and `src/app/**`. +- Treat files such as `src/onebot/onebot.ts`, `src/onebot/actions/index.ts`, `src/onebot/event/index.ts`, and `src/server/index.ts` as compatibility entrypoints; keep them thin. +- New OneBot actions should usually be implemented via `src/app/**` or adapter helpers, with `src/onebot/actions/**` remaining thin registration or compatibility layers. +- Server-facing changes should remain split between HTTP (`src/server/http.ts`, `src/adapters/http/**`) and WebSocket (`src/server/websocket.ts`, `src/adapters/ws/**`) concerns. +- Shared utilities belong in `src/common` or the nearest domain module instead of adding duplicate helpers. + +## Verification Guidance + +Choose the lightest useful verification for the files you touch. + +- For TypeScript or bundling changes: run `pnpm run yui:build` +- For protobuf changes: run `pnpm run proto:generate` +- For unit-testable logic: run `pnpm exec vitest run` +- For runtime/bootstrap changes: if possible, also validate with `pnpm run yui:nodestart` + +Because this repo integrates with local QQ/NTQQ assets and native modules, some runtime validation may not be possible in a clean sandbox. If you cannot run full validation, state that clearly. + +## Working Agreements For Agents + +- Prefer small, surgical edits over broad formatting churn. +- Do not rewrite generated protobuf outputs manually unless regeneration is intentionally out of scope and the change is very localized. +- When editing wrapper export files, preserve logging and call-through structure unless the task is explicitly about changing that behavior. +- Check for outdated docs before following them blindly; `package.json` is the source of truth for runnable scripts. +- Treat `program`, native paths, and startup scripts as integration-sensitive areas. + +## Known Gotchas + +- The repository may already contain built artifacts and local runtime assets such as `program`, `dist`, `cache`, and `tmp`. +- The working tree may also include local-only files or directories such as `.codex`; avoid touching unrelated files. +- The Vitest setup exists, but a large portion of the project behavior is integration-driven rather than covered by isolated tests. diff --git a/README.MD b/README.MD index c92bb14..6407127 100644 --- a/README.MD +++ b/README.MD @@ -29,6 +29,24 @@ 获取当前项目,对它进行进一步开发,请阅读[开发文档](./docs/dev.md) +常用命令: + +- `pnpm run yui:install` +- `pnpm run yui:dev` +- `pnpm run yui:build` +- `pnpm run yui:nodestart` +- `pnpm run ui:start` +- `pnpm run ui:start-log-file` + +当前目录约定: + +- 启动与运行时初始化优先查看 `src/bootstrap/**` +- HTTP / WS / OneBot 装配优先查看 `src/adapters/**` +- 动作用例与注册优先查看 `src/app/**` +- `src/onebot`、`src/server`、`src/http` 中多数文件已逐步退化为兼容入口 + +更多结构说明见:[重构计划](./docs/refactor-plan.md) 与 [迁移清单](./docs/refactor-migration-checklist.md) + ## 注意 -NodeJs版本要与官方客户端一致。目前是`v20.18.1`。 \ No newline at end of file +NodeJs版本要与官方客户端一致。目前是`v20.18.1`。 diff --git a/docs/dev-manual.md b/docs/dev-manual.md index 87e3972..5b00560 100644 --- a/docs/dev-manual.md +++ b/docs/dev-manual.md @@ -33,6 +33,6 @@ ## 4. 启动 - 1. pnpm run dev - 2. pnpm run nodestart + 1. `pnpm run yui:dev` + 2. `pnpm run yui:nodestart` 3. 看到输出中包含:`Hi Yui bot!!` diff --git a/docs/dev.md b/docs/dev.md index 4739f63..55c3859 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -12,9 +12,9 @@ ### 2.1 使用脚本安装 -1. 执行 `pnpm run install-ntqq` -2. pnpm run dev -3. pnpm run nodestart +1. 执行 `pnpm run yui:install` +2. 执行 `pnpm run yui:dev` +3. 执行 `pnpm run yui:nodestart` ### 2.2 手动安装 @@ -24,11 +24,11 @@ ## 1. 运行 - 1. 执行命令 `pnpm run dev`,开始编译ts文件 - 2. 生成的文件在 `program/resources/app/app_launcher/index.js` - 3. 启动无UI界面的Yui,执行`pnpm nodestart` - 4. 启动有UI界面的Yui,执行`pnpm start` - 5. 启动有UI界面保存Log的Yui,执行`pnpm start-log-file` (分析接口行为使用) + 1. 执行命令 `pnpm run yui:dev`,开始监听并编译 TypeScript + 2. Node 运行入口会构建到 `program/index.js` + 3. 启动无 UI 界面的 Yui,执行 `pnpm run yui:nodestart` + 4. 启动有 UI 界面的 Yui,执行 `pnpm run ui:start` + 5. 启动有 UI 界面并保存日志的 Yui,执行 `pnpm run ui:start-log-file`(分析接口行为使用) ## 2. 关于后续开发 @@ -57,7 +57,7 @@ ## 2. 实现功能流程 - 相关代码在:[getFriendList](../src/onebot/actions/friend.ts) + 相关代码在:[getFriendList](../src/app/friend/friend-service.ts) 1. 通过 `sendEvent` 先订阅 `onBuddyListChange` 事件 2. 再通过 `registerEventListener` 注册一次性的好友列表变更事件 `onBuddyListChange`,用来接收推送 @@ -65,6 +65,6 @@ ## 3. 注册Onebot的动作处理 - 相关代码在:[initFriend](../src/onebot/actions/friend.ts) + 相关代码在:[friendActionHandlers](../src/app/friend/register-friend-actions.ts) - 在这个函数中,通过 `registerActionHandle` 来注册动作调用函数。 + 当前动作注册已经迁移到 `src/app/**` 下的薄注册层中,通过 `action handler map + registerActionHandlers` 的方式统一完成注册。 diff --git a/docs/function.md b/docs/function.md index 0b6a1fe..8381e89 100644 --- a/docs/function.md +++ b/docs/function.md @@ -1,3 +1,10 @@ +> 说明 +> +> 本文主要记录 NT wrapper / kernel service 的方法清单,用于排查和能力索引。 +> +> 当前业务主路径已逐步迁移到 `src/bootstrap/**`、`src/adapters/**`、`src/app/**` 与 `src/ntqq/core/**`。 +> 若要新增或调整实际功能,优先从这些目录进入,而不是回到 `src/onebot/**`、`src/server/**` 等兼容入口扩展逻辑。 + ## NodeIKernelAVSDKService | 方法名 | 参数数量 | @@ -2285,4 +2292,3 @@ | 方法名 | 参数数量 | |-------|-----| | onResult | 4 | - diff --git a/docs/refactor-migration-checklist.md b/docs/refactor-migration-checklist.md new file mode 100644 index 0000000..b7f4018 --- /dev/null +++ b/docs/refactor-migration-checklist.md @@ -0,0 +1,122 @@ +# 重构迁移清单 + +> **本文是有限清单。所有条目均已完成并冻结。** +> 不追加新条目,不提出“下一阶段建议”。 + +## 状态图例 + +- `[x]` 已完成,冻结,不再验证 + +--- + +## 1. 已完成项(冻结) + +### 启动与装配 +- [x] `src/index.ts` 薄入口 +- [x] `src/bootstrap/**` 启动编排与 runtime 初始化 +- [x] `src/adapters/server/**` HTTP/WS 服务启动 +- [x] `src/adapters/http/**` 路由装配 +- [x] `src/adapters/ws/**` 连接管理、请求处理 +- [x] `src/adapters/onebot/**` 事件装配与构造 + +### Action/Event +- [x] `src/app/register-onebot-actions.ts` 统一 action handler map +- [x] `src/app/dispatch-action.ts` 统一分发 +- [x] `src/store/action-registry.ts` 独立注册表 +- [x] `src/adapters/onebot/register-events.ts` 统一事件注册 +- [x] `src/store/event-registry.ts` 独立注册表 + +### 应用服务层 +- [x] `src/app/login/**` 登录用例 +- [x] `src/app/friend/**` 好友用例 +- [x] `src/app/group/**` 群组用例 +- [x] `src/app/message/**` 消息用例 + +### NTQQ 生命周期 +- [x] ntqq/core 粒度合理(最大 164 行),不需进一步拆分 +- [x] runtime-store 统一到 current runtime / current self 语义 + +### 登录与配置 +- [x] `src/ntqq/login/` account-*.ts 系列 +- [x] `src/ntqq/store/` config-*.ts 系列 +- [x] 聚合出口 `@aggregation-boundary` 标注完成 + +### 消息转换 +- [x] 发送链路 flow / builder / resource 三层 +- [x] 接收链路 `convert-receive.ts` + +### Compat 清理 +- [x] 所有旧 compat 文件已删除(server、onebot/actions、onebot/event、http handler、store/store.ts 等) +- [x] 所有 `use*Service()` facade 已删除 +- [x] `useLogger()` 定性为长期 API +- [x] kicked offline `process.exit` 已加注释说明设计意图 + +### 测试 +- [x] `src/common/deep-merge.test.ts` +- [x] `src/transfer/message/send-basic-element-builders.test.ts` +- [x] `src/transfer/message/image-resource-preparer.test.ts` + +### 依赖方向验证 +- [x] app → adapters: 无(正确) +- [x] ntqq → adapters: 无(正确) +- [x] transfer → adapters: 无(正确) +- [x] 非 bootstrap → bootstrap: 无(正确) +- [x] ntqq 内部循环: 无(正确) + +--- + +## 2. 已完成收尾项 + +### [x] 任务 1:删除残留空目录 + +已删除: + +```text +src/server/error/ +src/onebot/actions/ +src/http/ +``` + +验证:上述路径已不存在 + +### [x] 任务 2:迁移 `forward-converter.ts` + +- `src/common/forward-converter.ts` 已迁移到 `src/transfer/message/forward-converter.ts` +- 群合并转发发送链路已由 `src/ntqq/message/group-forward.ts` 承接 + +### [x] 任务 3:清理 `common/utils.ts` 的 native 依赖 + +- `getHttpTicket()` 已迁移到 `src/ntqq/message/http-ticket.ts` +- `src/common/utils.ts` 已不再 import `native/native` + +### [x] 任务 4:处理 models 层 native 依赖 + +- `src/models/contact/group.ts` 已不再直接调用 native,改为通过 `src/ntqq/message/group-forward.ts` 薄委托 +- `src/models/contact/member.ts` 已不再直接调用 native,改为通过 `src/ntqq/group/nudge.ts` 薄委托 + +--- + +## 3. 完成定义 + +- [x] 任务 1 完成 +- [x] 任务 2 完成 +- [x] 任务 3 完成 +- [x] 任务 4 完成 + +**全部 4 项已完成,本轮重构结束。** + +## 4. 约束规则(给执行者) + +1. **不追加任务**:完成以上条目后停止 +2. **不重新验证 `[x]` 项**:冻结项不需重新检查 +3. **不扩展范围**:不做批量重命名、不做进一步拆分 +4. **维持当前文档口径**:后续仅在代码状态变化时同步更新 + +## 5. 验证记录 + +| 日期 | 命令 | 结果 | +|---|---|---| +| 2026-04-18 | `pnpm run yui:build` | 通过 | +| 2026-04-18 | `pnpm exec jest` | 通过(3 个测试文件) | +| 2026-04-18 | 空目录检查 | `src/server/error`、`src/onebot/actions`、`src/http` 已不存在 | +| 2026-04-18 | 依赖方向 grep 验证 | `common → native` 与 `models → native` 已清理 | diff --git a/docs/refactor-plan.md b/docs/refactor-plan.md new file mode 100644 index 0000000..32998b0 --- /dev/null +++ b/docs/refactor-plan.md @@ -0,0 +1,174 @@ +# 重构方案 + +> **本文是终态参考文档,不是任务生成器。** +> 本轮有限清单已全部完成,不追加新任务、不提出新阶段。 + +基于 2026-04-18 代码实际分析。 + +--- + +## 1. 分析结论 + +**主路径迁移与收尾清理均已完成,当前架构整体合理,依赖方向正确。** + +### 1.1 做得好的部分 + +- **层次分明**:bootstrap → adapters → app → ntqq/transfer 四层职责清晰,无跨层逆向依赖 +- **无循环依赖**:ntqq 内部 core/login/store/query/message 之间无交叉 import +- **Infra 干净**:common 和 store 作为底层被各层依赖,自身不反向依赖业务层 +- **Compat 清理彻底**:server/、onebot/actions、onebot/event、http/ 内的 compat 文件与空目录壳均已清理 +- **命名收口完成**:仅保留 `useLogger()` 一个 `use*` 命名作为长期 API,其余 facade 已清除 +- **聚合出口已标注**:三个聚合边界文件(`login/account.ts`、`login/index.ts`、`store/config.ts`)已用 `@aggregation-boundary` 定性 +- **文件落点合理**:合并转发转换器已迁入 `src/transfer/message/forward-converter.ts` +- **边界恢复纯净**:`src/common/utils.ts` 已去除 native 依赖,`src/models/contact/group.ts` 与 `member.ts` 已不再直接调用 native +- **process.exit 已明确**:kicked offline 处的 `process.exit` 已移除 TODO,注释说明了设计意图 + +### 1.2 本轮收尾项处理结果 + +#### 项目 A:残留空目录壳 + +已删除: + +```text +src/server/error/ +src/onebot/actions/ +src/http/ +``` + +#### 项目 B:`forward-converter.ts` 放置位置不合理 + +- 已从 `src/common/forward-converter.ts` 迁移到 `src/transfer/message/forward-converter.ts` +- 该文件继续承担合并转发图片上传、PB 消息构造与 XML 预览生成 +- 群合并转发发送动作已通过 `src/ntqq/message/group-forward.ts` 调用 + +#### 项目 C:`src/models/` 直接调用 native + +- `src/models/contact/group.ts` 不再直接 import `native/native` +- `src/models/contact/member.ts` 不再直接 import `native/native` +- native 调用已分别下沉到: + - `src/ntqq/message/group-forward.ts` + - `src/ntqq/group/nudge.ts` + +#### 项目 D:`src/common/utils.ts` 依赖 native + +- `getHttpTicket()` 已迁移到 `src/ntqq/message/http-ticket.ts` +- `src/common/utils.ts` 现在只保留通用工具能力,不再依赖 native/protobuf runtime + +## 2. 当前架构 + +### 2.1 启动链路 + +```text +src/index.ts + → bootstrap/start-app.ts # initRuntime → initializeOneBot → startServers + → bootstrap/init-runtime.ts # Linux preload、process hook +``` + +### 2.2 层次与依赖方向 + +```text +Bootstrap ──→ Adapters ──→ App ──→ Runtime/Transfer + │ │ │ │ + └──────────────┴──────────┴──────────┴──→ Infra (common, store) + Protocol Types (onebot/contracts) +``` + +实际依赖验证结果: + +| 检查项 | 结果 | +|---|---| +| app → adapters | **无**(正确) | +| ntqq → adapters | **无**(正确) | +| transfer → adapters | **无**(正确) | +| 非 bootstrap → bootstrap | **无**(正确) | +| 任何层 → server/(旧) | **无**(正确) | +| ntqq 内部循环 (core↔login↔store) | **无**(正确) | +| common → native | **无**(已清理) | +| models → native | **无**(已清理) | + +### 2.3 各层现状 + +| 层 | 路径 | 状态 | +|---|---|---| +| Entry | `src/index.ts` | 薄入口,正常 | +| Bootstrap | `src/bootstrap/` | 干净 | +| Adapters | `src/adapters/**` | 正常 | +| App | `src/app/**` | 正常 | +| NTQQ | `src/ntqq/**` | 正常 | +| Transfer | `src/transfer/**` | 正常 | +| OneBot Contracts | `src/onebot/contracts/` | 纯类型,正常 | +| Common | `src/common/` | 已恢复为通用基础设施 | +| Store | `src/store/` | 正常 | +| Models | `src/models/` | 已去除 direct native 调用 | +| Wrapper | `src/wrapper/` | 冻结区域,不动 | +| Native | `src/native/` | 底层 | + +## 3. 已完成项(冻结,不再验证) + +- 主路径迁移(bootstrap → adapters → app → ntqq/transfer) +- Action 注册收口(`register-onebot-actions.ts` + `dispatch-action.ts`) +- Event 注册收口(`register-events.ts` 体系) +- 所有 compat 文件删除 +- 所有 `use*Service()` / `useNTCore()` / `useNTWrapper()` facade 删除 +- `useLogger()` 定性为长期 API +- 消息发送链路拆为 flow / builder / resource 三层 +- NT 登录拆到 `account-*.ts`、NT 配置拆到 `config-*.ts` +- runtime-store 统一到 current runtime / current self 语义 +- ntqq/core 文件粒度已合理 +- 聚合出口标注完成 +- kicked offline process.exit 已明确 +- Jest 基础测试已补 3 个 + +## 4. 收尾任务完成记录 + +> 原有限清单中的 4 项任务已全部完成。完成后结束,不追加新任务。 + +### 任务 1:删除残留空目录 + +- 已删除 `src/server/error`、`src/onebot/actions`、`src/http` + +### 任务 2:迁移 `forward-converter.ts` + +- 已迁移到 `src/transfer/message/forward-converter.ts` +- 群转发发送链路已通过 `src/ntqq/message/group-forward.ts` 承接 + +### 任务 3:清理 `common/utils.ts` 的 native 依赖 + +- 已将 `getHttpTicket()` 迁移到 `src/ntqq/message/http-ticket.ts` +- `src/common/utils.ts` 不再依赖 native + +### 任务 4:处理 models 层 native 依赖 + +- 已将群转发发送 native 调用封装到 `src/ntqq/message/group-forward.ts` +- 已将群成员 nudge native 调用封装到 `src/ntqq/group/nudge.ts` +- `src/models/contact/group.ts` 与 `src/models/contact/member.ts` 现在通过 ntqq 层薄委托调用 + +## 5. 硬性约束(永久生效) + +1. **新代码落点**:`bootstrap` / `adapters` / `app` / `ntqq` / `transfer` / `common` / `store` +2. **不恢复 compat**:不新建 compat entry、compat facade、文件级 re-export alias +3. **不扩散 account 命名**:`account` 仅在协议层 action 名中使用,内部新代码用 runtime / self +4. **common 层纯净**:新增 common 代码不应依赖 ntqq / native / models +5. **冻结区域**:`src/wrapper/exports/**`、`src/ntqq/protobuf/**`、`src/native/**`、`tools/yui/nodestart.cjs` + +## 6. 非目标 + +以下内容不属于重构范围,不要作为“下一步”提出: + +- 全量重写 wrapper/exports +- 手工改写 protobuf 生成物 +- 全局批量替换历史命名 +- 进一步拆分 ntqq/core(粒度已合理) +- 重命名 account-*.ts 文件(导出已用新语义) +- 统一 useLogger() 命名 +- 任何形式的“继续拆分”、“继续收口”、“继续收紧” + +## 7. 验证记录 + +| 日期 | 命令 | 结果 | +|---|---|---| +| 2026-04-18 | `pnpm run yui:build` | 通过 | +| 2026-04-18 | `pnpm exec jest` | 通过(3 个测试文件) | +| 2026-04-18 | `pnpm run yui:nodestart` | 未执行(依赖本地 NTQQ 环境) | +| 2026-04-18 | 空目录检查 | `src/server/error`、`src/onebot/actions`、`src/http` 已不存在 | +| 2026-04-18 | 依赖方向检查 | `common → native` 与 `models → native` 已清理 | diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 852e70b..0000000 --- a/jest.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] -}; diff --git a/package.json b/package.json index 543588e..8d7b31b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "ui:install": "node tools/ui/install.cjs", "ui:start": "node tools/ui/start.cjs", "ui:start-log-file": "node tools/ui/start-log-file.cjs", + "test": "vitest run", + "test:watch": "vitest", "proto:generate": "protoc --ts_proto_out=src/ntqq/protobuf --proto_path=./src/ntqq/protobuf/protos ./src/ntqq/protobuf/protos/*.proto --ts_proto_opt=esModuleInterop=true", "proto:watch": "chokidar \"./src/**/*.proto\" -c \"npm run proto:generate\"", "compile-jsc": "node tools/dev.cjs compile-jsc" @@ -20,31 +22,22 @@ "author": "", "license": "ISC", "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-terser": "^0.4.4", - "@rollup/plugin-typescript": "^11.1.5", "@types/body-parser": "^1.19.5", "@types/express": "^4.17.21", "@types/express-fileupload": "^1.5.0", - "@types/jest": "^30.0.0", "@types/node": "^24.6.2", "@types/uuid": "^9.0.6", "@types/ws": "^8.5.8", "bytenode": "^1.5.3", "chokidar-cli": "^3.0.0", "electron": "25.8.1", - "javascript-obfuscator": "^4.1.0", - "jest": "^30.1.3", "nodejs-file-downloader": "^4.13.0", "protoc": "^32.1.0", - "rollup": "^4.13.2", - "rollup-plugin-obfuscator": "^1.1.0", - "ts-jest": "^29.4.4", "ts-proto": "^2.2.5", "tslib": "^2.6.2", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "vite": "^7.1.12", + "vitest": "^3.2.4" }, "dependencies": { "@bufbuild/protobuf": "^2.2.2", @@ -56,7 +49,7 @@ "image-size": "^1.1.1", "uuid": "^9.0.1", "ws": "^8.14.2", - "yaml": "^2.4.1" + "yaml": "^2.4.2" }, "packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9103ef1..f7379b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,24 +36,9 @@ importers: specifier: ^8.14.2 version: 8.16.0 yaml: - specifier: ^2.4.1 - version: 2.4.1 + specifier: ^2.4.2 + version: 2.8.3 devDependencies: - '@rollup/plugin-commonjs': - specifier: ^25.0.7 - version: 25.0.7(rollup@4.14.3) - '@rollup/plugin-json': - specifier: ^6.1.0 - version: 6.1.0(rollup@4.14.3) - '@rollup/plugin-node-resolve': - specifier: ^15.2.3 - version: 15.2.3(rollup@4.14.3) - '@rollup/plugin-terser': - specifier: ^0.4.4 - version: 0.4.4(rollup@4.14.3) - '@rollup/plugin-typescript': - specifier: ^11.1.5 - version: 11.1.6(rollup@4.14.3)(tslib@2.6.2)(typescript@5.4.5) '@types/body-parser': specifier: ^1.19.5 version: 1.19.5 @@ -63,9 +48,6 @@ importers: '@types/express-fileupload': specifier: ^1.5.0 version: 1.5.0 - '@types/jest': - specifier: ^30.0.0 - version: 30.0.0 '@types/node': specifier: ^24.6.2 version: 24.6.2 @@ -84,27 +66,12 @@ importers: electron: specifier: 25.8.1 version: 25.8.1 - javascript-obfuscator: - specifier: ^4.1.0 - version: 4.1.0 - jest: - specifier: ^30.1.3 - version: 30.1.3(@types/node@24.6.2) nodejs-file-downloader: specifier: ^4.13.0 version: 4.13.0 protoc: specifier: ^32.1.0 version: 32.1.0 - rollup: - specifier: ^4.13.2 - version: 4.14.3 - rollup-plugin-obfuscator: - specifier: ^1.1.0 - version: 1.1.0(javascript-obfuscator@4.1.0)(rollup@4.14.3) - ts-jest: - specifier: ^29.4.4 - version: 29.4.4(@babel/core@7.28.4)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.28.4))(jest-util@30.0.5)(jest@30.1.3(@types/node@24.6.2))(typescript@5.4.5) ts-proto: specifier: ^2.2.5 version: 2.2.5 @@ -114,174 +81,15 @@ importers: typescript: specifier: ^5.4.5 version: 5.4.5 + vite: + specifier: ^7.1.12 + version: 7.3.2(@types/node@24.6.2)(terser@5.30.3)(yaml@2.8.3) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.6.2)(terser@5.30.3)(yaml@2.8.3) packages: - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.28.4': - resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.28.4': - resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.3': - resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.28.4': - resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-attributes@7.27.1': - resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.27.1': - resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.28.4': - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} - engines: {node: '>=6.9.0'} - - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@bufbuild/protobuf@2.2.2': resolution: {integrity: sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==} @@ -289,338 +97,324 @@ packages: resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} engines: {node: '>=12'} - '@emnapi/core@1.5.0': - resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] - '@emnapi/runtime@1.5.0': - resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] - '@javascript-obfuscator/escodegen@2.3.0': - resolution: {integrity: sha512-QVXwMIKqYMl3KwtTirYIA6gOCiJ0ZDtptXqAv/8KWLG9uQU2fZqTVy7a/A5RvcoZhbDoFfveTxuGxJ5ibzQtkw==} - engines: {node: '>=6.0'} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] - '@javascript-obfuscator/estraverse@5.4.0': - resolution: {integrity: sha512-CZFX7UZVN9VopGbjTx4UXaXsi9ewoM1buL0kY7j1ftYdSs7p2spv9opxFjHlQ/QGTgh4UqufYqJJ0WKLml7b6w==} - engines: {node: '>=4.0'} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] - '@jest/console@30.1.2': - resolution: {integrity: sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] - '@jest/core@30.1.3': - resolution: {integrity: sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] - '@jest/diff-sequences@30.0.1': - resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] - '@jest/environment@30.1.2': - resolution: {integrity: sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] - '@jest/expect-utils@30.1.2': - resolution: {integrity: sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] - '@jest/expect@30.1.2': - resolution: {integrity: sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] - '@jest/fake-timers@30.1.2': - resolution: {integrity: sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] - '@jest/get-type@30.1.0': - resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] - '@jest/globals@30.1.2': - resolution: {integrity: sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] - '@jest/pattern@30.0.1': - resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] - '@jest/reporters@30.1.3': - resolution: {integrity: sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] - '@jest/schemas@30.0.5': - resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] - '@jest/snapshot-utils@30.1.2': - resolution: {integrity: sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] - '@jest/source-map@30.0.1': - resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] - '@jest/test-result@30.1.3': - resolution: {integrity: sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] - '@jest/test-sequencer@30.1.3': - resolution: {integrity: sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] - '@jest/transform@30.1.2': - resolution: {integrity: sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] - '@jest/types@30.0.5': - resolution: {integrity: sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.6': resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - '@jridgewell/sourcemap-codec@1.4.15': - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@pkgr/core@0.2.9': - resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - - '@rollup/plugin-commonjs@25.0.7': - resolution: {integrity: sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.68.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-json@6.1.0': - resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-node-resolve@15.2.3': - resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-terser@0.4.4': - resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-typescript@11.1.6': - resolution: {integrity: sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.14.0||^3.0.0||^4.0.0 - tslib: '*' - typescript: '>=3.7.0' - peerDependenciesMeta: - rollup: - optional: true - tslib: - optional: true - - '@rollup/pluginutils@5.1.0': - resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.14.3': - resolution: {integrity: sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==} + '@rollup/rollup-android-arm-eabi@4.60.1': + resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.14.3': - resolution: {integrity: sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==} + '@rollup/rollup-android-arm64@4.60.1': + resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.14.3': - resolution: {integrity: sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==} + '@rollup/rollup-darwin-arm64@4.60.1': + resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.14.3': - resolution: {integrity: sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==} + '@rollup/rollup-darwin-x64@4.60.1': + resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.14.3': - resolution: {integrity: sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==} + '@rollup/rollup-freebsd-arm64@4.60.1': + resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.1': + resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.14.3': - resolution: {integrity: sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==} + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] + libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.14.3': - resolution: {integrity: sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==} + '@rollup/rollup-linux-arm64-gnu@4.60.1': + resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.14.3': - resolution: {integrity: sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==} + '@rollup/rollup-linux-arm64-musl@4.60.1': + resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-powerpc64le-gnu@4.14.3': - resolution: {integrity: sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==} + '@rollup/rollup-linux-loong64-gnu@4.60.1': + resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.1': + resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.14.3': - resolution: {integrity: sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==} + '@rollup/rollup-linux-ppc64-musl@4.60.1': + resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-s390x-gnu@4.14.3': - resolution: {integrity: sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==} + '@rollup/rollup-linux-riscv64-musl@4.60.1': + resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.14.3': - resolution: {integrity: sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==} + '@rollup/rollup-linux-x64-gnu@4.60.1': + resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.14.3': - resolution: {integrity: sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==} + '@rollup/rollup-linux-x64-musl@4.60.1': + resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-win32-arm64-msvc@4.14.3': - resolution: {integrity: sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==} + '@rollup/rollup-openbsd-x64@4.60.1': + resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.1': + resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.14.3': - resolution: {integrity: sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==} + '@rollup/rollup-win32-ia32-msvc@4.60.1': + resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.14.3': - resolution: {integrity: sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==} + '@rollup/rollup-win32-x64-gnu@4.60.1': + resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} cpu: [x64] os: [win32] - '@sinclair/typebox@0.34.41': - resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} + '@rollup/rollup-win32-x64-msvc@4.60.1': + resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + cpu: [x64] + os: [win32] '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - - '@sinonjs/fake-timers@13.0.5': - resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} - '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} @@ -630,12 +424,21 @@ packages: '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-fileupload@1.5.0': resolution: {integrity: sha512-Y9v88IC5ItAxkKwfnyIi1y0jSZwTMY4jqXUQLZ3jFhYJlLdRnN919bKBNM8jbVVD2cxywA/uEC1kNNpZQGwx7Q==} @@ -654,27 +457,12 @@ packages: '@types/http-proxy@1.17.14': resolution: {integrity: sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==} - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} - - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - - '@types/jest@30.0.0': - resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} - '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/minimatch@3.0.5': - resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} - '@types/node@18.19.31': resolution: {integrity: sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==} @@ -690,9 +478,6 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} @@ -702,132 +487,43 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - '@types/validator@13.11.9': - resolution: {integrity: sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==} - '@types/ws@8.5.10': resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - - '@types/yargs@17.0.33': - resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} - cpu: [arm] - os: [android] - - '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} - cpu: [arm64] - os: [android] - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} - cpu: [arm64] - os: [darwin] - - '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} - cpu: [x64] - os: [darwin] - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} - cpu: [x64] - os: [freebsd] - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - 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] + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@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] + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} - cpu: [x64] - os: [linux] - libc: [musl] + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} - cpu: [arm64] - os: [win32] + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} - cpu: [ia32] - os: [win32] + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} - cpu: [x64] - os: [win32] + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} @@ -838,107 +534,28 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.8.2: - resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} - engines: {node: '>=0.4.0'} - hasBin: true - agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - array-differ@3.0.0: - resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==} - engines: {node: '>=8'} - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - arrify@2.0.1: - resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} - engines: {node: '>=8'} - - assert@2.0.0: - resolution: {integrity: sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==} - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - babel-jest@30.1.2: - resolution: {integrity: sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - '@babel/core': ^7.11.0 - - babel-plugin-istanbul@7.0.1: - resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} - engines: {node: '>=12'} - - babel-plugin-jest-hoist@30.0.1: - resolution: {integrity: sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} - peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 - - babel-preset-jest@30.0.1: - resolution: {integrity: sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - '@babel/core': ^7.11.0 - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - baseline-browser-mapping@2.8.7: - resolution: {integrity: sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==} - hasBin: true + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} @@ -951,42 +568,16 @@ packages: boolean@3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.26.2: - resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} - busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -999,6 +590,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + cacheable-lookup@5.0.4: resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} engines: {node: '>=10.6.0'} @@ -1011,38 +606,21 @@ packages: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-lite@1.0.30001745: - resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==} - case-anything@2.1.13: resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} engines: {node: '>=12.13'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - chance@1.1.9: - resolution: {integrity: sha512-TfxnA/DcZXRTA4OekA2zL9GH8qscbbl6X0ZqU4tXhGveVY/mXWvEQLt5GwZcYXTEyEFflVtj+pG8nc8EwSm1RQ==} - - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} - charenc@0.0.2: - resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} chokidar-cli@3.0.0: resolution: {integrity: sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==} @@ -1053,59 +631,21 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - ci-info@4.3.0: - resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} - engines: {node: '>=8'} - - cjs-module-lexer@2.1.0: - resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} - - class-validator@0.14.0: - resolution: {integrity: sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==} - cliui@5.0.0: resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} - color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@10.0.0: - resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==} - engines: {node: '>=14'} - commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -1114,9 +654,6 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -1124,13 +661,6 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - crypt@0.0.2: - resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} - debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -1148,6 +678,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -1156,20 +695,9 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} - dedent@1.7.0: - resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} defer-to-connect@2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} @@ -1196,43 +724,23 @@ packages: engines: {node: '>=0.10'} hasBin: true - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} dprint-node@1.0.8: resolution: {integrity: sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.224: - resolution: {integrity: sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==} - electron@25.8.1: resolution: {integrity: sha512-GtcP1nMrROZfFg0+mhyj1hamrHvukfF6of2B/pcWxmWkd5FVY1NJib0tlhiorFZRzQN5Z+APLPr7aMolt7i2AQ==} engines: {node: '>= 12.20.55'} hasBin: true - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} - emoji-regex@7.0.3: resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -1244,9 +752,6 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-define-property@1.0.0: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} @@ -1255,54 +760,26 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} - es6-object-assign@1.1.0: - resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-scope@7.1.1: - resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@3.3.0: - resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} @@ -1311,17 +788,9 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - exit-x@0.2.2: - resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} - engines: {node: '>= 0.8.0'} - - expect@30.1.2: - resolution: {integrity: sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} express-fileupload@1.5.0: resolution: {integrity: sha512-jSW3w9evqM37VWkEPkL2Ck5wUo2a8qa03MH+Ou/0ZSTpNlQFBvSLjU12k2nYcHhaMPv4JVvv6+Ac1OuLgUZb7w==} @@ -1336,21 +805,18 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-type-mime@0.3.10: resolution: {integrity: sha512-VzzSOoRehn8M9c5ckYrlurv7GxJBA7lvetMJ1sP9AHlwv1+1ZwREZhta7ndtg1QNR1HiuX2656Dq3okfG3qI4w==} @@ -1358,10 +824,6 @@ packages: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - finalhandler@1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} engines: {node: '>= 0.8'} @@ -1370,10 +832,6 @@ packages: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - follow-redirects@1.15.6: resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} engines: {node: '>=4.0'} @@ -1383,13 +841,6 @@ packages: debug: optional: true - for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1402,9 +853,6 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1413,10 +861,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1425,34 +869,14 @@ packages: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - hasBin: true - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} - global-agent@3.0.0: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} engines: {node: '>=10.0'} @@ -1471,15 +895,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -1491,17 +906,10 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -1525,10 +933,6 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -1538,53 +942,17 @@ packages: engines: {node: '>=16.x'} hasBin: true - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - inversify@6.0.1: - resolution: {integrity: sha512-B3ex30927698TJENHR++8FfEaJGqoWOgI6ZY5Ht/nLUsFCwHn6akbwtnUAPCgUepAnTpe2qHxhDNjoKLyz6rgQ==} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - is-arguments@1.1.1: - resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} - engines: {node: '>= 0.4'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - - is-builtin-module@3.2.1: - resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} - engines: {node: '>=6'} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - - is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1593,29 +961,10 @@ packages: resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} engines: {node: '>=4'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - - is-generator-function@1.0.10: - resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} - engines: {node: '>= 0.4'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - - is-nan@1.3.2: - resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} - engines: {node: '>= 0.4'} - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1624,278 +973,49 @@ packages: resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} engines: {node: '>=10'} - is-reference@1.2.1: - resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-typed-array@1.1.13: - resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} - engines: {node: '>= 0.4'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-lib-source-maps@5.0.6: - resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - - javascript-obfuscator@4.1.0: - resolution: {integrity: sha512-ckC0VFKQ0/sFtLH9apW/ZLfsP8LuZqZhVEM4VTJ5KLzyLaodW6C1lTU8808eboDmddKyvd2uyRx5bzc0Me0GYg==} - engines: {node: ^12.22.0 || ^14.0.0 || ^16.0.0 || ^17.0.0 || >=18.0.0} - hasBin: true - - jest-changed-files@30.0.5: - resolution: {integrity: sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-circus@30.1.3: - resolution: {integrity: sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-cli@30.1.3: - resolution: {integrity: sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jest-config@30.1.3: - resolution: {integrity: sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - '@types/node': '*' - esbuild-register: '>=3.4.0' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - esbuild-register: - optional: true - ts-node: - optional: true - - jest-diff@30.1.2: - resolution: {integrity: sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-docblock@30.0.1: - resolution: {integrity: sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-each@30.1.0: - resolution: {integrity: sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-environment-node@30.1.2: - resolution: {integrity: sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-haste-map@30.1.0: - resolution: {integrity: sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-leak-detector@30.1.0: - resolution: {integrity: sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-matcher-utils@30.1.2: - resolution: {integrity: sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-message-util@30.1.0: - resolution: {integrity: sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-mock@30.0.5: - resolution: {integrity: sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@30.0.1: - resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-resolve-dependencies@30.1.3: - resolution: {integrity: sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-resolve@30.1.3: - resolution: {integrity: sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-runner@30.1.3: - resolution: {integrity: sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-runtime@30.1.3: - resolution: {integrity: sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-snapshot@30.1.2: - resolution: {integrity: sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-util@30.0.5: - resolution: {integrity: sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-validate@30.1.0: - resolution: {integrity: sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-watcher@30.1.3: - resolution: {integrity: sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-worker@30.1.0: - resolution: {integrity: sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest@30.1.3: - resolution: {integrity: sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - js-string-escape@1.0.1: - resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} - engines: {node: '>= 0.8'} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - - levn@0.3.0: - resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} - engines: {node: '>= 0.8.0'} - - libphonenumber-js@1.10.60: - resolution: {integrity: sha512-Ctgq2lXUpEJo5j1762NOzl2xo7z7pqmVWYai0p07LvAkQ32tbPv3wb+tcUeHEiXhKU5buM4H9MXsXo6OlM6C2g==} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lowercase-keys@2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - magic-string@0.30.9: - resolution: {integrity: sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==} - engines: {node: '>=12'} - - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} - - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} matcher@3.0.0: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} - md5@2.3.0: - resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} - media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -1903,9 +1023,6 @@ packages: merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -1914,10 +1031,6 @@ packages: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1931,10 +1044,6 @@ packages: engines: {node: '>=4'} hasBin: true - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -1943,29 +1052,6 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - mkdirp@2.1.3: - resolution: {integrity: sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw==} - engines: {node: '>=10'} - hasBin: true - ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -1975,31 +1061,15 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multimatch@5.0.0: - resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==} - engines: {node: '>=10'} - - napi-postinstall@0.3.3: - resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - - node-releases@2.0.21: - resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} - nodejs-file-downloader@4.13.0: resolution: {integrity: sha512-nI2fKnmJWWFZF6SgMPe1iBodKhfpztLKJTtCtNYGhm/9QXmWa/Pk9Sv00qHgzEvNLe1x7hjGDRor7gcm/ChaIQ==} @@ -2011,17 +1081,9 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} - object-is@1.1.6: - resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} - engines: {node: '>= 0.4'} - object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -2033,18 +1095,6 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - opencollective-postinstall@2.0.3: - resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} - hasBin: true - - optionator@0.8.3: - resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} - engines: {node: '>= 0.8.0'} - p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -2053,29 +1103,14 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - p-locate@3.0.0: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -2084,28 +1119,16 @@ packages: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -2120,29 +1143,13 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - - possible-typed-array-names@1.0.0: - resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} - engines: {node: '>= 0.4'} - - prelude-ls@1.1.2: - resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} - engines: {node: '>= 0.8.0'} - - pretty-format@30.0.5: - resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} @@ -2160,9 +1167,6 @@ packages: pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} - pure-rand@7.0.1: - resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} - qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -2174,9 +1178,6 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -2185,16 +1186,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - reflect-metadata@0.1.13: - resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2208,18 +1203,6 @@ packages: resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} @@ -2227,14 +1210,8 @@ packages: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} - rollup-plugin-obfuscator@1.1.0: - resolution: {integrity: sha512-cMfQIKyGePlfHGGO+rSDhSATMBx7WWxXW/X66c53HylUE/owanTbG6nhttUQOkbdCiQH8tClskNEH/IPRoqZwA==} - peerDependencies: - javascript-obfuscator: '*' - rollup: ^2.56.3||^3.0.0||^4.0.0 - - rollup@4.14.3: - resolution: {integrity: sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==} + rollup@4.60.1: + resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2259,11 +1236,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} - engines: {node: '>=10'} - hasBin: true - send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -2272,9 +1244,6 @@ packages: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - serve-static@1.15.0: resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} @@ -2289,34 +1258,16 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - smob@1.5.0: - resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} - - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -2325,101 +1276,64 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} - - string-template@1.0.0: - resolution: {integrity: sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg==} - string-width@3.1.0: resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==} engines: {node: '>=6'} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - stringz@2.1.0: - resolution: {integrity: sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==} - strip-ansi@5.2.0: resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} engines: {node: '>=6'} - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} - engines: {node: '>=12'} - - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} sumchecker@3.0.1: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - synckit@0.11.11: - resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} - engines: {node: ^14.18.0 || >=16.0.0} - terser@5.30.3: resolution: {integrity: sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==} engines: {node: '>=10'} hasBin: true - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -2432,33 +1346,6 @@ packages: truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} - ts-jest@29.4.4: - resolution: {integrity: sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==} - engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 || ^30.0.0 - '@jest/types': ^29.0.0 || ^30.0.0 - babel-jest: ^29.0.0 || ^30.0.0 - esbuild: '*' - jest: ^29.0.0 || ^30.0.0 - jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <6' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/transform': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - jest-util: - optional: true - ts-poet@6.9.0: resolution: {integrity: sha512-roe6W6MeZmCjRmppyfOURklO5tQFQ6Sg7swURKkwYJvV7dbGCrK28um5+51iW3twdPRKtwarqFAVMU6G1mvnuQ==} @@ -2469,32 +1356,13 @@ packages: resolution: {integrity: sha512-P7arWANOAO2Jpzhey8x55H8mnK4XHzwepQph13eNf9nu93+lAB/JUIxKaIu18YnUQBpm3ZgHL0pTVPWa6dVqrQ==} hasBin: true - tslib@2.5.0: - resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - type-check@0.3.2: - resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} - engines: {node: '>= 0.8.0'} - - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - type-fest@0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} - type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -2504,11 +1372,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} - engines: {node: '>=0.8.0'} - hasBin: true - undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -2523,21 +1386,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - utf8-byte-length@1.0.5: resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} - util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -2546,59 +1397,98 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} - - validator@13.11.0: - resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} - engines: {node: '>= 0.10'} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - - which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true - which-typed-array@1.1.15: - resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} - engines: {node: '>= 0.4'} + vite@7.3.2: + resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true wrap-ansi@5.1.0: resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} engines: {node: '>=6'} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - write-file-atomic@5.0.1: - resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ws@8.16.0: resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} engines: {node: '>=10.0.0'} @@ -2614,232 +1504,24 @@ packages: y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.4.1: - resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==} - engines: {node: '>= 14'} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} hasBin: true yargs-parser@13.1.2: resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==} - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@13.3.2: - resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - -snapshots: - - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.27.1 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.28.4': {} - - '@babel/core@7.28.4': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) - '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.28.3': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.27.2': - dependencies: - '@babel/compat-data': 7.28.4 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.26.2 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.27.1': - dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.27.1': {} - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.27.1': {} - - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.28.4': - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - - '@babel/parser@7.28.4': - dependencies: - '@babel/types': 7.28.4 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - - '@babel/traverse@7.28.4': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.28.4': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + yargs@13.3.2: + resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - '@bcoe/v8-coverage@0.2.3': {} +snapshots: '@bufbuild/protobuf@2.2.2': {} @@ -2857,425 +1539,188 @@ snapshots: transitivePeerDependencies: - supports-color - '@emnapi/core@1.5.0': - dependencies: - '@emnapi/wasi-threads': 1.1.0 - tslib: 2.6.2 + '@esbuild/aix-ppc64@0.27.7': optional: true - '@emnapi/runtime@1.5.0': - dependencies: - tslib: 2.6.2 + '@esbuild/android-arm64@0.27.7': optional: true - '@emnapi/wasi-threads@1.1.0': - dependencies: - tslib: 2.6.2 + '@esbuild/android-arm@0.27.7': optional: true - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 + '@esbuild/android-x64@0.27.7': + optional: true - '@istanbuljs/load-nyc-config@1.1.0': - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.1 - resolve-from: 5.0.0 + '@esbuild/darwin-arm64@0.27.7': + optional: true - '@istanbuljs/schema@0.1.3': {} + '@esbuild/darwin-x64@0.27.7': + optional: true - '@javascript-obfuscator/escodegen@2.3.0': - dependencies: - '@javascript-obfuscator/estraverse': 5.4.0 - esprima: 4.0.1 - esutils: 2.0.3 - optionator: 0.8.3 - optionalDependencies: - source-map: 0.6.1 + '@esbuild/freebsd-arm64@0.27.7': + optional: true - '@javascript-obfuscator/estraverse@5.4.0': {} + '@esbuild/freebsd-x64@0.27.7': + optional: true - '@jest/console@30.1.2': - dependencies: - '@jest/types': 30.0.5 - '@types/node': 20.12.7 - chalk: 4.1.2 - jest-message-util: 30.1.0 - jest-util: 30.0.5 - slash: 3.0.0 - - '@jest/core@30.1.3': - dependencies: - '@jest/console': 30.1.2 - '@jest/pattern': 30.0.1 - '@jest/reporters': 30.1.3 - '@jest/test-result': 30.1.3 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - '@types/node': 20.12.7 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 4.3.0 - exit-x: 0.2.2 - graceful-fs: 4.2.11 - jest-changed-files: 30.0.5 - jest-config: 30.1.3(@types/node@20.12.7) - jest-haste-map: 30.1.0 - jest-message-util: 30.1.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.1.3 - jest-resolve-dependencies: 30.1.3 - jest-runner: 30.1.3 - jest-runtime: 30.1.3 - jest-snapshot: 30.1.2 - jest-util: 30.0.5 - jest-validate: 30.1.0 - jest-watcher: 30.1.3 - micromatch: 4.0.8 - pretty-format: 30.0.5 - slash: 3.0.0 - transitivePeerDependencies: - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node + '@esbuild/linux-arm64@0.27.7': + optional: true - '@jest/diff-sequences@30.0.1': {} + '@esbuild/linux-arm@0.27.7': + optional: true - '@jest/environment@30.1.2': - dependencies: - '@jest/fake-timers': 30.1.2 - '@jest/types': 30.0.5 - '@types/node': 20.12.7 - jest-mock: 30.0.5 + '@esbuild/linux-ia32@0.27.7': + optional: true - '@jest/expect-utils@30.1.2': - dependencies: - '@jest/get-type': 30.1.0 + '@esbuild/linux-loong64@0.27.7': + optional: true - '@jest/expect@30.1.2': - dependencies: - expect: 30.1.2 - jest-snapshot: 30.1.2 - transitivePeerDependencies: - - supports-color + '@esbuild/linux-mips64el@0.27.7': + optional: true - '@jest/fake-timers@30.1.2': - dependencies: - '@jest/types': 30.0.5 - '@sinonjs/fake-timers': 13.0.5 - '@types/node': 20.12.7 - jest-message-util: 30.1.0 - jest-mock: 30.0.5 - jest-util: 30.0.5 + '@esbuild/linux-ppc64@0.27.7': + optional: true - '@jest/get-type@30.1.0': {} + '@esbuild/linux-riscv64@0.27.7': + optional: true - '@jest/globals@30.1.2': - dependencies: - '@jest/environment': 30.1.2 - '@jest/expect': 30.1.2 - '@jest/types': 30.0.5 - jest-mock: 30.0.5 - transitivePeerDependencies: - - supports-color + '@esbuild/linux-s390x@0.27.7': + optional: true - '@jest/pattern@30.0.1': - dependencies: - '@types/node': 20.12.7 - jest-regex-util: 30.0.1 + '@esbuild/linux-x64@0.27.7': + optional: true - '@jest/reporters@30.1.3': - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 30.1.2 - '@jest/test-result': 30.1.3 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.12.7 - chalk: 4.1.2 - collect-v8-coverage: 1.0.2 - exit-x: 0.2.2 - glob: 10.4.5 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.2.0 - jest-message-util: 30.1.0 - jest-util: 30.0.5 - jest-worker: 30.1.0 - slash: 3.0.0 - string-length: 4.0.2 - v8-to-istanbul: 9.3.0 - transitivePeerDependencies: - - supports-color + '@esbuild/netbsd-arm64@0.27.7': + optional: true - '@jest/schemas@30.0.5': - dependencies: - '@sinclair/typebox': 0.34.41 + '@esbuild/netbsd-x64@0.27.7': + optional: true - '@jest/snapshot-utils@30.1.2': - dependencies: - '@jest/types': 30.0.5 - chalk: 4.1.2 - graceful-fs: 4.2.11 - natural-compare: 1.4.0 + '@esbuild/openbsd-arm64@0.27.7': + optional: true - '@jest/source-map@30.0.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - callsites: 3.1.0 - graceful-fs: 4.2.11 + '@esbuild/openbsd-x64@0.27.7': + optional: true - '@jest/test-result@30.1.3': - dependencies: - '@jest/console': 30.1.2 - '@jest/types': 30.0.5 - '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.2 + '@esbuild/openharmony-arm64@0.27.7': + optional: true - '@jest/test-sequencer@30.1.3': - dependencies: - '@jest/test-result': 30.1.3 - graceful-fs: 4.2.11 - jest-haste-map: 30.1.0 - slash: 3.0.0 - - '@jest/transform@30.1.2': - dependencies: - '@babel/core': 7.28.4 - '@jest/types': 30.0.5 - '@jridgewell/trace-mapping': 0.3.25 - babel-plugin-istanbul: 7.0.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 30.1.0 - jest-regex-util: 30.0.1 - jest-util: 30.0.5 - micromatch: 4.0.8 - pirates: 4.0.7 - slash: 3.0.0 - write-file-atomic: 5.0.1 - transitivePeerDependencies: - - supports-color + '@esbuild/sunos-x64@0.27.7': + optional: true - '@jest/types@30.0.5': - dependencies: - '@jest/pattern': 30.0.1 - '@jest/schemas': 30.0.5 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 20.12.7 - '@types/yargs': 17.0.33 - chalk: 4.1.2 + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + optional: true - '@jridgewell/gen-mapping@0.3.5': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/set-array@1.2.1': {} + '@jridgewell/resolve-uri@3.1.2': + optional: true '@jridgewell/source-map@0.3.6': dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/sourcemap-codec@1.4.15': {} + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + optional: true '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - - '@napi-rs/wasm-runtime@0.2.12': - dependencies: - '@emnapi/core': 1.5.0 - '@emnapi/runtime': 1.5.0 - '@tybys/wasm-util': 0.10.1 + '@jridgewell/sourcemap-codec': 1.5.5 optional: true - '@pkgjs/parseargs@0.11.0': + '@rollup/rollup-android-arm-eabi@4.60.1': optional: true - '@pkgr/core@0.2.9': {} + '@rollup/rollup-android-arm64@4.60.1': + optional: true - '@rollup/plugin-commonjs@25.0.7(rollup@4.14.3)': - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.14.3) - commondir: 1.0.1 - estree-walker: 2.0.2 - glob: 8.1.0 - is-reference: 1.2.1 - magic-string: 0.30.9 - optionalDependencies: - rollup: 4.14.3 + '@rollup/rollup-darwin-arm64@4.60.1': + optional: true - '@rollup/plugin-json@6.1.0(rollup@4.14.3)': - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.14.3) - optionalDependencies: - rollup: 4.14.3 + '@rollup/rollup-darwin-x64@4.60.1': + optional: true - '@rollup/plugin-node-resolve@15.2.3(rollup@4.14.3)': - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.14.3) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-builtin-module: 3.2.1 - is-module: 1.0.0 - resolve: 1.22.8 - optionalDependencies: - rollup: 4.14.3 + '@rollup/rollup-freebsd-arm64@4.60.1': + optional: true - '@rollup/plugin-terser@0.4.4(rollup@4.14.3)': - dependencies: - serialize-javascript: 6.0.2 - smob: 1.5.0 - terser: 5.30.3 - optionalDependencies: - rollup: 4.14.3 + '@rollup/rollup-freebsd-x64@4.60.1': + optional: true - '@rollup/plugin-typescript@11.1.6(rollup@4.14.3)(tslib@2.6.2)(typescript@5.4.5)': - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.14.3) - resolve: 1.22.8 - typescript: 5.4.5 - optionalDependencies: - rollup: 4.14.3 - tslib: 2.6.2 + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + optional: true - '@rollup/pluginutils@5.1.0(rollup@4.14.3)': - dependencies: - '@types/estree': 1.0.5 - estree-walker: 2.0.2 - picomatch: 2.3.1 - optionalDependencies: - rollup: 4.14.3 + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + optional: true - '@rollup/rollup-android-arm-eabi@4.14.3': + '@rollup/rollup-linux-arm64-gnu@4.60.1': optional: true - '@rollup/rollup-android-arm64@4.14.3': + '@rollup/rollup-linux-arm64-musl@4.60.1': optional: true - '@rollup/rollup-darwin-arm64@4.14.3': + '@rollup/rollup-linux-loong64-gnu@4.60.1': optional: true - '@rollup/rollup-darwin-x64@4.14.3': + '@rollup/rollup-linux-loong64-musl@4.60.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.14.3': + '@rollup/rollup-linux-ppc64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.14.3': + '@rollup/rollup-linux-ppc64-musl@4.60.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.14.3': + '@rollup/rollup-linux-riscv64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.14.3': + '@rollup/rollup-linux-riscv64-musl@4.60.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.14.3': + '@rollup/rollup-linux-s390x-gnu@4.60.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.14.3': + '@rollup/rollup-linux-x64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.14.3': + '@rollup/rollup-linux-x64-musl@4.60.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.14.3': + '@rollup/rollup-openbsd-x64@4.60.1': optional: true - '@rollup/rollup-linux-x64-musl@4.14.3': + '@rollup/rollup-openharmony-arm64@4.60.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.14.3': + '@rollup/rollup-win32-arm64-msvc@4.60.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.14.3': + '@rollup/rollup-win32-ia32-msvc@4.60.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.14.3': + '@rollup/rollup-win32-x64-gnu@4.60.1': optional: true - '@sinclair/typebox@0.34.41': {} + '@rollup/rollup-win32-x64-msvc@4.60.1': + optional: true '@sindresorhus/is@4.6.0': {} - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@13.0.5': - dependencies: - '@sinonjs/commons': 3.0.1 - '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.6.2 - optional: true - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.28.4 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.28.4 - '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 @@ -3292,12 +1737,21 @@ snapshots: '@types/node': 20.12.7 '@types/responselike': 1.0.3 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/connect@3.4.38': dependencies: '@types/node': 20.12.7 + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.5': {} + '@types/estree@1.0.8': {} + '@types/express-fileupload@1.5.0': dependencies: '@types/busboy': 1.5.3 @@ -3325,29 +1779,12 @@ snapshots: dependencies: '@types/node': 20.12.7 - '@types/istanbul-lib-coverage@2.0.6': {} - - '@types/istanbul-lib-report@3.0.3': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - - '@types/istanbul-reports@3.0.4': - dependencies: - '@types/istanbul-lib-report': 3.0.3 - - '@types/jest@30.0.0': - dependencies: - expect: 30.1.2 - pretty-format: 30.0.5 - '@types/keyv@3.1.4': dependencies: '@types/node': 20.12.7 '@types/mime@1.3.5': {} - '@types/minimatch@3.0.5': {} - '@types/node@18.19.31': dependencies: undici-types: 5.26.5 @@ -3364,8 +1801,6 @@ snapshots: '@types/range-parser@1.2.7': {} - '@types/resolve@1.20.2': {} - '@types/responselike@1.0.3': dependencies: '@types/node': 20.12.7 @@ -3381,96 +1816,66 @@ snapshots: '@types/node': 20.12.7 '@types/send': 0.17.4 - '@types/stack-utils@2.0.3': {} - '@types/uuid@9.0.8': {} - '@types/validator@13.11.9': {} - '@types/ws@8.5.10': dependencies: '@types/node': 20.12.7 - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.33': - dependencies: - '@types/yargs-parser': 21.0.3 - '@types/yauzl@2.10.3': dependencies: '@types/node': 20.12.7 optional: true - '@ungap/structured-clone@1.3.0': {} - - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - optional: true - - '@unrs/resolver-binding-android-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - optional: true + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - optional: true + '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@24.6.2)(terser@5.30.3)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.2(@types/node@24.6.2)(terser@5.30.3)(yaml@2.8.3) - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - optional: true + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 - '@unrs/resolver-binding-wasm32-wasi@1.11.1': + '@vitest/runner@3.2.4': dependencies: - '@napi-rs/wasm-runtime': 0.2.12 - optional: true + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - optional: true + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - optional: true + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - optional: true + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 accepts@1.3.8: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 - acorn@8.11.3: {} - - acorn@8.8.2: {} + acorn@8.11.3: + optional: true agent-base@6.0.2: dependencies: @@ -3478,113 +1883,20 @@ snapshots: transitivePeerDependencies: - supports-color - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 - ansi-regex@4.1.1: {} - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} - ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - ansi-styles@6.2.3: {} - anymatch@3.1.3: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - array-differ@3.0.0: {} - array-flatten@1.1.1: {} - array-union@2.1.0: {} - - arrify@2.0.1: {} - - assert@2.0.0: - dependencies: - es6-object-assign: 1.1.0 - is-nan: 1.3.2 - object-is: 1.1.6 - util: 0.12.5 - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.0.0 - - babel-jest@30.1.2(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - '@jest/transform': 30.1.2 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 7.0.1 - babel-preset-jest: 30.0.1(@babel/core@7.28.4) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-istanbul@7.0.1: - dependencies: - '@babel/helper-plugin-utils': 7.27.1 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 6.0.3 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-jest-hoist@30.0.1: - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - '@types/babel__core': 7.20.5 - - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4) - - babel-preset-jest@30.0.1(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - babel-plugin-jest-hoist: 30.0.1 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) - - balanced-match@1.0.2: {} - - baseline-browser-mapping@2.8.7: {} + assertion-error@2.0.1: {} binary-extensions@2.3.0: {} @@ -3608,44 +1920,14 @@ snapshots: boolean@3.2.0: optional: true - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 - braces@3.0.2: dependencies: fill-range: 7.0.1 - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.26.2: - dependencies: - baseline-browser-mapping: 2.8.7 - caniuse-lite: 1.0.30001745 - electron-to-chromium: 1.5.224 - node-releases: 2.0.21 - update-browserslist-db: 1.1.3(browserslist@4.26.2) - - bs-logger@0.2.6: - dependencies: - fast-json-stable-stringify: 2.1.0 - - bser@2.1.1: - dependencies: - node-int64: 0.4.0 - buffer-crc32@0.2.13: {} - buffer-from@1.1.2: {} - - builtin-modules@3.3.0: {} + buffer-from@1.1.2: + optional: true busboy@1.6.0: dependencies: @@ -3655,6 +1937,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + cacheable-lookup@5.0.4: {} cacheable-request@7.0.4: @@ -3675,26 +1959,19 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 - callsites@3.1.0: {} - camelcase@5.3.1: {} - camelcase@6.3.0: {} - - caniuse-lite@1.0.30001745: {} - case-anything@2.1.13: {} - chalk@4.1.2: + chai@5.3.3: dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - chance@1.1.9: {} - - char-regex@1.0.2: {} + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 - charenc@0.0.2: {} + check-error@2.1.3: {} chokidar-cli@3.0.0: dependencies: @@ -3715,55 +1992,24 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - ci-info@4.3.0: {} - - cjs-module-lexer@2.1.0: {} - - class-validator@0.14.0: - dependencies: - '@types/validator': 13.11.9 - libphonenumber-js: 1.10.60 - validator: 13.11.0 - cliui@5.0.0: dependencies: string-width: 3.1.0 strip-ansi: 5.2.0 wrap-ansi: 5.1.0 - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - clone-response@1.0.3: dependencies: mimic-response: 1.0.1 - co@4.6.0: {} - - collect-v8-coverage@1.0.2: {} - color-convert@1.9.3: dependencies: color-name: 1.1.3 - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} - - commander@10.0.0: {} - - commander@2.20.3: {} - - commondir@1.0.1: {} - - concat-map@0.0.1: {} + commander@2.20.3: + optional: true content-disposition@0.5.4: dependencies: @@ -3771,20 +2017,10 @@ snapshots: content-type@1.0.5: {} - convert-source-map@2.0.0: {} - cookie-signature@1.0.6: {} cookie@0.6.0: {} - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - crypt@0.0.2: {} - debug@2.6.9: dependencies: ms: 2.0.0 @@ -3793,17 +2029,17 @@ snapshots: dependencies: ms: 2.1.2 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decamelize@1.2.0: {} decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 - dedent@1.7.0: {} - - deep-is@0.1.4: {} - - deepmerge@4.3.1: {} + deep-eql@5.0.2: {} defer-to-connect@2.0.1: {} @@ -3818,6 +2054,7 @@ snapshots: define-data-property: 1.1.4 has-property-descriptors: 1.0.2 object-keys: 1.1.1 + optional: true depd@2.0.0: {} @@ -3825,8 +2062,6 @@ snapshots: detect-libc@1.0.3: {} - detect-newline@3.1.0: {} - detect-node@2.1.0: optional: true @@ -3834,12 +2069,8 @@ snapshots: dependencies: detect-libc: 1.0.3 - eastasianwidth@0.2.0: {} - ee-first@1.1.1: {} - electron-to-chromium@1.5.224: {} - electron@25.8.1: dependencies: '@electron/get': 2.0.3 @@ -3848,14 +2079,8 @@ snapshots: transitivePeerDependencies: - supports-color - emittery@0.13.1: {} - emoji-regex@7.0.3: {} - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - encodeurl@1.0.2: {} end-of-stream@1.4.4: @@ -3864,75 +2089,60 @@ snapshots: env-paths@2.2.1: {} - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - es-define-property@1.0.0: dependencies: get-intrinsic: 1.2.4 es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es6-error@4.1.1: optional: true - es6-object-assign@1.1.0: {} - - escalade@3.2.0: {} + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 escape-html@1.0.3: {} - escape-string-regexp@2.0.0: {} - escape-string-regexp@4.0.0: optional: true - eslint-scope@7.1.1: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.3.0: {} - - esprima@4.0.1: {} - - esrecurse@4.3.0: + estree-walker@3.0.3: dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@2.0.2: {} - - esutils@2.0.3: {} + '@types/estree': 1.0.5 etag@1.8.1: {} eventemitter3@4.0.7: {} - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - exit-x@0.2.2: {} - - expect@30.1.2: - dependencies: - '@jest/expect-utils': 30.1.2 - '@jest/get-type': 30.1.0 - jest-matcher-utils: 30.1.2 - jest-message-util: 30.1.0 - jest-mock: 30.0.5 - jest-util: 30.0.5 + expect-type@1.3.0: {} express-fileupload@1.5.0: dependencies: @@ -3984,30 +2194,24 @@ snapshots: transitivePeerDependencies: - supports-color - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fb-watchman@2.0.2: - dependencies: - bser: 2.1.1 - fd-slicer@1.1.0: dependencies: pend: 1.2.0 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + file-type-mime@0.3.10: {} fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - finalhandler@1.2.0: dependencies: debug: 2.6.9 @@ -4024,24 +2228,10 @@ snapshots: dependencies: locate-path: 3.0.0 - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - follow-redirects@1.15.6(debug@4.3.4): optionalDependencies: debug: 4.3.4 - for-each@0.3.3: - dependencies: - is-callable: 1.2.7 - - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - forwarded@0.2.0: {} fresh@0.5.2: {} @@ -4052,15 +2242,11 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true function-bind@1.1.2: {} - gensync@1.0.0-beta.2: {} - get-caller-file@2.0.5: {} get-intrinsic@1.2.4: @@ -4071,44 +2257,14 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 - get-package-type@0.1.0: {} - get-stream@5.2.0: dependencies: pump: 3.0.0 - get-stream@6.0.1: {} - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 - glob@10.4.5: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - glob@8.1.0: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 5.1.6 - once: 1.4.0 - global-agent@3.0.0: dependencies: boolean: 3.2.0 @@ -4144,17 +2300,6 @@ snapshots: graceful-fs@4.2.11: {} - handlebars@4.7.8: - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.19.3 - - has-flag@4.0.0: {} - has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.0 @@ -4163,573 +2308,86 @@ snapshots: has-symbols@1.0.3: {} - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.0.3 - hasown@2.0.2: dependencies: function-bind: 1.1.2 - html-escaper@2.0.2: {} - - http-cache-semantics@4.1.1: {} - - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - - http-proxy-middleware@3.0.0: - dependencies: - '@types/http-proxy': 1.17.14 - debug: 4.3.4 - http-proxy: 1.18.1(debug@4.3.4) - is-glob: 4.0.3 - is-plain-obj: 3.0.0 - micromatch: 4.0.5 - transitivePeerDependencies: - - supports-color - - http-proxy@1.18.1(debug@4.3.4): - dependencies: - eventemitter3: 4.0.7 - follow-redirects: 1.15.6(debug@4.3.4) - requires-port: 1.0.0 - transitivePeerDependencies: - - debug - - http2-wrapper@1.0.3: - dependencies: - quick-lru: 5.1.1 - resolve-alpn: 1.2.1 - - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - - human-signals@2.1.0: {} - - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - - image-size@1.1.1: - dependencies: - queue: 6.0.2 - - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - inversify@6.0.1: {} - - ipaddr.js@1.9.1: {} - - is-arguments@1.1.1: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - - is-arrayish@0.2.1: {} - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-buffer@1.1.6: {} - - is-builtin-module@3.2.1: - dependencies: - builtin-modules: 3.3.0 - - is-callable@1.2.7: {} - - is-core-module@2.13.1: - dependencies: - hasown: 2.0.2 - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@2.0.0: {} - - is-fullwidth-code-point@3.0.0: {} - - is-generator-fn@2.1.0: {} - - is-generator-function@1.0.10: - dependencies: - has-tostringtag: 1.0.2 - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-module@1.0.0: {} - - is-nan@1.3.2: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - - is-number@7.0.0: {} - - is-plain-obj@3.0.0: {} - - is-reference@1.2.1: - dependencies: - '@types/estree': 1.0.5 - - is-stream@2.0.1: {} - - is-typed-array@1.1.13: - dependencies: - which-typed-array: 1.1.15 - - isexe@2.0.0: {} - - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-instrument@6.0.3: - dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 7.6.0 - transitivePeerDependencies: - - supports-color - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-lib-source-maps@5.0.6: - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - debug: 4.3.4 - istanbul-lib-coverage: 3.2.2 - transitivePeerDependencies: - - supports-color - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - javascript-obfuscator@4.1.0: - dependencies: - '@javascript-obfuscator/escodegen': 2.3.0 - '@javascript-obfuscator/estraverse': 5.4.0 - acorn: 8.8.2 - assert: 2.0.0 - chalk: 4.1.2 - chance: 1.1.9 - class-validator: 0.14.0 - commander: 10.0.0 - eslint-scope: 7.1.1 - eslint-visitor-keys: 3.3.0 - fast-deep-equal: 3.1.3 - inversify: 6.0.1 - js-string-escape: 1.0.1 - md5: 2.3.0 - mkdirp: 2.1.3 - multimatch: 5.0.0 - opencollective-postinstall: 2.0.3 - process: 0.11.10 - reflect-metadata: 0.1.13 - source-map-support: 0.5.21 - string-template: 1.0.0 - stringz: 2.1.0 - tslib: 2.5.0 - - jest-changed-files@30.0.5: - dependencies: - execa: 5.1.1 - jest-util: 30.0.5 - p-limit: 3.1.0 - - jest-circus@30.1.3: - dependencies: - '@jest/environment': 30.1.2 - '@jest/expect': 30.1.2 - '@jest/test-result': 30.1.3 - '@jest/types': 30.0.5 - '@types/node': 20.12.7 - chalk: 4.1.2 - co: 4.6.0 - dedent: 1.7.0 - is-generator-fn: 2.1.0 - jest-each: 30.1.0 - jest-matcher-utils: 30.1.2 - jest-message-util: 30.1.0 - jest-runtime: 30.1.3 - jest-snapshot: 30.1.2 - jest-util: 30.0.5 - p-limit: 3.1.0 - pretty-format: 30.0.5 - pure-rand: 7.0.1 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-cli@30.1.3(@types/node@24.6.2): - dependencies: - '@jest/core': 30.1.3 - '@jest/test-result': 30.1.3 - '@jest/types': 30.0.5 - chalk: 4.1.2 - exit-x: 0.2.2 - import-local: 3.2.0 - jest-config: 30.1.3(@types/node@24.6.2) - jest-util: 30.0.5 - jest-validate: 30.1.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - - jest-config@30.1.3(@types/node@20.12.7): - dependencies: - '@babel/core': 7.28.4 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.1.3 - '@jest/types': 30.0.5 - babel-jest: 30.1.2(@babel/core@7.28.4) - chalk: 4.1.2 - ci-info: 4.3.0 - deepmerge: 4.3.1 - glob: 10.4.5 - graceful-fs: 4.2.11 - jest-circus: 30.1.3 - jest-docblock: 30.0.1 - jest-environment-node: 30.1.2 - jest-regex-util: 30.0.1 - jest-resolve: 30.1.3 - jest-runner: 30.1.3 - jest-util: 30.0.5 - jest-validate: 30.1.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 30.0.5 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.12.7 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@30.1.3(@types/node@24.6.2): - dependencies: - '@babel/core': 7.28.4 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.1.3 - '@jest/types': 30.0.5 - babel-jest: 30.1.2(@babel/core@7.28.4) - chalk: 4.1.2 - ci-info: 4.3.0 - deepmerge: 4.3.1 - glob: 10.4.5 - graceful-fs: 4.2.11 - jest-circus: 30.1.3 - jest-docblock: 30.0.1 - jest-environment-node: 30.1.2 - jest-regex-util: 30.0.1 - jest-resolve: 30.1.3 - jest-runner: 30.1.3 - jest-util: 30.0.5 - jest-validate: 30.1.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 30.0.5 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 24.6.2 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-diff@30.1.2: - dependencies: - '@jest/diff-sequences': 30.0.1 - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - pretty-format: 30.0.5 - - jest-docblock@30.0.1: - dependencies: - detect-newline: 3.1.0 - - jest-each@30.1.0: - dependencies: - '@jest/get-type': 30.1.0 - '@jest/types': 30.0.5 - chalk: 4.1.2 - jest-util: 30.0.5 - pretty-format: 30.0.5 - - jest-environment-node@30.1.2: - dependencies: - '@jest/environment': 30.1.2 - '@jest/fake-timers': 30.1.2 - '@jest/types': 30.0.5 - '@types/node': 20.12.7 - jest-mock: 30.0.5 - jest-util: 30.0.5 - jest-validate: 30.1.0 - - jest-haste-map@30.1.0: - dependencies: - '@jest/types': 30.0.5 - '@types/node': 20.12.7 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 30.0.1 - jest-util: 30.0.5 - jest-worker: 30.1.0 - micromatch: 4.0.8 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 - - jest-leak-detector@30.1.0: - dependencies: - '@jest/get-type': 30.1.0 - pretty-format: 30.0.5 - - jest-matcher-utils@30.1.2: - dependencies: - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - jest-diff: 30.1.2 - pretty-format: 30.0.5 - - jest-message-util@30.1.0: - dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 30.0.5 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 30.0.5 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@30.0.5: - dependencies: - '@jest/types': 30.0.5 - '@types/node': 20.12.7 - jest-util: 30.0.5 - - jest-pnp-resolver@1.2.3(jest-resolve@30.1.3): - optionalDependencies: - jest-resolve: 30.1.3 + http-cache-semantics@4.1.1: {} - jest-regex-util@30.0.1: {} + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 - jest-resolve-dependencies@30.1.3: + http-proxy-middleware@3.0.0: dependencies: - jest-regex-util: 30.0.1 - jest-snapshot: 30.1.2 + '@types/http-proxy': 1.17.14 + debug: 4.3.4 + http-proxy: 1.18.1(debug@4.3.4) + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.5 transitivePeerDependencies: - supports-color - jest-resolve@30.1.3: + http-proxy@1.18.1(debug@4.3.4): dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 30.1.0 - jest-pnp-resolver: 1.2.3(jest-resolve@30.1.3) - jest-util: 30.0.5 - jest-validate: 30.1.0 - slash: 3.0.0 - unrs-resolver: 1.11.1 - - jest-runner@30.1.3: - dependencies: - '@jest/console': 30.1.2 - '@jest/environment': 30.1.2 - '@jest/test-result': 30.1.3 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - '@types/node': 20.12.7 - chalk: 4.1.2 - emittery: 0.13.1 - exit-x: 0.2.2 - graceful-fs: 4.2.11 - jest-docblock: 30.0.1 - jest-environment-node: 30.1.2 - jest-haste-map: 30.1.0 - jest-leak-detector: 30.1.0 - jest-message-util: 30.1.0 - jest-resolve: 30.1.3 - jest-runtime: 30.1.3 - jest-util: 30.0.5 - jest-watcher: 30.1.3 - jest-worker: 30.1.0 - p-limit: 3.1.0 - source-map-support: 0.5.13 + eventemitter3: 4.0.7 + follow-redirects: 1.15.6(debug@4.3.4) + requires-port: 1.0.0 transitivePeerDependencies: - - supports-color + - debug - jest-runtime@30.1.3: + http2-wrapper@1.0.3: dependencies: - '@jest/environment': 30.1.2 - '@jest/fake-timers': 30.1.2 - '@jest/globals': 30.1.2 - '@jest/source-map': 30.0.1 - '@jest/test-result': 30.1.3 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - '@types/node': 20.12.7 - chalk: 4.1.2 - cjs-module-lexer: 2.1.0 - collect-v8-coverage: 1.0.2 - glob: 10.4.5 - graceful-fs: 4.2.11 - jest-haste-map: 30.1.0 - jest-message-util: 30.1.0 - jest-mock: 30.0.5 - jest-regex-util: 30.0.1 - jest-resolve: 30.1.3 - jest-snapshot: 30.1.2 - jest-util: 30.0.5 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 - jest-snapshot@30.1.2: - dependencies: - '@babel/core': 7.28.4 - '@babel/generator': 7.28.3 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) - '@babel/types': 7.28.4 - '@jest/expect-utils': 30.1.2 - '@jest/get-type': 30.1.0 - '@jest/snapshot-utils': 30.1.2 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) - chalk: 4.1.2 - expect: 30.1.2 - graceful-fs: 4.2.11 - jest-diff: 30.1.2 - jest-matcher-utils: 30.1.2 - jest-message-util: 30.1.0 - jest-util: 30.0.5 - pretty-format: 30.0.5 - semver: 7.7.2 - synckit: 0.11.11 + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 transitivePeerDependencies: - supports-color - jest-util@30.0.5: + iconv-lite@0.4.24: dependencies: - '@jest/types': 30.0.5 - '@types/node': 20.12.7 - chalk: 4.1.2 - ci-info: 4.3.0 - graceful-fs: 4.2.11 - picomatch: 4.0.3 + safer-buffer: 2.1.2 - jest-validate@30.1.0: + image-size@1.1.1: dependencies: - '@jest/get-type': 30.1.0 - '@jest/types': 30.0.5 - camelcase: 6.3.0 - chalk: 4.1.2 - leven: 3.1.0 - pretty-format: 30.0.5 + queue: 6.0.2 - jest-watcher@30.1.3: - dependencies: - '@jest/test-result': 30.1.3 - '@jest/types': 30.0.5 - '@types/node': 20.12.7 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 30.0.5 - string-length: 4.0.2 + inherits@2.0.4: {} - jest-worker@30.1.0: - dependencies: - '@types/node': 20.12.7 - '@ungap/structured-clone': 1.3.0 - jest-util: 30.0.5 - merge-stream: 2.0.0 - supports-color: 8.1.1 + ipaddr.js@1.9.1: {} - jest@30.1.3(@types/node@24.6.2): + is-binary-path@2.1.0: dependencies: - '@jest/core': 30.1.3 - '@jest/types': 30.0.5 - import-local: 3.2.0 - jest-cli: 30.1.3(@types/node@24.6.2) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node + binary-extensions: 2.3.0 - js-string-escape@1.0.1: {} + is-extglob@2.1.1: {} - js-tokens@4.0.0: {} + is-fullwidth-code-point@2.0.0: {} - js-yaml@3.14.1: + is-glob@4.0.3: dependencies: - argparse: 1.0.10 - esprima: 4.0.1 + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-plain-obj@3.0.0: {} - jsesc@3.1.0: {} + js-tokens@9.0.1: {} json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: {} - json-stringify-safe@5.0.1: optional: true - json5@2.2.3: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -4738,75 +2396,37 @@ snapshots: dependencies: json-buffer: 3.0.1 - leven@3.1.0: {} - - levn@0.3.0: - dependencies: - prelude-ls: 1.1.2 - type-check: 0.3.2 - - libphonenumber-js@1.10.60: {} - - lines-and-columns@1.2.4: {} - locate-path@3.0.0: dependencies: p-locate: 3.0.0 path-exists: 3.0.0 - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - lodash.debounce@4.0.8: {} - lodash.memoize@4.1.2: {} - lodash.throttle@4.1.1: {} - lowercase-keys@2.0.0: {} - - lru-cache@10.4.3: {} + loupe@3.2.1: {} - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 + lowercase-keys@2.0.0: {} lru-cache@6.0.0: dependencies: yallist: 4.0.0 + optional: true - magic-string@0.30.9: - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - - make-dir@4.0.0: - dependencies: - semver: 7.6.0 - - make-error@1.3.6: {} - - makeerror@1.0.12: + magic-string@0.30.21: dependencies: - tmpl: 1.0.5 + '@jridgewell/sourcemap-codec': 1.5.5 matcher@3.0.0: dependencies: escape-string-regexp: 4.0.0 optional: true - md5@2.3.0: - dependencies: - charenc: 0.0.2 - crypt: 0.0.2 - is-buffer: 1.1.6 - media-typer@0.3.0: {} merge-descriptors@1.0.1: {} - merge-stream@2.0.0: {} - methods@1.1.2: {} micromatch@4.0.5: @@ -4814,11 +2434,6 @@ snapshots: braces: 3.0.2 picomatch: 2.3.1 - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - mime-db@1.52.0: {} mime-types@2.1.35: @@ -4827,56 +2442,20 @@ snapshots: mime@1.6.0: {} - mimic-fn@2.1.0: {} - mimic-response@1.0.1: {} mimic-response@3.1.0: {} - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.1 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.1 - - minimist@1.2.8: {} - - minipass@7.1.2: {} - - mkdirp@2.1.3: {} - ms@2.0.0: {} ms@2.1.2: {} ms@2.1.3: {} - multimatch@5.0.0: - dependencies: - '@types/minimatch': 3.0.5 - array-differ: 3.0.0 - array-union: 2.1.0 - arrify: 2.0.1 - minimatch: 3.1.2 - - napi-postinstall@0.3.3: {} - - natural-compare@1.4.0: {} + nanoid@3.3.11: {} negotiator@0.6.3: {} - neo-async@2.6.2: {} - - node-int64@0.4.0: {} - - node-releases@2.0.21: {} - nodejs-file-downloader@4.13.0: dependencies: follow-redirects: 1.15.6(debug@4.3.4) @@ -4891,18 +2470,10 @@ snapshots: normalize-url@6.1.0: {} - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - object-inspect@1.13.1: {} - object-is@1.1.6: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - - object-keys@1.1.1: {} + object-keys@1.1.1: + optional: true on-finished@2.4.1: dependencies: @@ -4912,68 +2483,27 @@ snapshots: dependencies: wrappy: 1.0.2 - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - opencollective-postinstall@2.0.3: {} - - optionator@0.8.3: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.3.0 - prelude-ls: 1.1.2 - type-check: 0.3.2 - word-wrap: 1.2.5 - p-cancelable@2.1.1: {} p-limit@2.3.0: dependencies: p-try: 2.2.0 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - p-locate@3.0.0: dependencies: p-limit: 2.3.0 - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - p-try@2.2.0: {} - package-json-from-dist@1.0.1: {} - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - parseurl@1.3.3: {} path-exists@3.0.0: {} - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} + path-to-regexp@0.1.7: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 + pathe@2.0.3: {} - path-to-regexp@0.1.7: {} + pathval@2.0.1: {} pend@1.2.0: {} @@ -4983,23 +2513,13 @@ snapshots: picomatch@4.0.3: {} - pirates@4.0.7: {} - - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - - possible-typed-array-names@1.0.0: {} - - prelude-ls@1.1.2: {} + picomatch@4.0.4: {} - pretty-format@30.0.5: + postcss@8.5.10: dependencies: - '@jest/schemas': 30.0.5 - ansi-styles: 5.2.0 - react-is: 18.3.1 - - process@0.11.10: {} + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 progress@2.0.3: {} @@ -5015,8 +2535,6 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 - pure-rand@7.0.1: {} - qs@6.11.0: dependencies: side-channel: 1.0.6 @@ -5027,10 +2545,6 @@ snapshots: quick-lru@5.1.1: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - range-parser@1.2.1: {} raw-body@2.5.2: @@ -5040,14 +2554,10 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - react-is@18.3.1: {} - readdirp@3.6.0: dependencies: picomatch: 2.3.1 - reflect-metadata@0.1.13: {} - require-directory@2.1.1: {} require-main-filename@2.0.0: {} @@ -5056,18 +2566,6 @@ snapshots: resolve-alpn@1.2.1: {} - resolve-cwd@3.0.0: - dependencies: - resolve-from: 5.0.0 - - resolve-from@5.0.0: {} - - resolve@1.22.8: - dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - responselike@2.0.1: dependencies: lowercase-keys: 2.0.0 @@ -5082,32 +2580,35 @@ snapshots: sprintf-js: 1.1.3 optional: true - rollup-plugin-obfuscator@1.1.0(javascript-obfuscator@4.1.0)(rollup@4.14.3): - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.14.3) - javascript-obfuscator: 4.1.0 - rollup: 4.14.3 - - rollup@4.14.3: + rollup@4.60.1: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.14.3 - '@rollup/rollup-android-arm64': 4.14.3 - '@rollup/rollup-darwin-arm64': 4.14.3 - '@rollup/rollup-darwin-x64': 4.14.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.14.3 - '@rollup/rollup-linux-arm-musleabihf': 4.14.3 - '@rollup/rollup-linux-arm64-gnu': 4.14.3 - '@rollup/rollup-linux-arm64-musl': 4.14.3 - '@rollup/rollup-linux-powerpc64le-gnu': 4.14.3 - '@rollup/rollup-linux-riscv64-gnu': 4.14.3 - '@rollup/rollup-linux-s390x-gnu': 4.14.3 - '@rollup/rollup-linux-x64-gnu': 4.14.3 - '@rollup/rollup-linux-x64-musl': 4.14.3 - '@rollup/rollup-win32-arm64-msvc': 4.14.3 - '@rollup/rollup-win32-ia32-msvc': 4.14.3 - '@rollup/rollup-win32-x64-msvc': 4.14.3 + '@rollup/rollup-android-arm-eabi': 4.60.1 + '@rollup/rollup-android-arm64': 4.60.1 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-freebsd-arm64': 4.60.1 + '@rollup/rollup-freebsd-x64': 4.60.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 + '@rollup/rollup-linux-arm-musleabihf': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-arm64-musl': 4.60.1 + '@rollup/rollup-linux-loong64-gnu': 4.60.1 + '@rollup/rollup-linux-loong64-musl': 4.60.1 + '@rollup/rollup-linux-ppc64-gnu': 4.60.1 + '@rollup/rollup-linux-ppc64-musl': 4.60.1 + '@rollup/rollup-linux-riscv64-gnu': 4.60.1 + '@rollup/rollup-linux-riscv64-musl': 4.60.1 + '@rollup/rollup-linux-s390x-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-musl': 4.60.1 + '@rollup/rollup-openbsd-x64': 4.60.1 + '@rollup/rollup-openharmony-arm64': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-ia32-msvc': 4.60.1 + '@rollup/rollup-win32-x64-gnu': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 safe-buffer@5.2.1: {} @@ -5126,8 +2627,7 @@ snapshots: semver@7.6.0: dependencies: lru-cache: 6.0.0 - - semver@7.7.2: {} + optional: true send@0.18.0: dependencies: @@ -5152,10 +2652,6 @@ snapshots: type-fest: 0.13.1 optional: true - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - serve-static@1.15.0: dependencies: encodeurl: 1.0.2 @@ -5178,12 +2674,6 @@ snapshots: setprototypeof@1.2.0: {} - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - side-channel@1.0.6: dependencies: call-bind: 1.0.7 @@ -5191,45 +2681,29 @@ snapshots: get-intrinsic: 1.2.4 object-inspect: 1.13.1 - signal-exit@3.0.7: {} - - signal-exit@4.1.0: {} - - slash@3.0.0: {} - - smob@1.5.0: {} + siginfo@2.0.0: {} - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 + source-map-js@1.2.1: {} source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + optional: true - source-map@0.6.1: {} - - sprintf-js@1.0.3: {} + source-map@0.6.1: + optional: true sprintf-js@1.1.3: optional: true - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 + stackback@0.0.2: {} statuses@2.0.1: {} - streamsearch@1.1.0: {} - - string-length@4.0.2: - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 + std-env@3.10.0: {} - string-template@1.0.0: {} + streamsearch@1.1.0: {} string-width@3.1.0: dependencies: @@ -5237,39 +2711,13 @@ snapshots: is-fullwidth-code-point: 2.0.0 strip-ansi: 5.2.0 - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - - stringz@2.1.0: - dependencies: - char-regex: 1.0.2 - strip-ansi@5.2.0: dependencies: ansi-regex: 4.1.1 - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.2: + strip-literal@3.1.0: dependencies: - ansi-regex: 6.2.2 - - strip-bom@4.0.0: {} - - strip-final-newline@2.0.0: {} - - strip-json-comments@3.1.1: {} + js-tokens: 9.0.1 sumchecker@3.0.1: dependencies: @@ -5277,34 +2725,28 @@ snapshots: transitivePeerDependencies: - supports-color - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - synckit@0.11.11: - dependencies: - '@pkgr/core': 0.2.9 - terser@5.30.3: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.11.3 commander: 2.20.3 source-map-support: 0.5.21 + optional: true + + tinybench@2.9.0: {} - test-exclude@6.0.0: + tinyexec@0.3.2: {} + + tinyglobby@0.2.16: dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} - tmpl@1.0.5: {} + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} to-regex-range@5.0.1: dependencies: @@ -5316,26 +2758,6 @@ snapshots: dependencies: utf8-byte-length: 1.0.5 - ts-jest@29.4.4(@babel/core@7.28.4)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.28.4))(jest-util@30.0.5)(jest@30.1.3(@types/node@24.6.2))(typescript@5.4.5): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 30.1.3(@types/node@24.6.2) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.2 - type-fest: 4.41.0 - typescript: 5.4.5 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.4 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - babel-jest: 30.1.2(@babel/core@7.28.4) - jest-util: 30.0.5 - ts-poet@6.9.0: dependencies: dprint-node: 1.0.8 @@ -5351,23 +2773,11 @@ snapshots: ts-poet: 6.9.0 ts-proto-descriptors: 2.0.0 - tslib@2.5.0: {} - tslib@2.6.2: {} - type-check@0.3.2: - dependencies: - prelude-ls: 1.1.2 - - type-detect@4.0.8: {} - type-fest@0.13.1: optional: true - type-fest@0.21.3: {} - - type-fest@4.41.0: {} - type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -5375,9 +2785,6 @@ snapshots: typescript@5.4.5: {} - uglify-js@3.19.3: - optional: true - undici-types@5.26.5: {} undici-types@7.13.0: {} @@ -5386,81 +2793,96 @@ snapshots: unpipe@1.0.0: {} - unrs-resolver@1.11.1: - dependencies: - napi-postinstall: 0.3.3 - optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.1 - '@unrs/resolver-binding-android-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-x64': 1.11.1 - '@unrs/resolver-binding-freebsd-x64': 1.11.1 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-musl': 1.11.1 - '@unrs/resolver-binding-wasm32-wasi': 1.11.1 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - - update-browserslist-db@1.1.3(browserslist@4.26.2): - dependencies: - browserslist: 4.26.2 - escalade: 3.2.0 - picocolors: 1.1.1 - utf8-byte-length@1.0.5: {} - util@0.12.5: - dependencies: - inherits: 2.0.4 - is-arguments: 1.1.1 - is-generator-function: 1.0.10 - is-typed-array: 1.1.13 - which-typed-array: 1.1.15 - utils-merge@1.0.1: {} uuid@9.0.1: {} - v8-to-istanbul@9.3.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 - - validator@13.11.0: {} - vary@1.1.2: {} - walker@1.0.8: - dependencies: - makeerror: 1.0.12 - - which-module@2.0.1: {} - - which-typed-array@1.1.15: + vite-node@3.2.4(@types/node@24.6.2)(terser@5.30.3)(yaml@2.8.3): dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.2 + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.2(@types/node@24.6.2)(terser@5.30.3)(yaml@2.8.3) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml - which@2.0.2: + vite@7.3.2(@types/node@24.6.2)(terser@5.30.3)(yaml@2.8.3): dependencies: - isexe: 2.0.0 + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.10 + rollup: 4.60.1 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.6.2 + fsevents: 2.3.3 + terser: 5.30.3 + yaml: 2.8.3 + + vitest@3.2.4(@types/node@24.6.2)(terser@5.30.3)(yaml@2.8.3): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@24.6.2)(terser@5.30.3)(yaml@2.8.3)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.16 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.2(@types/node@24.6.2)(terser@5.30.3)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@24.6.2)(terser@5.30.3)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.6.2 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml - word-wrap@1.2.5: {} + which-module@2.0.1: {} - wordwrap@1.0.0: {} + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 wrap-ansi@5.1.0: dependencies: @@ -5468,44 +2890,22 @@ snapshots: string-width: 3.1.0 strip-ansi: 5.2.0 - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.1.2 - wrappy@1.0.2: {} - write-file-atomic@5.0.1: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 4.1.0 - ws@8.16.0: {} y18n@4.0.3: {} - y18n@5.0.8: {} - - yallist@3.1.1: {} - - yallist@4.0.0: {} + yallist@4.0.0: + optional: true - yaml@2.4.1: {} + yaml@2.8.3: {} yargs-parser@13.1.2: dependencies: camelcase: 5.3.1 decamelize: 1.2.0 - yargs-parser@21.1.1: {} - yargs@13.3.2: dependencies: cliui: 5.0.0 @@ -5519,19 +2919,7 @@ snapshots: y18n: 4.0.3 yargs-parser: 13.1.2 - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - yauzl@2.10.0: dependencies: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 - - yocto-queue@0.1.0: {} diff --git a/rollup.config.ts b/rollup.config.ts deleted file mode 100644 index c4df239..0000000 --- a/rollup.config.ts +++ /dev/null @@ -1,138 +0,0 @@ -// rollup.config.js -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import typescript from '@rollup/plugin-typescript'; -import json from '@rollup/plugin-json' -import terser from '@rollup/plugin-terser'; -import { InputPluginOption, RollupOptions } from "rollup"; -import fs from 'fs' -import obfuscator from 'rollup-plugin-obfuscator'; -import bytenode from 'bytenode' -import path from 'path'; - -const outputDir = process.env['outputDir'] || 'program' -const isDev = process.env.ENVIRONMENT !== 'production' -const options: RollupOptions[] = [ - // { - // // 此模块用于其它平台编译字节码 - // input: 'src/compile.ts', - // output: { - // dir: outputDir, - // format: 'cjs', - // }, - // plugins: [ - // resolve({ - // // 将自定义选项传递给解析插件 - // moduleDirectories: ['node_modules'], - // preferBuiltins: true, - // }), - // commonjs({ - // include: /node_modules/, - // requireReturnsDefault: 'auto', // <---- this solves default issue - // }), - // typescript(), - // // terser(), - // // obfuscator({ - // // global: true, - // // }), - // ], - // // 指出哪些模块应该视为外部模块 - // external: ['electron', 'module', 'ntwrapper'] - // }, - { - input: 'src/index.ts', - output: { - dir: outputDir, - // file: 'D:/GitHub/Yui-vscode/dist/program/resources/app/app_launcher/index.js', - format: 'cjs', - banner: (chunk) => { - // console.log('chunk:', chunk) - return fs.readFileSync('./tools/prepend.js').toString() - }, - }, - onwarn: function (warning, handler) { - // Skip certain warnings - - // should intercept ... but doesn't in some rollup versions - if (warning.code === 'THIS_IS_UNDEFINED') { return; } - if (warning.code === 'CIRCULAR_DEPENDENCY') { return; } - - // console.warn everything else - handler(warning); - }, - plugins: [ - resolve({ - // 将自定义选项传递给解析插件 - moduleDirectories: ['node_modules'], - preferBuiltins: true, - }), - commonjs({ - include: /node_modules/, - requireReturnsDefault: 'auto', // <---- this solves default issue - }), - typescript(), - json(), - // 压缩 - !isDev ? terser() : null, - // 混淆 - // obfuscator({ - // global: true, - // }), - // { - // name: 'jsc', - // writeBundle: (options, bundle) => { - // console.log('generate jsc...') - // if (bundle['core.js']) { - // const core = bundle['core.js'] - // // console.log(options) - // if (core.type === 'chunk' && options.dir) { - // const fromPath = path.resolve(options.dir, './core.js') - // console.log('compile file:', fromPath) - // // 字节码生成 - // bytenode.compileFile({ - // filename: fromPath, - // compileAsModule: true, - // electron: true, - // compress: true, - // electronPath: process.env['PROGRAM_PATH'], - // output: `${fromPath}c`, - // }) - // } - // } - // } - // } - ], - // 指出哪些模块应该视为外部模块 - external: ['module', 'ntwrapper', 'yui-native'], - }, - { - // UI hook 用于记录操作界面时调用的方法与参数 - input: 'src/ui.ts', - output: { - dir: 'dist', - format: 'cjs', - }, - plugins: [ - resolve({ - // 将自定义选项传递给解析插件 - moduleDirectories: ['node_modules'], - preferBuiltins: true, - }), - commonjs({ - include: /node_modules/, - requireReturnsDefault: 'auto', // <---- this solves default issue - }), - typescript(), - json(), - // 压缩 - // terser(), - // 混淆 - // obfuscator({ - // global: true, - // }), - ], - // 指出哪些模块应该视为外部模块 - external: ['electron', 'module'] - }, -]; -export default options \ No newline at end of file diff --git a/src/adapters/http/create-router.ts b/src/adapters/http/create-router.ts new file mode 100644 index 0000000..5903a97 --- /dev/null +++ b/src/adapters/http/create-router.ts @@ -0,0 +1,12 @@ +import { Router } from "express" +import { registerAppRoutes } from "./register-app-routes" +import { registerProxyRoutes } from "./register-proxy-routes" +import { registerStaticRoutes } from "./register-static-routes" + +export const createHTTPRouter = () => { + const router = Router() + registerStaticRoutes(router) + registerProxyRoutes(router) + registerAppRoutes(router) + return router +} diff --git a/src/adapters/http/download-rich-media.ts b/src/adapters/http/download-rich-media.ts new file mode 100644 index 0000000..085d2a6 --- /dev/null +++ b/src/adapters/http/download-rich-media.ts @@ -0,0 +1,45 @@ +import { NextFunction, Request, Response } from "express" +import { existsSync } from "fs" +import { useLogger } from "../../common/log" +import { getNTMsgService } from "../../ntqq/core/core" +import { registerEventListener } from "../../store/event-registry" +import { CustomError } from "../../common/error/custom-error" + +const log = useLogger("DownloadRichMedia") + +export const downloadRichMedia = (req: Request, res: Response, next: NextFunction) => { + const msgService = getNTMsgService() + const p = req.query + log.info("param:", JSON.stringify(p, null, 4)) + if (typeof p["file_path"] !== "string") { + next(new Error("file_path error")) + return + } + if (p["file_path"] && existsSync(p["file_path"])) { + res.download(p["file_path"]) + return + } + log.info("need download...") + const rm = registerEventListener("KernelMsgListener/onRichMediaDownloadComplete", "always", (a) => { + if (a.msgId !== p["msg_id"] || a.msgElementId !== p["element_id"]) return + rm.remove() + log.info("result:", a) + res.download(a.filePath) + }) + setTimeout(() => { + rm.remove() + next(new CustomError(10201, "timeout")) + }, 30000) + msgService.downloadRichMedia({ + peerUid: p["peer_uid"] as string, + chatType: parseInt(p["chat_type"] as string), + msgId: p["msg_id"] as `${number}`, + elementId: p["element_id"] as `${number}`, + downloadType: parseInt(p["download_type"] as string), + thumbSize: parseInt(p["chat_type"] as string), + filePath: p["file_path"], + fileModelId: p["file_model_id"] as `${number}`, + downSourceType: parseInt(p["down_source_type"] as string), + triggerType: parseInt(p["trigger_type"] as string), + }) +} diff --git a/src/adapters/http/external-download.ts b/src/adapters/http/external-download.ts new file mode 100644 index 0000000..13918f2 --- /dev/null +++ b/src/adapters/http/external-download.ts @@ -0,0 +1,38 @@ +import { NextFunction, Request, Response } from "express" +import { IncomingMessage, get as getHttp } from "http" +import { get as getHttps } from "https" +import { useLogger } from "../../common/log" + +const log = useLogger("ExternalDownload") + +export const externalDownload = (req: Request, res: Response, next: NextFunction) => { + log.info("external-download") + res.setHeader("X-ErrNo", 0) + const url = new URL(req.url, `http://${req.headers.host}`) + let link = url.searchParams.get("url") + if (link == null) { + res.status(400) + next() + return + } + link = decodeURIComponent(link) + const callback = (resp: IncomingMessage) => { + resp.pipe(res) + } + if (link.startsWith("https")) { + log.info("request https link:", link) + getHttps(link, callback).on("error", (err) => { + log.error("HTTP request error:", err) + res.status(500).send("Internal Server Error") + }) + return + } + if (link.startsWith("http")) { + getHttp(link, callback).on("error", (err) => { + log.error("HTTP request error:", err) + res.status(500).send("Internal Server Error") + }) + return + } + res.status(400).send("Bad Request: Invalid URL") +} diff --git a/src/adapters/http/register-app-routes.ts b/src/adapters/http/register-app-routes.ts new file mode 100644 index 0000000..9b9eb70 --- /dev/null +++ b/src/adapters/http/register-app-routes.ts @@ -0,0 +1,18 @@ +import { Router } from "express" +import { useLogger } from "../../common/log" +import { downloadRichMedia } from "./download-rich-media" +import { externalDownload } from "./external-download" +import { uploadFile } from "./upload-file" + +const log = useLogger('HTTP/AppRoutes') + +export const registerAppRoutes = (router: Router) => { + router.get('/downloadRichMedia', downloadRichMedia) + router.post('/group/uploadFile', uploadFile) + router.get('/external-download', externalDownload) + router.get('/test', (req, res, next) => { + log.info('test') + res.json({ data: 1 }) + next() + }) +} diff --git a/src/adapters/http/register-proxy-routes.ts b/src/adapters/http/register-proxy-routes.ts new file mode 100644 index 0000000..3262e3f --- /dev/null +++ b/src/adapters/http/register-proxy-routes.ts @@ -0,0 +1,13 @@ +import { Router } from "express" +import { createProxyMiddleware } from "http-proxy-middleware" + +export const registerProxyRoutes = (router: Router) => { + router.get('/download', createProxyMiddleware({ + target: 'https://gchat.qpic.cn', + changeOrigin: true, + })) + router.get('/gchatpic_new/*', createProxyMiddleware({ + target: 'https://gchat.qpic.cn', + changeOrigin: true, + })) +} diff --git a/src/adapters/http/register-static-routes.ts b/src/adapters/http/register-static-routes.ts new file mode 100644 index 0000000..88fb023 --- /dev/null +++ b/src/adapters/http/register-static-routes.ts @@ -0,0 +1,8 @@ +import { Router, static as static_ } from "express" +import { resolve } from "path" +import { getStoragePath } from "../../store/config" + +export const registerStaticRoutes = (router: Router) => { + const storage = getStoragePath() + router.use('/static', static_(resolve(storage, './upload'))) +} diff --git a/src/adapters/http/upload-file.ts b/src/adapters/http/upload-file.ts new file mode 100644 index 0000000..71ba8f8 --- /dev/null +++ b/src/adapters/http/upload-file.ts @@ -0,0 +1,57 @@ +import { NextFunction, Request, Response } from "express" +import fileUpload from "express-fileupload" +import fs from "fs" +import { resolve } from "path" +import { useLogger } from "../../common/log" +import { getConfig, getStoragePath } from "../../store/config" + +const log = useLogger("UploadFile") + +const mimeType2Extension = (type: string) => { + const m: Record = { + "image/jpeg": ".jpg", + "image/png": ".png", + } + if (m[type] != undefined) return m[type] + return "bin" +} + +export const uploadFile = (req: Request, res: Response, next: NextFunction) => { + const storage = getStoragePath() + + if (!req.files) { + next(new Error("No file found!")) + return + } + log.info("req:", req.url, "files:", req.files) + const file = req.files.file as fileUpload.UploadedFile + + if (!file) { + next(new Error('Parameter "file" is lost!')) + return + } + if (!file.name) { + next(new Error("Can not identify name!")) + return + } + const now = new Date() + const p = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}` + const realPath = resolve(storage, `./upload/${p}`) + try { + fs.mkdirSync(realPath, { recursive: true }) + } catch (_error) {} + log.info("mime type:", file.mimetype) + const ext = mimeType2Extension(file.mimetype) + file.mv(`${realPath}/${file.md5}${ext}`) + const cfg = getConfig() + let host = cfg.yui.http.host + if (host === "0.0.0.0") { + host = "127.0.0.1" + } + res.json({ + path: `${realPath}/${file.md5}${ext}`, + url: `http://${host}:${cfg.yui.http.port}/static/${p}/${file.md5}${ext}`, + md5: file.md5, + size: file.size, + }) +} diff --git a/src/adapters/onebot/account-event-builders.ts b/src/adapters/onebot/account-event-builders.ts new file mode 100644 index 0000000..9cb0824 --- /dev/null +++ b/src/adapters/onebot/account-event-builders.ts @@ -0,0 +1,20 @@ +import { getCurrentNTSelfOrNull } from "../../ntqq/core/runtime-store" +import { KickedOfflinePayload } from "../../ntqq/types/listener/msg" +import { EventDataType } from "../../onebot/contracts/event" + +export const buildKickedOfflineEvent = (payload: KickedOfflinePayload): EventDataType<{ p1: KickedOfflinePayload }> | null => { + const self = getCurrentNTSelfOrNull() + if (!self) { + return null + } + return { + self, + time: Date.now(), + type: "notice", + detailType: 'account.kickedOffline', + subType: "", + data: { + p1: payload, + } + } +} diff --git a/src/adapters/onebot/login-event-builders.ts b/src/adapters/onebot/login-event-builders.ts new file mode 100644 index 0000000..b9a8247 --- /dev/null +++ b/src/adapters/onebot/login-event-builders.ts @@ -0,0 +1,122 @@ +import { NTLogin } from "../../ntqq/login/interfaces" +import { getCurrentNTSelfOrNull } from "../../ntqq/core/runtime-store" +import { EventDataType } from "../../onebot/contracts/event" + +const requireCurrentSelf = (uid: `u_${string}` = 'u_0') => { + return getCurrentNTSelfOrNull(uid) +} + +export const buildQRCodeFetchEvent = (payload: NTLogin.GetQRCodeResponse): EventDataType | null => { + const self = requireCurrentSelf() + if (!self) return null + return { + self, + time: Date.now(), + type: "notice", + detailType: "login.qrcode.fetch", + subType: "", + data: payload + } +} + +export const buildQRCodeFailedEvent = (p1: number, p2: number): EventDataType<{ p1: number, p2: number }> | null => { + const self = requireCurrentSelf() + if (!self) return null + let detailType = 'login.qrcode.timeout' + if (p2 === 5) { + detailType = 'login.qrcode.refused' + } + return { + self, + time: Date.now(), + type: "notice", + detailType, + subType: "", + data: { p1, p2 } + } +} + +export const buildQRCodeScannedEvent = (p1: number, avatarUrl: string): EventDataType<{ p1: number, avatarUrl: string }> | null => { + const self = requireCurrentSelf() + if (!self) return null + return { + self, + time: Date.now(), + type: "notice", + detailType: "login.qrcode.scanned", + subType: "", + data: { p1, avatarUrl } + } +} + +export const buildQRCodeSuccessEvent = (userInfo: { uin: string, uid: `u_${string}`, account: string, nickName: string, faceUrl: string }) => { + return { + self: { + id: parseInt(userInfo.uin), + uid: userInfo.uid + }, + time: Date.now(), + type: "notice" as const, + detailType: "login.qrcode.success", + subType: "", + data: { + account: userInfo.account, + uin: userInfo.uin, + uid: userInfo.uid, + nick_name: userInfo.nickName, + face_url: userInfo.faceUrl, + } + } +} + +export const buildAlreadyLoginEvent = (uin: number) => { + return { + self: { + id: 0, + uid: 'u_' as const + }, + time: Date.now(), + type: "notice" as const, + detailType: "login.qrcode.alreadyLogin", + subType: "", + data: { uin } + } +} + +export const buildDeviceConfirmStatusEvent = (errCode: number): EventDataType | null => { + const self = requireCurrentSelf('u_') + if (!self) return null + let detailType = "login.account.deviceConfirm" + if (errCode === 5) { + detailType = 'login.account.deviceConfirm.cancel' + } + else if (errCode === 9) { + detailType = 'login.account.deviceConfirm.rejected' + } + else if (errCode === 10) { + detailType = 'login.account.deviceConfirm.timeout' + } else { + detailType = 'login.account.deviceConfirm.unknown' + } + return { + self, + time: Date.now(), + type: "notice", + detailType, + subType: "", + data: errCode + } +} + +export const buildDeviceConfirmSuccessEvent = (state: number): EventDataType | null => { + const self = requireCurrentSelf('u_') + if (!self) return null + return { + self, + time: Date.now(), + type: "notice", + detailType: "login.account.deviceConfirm.success", + subType: "", + data: state + } +} diff --git a/src/adapters/onebot/message-event-builders.ts b/src/adapters/onebot/message-event-builders.ts new file mode 100644 index 0000000..2d87102 --- /dev/null +++ b/src/adapters/onebot/message-event-builders.ts @@ -0,0 +1,111 @@ +import { convertNTMsg2BotMsg, convertNTMsgElement2BotMsgElement } from "../../transfer/message/convert" +import { NTReceiveMessageType } from "../../ntqq/message/interfaces" +import { BotMessageData, EventDataType, NudgeMessageData, RecallMessageData } from "../../onebot/contracts/event" +import { buildParsedNudgeEvent, hasRecallMessageEvent } from "./message-event-parsers" +import { + applyMessageEventChatContext, + hydrateMentionUin, + resolveCurrentEventSelf, + resolveSenderUin, +} from "./message-event-resolvers" + +export const isExpiredMessage = (msgTime: string) => Date.now() / 1000 - parseInt(msgTime) > 60 + +export const isUnsupportedChatType = (chatType: number) => chatType === 4 + +export const shouldSkipMessageEvent = (msg: Pick) => { + return isExpiredMessage(msg.msgTime) || isUnsupportedChatType(msg.chatType) +} + +export { hasRecallMessageEvent } + +const buildBaseMessageEventData = ( + msg: NTReceiveMessageType.NTMessageItemType, + senderId: number, +): BotMessageData => ({ + messageId: msg.msgId, + messageSeq: msg.msgSeq, + groupId: 0, + groupName: '', + senderId, + senderUid: msg.senderUid, + senderMemberName: msg.sendMemberName, + time: parseInt(msg.msgTime), + elements: [], + records: [], +}) + +export const buildMessageEvent = async (msg: NTReceiveMessageType.NTMessageItemType): Promise> => { + const senderId = await resolveSenderUin(msg.senderUid) + const ret: EventDataType = { + self: resolveCurrentEventSelf(), + time: parseInt(msg.msgTime), + type: "message", + detailType: "", + subType: "", + data: buildBaseMessageEventData(msg, senderId), + } + ret.detailType = applyMessageEventChatContext(msg, ret) + ret.data.elements = convertNTMsgElement2BotMsgElement({ + chatType: msg.chatType, + peerUid: msg.peerUid, + guildId: '' + }, msg.msgId, msg.elements) + await hydrateMentionUin(msg, ret.data.elements) + ret.data.records = msg.records.map(e => convertNTMsg2BotMsg(e)) + return ret +} + +export const buildPostSendEvent = async (msg: NTReceiveMessageType.NTMessageItemType): Promise> => { + const senderId = await resolveSenderUin(msg.senderUid) + const ret: EventDataType = { + self: resolveCurrentEventSelf(), + time: parseInt(msg.msgTime), + type: "notice", + detailType: "group_message_post_send", + subType: "", + data: buildBaseMessageEventData(msg, senderId), + } + applyMessageEventChatContext(msg, ret) + ret.data.elements = convertNTMsgElement2BotMsgElement({ + chatType: msg.chatType, + peerUid: msg.peerUid, + guildId: '' + }, msg.msgId, msg.elements) + ret.data.records = msg.records.map(e => convertNTMsg2BotMsg(e)) + return ret +} + +export const buildRecallEvent = async (msg: NTReceiveMessageType.NTMessageItemType): Promise> => { + const senderId = await resolveSenderUin(msg.senderUid) + const ret: EventDataType = { + self: resolveCurrentEventSelf(), + time: parseInt(msg.msgTime), + type: "notice", + detailType: "group_message_delete", + subType: "recall", + data: { + messageId: msg.msgId, + messageSeq: msg.msgSeq, + groupId: 0, + senderId, + senderUid: msg.senderUid, + senderMemberName: msg.sendMemberName || msg.sendNickName, + time: parseInt(msg.recallTime), + } + } + switch (msg.chatType) { + case 1: + ret.detailType = 'private_message_delete' + break + case 2: + ret.detailType = 'group_message_delete' + applyMessageEventChatContext(msg, ret) + break + } + return ret +} + +export const buildNudgeEvent = async (msg: NTReceiveMessageType.NTMessageItemType): Promise | null> => { + return buildParsedNudgeEvent(msg) +} diff --git a/src/adapters/onebot/message-event-parsers.ts b/src/adapters/onebot/message-event-parsers.ts new file mode 100644 index 0000000..4ccb11f --- /dev/null +++ b/src/adapters/onebot/message-event-parsers.ts @@ -0,0 +1,83 @@ +import { EventDataType, NudgeMessageData } from "../../onebot/contracts/event" +import { NTReceiveMessageType } from "../../ntqq/message/interfaces" +import { applyMessageEventChatContext, resolveCurrentEventSelf, resolveNudgeTargets } from "./message-event-resolvers" + +export const hasRecallMessageEvent = (msg: NTReceiveMessageType.NTMessageItemType) => { + return msg.elements.some((element) => element.elementType === 8 && element.grayTipElement?.subElementType === 1) +} + +type NudgeEventPayload = { + fromUid: `u_${string}` + toUid: `u_${string}` +} + +const parseNudgeEventPayload = (msg: NTReceiveMessageType.NTMessageItemType): NudgeEventPayload | null => { + if (msg.msgType !== 5 || msg.subMsgType !== 12) { + return null + } + + const element = msg.elements[0] + if (element.elementType !== 8) { + return null + } + + const grayTip = element.grayTipElement + if (!grayTip || grayTip.subElementType !== 17) { + return null + } + + const jsonGrayTip = grayTip.jsonGrayTipElement + if (jsonGrayTip.busiId !== '1061') { + return null + } + + const data = JSON.parse(jsonGrayTip.jsonStr) as { items?: Array<{ type?: string, uid?: `u_${string}` }> } + const qqItems = (data.items ?? []).filter((item) => item.type === 'qq' && item.uid) + if (qqItems.length < 2) { + return null + } + + return { + fromUid: qqItems[0].uid!, + toUid: qqItems[1].uid!, + } +} + +export const buildParsedNudgeEvent = async ( + msg: NTReceiveMessageType.NTMessageItemType, +): Promise | null> => { + const parsed = parseNudgeEventPayload(msg) + if (!parsed) { + return null + } + + const { senderUin, targetUin } = await resolveNudgeTargets(parsed.fromUid, parsed.toUid) + const event: EventDataType = { + self: resolveCurrentEventSelf(), + time: parseInt(msg.msgTime), + type: "notice", + detailType: "group_member_nudge", + subType: "", + data: { + messageId: msg.msgId, + messageSeq: msg.msgSeq, + groupId: parseInt(msg.peerUin), + senderUin, + senderUid: parsed.fromUid, + targetUin, + targetUid: parsed.toUid, + }, + } + + switch (msg.chatType) { + case 1: + event.detailType = 'private_nudge' + break + case 2: + event.detailType = 'group_nudge' + applyMessageEventChatContext(msg, event) + break + } + + return event +} diff --git a/src/adapters/onebot/message-event-resolvers.ts b/src/adapters/onebot/message-event-resolvers.ts new file mode 100644 index 0000000..15606eb --- /dev/null +++ b/src/adapters/onebot/message-event-resolvers.ts @@ -0,0 +1,68 @@ +import { requireCurrentNTUserInfo, requireCurrentNTUserModel } from "../../ntqq/core/runtime-store" +import { getUserInfoByUid } from "../../ntqq/query/detail-info" +import { BotMessageData, BotUserInfo, EventDataType, NudgeMessageData, RecallMessageData } from "../../onebot/contracts/event" +import { NTReceiveMessageType } from "../../ntqq/message/interfaces" + +export const resolveCurrentEventSelf = (): BotUserInfo => { + const user = requireCurrentNTUserInfo() + return { + id: user.uin, + uid: user.uid, + } +} + +export const resolveSenderUin = async (senderUid?: `u_${string}`) => { + if (!senderUid) { + return 0 + } + + const senderUserInfo = await getUserInfoByUid(senderUid) + return parseInt(senderUserInfo.uin) +} + +export const applyMessageEventChatContext = ( + msg: NTReceiveMessageType.NTMessageItemType, + event: EventDataType, +) => { + switch (msg.chatType) { + case 1: + return 'private' + case 2: + event.data.groupId = parseInt(msg.peerUid) + if ('groupName' in event.data) { + event.data.groupName = msg.peerName + } + return 'group' + default: + return '' + } +} + +export const hydrateMentionUin = async ( + msg: NTReceiveMessageType.NTMessageItemType, + elements: BotMessageData["elements"], +) => { + for (const element of elements) { + if (element.type !== 'mention' || element.data.at?.isAll !== false) { + continue + } + + const user = requireCurrentNTUserModel() + const group = user.getGroup(parseInt(msg.peerUid)) + const member = group.getMemberByUid(element.data.at.uid as `u_${string}`) + const info = await member.getInfo() + element.data.at.uin = parseInt(info.uin) + } +} + +export const resolveNudgeTargets = async (fromUid: `u_${string}`, toUid: `u_${string}`) => { + const [fromInfo, toInfo] = await Promise.all([ + getUserInfoByUid(fromUid), + getUserInfoByUid(toUid), + ]) + + return { + senderUin: parseInt(fromInfo.uin), + targetUin: parseInt(toInfo.uin), + } +} diff --git a/src/adapters/onebot/register-account-events.ts b/src/adapters/onebot/register-account-events.ts new file mode 100644 index 0000000..7121e8c --- /dev/null +++ b/src/adapters/onebot/register-account-events.ts @@ -0,0 +1,25 @@ +import { useLogger } from "../../common/log" +import { KickedOfflinePayload } from "../../ntqq/types/listener/msg" +import { buildKickedOfflineEvent } from "./account-event-builders" +import { defineOneBotEventDomainRegistrar } from "./register-event-domain" +import { registerBuiltOneBotEvent } from "./register-built-event" + +const log = useLogger('Event/Account') + +export const accountEventRegistrar = defineOneBotEventDomainRegistrar('account', () => { + registerBuiltOneBotEvent({ + event: 'KernelMsgListener/onKickedOffLine', + build: (payload: KickedOfflinePayload) => buildKickedOfflineEvent(payload), + onEmpty: () => { + log.error('onKickedOffLine: user not found') + }, + onEmit: () => { + // 被踢下线后当前实例已无法可靠继续提供服务,保留延迟退出以便上层拉起或人工接管。 + setTimeout(process.exit, 5000, 1) + }, + }) +}) + +export const registerAccountEvents = () => { + accountEventRegistrar.register() +} diff --git a/src/adapters/onebot/register-built-event.ts b/src/adapters/onebot/register-built-event.ts new file mode 100644 index 0000000..1386f65 --- /dev/null +++ b/src/adapters/onebot/register-built-event.ts @@ -0,0 +1,74 @@ +import { registerEventListener } from "../../store/event-registry" +import { EventDataType } from "../../onebot/contracts/event" +import { NTReceiveMessageType } from "../../ntqq/message/interfaces" +import { sendOneBotEvent } from "./send-event" + +const emitBuiltOneBotEvent = ( + event: EventDataType | null | undefined, + onEmpty?: () => void, + onEmit?: (event: EventDataType) => void, +) => { + if (!event) { + onEmpty?.() + return + } + + sendOneBotEvent(event) + onEmit?.(event) +} + +export const registerBuiltOneBotEvent = ({ + event, + build, + onEmpty, + onEmit, +}: { + event: `${string}/${string}` + build: (...args: TArgs) => EventDataType | null | undefined | Promise | null | undefined> + onEmpty?: () => void + onEmit?: (event: EventDataType) => void +}) => { + registerEventListener(event, 'always', async (...args: TArgs) => { + emitBuiltOneBotEvent(await build(...args), onEmpty, onEmit) + }) +} + +export const registerSimpleOneBotEvent = ({ + event, + build, +}: { + event: `${string}/${string}` + build: (...args: TArgs) => EventDataType +}) => { + registerEventListener(event, 'always', (...args: TArgs) => { + sendOneBotEvent(build(...args)) + }) +} + +export const registerIteratedBuiltOneBotEvent = ({ + event, + shouldSkip, + build, + iterate, + onSkip, + onBeforeBuild, +}: { + event: `${string}/${string}` + iterate: (payload: NTReceiveMessageType.NTMessageItemType[]) => Iterable + shouldSkip?: (payload: NTReceiveMessageType.NTMessageItemType) => boolean + build: (payload: NTReceiveMessageType.NTMessageItemType) => EventDataType | null | undefined | Promise | null | undefined> + onSkip?: (payload: NTReceiveMessageType.NTMessageItemType) => void + onBeforeBuild?: (payload: NTReceiveMessageType.NTMessageItemType) => void +}) => { + registerEventListener(event, 'always', async (payload: NTReceiveMessageType.NTMessageItemType[]) => { + for (const item of iterate(payload)) { + if (shouldSkip?.(item)) { + onSkip?.(item) + continue + } + + onBeforeBuild?.(item) + emitBuiltOneBotEvent(await build(item)) + } + }) +} diff --git a/src/adapters/onebot/register-event-domain.ts b/src/adapters/onebot/register-event-domain.ts new file mode 100644 index 0000000..43bdcfd --- /dev/null +++ b/src/adapters/onebot/register-event-domain.ts @@ -0,0 +1,35 @@ +export type OneBotEventDomainRegistrar = { + name: string + register: () => void +} + +const registeredOneBotEventDomains = new Set() + +export const defineOneBotEventDomainRegistrar = ( + name: string, + register: () => void, +): OneBotEventDomainRegistrar => { + return { + name, + register: () => { + if (registeredOneBotEventDomains.has(name)) { + return + } + + register() + registeredOneBotEventDomains.add(name) + }, + } +} + +export const registerOneBotEventDomain = (registrar: OneBotEventDomainRegistrar) => { + registrar.register() +} + +export const registerOneBotEventDomains = ( + registrars: readonly OneBotEventDomainRegistrar[], +) => { + for (const registrar of registrars) { + registerOneBotEventDomain(registrar) + } +} diff --git a/src/adapters/onebot/register-events.ts b/src/adapters/onebot/register-events.ts new file mode 100644 index 0000000..898ba4a --- /dev/null +++ b/src/adapters/onebot/register-events.ts @@ -0,0 +1,20 @@ +import { accountEventRegistrar, registerAccountEvents } from "./register-account-events" +import { loginEventRegistrar, registerLoginEvents } from "./register-login-events" +import { messageEventRegistrar, registerMessageEvents } from "./register-message-events" +import { registerOneBotEventDomains as registerNamedOneBotEventDomains } from "./register-event-domain" + +export const oneBotEventRegistrars = [ + messageEventRegistrar, + loginEventRegistrar, + accountEventRegistrar, +] as const + +export const registerOneBotEventDomains = ( + registrars = oneBotEventRegistrars, +) => { + registerNamedOneBotEventDomains(registrars) +} + +export const registerOneBotEvents = () => { + registerOneBotEventDomains() +} diff --git a/src/adapters/onebot/register-login-events.ts b/src/adapters/onebot/register-login-events.ts new file mode 100644 index 0000000..9f80c7a --- /dev/null +++ b/src/adapters/onebot/register-login-events.ts @@ -0,0 +1,104 @@ +import { useLogger } from "../../common/log" +import { NTLogin } from "../../ntqq/login/interfaces" +import { + buildAlreadyLoginEvent, + buildDeviceConfirmStatusEvent, + buildDeviceConfirmSuccessEvent, + buildQRCodeFailedEvent, + buildQRCodeFetchEvent, + buildQRCodeScannedEvent, + buildQRCodeSuccessEvent, +} from "./login-event-builders" +import { defineOneBotEventDomainRegistrar } from "./register-event-domain" +import { registerBuiltOneBotEvent, registerSimpleOneBotEvent } from "./register-built-event" + +const log = useLogger('Event/Login') + +const registerQRCodeFetchEvent = () => { + registerBuiltOneBotEvent({ + event: 'KernelLoginListener/onQRCodeGetPicture', + build: (payload: NTLogin.GetQRCodeResponse) => buildQRCodeFetchEvent(payload), + onEmpty: () => { + log.error('qrCodeFetch: user not found') + }, + }) +} + +const registerQRCodeFailedEvent = () => { + registerBuiltOneBotEvent({ + event: 'KernelLoginListener/onQRCodeSessionFailed', + build: (p1: number, p2: number) => buildQRCodeFailedEvent(p1, p2), + onEmpty: () => { + log.error('qrCodeFetch: user not found') + }, + }) +} + +const registerQRCodeScannedEvent = () => { + registerBuiltOneBotEvent({ + event: 'KernelLoginListener/onQRCodeSessionUserScaned', + build: (p1: number, avatarUrl: string) => buildQRCodeScannedEvent(p1, avatarUrl), + onEmpty: () => { + log.error('qrCodeFetch: user not found') + }, + }) +} + +const registerQRCodeSuccessEvent = () => { + registerSimpleOneBotEvent({ + event: 'KernelLoginListener/onQRCodeLoginSucceed', + build: (userInfo) => buildQRCodeSuccessEvent(userInfo), + }) +} + +const registerAlreadyLoginEvent = () => { + registerSimpleOneBotEvent({ + event: 'KernelLoginListener/onUserLoggedIn', + build: (uin: number) => buildAlreadyLoginEvent(uin), + }) +} + +const registerDeviceConfirmStatusEvent = () => { + registerBuiltOneBotEvent({ + event: 'KernelLoginListener/OnConfirmUnusualDeviceFailed', + build: (errCode: number, _errMsg: string, _errTips: NTLogin.PayloadConfirmUnusualDeviceFailed) => { + log.info('deviceConfirmStatus payload:', errCode) + return buildDeviceConfirmStatusEvent(errCode) + }, + onEmpty: () => { + log.error('qrCodeFetch: user not found') + }, + }) +} + +const registerLoginStatusChangeEvent = () => { + registerBuiltOneBotEvent({ + event: 'KernelLoginListener/onLoginState', + build: (state: number) => { + log.info('onLoginState state:', state) + if (state !== 0) { + return undefined + } + const event = buildDeviceConfirmSuccessEvent(state) + if (!event) { + log.error('qrCodeFetch: user not found') + } + return event + }, + }) +} + +export const loginEventRegistrar = defineOneBotEventDomainRegistrar('login', () => { + log.info('registerLoginEvents') + registerQRCodeFetchEvent() + registerQRCodeFailedEvent() + registerQRCodeScannedEvent() + registerQRCodeSuccessEvent() + registerAlreadyLoginEvent() + registerDeviceConfirmStatusEvent() + registerLoginStatusChangeEvent() +}) + +export const registerLoginEvents = () => { + loginEventRegistrar.register() +} diff --git a/src/adapters/onebot/register-message-events.ts b/src/adapters/onebot/register-message-events.ts new file mode 100644 index 0000000..350b640 --- /dev/null +++ b/src/adapters/onebot/register-message-events.ts @@ -0,0 +1,94 @@ +import { useLogger } from "../../common/log" +import { NTReceiveMessageType } from "../../ntqq/message/interfaces" +import { + buildMessageEvent, + buildNudgeEvent, + buildPostSendEvent, + buildRecallEvent, + hasRecallMessageEvent, + shouldSkipMessageEvent, +} from "./message-event-builders" +import { defineOneBotEventDomainRegistrar } from "./register-event-domain" +import { registerBuiltOneBotEvent, registerIteratedBuiltOneBotEvent } from "./register-built-event" + +const log = useLogger('Event/Message') + +const shouldIgnoreMessageEvent = (msg: NTReceiveMessageType.NTMessageItemType) => { + if (shouldSkipMessageEvent(msg)) { + return true + } + return false +} + +const logSkippedMessageEvent = (msg: NTReceiveMessageType.NTMessageItemType) => { + if (Date.now() / 1000 - parseInt(msg.msgTime) > 60) { + log.info('一分钟前的消息不处理') + } +} + +const buildReceiveMessageEvent = async (msg: NTReceiveMessageType.NTMessageItemType) => { + const nudgeEvent = await buildNudgeEvent(msg) + if (nudgeEvent) { + return nudgeEvent + } + + return buildMessageEvent(msg) +} + +const registerRecvMessageEvent = () => { + registerIteratedBuiltOneBotEvent({ + event: 'KernelMsgListener/onRecvMsg', + iterate: (payload) => payload, + shouldSkip: shouldIgnoreMessageEvent, + onSkip: logSkippedMessageEvent, + onBeforeBuild: (msg) => { + log.info('receive:', JSON.stringify(msg)) + }, + build: buildReceiveMessageEvent, + }) +} + +const registerPostSendMessageEvent = () => { + registerBuiltOneBotEvent({ + event: 'KernelMsgListener/onAddSendMsg', + build: async (payload: NTReceiveMessageType.NTMessageItemType) => { + if (shouldIgnoreMessageEvent(payload)) { + return undefined + } + log.info('receive:', JSON.stringify(payload)) + return buildPostSendEvent(payload) + }, + }) +} + +const registerUpdateMessageEvent = () => { + let oldPayload = '' + registerIteratedBuiltOneBotEvent({ + event: 'KernelMsgListener/onMsgInfoListUpdate', + iterate: (payload) => { + const rawPayload = JSON.stringify(payload) + if (oldPayload === rawPayload) { + log.warn('onMsgInfoListUpdate payload no change') + return [] + } + + oldPayload = rawPayload + log.info('onMsgInfoListUpdate count:', payload.length) + return payload.filter(hasRecallMessageEvent) + }, + onBeforeBuild: (msg) => { + log.info(`get user info for user: ${msg.senderUid}`) + }, + build: buildRecallEvent, + }) +} + +export const messageEventRegistrar = defineOneBotEventDomainRegistrar('message', () => { + registerRecvMessageEvent() + registerPostSendMessageEvent() + registerUpdateMessageEvent() +}) + +export const registerMessageEvents = () => { + messageEventRegistrar.register() +} diff --git a/src/adapters/onebot/send-event.ts b/src/adapters/onebot/send-event.ts new file mode 100644 index 0000000..9b335d3 --- /dev/null +++ b/src/adapters/onebot/send-event.ts @@ -0,0 +1,6 @@ +import { broadcast } from "../ws/connection-manager" +import { EventDataType } from "../../onebot/contracts/event" + +export const sendOneBotEvent = (event: EventDataType) => { + broadcast(event) +} diff --git a/src/adapters/server/start-http-server.ts b/src/adapters/server/start-http-server.ts new file mode 100644 index 0000000..d531903 --- /dev/null +++ b/src/adapters/server/start-http-server.ts @@ -0,0 +1,60 @@ +import express from "express" +import { NextFunction, Request, Response } from "express" +import bodyParser from "body-parser" +import fileUpload from "express-fileupload" +import { createServer } from "http" +import { useLogger } from "../../common/log" +import { getConfig, getStoragePath } from "../../store/config" +import { createHTTPRouter } from "../http/create-router" + +const log = useLogger("HTTP Server") +const router = createHTTPRouter() + +export const startHTTPServerRuntime = () => { + const cfg = getConfig() + const app = express() + const storagePath = getStoragePath() + + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded()) + log.info("storagePath:", storagePath) + app.use(fileUpload({ + createParentPath: true, + useTempFiles: true, + tempFileDir: storagePath, + preserveExtension: true, + })) + app.use((req, res, next) => { + log.info("before route", req.url) + const oldJson = res.json + + res.json = function(body?: any) { + if (body?.code === undefined || body?.code === null) { + body = { + code: 0, + msg: "success", + error: "", + data: body, + } + } + return oldJson.apply(res, [body]) + } + next() + }) + app.use(router) + app.use((_req, _res, _next) => { + log.info("after route") + }) + app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { + log.error("error handle:", err) + res.status(500).send({ + code: 500, + msg: "error", + error: err.message, + data: null, + }) + }) + + const httpServer = createServer(app) + httpServer.listen(cfg.yui.http.port, cfg.yui.http.host) +} diff --git a/src/adapters/server/start-servers.ts b/src/adapters/server/start-servers.ts new file mode 100644 index 0000000..87c0a0e --- /dev/null +++ b/src/adapters/server/start-servers.ts @@ -0,0 +1,12 @@ +import { useLogger } from "../../common/log" +import { startHTTPServerRuntime } from "./start-http-server" +import { startWebsocketServerRuntime } from "./start-websocket-server" + +const log = useLogger('Adapters/Server') + +export const startServers = () => { + log.info('start websocket server...') + startWebsocketServerRuntime() + log.info('start http server...') + startHTTPServerRuntime() +} diff --git a/src/adapters/server/start-websocket-server.ts b/src/adapters/server/start-websocket-server.ts new file mode 100644 index 0000000..bf53fb1 --- /dev/null +++ b/src/adapters/server/start-websocket-server.ts @@ -0,0 +1,63 @@ +import { WebSocketServer } from "ws" +import { useLogger } from "../../common/log" +import { + addConnection, + getConnectionCount, + removeConnection, +} from "../ws/connection-manager" +import { getConfig } from "../../store/config" +import { handleBotActionRequest } from "../ws/handle-bot-action-request" + +const log = useLogger("WebSocket") + +export const startWebsocketServerRuntime = () => { + const cfg = getConfig() + const wss = new WebSocketServer({ + host: cfg.yui.ws.host, + port: cfg.yui.ws.port, + }) + + wss.once("close", () => { + log.info("websocket server closed.") + }) + wss.once("listening", () => { + log.info(`websocket server listening on ${cfg.yui.ws.host}:${cfg.yui.ws.port}`) + }) + wss.on("connection", (ws, req) => { + log.info("receive connection.") + try { + log.info("async run.") + if (!req.url) { + log.error("url error") + ws.terminate() + ws.close(400, "url error") + return + } + + const eventHandle = (msg: string) => { + ws.send(msg) + } + addConnection(eventHandle) + log.info("connection registered, total:", getConnectionCount()) + ws.on("close", (code, reason) => { + log.info("connection close.", code, reason.toString()) + removeConnection(eventHandle) + log.info("connection removed, total:", getConnectionCount()) + }) + + ws.on("message", (message) => { + void (async () => { + const resp = await handleBotActionRequest(message.toString()) + ws.send(JSON.stringify(resp)) + })() + }) + } + catch (err) { + log.error(err) + } + }) + wss.on("error", (error) => { + log.info("failed to create server!", error) + }) + return wss +} diff --git a/src/adapters/ws/connection-manager.ts b/src/adapters/ws/connection-manager.ts new file mode 100644 index 0000000..0dc6206 --- /dev/null +++ b/src/adapters/ws/connection-manager.ts @@ -0,0 +1,64 @@ +import { useLogger } from "../../common/log" +import { convertToSnakeCase } from "../../common/utils" +import { WSMessageSender } from "../../common/interface/ws" + +const log = useLogger('WS/ConnectionManager') +type ConnectionRecord = { + id: number + send: WSMessageSender +} + +let nextConnectionId = 1 +const clientConnections = new Map() + +const serializePublishedMessage = (message: unknown) => { + return JSON.stringify(convertToSnakeCase(message)) +} + +export const addConnection = (handle: WSMessageSender) => { + if (clientConnections.has(handle)) { + return clientConnections.get(handle)! + } + + const record: ConnectionRecord = { + id: nextConnectionId++, + send: handle, + } + clientConnections.set(handle, record) + log.info('connection added, current count:', clientConnections.size) + return record +} + +export const removeConnection = (handle: WSMessageSender) => { + const removed = clientConnections.delete(handle) + if (!removed) { + log.warn('事件处理器移除失败,未找到该处理器') + return false + } + log.info('connection removed, current count:', clientConnections.size) + return true +} + +export const publish = (message: unknown) => { + const payload = serializePublishedMessage(message) + for (const [handle, connection] of clientConnections.entries()) { + try { + handle(payload) + } + catch (error) { + log.warn('connection publish failed, remove broken handle:', connection.id) + clientConnections.delete(handle) + } + } + return payload +} + +export const broadcast = (message: unknown) => { + publish(message) +} + +export const getConnectionCount = () => clientConnections.size + +export const listConnectionIds = () => { + return Array.from(clientConnections.values(), (connection) => connection.id) +} diff --git a/src/adapters/ws/ensure-runtime-ready.ts b/src/adapters/ws/ensure-runtime-ready.ts new file mode 100644 index 0000000..7786db0 --- /dev/null +++ b/src/adapters/ws/ensure-runtime-ready.ts @@ -0,0 +1,22 @@ +import { useLogger } from "../../common/log" +import { BotActionRequest } from "../../onebot/contracts/actions" +import { initializeNTRuntime } from "../../ntqq/core/lifecycle" +import { isCurrentNTUserReady, setCurrentNTUin } from "../../ntqq/core/runtime-store" +import { registerOneBotEvents as registerOneBotRuntimeEvents } from "../onebot/register-events" + +const log = useLogger('WS/EnsureRuntimeReady') + +export const ensureBotActionRuntimeReady = async ( + request: Pick, +) => { + log.info('uin:', request.user.uin) + setCurrentNTUin(request.user.uin) + + if (isCurrentNTUserReady()) { + return + } + + log.info(`user(${request.user.uin}) has not been initialized, start to initialize...`) + await initializeNTRuntime() + registerOneBotRuntimeEvents() +} diff --git a/src/adapters/ws/handle-bot-action-request.ts b/src/adapters/ws/handle-bot-action-request.ts new file mode 100644 index 0000000..1ea6cc5 --- /dev/null +++ b/src/adapters/ws/handle-bot-action-request.ts @@ -0,0 +1,27 @@ +import { parseBotActionRequest, toBadRequestResponse } from "../../common/action" +import { useLogger } from "../../common/log" +import { BotActionResponse } from "../../onebot/contracts/actions" +import { clearTraceId, setTraceId } from "../../store/runtime" +import { executeAppAction } from "../../app/dispatch-action" +import { ensureBotActionRuntimeReady } from "./ensure-runtime-ready" + +const log = useLogger('WS/HandleBotAction') + +export const handleBotActionRequest = async (rawMessage: string): Promise => { + try { + log.info('server: received: ', rawMessage) + const msg = parseBotActionRequest(rawMessage) + setTraceId(msg.id) + await ensureBotActionRuntimeReady(msg) + const resp = await executeAppAction(msg) + log.info(`reply ${resp.id}:`, JSON.stringify(resp)) + return resp + } + catch (error) { + log.error(error) + return toBadRequestResponse(error) + } + finally { + clearTraceId() + } +} diff --git a/src/app/dispatch-action.ts b/src/app/dispatch-action.ts new file mode 100644 index 0000000..7376a18 --- /dev/null +++ b/src/app/dispatch-action.ts @@ -0,0 +1,76 @@ +import { useLogger } from "../common/log" +import { convertToCamelCase, convertToSnakeCase } from "../common/utils" +import { BotActionRequest, BotActionResponse } from "../onebot/contracts/actions" +import { CustomError } from "../common/error/custom-error" +import { getRegisteredActionHandler } from "../store/action-registry" +import { appActionHandlers, AppActionName } from "./register-onebot-actions" + +const log = useLogger('App/DispatchAction') + +export type ActionDispatchResult = + | { found: false } + | { found: true, data: unknown } + +export const isAppActionName = (action: string): action is AppActionName => { + return Object.prototype.hasOwnProperty.call(appActionHandlers, action) +} + +export const dispatchAppAction = async (request: Pick): Promise => { + const handle = getRegisteredActionHandler(request.action) + if (!handle) { + return { found: false } + } + + const data = await handle(convertToCamelCase(request.params)) + return { + found: true, + data: convertToSnakeCase(data), + } +} + +const createActionErrorResponse = ( + requestId: string, + retcode: number, + message: string, + data: unknown = null, +): BotActionResponse => ({ + id: requestId, + status: 'failed', + retcode, + data, + message, +}) + +export const executeAppAction = async ( + request: Pick, +): Promise => { + log.info('request action:', request.action) + + try { + log.info('start handle for ', request.action) + const result = await dispatchAppAction(request) + if (!result.found) { + if (!isAppActionName(request.action)) { + log.warn('unsupported app action: %s', request.action) + } + return createActionErrorResponse(request.id, 10002, 'Unsupported Action') + } + + const response: BotActionResponse = { + id: request.id, + status: 'ok', + retcode: 0, + data: result.data, + message: '', + } + log.info('end handle for ', request.action) + return response + } + catch (error) { + log.error('内部错误:', error) + if (error instanceof CustomError) { + return createActionErrorResponse(request.id, error.code, error.message, undefined) + } + return createActionErrorResponse(request.id, 20002, 'Internal Handler Error') + } +} diff --git a/src/app/friend/friend-query-service.ts b/src/app/friend/friend-query-service.ts new file mode 100644 index 0000000..bea5903 --- /dev/null +++ b/src/app/friend/friend-query-service.ts @@ -0,0 +1,24 @@ +import { useLogger } from "../../common/log" +import { getUserInfoByUid } from "../../ntqq/query/detail-info" +import { UserInfoReq, UserInfoResp } from "../../onebot/contracts/friend" + +const log = useLogger('App/FriendQueryService') + +export const getUserInfo = async (payload: UserInfoReq): Promise => { + const response: UserInfoResp = { + uid: "u_", + uin: 0, + nick: "", + userDisplayname: "", + userRemark: "", + avatarUrl: '', + } + + const detail = await getUserInfoByUid(payload.userUid) + log.info('getUserDetailInfo:', detail) + response.uid = detail.uid + response.uin = parseInt(detail.uin) + response.nick = detail.simpleInfo.coreInfo.nick + response.avatarUrl = `http://q1.qlogo.cn/g?b=qq&nk=${response.uin}&s=640` + return response +} diff --git a/src/app/friend/friend-service.ts b/src/app/friend/friend-service.ts new file mode 100644 index 0000000..00702c3 --- /dev/null +++ b/src/app/friend/friend-service.ts @@ -0,0 +1,31 @@ +import { useLogger } from "../../common/log" +import { NTGetFriendList, NTSendLikeFriend } from "../../ntqq/friend/friend" +import { LikeUserReq } from "../../onebot/contracts/friend" +import { getUserInfo } from "./friend-query-service" + +const log = useLogger('App/FriendService') + +export { getUserInfo } from "./friend-query-service" + +export const getFriendList = async () => { + const list = await NTGetFriendList() + return list.map(e => ({ + id: e.categoryId, + name: e.categroyName, + count: e.categroyMbCount, + friend_list: e.buddyList.map(f => ({ + uin: parseInt(f.uin), + nick: f.nick, + avatar_url: f.avatarUrl, + remark: f.remark, + long_nick: f.longNick, + })) + })) +} + +export const sendLikeFriend = async (payload: LikeUserReq) => { + log.info('sendLikeFriend') + const result = await NTSendLikeFriend(payload.userId, payload.count) + log.info('sendLikeFriend end') + return result +} diff --git a/src/app/friend/register-friend-actions.ts b/src/app/friend/register-friend-actions.ts new file mode 100644 index 0000000..3454074 --- /dev/null +++ b/src/app/friend/register-friend-actions.ts @@ -0,0 +1,13 @@ +import { defineActionHandlers, registerActionHandlers } from "../register-action-handlers" +import { getFriendList, getUserInfo, sendLikeFriend } from "./friend-service" + +export const friendActionHandlers = defineActionHandlers({ + get_friend_list: getFriendList, + get_friend_list_with_group: getFriendList, + send_like_friend: sendLikeFriend, + get_user_info: getUserInfo, +}) + +export const registerFriendActions = () => { + registerActionHandlers(friendActionHandlers) +} diff --git a/src/app/group/group-query-service.ts b/src/app/group/group-query-service.ts new file mode 100644 index 0000000..2d01a24 --- /dev/null +++ b/src/app/group/group-query-service.ts @@ -0,0 +1,64 @@ +import { useLogger } from "../../common/log" +import { requireCurrentNTUserModel } from "../../ntqq/core/runtime-store" +import { getGroupInfoById } from "../../ntqq/query/detail-info" +import { CustomError } from "../../common/error/custom-error" +import { GroupMemberDetailInfoType } from "../../onebot/contracts/common" +import { + GroupInfoReq, + GroupInfoResp, + GroupMemberInfoReq, + GroupMemberInfoResp, +} from "../../onebot/contracts/friend" + +const log = useLogger('App/GroupQueryService') + +export const getGroupInfo = async (payload: GroupInfoReq): Promise => { + const response: GroupInfoResp = { + name: '', + avatarUrl: '', + id: payload.groupId, + } + + const detail = await getGroupInfoById(payload.groupId) + log.info('getGroupInfoById:', detail) + response.name = detail.groupName + response.avatarUrl = `https://p.qlogo.cn/gh/${payload.groupId}/${payload.groupId}/640` + return response +} + +export const getGroupMemberInfo = async (payload: GroupMemberInfoReq): Promise => { + const response: GroupMemberInfoResp = { + userUid: "u_", + userUin: 0, + nick: "", + cardName: "", + avatarUrl: '', + } + + let detail: GroupMemberDetailInfoType | undefined + const user = requireCurrentNTUserModel() + const group = user.getGroup(payload.groupId) + + if (payload.userUid === undefined || payload.userUid === null || payload.userUid.length === 0) { + if (payload.userUin <= 0) { + throw new CustomError(1, 'uid or uin is error!') + } + const member = group.getMember(payload.userUin) + detail = await member.getInfo() + } else { + const member = group.getMemberByUid(payload.userUid) + detail = await member.getInfo() + } + + if (detail === undefined) { + throw new CustomError(1, 'Failed to get member info!') + } + + log.info('getGroupMemberInfoById:', detail) + response.userUid = detail.uid + response.userUin = parseInt(detail.uin) + response.nick = detail.nick + response.cardName = detail.cardName + response.avatarUrl = `http://q1.qlogo.cn/g?b=qq&nk=${response.userUin}&s=640` + return response +} diff --git a/src/app/group/group-service.ts b/src/app/group/group-service.ts new file mode 100644 index 0000000..aaffe0c --- /dev/null +++ b/src/app/group/group-service.ts @@ -0,0 +1,52 @@ +import { useLogger } from "../../common/log" +import { requireCurrentNTUserModel } from "../../ntqq/core/runtime-store" +import { NTGetGroupList } from "../../ntqq/group/list" +import { NTGroupMsgList } from "../../ntqq/group/msg" +import { CustomError } from "../../common/error/custom-error" +import { convertNTMsg2BotMsg } from "../../transfer/message/convert" +import { GroupMemberNudgeReq } from "../../onebot/contracts/friend" +import { GroupDetailInfoResp, GroupMsgReq } from "../../onebot/contracts/group" +import { getGroupInfo, getGroupMemberInfo } from "./group-query-service" + +const log = useLogger('App/GroupService') + +export { getGroupInfo, getGroupMemberInfo } from "./group-query-service" + +const code2role: Record = { + 2: 'member', + 3: 'manager', + 4: 'owner' +} + +export const getGroupList = async (): Promise => { + const ret = await NTGetGroupList() + log.info('getGroupList:', ret) + return ret.map(e => ({ + code: parseInt(e.groupCode), + name: e.groupName, + avatarUrl: `https://p.qlogo.cn/gh/${e.groupCode}/${e.groupCode}/640/`, + role: code2role[e.memberRole], + top: e.isTop, + toppedTimestamp: parseInt(e.toppedTimestamp), + isConf: e.isConf, + })) +} + +export const getGroupMsgList = async (payload: GroupMsgReq) => { + const ret = await NTGroupMsgList(payload.code, payload.msgId, payload.cnt) + log.info('group msg list:', JSON.stringify(ret, null, 4)) + return ret.map(msg => convertNTMsg2BotMsg(msg)) +} + +export const sendNudgeToMember = async (payload: GroupMemberNudgeReq): Promise => { + const user = requireCurrentNTUserModel() + const group = user.getGroup(payload.groupId) + if (!group) { + throw new CustomError(10000002, '获取群实例失败') + } + const member = group.getMember(payload.userUin) + if (!member) { + throw new CustomError(10000002, '获取群成员失败') + } + return await member.nudge() +} diff --git a/src/app/group/register-group-actions.ts b/src/app/group/register-group-actions.ts new file mode 100644 index 0000000..fb95ffe --- /dev/null +++ b/src/app/group/register-group-actions.ts @@ -0,0 +1,40 @@ +import { + defineActionHandlers, + mergeActionHandlerMaps, + registerActionHandlers, +} from "../register-action-handlers" +import { + getGroupInfo, + getGroupList, + getGroupMemberInfo, + getGroupMsgList, + sendNudgeToMember, +} from "./group-service" + +export const groupActionHandlers = defineActionHandlers({ + get_group_list: getGroupList, + get_group_info: getGroupInfo, + get_group_msg: getGroupMsgList, +}) + +export const groupMemberActionHandlers = defineActionHandlers({ + send_nudge_to_member: sendNudgeToMember, + get_group_member_info: getGroupMemberInfo, +}) + +export const groupDomainActionHandlers = mergeActionHandlerMaps( + groupActionHandlers, + groupMemberActionHandlers, +) + +export const registerGroupActions = () => { + registerActionHandlers(groupActionHandlers) +} + +export const registerGroupMemberActions = () => { + registerActionHandlers(groupMemberActionHandlers) +} + +export const registerGroupDomainActions = () => { + registerActionHandlers(groupDomainActionHandlers) +} diff --git a/src/app/login/login-quick-service.ts b/src/app/login/login-quick-service.ts new file mode 100644 index 0000000..b974f43 --- /dev/null +++ b/src/app/login/login-quick-service.ts @@ -0,0 +1,16 @@ +import { NTGetQuickLoginList } from "../../ntqq/login" +import { QuickLoginItem } from "../../onebot/contracts/bot" + +export const getQuickLoginList = async (): Promise => { + const list = await NTGetQuickLoginList() + return list.map((entry) => ({ + uin: entry.uin, + uid: entry.uid, + nick_name: entry.nickName, + face_path: entry.facePath, + face_url: entry.faceUrl, + login_type: entry.loginType, + is_quick_login: entry.isQuickLogin, + is_auto_login: entry.isAutoLogin, + })) +} diff --git a/src/app/login/login-self-service.ts b/src/app/login/login-self-service.ts new file mode 100644 index 0000000..b8d3ee0 --- /dev/null +++ b/src/app/login/login-self-service.ts @@ -0,0 +1,37 @@ +import { useLogger } from "../../common/log" +import { getCurrentNTUin, getCurrentNTUserInfo } from "../../ntqq/core/runtime-store" +import { getUserInfoByUid } from "../../ntqq/query/detail-info" +import { UserInfoResp } from "../../onebot/contracts/friend" +import { CustomError } from "../../common/error/custom-error" + +const log = useLogger('App/LoginSelfService') + +export const getBotInfo = async (): Promise => { + const userInfo = getCurrentNTUserInfo() + const response: UserInfoResp = { + uid: "u_", + uin: 0, + nick: "", + userDisplayname: "", + userRemark: "", + avatarUrl: '', + } + + if (!userInfo || !userInfo.uid) { + log.error('user info:', userInfo) + throw new CustomError(1, 'user info error!') + } + + const detail = await getUserInfoByUid(userInfo.uid) + log.info('getUserDetailInfo:', detail) + response.uid = detail.uid + response.uin = parseInt(detail.uin) + response.nick = detail.simpleInfo.coreInfo.nick + response.avatarUrl = `http://q1.qlogo.cn/g?b=qq&nk=${response.uin}&s=640` + return response +} + +export const getAccountList = () => { + const uin = getCurrentNTUin() + return uin ? [String(uin)] : [] +} diff --git a/src/app/login/login-service.ts b/src/app/login/login-service.ts new file mode 100644 index 0000000..7c29bc0 --- /dev/null +++ b/src/app/login/login-service.ts @@ -0,0 +1,78 @@ +import { useLogger } from "../../common/log" +import { + NTCheckDeviceVerifyQRCodeStatus, + NTGetDeviceVerifyQRCode, + NTGetLoginQrCode, + NTGetLoginStatus, + NTLoginByAccountInfo, + NTQuickLoginByUin, +} from "../../ntqq/login" +import { NTParseClipboard2Msg } from "../../ntqq/common/clipboard" +import { CheckScanStatusResult } from "../../ntqq/types/core/oidb/0xc9e_8" +import { BotLogin } from "../../onebot/contracts/bot" +import { CustomError } from "../../common/error/custom-error" +import { NTLogin } from "../../ntqq/login/interfaces" +import { getAccountList, getBotInfo } from "./login-self-service" +import { getQuickLoginList } from "./login-quick-service" + +const log = useLogger('App/LoginService') + +export { getAccountList, getBotInfo } from "./login-self-service" +export { getQuickLoginList } from "./login-quick-service" + +const buildAccountLoginRequest = (payload: BotLogin.AccountLoginData): NTLogin.AccountLoginRequest => { + if (payload.id === undefined || payload.id === null || typeof payload.id !== 'number') { + throw new CustomError(10003, 'id参数不正确') + } + return { + loginInfo: { + uin: `${payload.id}`, + passwordMd5: payload.password, + step: payload.step, + newDeviceLoginSig: payload.deviceVerifySig || '', + proofWaterSig: payload.proofWaterSig, + proofWaterRand: payload.proofWaterRand, + proofWaterSid: payload.proofWaterSid + } + } +} + +export const loginByAccount = async (payload: BotLogin.AccountLoginData) => { + log.info("req param from client:", JSON.stringify(payload)) + const request = buildAccountLoginRequest(payload) + log.info("req to nt:", JSON.stringify(request)) + try { + return await NTLoginByAccountInfo(request) + } + catch (error) { + return error + } +} + +export const getDeviceVerifyQRCode = async (payload: BotLogin.DeviceVerifyQRCodeData): Promise => { + return await NTGetDeviceVerifyQRCode(payload.uin, payload.url) +} + +export const checkDeviceVerifyQRCodeStatus = async (payload: BotLogin.DeviceVerifyStatusData): Promise => { + return await NTCheckDeviceVerifyQRCodeStatus(payload.url) +} + +export const loginByQrCode = async () => { + return await NTGetLoginQrCode() +} + +export const getLoginStatus = async (): Promise => { + return await NTGetLoginStatus() +} + +export const getClipboardMsg = async () => { + return await NTParseClipboard2Msg() +} + +export const quickLoginByUin = async (uin: `${number}`) => { + const result = await NTQuickLoginByUin(uin) + if (result.result !== '0') { + throw new CustomError(parseInt(result.result), result.loginErrorInfo.errMsg) + } + return {} +} diff --git a/src/app/login/register-login-actions.ts b/src/app/login/register-login-actions.ts new file mode 100644 index 0000000..a046bf6 --- /dev/null +++ b/src/app/login/register-login-actions.ts @@ -0,0 +1,43 @@ +import { BotActionParams } from "../../onebot/contracts/actions" +import { defineActionHandlers, registerActionHandlers } from "../register-action-handlers" +import { + checkDeviceVerifyQRCodeStatus, + getAccountList, + getBotInfo, + getClipboardMsg, + getDeviceVerifyQRCode, + getLoginStatus, + getQuickLoginList, + loginByAccount, + loginByQrCode, + quickLoginByUin, +} from "./login-service" + +interface QuickLoginReq extends BotActionParams { + uin: `${number}` +} + +const quickLoginByUinAction = async (payload: QuickLoginReq) => { + return await quickLoginByUin(payload.uin) +} + +const getAccountListAction = async () => { + return getAccountList() +} + +export const loginActionHandlers = defineActionHandlers({ + login_by_account: loginByAccount, + check_device_verify_qrcode_status: checkDeviceVerifyQRCodeStatus, + get_device_verify_qrcode: getDeviceVerifyQRCode, + login_by_qrcode: loginByQrCode, + get_self_info: getBotInfo, + is_self_login: getLoginStatus, + get_quick_login_list: getQuickLoginList, + quick_login_by_uin: quickLoginByUinAction, + get_clipboard_msg: getClipboardMsg, + get_account_list: getAccountListAction, +}) + +export const registerLoginActions = () => { + registerActionHandlers(loginActionHandlers) +} diff --git a/src/app/message/message-recall-service.ts b/src/app/message/message-recall-service.ts new file mode 100644 index 0000000..e732ccf --- /dev/null +++ b/src/app/message/message-recall-service.ts @@ -0,0 +1,32 @@ +import { getNTMsgService } from "../../ntqq/core/core" +import { CustomError } from "../../common/error/custom-error" +import { BotMessage } from "../../onebot/contracts/message" + +export const recallMessage = async (payload: BotMessage.DeleteMsg): Promise => { + const msgService = getNTMsgService() + const peerInfo: PeerInfo = { + chatType: 0, + peerUid: "", + guildId: "", + } + + if (payload.detailType === 'group') { + peerInfo.chatType = 2 + if (!payload.groupId) { + throw new CustomError(1, 'group_id is not supported!') + } + peerInfo.peerUid = `${payload.groupId}` + return msgService.recallMsg(peerInfo, payload.messageId) + } + + if (payload.detailType === 'private') { + peerInfo.chatType = 1 + if (!payload.userId) { + throw new CustomError(1, 'user_id is not supported!') + } + peerInfo.peerUid = `${payload.userId}` + return msgService.recallMsg(peerInfo, payload.messageId) + } + + throw new CustomError(1, 'detail_type is not supported!') +} diff --git a/src/app/message/message-service.ts b/src/app/message/message-service.ts new file mode 100644 index 0000000..76eff18 --- /dev/null +++ b/src/app/message/message-service.ts @@ -0,0 +1,43 @@ +import { useLogger } from "../../common/log" +import { requireCurrentNTUserModel } from "../../ntqq/core/runtime-store" +import { CustomError } from "../../common/error/custom-error" +import { sendMessageToFriend } from "../../transfer/message/friend" +import { sendMessageToGroup } from "../../transfer/message/group" +import { BotMessage } from "../../onebot/contracts/message" +import { recallMessage } from "./message-recall-service" + +const log = useLogger('App/MessageService') + +export { recallMessage } from "./message-recall-service" + +const sendForwardMessageToGroup = async (payload: BotMessage.SendMsg) => { + const user = requireCurrentNTUserModel() + const group = user.getGroup(payload.groupId!) + if (!group) { + throw new CustomError(10000002, '获取群实例失败') + } + const msg = payload.message[0] + if (msg.type !== 'multiforward' || !msg.data.forwardData) { + throw new CustomError(1, 'forward message payload is invalid!') + } + return await group.sendForwardMessage(msg.data.forwardData) +} + +export const sendMessage = async (payload: BotMessage.SendMsg): Promise => { + log.info('sendMessage payload:', JSON.stringify(payload)) + if (payload.detailType === 'group' && payload.groupId) { + return await sendMessageToGroup(`${payload.groupId}`, payload.message) + } + if (payload.detailType === 'private' && payload.user_id) { + return await sendMessageToFriend(payload.user_id as `u_${string}`, payload.message) + } + throw new CustomError(1, 'detail_type not supported!') +} + +export const sendForwardMessage = async (payload: BotMessage.SendMsg): Promise => { + log.info('sendForwardMessage payload:', JSON.stringify(payload)) + if (payload.detailType === 'group' && payload.groupId) { + return await sendForwardMessageToGroup(payload) + } + throw new CustomError(1, 'detail_type is not supported!') +} diff --git a/src/app/message/register-message-actions.ts b/src/app/message/register-message-actions.ts new file mode 100644 index 0000000..ef7b69e --- /dev/null +++ b/src/app/message/register-message-actions.ts @@ -0,0 +1,32 @@ +import { + defineActionHandlers, + mergeActionHandlerMaps, + registerActionHandlers, +} from "../register-action-handlers" +import { recallMessage, sendForwardMessage, sendMessage } from "./message-service" + +export const sendMessageActionHandlers = defineActionHandlers({ + send_message: sendMessage, + send_forward_message: sendForwardMessage, +}) + +export const recallMessageActionHandlers = defineActionHandlers({ + recall_message: recallMessage, +}) + +export const messageActionHandlers = mergeActionHandlerMaps( + sendMessageActionHandlers, + recallMessageActionHandlers, +) + +export const registerSendMessageActions = () => { + registerActionHandlers(sendMessageActionHandlers) +} + +export const registerRecallMessageActions = () => { + registerActionHandlers(recallMessageActionHandlers) +} + +export const registerMessageActions = () => { + registerActionHandlers(messageActionHandlers) +} diff --git a/src/app/register-action-handlers.ts b/src/app/register-action-handlers.ts new file mode 100644 index 0000000..56fa7c7 --- /dev/null +++ b/src/app/register-action-handlers.ts @@ -0,0 +1,23 @@ +import { ActionHandlerMap } from "../store/action-registry" +import { registerActionHandler } from "../store/action-registry" + +export type { ActionHandler, ActionHandlerMap } from "../store/action-registry" + +type UnionToIntersection = + (T extends unknown ? (value: T) => void : never) extends ((value: infer I) => void) + ? I + : never + +export type MergedActionHandlerMap = UnionToIntersection + +export const defineActionHandlers = (handlers: THandlers) => handlers + +export const mergeActionHandlerMaps = (...handlers: THandlers) => { + return Object.assign({}, ...handlers) as MergedActionHandlerMap +} + +export const registerActionHandlers = (handlers: THandlers) => { + for (const [action, handler] of Object.entries(handlers)) { + registerActionHandler(action, handler) + } +} diff --git a/src/app/register-onebot-actions.ts b/src/app/register-onebot-actions.ts new file mode 100644 index 0000000..e03e94f --- /dev/null +++ b/src/app/register-onebot-actions.ts @@ -0,0 +1,49 @@ +import { registerFriendActions } from "./friend/register-friend-actions" +import { friendActionHandlers } from "./friend/register-friend-actions" +import { + groupDomainActionHandlers, + groupActionHandlers, + groupMemberActionHandlers, + registerGroupActions, + registerGroupDomainActions, + registerGroupMemberActions, +} from "./group/register-group-actions" +import { registerLoginActions } from "./login/register-login-actions" +import { loginActionHandlers } from "./login/register-login-actions" +import { + messageActionHandlers, + recallMessageActionHandlers, + registerMessageActions, + registerRecallMessageActions, + registerSendMessageActions, + sendMessageActionHandlers, +} from "./message/register-message-actions" +import { mergeActionHandlerMaps, registerActionHandlers } from "./register-action-handlers" + +export const appActionHandlers = mergeActionHandlerMaps( + loginActionHandlers, + messageActionHandlers, + groupDomainActionHandlers, + friendActionHandlers, +) + +export type AppActionName = keyof typeof appActionHandlers + +export const registerAppOneBotActions = () => { + registerActionHandlers(appActionHandlers) +} + +export { + registerFriendActions, + registerGroupActions, + registerGroupDomainActions, + registerGroupMemberActions, + registerLoginActions, + registerMessageActions, + registerRecallMessageActions, + registerSendMessageActions, + groupActionHandlers, + groupMemberActionHandlers, + recallMessageActionHandlers, + sendMessageActionHandlers, +} diff --git a/src/bootstrap/init-runtime.ts b/src/bootstrap/init-runtime.ts new file mode 100644 index 0000000..bee180c --- /dev/null +++ b/src/bootstrap/init-runtime.ts @@ -0,0 +1,31 @@ +import { constants } from "os" +import { resolve } from "path" +import { useLogger } from "../common/log" + +const log = useLogger('Bootstrap/Runtime') + +const loadLinuxPreload = () => { + if (process.platform !== 'linux') { + return + } + log.info('preload.node') + const m = { exports: {} } + const preload = process.env['YUI_PRELOAD'] || resolve(__dirname, '../preload.node') + process.dlopen( + m, + preload, + constants.dlopen.RTLD_NOW | constants.dlopen.RTLD_GLOBAL + ) +} + +const registerProcessHooks = () => { + process.on('unhandledRejection', (err) => { + log.error('unhandledRejection:', err) + }) +} + +export const initRuntime = () => { + global.module = module + loadLinuxPreload() + registerProcessHooks() +} diff --git a/src/bootstrap/start-app.ts b/src/bootstrap/start-app.ts new file mode 100644 index 0000000..763a9f8 --- /dev/null +++ b/src/bootstrap/start-app.ts @@ -0,0 +1,20 @@ +import { useLogger } from "../common/log" +import { registerAppOneBotActions as initializeOneBot } from "../app/register-onebot-actions" +import { startServers } from "../adapters/server/start-servers" +import { initRuntime } from "./init-runtime" + +const log = useLogger('Bootstrap/App') + +export const startApp = () => { + log.info('Hi Yui bot!!') + + initRuntime() + + log.info('initOnebot') + initializeOneBot() + + log.info('startServer') + startServers() + + log.info('end....') +} diff --git a/src/common/action.ts b/src/common/action.ts index 765f5ef..f42a958 100644 --- a/src/common/action.ts +++ b/src/common/action.ts @@ -1,5 +1,6 @@ import { NIL as NIL_UUID } from "uuid"; -import { BotActionRequest, BotActionResponse } from "../onebot/actions/interfaces"; +import { BotActionRequest, BotActionResponse } from "../onebot/contracts/actions"; +import { CustomError } from "./error/custom-error"; /** * 检查必要的请求参数 @@ -57,4 +58,28 @@ export const checkBaseRequestField = (req: BotActionRequest): BotActionResponse return ret } return undefined -} \ No newline at end of file +} + +export const toBadRequestResponse = (error: unknown): BotActionResponse => { + const result: BotActionResponse = { + id: NIL_UUID, + status: 'failed', + retcode: 10004, + data: null, + message: 'Bad Request' + } + if (error instanceof CustomError) { + result.retcode = error.code + result.message = error.message + } + return result +} + +export const parseBotActionRequest = (rawMessage: string): BotActionRequest => { + const request = JSON.parse(rawMessage) as BotActionRequest + const invalidResponse = checkBaseRequestField(request) + if (invalidResponse !== undefined) { + throw new CustomError(invalidResponse.retcode, invalidResponse.message) + } + return request +} diff --git a/src/common/deep-merge.test.ts b/src/common/deep-merge.test.ts new file mode 100644 index 0000000..4bde18c --- /dev/null +++ b/src/common/deep-merge.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest" +import { mergeDeep } from "./deep-merge" + +describe("mergeDeep", () => { + it("merges nested objects while preserving sibling keys", () => { + const target = { + profile: { + id: 1, + name: "yui", + }, + } + + const result = mergeDeep(target, { + profile: { + role: "admin", + }, + }) + + expect(result).toEqual({ + profile: { + id: 1, + name: "yui", + role: "admin", + }, + }) + }) +}) diff --git a/src/common/error/custom-error.ts b/src/common/error/custom-error.ts new file mode 100644 index 0000000..4bc645a --- /dev/null +++ b/src/common/error/custom-error.ts @@ -0,0 +1,13 @@ +export class CustomError extends Error { + private readonly _code: number + + constructor(code: number, msg: string) { + super(msg) + this.name = 'CustomError' + this._code = code + } + + get code() { + return this._code + } +} diff --git a/src/common/interface/ws.ts b/src/common/interface/ws.ts new file mode 100644 index 0000000..3774b99 --- /dev/null +++ b/src/common/interface/ws.ts @@ -0,0 +1 @@ +export type WSMessageSender = (msg: string) => void diff --git a/src/common/log.ts b/src/common/log.ts index cec2976..555cf2a 100644 --- a/src/common/log.ts +++ b/src/common/log.ts @@ -1,4 +1,4 @@ -import { useAsyncStore } from "../store/async-store" +import { getTraceId } from "../store/runtime" const enum LogLevel { /** 追踪日志 */ @@ -86,8 +86,7 @@ class Logger { } private _log (level: LogLevel, args: unknown[]) { - const asyncStore = useAsyncStore() - let traceId = asyncStore?.getStore()?.get('traceId') + const traceId = getTraceId() if (!isLogEnabled) return // 优先级小的不显示 if (level < CurrentLogLevel) return diff --git a/src/common/utils.ts b/src/common/utils.ts index 738050a..c88215b 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,11 +1,5 @@ import { createHash } from "crypto"; import { createReadStream } from "fs"; -import { sendCustomPkgV2 } from "../native/native"; -import { ReqBody, RspBody, SubCmd0x501ReqBody, SubCmd0x501Rspbody } from "../ntqq/protobuf/subcmd0x501"; -import { CommunicationPkg } from "../ntqq/protobuf/communication"; -import { useLogger } from "./log"; - -const log = useLogger('Utils') export const sleep = (ms: number) => { return new Promise((resolve) => { setTimeout(resolve, ms) @@ -77,31 +71,9 @@ export const calculateFileSha1 = (filePath: string): Promise => new Prom stream.on('error', (err) => reject(err)); }); -export const getHttpTicket = async(): Promise => { - // com.tencent.mobileqq\classes.jar\com\tencent\mobileqq\highway\config\HwServlet.java - log.info('Getting http ticket...'); - const resBuf = await sendCustomPkgV2('HttpConn.0x6ff_501', ReqBody.encode({ - msgSubcmd0x501ReqBody: { - uint64Uin: 0, - uint32IdcId: 0, - uint32Appid: 16, - uint32LoginSigType: 1, - uint32RequestFlag: 3, - rptUint32ServiceTypes: [1, 5, 10, 21], - uint32Term: 2, - uint32Plat: 9, - uint32Net: 8, - bytesVer: '1.0.1', - } - }).finish()) - const resp = RspBody.decode(resBuf) - log.info('Getting http ticket response:', resp); - return resp.msgSubcmd0x501RspBody?.bytesHttpconnSigSession || new Uint8Array(); -} - export const admZip = () => { // 暂时没用,记录一下 require('../major.node').load('internal_admzip', module); // const admzip = new AdmZip.admZip.default(zipPath); // admzip.extractAllTo(path.resolve(__dirname, `../versions/${version}`)); // 把整个压缩包完全解压 -} \ No newline at end of file +} diff --git a/src/http/common/download-rich-media.ts b/src/http/common/download-rich-media.ts deleted file mode 100644 index ae9f6bd..0000000 --- a/src/http/common/download-rich-media.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { NextFunction, Request, Response } from "express" -import { useNTCore } from "../../ntqq/core/core" -import { existsSync } from "fs" -import { useStore } from "../../store/store" -import { useLogger } from "../../common/log" -import { CustomError } from "../../server/error/custom-error" - -const log = useLogger('DownloadRichMedia') -export const downloadRichMedia = (req: Request, res: Response, next: NextFunction) => { - const { getWrapperSession } = useNTCore() - const msgService = getWrapperSession().getMsgService() - const p = req.query - log.info('param:', JSON.stringify(p, null, 4)) - if (typeof p['file_path'] !== 'string') - { - next(new Error('file_path error')) - return; - } - if (p['file_path'] && existsSync(p['file_path'])) - { - // 已存在,不用下载 - res.download(p['file_path']) - return; - } - log.info('need download...') - const { registerEventListener } = useStore() - const rm = registerEventListener('KernelMsgListener/onRichMediaDownloadComplete', 'always', (a) => { - if (a.msgId !== p['msg_id'] || a.msgElementId !== p['element_id']) return; - rm.remove() - log.info('result:', a) - // download 结果的path和请求的会不一致 - res.download(a.filePath) - }) - setTimeout(() => { - rm.remove() - next(new CustomError(10201, 'timeout')) - }, 30000) - // 不存在,下载 - msgService.downloadRichMedia({ - peerUid: p['peer_uid'] as string, - chatType: parseInt(p['chat_type'] as string), - msgId: p['msg_id'] as `${number}`, - elementId: p['element_id'] as `${number}`, - downloadType: parseInt(p['download_type'] as string), - thumbSize: parseInt(p['chat_type'] as string), - filePath: p['file_path'], - fileModelId: p['file_model_id'] as `${number}`, - downSourceType: parseInt(p['down_source_type'] as string), - triggerType: parseInt(p['trigger_type'] as string) - }) -} \ No newline at end of file diff --git a/src/http/gchat/external-download.ts b/src/http/gchat/external-download.ts deleted file mode 100644 index 3af382f..0000000 --- a/src/http/gchat/external-download.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NextFunction, Response, Request } from "express"; -import { get as getHttps } from 'https' -import { IncomingMessage, get as getHttp } from 'http' -import { useLogger } from "../../common/log"; - -const log = useLogger('ExternalDownload') - -export const externalDownload = (req: Request, res: Response, next: NextFunction) => { - log.info('external-download') - // X-ErrNo - res.setHeader('X-ErrNo', 0) - const url = new URL(req.url, `http://${req.headers.host}`); - // 图片地址 - let link = url.searchParams.get('url') - if (link == null) - { - res.status(400) - next() - return - } - link = decodeURIComponent(link) - const callback = (resp: IncomingMessage) => { - resp.pipe(res) - - } - if (link.startsWith('https')) - { - log.info('request https link:', link) - getHttps(link, callback).on('error', (err) => { - log.error('HTTP request error:', err); - res.status(500).send('Internal Server Error'); - }); - return - } - else if (link.startsWith('http')) - { - getHttp(link, callback).on('error', (err) => { - log.error('HTTP request error:', err); - res.status(500).send('Internal Server Error'); - }); - return - } - else - { - res.status(400).send('Bad Request: Invalid URL'); - return - } -} \ No newline at end of file diff --git a/src/http/group/upload-file.ts b/src/http/group/upload-file.ts deleted file mode 100644 index 904e6e3..0000000 --- a/src/http/group/upload-file.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { NextFunction, Request, Response } from "express"; -import { useLogger } from "../../common/log"; -import fs from "fs"; -import fileUpload from 'express-fileupload' -import { useConfigStore } from "../../store/config"; -import { resolve } from "path"; - -const log = useLogger('UploadFile') - -const mimeType2Extension = (type: string) => { - const m: Record = { - 'image/jpeg': '.jpg', - 'image/png': '.png', - } - if (m[type] != undefined) return m[type] - return 'bin' -} - -export const uploadFile = (req: Request, res: Response, next: NextFunction) => { - const { getStoragePath, getConfig } = useConfigStore() - const storage = getStoragePath() - - if (!req.files) { - next(new Error('No file found!')); - return; - } - log.info('req:', req.url, 'files:', req.files) - const file = req.files.file as fileUpload.UploadedFile - - if (!file) { - next(new Error('Parameter "file" is lost!')); - return; - } - if (!file.name) { - next(new Error('Can not identify name!')); - return - } - const now = new Date() - const p = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}` - const realPath = resolve(storage, `./upload/${p}`) - try { - // 创建文件夹 - fs.mkdirSync(realPath, { recursive: true}) - } catch (error) { - - } - log.info('mime type:', file.mimetype) - const ext = mimeType2Extension(file.mimetype) - file.mv(`${realPath}/${file.md5}${ext}`) - const cfg = getConfig() - let host = cfg.yui.http.host - if (host === '0.0.0.0') { - host = '127.0.0.1' - } - res.json({ - path: `${realPath}/${file.md5}${ext}`, - url: `http://${host}:${cfg.yui.http.port}/static/${p}/${file.md5}${ext}`, - md5: file.md5, - size: file.size, - }) -} \ No newline at end of file diff --git a/src/http/route.ts b/src/http/route.ts deleted file mode 100644 index f827ce7..0000000 --- a/src/http/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Router, static as static_ } from 'express' -import { useLogger } from '../common/log' -import { uploadFile } from './group/upload-file' -import { createProxyMiddleware } from 'http-proxy-middleware' -import { externalDownload } from './gchat/external-download' -import { useConfigStore } from '../store/config' -import { resolve } from 'path' -import { downloadRichMedia } from './common/download-rich-media' - -const log = useLogger('HTTP Route') - -const { getStoragePath } = useConfigStore() -const router = Router() -const storage = getStoragePath() -router.use('/static', static_(resolve(storage, './upload'))) -router.get('/downloadRichMedia', downloadRichMedia) -router.get('/download', createProxyMiddleware({ - target: 'https://gchat.qpic.cn', - changeOrigin: true, -})) -router.get('/gchatpic_new/*', createProxyMiddleware({ - target: 'https://gchat.qpic.cn', - changeOrigin: true, -})) -router.post('/group/uploadFile', uploadFile) -router.get('/external-download', externalDownload) -router.get('/test', (req, res, next) => { - log.info('test') - res.json({data: 1}) - next() -}) - -export default router \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 5cc92ad..ea95a6a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,46 +1,11 @@ -import { resolve } from "path"; import { useLogger } from "./common/log" -import { hook } from "./hook"; -import { initNative } from "./native/native"; -import { initOnebot } from "./onebot/onebot"; -import { startServer } from "./server"; -import { test } from "./test/testaction"; -import { hookWrapper } from "./wrapper/hook"; -import { constants } from 'os' +import { startApp } from "./bootstrap/start-app" -global.module = module const log = useLogger('Index') try { - log.info('Hi Yui bot!!') - if (process.platform === 'linux') { - log.info('preload.node') - const m = { exports: {} }; - const preload = process.env['YUI_PRELOAD'] || resolve(__dirname, './preload.node') - process.dlopen( - m, - preload, - constants.dlopen.RTLD_NOW | constants.dlopen.RTLD_GLOBAL - ) - } - - process.on('unhandledRejection', (err) => { - log.error('unhandledRejection:', err) - // process.exit(1) - }) - - log.info('initOnebot') - initOnebot() - - log.info('startServer') - // 启动服务器 - startServer() - - // 测试 - test(module) - - log.info('end....') + startApp() } catch(err) { log.error('Error:', err) process.exit(1) -} \ No newline at end of file +} diff --git a/src/models/contact/group.ts b/src/models/contact/group.ts index 13dee23..80ff04d 100644 --- a/src/models/contact/group.ts +++ b/src/models/contact/group.ts @@ -1,18 +1,8 @@ -import { sendCustomPkgV2 } from "../../native/native"; import { GroupInfoType } from "../types/group"; import { Contact } from "./contact"; import { Member } from "./member"; -import { useLogger } from "../../common/log"; -import { BotMessageSendElements } from "../../onebot/common/message"; -import { gzipSync } from "zlib"; -import { randomBytes } from "crypto"; -import { useNTUserStore } from "../../ntqq/store/user"; -import { useNTCore } from "../../ntqq/core/core"; -import { SendLongMsgReq, SendLongMsgResp, SsoSendLongMsgReq } from "../../ntqq/protobuf/longmsg"; -import { PbSendMsgReq, PbSendMsgResp } from "../../ntqq/protobuf/msg_svc"; -import { ForwardConverter } from "../../common/forward-converter"; - -const log = useLogger('Group') +import { BotMessageSendElements } from "../../onebot/contracts/message"; +import { sendGroupForwardMessage } from "../../ntqq/message/group-forward"; /** * 群组对象 */ @@ -51,89 +41,6 @@ export class Group extends Contact { }) } async sendForwardMessage(forwardData: BotMessageSendElements.ForwardElement) { - const { getUserInfo } = useNTUserStore() - const core = useNTCore() - const user = getUserInfo() - const rand2uuid = (rand: number) => { - return 16777216n << 32n | BigInt(rand) - } - const seq = randomBytes(2).readUInt16BE() - const rand = randomBytes(4).readUInt32BE() - const msgUid = rand2uuid(rand) - log.info('seq:', seq, 'rand:', rand, 'msgUid:', msgUid) - // 1. 组装PbMultiMsgTransmit - const forwardConverter = new ForwardConverter(forwardData) - await forwardConverter.uploadImage() - const msgItem = forwardConverter.toPbMsg() - const transmit = SsoSendLongMsgReq.encode({ - body: { - fileName: "MultiMsg", - data: { - msgs: msgItem, - } - - } - }).finish() - log.info('transmit data:', Buffer.from(transmit).toString('hex')) - // 2. gzip压缩 - const compressed = gzipSync(transmit) - let body = SendLongMsgReq.encode({ - info: { - type: 3, - peer: { - uid: this.id.toString(), - }, - groupCode: this.id, - payload: new Uint8Array(compressed) - }, - settings: { - field1: 4, - field2: 1, - field3: 3, - field4: 0 - } - }).finish() - log.info('full data:', Buffer.from(body).toString('hex')) - const sendLongResp = await sendCustomPkgV2('trpc.group.long_msg_interface.MsgService.SsoSendLongMsg', body) - log.info('send long resp:', Buffer.from(sendLongResp).toString('hex')) - const sendLongResult = SendLongMsgResp.decode(sendLongResp, sendLongResp.byteLength) - const resId = sendLongResult.result?.resId - if (!resId) { - throw new Error('发送失败,没有获取到resId') - } - - const xml = forwardConverter.toXml(resId) - - log.info('send message.') - const sendResultData = await sendCustomPkgV2('MessageSvc.PbSendMsg', PbSendMsgReq.encode({ - routingHead: { - grp: { - groupCode: this.id, - }, - }, - contentHead: { - pkgNum: 1, - pkgIndex: 0, - divSeq: 0, - }, - msgBody: { - richText: { - elems: [ - { - richMsg: { - bytesTemplate1: new Uint8Array(Buffer.from(xml)), - uint32ServiceId: 35, - bytesMsgResid: new Uint8Array(Buffer.from(resId || '')), - } - } - ], - } - }, - msgSeq: randomBytes(2).readUInt16BE(), - msgRand: randomBytes(4).readUInt32BE(), - msgVia: 0, - }).finish()) - const sendResult = PbSendMsgResp.decode(sendResultData, sendResultData.byteLength) - return sendResult + return await sendGroupForwardMessage(this.id, forwardData) } } diff --git a/src/models/contact/member.ts b/src/models/contact/member.ts index 2254819..2836b7c 100644 --- a/src/models/contact/member.ts +++ b/src/models/contact/member.ts @@ -1,10 +1,9 @@ import { useLogger } from "../../common/log"; -import { sendCustomPkg } from "../../native/native"; -import { useNTCore } from "../../ntqq/core/core"; -import { CommunicationPkg, PaiYiPaiReq } from "../../ntqq/protobuf/communication"; -import { GroupMemberDetailInfoType } from "../../onebot/common/interfaces"; -import { CustomError } from "../../server/error/custom-error"; -import { useStore } from "../../store/store"; +import { getNTGroupService, getNTUixConvertService } from "../../ntqq/core/core"; +import { nudgeGroupMember } from "../../ntqq/group/nudge"; +import { GroupMemberDetailInfoType } from "../../onebot/contracts/common"; +import { createNTEventWaiter } from "../../ntqq/query/wait"; +import { CustomError } from "../../common/error/custom-error"; import { MemberUserInfoType } from "../types/user"; import { BaseUser } from "./base-user"; @@ -40,9 +39,7 @@ export class Member extends BaseUser { if (!this.uid) { log.info('getGroupMemberInfoByUin', this._groupId, this.id) - const { getWrapperSession } = useNTCore() - const session = getWrapperSession() - const convert = session.getUixConvertService() + const convert = getNTUixConvertService() const info = await convert.getUid([`${this.id}`]) log.info('get uid:', info) const uid = info?.uidInfo?.get(`${this.id}`) @@ -58,32 +55,27 @@ export class Member extends BaseUser { if (!this.uid) throw new CustomError(1, 'uid lost') const uid = this.uid return new Promise(async (resolve, reject) => { - const { registerEventListener } = useStore() - let remover: { remove: () => void } | null = null - // 超时拒绝 - let time = setTimeout(() => { - if (remover) - remover?.remove() - reject('getGroupMemberInfoByUid timeout') - }, 30000) - remover = registerEventListener(`KernelGroupListener/onMemberInfoChange`, 'always', (groupId: `${number}`, b: number, payload: Map<`u_${string}`, GroupMemberDetailInfoType>) => { - if (groupId !== `${this._groupId}`) return - if (!payload.has(uid)) return - remover?.remove() - // 清除超时计时 - clearTimeout(time) - const result = payload.get(uid) - if (!result) { - log.error('info:', result, 'payload:', payload) + try { + const waiter = createNTEventWaiter< + [groupId: `${number}`, _unused: number, payload: Map<`u_${string}`, GroupMemberDetailInfoType>], + GroupMemberDetailInfoType | undefined + >({ + event: 'KernelGroupListener/onMemberInfoChange', + match: (groupId, _unused, payload) => groupId === `${this._groupId}` && payload.has(uid), + select: (_groupId, _unused, payload) => payload.get(uid), + }) + const service = getNTGroupService() + const result = await service.getMemberInfo(`${this._groupId}`, [uid], true) + log.info(`getGroupMemberInfoByUid[${this._groupId}]:`, result) + + const memberInfo = await waiter.promise + if (!memberInfo) { throw new CustomError(1, 'Failed to get member info!') } - resolve(result) - }) - const { getWrapperSession } = useNTCore() - const session = getWrapperSession() - const service = session.getGroupService() - const result = await service.getMemberInfo(`${this._groupId}`, [uid], true) - log.info(`getGroupMemberInfoByUid[${this._groupId}]:`, result) + resolve(memberInfo) + } catch (error) { + reject(error) + } }) } /** @@ -92,16 +84,6 @@ export class Member extends BaseUser { * @returns */ async nudge() { - const result = await sendCustomPkg('OidbSvcTrpcTcp.0xed3_1', CommunicationPkg.encode({ - uint32Command: 0xed3, - uint32ServiceType: 1, - bytesBodybuffer: PaiYiPaiReq.encode({ - uint64ToUin: this.id, - uint64GroupCode: this._groupId, - uint32NudgeType: 0, - }).finish(), - trpcTransInfo: [], - }).finish()) - return result.uint32Result === 0 + return await nudgeGroupMember(this._groupId, this.id) } -} \ No newline at end of file +} diff --git a/src/native/native.ts b/src/native/native.ts index 79dcc05..1c071ac 100644 --- a/src/native/native.ts +++ b/src/native/native.ts @@ -1,16 +1,14 @@ import { addPkg, install } from 'yui-native' -import { useConfigStore } from '../store/config' +import { getSignature } from '../store/config' import { useLogger } from '../common/log' -import { useNTCore } from '../ntqq/core/core' +import { getNTSearchService } from '../ntqq/core/core' +import { requireCurrentNTSelfInfo } from '../ntqq/core/runtime-store' import { CommunicationPkg } from '../ntqq/protobuf/communication' -import { useNTConfig } from '../ntqq/store/config' -import { useNTUserStore } from '../ntqq/store/user' import { sleep } from '../common/utils' const log = useLogger('Native') export const initNative = (name: string) => { // 获取函数签名 - const { getSignature } = useConfigStore() const sig = getSignature() const sigData: YuiNativeWrapper.SignatureType = {} @@ -54,16 +52,12 @@ export const initNative = (name: string) => { * @returns */ export const sendCustomPkg = async (cmd: string, data: Uint8Array) => { - const { getWrapperSession } = useNTCore() - const session = getWrapperSession() - log.debug('wrapper session:', session) - const search = session.getSearchService() + const search = getNTSearchService() log.debug('search service:', search) - const { getCurrentAccountData } = useNTUserStore() - const info = getCurrentAccountData() + const info = requireCurrentNTSelfInfo() const send = addPkg({ data: data, - uin: info.info.uin, + uin: info.uin, cmd, }) log.debug('emit start ...') @@ -89,17 +83,13 @@ export const sendCustomPkg = async (cmd: string, data: Uint8Array) => { * @returns */ export const sendCustomPkgV2 = async (cmd: string, data: Uint8Array) => { - const { getWrapperSession } = useNTCore() - const session = getWrapperSession() - log.debug('wrapper session:', session) - const search = session.getSearchService() + const search = getNTSearchService() log.debug('search service:', search) - const { getCurrentAccountData } = useNTUserStore() - const info = getCurrentAccountData() + const info = requireCurrentNTSelfInfo() log.info('send pb:', cmd, Buffer.from(data).toString('hex')) const send = addPkg({ data: data, - uin: info.info.uin, + uin: info.uin, cmd, }) log.debug('emit start ...') @@ -115,4 +105,4 @@ export const sendCustomPkgV2 = async (cmd: string, data: Uint8Array) => { log.debug('emit over, await ...') const result = await send return result -} \ No newline at end of file +} diff --git a/src/ntqq/common/clipboard.ts b/src/ntqq/common/clipboard.ts index ef4061c..dca0ab0 100644 --- a/src/ntqq/common/clipboard.ts +++ b/src/ntqq/common/clipboard.ts @@ -1,12 +1,11 @@ import { useLogger } from "../../common/log" import { convertNTMsgElement2BotMsgElement } from "../../transfer/message/convert" -import { useNTCore } from "../core/core" +import { getNTNodeMiscService } from "../core/core" const log = useLogger('NTQQ/Clipboard') export const NTParseClipboard2Msg = async () => { - const { getWrapperSession } = useNTCore() - const miscService = getWrapperSession().getNodeMiscService() + const miscService = getNTNodeMiscService() const ret = await miscService.wantParseClipboard() log.info('cliboard result:', typeof ret, JSON.stringify(ret, null, 4)) return convertNTMsgElement2BotMsgElement({chatType: 1}, '0', ret.msgElements) -} \ No newline at end of file +} diff --git a/src/ntqq/common/face-emoji.ts b/src/ntqq/common/face-emoji.ts index 2a0aab0..0061472 100644 --- a/src/ntqq/common/face-emoji.ts +++ b/src/ntqq/common/face-emoji.ts @@ -1,9 +1,8 @@ -import { useNTCore } from "../core/core" +import { getNTBaseEmojiService } from "../core/core" export const NTgetFaceEmoji = async () => { // 获取全部表情 - const { getWrapperSession } = useNTCore() - const emojiService = getWrapperSession().getBaseEmojiService() + const emojiService = getNTBaseEmojiService() const emojis = await emojiService.fetchFullSysEmojis({ pullMoment: 1, pullType: 7, @@ -13,4 +12,4 @@ export const NTgetFaceEmoji = async () => { thresholdValue: 40 }) // 获取最近常用表情 -} \ No newline at end of file +} diff --git a/src/ntqq/common/fs-api.ts b/src/ntqq/common/fs-api.ts index 1a3ac0b..b129205 100644 --- a/src/ntqq/common/fs-api.ts +++ b/src/ntqq/common/fs-api.ts @@ -1,6 +1,6 @@ import { randomUUID } from "crypto" import { FileType, ImageSizeInfo } from "./fsapi" -import { useNTCore } from "../core/core" +import { getNTWrapperUtil } from "../core/core" import { useLogger } from "../../common/log" import { parse } from 'file-type-mime' import { readFileSync } from "fs" @@ -77,9 +77,8 @@ export const getImageSizeFromPath = async (path: string) => { * @returns boolean */ export const copyFile = (from: string, to: string) => { - const { getWrapperUtil } = useNTCore() - const util = getWrapperUtil() + const util = getNTWrapperUtil() const ret = util.copyFile(from, to) log.info('copyFile:', ret) return ret -} \ No newline at end of file +} diff --git a/src/ntqq/common/nt-api.ts b/src/ntqq/common/nt-api.ts index 416e8d5..077b5d2 100644 --- a/src/ntqq/common/nt-api.ts +++ b/src/ntqq/common/nt-api.ts @@ -1,5 +1,4 @@ - -import { useNTCore } from "../core/core" +import { getNTMsgService } from "../core/core" /** * 调用NT自带的NtApi获取存储路径 @@ -9,8 +8,7 @@ import { useNTCore } from "../core/core" * @returns 存储路径 */ export const getRichMediaFilePathForGuild = (md5: string, fileName: string) => { - const { getWrapperSession } = useNTCore() - const msgService = getWrapperSession().getMsgService() + const msgService = getNTMsgService() return msgService.getRichMediaFilePathForGuild({ md5HexStr: md5, fileName: fileName, @@ -21,4 +19,4 @@ export const getRichMediaFilePathForGuild = (md5: string, fileName: string) => { downloadType: 1, file_uuid: '' }) -} \ No newline at end of file +} diff --git a/src/ntqq/common/utils.ts b/src/ntqq/common/utils.ts index ec64e66..18e8da0 100644 --- a/src/ntqq/common/utils.ts +++ b/src/ntqq/common/utils.ts @@ -1,7 +1,7 @@ import { NTPackageInfo } from "./types/utils" import { existsSync, readFileSync } from "fs" import path from "path" -import { CustomError } from "../../server/error/custom-error" +import { CustomError } from "../../common/error/custom-error" export const getNTPackageInfo = (): NTPackageInfo => { const pkgPath = path.resolve(__dirname, './package.json') @@ -15,4 +15,4 @@ export const getNTPackageInfo = (): NTPackageInfo => { return { version: pkg.version } -} \ No newline at end of file +} diff --git a/src/ntqq/core/core.test.ts b/src/ntqq/core/core.test.ts new file mode 100644 index 0000000..5f29097 --- /dev/null +++ b/src/ntqq/core/core.test.ts @@ -0,0 +1,58 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +const runtimeState = { + wrapperEngine: { kind: 'engine' }, + loginService: { kind: 'login' }, + wrapperSession: { kind: 'session' }, + startupSessionWrapper: { kind: 'startup' }, +} + +const mockState = vi.hoisted(() => ({ + initialized: false, +})) + +vi.mock("./service/nt-wrapper", () => ({ + getNTWrapper: vi.fn(() => { + mockState.initialized = true + return {} + }), + resetNTWrapper: vi.fn(), +})) + +vi.mock("./runtime-store", () => ({ + getCurrentNTUserInfo: vi.fn(), + patchCurrentNTSelfIdentity: vi.fn(), + requireCurrentNTRuntime: vi.fn(() => { + if (!mockState.initialized) { + throw new Error('runtime not initialized') + } + return runtimeState + }), + requireCurrentNTSelfInfo: vi.fn(), + requireCurrentNTUserInfo: vi.fn(), +})) + +vi.mock("./dispatcher", () => ({ + createNTListenerProxy: vi.fn(() => ({})), +})) + +import { getNTLoginService, getNTWrapperEngine, getNTWrapperSession } from "./core" + +describe("nt core runtime service access", () => { + beforeEach(() => { + mockState.initialized = false + vi.clearAllMocks() + }) + + it("initializes wrapper runtime before reading wrapper engine", () => { + expect(getNTWrapperEngine()).toBe(runtimeState.wrapperEngine) + }) + + it("initializes wrapper runtime before reading login service", () => { + expect(getNTLoginService()).toBe(runtimeState.loginService) + }) + + it("initializes wrapper runtime before reading wrapper session", () => { + expect(getNTWrapperSession()).toBe(runtimeState.wrapperSession) + }) +}) diff --git a/src/ntqq/core/core.ts b/src/ntqq/core/core.ts index cd5020d..16b7a11 100644 --- a/src/ntqq/core/core.ts +++ b/src/ntqq/core/core.ts @@ -1,89 +1,151 @@ -import { useListenerProxy, useNTDispatcher } from "./dispatcher" +import { createNTListenerProxy } from "./dispatcher" import { useLogger } from "../../common/log" import { NodeIKernelLoginService } from "../types/services/NodeIKernelLoginService" -import { useAsyncStore } from "../../store/async-store" -import { useNTUserStore } from "../store/user" -import { resetNTWrapper, useNTWrapper } from "./service/nt-wrapper" -import { CustomError } from "../../server/error/custom-error" +import { + getCurrentNTUserInfo, + patchCurrentNTSelfIdentity, + requireCurrentNTRuntime, + requireCurrentNTSelfInfo, + requireCurrentNTUserInfo, +} from "./runtime-store" +import { getNTWrapper, resetNTWrapper } from "./service/nt-wrapper" const log = useLogger('NTStore') -export const useNTCore = () => ({ - getWrapperUtil: () => { - const wrapper = useNTWrapper() - return wrapper.NodeQQNTWrapperUtil - }, - /** - * 暂时只调用一次 - * @returns - */ - getGlobalAdapter: () => { - return useListenerProxy('NodeIGlobalAdapter') - }, - /** - * 暂时只调用一次 - * @returns - */ - getWrapperEngine: () => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - const uin: number = s?.get('uin') - if (!uin) { - throw new CustomError(500, 'id error') - } - const userStore = useNTUserStore() - const accountNTData = userStore.getCurrentAccountData() - return accountNTData.wrapperEngine - }, - /** - * 频繁调用 - * @returns - */ - getLoginService: (): NodeIKernelLoginService => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - const uin: number = s?.get('uin') - if (!uin) { - throw new CustomError(500, 'id error') - } - const userStore = useNTUserStore() - const accountNTData = userStore.getCurrentAccountData() - return accountNTData.loginService - }, - /** - * 频繁调用 - * @returns - */ - getWrapperSession: (): NTNativeWrapper.NodeIQQNTWrapperSession => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - const uin: number = s?.get('uin') - if (!uin) { - throw new CustomError(500, 'id error') - } - const userStore = useNTUserStore() - const accountNTData = userStore.getCurrentAccountData() - return accountNTData.wrapperSession - }, - /** - * 频繁调用 - * @returns - */ - getStartupSessionWrapper: (): NTNativeWrapper.NodeIQQNTStartupSessionWrapper => { - const wrapper = useNTWrapper() - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - const uin: number = s?.get('uin') - if (!uin) { - throw new CustomError(500, 'id error') - } - const userStore = useNTUserStore() - const accountNTData = userStore.getCurrentAccountData() - return accountNTData.startupSessionWrapper - }, - - destroy: () => { - log.warn('destroy ntqq core') - resetNTWrapper() +export { + getCurrentNTUserInfo, + patchCurrentNTSelfIdentity, + requireCurrentNTRuntime, + requireCurrentNTSelfInfo, + requireCurrentNTUserInfo, +} + +const ensureNTWrapperRuntime = () => { + getNTWrapper() +} + +const requireNTRuntimeServiceState = () => { + ensureNTWrapperRuntime() + return requireCurrentNTRuntime() +} + +const getNTSessionService = ( + getService: (session: NTNativeWrapper.NodeIQQNTWrapperSession) => TService, +) => { + return getService(getNTWrapperSession()) +} + +export const getNTWrapperUtil = () => { + const wrapper = getNTWrapper() + return wrapper.NodeQQNTWrapperUtil +} + +/** + * 暂时只调用一次 + */ +export const getNTGlobalAdapter = () => { + return createNTListenerProxy('NodeIGlobalAdapter') +} + +/** + * 暂时只调用一次 + */ +export const getNTWrapperEngine = () => { + return requireNTRuntimeServiceState().wrapperEngine +} + +/** + * 频繁调用 + */ +export const getNTLoginService = (): NodeIKernelLoginService => { + return requireNTRuntimeServiceState().loginService +} + +/** + * 频繁调用 + */ +export const getNTWrapperSession = (): NTNativeWrapper.NodeIQQNTWrapperSession => { + return requireNTRuntimeServiceState().wrapperSession +} + +export const getNTMsgService = () => { + return getNTSessionService((session) => session.getMsgService()) +} + +export const getNTProfileService = () => { + return getNTSessionService((session) => session.getProfileService()) +} + +export const getNTGroupService = () => { + return getNTSessionService((session) => session.getGroupService()) +} + +export const getNTBuddyService = () => { + return getNTSessionService((session) => session.getBuddyService()) +} + +export const getNTProfileLikeService = () => { + return getNTSessionService((session) => session.getProfileLikeService()) +} + +export const getNTNodeMiscService = () => { + return getNTSessionService((session) => session.getNodeMiscService()) +} + +export const getNTBaseEmojiService = () => { + return getNTSessionService((session) => session.getBaseEmojiService()) +} + +export const getNTStorageCleanService = () => { + return getNTSessionService((session) => session.getStorageCleanService()) +} + +export const getNTSearchService = () => { + return getNTSessionService((session) => session.getSearchService()) +} + +export const getNTUixConvertService = () => { + return getNTSessionService((session) => session.getUixConvertService()) +} + +export const getNTMSFService = () => { + return getNTSessionService((session) => session.getMSFService()) +} + +export const getNTUnitedConfigService = () => { + return getNTSessionService((session) => session.getUnitedConfigService()) +} + +/** + * 频繁调用 + */ +export const getNTStartupSessionWrapper = (): NTNativeWrapper.NodeIQQNTStartupSessionWrapper => { + ensureNTWrapperRuntime() + return requireNTRuntimeServiceState().startupSessionWrapper +} + +export const createNTSessionLifecycleAdapters = () => { + return { + dependsAdapter: createNTListenerProxy('DependsAdapter'), + dispatcherAdapter: createNTListenerProxy('DispatcherAdapter'), + sessionListener: createNTListenerProxy('KernelSessionListener'), } -}) \ No newline at end of file +} + +export const initializeCurrentNTWrapperSession = (initConfig: NodeIQQNTWrapperSessionType.Init) => { + const session = getNTWrapperSession() + const { dependsAdapter, dispatcherAdapter, sessionListener } = createNTSessionLifecycleAdapters() + session.init(initConfig, dependsAdapter, dispatcherAdapter, sessionListener) + return session +} + +export const startNTStartupSession = () => { + const startupSessionWrapper = getNTStartupSessionWrapper() + startupSessionWrapper.start() + return startupSessionWrapper +} + +export const destroyNTCore = () => { + log.warn('destroy ntqq core') + resetNTWrapper() +} diff --git a/src/ntqq/core/dispatcher.ts b/src/ntqq/core/dispatcher.ts index 781fab7..b2fddac 100644 --- a/src/ntqq/core/dispatcher.ts +++ b/src/ntqq/core/dispatcher.ts @@ -1,28 +1,35 @@ -import EventEmitter from "events" import { useLogger } from "../../common/log" -import { useAsyncStore } from "../../store/async-store" -import { useNTUserStore } from "../store/user" -import { CustomError } from "../../server/error/custom-error" - +import { CustomError } from "../../common/error/custom-error" +import { requireCurrentNTRuntime } from "./runtime-store" const log = useLogger('Dispatcher') +const requireCurrentDispatcher = () => { + const runtime = requireCurrentNTRuntime() + const uin = runtime.info.uin + const dispatcher = runtime.dispatcher + if (!dispatcher) { + log.error('dispatcher does not exist for current uin:', uin) + throw new CustomError(500, `dispatcher of ${uin} does not exists.`) + } + return { + uin, + dispatcher, + } +} + /** * * @returns 调度器 */ -export const useNTDispatcher = () => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - log.info('useNTDispatcher async store:', s) - const uin: number = s?.get('uin') - if (!uin) - { - throw new CustomError(500, 'uin error') - } - const { getCurrentAccountData } = useNTUserStore() - const dispatcher = getCurrentAccountData().dispatcher - return dispatcher +export const getCurrentNTDispatcher = () => { + const { uin, dispatcher } = requireCurrentDispatcher() + log.info('current dispatcher uin:', uin) + return dispatcher +} + +export const emitCurrentNTDispatcherEvent = (eventFullName: `${string}/${string}`, ...args: any[]) => { + getCurrentNTDispatcher().emit(eventFullName, ...args) } /** @@ -30,47 +37,22 @@ export const useNTDispatcher = () => { * @param name 监听类型名 * @returns 监听代理 */ -export const useListenerProxy = (name: string) => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - log.info('useListenerProxy async store:', name, s) - const handler = { - get(obj: any, prop: string) { - log.info(`${name}/${prop}`, 'handle get', obj, prop, name) - const uin: number = s?.get('uin') - if (!uin) - { - throw new CustomError(500, 'id error') - } - return (...args: any[]) => { - log.debug(`${name}/${prop}`, 'get async store') - const asyncStore = useAsyncStore() - if (!s) { - log.error(`${name}/${prop}`, 'Async store error!') - throw new CustomError(500, 'Async store error!') - } - asyncStore.run(s, () => { - log.debug(`${name}/${prop}`, 'useListenerProxy call') - const s = asyncStore.getStore() - log.debug(`${name}/${prop}`, 'useListenerProxy call async store:', s) - s?.set('uin', uin) - log.debug(`${name}/${prop}`, JSON.stringify(args)) - const { getCurrentAccountData } = useNTUserStore() - const dispatcher = getCurrentAccountData().dispatcher - if (!dispatcher){ - log.error(`${name}/${prop}`, `dispatcher of ${uin} does not exists.`) - throw new CustomError(500, `dispatcher of ${uin} does not exists.`) - } - dispatcher.emit(`${name}/${prop}`, ...args) - }) - } - }, - set(obj: any, prop: string, nv: any, rec: any) { - log.info('handle set', obj, prop) - return true; - }, - }; - - const p = new Proxy({}, handler); - return p -} \ No newline at end of file +export const createNTListenerProxy = (name: string) => { + log.info('createNTListenerProxy:', name) + const handler = { + get(_obj: unknown, prop: string) { + return (...args: any[]) => { + const eventFullName = `${name}/${prop}` as `${string}/${string}` + log.debug(eventFullName, JSON.stringify(args)) + emitCurrentNTDispatcherEvent(eventFullName, ...args) + } + }, + set() { + return true + }, + } + + return new Proxy({}, handler) +} + +export { requireCurrentDispatcher } diff --git a/src/ntqq/core/lifecycle-phases.ts b/src/ntqq/core/lifecycle-phases.ts new file mode 100644 index 0000000..03c391d --- /dev/null +++ b/src/ntqq/core/lifecycle-phases.ts @@ -0,0 +1,160 @@ +import { useLogger } from "../../common/log" +import { sleep } from "../../common/utils" +import { getNTPackageInfo } from "../common/utils" +import { + getNTAppId, + getNTAppInfo, + getNTConfigStoreFolder, + getNTDeviceInfo, + getNTDesktopPlatformType, +} from "../store/config" +import { + getNTGlobalAdapter, + getNTLoginService, + getNTMSFService, + getNTWrapperEngine, + initializeCurrentNTWrapperSession, + startNTStartupSession, +} from "./core" +import { getNTWrapper } from "./service/nt-wrapper" +import path from "path" +import { release, userInfo } from "os" + +const log = useLogger('NTCore/LifecyclePhases') + +export const buildNTDesktopInitConfig = () => { + const configFolder = getNTConfigStoreFolder() + log.info('configFolder:', configFolder) + const globalPath = path.resolve(configFolder, './global') + const appInfo = getNTAppInfo() + const config: NodeIQQNTWrapperEngineType.Init = { + base_path_prefix: '', + platform_type: appInfo.platform_type, + app_type: appInfo.app_type, + app_version: appInfo.app_version, + os_version: appInfo.os_version, + use_xlog: true, + qua: appInfo.qua, + global_path_config: { + desktopGlobalPath: globalPath + }, + thumb_config: { + maxSide: 324, + minSide: 48, + longLimit: 6, + density: 2 + } + } + return { + configFolder, + globalPath, + config, + } +} + +export const initializeNTWrapperEngineRuntime = () => { + const { config, globalPath } = buildNTDesktopInitConfig() + const globalAdapter = getNTGlobalAdapter() + const wrapperEngine = getNTWrapperEngine() + log.info('cfg:', config) + log.info('globalAdapter:', globalAdapter) + const initResult = wrapperEngine.initWithDeskTopConfig(config, globalAdapter) + log.info('initResult:', initResult) + return { + globalPath, + initResult, + } +} + +export const initializeNTLoginRuntime = async (globalPath: string) => { + const pkgInfo = getNTPackageInfo() + const login = getNTLoginService() + login.initConfig({ + machineId: '', + appid: getNTAppId(), + platVer: release(), + commonPath: globalPath, + clientVer: pkgInfo.version, + hostName: userInfo().username, + externalVersion: false + }) + const hotUpdateSign = await login.getLoginMiscData('hotUpdateSign') + log.info('hotUpdateSign:', hotUpdateSign) + const setLoginMiscData = await login.setLoginMiscData('hotUpdateSign', '') + log.info('setLoginMiscData:', setLoginMiscData) + const connect = login.connect() + log.info('connect:', connect) + const appVersion = await login.getLoginMiscData('appVersion') + log.info('appVersion:', appVersion) + const msfStatus = await login.getMsfStatus() + log.info('MsfStatus:', msfStatus) + const isLogout = await login.getLoginMiscData('isLogout') + log.info('isLogout:', isLogout) +} + +export const waitNTLoginRuntimeReadyPhase = async () => { + log.info('phase: wait login runtime ready') + await sleep(2000) +} + +export const prepareNTBaseEnvironmentPhases = async () => { + log.info('phase: init wrapper engine') + const { globalPath } = initializeNTWrapperEngineRuntime() + log.info('phase: init login config and connect') + await initializeNTLoginRuntime(globalPath) + await waitNTLoginRuntimeReadyPhase() + return { globalPath } +} + +export const buildNTSessionInitConfig = (uin: `${number}`, uid: `u_${string}`): NodeIQQNTWrapperSessionType.Init => { + const configFolder = getNTConfigStoreFolder() + const pkgInfo = getNTPackageInfo() + + return { + selfUin: uin, + selfUid: uid, + desktopPathConfig: { + account_path: configFolder + }, + clientVer: pkgInfo.version, + a2: '', + d2: '', + d2Key: '', + machineId: '', + platform: getNTDesktopPlatformType(), + platVer: release(), + appid: getNTAppId(), + rdeliveryConfig: { + appKey: '', + systemId: 0, + appId: '', + logicEnvironment: '', + platform: 5, + language: '', + sdkVersion: '', + userId: '', + appVersion: '', + osVersion: '', + bundleId: '', + serverUrl: '', + fixedAfterHitKeys: [''] + }, + defaultFileDownloadPath: '/home/msojocs', + deviceInfo: getNTDeviceInfo(), + deviceConfig: '{"appearance":{"isSplitViewMode":true},"msg":{}}', + deviceType: 3 + } +} + +export const startNTSessionBootstrapPhases = (uin: `${number}`, uid: `u_${string}`) => { + log.info('phase: start wrapper session', uin, uid) + const msfService = getNTMSFService() + log.info('getMsfStatus:', msfService.getMsfStatus()) + + const wrapper = getNTWrapper() + wrapper.NodeQQNTWrapperUtil.emptyWorkingSet(61444) + + const initConfig = buildNTSessionInitConfig(uin, uid) + initializeCurrentNTWrapperSession(initConfig) + startNTStartupSession() +} diff --git a/src/ntqq/core/lifecycle.ts b/src/ntqq/core/lifecycle.ts new file mode 100644 index 0000000..3088918 --- /dev/null +++ b/src/ntqq/core/lifecycle.ts @@ -0,0 +1,158 @@ +import { useLogger } from "../../common/log" +import { registerEventListener } from "../../store/event-registry" +import { + getNTLoginService, +} from "./core" +import { initLogin } from "./service/login" +import { NodeIKernelLoginService } from "../types/services/NodeIKernelLoginService" +import { + buildNTDesktopInitConfig as buildLifecycleNTDesktopInitConfig, + buildNTSessionInitConfig as buildLifecycleNTSessionInitConfig, + initializeNTLoginRuntime as initializeLifecycleNTLoginRuntime, + initializeNTWrapperEngineRuntime as initializeLifecycleNTWrapperEngineRuntime, + prepareNTBaseEnvironmentPhases, + startNTSessionBootstrapPhases, + waitNTLoginRuntimeReadyPhase, +} from "./lifecycle-phases" +import { runNTLoginSuccessPhases } from "./login-success-lifecycle" +import { initializeNTPostLoginServices as initializeNTPostLoginServicePhases } from "./post-login-services" +import { + runNTSessionInitCompletePhases, + runNTSessionReadyPhases, +} from "./session-lifecycle" + +const log = useLogger('NTCore/Lifecycle') +let ntLoginLifecycleBound = false +let ntSessionLifecycleBound = false +let ntLoginSuccessLifecycleBound = false + +const runNTWrapperEnginePhase = () => { + log.info('phase: init wrapper engine') + return initializeLifecycleNTWrapperEngineRuntime() +} + +const runNTLoginConnectPhase = async (globalPath: string) => { + log.info('phase: init login config and connect') + await initializeLifecycleNTLoginRuntime(globalPath) +} + +const runNTLoginRuntimeReadyPhase = async () => { + await waitNTLoginRuntimeReadyPhase() +} + +const runNTBaseEnvironmentPhases = async () => { + const { globalPath } = runNTWrapperEnginePhase() + await runNTLoginConnectPhase(globalPath) + await runNTLoginRuntimeReadyPhase() + return { globalPath } +} + +export const buildNTDesktopInitConfig = () => { + return buildLifecycleNTDesktopInitConfig() +} + +export const initializeNTWrapperEngineRuntime = () => { + return initializeLifecycleNTWrapperEngineRuntime() +} + +export const initializeNTLoginRuntime = async (globalPath: string) => { + await initializeLifecycleNTLoginRuntime(globalPath) +} + +export const prepareNTCoreRuntime = () => { + return runNTWrapperEnginePhase() +} + +export const connectNTLoginRuntime = async (globalPath: string) => { + await runNTLoginConnectPhase(globalPath) +} + +export const waitNTLoginRuntimeReady = async () => { + await runNTLoginRuntimeReadyPhase() +} + +export const getNTLoginServiceWhenReady = async (): Promise => { + await waitNTLoginRuntimeReady() + return getNTLoginService() +} + +export const bindNTLoginLifecycle = () => { + if (ntLoginLifecycleBound) { + log.info('phase: login lifecycle already bound') + return + } + + log.info('phase: bind login lifecycle') + initLogin() + bindNTLoginSuccessLifecycle() + ntLoginLifecycleBound = true +} + +export const initializeNTRuntime = async () => { + await prepareNTBaseEnvironment() + bindNTLoginLifecycle() +} + +export const prepareNTBaseEnvironment = async () => { + log.info('phase: prepare base environment') + return prepareNTBaseEnvironmentPhases() +} + +const handleNTQRCodeLoginSuccess = async (info: NodeIKernelLoginListenerType.UserInfo) => { + await runNTLoginSuccessPhases(info, startNTWrapperSessionRuntime) +} + +export const bindNTLoginSuccessLifecycle = () => { + if (ntLoginSuccessLifecycleBound) { + log.info('phase: login success lifecycle already bound') + return + } + + log.info('phase: bind login success lifecycle') + registerEventListener('KernelLoginListener/onQRCodeLoginSucceed', 'always', handleNTQRCodeLoginSuccess) + ntLoginSuccessLifecycleBound = true +} + +export const initializeNTPostLoginServices = async () => { + await initializeNTPostLoginServicePhases() +} + +const handleNTSessionCreate = async (result: unknown) => { + log.info('onOpentelemetryInit called:', result) + log.info('NTWrapperSession init successful!') + await runNTSessionReadyPhases() +} + +const handleNTSessionInitComplete = async () => { + await runNTSessionInitCompletePhases() +} + +export const bindNTSessionLifecycle = () => { + if (ntSessionLifecycleBound) { + log.info('phase: session lifecycle already bound') + return + } + + log.info('phase: bind session lifecycle') + registerEventListener('KernelSessionListener/onNTSessionCreate', 'always', handleNTSessionCreate) + registerEventListener('KernelSessionListener/onSessionInitComplete', 'always', handleNTSessionInitComplete) + ntSessionLifecycleBound = true +} + +export const buildNTSessionInitConfig = (uin: `${number}`, uid: `u_${string}`): NodeIQQNTWrapperSessionType.Init => { + return buildLifecycleNTSessionInitConfig(uin, uid) +} + +const initializeNTSessionRuntime = (initConfig: NodeIQQNTWrapperSessionType.Init) => { + bindNTSessionLifecycle() + startNTSessionBootstrapPhases(initConfig.selfUin as `${number}`, initConfig.selfUid as `u_${string}`) +} + +const runNTSessionStartPhases = (uin: `${number}`, uid: `u_${string}`) => { + const initConfig = buildNTSessionInitConfig(uin, uid) + initializeNTSessionRuntime(initConfig) +} + +export const startNTWrapperSessionRuntime = async (uin: `${number}`, uid: `u_${string}`) => { + runNTSessionStartPhases(uin, uid) +} diff --git a/src/ntqq/core/login-success-lifecycle.ts b/src/ntqq/core/login-success-lifecycle.ts new file mode 100644 index 0000000..1ef13b6 --- /dev/null +++ b/src/ntqq/core/login-success-lifecycle.ts @@ -0,0 +1,45 @@ +import { useLogger } from "../../common/log" +import { getCurrentNTUserInfo, patchCurrentNTSelfIdentity } from "./core" + +const log = useLogger('NTCore/LoginSuccessLifecycle') + +type NTLoginSuccessIdentity = Pick + +const requireNTLoginSuccessUserInfo = () => { + const userInfo = getCurrentNTUserInfo() + if (!userInfo) { + log.error('qrcode login success but user info is empty') + return null + } + return userInfo +} + +const patchNTLoginSuccessIdentity = (info: NTLoginSuccessIdentity) => { + log.info('qrcode login success payload:', info) + return patchCurrentNTSelfIdentity({ + uid: info.uid, + uin: parseInt(info.uin), + }) +} + +const startNTSessionAfterLoginSuccess = async ( + info: NTLoginSuccessIdentity, + startSession: (uin: `${number}`, uid: `u_${string}`) => Promise, +) => { + const patchedUserInfo = patchNTLoginSuccessIdentity(info) + log.info('qrcode login user info:', info, patchedUserInfo) + await startSession(info.uin, info.uid) +} + +export const runNTLoginSuccessPhases = async ( + info: NodeIKernelLoginListenerType.UserInfo, + startSession: (uin: `${number}`, uid: `u_${string}`) => Promise, +) => { + const userInfo = requireNTLoginSuccessUserInfo() + if (!userInfo) { + return + } + + log.info('qrcode login success current user info:', userInfo) + await startNTSessionAfterLoginSuccess(info, startSession) +} diff --git a/src/ntqq/core/post-login-services.ts b/src/ntqq/core/post-login-services.ts new file mode 100644 index 0000000..313af6a --- /dev/null +++ b/src/ntqq/core/post-login-services.ts @@ -0,0 +1,64 @@ +import { useLogger } from "../../common/log" +import { initBuddyService } from "./service/buddy" +import { initGroupService } from "./service/group" +import { initMsgService } from "./service/msg" +import { initProfileService } from "./service/profile" +import { initStorageCleanService } from "./service/storage-clean" +import { initUnitedConfig } from "./service/united-config" + +const log = useLogger('NTCore/PostLoginServices') + +let ntPostLoginServicesInitialized = false + +const initializeNTPostLoginConfigPhase = () => { + log.info('phase: init united config') + initUnitedConfig() +} + +const initializeNTPostLoginMessagingPhase = async () => { + log.info('phase: init msg service') + await initMsgService() +} + +const initializeNTPostLoginProfilePhase = () => { + log.info('phase: init profile service') + initProfileService() +} + +const initializeNTPostLoginGroupPhase = () => { + log.info('phase: init group service') + initGroupService() +} + +const initializeNTPostLoginBuddyPhase = () => { + log.info('phase: init buddy service') + initBuddyService() +} + +const initializeNTPostLoginStoragePhase = () => { + log.info('phase: init storage clean service') + initStorageCleanService() +} + +const runNTPostLoginServicePhases = async () => { + initializeNTPostLoginConfigPhase() + await initializeNTPostLoginMessagingPhase() + initializeNTPostLoginProfilePhase() + initializeNTPostLoginGroupPhase() + initializeNTPostLoginBuddyPhase() + initializeNTPostLoginStoragePhase() +} + +export const initializeNTPostLoginServices = async () => { + if (ntPostLoginServicesInitialized) { + log.info('phase: post-login services already initialized') + return + } + + await runNTPostLoginServicePhases() + ntPostLoginServicesInitialized = true +} + +export const resetNTPostLoginServicesState = () => { + ntPostLoginServicesInitialized = false +} diff --git a/src/ntqq/core/prepare.ts b/src/ntqq/core/prepare.ts deleted file mode 100644 index f8aa0fe..0000000 --- a/src/ntqq/core/prepare.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { useLogger } from "../../common/log" -import { sleep } from "../../common/utils" -import path from 'path' -import { release, userInfo } from "os" -import { getNTPackageInfo } from "../common/utils" -import { useNTCore } from "./core" -import { useNTConfig } from "../store/config" - -const log = useLogger('NTCore/init') - -/** - * 准备NT环境 - */ -export const prepareBaseEnvironment = async () => { - log.info('init start') - const { getGlobalAdapter, getWrapperEngine, getLoginService } = useNTCore() - const { getNTConfigStoreFolder, getAppId, getAppInfo } = useNTConfig() - // const wrapper = require('../versions/9.9.7-21357/wrapper.node') as typeof NTNativeWrapper - const configFolder = getNTConfigStoreFolder() - log.info('configFolder:', configFolder) - const globalPath = path.resolve(configFolder, './global') - const pkgInfo = getNTPackageInfo() - const appInfo = getAppInfo() - const cfg: NodeIQQNTWrapperEngineType.Init = { - base_path_prefix: '', - /** - * 3 - windows - * 5 - linux - */ - platform_type: appInfo.platform_type, - /** - * 4 - desktop ? - */ - app_type: appInfo.app_type, - app_version: appInfo.app_version, - os_version: appInfo.os_version, - use_xlog: true, - /** - * - * V1_LNX_NQ_${getQQVersion(MobileQQ.getContext())}_${getQQVersionCode()}_GW_B - */ - qua: appInfo.qua, - global_path_config: { - desktopGlobalPath: globalPath - }, - thumb_config: { - maxSide: 324, - minSide: 48, - longLimit: 6, - density: 2 - } - } - const globalAdapter = getGlobalAdapter() - const wrapperEngine = getWrapperEngine() - log.info('cfg:', cfg) - log.info('globalAdapter:', globalAdapter) - const initResult = wrapperEngine.initWithDeskTopConfig(cfg, globalAdapter) - log.info('initResult:', initResult) - const login = getLoginService() - login.initConfig({ - machineId: '', - appid: getAppId(), - platVer: release(), - commonPath: globalPath, - clientVer: pkgInfo.version, - hostName: userInfo().username, - externalVersion: false - }) - const hotUpdateSign = await login.getLoginMiscData('hotUpdateSign') - log.info('hotUpdateSign:', hotUpdateSign) - const setLoginMiscData = await login.setLoginMiscData('hotUpdateSign', '') - log.info('setLoginMiscData:', setLoginMiscData) - const connect = login.connect() - log.info('connect:', connect) - const appVersion = await login.getLoginMiscData('appVersion') - log.info('appVersion:', appVersion) - const msfStatus = await login.getMsfStatus() - log.info('MsfStatus:', msfStatus) - const isLogout = await login.getLoginMiscData('isLogout') - log.info('isLogout:', isLogout) - await sleep(2000) - -} \ No newline at end of file diff --git a/src/ntqq/core/runtime-store.ts b/src/ntqq/core/runtime-store.ts new file mode 100644 index 0000000..295f3d6 --- /dev/null +++ b/src/ntqq/core/runtime-store.ts @@ -0,0 +1,99 @@ +import { CustomError } from "../../common/error/custom-error" +import { User } from "../../models/contact/user" +import { + getCurrentRuntime, + getCurrentSelfInfo, + getCurrentUin, + getUserInfo, + NTRuntimeState, + requireCurrentUin as requireStoreCurrentUin, + setCurrentRuntime, + setCurrentUin, +} from "../store/user" + +export const getCurrentNTUin = () => { + return getCurrentUin() +} + +export const getCurrentNTRuntimeOrNull = () => { + try { + return getCurrentRuntime() + } catch { + return null + } +} + +export const requireCurrentNTRuntime = (): NTRuntimeState => { + requireStoreCurrentUin() + return getCurrentRuntime() +} + +export const setCurrentNTUin = (uin?: number) => { + setCurrentUin(uin) +} + +export const requireCurrentNTUin = () => { + const uin = getCurrentNTUin() + if (!uin) { + throw new CustomError(500, 'id error') + } + return uin +} + +export const setCurrentNTRuntime = (runtime: NTRuntimeState) => { + setCurrentRuntime(runtime) +} + +export const getCurrentNTUserInfo = () => { + return getUserInfo() +} + +export const isCurrentNTUserReady = () => { + return Boolean(getCurrentNTUserInfo()) +} + +export const getCurrentNTSelfOrNull = (uid: `u_${string}` = 'u_0') => { + const uin = getCurrentNTUin() + if (!uin) { + return null + } + return { + id: uin, + uid, + } +} + +export const requireCurrentNTSelfInfo = () => { + return getCurrentSelfInfo() +} + +export const requireCurrentNTUserInfo = () => { + const info = getUserInfo() + if (!info) { + requireStoreCurrentUin() + throw new CustomError(500, 'current nt user info is empty') + } + return info +} + +export const requireCurrentNTUserModel = (): User => { + const runtime = requireCurrentNTRuntime() + const uin = runtime.info.uin || getCurrentNTUin() + if (!uin) { + throw new CustomError(500, 'current nt user id error.') + } + let user = runtime.user + if (user) { + return user + } + user = User.create(uin) + runtime.user = user + return user +} + +export const patchCurrentNTSelfIdentity = (identity: Pick) => { + const info = requireCurrentNTUserInfo() + info.uin = identity.uin + info.uid = identity.uid + return info +} diff --git a/src/ntqq/core/service.ts b/src/ntqq/core/service.ts deleted file mode 100644 index 825b771..0000000 --- a/src/ntqq/core/service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { initLogin } from "./service/login" - -export const initService = () => { - initLogin() -} \ No newline at end of file diff --git a/src/ntqq/core/service/bind-kernel-listener.ts b/src/ntqq/core/service/bind-kernel-listener.ts new file mode 100644 index 0000000..8431f70 --- /dev/null +++ b/src/ntqq/core/service/bind-kernel-listener.ts @@ -0,0 +1,77 @@ +import { getNTLoginService, getNTWrapperSession } from "../core" +import { createNTListenerProxy } from "../dispatcher" + +export const bindKernelListener = ({ + service, + listenerName, + attach, +}: { + service: TService + listenerName: string + attach: (service: TService, listener: TListener) => void +}) => { + const listener = createNTListenerProxy(listenerName) as TListener + attach(service, listener) +} + +/** + * Listener-binding helper for flows that still start from wrapper session. + */ +export const bindWrapperSessionKernelListener = < + TService, + TListener extends object, +>({ + listenerName, + getService, + attach, +}: { + listenerName: string + getService: (session: NTNativeWrapper.NodeIQQNTWrapperSession) => TService + attach: (service: TService, listener: TListener) => void +}) => { + return bindRuntimeServiceKernelListener({ + listenerName, + getService: () => getService(getNTWrapperSession()), + attach: (service, listener) => attach(service, listener as TListener), + }) +} + +/** + * Preferred helper for binding listeners against already-exposed runtime service getters. + */ +export const bindRuntimeServiceKernelListener = < + TService, + TListener extends object, +>({ + listenerName, + getService, + attach, +}: { + listenerName: string + getService: () => TService + attach: (service: TService, listener: TListener) => void +}) => { + const service = getService() + bindKernelListener({ + service, + listenerName, + attach, + }) + return service +} + +export const bindLoginKernelListener = < + TListener extends object, +>({ + listenerName, + attach, +}: { + listenerName: string + attach: (service: ReturnType, listener: TListener) => void +}) => { + return bindRuntimeServiceKernelListener({ + listenerName, + getService: getNTLoginService, + attach, + }) +} diff --git a/src/ntqq/core/service/buddy.ts b/src/ntqq/core/service/buddy.ts index 3b841fa..4a627ca 100644 --- a/src/ntqq/core/service/buddy.ts +++ b/src/ntqq/core/service/buddy.ts @@ -1,6 +1,6 @@ -import { useNTCore } from "../core" -import { useListenerProxy, useNTDispatcher } from "../dispatcher" import { useLogger } from "../../../common/log" +import { getNTBuddyService } from "../core" +import { bindRuntimeServiceKernelListener } from "./bind-kernel-listener" const log = useLogger('BuddyService') /** @@ -9,8 +9,9 @@ const log = useLogger('BuddyService') * 登陆后调用 */ export const initBuddyService = () => { - const { getWrapperSession } = useNTCore() - const buddy = getWrapperSession().getBuddyService() - const p = useListenerProxy('KernelBuddyListener') - buddy.addKernelBuddyListener(p) -} \ No newline at end of file + bindRuntimeServiceKernelListener({ + listenerName: 'KernelBuddyListener', + getService: getNTBuddyService, + attach: (service, listener) => service.addKernelBuddyListener(listener), + }) +} diff --git a/src/ntqq/core/service/group.ts b/src/ntqq/core/service/group.ts index 6c9f9d0..0e168e1 100644 --- a/src/ntqq/core/service/group.ts +++ b/src/ntqq/core/service/group.ts @@ -1,5 +1,5 @@ -import { useNTCore } from "../core" -import { useListenerProxy } from "../dispatcher" +import { getNTGroupService } from "../core" +import { bindRuntimeServiceKernelListener } from "./bind-kernel-listener" /** * 初始化群组服务 @@ -7,8 +7,9 @@ import { useListenerProxy } from "../dispatcher" * 登陆后调用 */ export const initGroupService = () => { - const { getWrapperSession } = useNTCore() - const group = getWrapperSession().getGroupService() - const p = useListenerProxy('KernelGroupListener') - group.addKernelGroupListener(p) -} \ No newline at end of file + bindRuntimeServiceKernelListener({ + listenerName: 'KernelGroupListener', + getService: getNTGroupService, + attach: (service, listener) => service.addKernelGroupListener(listener), + }) +} diff --git a/src/ntqq/core/service/login.ts b/src/ntqq/core/service/login.ts index 7aeeb69..2c721c1 100644 --- a/src/ntqq/core/service/login.ts +++ b/src/ntqq/core/service/login.ts @@ -1,11 +1,6 @@ import { useLogger } from "../../../common/log" -import { useListenerProxy } from "../dispatcher" -import { useNTCore } from "../core" -import { initWrapperSession } from "../wrapper-session" -import { useNTUserStore } from "../../store/user" -import { useStore } from "../../../store/store" -import { useAsyncStore } from "../../../store/async-store" +import { bindLoginKernelListener } from "./bind-kernel-listener" const log = useLogger('LoginInit') @@ -13,24 +8,9 @@ const log = useLogger('LoginInit') * 初始化登录服务 */ export const initLogin = () => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - log.info('initLogin async store:', s) - const { getLoginService } = useNTCore() - const login = getLoginService() - const p = useListenerProxy('KernelLoginListener') log.info('create listener') - const { registerEventListener } = useStore() - log.info('add handle after login success.') - registerEventListener('KernelLoginListener/onQRCodeLoginSucceed', 'always', (info) => { - const { getUserInfo } = useNTUserStore() - const userInfo = getUserInfo() - log.info('qrcode login success:', info, userInfo) - userInfo.uid = info.uid - userInfo.uin = parseInt(info.uin) - log.info('qrcode login user info:', info) - // start to init session - initWrapperSession(info.uin, info.uid) + bindLoginKernelListener({ + listenerName: 'KernelLoginListener', + attach: (service, listener) => service.addKernelLoginListener(listener), }) - login.addKernelLoginListener(p) -} \ No newline at end of file +} diff --git a/src/ntqq/core/service/msg.ts b/src/ntqq/core/service/msg.ts index 0fef978..6714776 100644 --- a/src/ntqq/core/service/msg.ts +++ b/src/ntqq/core/service/msg.ts @@ -1,6 +1,6 @@ -import { useListenerProxy } from "../dispatcher" -import { useNTCore } from "../core" import { useLogger } from "../../../common/log" +import { getNTMsgService } from "../core" +import { bindRuntimeServiceKernelListener } from "./bind-kernel-listener" const log = useLogger('Service/msg') /** @@ -10,19 +10,18 @@ const log = useLogger('Service/msg') */ export const initMsgService = async () => { log.info('initMsgService') - const { getWrapperSession } = useNTCore() - const session = getWrapperSession() log.info('getMsgService') - const msgService = session.getMsgService() - log.info('create listener') - const p = useListenerProxy('KernelMsgListener') // 添加事件监听器 log.info('add msg listener') - msgService.addKernelMsgListener(p) + const msgService = bindRuntimeServiceKernelListener({ + listenerName: 'KernelMsgListener', + getService: getNTMsgService, + attach: (service, listener) => service.addKernelMsgListener(listener), + }) const dev = await msgService.getOnLineDev() log.info('dev:', dev) log.info('registerSysMsgNotification') - // session.getRecentContactService().addKernelRecentContactListener(useListenerProxy('RecentContactListener')) + // session.getRecentContactService().addKernelRecentContactListener(createNTListenerProxy('RecentContactListener')) // try // { // // const t = msgService.registerSysMsgNotification(1, 2, [1]) @@ -37,4 +36,4 @@ export const initMsgService = async () => { // }catch (err) { // log.error('error:', err) // } -} \ No newline at end of file +} diff --git a/src/ntqq/core/service/nt-wrapper-runtime.ts b/src/ntqq/core/service/nt-wrapper-runtime.ts new file mode 100644 index 0000000..138a66b --- /dev/null +++ b/src/ntqq/core/service/nt-wrapper-runtime.ts @@ -0,0 +1,85 @@ +import { useWrapper } from "ntwrapper" +import EventEmitter from "events" +import { CustomError } from "../../../common/error/custom-error" +import { requireCurrentNTRuntime, setCurrentNTRuntime } from "../runtime-store" + +type NTWrapperInstance = ReturnType + +export type NTWrapperSessionContext = { + startupSessionWrapper: NTNativeWrapper.NodeIQQNTStartupSessionWrapper + sessionId: `${number}` +} + +export const createCurrentNTWrapperSessionContext = ( + wrapper: NTWrapperInstance, +): NTWrapperSessionContext => { + const startupSessionWrapper = wrapper.NodeIQQNTStartupSessionWrapper.create() + const sessionIdList = startupSessionWrapper.getSessionIdList() + const sessionId = sessionIdList.get('nt') + if (!sessionId) { + throw new CustomError(500, `sessionId error: ${JSON.stringify(sessionIdList)}`) + } + return { + startupSessionWrapper, + sessionId: sessionId as `${number}`, + } +} + +export const createCurrentNTRuntimeServiceState = ( + wrapper: NTWrapperInstance, + sessionContext: NTWrapperSessionContext, +) => { + return { + loginService: new wrapper.NodeIKernelLoginService(), + wrapperSession: wrapper.NodeIQQNTWrapperSession.getNTWrapperSession(sessionContext.sessionId), + wrapperEngine: new wrapper.NodeIQQNTWrapperEngine(), + startupSessionWrapper: sessionContext.startupSessionWrapper, + } +} + +export const buildCurrentNTRuntimeState = ( + wrapper: NTWrapperInstance, + sessionContext: NTWrapperSessionContext, + uin?: number, +): ReturnType => { + return { + info: { + uin: uin || 0, + uid: 'u_' as `u_${string}`, + userNick: "", + }, + dispatcher: new EventEmitter(), + ...createCurrentNTRuntimeServiceState(wrapper, sessionContext), + } +} + +export const applyCurrentNTRuntimeServiceState = ( + runtime: ReturnType, + serviceState: ReturnType, +) => { + runtime.loginService = serviceState.loginService + runtime.wrapperSession = serviceState.wrapperSession + runtime.wrapperEngine = serviceState.wrapperEngine + runtime.startupSessionWrapper = serviceState.startupSessionWrapper +} + +export const initializeCurrentNTRuntimeState = ( + wrapper: NTWrapperInstance, + uin?: number, +) => { + const sessionContext = createCurrentNTWrapperSessionContext(wrapper) + const runtime = buildCurrentNTRuntimeState(wrapper, sessionContext, uin) + setCurrentNTRuntime(runtime) + return runtime +} + +export const refreshCurrentNTRuntimeServiceState = ( + wrapper: NTWrapperInstance, + runtime: ReturnType, +) => { + const sessionContext = createCurrentNTWrapperSessionContext(wrapper) + applyCurrentNTRuntimeServiceState( + runtime, + createCurrentNTRuntimeServiceState(wrapper, sessionContext), + ) +} diff --git a/src/ntqq/core/service/nt-wrapper.ts b/src/ntqq/core/service/nt-wrapper.ts index 2fb2cc3..65375d1 100644 --- a/src/ntqq/core/service/nt-wrapper.ts +++ b/src/ntqq/core/service/nt-wrapper.ts @@ -1,74 +1,67 @@ import { useWrapper } from "ntwrapper" -import { useAsyncStore } from "../../../store/async-store" -import { useNTUserStore } from "../../store/user" -import { useLogger } from "../../../common/log"; -import EventEmitter from "events"; -import { CustomError } from "../../../server/error/custom-error"; -import { initNative } from "../../../native/native"; +import { useLogger } from "../../../common/log" +import { initNative } from "../../../native/native" +import { + getCurrentNTUin, + getCurrentNTRuntimeOrNull, + requireCurrentNTRuntime, + requireCurrentNTUin, +} from "../runtime-store" +import { + initializeCurrentNTRuntimeState, + refreshCurrentNTRuntimeServiceState, +} from "./nt-wrapper-runtime" +import { resetNTPostLoginServicesState } from "../post-login-services" const log = useLogger('NTWrapper') -export const useNTWrapper = () => { - // QQ号索引处理 - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - const uin: number = s?.get('uin') - if (!uin) - { - throw new CustomError(500, 'useNTWrapper id error.') - } - const userStore = useNTUserStore().getAllAccountData() - if (!userStore[uin]) - { - const ids = Object.keys(userStore) - const idx = ids.length + 1 - log.info('wrapper index:', uin, idx, ids) - const wrapper = useWrapper(idx) - const sw = wrapper.NodeIQQNTStartupSessionWrapper.create() - const sessionIdList = sw.getSessionIdList() - const sessionId = sessionIdList.get('nt') - if (!sessionId) { - log.error('get sessionId error:', sessionIdList) - throw new CustomError(500, 'sessionId error.') - } - userStore[uin] = { - moduleIndex: idx, - info: { - uin: 0, - uid: 'u_', - userNick: "" - }, - dispatcher: new EventEmitter(), - loginService: new wrapper.NodeIKernelLoginService(), - wrapperSession: wrapper.NodeIQQNTWrapperSession.getNTWrapperSession(sessionId), - wrapperEngine: new wrapper.NodeIQQNTWrapperEngine(), - startupSessionWrapper: sw, - } - initNative(`wrapper-${idx}.node`) - } - return useWrapper(userStore[uin].moduleIndex) + +const createNTWrapperInstance = () => { + return useWrapper() } -export const resetNTWrapper = () => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - const uin: number = s?.get('uin') - if (!uin) { - throw new CustomError(500, 'id error') - } - const userStore = useNTUserStore() - const accountNTData = userStore.getCurrentAccountData() - accountNTData.wrapperEngine.destroy() - accountNTData.loginService.destroy() +const runNTWrapperNativeBootstrapPhase = () => { + initNative('wrapper.node') +} + +const runNTWrapperRuntimeBootstrapPhases = (uin?: number) => { + log.info('wrapper file:', uin, 'wrapper.node') + const wrapper = createNTWrapperInstance() + initializeCurrentNTRuntimeState(wrapper, uin) + runNTWrapperNativeBootstrapPhase() + return wrapper +} + +export const initializeNTWrapperRuntime = (uin?: number) => { + return runNTWrapperRuntimeBootstrapPhases(uin) +} + +const destroyCurrentNTRuntimeServices = (runtime: ReturnType) => { + runtime.wrapperEngine.destroy() + runtime.loginService.destroy() +} - const wrapper = useWrapper(accountNTData.moduleIndex) - const sw = wrapper.NodeIQQNTStartupSessionWrapper.create() - const sessionIdList = sw.getSessionIdList() - const sessionId = sessionIdList.get('nt') - if (!sessionId) { - log.error('get sessionId error:', sessionIdList) - throw new CustomError(500, 'sessionId error.') - } - accountNTData.loginService = new wrapper.NodeIKernelLoginService() - accountNTData.wrapperSession = wrapper.NodeIQQNTWrapperSession.getNTWrapperSession(sessionId) - accountNTData.wrapperEngine = new wrapper.NodeIQQNTWrapperEngine() -} \ No newline at end of file +const runNTWrapperResolvePhase = () => { + const current = getCurrentNTRuntimeOrNull() + if (!current) { + return initializeNTWrapperRuntime(getCurrentNTUin()) + } + return createNTWrapperInstance() +} + +const runNTWrapperResetPhases = () => { + requireCurrentNTUin() + const currentRuntime = requireCurrentNTRuntime() + destroyCurrentNTRuntimeServices(currentRuntime) + resetNTPostLoginServicesState() + + const wrapper = createNTWrapperInstance() + refreshCurrentNTRuntimeServiceState(wrapper, currentRuntime) +} + +export const getNTWrapper = () => { + return runNTWrapperResolvePhase() +} + +export const resetNTWrapper = () => { + runNTWrapperResetPhases() +} diff --git a/src/ntqq/core/service/profile.ts b/src/ntqq/core/service/profile.ts index 5bdabcb..5f6388f 100644 --- a/src/ntqq/core/service/profile.ts +++ b/src/ntqq/core/service/profile.ts @@ -1,6 +1,6 @@ -import { useNTCore } from "../core" import { useLogger } from "../../../common/log" -import { useListenerProxy } from "../dispatcher" +import { getNTProfileService } from "../core" +import { bindRuntimeServiceKernelListener } from "./bind-kernel-listener" const log = useLogger('service/profile') @@ -9,8 +9,9 @@ const log = useLogger('service/profile') */ export const initProfileService = () => { log.info('initProfileService') - const { getWrapperSession } = useNTCore() - const profileService = getWrapperSession().getProfileService() - const p = useListenerProxy('KernelProfileListener') - profileService.addKernelProfileListener(p) -} \ No newline at end of file + bindRuntimeServiceKernelListener({ + listenerName: 'KernelProfileListener', + getService: getNTProfileService, + attach: (service, listener) => service.addKernelProfileListener(listener), + }) +} diff --git a/src/ntqq/core/service/storage-clean.ts b/src/ntqq/core/service/storage-clean.ts index 564d773..d4824cd 100644 --- a/src/ntqq/core/service/storage-clean.ts +++ b/src/ntqq/core/service/storage-clean.ts @@ -1,12 +1,13 @@ import { useLogger } from "../../../common/log" -import { useNTCore } from "../core" -import { useListenerProxy } from "../dispatcher" +import { getNTStorageCleanService } from "../core" +import { bindRuntimeServiceKernelListener } from "./bind-kernel-listener" const log = useLogger('RichMedia') export const initStorageCleanService = () => { - const { getWrapperSession } = useNTCore() - const storageClean = getWrapperSession().getStorageCleanService() - const p = useListenerProxy('KernelStorageCleanListener') - storageClean.addKernelStorageCleanListener(p) -} \ No newline at end of file + bindRuntimeServiceKernelListener({ + listenerName: 'KernelStorageCleanListener', + getService: getNTStorageCleanService, + attach: (service, listener) => service.addKernelStorageCleanListener(listener), + }) +} diff --git a/src/ntqq/core/service/united-config.ts b/src/ntqq/core/service/united-config.ts index 9a5b00c..dca16d9 100644 --- a/src/ntqq/core/service/united-config.ts +++ b/src/ntqq/core/service/united-config.ts @@ -1,5 +1,5 @@ import { useLogger } from "../../../common/log" -import { useNTCore } from "../core" +import { getNTUnitedConfigService } from "../core" const log = useLogger('UnitedConfig') @@ -8,13 +8,10 @@ const log = useLogger('UnitedConfig') */ export const initUnitedConfig = () => { log.info('initUnitedConfig') - const { getWrapperSession } = useNTCore() - const session = getWrapperSession() - - const unitedConfig = session.getUnitedConfigService() + const unitedConfig = getNTUnitedConfigService() log.info('unitedConfig:', unitedConfig) unitedConfig.registerUnitedConfigPushGroupList([ "100237","100241","100279","100267","101786","100343","100350","100636","101191","100165","100156","102294","102331","102814","103261","103284","105473","106408","106583","106817","100076","102495","100392","100473","100809","102217","103114","103198","101121","101241","100072","100073","100242","100083","100243","100084","100244","100085","100272","100629","group_bacth_download_max_num","100164","100690","100691","102738","102544","100354","100716","100167","100647","102232","102626","103249","105555","100069","100354","100145","101227","101206","101205","101204","101203","101202","101201","101200","fav_face_lottie_emj","102176","102810","102772","102793","103224","105772","106926","105713","100075","100074","102219","100759","100077","100987","101558","101718","101963","101989","102027","102878","105417","105517","106259","106566","102926","102890","103155","105502","ntrtc_dsp_switch","105938","106687","106048","106593","106089","106090","106238","106235","106304","106413","106646","106704","106633","106868","105748","106894","106876","106981","107014" ]) log.info('registerUnitedConfigPushGroupList') -} \ No newline at end of file +} diff --git a/src/ntqq/core/session-lifecycle.ts b/src/ntqq/core/session-lifecycle.ts new file mode 100644 index 0000000..06b0534 --- /dev/null +++ b/src/ntqq/core/session-lifecycle.ts @@ -0,0 +1,35 @@ +import { useLogger } from "../../common/log" +import { CustomError } from "../../common/error/custom-error" +import { getCurrentNTUserInfo, getNTLoginService, requireCurrentNTRuntime } from "./core" +import { initializeNTPostLoginServices } from "./post-login-services" + +const log = useLogger('NTCore/SessionLifecycle') + +const ensureNTSessionReady = () => { + const currentRuntime = requireCurrentNTRuntime() + log.info('runtime data:', currentRuntime) + if (!currentRuntime.info.uin) { + throw new CustomError(20302, 'uin error') + } + log.info('current user info:', getCurrentNTUserInfo()) +} + +const finalizeNTSessionReady = async () => { + await initializeNTPostLoginServices() + ensureNTSessionReady() +} + +const destroyNTLoginServiceAfterSessionInit = () => { + const login = getNTLoginService() + login.destroy() +} + +export const runNTSessionReadyPhases = async () => { + log.info('phase: finalize session ready') + await finalizeNTSessionReady() +} + +export const runNTSessionInitCompletePhases = async () => { + log.info('phase: finalize session init complete') + destroyNTLoginServiceAfterSessionInit() +} diff --git a/src/ntqq/core/wrapper-session.ts b/src/ntqq/core/wrapper-session.ts deleted file mode 100644 index f91b4ff..0000000 --- a/src/ntqq/core/wrapper-session.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { useLogger } from "../../common/log" -import { useNTCore } from "./core" -import { sleep } from "../../common/utils" -import { release } from "os" -import { getNTPackageInfo } from "../common/utils" -import { initMsgService } from "./service/msg" -import { initUnitedConfig } from "./service/united-config" -import { initProfileService } from "./service/profile" -import { useListenerProxy } from "./dispatcher" -import { useStore } from "../../store/store" -import { initGroupService } from "./service/group" -import { initBuddyService } from "./service/buddy" -import { useNTConfig } from "../store/config" -import { initStorageCleanService } from "./service/storage-clean" -import { useNTWrapper } from "./service/nt-wrapper" -import { useNTUserStore } from "../store/user" -import { useAsyncStore } from "../../store/async-store" -import { CustomError } from "../../server/error/custom-error" - -const log = useLogger('AfterLogin') - -export const initWrapperSession = async (uin: `${number}`, uid: `u_${string}`) => { - const wrapper = useNTWrapper() - log.info('start to init wrapper session', uin, uid) - const { getWrapperSession, getStartupSessionWrapper, getLoginService } = useNTCore() - const { getDeviceInfo } = useNTConfig() - const { getAppId, getNTConfigStoreFolder } = useNTConfig() - const session = getWrapperSession() - const msfService = session.getMSFService() - log.info('getMsfStatus:', msfService.getMsfStatus()) - const { registerEventListener } = useStore() - - // 登陆后初始化 - wrapper.NodeQQNTWrapperUtil.emptyWorkingSet(61444); - const p1 = useListenerProxy('DependsAdapter') - - const p2 = useListenerProxy('DispatcherAdapter') - - const p = useListenerProxy('KernelSessionListener') - registerEventListener('KernelSessionListener/onNTSessionCreate', 'always', async (result) => { - log.info('onOpentelemetryInit called:', result) - log.info('NTWrapperSession init successful!') - log.info('initUnitedConfig start') - initUnitedConfig() - log.info('initMsgService start') - await initMsgService() - log.info('initProfileService start') - initProfileService() - log.info('initGroupService start') - initGroupService() - log.info('initBuddyService start') - initBuddyService() - log.info('initStorageCleanService start') - initStorageCleanService() - // 切换模块附属 - const userStore = useNTUserStore() - const account = userStore.getAllAccountData() - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - const uin = s?.get('uin') as number - const t = account[uin] - log.info('account data:', t) - if (!t.info.uin) { - throw new CustomError(20302, 'uin error') - } - log.info(`变更模块${uin}所属为${t.info.uin}`) - s?.set('uin', t.info.uin) - delete account[uin] - account[t.info.uin] = t - const userInfo = useNTUserStore() - log.info('current user info:', userInfo.getUserInfo()) - }) - registerEventListener('KernelSessionListener/onSessionInitComplete', 'always', async (result) => { - const login = getLoginService() - login.destroy() - }) - const configFolder = getNTConfigStoreFolder() - const pkgInfo = getNTPackageInfo() - session.init({ - selfUin: uin, - selfUid: uid, - desktopPathConfig: { - account_path: configFolder - }, - clientVer: pkgInfo.version, - a2: '', - d2: '', - d2Key: '', - machineId: '', - platform: 3, - platVer: release(), - appid: getAppId(), - rdeliveryConfig: { - appKey: '', - systemId: 0, - appId: '', - logicEnvironment: '', - platform: 5, - language: '', - sdkVersion: '', - userId: '', - appVersion: '', - osVersion: '', - bundleId: '', - serverUrl: '', - fixedAfterHitKeys: [ '' ] - }, - defaultFileDownloadPath: '/home/msojocs', - deviceInfo: getDeviceInfo(), - deviceConfig: '{"appearance":{"isSplitViewMode":true},"msg":{}}', - deviceType: 3 - }, p1, p2, p) - const sw = getStartupSessionWrapper() - sw.start() -} \ No newline at end of file diff --git a/src/ntqq/friend/friend.ts b/src/ntqq/friend/friend.ts index e5843a3..9f80b5e 100644 --- a/src/ntqq/friend/friend.ts +++ b/src/ntqq/friend/friend.ts @@ -1,12 +1,8 @@ import { randomUUID } from "crypto" -import { useStore } from "../../store/store" import { NTFriend } from "./interfaces" import { useLogger } from "../../common/log" -import { useNTCore } from "../core/core" -import { CustomError } from "../../server/error/custom-error" - - -const { registerEventListener } = useStore() +import { getNTBuddyService, getNTProfileLikeService, getNTProfileService } from "../core/core" +import { CustomError } from "../../common/error/custom-error" const log = useLogger('NTQQ/Friend') /** @@ -15,10 +11,8 @@ const log = useLogger('NTQQ/Friend') * @returns 好友列表 */ export const NTGetFriendList = async (): Promise => { - - const { getWrapperSession } = useNTCore() - const buddyService = getWrapperSession().getBuddyService() - const profileService = getWrapperSession().getProfileService(); + const buddyService = getNTBuddyService() + const profileService = getNTProfileService(); const groupList = await buddyService.getBuddyListV2(true, 0); const result: NTFriend.FriendGroupType[] = [] @@ -62,8 +56,7 @@ export const NTGetFriendList = async (): Promise => */ export const NTSendLikeFriend = async (userId: `u_${string}`, count: number): Promise => { log.info('send like:', userId, count) - const { getWrapperSession } = useNTCore() - const likeService = getWrapperSession().getProfileLikeService() + const likeService = getNTProfileLikeService() const likeResult = await likeService.setBuddyProfileLike({ friendUid: userId, sourceId: 71, @@ -81,11 +74,3 @@ export const NTSendLikeFriend = async (userId: `u_${string}`, count: number): Pr * @param count 点赞次数 * @returns 好友列表 */ -// export const NTGetUserInfo = async (userId: `u_${string}`, count: number): Promise => { -// log.info('send like:', userId, count) -// const { getWrapperSession } = useNTCore() -// const profileService = getWrapperSession().getProfileService() -// const likeResult = await profileService.getUserSimpleInfo() -// log.info('send like result:', likeResult) -// return likeResult -// } \ No newline at end of file diff --git a/src/ntqq/group/list.ts b/src/ntqq/group/list.ts index 5efd098..9fee1ea 100644 --- a/src/ntqq/group/list.ts +++ b/src/ntqq/group/list.ts @@ -1,31 +1,32 @@ import { useLogger } from "../../common/log" -import { useStore } from "../../store/store" -import { useNTCore } from "../core/core" +import { getNTGroupService } from "../core/core" +import { createNTEventWaiter } from "../query/wait" import { NTGroupInfo } from "./types" -const { registerEventListener } = useStore() const log = useLogger('GroupList') export const NTGetGroupList = () => { return new Promise(async (resolve, reject) => { - - const { getWrapperSession } = useNTCore() - const wrapperSession = getWrapperSession() - log.info('wrapperSession:', wrapperSession) - const groupService = wrapperSession.getGroupService() + const groupService = getNTGroupService() log.info('groupService:', groupService) - const listener = registerEventListener( - 'KernelGroupListener/onGroupListUpdate', - 'once', - (a: number, list: NTGroupInfo[]) => { - log.info(a, list) - resolve(list) - }) - const result = await groupService.getGroupList(true) - if (result.result !== 0) - { - listener.remove() - reject(result) + const waiter = createNTEventWaiter<[number, NTGroupInfo[]], NTGroupInfo[]>({ + event: 'KernelGroupListener/onGroupListUpdate', + match: () => true, + select: (_unused, list) => { + log.info(_unused, list) + return list + }, + }) + try { + const result = await groupService.getGroupList(true) + if (result.result !== 0) { + waiter.cancel() + reject(result) + return + } + resolve(await waiter.promise) + } catch (error) { + waiter.cancel() + reject(error) } }) - -} \ No newline at end of file +} diff --git a/src/ntqq/group/msg.ts b/src/ntqq/group/msg.ts index 119586b..91cd47e 100644 --- a/src/ntqq/group/msg.ts +++ b/src/ntqq/group/msg.ts @@ -1,9 +1,8 @@ -import { CustomError } from "../../server/error/custom-error" -import { useNTCore } from "../core/core" +import { CustomError } from "../../common/error/custom-error" +import { getNTMsgService } from "../core/core" export const NTGroupMsgList = async (groupId: `${number}`, msgId: `${number}`, cnt: number) => { - const { getWrapperSession } = useNTCore() - const msgService = getWrapperSession().getMsgService() + const msgService = getNTMsgService() const ret = await msgService.getMsgsIncludeSelf({ chatType: 2, peerUid: groupId, @@ -14,4 +13,4 @@ export const NTGroupMsgList = async (groupId: `${number}`, msgId: `${number}`, c throw new CustomError(333, ret.errMsg) } return ret.msgList -} \ No newline at end of file +} diff --git a/src/ntqq/group/nudge.ts b/src/ntqq/group/nudge.ts new file mode 100644 index 0000000..c4d4857 --- /dev/null +++ b/src/ntqq/group/nudge.ts @@ -0,0 +1,16 @@ +import { sendCustomPkg } from "../../native/native"; +import { CommunicationPkg, PaiYiPaiReq } from "../protobuf/communication"; + +export const nudgeGroupMember = async (groupId: number, userUin: number): Promise => { + const result = await sendCustomPkg('OidbSvcTrpcTcp.0xed3_1', CommunicationPkg.encode({ + uint32Command: 0xed3, + uint32ServiceType: 1, + bytesBodybuffer: PaiYiPaiReq.encode({ + uint64ToUin: userUin, + uint64GroupCode: groupId, + uint32NudgeType: 0, + }).finish(), + trpcTransInfo: [], + }).finish()) + return result.uint32Result === 0 +} diff --git a/src/ntqq/index.ts b/src/ntqq/index.ts deleted file mode 100644 index 807fb3d..0000000 --- a/src/ntqq/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { prepareBaseEnvironment } from "./core/prepare" -import { initService } from "./core/service" - -/** - * 初始化NTQQ - * - * @param id QQ号 - */ -export const NTInitialize = async () => { - await prepareBaseEnvironment() - initService() -} \ No newline at end of file diff --git a/src/ntqq/login/account-device-verify.ts b/src/ntqq/login/account-device-verify.ts new file mode 100644 index 0000000..ee7abe0 --- /dev/null +++ b/src/ntqq/login/account-device-verify.ts @@ -0,0 +1,30 @@ +import { Oidb0xc9e_8 } from "../core/oidb/0xc9e_8" +import { CheckScanStatusResult } from "../types/core/oidb/0xc9e_8" + +const resolveDeviceVerifyTokens = (url: string) => { + const parsed = new URL(url) + return { + uinToken: parsed.searchParams.get('uin-token') || '', + devToken: parsed.searchParams.get('sig') || '', + } +} + +export const getDeviceVerifyQRCodeFromJumpUrl = async ( + uin: number, + url: string, +): Promise => { + const deviceQRCode = new Oidb0xc9e_8(uin) + const { uinToken, devToken } = resolveDeviceVerifyTokens(url) + return deviceQRCode.getQRCode(uinToken, devToken) +} + +export const NTGetDeviceVerifyQRCode = async (uin: number, url: string): Promise => { + return getDeviceVerifyQRCodeFromJumpUrl(uin, url) +} + +export const NTCheckDeviceVerifyQRCodeStatus = async ( + url: string, +): Promise => { + const deviceQRCode = new Oidb0xc9e_8(0) + return deviceQRCode.checkScanStatus(url) +} diff --git a/src/ntqq/login/account-password-login.ts b/src/ntqq/login/account-password-login.ts new file mode 100644 index 0000000..9d832c4 --- /dev/null +++ b/src/ntqq/login/account-password-login.ts @@ -0,0 +1,72 @@ +import { useLogger } from "../../common/log" +import { NTLogin } from "./interfaces" +import { withReadyNTLoginService } from "./runtime" +import { + createFailedAccountLoginResponse, + isAccountLoginResponse, + resolvePasswordLoginResponse, +} from "./account-response" +import { + createDuplicateLoginWaiter, + createQuickLoginFailedWaiter, +} from "./account-waiters" + +const log = useLogger('NT Account') + +const buildPasswordLoginRequest = ( + ntLogin: NTLogin.AccountLoginRequest, +): NodeIKernelLoginServiceType.PasswordLoginReq => ({ + uin: `${ntLogin.loginInfo.uin}`, + passwordMd5: ntLogin.loginInfo.passwordMd5, + step: ntLogin.loginInfo.step, + newDeviceLoginSig: new Uint8Array(Buffer.from(ntLogin.loginInfo.newDeviceLoginSig || '')), + proofWaterSig: new Uint8Array(Buffer.from(ntLogin.loginInfo.proofWaterSig || '')), + proofWaterRand: new Uint8Array(Buffer.from(ntLogin.loginInfo.proofWaterRand || '')), + proofWaterSid: new Uint8Array(Buffer.from(ntLogin.loginInfo.proofWaterSid || '')), +}) + +const runPasswordLoginRequest = async ( + ntLogin: NTLogin.AccountLoginRequest, +): Promise => { + try { + const response = await withReadyNTLoginService(async (loginService) => { + log.info('using password login:', ntLogin.loginInfo) + const loginInfo = buildPasswordLoginRequest(ntLogin) + const passwordLoginResponse = await loginService.passwordLogin(loginInfo) + log.debug('passwordLogin resp:', passwordLoginResponse) + return passwordLoginResponse + }) + + const resolved = await resolvePasswordLoginResponse(ntLogin, response) + if (resolved.type === 'resolve') { + return resolved.value + } + throw resolved.value + } catch (error) { + if (isAccountLoginResponse(error)) { + throw error + } + log.error('login error:', error) + throw createFailedAccountLoginResponse('登录发生错误') + } +} + +export const NTLoginByAccountInfo = async ( + ntLogin: NTLogin.AccountLoginRequest, +): Promise => { + log.info('req to nt:', JSON.stringify(ntLogin)) + + const duplicateLoginWaiter = createDuplicateLoginWaiter() + const quickLoginFailedWaiter = createQuickLoginFailedWaiter() + + try { + return await Promise.race([ + duplicateLoginWaiter.promise, + quickLoginFailedWaiter.promise.then((response) => Promise.reject(response)), + runPasswordLoginRequest(ntLogin), + ]) + } finally { + duplicateLoginWaiter.cancel() + quickLoginFailedWaiter.cancel() + } +} diff --git a/src/ntqq/login/account-quick-login.ts b/src/ntqq/login/account-quick-login.ts new file mode 100644 index 0000000..5e7efa6 --- /dev/null +++ b/src/ntqq/login/account-quick-login.ts @@ -0,0 +1,29 @@ +import { useLogger } from "../../common/log" +import { withReadyNTLoginService } from "./runtime" + +const log = useLogger('NT Account') + +const getNTQuickLoginEntry = async (uin: `${number}`) => { + return withReadyNTLoginService(async (loginService) => { + const loginList = await loginService.getLoginList() + log.info('login list:', loginList) + return loginList.LocalLoginInfoList.find((entry) => entry.uin === uin && entry.isQuickLogin) + }) +} + +export const NTGetQuickLoginList = async () => { + return withReadyNTLoginService(async (loginService) => { + const loginList = await loginService.getLoginList() + return loginList.LocalLoginInfoList + }) +} + +export const NTQuickLoginByUin = async (uin: `${number}`) => { + const quickLoginEntry = await getNTQuickLoginEntry(uin) + if (!quickLoginEntry) { + log.warn('quick login entry not found for uin:', uin) + } + return withReadyNTLoginService((loginService) => { + return loginService.quickLoginWithUin(uin) + }) +} diff --git a/src/ntqq/login/account-response.ts b/src/ntqq/login/account-response.ts new file mode 100644 index 0000000..be724b0 --- /dev/null +++ b/src/ntqq/login/account-response.ts @@ -0,0 +1,80 @@ +import { useLogger } from "../../common/log" +import { NTLogin } from "./interfaces" +import { getDeviceVerifyQRCodeFromJumpUrl } from "./account-device-verify" + +const log = useLogger('NT Account') + +const PASSWORD_LOGIN_RESOLVE_RESULTS = new Set([ + '0', + '4', + '140022008', +]) + +export const createDefaultAccountLoginResponse = (): NTLogin.AccountLoginResponse => ({ + result: '0', + loginErrorInfo: { + step: 0, + errMsg: '', + proofWaterUrl: '', + newDevicePullQrCodeSig: '', + jumpUrl: '', + jumpWord: '', + tipsTitle: '', + tipsContent: '', + }, +}) + +const appendDeviceVerifyQRCode = async ( + ntLogin: NTLogin.AccountLoginRequest, + response: NTLogin.AccountLoginResponse, +) => { + const deviceVerifyQRCodeUrl = await getDeviceVerifyQRCodeFromJumpUrl( + Number(ntLogin.loginInfo.uin), + response.loginErrorInfo.jumpUrl, + ) + ;( + response.loginErrorInfo as typeof response.loginErrorInfo & { + deviceVerifyQRCodeUrl?: string + } + ).deviceVerifyQRCodeUrl = deviceVerifyQRCodeUrl + return response +} + +export const resolvePasswordLoginResponse = async ( + ntLogin: NTLogin.AccountLoginRequest, + response: NTLogin.AccountLoginResponse, +): Promise<{ type: 'resolve' | 'reject', value: NTLogin.AccountLoginResponse }> => { + if (PASSWORD_LOGIN_RESOLVE_RESULTS.has(response.result)) { + return { + type: 'resolve', + value: response, + } + } + + if (response.result === '140022010') { + log.info('need device verify, start get qrcode') + return { + type: 'resolve', + value: await appendDeviceVerifyQRCode(ntLogin, response), + } + } + + return { + type: 'reject', + value: response, + } +} + +export const createFailedAccountLoginResponse = (errMsg: string) => { + const response = createDefaultAccountLoginResponse() + response.result = '-1' + response.loginErrorInfo.errMsg = errMsg + return response +} + +export const isAccountLoginResponse = (value: unknown): value is NTLogin.AccountLoginResponse => { + return typeof value === 'object' + && value !== null + && 'result' in value + && 'loginErrorInfo' in value +} diff --git a/src/ntqq/login/account-waiters.ts b/src/ntqq/login/account-waiters.ts new file mode 100644 index 0000000..60aaee1 --- /dev/null +++ b/src/ntqq/login/account-waiters.ts @@ -0,0 +1,22 @@ +import { createNTEventWaiter } from "../query/wait" +import { NTLogin } from "./interfaces" +import { createFailedAccountLoginResponse } from "./account-response" + +export const createDuplicateLoginWaiter = () => { + return createNTEventWaiter<[unknown], NTLogin.AccountLoginResponse>({ + event: 'KernelLoginListener/onUserLoggedIn', + match: () => true, + select: () => createFailedAccountLoginResponse('重复登录'), + }) +} + +export const createQuickLoginFailedWaiter = () => { + return createNTEventWaiter< + [NTLogin.PayloadQRCodeSessionQuickLoginFailed], + NTLogin.AccountLoginResponse + >({ + event: 'KernelLoginListener/onQRCodeSessionQuickLoginFailed', + match: () => true, + select: () => createFailedAccountLoginResponse('需要手Q确认,但是手Q没有登录'), + }) +} diff --git a/src/ntqq/login/account.ts b/src/ntqq/login/account.ts index 6dfea9d..96aef44 100644 --- a/src/ntqq/login/account.ts +++ b/src/ntqq/login/account.ts @@ -1,234 +1,11 @@ -import { randomUUID } from "crypto" -import { useLogger } from "../../common/log" -import { NTLogin } from "./interfaces" -import { useStore } from "../../store/store" -import { useNTCore } from "../core/core" -import { sleep } from "../../common/utils" -import { Oidb0xc9e_8 } from "../core/oidb/0xc9e_8" -import { CheckScanStatusResult } from "../types/core/oidb/0xc9e_8" - -const log = useLogger('NT Account') -const { registerEventListener } = useStore() - -/** - * 使用账户登录 - * - * @param ntLogin.LoginRequest 登录信息 - * @returns 登录结果 - */ -export const NTLoginByAccountInfo = (ntLogin: NTLogin.AccountLoginRequest): Promise => { - return new Promise(async (resolve, reject) => { - log.info("req to nt:", JSON.stringify(ntLogin)) - let responseStart = false - const result: NTLogin.AccountLoginResponse = { - result: "0", - loginErrorInfo: { - step: 0, - errMsg: "", - proofWaterUrl: "", - newDevicePullQrCodeSig: "", - jumpUrl: "", - jumpWord: "", - tipsTitle: "", - tipsContent: "" - } - } - - // 注册重复登录监听事件 - const onUserLoggedIn = registerEventListener('KernelLoginListener/onUserLoggedIn', 'once', (payload: any) => { - if (!responseStart) { - responseStart = true - result.result = '-1' - result.loginErrorInfo.errMsg = '重复登录' - resolve(result) - } - }) - // 注册登录失败事件 - const onQRCodeSessionQuickLoginFailed = registerEventListener('KernelLoginListener/onQRCodeSessionQuickLoginFailed', 'once', (payload: NTLogin.PayloadQRCodeSessionQuickLoginFailed) => { - // 需要手Q确认,但是手Q没有登录 - result.result = '-1' - result.loginErrorInfo.errMsg = '需要手Q确认,但是手Q没有登录' - reject(result) - }) - - try { - // 重复登录会导致超时 - const { getLoginService } = useNTCore() - await sleep(2000) - const loginService = getLoginService() - - log.info('using password login:', ntLogin.loginInfo) - // str转Uint8Array - const loginInfo: NodeIKernelLoginServiceType.PasswordLoginReq = { - uin: `${ntLogin.loginInfo.uin}`, - passwordMd5: ntLogin.loginInfo.passwordMd5, - step: ntLogin.loginInfo.step, - newDeviceLoginSig: new Uint8Array(Buffer.from(ntLogin.loginInfo.newDeviceLoginSig || '')), - proofWaterSig: new Uint8Array(Buffer.from(ntLogin.loginInfo.proofWaterSig || '')), - proofWaterRand: new Uint8Array(Buffer.from(ntLogin.loginInfo.proofWaterRand || '')), - proofWaterSid: new Uint8Array(Buffer.from(ntLogin.loginInfo.proofWaterSid || '')) - } - const resp = await loginService.passwordLogin(loginInfo) - log.debug('passwordLogin resp:', resp) - - // 非重复登录,移除监听 - onUserLoggedIn.remove() - - if (!responseStart) { - responseStart = true - if (resp.result === '0') { - // 直接成功 - onQRCodeSessionQuickLoginFailed.remove() - resolve(resp) - } - else if (resp.result === '4'){ - // 需要手Q确认,通知客户端 - resolve(resp) - } - else if (resp.result === '140022008') { - // 滑动验证 - onQRCodeSessionQuickLoginFailed.remove() - resolve(resp) - } else if (resp.result === '140022010') { - // 设备验证 - log.info('need device verify, start get qrcode') - const deviceQRCode = new Oidb0xc9e_8(Number(ntLogin.loginInfo.uin)) - const jumpUrl = resp.loginErrorInfo.jumpUrl - const u = new URL(jumpUrl) - const uinToken = u.searchParams.get('uin-token') || '' - const devToken = u.searchParams.get('sig') || '' - const url = await deviceQRCode.getQRCode(uinToken, devToken) - resp.loginErrorInfo.deviceVerifyQRCodeUrl = url - - onQRCodeSessionQuickLoginFailed.remove() - resolve(resp) - } else { - // 其它情况 - onQRCodeSessionQuickLoginFailed.remove() - reject(resp) - } - } - } - catch(e) { - log.error('login error:', e) - if (!responseStart) { - responseStart = true - result.result = '-1' - result.loginErrorInfo.errMsg = '登录发生错误' - onUserLoggedIn.remove() - onQRCodeSessionQuickLoginFailed.remove() - reject(result) - } - } - - }) -} - -export const NTGetDeviceVerifyQRCode = async (uin: number, url: string): Promise => { - const deviceQRCode = new Oidb0xc9e_8(uin) - const u = new URL(url) - const uinToken = u.searchParams.get('uin-token') || '' - const devToken = u.searchParams.get('sig') || '' - const result = await deviceQRCode.getQRCode(uinToken, devToken) - return result -} - -export const NTCheckDeviceVerifyQRCodeStatus = async (url: string): Promise => { - const deviceQRCode = new Oidb0xc9e_8(0) - const result = await deviceQRCode.checkScanStatus(url) - return result -} - -export const NTLoginByQuickList = (ntLogin: NTLogin.AccountLoginRequest): Promise => { - return new Promise(async (resolve, reject) => { - log.info("req to nt:", JSON.stringify(ntLogin)) - let responseStart = false - const result: NTLogin.AccountLoginResponse = { - result: "0", - loginErrorInfo: { - step: 0, - errMsg: "", - proofWaterUrl: "", - newDevicePullQrCodeSig: "", - jumpUrl: "", - jumpWord: "", - tipsTitle: "", - tipsContent: "" - } - } - - // 注册重复登录监听事件 - const onUserLoggedIn = registerEventListener('KernelLoginListener/onUserLoggedIn', 'once', (payload: any) => { - if (!responseStart) { - responseStart = true - result.result = '-1' - result.loginErrorInfo.errMsg = '重复登录' - resolve(result) - } - }) - try { - // 重复登录会导致超时 - const { getLoginService } = useNTCore() - await sleep(2000) - const loginService = getLoginService() - // 获取登录列表 - const loginList = await loginService.getLoginList() - log.info('login list:', loginList) - const quick = loginList.LocalLoginInfoList.find(e => e.uin === ntLogin.loginInfo.uin && e.isQuickLogin) - if (quick) { - // 可以快速登陆 - log.info('using quick login.') - const loginResult = await loginService.quickLoginWithUin( ntLogin.loginInfo.uin) - log.info('loginResult:', loginResult) - resolve(loginResult) - return - } - } - catch(e) { - - } - - }) -} - -export const NTGetQuickLoginList = async () => { - const { getLoginService } = useNTCore() - await sleep(2000) - const loginService = getLoginService() - const loginList = await loginService.getLoginList() - return loginList.LocalLoginInfoList -} - -export const NTQuickLoginByUin = async (uin: `${number}`) => { - const { getLoginService } = useNTCore() - await sleep(2000) - const loginService = getLoginService() - const loginResult = await loginService.quickLoginWithUin(uin) - return loginResult -} - -export const NTGetLoginStatus = () => new Promise(async (resolve) => { - const { getWrapperSession } = useNTCore() - try{ - const profileService = getWrapperSession().getProfileService() - if (!profileService) { - resolve(false) - return - } - const l = registerEventListener('KernelProfileListener/onSelfStatusChanged', 'once', (payload: any) => { - log.info('onSelfStatusChanged:', payload) - log.info('result:', payload.status === 10) - resolve(payload.status === 10) - }) - const status = await profileService.getSelfStatus() - if (status.result !== 0) { - l.remove() - resolve(false) - return - } - log.info('self status:', status) - } catch(e) { - log.error('get login status error:', e) - resolve(false) - } -}) \ No newline at end of file +// @aggregation-boundary — 长期聚合出口,勿删 +export { NTGetLoginStatus } from "./status" +export { + NTCheckDeviceVerifyQRCodeStatus, + NTGetDeviceVerifyQRCode, +} from "./account-device-verify" +export { NTLoginByAccountInfo } from "./account-password-login" +export { + NTGetQuickLoginList, + NTQuickLoginByUin, +} from "./account-quick-login" diff --git a/src/ntqq/login/index.ts b/src/ntqq/login/index.ts new file mode 100644 index 0000000..739438b --- /dev/null +++ b/src/ntqq/login/index.ts @@ -0,0 +1,11 @@ +// @aggregation-boundary — 长期聚合出口,勿删 +export { + NTCheckDeviceVerifyQRCodeStatus, + NTGetDeviceVerifyQRCode, + NTGetLoginStatus, + NTGetQuickLoginList, + NTLoginByAccountInfo, + NTQuickLoginByUin, +} from "./account" + +export { NTGetLoginQrCode } from "./qrcode" diff --git a/src/ntqq/login/qrcode.ts b/src/ntqq/login/qrcode.ts index b8bc7c5..6f1904d 100644 --- a/src/ntqq/login/qrcode.ts +++ b/src/ntqq/login/qrcode.ts @@ -1,9 +1,5 @@ -import { useStore } from "../../store/store" -import { useNTCore } from "../core/core" -import { sleep } from "../../common/utils" import { useLogger } from "../../common/log" - -const { registerEventListener } = useStore() +import { withReadyNTLoginService } from "./runtime" const log = useLogger('NTQQ/Login/qrcode') /** @@ -12,8 +8,9 @@ const log = useLogger('NTQQ/Login/qrcode') */ export const NTGetLoginQrCode = async () => { log.info('NTGetLoginQrCode') - const { getLoginService } = useNTCore() - await sleep(2000) - const login = getLoginService() - log.info('get qrcode:', login.getQRCodePicture()) -} \ No newline at end of file + return withReadyNTLoginService(async (loginService) => { + const result = loginService.getQRCodePicture() + log.info('get qrcode:', result) + return result + }) +} diff --git a/src/ntqq/login/runtime.ts b/src/ntqq/login/runtime.ts new file mode 100644 index 0000000..f994e41 --- /dev/null +++ b/src/ntqq/login/runtime.ts @@ -0,0 +1,10 @@ +import { getNTLoginServiceWhenReady } from "../core/lifecycle" + +type NTReadyLoginService = Awaited> + +export const withReadyNTLoginService = async ( + execute: (loginService: NTReadyLoginService) => Promise, +) => { + const loginService = await getNTLoginServiceWhenReady() + return execute(loginService) +} diff --git a/src/ntqq/login/status.ts b/src/ntqq/login/status.ts new file mode 100644 index 0000000..bd50958 --- /dev/null +++ b/src/ntqq/login/status.ts @@ -0,0 +1,38 @@ +import { useLogger } from "../../common/log" +import { getNTProfileService } from "../core/core" +import { createNTEventWaiter } from "../query/wait" + +const log = useLogger('NTQQ/Login/status') + +const createNTSelfStatusWaiter = () => createNTEventWaiter<[any], boolean>({ + event: 'KernelProfileListener/onSelfStatusChanged', + match: () => true, + select: (payload) => { + log.info('onSelfStatusChanged:', payload) + log.info('result:', payload.status === 10) + return payload.status === 10 + }, +}) + +export const NTGetLoginStatus = async (): Promise => { + try { + const profileService = getNTProfileService() + if (!profileService) { + return false + } + + const waiter = createNTSelfStatusWaiter() + const status = await profileService.getSelfStatus() + if (status.result !== 0) { + waiter.cancel() + return false + } + + log.info('self status:', status) + return await waiter.promise + } + catch (error) { + log.error('get login status error:', error) + return false + } +} diff --git a/src/ntqq/message/group-forward.ts b/src/ntqq/message/group-forward.ts new file mode 100644 index 0000000..6257869 --- /dev/null +++ b/src/ntqq/message/group-forward.ts @@ -0,0 +1,99 @@ +import { randomBytes } from "crypto"; +import { gzipSync } from "zlib"; +import { useLogger } from "../../common/log"; +import { sendCustomPkgV2 } from "../../native/native"; +import { BotMessageSendElements } from "../../onebot/contracts/message"; +import { ForwardConverter } from "../../transfer/message/forward-converter"; +import { SendLongMsgReq, SendLongMsgResp, SsoSendLongMsgReq } from "../protobuf/longmsg"; +import { PbSendMsgReq, PbSendMsgResp } from "../protobuf/msg_svc"; + +const log = useLogger('NTMessage/GroupForward') + +const rand2uuid = (rand: number) => { + return 16777216n << 32n | BigInt(rand) +} + +export const sendGroupForwardMessage = async ( + groupId: number, + forwardData: BotMessageSendElements.ForwardElement, +) => { + const seq = randomBytes(2).readUInt16BE() + const rand = randomBytes(4).readUInt32BE() + const msgUid = rand2uuid(rand) + log.info('seq:', seq, 'rand:', rand, 'msgUid:', msgUid) + + const normalizedForwardData = forwardData.groupId + ? forwardData + : { ...forwardData, groupId } + const forwardConverter = new ForwardConverter(normalizedForwardData) + await forwardConverter.uploadImage() + + const transmit = SsoSendLongMsgReq.encode({ + body: { + fileName: 'MultiMsg', + data: { + msgs: forwardConverter.toPbMsg(), + }, + }, + }).finish() + log.info('transmit data:', Buffer.from(transmit).toString('hex')) + + const body = SendLongMsgReq.encode({ + info: { + type: 3, + peer: { + uid: groupId.toString(), + }, + groupCode: groupId, + payload: new Uint8Array(gzipSync(transmit)), + }, + settings: { + field1: 4, + field2: 1, + field3: 3, + field4: 0, + }, + }).finish() + log.info('full data:', Buffer.from(body).toString('hex')) + + const sendLongResp = await sendCustomPkgV2('trpc.group.long_msg_interface.MsgService.SsoSendLongMsg', body) + log.info('send long resp:', Buffer.from(sendLongResp).toString('hex')) + const sendLongResult = SendLongMsgResp.decode(sendLongResp, sendLongResp.byteLength) + const resId = sendLongResult.result?.resId + if (!resId) { + throw new Error('发送失败,没有获取到resId') + } + + const xml = forwardConverter.toXml(resId) + + log.info('send message.') + const sendResultData = await sendCustomPkgV2('MessageSvc.PbSendMsg', PbSendMsgReq.encode({ + routingHead: { + grp: { + groupCode: groupId, + }, + }, + contentHead: { + pkgNum: 1, + pkgIndex: 0, + divSeq: 0, + }, + msgBody: { + richText: { + elems: [ + { + richMsg: { + bytesTemplate1: new Uint8Array(Buffer.from(xml)), + uint32ServiceId: 35, + bytesMsgResid: new Uint8Array(Buffer.from(resId)), + }, + }, + ], + }, + }, + msgSeq: randomBytes(2).readUInt16BE(), + msgRand: randomBytes(4).readUInt32BE(), + msgVia: 0, + }).finish()) + return PbSendMsgResp.decode(sendResultData, sendResultData.byteLength) +} diff --git a/src/ntqq/message/http-ticket.ts b/src/ntqq/message/http-ticket.ts new file mode 100644 index 0000000..9bf1206 --- /dev/null +++ b/src/ntqq/message/http-ticket.ts @@ -0,0 +1,27 @@ +import { useLogger } from "../../common/log"; +import { sendCustomPkgV2 } from "../../native/native"; +import { ReqBody, RspBody } from "../protobuf/subcmd0x501"; + +const log = useLogger('NTMessage/HttpTicket') + +export const getHttpTicket = async (): Promise => { + // com.tencent.mobileqq\classes.jar\com\tencent\mobileqq\highway\config\HwServlet.java + log.info('Getting http ticket...') + const resBuf = await sendCustomPkgV2('HttpConn.0x6ff_501', ReqBody.encode({ + msgSubcmd0x501ReqBody: { + uint64Uin: 0, + uint32IdcId: 0, + uint32Appid: 16, + uint32LoginSigType: 1, + uint32RequestFlag: 3, + rptUint32ServiceTypes: [1, 5, 10, 21], + uint32Term: 2, + uint32Plat: 9, + uint32Net: 8, + bytesVer: '1.0.1', + }, + }).finish()) + const resp = RspBody.decode(resBuf) + log.info('Getting http ticket response:', resp) + return resp.msgSubcmd0x501RspBody?.bytesHttpconnSigSession || new Uint8Array() +} diff --git a/src/ntqq/message/message.ts b/src/ntqq/message/message.ts index e9ea2a9..ed366d1 100644 --- a/src/ntqq/message/message.ts +++ b/src/ntqq/message/message.ts @@ -1,13 +1,15 @@ -import { randomUUID } from "crypto" import { NTSendMessageType, AddMsgType, NTRecallMessage as NTRecallMessageType } from "./interfaces" import { useLogger } from "../../common/log" -import { useStore } from "../../store/store" -import { Lock } from "../../common/lock" -import { useNTCore } from "../core/core" +import { getNTMsgService } from "../core/core" +import { createNTEventWaiter } from "../query/wait" const log = useLogger('NTMessage') -// const msgLock = new Lock() -const { registerEventListener } = useStore() + +const waitForAddSendMessage = () => createNTEventWaiter<[AddMsgType], AddMsgType>({ + event: 'KernelMsgListener/onAddSendMsg', + match: () => true, + select: (payload) => payload, +}) /** * 调用NTAPI发送普通消息 @@ -16,24 +18,19 @@ const { registerEventListener } = useStore() * @returns 发送结果 */ export const NTSendMessage = async (msg: NTSendMessageType.SendRequest): Promise => { - const { getWrapperSession } = useNTCore() - const session = getWrapperSession() - const service = session.getMsgService() - // await msgLock.lock() - const msgInfo = new Promise((resolve) => { - - registerEventListener('KernelMsgListener/onAddSendMsg', 'once', (payload) => { - resolve(payload) - }) - }) - await service.sendMsg(msg.msgId, msg.peer, msg.msgElements, msg.msgAttributeInfos) - - const info = await msgInfo - log.info('onAddSendMsg info:', JSON.stringify(info, null, 4)) - // msgLock.unlock() - return { - msgId: info.msgId, - subMsgType: info.subMsgType, + const service = getNTMsgService() + const waiter = waitForAddSendMessage() + try { + await service.sendMsg(msg.msgId, msg.peer, msg.msgElements, msg.msgAttributeInfos) + const info = await waiter.promise + log.info('onAddSendMsg info:', JSON.stringify(info, null, 4)) + return { + msgId: info.msgId, + subMsgType: info.subMsgType, + } + } catch (error) { + waiter.cancel() + throw error } } @@ -45,32 +42,20 @@ export const NTSendMessage = async (msg: NTSendMessageType.SendRequest): Promise */ export const NTSendForwardMessage = async (msg: NTSendMessageType.SendForwardRequest): Promise => { log.info('send data:', msg, msg.msgInfos) - const uuid = randomUUID() - // nodeIKernelMsgService/forwardMsgWithComment 是逐条转发 - // 合并转发 - const reqData: [string, NTSendMessageType.SendForwardRequest, any] = [ - "nodeIKernelMsgService/multiForwardMsgWithComment", - msg, - null - ] - - // await msgLock.lock() - const msgInfo = new Promise((resolve) => { - - registerEventListener('KernelMsgListener/onAddSendMsg', 'once', (payload) => { - resolve(payload) - }) - }) + const waiter = waitForAddSendMessage() - const { getWrapperSession } = useNTCore() - const msgService = getWrapperSession().getMsgService() - const sendResult = await msgService.multiForwardMsgWithComment(msg.msgInfos, msg.srcContact, msg.dstContact, msg.commentElements, new Map()) - const info = await msgInfo - log.info('onAddSendMsg info:', JSON.stringify(info, null, 4)) - // msgLock.unlock() - return { - msgId: info.msgId, - subMsgType: info.subMsgType, + const msgService = getNTMsgService() + try { + await msgService.multiForwardMsgWithComment(msg.msgInfos, msg.srcContact, msg.dstContact, msg.commentElements, new Map()) + const info = await waiter.promise + log.info('onAddSendMsg info:', JSON.stringify(info, null, 4)) + return { + msgId: info.msgId, + subMsgType: info.subMsgType, + } + } catch (error) { + waiter.cancel() + throw error } } @@ -83,10 +68,9 @@ export const NTSendForwardMessage = async (msg: NTSendMessageType.SendForwardReq export const NTRecallMessage = async (msg: NTRecallMessageType.Request): Promise => { log.info(`recall msg:`, msg) - const { getWrapperSession } = useNTCore() - const msgService = getWrapperSession().getMsgService() + const msgService = getNTMsgService() const sendResult = await msgService.recallMsg(msg.peer, msg.msgIds) log.info('onAddSendMsg info:', JSON.stringify(sendResult, null, 4)) return sendResult -} \ No newline at end of file +} diff --git a/src/ntqq/query/detail-info.ts b/src/ntqq/query/detail-info.ts new file mode 100644 index 0000000..e1ab569 --- /dev/null +++ b/src/ntqq/query/detail-info.ts @@ -0,0 +1,45 @@ +import { useLogger } from "../../common/log" +import { getNTGroupService, getNTProfileService } from "../core/core" +import { GroupDetailInfoType, UserDetailInfoType } from "../../onebot/contracts/common" +import { createNTEventWaiter } from "./wait" +const log = useLogger('NTQQ/Query/DetailInfo') + +export const getUserInfoByUid = (uid: `u_${string}`): Promise => { + log.info('getUserInfoByUid start') + return new Promise(async (resolve, reject) => { + const waiter = createNTEventWaiter<[UserDetailInfoType], UserDetailInfoType>({ + event: 'KernelProfileListener/onUserDetailInfoChanged', + match: (payload) => payload.uid === uid, + select: (payload) => payload, + }) + try { + const service = getNTProfileService() + log.info('getUserInfoByUid call', service) + const result = await service.fetchUserDetailInfo('BuddyProfileStore', [uid], 1, [0]) + log.info(`getUserInfoByUid[${uid}]:`, result) + resolve(await waiter.promise) + } catch (error) { + waiter.cancel() + reject(error) + } + }) +} + +export const getGroupInfoById = (gid: number): Promise => { + return new Promise(async (resolve, reject) => { + const waiter = createNTEventWaiter<[GroupDetailInfoType], GroupDetailInfoType>({ + event: 'KernelGroupListener/onGroupDetailInfoChange', + match: (payload) => payload.groupCode === `${gid}`, + select: (payload) => payload, + }) + try { + const service = getNTGroupService() + const result = await service.getGroupDetailInfo(`${gid}`, 1) + log.info(`getGroupDetailInfo[${gid}]:`, result) + resolve(await waiter.promise) + } catch (error) { + waiter.cancel() + reject(error) + } + }) +} diff --git a/src/ntqq/query/interfaces.ts b/src/ntqq/query/interfaces.ts new file mode 100644 index 0000000..10a3b75 --- /dev/null +++ b/src/ntqq/query/interfaces.ts @@ -0,0 +1 @@ +export type { GroupDetailInfoType, UserDetailInfoType } from "../../onebot/contracts/common" diff --git a/src/ntqq/query/wait.ts b/src/ntqq/query/wait.ts new file mode 100644 index 0000000..fa60fa1 --- /dev/null +++ b/src/ntqq/query/wait.ts @@ -0,0 +1,51 @@ +import { registerEventListener } from "../../store/event-registry" + +const DEFAULT_QUERY_TIMEOUT = 30000 + +interface WaitForNTEventOptions { + event: `${string}/${string}` + timeoutMs?: number + match: (...args: TArgs) => boolean + select: (...args: TArgs) => TResult +} + +export const createNTEventWaiter = ({ + event, + timeoutMs = DEFAULT_QUERY_TIMEOUT, + match, + select, +}: WaitForNTEventOptions) => { + let listener: { remove: () => void } | null = null + let timeout: ReturnType | null = null + + const cleanup = () => { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + listener?.remove() + listener = null + } + + const promise = new Promise((resolve, reject) => { + timeout = setTimeout(() => { + cleanup() + reject(`${event} timeout`) + }, timeoutMs) + + listener = registerEventListener(event, 'always', (...args: TArgs) => { + if (!match(...args)) return + cleanup() + resolve(select(...args)) + }) + }) + + return { + promise, + cancel: cleanup, + } +} + +export const waitForNTEvent = (options: WaitForNTEventOptions): Promise => { + return createNTEventWaiter(options).promise +} diff --git a/src/ntqq/store/config-app.ts b/src/ntqq/store/config-app.ts new file mode 100644 index 0000000..50b11b2 --- /dev/null +++ b/src/ntqq/store/config-app.ts @@ -0,0 +1,27 @@ +import { useLogger } from "../../common/log" +import { getNTPackageInfo } from "../common/utils" +import { StoreAppInfo } from "../types/store/config" +import { getNTOsVersion } from "./config-device" +import { + getNTAppId, + getNTDesktopPlatform, + getNTDesktopPlatformType, +} from "./config-platform" + +const log = useLogger('NTConfig') + +export const getNTAppInfo = (): StoreAppInfo => { + const pkgInfo = getNTPackageInfo() + const platform = getNTDesktopPlatform() + const osVersion = getNTOsVersion() + log.info('osVersion:', osVersion) + return { + app_id: getNTAppId(), + platform_type: getNTDesktopPlatformType(), + app_type: 4, + app_version: pkgInfo.version, + os_version: osVersion, + qua: `V1_${platform === 'win32' ? 'WIN' : 'LNX'}_NQ_${pkgInfo.version.replace('-', '_')}_GW_B`, + clientVer: pkgInfo.version, + } +} diff --git a/src/ntqq/store/config-device.ts b/src/ntqq/store/config-device.ts new file mode 100644 index 0000000..01d1dec --- /dev/null +++ b/src/ntqq/store/config-device.ts @@ -0,0 +1,38 @@ +import { execSync } from "child_process" +import { randomUUID } from "crypto" +import { hostname } from "os" +import { getNTPackageInfo } from "../common/utils" +import { + getNTDesktopDeviceName, + getNTDesktopPlatform, +} from "./config-platform" + +export const getNTOsVersion = (): string => { + const platform = getNTDesktopPlatform() + if (platform === 'win32') { + return 'Windows 10 Pro' + } + + try { + return execSync('uname -v').toString() + } catch { + return '#1 SMP Thu Mar 7 03:22:57 UTC 2024' + } +} + +export const getNTDeviceInfo = (): NTNativeWrapper.DeviceInfo => { + const pkgInfo = getNTPackageInfo() + const platform = getNTDesktopPlatform() + return { + guid: randomUUID(), + buildVer: pkgInfo.version, + localId: 2052, + devName: hostname(), + devType: getNTDesktopDeviceName(), + vendorName: '', + osVer: getNTOsVersion(), + vendorOsName: platform, + setMute: false, + vendorType: 0, + } +} diff --git a/src/ntqq/store/config-path.ts b/src/ntqq/store/config-path.ts new file mode 100644 index 0000000..5d008ec --- /dev/null +++ b/src/ntqq/store/config-path.ts @@ -0,0 +1,12 @@ +import { homedir } from "os" +import { getNTWrapper } from "../core/service/nt-wrapper" +import { getNTDesktopPlatform } from "./config-platform" + +export const getNTConfigStoreFolder = (): string => { + const platform = getNTDesktopPlatform() + if (platform === 'win32') { + const wrapper = getNTWrapper() + return wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig() + '' + } + return `${homedir()}/.config/QQ` +} diff --git a/src/ntqq/store/config-platform.ts b/src/ntqq/store/config-platform.ts new file mode 100644 index 0000000..7be3ba4 --- /dev/null +++ b/src/ntqq/store/config-platform.ts @@ -0,0 +1,24 @@ +import { CustomError } from "../../common/error/custom-error" + +export type NTDesktopPlatform = 'win32' | 'linux' + +export const getNTDesktopPlatform = (): NTDesktopPlatform => { + if (process.platform === 'win32' || process.platform === 'linux') { + return process.platform + } + throw new CustomError(500, `Current platform ${process.platform} is not supported !`) +} + +export const getNTDesktopPlatformType = () => { + return getNTDesktopPlatform() === 'win32' ? 3 : 5 +} + +export const getNTDesktopDeviceName = () => { + return getNTDesktopPlatform() === 'win32' ? 'Windows_NT' : 'Linux' +} + +export const getNTAppId = () => { + return getNTDesktopPlatform() === 'win32' + ? '537249321' + : '537328659' +} diff --git a/src/ntqq/store/config.ts b/src/ntqq/store/config.ts index 7057e58..66cd468 100644 --- a/src/ntqq/store/config.ts +++ b/src/ntqq/store/config.ts @@ -1,111 +1,10 @@ -import { homedir, hostname } from "os"; -import { getNTPackageInfo } from "../common/utils"; -import { StoreAppInfo } from "../types/store/config"; -import { randomUUID } from "crypto"; -import { execSync } from "child_process"; -import { useLogger } from "../../common/log"; -import { useNTWrapper } from "../core/service/nt-wrapper"; -import { CustomError } from "../../server/error/custom-error"; - -const platform: 'win32' | 'linux' = process.platform as 'win32' | 'linux' -const log = useLogger('NTConfig') - -const getOsVersion = (): string => { - let ret = '' - switch(platform) { - case 'win32': - { - // const cmd = 'wmic os get Caption' - // ret = execSync(cmd).toString() - return 'Windows 10 Pro' - } - break; - case 'linux': - { - try - { - const cmd = 'uname -v' - ret = execSync(cmd).toString() - }catch(err) { - ret = '#1 SMP Thu Mar 7 03:22:57 UTC 2024' - } - } - break; - default: - throw new CustomError(500, `Current platform ${platform} is not supported !`) - break - } - return ret - } - -const getDeviceInfo = (): NTNativeWrapper.DeviceInfo => { - const guid = randomUUID() - const pkgInfo = getNTPackageInfo() - const osVersion = getOsVersion() - return { - guid, - buildVer: pkgInfo.version, - localId: 2052, - devName: hostname(), - devType: platform == 'win32' ? 'Windows_NT' : 'Linux', - vendorName: '', - osVer: osVersion, - vendorOsName: platform, - setMute: false, - vendorType: 0 - } - } -const getAppInfo = (): StoreAppInfo => { - const pkgInfo = getNTPackageInfo() - const devInfo = getDeviceInfo() - log.info('deviceInfo:', devInfo) - return { - app_id: getAppId(), - /** - * 3 - windows - * 5 - linux - */ - platform_type: platform === 'win32' ? 3 : 5, - /** - * 4 - desktop ? - */ - app_type: 4, - app_version: pkgInfo.version, - os_version: devInfo.osVer, - /** - * - * V1_LNX_NQ_${getQQVersion(MobileQQ.getContext())}_${getQQVersionCode()}_GW_B - */ - qua: `V1_${platform === 'win32' ? 'WIN' : 'LNX' }_NQ_${pkgInfo.version.replace('-', '_')}_GW_B`, - clientVer: pkgInfo.version, - } -} -const getAppId = () => { - return platform === 'win32' - ? - '537249321' - : - // '537207207' - // major.node 中字符串搜索 5372 - // '537249996' - '537328659' - } -const getNTConfigStoreFolder = (): string => { - const wrapper = useNTWrapper() - switch(platform) { - case 'win32': - return wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig() + '' - case 'linux': - return `${homedir()}/.config/QQ` - break - default: - return './nt_qq' - break - } -} -export const useNTConfig = () => ({ - getNTConfigStoreFolder, - getAppId, - getDeviceInfo, - getAppInfo, -}) \ No newline at end of file +// @aggregation-boundary — 长期聚合出口,勿删 +export { getNTAppInfo } from "./config-app" +export { getNTDeviceInfo, getNTOsVersion } from "./config-device" +export { getNTConfigStoreFolder } from "./config-path" +export { + getNTAppId, + getNTDesktopDeviceName, + getNTDesktopPlatform, + getNTDesktopPlatformType, +} from "./config-platform" diff --git a/src/ntqq/store/user.ts b/src/ntqq/store/user.ts index 4822917..8b4bdad 100644 --- a/src/ntqq/store/user.ts +++ b/src/ntqq/store/user.ts @@ -1,13 +1,12 @@ import EventEmitter from "events" import { useLogger } from "../../common/log" -import { useAsyncStore } from "../../store/async-store" import { NodeIKernelLoginService } from "../types/services/NodeIKernelLoginService" -import { CustomError } from "../../server/error/custom-error" +import { CustomError } from "../../common/error/custom-error" import { User } from "../../models/contact/user" const log = useLogger('Store/User') -interface CoreData { - moduleIndex: number + +export interface NTRuntimeState { loginService: NodeIKernelLoginService wrapperSession: NTNativeWrapper.NodeIQQNTWrapperSession wrapperEngine: NTNativeWrapper.NodeIQQNTWrapperEngine @@ -16,56 +15,76 @@ interface CoreData { info: UserInfo user?: User } -const accountNTData: Record = {} -export const useNTUserStore = () => ({ - getUserInfo: (uin?: number) => { - let resultUin: number - if (!uin) - { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - resultUin = s?.get('uin') - if (!resultUin) - { - throw new CustomError(500, 'useNTUserStore -> getUserInfo id error.') - } - } - else - { - resultUin = uin - } - log.info('useNTUserStore -> getUserInfo', resultUin) - return accountNTData[resultUin]?.info - }, - /** - * 获取当前登录的用户 - * @returns - */ - getUser: () => { - let resultUin: number - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - resultUin = s?.get('uin') - if (!resultUin) - { - throw new CustomError(500, 'useNTUserStore -> getUser id error.') - } - log.info('useNTUserStore -> getUser', resultUin, accountNTData) - let user = accountNTData[resultUin]?.user - if (user) return user - user = User.create(resultUin) - accountNTData[resultUin].user = user - return user - }, - getAllAccountData: () => accountNTData, - getCurrentAccountData: () => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - const uin: number = s?.get('uin') - if (!uin) - { - throw new CustomError(500, 'useNTUserStore -> getCurrentAccountData id error.') - } - return accountNTData[uin] + +let currentRuntime: NTRuntimeState | null = null +let currentUin: number | undefined + +export const getCurrentUin = () => currentRuntime?.info.uin || currentUin + +export const setCurrentUin = (uin?: number) => { + currentUin = uin +} + +export const setCurrentRuntime = (runtime: NTRuntimeState) => { + currentRuntime = runtime + if (runtime.info.uin) { + currentUin = runtime.info.uin + } +} + +export const getUserInfo = (uin?: number) => { + const info = currentRuntime?.info + if (!info) { + return undefined + } + if (uin !== undefined && info.uin && info.uin !== uin) { + return undefined + } + log.info('NTUserStore -> getUserInfo', info.uin || currentUin) + return info +} + +/** + * 获取当前登录的用户 + * @returns + */ +export const getCurrentUser = () => { + const uin = currentRuntime?.info.uin || currentUin + if (!uin || !currentRuntime) { + throw new CustomError(500, 'current user id error.') } -}) \ No newline at end of file + log.info('NTUserStore -> getCurrentUser', uin, currentRuntime) + let user = currentRuntime.user + if (user) return user + user = User.create(uin) + currentRuntime.user = user + return user +} + +export const listCurrentUins = () => { + const uin = currentRuntime?.info.uin || currentUin + return uin ? [uin] : [] +} + +export const requireCurrentUin = () => { + const uin = currentRuntime?.info.uin || currentUin + if (!uin) { + throw new CustomError(500, 'current uin id error.') + } + return uin +} + +export const getCurrentRuntime = () => { + if (!currentRuntime) { + throw new CustomError(500, 'current runtime id error.') + } + return currentRuntime +} + +export const getCurrentSelfInfo = () => { + const runtime = currentRuntime + if (!runtime) { + throw new CustomError(500, 'current self info id error.') + } + return runtime.info +} diff --git a/src/ntqq/types/wrapper.d.ts b/src/ntqq/types/wrapper.d.ts index 767be9e..1512d11 100644 --- a/src/ntqq/types/wrapper.d.ts +++ b/src/ntqq/types/wrapper.d.ts @@ -10,7 +10,7 @@ declare namespace NTNativeWrapper { isNull(): boolean } namespace CrossProcessExports2 { - const useWrapper: (idx: number) => typeof CrossProcessExports + const useWrapper: (idx?: number) => typeof CrossProcessExports } namespace CrossProcessExports { const NodeIGlobalAdapter: typeof import("./adapters/NodeIGlobalAdapter").NodeIGlobalAdapter diff --git a/src/onebot/actions/bot/bot.ts b/src/onebot/actions/bot/bot.ts deleted file mode 100644 index 18928e1..0000000 --- a/src/onebot/actions/bot/bot.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { useLogger } from "../../../common/log" -import { NTGetQuickLoginList, NTGetLoginStatus, NTQuickLoginByUin } from "../../../ntqq/login/account" -import { useNTUserStore } from "../../../ntqq/store/user" -import { CustomError } from "../../../server/error/custom-error" -import { useStore } from "../../../store/store" -import { checkDeviceVerifyQRCodeStatus, getDeviceVerifyQRCode, loginByAccount, loginByQrCode } from "../../../transfer/login/login" -import { getUserInfoByUid } from "../../common/user" -import { UserInfoResp } from "../friend/interfaces" -import { BotActionParams } from "../interfaces" -import { getClipboardMsg } from "./clipboard" -import { QuickLoginItem } from "./types" - -const log = useLogger('Bot') -const getBotInfo = async (p: {}): Promise => { - const { getUserInfo } = useNTUserStore() - const userInfo = getUserInfo() - const resp: UserInfoResp = { - uid: "u_", - uin: 0, - nick: "", - userDisplayname: "", - userRemark: "", - avatarUrl: '' - } - if (!userInfo || !userInfo.uid) { - log.error('user info:', userInfo) - throw new CustomError(1, 'user info error!') - } - const ret = await getUserInfoByUid(userInfo.uid) - log.info('getUserDetailInfo:', ret) - resp.uid = ret.uid - resp.uin = parseInt(ret.uin) - resp.nick = ret.simpleInfo.coreInfo.nick - resp.avatarUrl = `http://q1.qlogo.cn/g?b=qq&nk=${resp.uin}&s=640` - return resp -} -const getQuickLoginList = async (p: {}) => { - const list = await NTGetQuickLoginList() - return list.map(e => ({ - uin: e.uin, - uid: e.uid, - nick_name: e.nickName, - face_path: e.facePath, - face_url: e.faceUrl, - login_type: e.loginType, - is_quick_login: e.isQuickLogin, - is_auto_login: e.isAutoLogin, - })) -} - -interface QuickLoginReq extends BotActionParams { - uin: `${number}` -} -const quickLoginByUin = async (p: QuickLoginReq) => { - const result = await NTQuickLoginByUin(p.uin) - if (result.result !== '0') - { - throw new CustomError(parseInt(result.result), result.loginErrorInfo.errMsg) - } - return {} -} -const getAccountList = (p: BotActionParams): any => { - const { getAllAccountData } = useNTUserStore() - const allAccount = getAllAccountData() - return Object.keys(allAccount).filter(e => e !== '1234567890') -} -const checkUserLogin = async (p: {}): Promise => { - const status = await NTGetLoginStatus() - return status -} -export const initBot = () => { - const { registerActionHandle } = useStore() - // 登录 - registerActionHandle('login_by_account', loginByAccount) - registerActionHandle('check_device_verify_qrcode_status', checkDeviceVerifyQRCodeStatus) - registerActionHandle('get_device_verify_qrcode', getDeviceVerifyQRCode) - registerActionHandle('login_by_qrcode', loginByQrCode) - registerActionHandle('get_self_info', getBotInfo) - registerActionHandle('is_self_login', checkUserLogin) - registerActionHandle('get_quick_login_list', getQuickLoginList) - registerActionHandle('quick_login_by_uin', quickLoginByUin) - registerActionHandle('get_clipboard_msg', getClipboardMsg) - registerActionHandle('get_account_list', getAccountList) -} \ No newline at end of file diff --git a/src/onebot/actions/bot/clipboard.ts b/src/onebot/actions/bot/clipboard.ts deleted file mode 100644 index b0315e0..0000000 --- a/src/onebot/actions/bot/clipboard.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NTParseClipboard2Msg } from "../../../ntqq/common/clipboard" - -export const getClipboardMsg = async (p: any) => { - return await NTParseClipboard2Msg() -} \ No newline at end of file diff --git a/src/onebot/actions/bot/types.d.ts b/src/onebot/actions/bot/types.d.ts deleted file mode 100644 index cc11298..0000000 --- a/src/onebot/actions/bot/types.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -export interface QuickLoginItem { - /** - * QQ号 - */ - uin: `${number}` - /** - * 用户ID - */ - uid: `u_${string}` - /** - * 昵称 - */ - nick_name: string - face_url: string - face_path: string - login_type: 1 | 2 - /** - * 是否快速登录 - */ - is_quick_login: boolean - /** - * 是否自动登录 - */ - is_auto_login: boolean -} \ No newline at end of file diff --git a/src/onebot/actions/friend/friend.ts b/src/onebot/actions/friend/friend.ts deleted file mode 100644 index e91659f..0000000 --- a/src/onebot/actions/friend/friend.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useStore } from "../../../store/store" -import { BotActionResponse } from "../interfaces" -import { NTGetFriendList, NTSendLikeFriend } from "../../../ntqq/friend/friend" -import { LikeUserReq, UserInfoReq, UserInfoResp } from "./interfaces" -import { useLogger } from "../../../common/log" -import { useNTCore } from "../../../ntqq/core/core" -import { getUserInfoByUid } from "../../common/user" - -const { registerActionHandle } = useStore() -const log = useLogger('Action/Friend') - -/** - * 获取好友列表 - * - * @param p 空参数 - * @returns 好友列表 - */ -const getFriendList = async (p: {}): Promise => { - const list = await NTGetFriendList() - const result = list.map(e => ({ - id: e.categoryId, - name: e.categroyName, - count: e.categroyMbCount, - friend_list: e.buddyList.map(f => ({ - uin: parseInt(f.uin), - nick: f.nick, - avatar_url: f.avatarUrl, - remark: f.remark, - long_nick: f.longNick, - })) - })) - return result -} - -/** - * 对好友进行点赞 - * - * @param p 点赞参数 - * @returns 点赞结果 - */ -const sendLikeFriend = async (p: LikeUserReq): Promise => { - log.info('sendLikeFriend') - const r = await NTSendLikeFriend(p.userId, p.count) - log.info('sendLikeFriend end') - // TODO: 错误码处理 - return r -} - -const getUserInfo = async (p: UserInfoReq): Promise => { - const resp: UserInfoResp = { - uid: "u_", - uin: 0, - nick: "", - userDisplayname: "", - userRemark: "", - avatarUrl: '' - } - const ret = await getUserInfoByUid(p.userUid) - log.info('getUserDetailInfo:', ret) - resp.uid = ret.uid - resp.uin = parseInt(ret.uin) - resp.nick = ret.simpleInfo.coreInfo.nick - resp.avatarUrl = `http://q1.qlogo.cn/g?b=qq&nk=${resp.uin}&s=640` - return resp -} -/** - * 初始化好友动作 - */ -export const initFriend = () => { - // 注册获取好友列表 - registerActionHandle('get_friend_list', getFriendList) - registerActionHandle('get_friend_list_with_group', getFriendList) - registerActionHandle('send_like_friend', sendLikeFriend) - registerActionHandle('get_user_info', getUserInfo) -} \ No newline at end of file diff --git a/src/onebot/actions/group/group.ts b/src/onebot/actions/group/group.ts deleted file mode 100644 index 32ba18f..0000000 --- a/src/onebot/actions/group/group.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useLogger } from "../../../common/log" -import { NTGetGroupList } from "../../../ntqq/group/list" -import { useStore } from "../../../store/store" -import { getGroupInfoById } from "../../common/group" -import { GroupInfoReq, GroupInfoResp } from "../friend/interfaces" -import { getGroupMsgList } from "./msg" -import { GroupDetailInfoResp } from "./types" - -const log = useLogger('Group/Member') - -const getGroupList = async (p: any): Promise => { - const ret = await NTGetGroupList() - log.info('getGroupList:', ret) - const code2role: Record = { - 2: 'member', - 3: 'manager', - 4: 'owner' - } - const resp: GroupDetailInfoResp[] = ret.map(e => ({ - code: parseInt(e.groupCode), - name: e.groupName, - avatarUrl: `https://p.qlogo.cn/gh/${e.groupCode}/${e.groupCode}/640/`, - role: code2role[e.memberRole], - top: e.isTop, - toppedTimestamp: parseInt(e.toppedTimestamp), - isConf: e.isConf, - })) - return resp -} - -const getGroupInfo = async (p: GroupInfoReq): Promise => { - const resp: GroupInfoResp = { - name: '', - avatarUrl: '', - id: p.groupId - } - const ret = await getGroupInfoById(p.groupId) - log.info('getGroupInfoById:', ret) - resp.name = ret.groupName - resp.avatarUrl = `https://p.qlogo.cn/gh/${p.groupId}/${p.groupId}/640` - return resp -} - -/** - * 初始化群组动作 - */ -export const initGroup = () => { - const { registerActionHandle } = useStore() - registerActionHandle('get_group_list', getGroupList) - registerActionHandle('get_group_info', getGroupInfo) - registerActionHandle('get_group_msg', getGroupMsgList) -} \ No newline at end of file diff --git a/src/onebot/actions/group/member.ts b/src/onebot/actions/group/member.ts deleted file mode 100644 index 9465ccf..0000000 --- a/src/onebot/actions/group/member.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useLogger } from "../../../common/log" -import { useNTUserStore } from "../../../ntqq/store/user" -import { CustomError } from "../../../server/error/custom-error" -import { useStore } from "../../../store/store" -import { GroupMemberDetailInfoType } from "../../common/interfaces" -import { GroupMemberInfoReq, GroupMemberInfoResp, GroupMemberNudgeReq, UserInfoReq, UserInfoResp } from "../friend/interfaces" - -const log = useLogger('Group/Member') -/** - * - * @param p - * @returns - */ -const getGroupMemberInfo = async (p: GroupMemberInfoReq): Promise => { - const resp: GroupMemberInfoResp = { - userUid: "u_", - userUin: 0, - nick: "", - cardName: "", - avatarUrl: '' - } - // 检查uid是否可用 - let ret: GroupMemberDetailInfoType | undefined = undefined - const { getUser } = useNTUserStore() - const user = getUser() - const group = user.getGroup(p.groupId) - if (p.userUid === undefined || p.userUid === null || p.userUid.length === 0) { - if (p.userUin <= 0) { - throw new CustomError(1, 'uid or uin is error!'); - } - const member = group.getMember(p.userUin) - ret = await member.getInfo() - } - else { - const member = group.getMemberByUid(p.userUid) - ret = await member.getInfo() - } - if (ret === undefined) { - throw new CustomError(1, 'Failed to get member info!'); - } - log.info('getGroupMemberInfoById:', ret) - resp.userUid = ret.uid - resp.userUin = parseInt(ret.uin) - resp.nick = ret.nick - resp.cardName = ret.cardName - resp.avatarUrl = `http://q1.qlogo.cn/g?b=qq&nk=${resp.userUin}&s=640` - return resp -} - -const sendNudgeToMember = async (p: GroupMemberNudgeReq): Promise => { - const { getUser } = useNTUserStore() - // 1. get operate user - const user = getUser() - // 2. get group - const group = user.getGroup(p.groupId) - if (!group) - { - throw new CustomError(10000002, '获取群实例失败') - } - // 3. get member - const member = group.getMember(p.userUin) - if (!member) - { - throw new CustomError(10000002, '获取群成员失败') - } - // 4. send nudge - return await member.nudge() - // 5. return result -} -/** - * 初始化好友动作 - */ -export const initGroupMember = () => { - const { registerActionHandle } = useStore() - registerActionHandle('send_nudge_to_member', sendNudgeToMember) - registerActionHandle('get_group_member_info', getGroupMemberInfo) -} \ No newline at end of file diff --git a/src/onebot/actions/group/msg.ts b/src/onebot/actions/group/msg.ts deleted file mode 100644 index ec9eb7a..0000000 --- a/src/onebot/actions/group/msg.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useLogger } from "../../../common/log" -import { NTGroupMsgList } from "../../../ntqq/group/msg" -import { convertNTMsg2BotMsg } from "../../../transfer/message/convert" -import { GroupMsgReq } from "./types" -const log = useLogger('GroupMsg') - -export const getGroupMsgList = async (p: GroupMsgReq) => { - const ret = await NTGroupMsgList(p.code, p.msgId, p.cnt) - log.info('group msg list:', JSON.stringify(ret, null, 4)) - let resp = ret.map(msg => convertNTMsg2BotMsg(msg)) - - return resp -} \ No newline at end of file diff --git a/src/onebot/actions/group/types.d.ts b/src/onebot/actions/group/types.d.ts deleted file mode 100644 index ebe80c6..0000000 --- a/src/onebot/actions/group/types.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { BotActionParams } from "../interfaces" - -/** - * 群列表类型 - */ -export interface GroupDetailInfoResp { - /** - * 群号 - */ - code: number - name: string - avatarUrl: string - role: 'owner' | 'member' | 'manager' - top: boolean - toppedTimestamp: number - isConf: boolean -} - -/** - * 群消息查询类型 - */ -export interface GroupMsgReq extends BotActionParams { - code: `${number}` - msgId: `${number}` - cnt: number -} \ No newline at end of file diff --git a/src/onebot/actions/index.ts b/src/onebot/actions/index.ts deleted file mode 100644 index 29c52a2..0000000 --- a/src/onebot/actions/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { initBot } from "./bot/bot" -import { initFriend } from "./friend/friend" -import { initGroup } from "./group/group" -import { initGroupMember } from "./group/member" -import { initRecallMessage } from "./message/recall_message" -import { initSendMessage } from "./message/send_message" - -/** - * 初始化onebot的动作 - */ -export const initAction = () => { - initBot() - initSendMessage() - initRecallMessage() - initGroup() - initGroupMember() - initFriend() -} \ No newline at end of file diff --git a/src/onebot/actions/message/recall_message.ts b/src/onebot/actions/message/recall_message.ts deleted file mode 100644 index 1930953..0000000 --- a/src/onebot/actions/message/recall_message.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useNTCore } from "../../../ntqq/core/core" -import { CustomError } from "../../../server/error/custom-error" -import { useStore } from "../../../store/store" -import { BotMessage } from "../../common/message" - -const recallMessage = async (p: BotMessage.DeleteMsg): Promise => { - const { getWrapperSession } = useNTCore() - const msgService = getWrapperSession().getMsgService() - const peerInfo: PeerInfo = { - chatType: 0, - peerUid: "", - guildId: "" - } - if (p.detailType == 'group') { - peerInfo.chatType = 2 - if (!p.groupId) { - throw new CustomError(1, 'group_id is not supported!') - } - peerInfo.peerUid = `${p.groupId}` - } - else if (p.detailType == 'private') { - peerInfo.chatType = 1 - if (!p.userId) { - throw new CustomError(1, 'user_id is not supported!') - } - peerInfo.peerUid = `${p.userId}` - } - else { - throw new CustomError(1, 'detail_type is not supported!') - } - const ret = msgService.recallMsg(peerInfo, p.messageId) - return ret -} -export const initRecallMessage = () => { - const { registerActionHandle } = useStore() - registerActionHandle('recall_message', recallMessage) -} \ No newline at end of file diff --git a/src/onebot/actions/message/send_message.ts b/src/onebot/actions/message/send_message.ts deleted file mode 100644 index 139ee02..0000000 --- a/src/onebot/actions/message/send_message.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useLogger } from "../../../common/log" -import { useNTUserStore } from "../../../ntqq/store/user" -import { CustomError } from "../../../server/error/custom-error" -import { useStore } from "../../../store/store" -import { sendMessageToGroup } from "../../../transfer/message/group" -import { BotMessage } from "../../common/message" - -const log = useLogger('ActionGroup') -const sendMessage = async (p: BotMessage.SendMsg): Promise => { - if (p.detailType === 'group' && p.groupId) { - const result = await sendMessageToGroup(`${p.groupId}`, p.message) - return result - } - else { - throw new CustomError(1, 'detail_type not supported!') - } -} - -/** - * 向群发送转发消息 - * - * 待完善 - * - * @param p 转发消息内容 - * @returns 发送结果 - */ -const sendForwardMessage = async (p: BotMessage.SendMsg): Promise => { - const { getUser } = useNTUserStore() - // 1. get operate user - const user = getUser() - - if (p.detailType === 'group' && p.groupId) { - // 2. get group - const group = user.getGroup(p.groupId) - if (!group) - { - throw new CustomError(10000002, '获取群实例失败') - } - /** - * 1. 检查是否为转发消息 - * 2. 存储转发消息的内容 - * 3. 创建并发送转发消息 - */ - const msg = p.message[0] - if (msg.type == 'multiforward') { - if (msg.data.forwardData) { - // 3. send forwardMsg - const result = await group.sendForwardMessage(msg.data.forwardData) - return result - } - } - } - else { - throw new CustomError(1, 'detail_type is not supported!') - } -} - -/** - * - */ -export const initSendMessage = () => { - const { registerActionHandle } = useStore() - registerActionHandle('send_message', sendMessage) - registerActionHandle('send_forward_message', sendForwardMessage) -} \ No newline at end of file diff --git a/src/onebot/common/group.ts b/src/onebot/common/group.ts deleted file mode 100644 index 10dd669..0000000 --- a/src/onebot/common/group.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { randomUUID } from "crypto" -import { NTEventListenerHandle, useStore } from "../../store/store" -import { CurrentAccountInfo, GroupDetailInfoType, GroupMemberDetailInfoType, UserDetailInfoType } from "./interfaces" -import { useNTCore } from "../../ntqq/core/core" -import { useNTUserStore } from "../../ntqq/store/user" -import { useLogger } from "../../common/log" -import { CustomError } from "../../server/error/custom-error" - -const { registerEventListener } = useStore() -const log = useLogger('Common/User') - -export const getGroupInfoById = (gid: number): Promise => { - return new Promise(async (resolve, reject) => { - let remover: { remove: () => void } | null = null - // 超时拒绝 - let time = setTimeout(() => { - if (remover) - remover?.remove() - reject('getGroupInfoByUid timeout') - }, 30000) - remover = registerEventListener(`KernelGroupListener/onGroupDetailInfoChange`, 'always', (payload: GroupDetailInfoType) => { - if (payload.groupCode !== `${gid}`) return - remover?.remove() - // 清除超时计时 - clearTimeout(time) - resolve(payload) - }) - const { getWrapperSession } = useNTCore() - const session = getWrapperSession() - const service = session.getGroupService() - const result = await service.getGroupDetailInfo(`${gid}`, 1) - log.info(`getGroupDetailInfo[${gid}]:`, result) - }) -} \ No newline at end of file diff --git a/src/onebot/common/interfaces.d.ts b/src/onebot/common/interfaces.d.ts deleted file mode 100644 index 100025b..0000000 --- a/src/onebot/common/interfaces.d.ts +++ /dev/null @@ -1,145 +0,0 @@ -export interface UserDetailInfoType { - /** - * 唯一标识 - */ - uid: `u_${string}` - /** - * QQ号 - */ - uin: string - simpleInfo: { - coreInfo: { - nick: string - } - } -} -export interface GroupDetailInfoType { - /** - * 群号 - */ - groupCode: `${number}`, - /** - * 群主uid - */ - ownerUid: `u_${string}`, - groupFlag: number, - groupFlagExt: number, - /** - * 最大成员数 - * - * 200 - */ - maxMemberNum: number, - /** - * 成员数量 - */ - memberNum: number, - groupOption: 4, - classExt: 34, - /** - * 群名称 - */ - groupName: string, - fingerMemo: '', - /** - * 入群提问 - */ - groupQuestion: string, - certType: 0, - richFingerMemo: '', - tagRecord: [], - shutUpAllTimestamp: 0, - shutUpMeTimestamp: 0, - groupTypeFlag: 1, - privilegeFlag: 590018, - groupSecLevel: 5, - groupFlagExt3: 9344, - isConfGroup: 0, - isModifyConfGroupFace: 1, - isModifyConfGroupName: 1, - noFigerOpenFlag: 1, - noCodeFingerOpenFlag: 1, - groupFlagExt4: 131072, - /** - * 群公告 - */ - groupMemo: string - cmdUinMsgSeq: 11474, - cmdUinJoinTime: 1680788268, - cmdUinUinFlag: 0, - cmdUinMsgMask: 1, - groupSecLevelInfo: 2, - cmdUinPrivilege: 2, - cmdUinFlagEx2: 0, - appealDeadline: 0, - remarkName: '', - isTop: false -} - -export interface GroupMemberDetailInfoType { - /** - * 唯一标识 - */ - uid: `u_${string}`, - qid: string, - /** - * QQ号 - */ - uin: `${number}`, - /** - * 昵称 - */ - nick: string - remark: '', - cardType: 0, - /** - * 群昵称 - */ - cardName: string - role: 4, - /** - * 头像路径 - * - * 磁盘的文件路径 - */ - avatarPath: string - shutUpTime: 0, - isDelete: false, - isSpecialConcerned: false, - isSpecialShield: false, - /** - * 是否机器人 - */ - isRobot: boolean, - groupHonor: '0x1064', - memberRealLevel: 100, - memberLevel: 2, - memberTitleId: 10315, - memberSpecialTitle: '', - specialTitleExpireTime: '0', - userShowFlag: 0, - userShowFlagNew: 0 -} - -/** - * 当前账户信息 - */ -export interface CurrentAccountInfo { - /** - * qq号 - */ - account: string - mainAccount: string - /** - * qq号 - */ - uin: string - uid: `u_${string}` - nickName: string - gender: number - age: number - faceUrl: string - a2: string - d2: string - d2key: string -} \ No newline at end of file diff --git a/src/onebot/common/message.d.ts b/src/onebot/common/message.d.ts deleted file mode 100644 index febf89f..0000000 --- a/src/onebot/common/message.d.ts +++ /dev/null @@ -1,510 +0,0 @@ -import { BotActionParams } from "../actions/interfaces" - -export declare namespace BotMessage { - - /** - * 发送消息 - * - * bot -> framework - */ - export interface SendMsg extends BotActionParams { - detailType: 'private' | 'group' - groupId?: number - user_id?: string - message: SendElement[] - } - - /** - * 撤回消息 - */ - export interface DeleteMsg extends BotActionParams { - detailType: 'private' | 'group' - groupId?: number - userId?: `u_${string}` - messageId: `${number}`[] - } - - /** - * 消息基础类型 - * - * bot -> framework - */ - export interface SendElement { - /** - * text - 纯文本的内容 - * - * mention - At消息 - * - * mention_all - At所有成员 - * - * image - 图片(未实现) - * - * vioce - 语音消息(未实现) - * - * viedo - 视频消息(未实现) - * - * file - 文件消息(未实现) - * - * location - 位置消息(未实现) - * - * reply - 引用回复 - * - * multiforward - 转发消息(未实现) - * - */ - type: 'text' | 'mention' | 'mention_all' | 'image' | 'vioce' | 'audio' | 'video' | 'file' | 'location' | 'reply' | 'multiforward' - data: { - /** - * 纯文本的内容 - */ - text?: string - - /** - * 图片 - */ - pic?: BotMessageSendElements.PicElement - - /** - * 文件 - */ - file?: BotMessageSendElements.FileElement - - /** - * 图像等的文件ID - * - * 待定 - */ - file_id?: string - - /** - * At时的对象ID - * - * 待定 - * - * 注意区分 [全体成员] 与 [群成员] - */ - at?: BotMessageSendElements.AtElement - - /** - * 引用回复的信息 - */ - reply?: BotMessageSendElements.ReplyElement - - /** - * 转发消息的内容 - */ - forwardData?: BotMessageSendElements.ForwardElement - } - } - - /** - * 消息基础类型 - * - * framework -> bot - */ - export interface ReceiveElement { - /** - * text - 纯文本的内容 - * - * mention - At消息 - * - * mention_all - At所有成员 - * - * image - 图片(未实现) - * - * vioce - 语音消息(未实现) - * - * viedo - 视频消息(未实现) - * - * file - 文件消息(未实现) - * - * location - 位置消息(未实现) - * - * reply - 引用回复 - * - * multiforward - 转发消息(未实现) - * - */ - type: 'text' | 'mention' | 'mention_all' | 'image' | 'vioce' | 'audio' | 'video' | 'file' | 'location' | 'reply' | 'multiforward' - data: { - /** - * 纯文本的内容 - */ - text?: string - - /** - * 图片 - */ - pic?: BotMessageReceiveElements.PicElement - - /** - * 图像等的文件ID - * - * 待定 - */ - fileId?: string - - /** - * At时的对象ID - * - * 待定 - * - * 注意区分 [全体成员] 与 [群成员] - */ - at?: BotMessageReceiveElements.AtElement - - /** - * 引用回复的信息 - */ - reply?: BotMessageReceiveElements.ReplyElement - - /** - * 转发消息的内容 - */ - forwardList?: SendElement[][] | `${number}`[] - } - } -} - -/** - * 消息元素定义 - * - * bot -> framework - */ -declare namespace BotMessageSendElements { - /** - * 转发消息元素 - */ - interface ForwardElement { - /** - * 转发消息的来源 - * - * group - 群 - * - * friend - 好友 - */ - fromType: 'group' | 'friend' - - /** - * 来源群号,`from_type` 为group时必须 - */ - groupId?: number - - /** - * 来源好友QQ,`from_type` 为friend时必须 - */ - senderId?: `${number}` - - /** - * 转发消息的内容 - */ - items: ForwardItem[] - } - - /** - * 转发消息的内容 - */ - interface ForwardItem { - /** - * 消息id - * - * 有的时候会去数据库找一下,找不到会替换成随机生成的id - */ - msgId?: `${number}` - - /** - * QQ号 - */ - senderUin: number - - /** - * QQ用户UID - */ - senderUid: `u_${string}` - - /** - * 消息发送的时间 - */ - msgTime: number - - senderMemberName: string - - /** - * 组成消息内容的元素 - */ - elements: BotMessage.SendElement[] - } - - /** - * 引用回复消息类型 - * - * bot -> framework - */ - interface ReplyElement { - /** - * 消息ID - */ - msgId: `${number}` - /** - * 消息序列 - */ - msgSeq: `${number}` - /** - * 源消息的文字内容 - */ - text: string - /** - * 源消息发送者的id - */ - uid: `u_${string}` - /** - * 源消息发送者的id - */ - uin: `${number}` - } - - /** - * At特定人员 - * - * bot -> framework - */ - interface AtElement { - /** - * 是否是全体成员 - */ - isAll: boolean - /** - * 用户QQ - * - */ - uin: number - /** - * 用户ID - * - */ - uid: `u_${string}` - name?: string - } - - /** - * 图片 - * - * bot -> framework - */ - interface PicElement { - /** - * simple - 普通图片 - * - * emoji - 表情图片 - * - */ - type: 'simple' | 'emoji' - - /** - * 图片宽度 - */ - width: number - - /** - * 图片高度 - */ - height: number - - /** - * 图片大小 - */ - size: number - - /** - * 图片MD5 - */ - md5?: string - - - /** - * 网络路径 - * - */ - url?: string - - /** - * 图片路径 - * - */ - path: string - /** - * 上传准备后的数据 - */ - pbElem?: Uint8Array - } - - /** - * 文件 - * - * bot -> framework - */ - interface FileElement { - - /** - * 文件名称 - * - */ - name: string - - /** - * 视频文件路径 - * - */ - path: string - } - - /** - * 位置消息 - */ - export interface Location { - latitude: number - longitude: number - title: string - content: string - } - - /** - * 引用回复消息 - */ - export interface Reply { - message_id: string - user_id: string - } -} - -/** - * 消息元素定义 - * - * framework -> bot - */ -declare namespace BotMessageReceiveElements { - - /** - * 引用回复消息类型 - * - * bot -> framework - */ - interface ReplyElement { - /** - * 消息ID - */ - srcMsgId: `${number}` - /** - * 消息序列 - */ - msgSeq: `${number}` - /** - * 源消息的文字内容 - */ - text: string - /** - * 源消息发送者的QQ号 - */ - senderUin: `${number}` - /** - * 源消息发送者的id - */ - senderUid: `u_${string}` - } - - /** - * At特定人员 - * - * bot -> framework - */ - interface AtElement { - /** - * 是否是全体成员 - */ - isAll: boolean - /** - * 用户ID - * - * - */ - uid: `u_${string}` | 'all' - /** - * QQ号 - */ - uin: number - name?: string - } - - /** - * 图片 - * - * bot -> framework - */ - interface PicElement { - /** - * simple - 普通图片 - * - * emoji - 表情图片 - * - */ - type: 'simple' | 'emoji' - - /** - * 图片宽度 - */ - width: number - - /** - * 图片高度 - */ - height: number - - /** - * 图片大小 - */ - size: number - - /** - * 图片MD5 - */ - md5: string - - /** - * 文件uuid - */ - uuid: `${number}` - - /** - * 图片网络地址 - * - * 没有host, https://gchat.qpic.cn/ - * - * /gchatpic_new/发送者QQ/群号-uuid-MD5/0 - */ - url: string | null - - /** - * 图片路径 - */ - path: string - } - - - /** - * 位置消息 - */ - export interface Location { - latitude: number - longitude: number - title: string - content: string - } - - /** - * 引用回复消息 - */ - export interface Reply { - message_id: string - user_id: string - } -} \ No newline at end of file diff --git a/src/onebot/common/user.ts b/src/onebot/common/user.ts deleted file mode 100644 index 74ff26b..0000000 --- a/src/onebot/common/user.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { randomUUID } from "crypto" -import { NTEventListenerHandle, useStore } from "../../store/store" -import { CurrentAccountInfo, UserDetailInfoType } from "./interfaces" -import { useNTCore } from "../../ntqq/core/core" -import { useNTUserStore } from "../../ntqq/store/user" -import { useLogger } from "../../common/log" -import { CustomError } from "../../server/error/custom-error" - -const { registerEventListener } = useStore() -const log = useLogger('Common/User') - -export const getUserInfoByUid = (uid: `u_${string}`): Promise => { - log.info('getUserInfoByUid start') - return new Promise(async (resolve, reject) => { - let userInfoListener: {remove: () => void} | null = null - // 超时拒绝 - let time = setTimeout(() => { - userInfoListener?.remove() - reject('getUserInfoByUid timeout') - }, 30000) - userInfoListener = registerEventListener(`KernelProfileListener/onUserDetailInfoChanged`, 'always', (payload: UserDetailInfoType) => { - if (payload.uid !== uid) return; - // 清除超时计时 - clearTimeout(time) - userInfoListener?.remove() - resolve(payload) - }) - const { getWrapperSession } = useNTCore() - const session = getWrapperSession() - const service = session.getProfileService() - log.info('getUserInfoByUid call', service) - const result = await service.fetchUserDetailInfo('BuddyProfileStore', [uid], 1, [0]) - log.info(`getUserInfoByUid[${uid}]:`, result) - }) -} - -/** - * 获取机器人的信息 - * - * @returns - */ -export const getBotAccount = () => { - const { getUserInfo } = useNTUserStore() - const info = getUserInfo() - if (!info) - { - throw new CustomError(500, 'Failed to get user info') - } - return info -} \ No newline at end of file diff --git a/src/onebot/actions/interfaces.ts b/src/onebot/contracts/actions.ts similarity index 100% rename from src/onebot/actions/interfaces.ts rename to src/onebot/contracts/actions.ts diff --git a/src/onebot/actions/bot/interfaces.ts b/src/onebot/contracts/bot.ts similarity index 73% rename from src/onebot/actions/bot/interfaces.ts rename to src/onebot/contracts/bot.ts index 9b16db1..780d07c 100644 --- a/src/onebot/actions/bot/interfaces.ts +++ b/src/onebot/contracts/bot.ts @@ -1,12 +1,10 @@ -import { BotActionParams } from "../interfaces" +import { BotActionParams } from "./actions" /** * 登陆数据 * 机器人 <-> 框架 - * */ export declare namespace BotLogin { - /** * 请求体 * 机器人 -> 框架 @@ -55,9 +53,36 @@ export declare namespace BotLogin { tipsContent: string } } + export interface QrCodeResponse { qrCodeImage: string qrCodeUrl: string expireTime: number } -} \ No newline at end of file +} + +export interface QuickLoginItem { + /** + * QQ号 + */ + uin: `${number}` + /** + * 用户ID + */ + uid: `u_${string}` + /** + * 昵称 + */ + nick_name: string + face_url: string + face_path: string + login_type: 1 | 2 + /** + * 是否快速登录 + */ + is_quick_login: boolean + /** + * 是否自动登录 + */ + is_auto_login: boolean +} diff --git a/src/onebot/contracts/common.ts b/src/onebot/contracts/common.ts new file mode 100644 index 0000000..dfe4168 --- /dev/null +++ b/src/onebot/contracts/common.ts @@ -0,0 +1,144 @@ +export interface UserDetailInfoType { + /** + * 唯一标识 + */ + uid: `u_${string}` + /** + * QQ号 + */ + uin: string + simpleInfo: { + coreInfo: { + nick: string + } + } +} + +export interface GroupDetailInfoType { + /** + * 群号 + */ + groupCode: `${number}` + /** + * 群主uid + */ + ownerUid: `u_${string}` + groupFlag: number + groupFlagExt: number + /** + * 最大成员数 + */ + maxMemberNum: number + /** + * 成员数量 + */ + memberNum: number + groupOption: 4 + classExt: 34 + /** + * 群名称 + */ + groupName: string + fingerMemo: '' + /** + * 入群提问 + */ + groupQuestion: string + certType: 0 + richFingerMemo: '' + tagRecord: [] + shutUpAllTimestamp: 0 + shutUpMeTimestamp: 0 + groupTypeFlag: 1 + privilegeFlag: 590018 + groupSecLevel: 5 + groupFlagExt3: 9344 + isConfGroup: 0 + isModifyConfGroupFace: 1 + isModifyConfGroupName: 1 + noFigerOpenFlag: 1 + noCodeFingerOpenFlag: 1 + groupFlagExt4: 131072 + /** + * 群公告 + */ + groupMemo: string + cmdUinMsgSeq: 11474 + cmdUinJoinTime: 1680788268 + cmdUinUinFlag: 0 + cmdUinMsgMask: 1 + groupSecLevelInfo: 2 + cmdUinPrivilege: 2 + cmdUinFlagEx2: 0 + appealDeadline: 0 + remarkName: '' + isTop: false +} + +export interface GroupMemberDetailInfoType { + /** + * 唯一标识 + */ + uid: `u_${string}` + qid: string + /** + * QQ号 + */ + uin: `${number}` + /** + * 昵称 + */ + nick: string + remark: '' + cardType: 0 + /** + * 群昵称 + */ + cardName: string + role: 4 + /** + * 头像路径 + * + * 磁盘的文件路径 + */ + avatarPath: string + shutUpTime: 0 + isDelete: false + isSpecialConcerned: false + isSpecialShield: false + /** + * 是否机器人 + */ + isRobot: boolean + groupHonor: '0x1064' + memberRealLevel: 100 + memberLevel: 2 + memberTitleId: 10315 + memberSpecialTitle: '' + specialTitleExpireTime: '0' + userShowFlag: 0 + userShowFlagNew: 0 +} + +/** + * 当前账户信息 + */ +export interface CurrentAccountInfo { + /** + * qq号 + */ + account: string + mainAccount: string + /** + * qq号 + */ + uin: string + uid: `u_${string}` + nickName: string + gender: number + age: number + faceUrl: string + a2: string + d2: string + d2key: string +} diff --git a/src/onebot/event/interfaces.ts b/src/onebot/contracts/event.ts similarity index 93% rename from src/onebot/event/interfaces.ts rename to src/onebot/contracts/event.ts index 8afa66b..559d2a2 100644 --- a/src/onebot/event/interfaces.ts +++ b/src/onebot/contracts/event.ts @@ -1,4 +1,4 @@ -import { BotMessage } from "../common/message" +import { BotMessage } from "./message" /** * 事件数据 @@ -24,7 +24,7 @@ export interface EventDataType { /** * 机器人信息 */ -export interface BotUserInfo { +export interface BotUserInfo { id: number uid: `u_${string}` } @@ -67,7 +67,7 @@ export interface BotMessageData { /** * 撤回消息 - * + * * 回给对面ws的 */ export interface RecallMessageData { @@ -83,9 +83,10 @@ export interface RecallMessageData { */ time: number } + /** * 戳一戳消息 - * + * * 回给对面ws的 */ export interface NudgeMessageData { @@ -96,4 +97,4 @@ export interface NudgeMessageData { senderUid: `u_${string}` targetUin: number targetUid: `u_${string}` -} \ No newline at end of file +} diff --git a/src/onebot/actions/friend/interfaces.d.ts b/src/onebot/contracts/friend.ts similarity index 97% rename from src/onebot/actions/friend/interfaces.d.ts rename to src/onebot/contracts/friend.ts index 0e3df9c..38a7a06 100644 --- a/src/onebot/actions/friend/interfaces.d.ts +++ b/src/onebot/contracts/friend.ts @@ -1,4 +1,4 @@ -import { BotActionParams } from "../interfaces" +import { BotActionParams } from "./actions" //#region "UserInfo" /** diff --git a/src/onebot/contracts/group.ts b/src/onebot/contracts/group.ts new file mode 100644 index 0000000..ba46a21 --- /dev/null +++ b/src/onebot/contracts/group.ts @@ -0,0 +1,26 @@ +import { BotActionParams } from "./actions" + +/** + * 群列表类型 + */ +export interface GroupDetailInfoResp { + /** + * 群号 + */ + code: number + name: string + avatarUrl: string + role: 'owner' | 'member' | 'manager' + top: boolean + toppedTimestamp: number + isConf: boolean +} + +/** + * 群消息查询类型 + */ +export interface GroupMsgReq extends BotActionParams { + code: `${number}` + msgId: `${number}` + cnt: number +} diff --git a/src/onebot/contracts/message.ts b/src/onebot/contracts/message.ts new file mode 100644 index 0000000..b215764 --- /dev/null +++ b/src/onebot/contracts/message.ts @@ -0,0 +1,161 @@ +import { BotActionParams } from "./actions" + +export declare namespace BotMessage { + /** + * 发送消息 + * + * bot -> framework + */ + export interface SendMsg extends BotActionParams { + detailType: 'private' | 'group' + groupId?: number + user_id?: string + message: SendElement[] + } + + /** + * 撤回消息 + */ + export interface DeleteMsg extends BotActionParams { + detailType: 'private' | 'group' + groupId?: number + userId?: `u_${string}` + messageId: `${number}`[] + } + + /** + * 消息基础类型 + * + * bot -> framework + */ + export interface SendElement { + type: 'text' | 'mention' | 'mention_all' | 'image' | 'vioce' | 'audio' | 'video' | 'file' | 'location' | 'reply' | 'multiforward' + data: { + text?: string + pic?: BotMessageSendElements.PicElement + file?: BotMessageSendElements.FileElement + file_id?: string + at?: BotMessageSendElements.AtElement + reply?: BotMessageSendElements.ReplyElement + forwardData?: BotMessageSendElements.ForwardElement + } + } + + /** + * 消息基础类型 + * + * framework -> bot + */ + export interface ReceiveElement { + type: 'text' | 'mention' | 'mention_all' | 'image' | 'vioce' | 'audio' | 'video' | 'file' | 'location' | 'reply' | 'multiforward' + data: { + text?: string + pic?: BotMessageReceiveElements.PicElement + fileId?: string + at?: BotMessageReceiveElements.AtElement + reply?: BotMessageReceiveElements.ReplyElement + forwardList?: SendElement[][] | `${number}`[] + } + } +} + +export declare namespace BotMessageSendElements { + interface ForwardElement { + fromType: 'group' | 'friend' + groupId?: number + senderId?: `${number}` + items: ForwardItem[] + } + + interface ForwardItem { + msgId?: `${number}` + senderUin: number + senderUid: `u_${string}` + msgTime: number + senderMemberName: string + elements: BotMessage.SendElement[] + } + + interface ReplyElement { + msgId: `${number}` + msgSeq: `${number}` + text: string + uid: `u_${string}` + uin: `${number}` + } + + interface AtElement { + isAll: boolean + uin: number + uid: `u_${string}` + name?: string + } + + interface PicElement { + type: 'simple' | 'emoji' + width: number + height: number + size: number + md5?: string + url?: string + path: string + pbElem?: Uint8Array + } + + interface FileElement { + name: string + path: string + } + + export interface Location { + latitude: number + longitude: number + title: string + content: string + } + + export interface Reply { + message_id: string + user_id: string + } +} + +export declare namespace BotMessageReceiveElements { + interface ReplyElement { + srcMsgId: `${number}` + msgSeq: `${number}` + text: string + senderUin: `${number}` + senderUid: `u_${string}` + } + + interface AtElement { + isAll: boolean + uid: `u_${string}` | 'all' + uin: number + name?: string + } + + interface PicElement { + type: 'simple' | 'emoji' + width: number + height: number + size: number + md5: string + uuid: `${number}` + url: string | null + path: string + } + + export interface Location { + latitude: number + longitude: number + title: string + content: string + } + + export interface Reply { + message_id: string + user_id: string + } +} diff --git a/src/onebot/event/account.ts b/src/onebot/event/account.ts deleted file mode 100644 index c2c793b..0000000 --- a/src/onebot/event/account.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { useLogger } from "../../common/log" -import { useNTCore } from "../../ntqq/core/core" -import { KickedOfflinePayload } from "../../ntqq/types/listener/msg" -import { useServer } from "../../server/server" -import { useAsyncStore } from "../../store/async-store" -import { useStore } from "../../store/store" -import { EventDataType } from "./interfaces" - -const { registerEventListener } = useStore() -const log = useLogger('Event/Account') - -const kickedOffline = () => { - const { sendMessage } = useServer() - registerEventListener('KernelMsgListener/onKickedOffLine', 'always', (p1: KickedOfflinePayload) => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - log.info('onKickedOffLine async store:', s) - const uin = s?.get('uin') - if (!uin) { - log.error('onKickedOffLine: user not found') - return - } - const ret: EventDataType = { - self: { - id: uin, - uid: 'u_0' - }, - time: new Date().getTime(), - type: "notice", - detailType: 'account.kickedOffline', - subType: "", - data: { - p1, - } - } - sendMessage(ret) - - // 直接退出实例 - // TODO: 找一个不退出的办法 - setTimeout(process.exit, 5000, 1) - - // destory - // const { destroy } = useNTCore() - // destroy() - }) -} -export const listenAccountEvent = () => { - kickedOffline() -} \ No newline at end of file diff --git a/src/onebot/event/index.ts b/src/onebot/event/index.ts deleted file mode 100644 index 11d4909..0000000 --- a/src/onebot/event/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { listenAccountEvent } from "./account" -import { listenLoginEvent } from "./login" -import { listenMessage } from "./message" - -/** - * 初始化onebot的事件 - */ -export const initEvent = () => { - listenMessage() - listenLoginEvent() - listenAccountEvent() -} \ No newline at end of file diff --git a/src/onebot/event/login.ts b/src/onebot/event/login.ts deleted file mode 100644 index 497b407..0000000 --- a/src/onebot/event/login.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { useLogger } from "../../common/log" -import { NTLogin } from "../../ntqq/login/interfaces" -import { useServer } from "../../server/server" -import { useAsyncStore } from "../../store/async-store" -import { useStore } from "../../store/store" -import { EventDataType } from "./interfaces" - -const { registerEventListener } = useStore() -const log = useLogger('Event/Login') -/** - * 获取登录二维码 - * @returns 二维码信息 - */ -const qrCodeFetch = () => { - const { sendMessage } = useServer() - const getQrCode = registerEventListener('KernelLoginListener/onQRCodeGetPicture', 'always', (payload: NTLogin.GetQRCodeResponse) => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - log.info('qrCodeFetch async store:', s) - const uin = s?.get('uin') - if (!uin) { - log.error('qrCodeFetch: user not found') - return - } - /** - * 二维码数据 - */ - const ret: EventDataType = { - self: { - id: parseInt(uin), - uid: 'u_0' - }, - time: new Date().getTime(), - type: "notice", - detailType: "login.qrcode.fetch", - subType: "", - data: payload - } - sendMessage(ret) - }) -} - -/** - * 超时 - * @returns 二维码信息 - */ -const qrCodeFailed = () => { - const { sendMessage } = useServer() - const getQrCode = registerEventListener('KernelLoginListener/onQRCodeSessionFailed', 'always', (p1: number, p2: number) => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - log.info('qrCodeFetch async store:', s) - const uin = s?.get('uin') - if (!uin) { - log.error('qrCodeFetch: user not found') - return - } - /** - * 未扫描,超时:1 10 - * 扫描了,超时未确认:1 10 - * 扫描了,拒绝:1 5 - */ - let detailType = 'login.qrcode.timeout' - if (p2 === 5) { - detailType = 'login.qrcode.refused' - } - const ret: EventDataType = { - self: { - id: uin, - uid: 'u_0' - }, - time: new Date().getTime(), - type: "notice", - detailType: detailType, - subType: "", - data: { - p1, - p2, - } - } - sendMessage(ret) - }) -} - -/** - * 登录二维码被扫描 - * - * @returns - */ -const qrCodeScaned = () => { - const { sendMessage } = useServer() - const getQrCode = registerEventListener('KernelLoginListener/onQRCodeSessionUserScaned', 'always', (p1: number, avatarUrl: string) => { - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - log.info('qrCodeFetch async store:', s) - const uin = s?.get('uin') - if (!uin) { - log.error('qrCodeFetch: user not found') - return - } - const ret: EventDataType = { - self: { - id: uin, - uid: 'u_0' - }, - time: new Date().getTime(), - type: "notice", - detailType: "login.qrcode.scanned", - subType: "", - data: { - p1, - avatarUrl: avatarUrl, - } - } - sendMessage(ret) - }) -} - -/** - * 二维码登录成功 - * - * @returns - */ -const qrCodeSuccess = () => { - const { sendMessage } = useServer() - const getQrCode = registerEventListener('KernelLoginListener/onQRCodeLoginSucceed', 'always', (userInfo) => { - - const ret: EventDataType = { - self: { - id: parseInt(userInfo.uin), - uid: userInfo.uid - }, - time: new Date().getTime(), - type: "notice", - detailType: "login.qrcode.success", - subType: "", - data: { - account: userInfo.account, - uin: userInfo.uin, - uid: userInfo.uid, - nick_name: userInfo.nickName, - face_url: userInfo.faceUrl, - } - } - sendMessage(ret) - }) -} - -/** - * 二维码登录成功 - * - * @returns - */ -const alreadyLogin = () => { - const { sendMessage } = useServer() - const getQrCode = registerEventListener('KernelLoginListener/onUserLoggedIn', 'always', (uin: number) => { - const ret: EventDataType = { - self: { - id: 0, - uid: 'u_' - }, - time: new Date().getTime(), - type: "notice", - detailType: "login.qrcode.alreadyLogin", - subType: "", - data: { - uin: uin, - } - } - sendMessage(ret) - }) -} - -const deviceConfirmStatus = () => { - const { sendMessage } = useServer() - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - log.info('qrCodeFetch async store:', s) - const uin = s?.get('uin') - if (!uin) { - log.error('qrCodeFetch: user not found') - return - } - // 手Q确认异常的处理 - /** - * 5 - 手Q取消授权 - * 9 - 手Q拒绝授权 - * 10 - 手机端长时间未确认 - */ - const getStatus = registerEventListener('KernelLoginListener/OnConfirmUnusualDeviceFailed', 'always', (errCode: number, errMsg: string, errTips: NTLogin.PayloadConfirmUnusualDeviceFailed) => { - - log.info('deviceConfirmStatus payload:', errCode) - const ret: EventDataType = { - self: { - id: uin, - uid: 'u_' - }, - time: new Date().getTime(), - type: "notice", - detailType: "login.account.deviceConfirm", - subType: "", - data: errCode - } - - // 手Q有登录,但是手Q取消/拒绝授权 - if (errCode === 5) { - ret.detailType = 'login.account.deviceConfirm.cancel' - } - else if (errCode === 9) { - ret.detailType = 'login.account.deviceConfirm.rejected' - } - else if (errCode === 10) { - ret.detailType = 'login.account.deviceConfirm.timeout' - } else { - ret.detailType = 'login.account.deviceConfirm.unknown' - } - - sendMessage(ret) - }) -} - -const loginStatusChange = () => { - const { sendMessage } = useServer() - const asyncStore = useAsyncStore() - const s = asyncStore.getStore() - log.info('qrCodeFetch async store:', s) - const uin = s?.get('uin') - if (!uin) { - log.error('qrCodeFetch: user not found') - return - } - const onLoginState = registerEventListener('KernelLoginListener/onLoginState', 'always', (state: number) => { - log.info('onLoginState state:', state) - if (state === 0) { - // 手Q确认,登录成功 - const ret: EventDataType = { - self: { - id: uin, - uid: 'u_' - }, - time: new Date().getTime(), - type: "notice", - detailType: "login.account.deviceConfirm.success", - subType: "", - data: state - } - sendMessage(ret) - } - }) -} -/** - * 初始化时调用 - */ -export const listenLoginEvent = () => { - log.info('listenLoginEvent') - qrCodeFetch() - qrCodeFailed(); - qrCodeScaned(); - qrCodeSuccess(); - alreadyLogin(); - deviceConfirmStatus() - loginStatusChange() -} \ No newline at end of file diff --git a/src/onebot/event/message.ts b/src/onebot/event/message.ts deleted file mode 100644 index 623564a..0000000 --- a/src/onebot/event/message.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { convertNTMsg2BotMsg, convertNTMsgElement2BotMsgElement } from "../../transfer/message/convert" -import { NTReceiveMessageType } from "../../ntqq/message/interfaces" -import { useServer } from "../../server/server" -import { useStore } from "../../store/store" -import { getBotAccount, getUserInfoByUid } from "../common/user" -import { EventDataType, BotMessageData, RecallMessageData, NudgeMessageData } from "./interfaces" -import { useLogger } from "../../common/log" -import { CustomError } from "../../server/error/custom-error" -import { useNTUserStore } from "../../ntqq/store/user" -import { CommunicationPkg } from "../../ntqq/protobuf/communication" - -const { registerEventListener } = useStore() -const { sendMessage } = useServer() -const log = useLogger('event/Message') - -/** - * 监听新消息 - * - */ -const onRecvMsg = () => { - registerEventListener('KernelMsgListener/onRecvMsg', 'always', async (payload: NTReceiveMessageType.NTMessageItemType[]) => { - const msgList = payload - const user = getBotAccount() - if (user.uid === undefined || user.uin === undefined) - throw new CustomError(500, 'can not get user info!') - for (const msg of msgList) { - // 一分钟前的消息不处理 - if (Date.now()/1000 - parseInt(msg.msgTime) > 60) { - log.info('一分钟前的消息不处理') - continue - } - log.info('receive:', JSON.stringify(msg)) - // 频道消息暂不处理 - if (msg.chatType === 4) continue - - let uin: number = 0 - if (msg.senderUid) - { - const senderUserInfo = await getUserInfoByUid(msg.senderUid) - uin = parseInt(senderUserInfo.uin) - } - if (msg.msgType == 5 && msg.subMsgType === 12) - { - const ele = msg.elements[0] - if (ele.elementType !== 8) return; - const grayTip = ele.grayTipElement! - if (grayTip.subElementType !== 17) return; - const jg = grayTip.jsonGrayTipElement - if (jg.busiId !== '1061') return - const data = JSON.parse(jg.jsonStr) - const items = (data.items as any[]).filter(e => e.type === 'qq') - const from = items[0] - const to = items[1] - // 是戳一戳消息 - const ret: EventDataType = { - self: { - id: user.uin, - uid: user.uid - }, - time: parseInt(msg.msgTime), - type: "notice", - detailType: "group_member_nudge", - subType: "", - data: { - messageId: msg.msgId, - messageSeq: msg.msgSeq, - groupId: parseInt(msg.peerUin), - senderUin: parseInt((await getUserInfoByUid(from.uid)).uin), - senderUid: from.uid, - targetUin: parseInt((await getUserInfoByUid(to.uid)).uin), - targetUid: to.uid - } - } - switch (msg.chatType) { - case 1: - // 好友消息 - ret.detailType = 'private_nudge' - break - case 2: - // 群聊消息 - ret.detailType = 'group_nudge' - ret.data.groupId = parseInt(msg.peerUid) - break - } - sendMessage(ret) - return - } - const ret: EventDataType = { - self: { - id: user.uin, - uid: user.uid - }, - time: parseInt(msg.msgTime), - type: "message", - detailType: "", - subType: "", - data: { - messageId: msg.msgId, - messageSeq: msg.msgSeq, - groupId: 0, - groupName: '', - senderId: uin, - senderUid: msg.senderUid, - senderMemberName: msg.sendMemberName, - time: parseInt(msg.msgTime), - elements: [], - records: [], - } - } - switch (msg.chatType) { - case 1: - // 好友消息 - ret.detailType = 'private' - - break - case 2: - // 群聊消息 - ret.detailType = 'group' - ret.data.groupId = parseInt(msg.peerUid) - ret.data.groupName = msg.peerName - break - } - ret.data.elements = convertNTMsgElement2BotMsgElement({ - chatType: msg.chatType, - peerUid: msg.peerUid, - guildId: '' - }, msg.msgId, msg.elements) - for (const ele of ret.data.elements) { - if (ele.type === 'mention') { - if(false === ele.data.at?.isAll) { - // at特定成员,获取QQ号 - const { getUser } = useNTUserStore() - const user = getUser() - const group = user.getGroup(parseInt(msg.peerUid)) - const member = group.getMemberByUid(ele.data.at.uid as `u_${string}`) - const info = await member.getInfo() - ele.data.at.uin = parseInt(info.uin) - } - } - } - ret.data.records = msg.records.map(e => convertNTMsg2BotMsg(e)) - sendMessage(ret) - } - }) - // registerEventListener('KernelMsgListener/onRecvSysMsg', 'always', async (payload: number[]) => { - - // log.info('onRecvSysMsg:', payload) - // const data = CommunicationPkg.decode(new Uint8Array(payload[0])) - // log.info('onRecvSysMsg', data) - // }) -} - -/** - * 监听新发送的消息 - * - */ -const onAddSendMsg = () => { - registerEventListener('KernelMsgListener/onAddSendMsg', 'always', async (payload: NTReceiveMessageType.NTMessageItemType) => { - const msg = payload - const user = getBotAccount() - if (user.uid === undefined || user.uin === undefined) - throw new CustomError(500, 'can not get user info!') - // 一分钟前的消息不处理 - if (Date.now()/1000 - parseInt(msg.msgTime) > 60) { - log.info('一分钟前的消息不处理') - return; - } - log.info('receive:', JSON.stringify(msg)) - // 频道消息暂不处理 - if (msg.chatType === 4) return - - const senderUserInfo = await getUserInfoByUid(msg.senderUid) - const ret: EventDataType = { - self: { - id: user.uin, - uid: user.uid - }, - time: parseInt(msg.msgTime), - type: "notice", - detailType: "group_message_post_send", - subType: "", - data: { - messageId: msg.msgId, - messageSeq: msg.msgSeq, - groupId: 0, - groupName: '', - senderId: parseInt(senderUserInfo.uin), - senderUid: msg.senderUid, - senderMemberName: msg.sendMemberName, - time: parseInt(msg.msgTime), - elements: [], - records: [], - } - } - switch (msg.chatType) { - case 2: - // 群聊消息 - ret.data.groupId = parseInt(msg.peerUid) - break - } - ret.data.elements = convertNTMsgElement2BotMsgElement({ - chatType: msg.chatType, - peerUid: msg.peerUid, - guildId: '' - }, msg.msgId, msg.elements) - ret.data.records = msg.records.map(e => convertNTMsg2BotMsg(e)) - sendMessage(ret) - }) -} - -/** - * 监听消息更新 - */ -const onUpdateMsg = () => { - let oldPayload: string = '' - registerEventListener('KernelMsgListener/onMsgInfoListUpdate', 'always', async (payload: NTReceiveMessageType.NTMessageItemType[]) => { - const msgList = payload - { - // repeat check - const p = JSON.stringify(payload) - if (oldPayload === p) { - log.warn('onMsgInfoListUpdate payload no change') - return - } - oldPayload = p - } - log.info('onMsgInfoListUpdate count:', msgList.length) - const user = getBotAccount() - if (user.uid === undefined || user.uin === undefined) - throw new CustomError(500, 'can not get user info!') - for (const msg of msgList) { - log.info(`get user info for user: ${msg.senderUid}`) - const senderUserInfo = await getUserInfoByUid(msg.senderUid) - // 判断一下撤回消息 - const rMsg = msg.elements.find(e => e.elementType === 8 && e.grayTipElement?.subElementType === 1) - if (rMsg) { - // 是撤回消息 - const ret: EventDataType = { - self: { - id: user.uin, - uid: user.uid - }, - time: parseInt(msg.msgTime), - type: "notice", - detailType: "group_message_delete", - subType: "recall", - data: { - messageId: msg.msgId, - messageSeq: msg.msgSeq, - groupId: 0, - senderId: parseInt(senderUserInfo.uin), - senderUid: msg.senderUid, - senderMemberName: msg.sendMemberName || msg.sendNickName, - time: parseInt(msg.recallTime), - } - } - switch (msg.chatType) { - case 1: - // 好友消息 - ret.detailType = 'private_message_delete' - break - case 2: - // 群聊消息 - ret.detailType = 'group_message_delete' - ret.data.groupId = parseInt(msg.peerUid) - break - } - sendMessage(ret) - } - } - }) -} - -/** - * 监听消息事件 - * - * 每收到一条新消息就会触发事件 - */ -export const listenMessage = () => { - onRecvMsg() - onAddSendMsg() - onUpdateMsg() -} \ No newline at end of file diff --git a/src/onebot/onebot.ts b/src/onebot/onebot.ts deleted file mode 100644 index d7f5ded..0000000 --- a/src/onebot/onebot.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { initAction } from "./actions" -import { initEvent } from "./event" - -/** - * 初始化onebot - */ -export const initOnebot = () => { - initAction() -} \ No newline at end of file diff --git a/src/server/error/custom-error.ts b/src/server/error/custom-error.ts deleted file mode 100644 index 2355f35..0000000 --- a/src/server/error/custom-error.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class CustomError extends Error { - private _code: number - - constructor(code: number, msg: string) { - super(); - this._code = code - this.message = msg - } - get code() { - return this._code - } - -} \ No newline at end of file diff --git a/src/server/http.ts b/src/server/http.ts deleted file mode 100644 index 88fbb43..0000000 --- a/src/server/http.ts +++ /dev/null @@ -1,68 +0,0 @@ -import express from "express" -import { NextFunction, Request, Response } from 'express' -import { useConfigStore } from "../store/config"; -import { useLogger } from "../common/log"; -import router from "../http/route"; -import bodyParser from 'body-parser' -import fileUpload from 'express-fileupload' -import https from 'https' -import { readFileSync } from "fs"; -import { resolve } from "path"; -import { createServer } from "http"; - -const log = useLogger('HTTP Server') -/** - * 启动HTTP服务器 - */ -export const startHTTPServer = () => { - - const { getConfig } = useConfigStore() - const cfg = getConfig() - const app = express() - - app.use(bodyParser.json()) - app.use(bodyParser.urlencoded()) - const { getStoragePath } = useConfigStore() - const storagePath = getStoragePath() - log.info('storagePath:', storagePath) - app.use(fileUpload({ - createParentPath: true, - useTempFiles : true, - tempFileDir : storagePath, - preserveExtension: true, - })) - app.use((req, res, next) => { - log.info('before route', req.url) - var oldJson = res.json; - - res.json = function(body?: any){ - if (body?.code === undefined || body?.code === null) { - body = { - code: 0, - msg: 'success', - error: '', - data: body - } - } - return oldJson.apply(res, [body]); - } - next() - }) - app.use(router) - app.use((req, res, next) => { - log.info('after route') - // next() - }) - app.use((err: Error, req: Request, res: Response, next: NextFunction) => { - log.error('error handle:', err) - res.status(500).send({ - code: 500, - msg: 'error', - error: err.message, - data: null, - }) - }) - - const httpServer = createServer(app) - httpServer.listen(cfg.yui.http.port, cfg.yui.http.host) -} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts deleted file mode 100644 index 7d03d20..0000000 --- a/src/server/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useLogger } from "../common/log" -import { startHTTPServer } from "./http" -import { startWebsocketServer } from "./websocket" - -const log = useLogger('ServerIndex') - -export const startServer = () => { - log.info('start websocket server...') - startWebsocketServer() - log.info('start http server...') - startHTTPServer() -} \ No newline at end of file diff --git a/src/server/interfacces.ts b/src/server/interfacces.ts deleted file mode 100644 index e25c534..0000000 --- a/src/server/interfacces.ts +++ /dev/null @@ -1 +0,0 @@ -export type SendHandleType = (msg: string) => void \ No newline at end of file diff --git a/src/server/server.ts b/src/server/server.ts deleted file mode 100644 index c2e0d06..0000000 --- a/src/server/server.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { WebSocketServer } from "ws" -import { useLogger } from "../common/log" -import { startHTTPServer } from "./http" -import { startWebsocketServer } from "./websocket" -import { SendHandleType } from "./interfacces" -import { convertToSnakeCase } from "../common/utils" - -const log = useLogger('Server') -const clientHandleList: SendHandleType[] = [] - -/** - * 添加发送消息的监听器 - * - * @param h 发送消息的监听器 - */ -const addSendHandle = (h: SendHandleType) => { - clientHandleList.push(h) -} - -/** - * 移除发送消息的监听器 - * - * @param h 发送消息的监听器 - */ -const removeSendHandle = (h: SendHandleType) => { - const i = clientHandleList.indexOf(h) - if (i < 0) { - log.warn('事件处理器移除失败,未找到该处理器') - return - } - clientHandleList.splice(i, 1) -} - -/** - * 服务器主动发送消息给机器人 - * - * 框架 -> 机器人 - * - * @param msg 消息内容 - */ -const sendMessage = (msg: any) => { - for (let i = 0; i < clientHandleList.length; i++) { - const clientHandle = clientHandleList[i]; - try { - clientHandle(JSON.stringify(convertToSnakeCase(msg))) - } catch (error) { - clientHandleList.splice(i, 1) - i-- - } - } - -} -export const useServer = () => ({ - sendMessage, - removeSendHandle, - addSendHandle: addSendHandle -}) \ No newline at end of file diff --git a/src/server/websocket.ts b/src/server/websocket.ts deleted file mode 100644 index cdb830b..0000000 --- a/src/server/websocket.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { WebSocketServer } from 'ws' // https://github.com/websockets/ws/issues/1538#issuecomment-1150203821 -import { BotActionRequest, BotActionResponse } from '../onebot/actions/interfaces'; -import { useStore } from '../store/store'; -import { NIL as NIL_UUID } from 'uuid' -import { checkBaseRequestField } from '../common/action'; -import { useLogger } from '../common/log'; -import { useServer } from './server'; -import { useConfigStore } from '../store/config'; -import { CustomError } from './error/custom-error'; -import { convertToCamelCase, convertToSnakeCase } from '../common/utils'; -import { NTInitialize } from '../ntqq'; -import { useAsyncStore } from '../store/async-store'; -import { useNTUserStore } from '../ntqq/store/user'; -import { initEvent } from '../onebot/event'; - -const { getActionHandle } = useStore() -const log = useLogger('WebSocket') -const { addSendHandle, removeSendHandle } = useServer() - -/** - * 启动Websocket服务器 - */ -export const startWebsocketServer = () => { - - const { getConfig } = useConfigStore() - const cfg = getConfig() - const wss = new WebSocketServer({ - host: cfg.yui.ws.host, - port: cfg.yui.ws.port, - }); - wss.once('close', () => { - log.info('websocket server closed.') - }) - wss.once('listening', () => { - log.info(`websocket server listening on ${cfg.yui.ws.host}:${cfg.yui.ws.port}`) - }) - wss.on('connection', function connection(ws, req) { - log.info('receive connection.'); - const asyncStore = useAsyncStore() - try { - log.info('async run.'); - if (!req.url) { - log.error('url error') - ws.terminate() - ws.close(400, 'url error') - return; - } - // TODO: 注册事件监听 - const eventHandle = (msg: string) => { - ws.send(msg) - } - addSendHandle(eventHandle) - ws.on('close', (code, reason) => { - log.info('connection close.', code, reason.toString()) - removeSendHandle(eventHandle) - }) - - ws.on('message', function incoming(message) { - - asyncStore.run(new Map(), async () => { - let msg: BotActionRequest - try { - log.info('server: received: ', message.toString()); - // 此处解析可能会失败 - msg = JSON.parse(message.toString()) - const ret = checkBaseRequestField(msg) - if (ret !== undefined) { - ws.send(JSON.stringify(ret)); - return - } - log.info('uin:', msg.user.uin) - asyncStore.getStore()?.set('uin', msg.user.uin) - asyncStore.getStore()?.set('traceId', msg.id) - const { getUserInfo } = useNTUserStore() - let u = getUserInfo() - if (!u) { - log.info(`user(${msg.user.uin}) has not been initialized, start to initialize...`) - await NTInitialize() - initEvent() - } - } - catch (e) { - // 消息json格式不正确 - log.error(e) - const result: BotActionResponse = { - id: NIL_UUID, - status: 'failed', - retcode: 10004, - data: null, - message: 'Bad Request' - } - if (e instanceof CustomError) { - result.status = 'failed' - result.retcode = e.code - result.message = e.message - } - ws.send(JSON.stringify(result)); - return - } - - // 获取动作处理函数 - log.info('request action:', msg.action) - const handle = getActionHandle(msg.action) - if (handle != undefined) { - let resp: BotActionResponse = { - id: msg.id, - status: 'ok', - retcode: 0, - data: undefined, - message: '' - } - try { - log.info('start handle for ', msg.action) - resp.data = convertToSnakeCase(await handle(convertToCamelCase(msg.params))) - log.info('end handle for ', msg.action) - } - catch (e) { - log.error('内部错误:', e) - // TODO: 支持Error和CustomError处理 - if (e instanceof CustomError) { - resp.status = 'failed' - resp.retcode = e.code - resp.message = e.message - } - else { - resp = { - id: msg.id, - status: 'failed', - retcode: 20002, - data: null, - message: 'Internal Handler Error' - } - } - } - log.info(`reply ${resp.id}:`, JSON.stringify(resp)) - ws.send(JSON.stringify(resp)); - } - else { - // 没有找到处理函数 - const result: BotActionResponse = { - id: msg.id, - status: 'failed', - retcode: 10002, - data: null, - message: 'Unsupported Action' - } - ws.send(JSON.stringify(result)); - } - }); - }) - } - catch (err) { - log.error(err) - } - }); - wss.on('error', (e) => { - log.info('failed to create server!', e) - }) - return wss -} \ No newline at end of file diff --git a/src/store/action-registry.ts b/src/store/action-registry.ts new file mode 100644 index 0000000..c925332 --- /dev/null +++ b/src/store/action-registry.ts @@ -0,0 +1,23 @@ +import { BotActionParams } from "../onebot/contracts/actions" +import { useLogger } from "../common/log" + +export type ActionHandler = (payload: TPayload) => Promise +export type AnyActionHandler = (payload: any) => Promise +export type ActionHandlerMap = Readonly> + +const log = useLogger('Store/ActionRegistry') +const actionMap: Record = {} + +export const getRegisteredActionHandler = (action: string): AnyActionHandler | undefined => { + return actionMap[action] +} + +export const registerActionHandler = ( + name: TName, + handle: ActionHandler, +) => { + if (actionMap[name]) { + log.warn('Action: %s已经被注册,将覆盖旧的处理函数', name) + } + actionMap[name] = handle as AnyActionHandler +} diff --git a/src/store/async-store.ts b/src/store/async-store.ts deleted file mode 100644 index c0d32fa..0000000 --- a/src/store/async-store.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { AsyncLocalStorage } from "async_hooks"; - -export const useAsyncStore = () => (global as any).asyncLocalStorage as AsyncLocalStorage> \ No newline at end of file diff --git a/src/store/config.ts b/src/store/config.ts index 7fc7632..d2aad97 100644 --- a/src/store/config.ts +++ b/src/store/config.ts @@ -4,6 +4,7 @@ import { YuiConfig } from "./config-type" import { getNTPackageInfo } from "../ntqq/common/utils" import { useLogger } from "../common/log" import { resolve } from "path" +import { mergeDeep } from "../common/deep-merge" let configCache: YuiConfig = { yui: { @@ -28,6 +29,14 @@ let configCache: YuiConfig = { let inited: boolean = false const log = useLogger('Yui Config') +const cloneConfig = (config: TConfig): TConfig => { + return JSON.parse(JSON.stringify(config)) as TConfig +} + +const loadYamlConfig = (filePath: string) => { + return yaml.parse(readFileSync(filePath).toString()) as YuiConfig +} + /** * 从文件加载配置 * @@ -35,21 +44,18 @@ const log = useLogger('Yui Config') */ const loadFromFile = () => { log.info('loadFromFile') - const cfg = readFileSync(resolve(__dirname, './yui.yaml')).toString() - let defaultConfig = yaml.parse(cfg) as YuiConfig + const defaultPath = resolve(__dirname, './yui.yaml') + const localPath = resolve(__dirname, './yui.local.yaml') + const defaultConfig = loadYamlConfig(defaultPath) log.info('cfg data:', defaultConfig) - { - const localPath = resolve(__dirname, './yui.local.yaml') - if (existsSync(localPath)) { - const localCfg = readFileSync(localPath).toString() - const localConfig = yaml.parse(localCfg) as YuiConfig - defaultConfig = { - ...defaultConfig, - ...localConfig, - } - } + + if (!existsSync(localPath)) { + return defaultConfig } - return defaultConfig + + const localConfig = loadYamlConfig(localPath) + log.info('local cfg data:', localConfig) + return mergeDeep(cloneConfig(defaultConfig), localConfig) as YuiConfig } /** @@ -58,15 +64,11 @@ const loadFromFile = () => { * @param update 强制读取最新配置 * @returns 配置信息 */ -const getConfig = (update: boolean = false) => { +export const getConfig = (update: boolean = false) => { if (!inited || update) { try { inited = true - // TODO: 深层合并 - configCache = { - ...configCache, - ...loadFromFile() - } + configCache = mergeDeep(cloneConfig(configCache), loadFromFile()) as YuiConfig } catch (ex) { log.error('failed to load config!', ex) @@ -74,31 +76,13 @@ const getConfig = (update: boolean = false) => { } return configCache } -const getSignature = () => { +export const getSignature = () => { const pkg = getNTPackageInfo() const cfg = getConfig() if (process.platform === 'linux' || process.platform === 'win32') return cfg.yui.signature[process.platform][pkg.version] } -const getStoragePath = () => { - const cfg = getConfig() - return resolve(__dirname, cfg.yui['storage-path']) +export const getStoragePath = () => { + const cfg = getConfig() + return resolve(__dirname, cfg.yui['storage-path']) } -export const useConfigStore = () => ({ - /** - * 获取配置 - */ - getConfig, - /** - * 获取hook签名 - * - * 用于native使用 - */ - getSignature, - /** - * 存储路径 - * - * 绝对路径 - */ - getStoragePath, -}) \ No newline at end of file diff --git a/src/store/event-registry.ts b/src/store/event-registry.ts new file mode 100644 index 0000000..9c502f9 --- /dev/null +++ b/src/store/event-registry.ts @@ -0,0 +1,38 @@ +import { getCurrentNTDispatcher } from "../ntqq/core/dispatcher" + +/** + * NT事件监听器类型 + */ +export type NTEventListenerHandle = (...payload: any[]) => void + +/** + * Service/action + */ +export type EventFullNameType = `${string}/${string}` + +/** + * 添加事件的监听器 + * @param eventFullName 事件名称 + * @param type 事件类型 always长期 | once一次性 + * @param listener 事件监听器 + */ +export const registerEventListener = (eventFullName: EventFullNameType, type: 'always' | 'once', listener: NTEventListenerHandle) => { + const dispatcher = getCurrentNTDispatcher() + if (type === 'once') { + dispatcher.once(eventFullName, listener) + } + else if (type === 'always') { + dispatcher.on(eventFullName, listener) + } + return { + remove: () => dispatcher.removeListener(eventFullName, listener) + } +} + +/** + * 移除事件的监听器 + */ +export const removeEventListener = (eventFullName: EventFullNameType, listener: NTEventListenerHandle) => { + const dispatcher = getCurrentNTDispatcher() + dispatcher.removeListener(eventFullName, listener) +} diff --git a/src/store/runtime.ts b/src/store/runtime.ts new file mode 100644 index 0000000..248b8d8 --- /dev/null +++ b/src/store/runtime.ts @@ -0,0 +1,11 @@ +let currentTraceId: string | undefined + +export const getTraceId = () => currentTraceId + +export const setTraceId = (traceId?: string) => { + currentTraceId = traceId +} + +export const clearTraceId = () => { + currentTraceId = undefined +} diff --git a/src/store/store.ts b/src/store/store.ts deleted file mode 100644 index 96784c1..0000000 --- a/src/store/store.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { AsyncLocalStorage } from "node:async_hooks" -import { useLogger } from "../common/log" -import { useNTDispatcher } from "../ntqq/core/dispatcher" -import { BotActionParams } from "../onebot/actions/interfaces" - -const log = useLogger('Store') - -const ActionMap: Record Promise> = {} - -/** - * 获取动作处理函数 - * @param action 动作名称 - * @returns 处理函数 - */ -const getActionHandle = (action: string) => { - return ActionMap[action] -} - -/** - * 注册动作处理函数 - * @param name 动作名称 - * @param handle 动作处理函数 - */ -const registerActionHandle = (name: string, handle: (p: T) => Promise) => { - if (ActionMap[name]) { - log.warn('Action: %s已经被注册,将覆盖旧的处理函数', name) - } - ActionMap[name] = handle -} - -/** - * NT事件监听器类型 - */ -export type NTEventListenerHandle = (...payload: any[]) => void - -/** - * Service/action - */ -type EventFullNameType = `${string}/${string}` - -/** - * 添加事件的监听器 - * @param eventFullName 事件名称 - * @param type 事件类型 always长期 | once一次性 - * @param listener 事件监听器 - */ -const registerEventListener = (eventFullName: EventFullNameType, type: 'always' | 'once', listener: NTEventListenerHandle) => { - const dispatcher = useNTDispatcher() - if (type === 'once') { - // 一次性监听 - dispatcher.once(eventFullName, listener); - } - else if (type === 'always') { - // 持续性监听 - dispatcher.on(eventFullName, listener) - } - return { - /** - * 移除监听器 - * @returns - */ - remove: () => dispatcher.removeListener(eventFullName, listener) - } -} - -/** - * 移除事件的监听器 - * - * @param eventFullName 事件名称 - * @param type 事件类型 always长期 | once一次性 - * @param listener 事件监听器 - */ -const removeEventListener = (eventFullName: EventFullNameType, listener: NTEventListenerHandle) => { - const dispatcher = useNTDispatcher() - dispatcher.removeListener(eventFullName, listener) -} - -export const useStore = () => { - return { - - /** - * 注册动作处理函数 - * @param name 动作名称 - * @param handle 动作处理函数 - */ - registerActionHandle, - /** - * 获取动作处理函数 - * @param action 动作名称 - * @returns 处理函数 - */ - getActionHandle, - - /** - * 添加事件的监听器 - * @param eventFullName 事件名称 - * @param type 事件类型 always长期 | once一次性 - * @param listener 事件监听器 - */ - registerEventListener, - /** - * 移除事件的监听器 - * - * @param eventFullName 事件名称 - * @param type 事件类型 always长期 | once一次性 - * @param listener 事件监听器 - */ - removeEventListener, - } -} \ No newline at end of file diff --git a/src/test/testaction.ts b/src/test/testaction.ts index e7515d5..0d7a95a 100644 --- a/src/test/testaction.ts +++ b/src/test/testaction.ts @@ -1,471 +1,34 @@ -import path, { resolve } from "path" import { useLogger } from "../common/log" -import { NTSendForwardMessage, NTSendMessage } from "../ntqq/message/message" -import { BotActionResponse } from "../onebot/actions/interfaces" -import { useStore } from "../store/store" +import { registerActionHandler } from "../store/action-registry" import { sendMessageToGroup } from "../transfer/message/group" -import fs, { readFileSync } from 'fs' -import { Script } from 'vm' -import { randomUUID } from "crypto" -import vm from 'vm' -import v8 from 'v8' -import events from 'events' +import { BotActionParams } from "../onebot/contracts/actions" +import { BotMessage } from "../onebot/contracts/message" - -const { registerActionHandle } = useStore() const log = useLogger('Test') -const testSendMsg = async (p: any): Promise => { - log.info('testSendMsg') - - const result = await sendCustomPkg(p.pkg.cmd, CommunicationPkg.encode({ - uint32Command: 0xed3, - uint32ServiceType: 1, - bytesBodybuffer: PaiYiPaiReq.encode({ - uint64ToUin: p.uin, - uint64GroupCode: p.groupCode, - uint32NudgeType: 0 - }).finish(), - trpcTransInfo: [], - }).finish()) - return result +interface TestSendMessagePayload extends BotActionParams { + groupId: `${number}` + message: BotMessage.SendElement[] } -class ScriptHook extends Script{ - private id: string - private script: Script - private script1: Script - constructor(code: any, options?: any) { - super(code, options) - // if (code) { - // code = `undefined;` + code - // } - this.script = new Script(code, options) - this.script1 = new Script(` - // const keys = Object.keys(global) - // console.log('keys of global', keys) - // setInterval(() => { - // const keys = Object.keys(global.global) - // console.log('keys of global', keys) - // for (const k of keys) { - // console.log('key:', k, 'type:', typeof global[k]) - // } - // for (const k of keys) { - // console.log('key:', k, '---->', global[k]) - - // } - // }, 1000) - (() => { - // const rep = global.require - let keys = [ - // 'global', 'queueMicrotask', - // 'clearImmediate', 'setImmediate', - // 'structuredClone', 'clearInterval', - // 'clearTimeout', 'setInterval', - // 'setTimeout', 'atob', - // 'btoa', 'performance', - // 'fetch', - 'TIMES', - 'launcher', 'loginWin', - 'loginWindowPid', 'TIMES_LABEL', - 'isFirstWinFromPool', '_', - 'isAppQuiting', 'multiInstancePort' - ]; - for (const k of keys) { - if (global[k]) continue - console.log('\\n') - console.log('add:', k) - global[k] = {} - let v1; - Object.defineProperty(global, k, { - get() { - console.log('get:', k, v1) - return v1 - }, - set(v) { - console.log('set', k, '=', v) - v1 = v - } - }) - } - })(); - `) - if (options) { - const r = this.script1.runInThisContext() - // log.info('result:', r) - } - - this.id = randomUUID() - log.info('ScriptHook/constructor', this.id, 'code:', code, 'options:', options) - log.info('length:', code?.length, options?.cachedData?.length) - if (options) { - log.info('cachedData:', options.cachedData?.toString()) - fs.writeFileSync(`D:\\GitHub\\Yui\\tmp\\start.code`, options.cachedData) - } - } - runInContext(obj: any, options?: any) { - // const ret = super.runInContext(obj, options) - const ret = this.script.runInContext(obj, options) - log.info('ScriptHook/runInContext', this.id, obj, options, ret) - return ret - } - runInNewContext(...args: any) { - // const ret = super.runInNewContext(...args) - const ret = this.script.runInNewContext(...args) - log.info('ScriptHook/runInNewContext', this.id, ...args, ret) - return ret - } - runInThisContext(options: any) { - log.info('runInThisContext', this.id, 'options:', options); - // const ret = super.runInContext({}, options) - // const ret = super.runInThisContext(options) - const ret = this.script.runInThisContext(options) - log.info('ScriptHook/runInThisContext', this.id, options) - log.info('ScriptHook/runInThisContext ret1:', this.id, ret) - - // log.info('length:', ret.length) - // log.info('ScriptHook/runInThisContext ret2:', ret()) - return function(...args: any[]) { - log.info('execute function:', ...args) - log.info('arg count:', args.length) - for (let i=0; i < args.length; i++) { - log.info(`arg ${i}:`, args[i]) - } - const r = ret(...args) - log.info('execute result:', r) - return r - } - } - createCachedData() { - // const ret = super.createCachedData() - const ret = this.script.createCachedData() - log.info('ScriptHook/createCachedData', this.id, ret) - return ret - } +const sendTestMessage = async ( + payload: TestSendMessagePayload, +): Promise => { + log.info('run test action') + return String(await sendMessageToGroup(payload.groupId, payload.message)) } -class CustomEventEmitter extends events.EventEmitter -{ - constructor(options?: any){ - super(options) - log.info('CustomEventEmitter', options) - } - // on(eventName: string | symbol, listener: (...args: any[]) => void): this { - // log.info('CustomEventEmitter/on', eventName) - // // super.on(eventName, listener) - // return this - // } - // once(eventName: string | symbol, listener: (...args: any[]) => void): this { - // log.info('CustomEventEmitter/once', eventName) - // super.once(eventName, listener) - // return this - // } - // off(eventName: string | symbol, listener: (...args: any[]) => void): this { - // log.info('CustomEventEmitter/off', eventName) - // super.once(eventName, listener) - // return this - // } - // listenerCount(eventName: string | symbol, listener?: Function | undefined): number { - // const ret = super.listenerCount(eventName, listener) - // log.info('CustomEventEmitter/listenerCount', eventName) - // return ret - // return 1 - // } - // addListener(eventName: string | symbol, listener: (...args: any[]) => void): this { - // log.info('CustomEventEmitter/addListener', eventName) - // return this - // } - // removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this { - // log.info('CustomEventEmitter/removeListener', eventName) - // return this - // } - // setMaxListeners(n: number): this { - // log.info('CustomEventEmitter/removeListener', n) - // return this - // } +export const isDebugTestActionEnabled = () => { + return process.env.YUI_ENABLE_TEST_ACTION === '1' } -const nodeModule = async () => { - const open = (process as any).dlopen - // 拦截native加载方法 - ;(process as any).dlopen = function(m: any, file: string){ - log.info('process.dlopen', m, file) - // 调用默认native加载方法 - const ret = open(m, file) - log.info('dlopen result:', ret, m) - // 修改加载后的native方法 - if (file.includes('wrapper')){ - const _exports = m.exports - for (const serviceName in _exports) { - const service = _exports[serviceName] - - const ori = service - const temp = function (...args: any) { - log.info(`wrapper/${serviceName}/constructor`, ...args) - return new ori(...args) - } - temp.prototype.constructor = Object.create(ori.prototype) - temp.prototype.constructor = temp - Object.setPrototypeOf(temp, ori); - _exports[serviceName] = temp - - const funcs = service.prototype - for (const funcName in funcs) { - const func = funcs[funcName] - funcs[funcName] = function(...args: any) { - let r = func.apply(this, args) - if (r instanceof Promise) { - const uuid = randomUUID() - log.info(`wrapper/${serviceName}/${funcName}`, uuid) - return r.then((res) => { - log.info(`wrapper/${serviceName}/${funcName}/${uuid}`, '\narguments:', ...args, '\nJSON arguments:', JSON.stringify(args), '\n----\nresponse:\n', res) - return Promise.resolve(res) - }) - } - else { - log.info(`wrapper/${serviceName}/${funcName}`, '\narguments:', ...args, '\nJSON arguments:', JSON.stringify(args), '\n----\nresponse:\n', r) - } - return r - } - } - } - } - return ret - } +export const registerDebugTestAction = () => { + registerActionHandler('test', sendTestMessage) } -const genMarkdown = () => { - const _exports = require('../wrapper.node') - let markdown = '' - // 遍历类 - for (const serviceName in _exports) { - log.info('serviceName:', serviceName) - const service = _exports[serviceName] - const obj = service.get?.() - if (obj) - log.info(Object.keys(obj)) - log.info('service:', service.prototype) - const funcs = service.prototype - markdown += `## ${serviceName}\n\n` - markdown += '| 方法名 | 描述 |\n' - markdown += '|-------|-----|\n' - // 遍历方法 - for (const funcName in funcs) { - markdown += `| ${funcName} | |\n` - } - markdown += '\n' - } - // log.info('markdown:', markdown) - fs.writeFileSync(path.resolve(__dirname, '../../../../docs/function.md'), markdown) - - // process.exit(1) -} -const vmSrcipt = () => { - global.require = require - const script = new vm.Script(`const fs = require('fs')`) - script.runInThisContext() - -} - -import async_hooks from 'node:async_hooks'; -import { stdout } from 'node:process'; -import { useNTCore } from "../ntqq/core/core" -import { PaiYiPaiReq, CommunicationPkg } from "../ntqq/protobuf/communication" -import { sendCustomPkg } from "../native/native" -const hookAsync = () => { - const { fd } = stdout; - - let indent = 0; - async_hooks.createHook({ - init(asyncId, type, triggerAsyncId, resource) { - const eid = async_hooks.executionAsyncId(); - const indentStr = ' '.repeat(indent); - fs.writeSync( - fd, - `${indentStr}${type}(${asyncId}):` + - ` trigger: ${triggerAsyncId} execution: ${eid} ${resource}\n`); - }, - before(asyncId) { - const indentStr = ' '.repeat(indent); - fs.writeSync(fd, `${indentStr}before: ${asyncId}\n`); - indent += 2; - }, - after(asyncId) { - indent -= 2; - const indentStr = ' '.repeat(indent); - fs.writeSync(fd, `${indentStr}after: ${asyncId}\n`); - }, - destroy(asyncId) { - const indentStr = ' '.repeat(indent); - fs.writeSync(fd, `${indentStr}destroy: ${asyncId}\n`); - }, - }).enable(); - -} -const vmTest = () => { - let codeTarget = 1 - const config = JSON.parse(fs.readFileSync(`D:\\GitHub\\Yui\\tmp\\bytecode\\start${codeTarget}.json`).toString()) - const byteCode = fs.readFileSync(`D:\\GitHub\\Yui\\tmp\\bytecode\\start${codeTarget}.jsc`) - log.info('start with:', config.filename) - // bytenode.runBytecode(code) - v8.setFlagsFromString('--no-lazy') - v8.setFlagsFromString('--no-flush-bytecode') - // const s1 = new Script('') - // const cache1 = s1.createCachedData() - // log.info('cache1:', cache1) - // const s2 = new Script('') - // const cache2 = s2.createCachedData() - // log.info('cache2:', cache2) - - const script = new Script(config.code, { - filename: config.filename, - cachedData: byteCode, - lineOffset: 0, - }) - // const platform = process.platform - // Object.defineProperty(process, 'platform', { - // get () { - // log.info('get platform') - // return platform - // } - // }) - log.info(Object.keys(global)) - - const wrapper1 = require('../versions/9.9.7-21357/wrapper.node') - for (const k in wrapper1.NodeIKernelLoginService.prototype) { - wrapper1.NodeIKernelLoginService.prototype[k] = null - } - const keys = [ - // 'global', 'queueMicrotask', - // 'clearImmediate', 'setImmediate', - // 'structuredClone', 'clearInterval', - // 'clearTimeout', 'setInterval', - // 'setTimeout', 'atob', - // 'btoa', 'performance', - // 'fetch', - 'TIMES', - 'launcher', 'loginWin', - 'loginWindowPid', 'TIMES_LABEL', - 'isFirstWinFromPool', '_', - 'isAppQuiting', 'multiInstancePort' - ] - for (const k of keys) { - if ((global as any)[k]) continue; - (global as any)[k] = {} - console.log('register:', k) - let v1: any = null; - Object.defineProperty(global, k, { - get() { - log.info('get', k, v1) - return v1 - }, - set(v) { - log.info('set', k, '=', v) - v1 = v - } - }) - } - try { - const f = script.runInThisContext({ - filename: config.filename, - lineOffset: 0, - columnOffset: 0, - displayErrors: true, - }) - - log.info('runInThisContext ret:', f, 'type:', typeof f) - log.info('keys:', Object.keys(f), f === undefined) - log.info('f:', f) - const a0 = {} - const a1 = function(file: string) { - log.info('require:', file) - if (file.startsWith('./') && file.endsWith('.js')) file = path.resolve(__dirname, `../versions/9.9.7-21357/application/${file}`) - return require(file) - } - const a2 = module - const a3 = config.filename - const a4 = path.dirname(config.filename) - log.info('f():', f(a0, a1, a2, a3, a4)) +export const registerDebugTestActionFromEnv = () => { + if (!isDebugTestActionEnabled()) { + return } - catch(err) { - log.error('error1:', err) - } - // try { - - // const ctx = vm.createContext({ - // 'global': function() {}, - // 'queueMicrotask': function() {}, - // 'clearImmediate': function(){}, - // 'setImmediate': function(){}, - // 'structuredClone': function() {}, - // 'clearInterval': function() {}, - // 'clearTimeout': function() {}, - // 'setInterval': function() {}, - // 'setTimeout': function() {}, - // 'atob': function() {}, - // 'btoa': function(){}, - // 'performance': function(){}, - // 'fetch': function() {}, - // 'TIMES': function(){}, - // 'launcher': function(){}, - // 'loginWin': function(){}, - // 'loginWindowPid': function(){}, - // 'TIMES_LABEL': function(){}, - // 'isFirstWinFromPool': function(){}, - // '_': function(){}, - // 'isAppQuiting': function(){}, - // 'multiInstancePort': function(){} - // }) - // const f = script.runInNewContext(ctx, { - // filename: 'D:\\GitHub\\Yui\\ntqq\\resources\\app\\versions\\9.9.7-21357\\app_launcher\\index.js', - // lineOffset: 0, - // columnOffset: 0, - // displayErrors: true, - // }) - // ;(global as any).f = f - // const t = new Script('console.log(f)') - // const ret = t.runInThisContext() - // log.info('ret:', ret) - - // log.info(ctx) - // log.info('runInThisContext ret:', f, 'type:', typeof f) - // log.info('keys:', Object.keys(f), f === undefined) - // log.info('f:', f) - // log.info('f():', f()) - // } - // catch(err) { - // log.error('error2:', err) - // process.exit(1) - // } + registerDebugTestAction() } -export const test = (m: NodeModule) => { - try { - // log.info(require('os').platform()) - // vmSrcipt() - // nodeModule() - // useExample() - // setTimeout(() => { - // genMarkdown() - // }, 5000) - // setTimeout(() => { - // vmTest() - // }, 30000) - // getExports() - // process.exit(1) - // hookFunction() - // hookAsync() - // const { registerEventListener } = useStore() - // let i = 0 - // registerEventListener('KernelMsgListener/onRecvSysMsg', 'always', (data: number[]) => { - // log.info('sys msg:', data) - // fs.writeFileSync(`tmp/test${i++}.bin`, Buffer.from(data)) - // }) - } - catch(err) { - log.error('error:', err) - process.exit(1) - } - const wrapper = useNTCore() - // wrapper.getWrapperEngine().onSendSSOReply - registerActionHandle('test', testSendMsg) -} \ No newline at end of file diff --git a/src/transfer/login/login.ts b/src/transfer/login/login.ts deleted file mode 100644 index 65757c7..0000000 --- a/src/transfer/login/login.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NTCheckDeviceVerifyQRCodeStatus, NTGetDeviceVerifyQRCode, NTLoginByAccountInfo } from "../../ntqq/login/account" -import { NTLogin } from "../../ntqq/login/interfaces" -import { NTGetLoginQrCode } from "../../ntqq/login/qrcode" -import { BotLogin } from "../../onebot/actions/bot/interfaces" -import { BotActionParams } from "../../onebot/actions/interfaces" -import { useLogger } from "../../common/log" -import { CustomError } from "../../server/error/custom-error" -import { CheckScanStatusResult } from "../../ntqq/types/core/oidb/0xc9e_8" - -const log = useLogger('Login') - -/** - * 使用账户进行登录 - * - * wss -> NTQQ - * - * @param p 账户信息 - * @returns 登录结果 - */ -export const loginByAccount = async (p: BotLogin.AccountLoginData): Promise => { - log.info("req param from client:", JSON.stringify(p)) - if (p.id === undefined || p.id == null || typeof p.id !== 'number') { - throw new CustomError(10003, 'id参数不正确') - } - const ntLogin: NTLogin.AccountLoginRequest = { - loginInfo: { - uin: `${p.id}`, - passwordMd5: p.password, - step: p.step, - newDeviceLoginSig: p.deviceVerifySig || '', - proofWaterSig: p.proofWaterSig, - proofWaterRand: p.proofWaterRand, - proofWaterSid: p.proofWaterSid - } - } - log.info("req to nt:", JSON.stringify(ntLogin)) - try { - const resp = await NTLoginByAccountInfo(ntLogin) - return resp - } - catch(e) { - return e - } - -} - -export const getDeviceVerifyQRCode = async (p: BotLogin.DeviceVerifyQRCodeData): Promise => { - const result = await NTGetDeviceVerifyQRCode(p.uin, p.url) - return result -} - -export const checkDeviceVerifyQRCodeStatus = async (p: BotLogin.DeviceVerifyStatusData): Promise => { - const result = await NTCheckDeviceVerifyQRCodeStatus(p.url) - return result -} - -/** - * 使用二维码登录 - * - * @param p - * @returns 登录的二维码 - */ -export const loginByQrCode = async (p: BotActionParams) => { - log.info('loginByQrCode', p); - const resp = await NTGetLoginQrCode() -} \ No newline at end of file diff --git a/src/transfer/message/convert-receive.ts b/src/transfer/message/convert-receive.ts new file mode 100644 index 0000000..155c864 --- /dev/null +++ b/src/transfer/message/convert-receive.ts @@ -0,0 +1,127 @@ +import { getConfig } from "../../store/config"; +import { NTReceiveMessageType } from "../../ntqq/message/interfaces"; +import { BotMessage } from "../../onebot/contracts/message"; +import { BotMessageData } from "../../onebot/contracts/event"; + +const buildReceiveImageUrl = (peer: PeerInfo, msgId: `${number}`, element: NTReceiveMessageType.NTMessageElementType) => { + const p = element.picElement + if (!p?.originImageUrl) { + return p?.originImageUrl ?? null + } + let url = p.originImageUrl + if (url.startsWith('/gchatpic_new')) { + return `https://gchat.qpic.cn${url}` + } + if (url.startsWith('/download')) { + const cfg = getConfig() + return `http://${cfg.yui.http.host}:${cfg.yui.http.port}/downloadRichMedia?file_model_id=0&down_source_type=0&trigger_type=1&msg_id=${msgId}&chat_type=${peer.chatType}&peer_uid=${peer.peerUid}&element_id=${element.elementId}&thumb_size=0&download_type=2&file_path=${encodeURIComponent(p.sourcePath)}` + } + return url ?? null +} + +export const convertNTMsgElement2BotMsgElement = (peer: PeerInfo, msgId: `${number}`, elems: NTReceiveMessageType.NTMessageElementType[]): BotMessage.ReceiveElement[] => { + const result: BotMessage.ReceiveElement[] = [] + for (const ele of elems) { + switch (ele.elementType) { + case 1: + if (ele.textElement) { + const cur = ele.textElement + if (cur.atType === 2) { + result.push({ + type: 'mention', + data: { + at: { + isAll: false, + uid: cur.atNtUid as `u_${string}`, + uin: parseInt(cur.atTinyId), + name: cur.content, + } + } + }) + continue + } + if (cur.atType === 1) { + result.push({ + type: 'mention', + data: { + at: { + isAll: true, + uid: 'all', + uin: 0, + name: cur.content, + } + } + }) + continue + } + result.push({ + type: 'text', + data: { + text: ele.textElement.content + } + }) + } + break + case 2: + if (ele.picElement) { + const p = ele.picElement + result.push({ + type: 'image', + data: { + pic: { + type: p.picSubType === 0 ? 'simple' : 'emoji', + width: p.picWidth, + height: p.picHeight, + size: parseInt(p.fileSize), + md5: p.md5HexStr, + uuid: p.fileUuid, + url: buildReceiveImageUrl(peer, msgId, ele), + path: p.sourcePath, + } + } + }) + } + break + case 7: + if (ele.replyElement) { + result.push({ + type: 'reply', + data: { + reply: { + srcMsgId: ele.replyElement.sourceMsgIdInRecords, + msgSeq: ele.replyElement.replayMsgSeq, + text: ele.replyElement.sourceMsgText, + senderUin: ele.replyElement.senderUid, + senderUid: ele.replyElement.senderUidStr, + } + } + }) + } + break + default: + break + } + } + return result +} + +export const convertNTMsg2BotMsg = (msg: NTReceiveMessageType.NTMessageItemType): BotMessageData => { + const ret: BotMessageData = { + messageId: msg.msgId, + messageSeq: msg.msgSeq, + groupId: 0, + groupName: '', + senderId: parseInt(msg.senderUin), + senderUid: msg.senderUid, + senderMemberName: msg.sendMemberName, + time: parseInt(msg.msgTime), + elements: convertNTMsgElement2BotMsgElement({ + chatType: msg.chatType, + peerUid: msg.peerUid, + guildId: '' + }, msg.msgId, msg.elements), + records: [], + } + ret.records = msg.records.map(e => convertNTMsg2BotMsg(e)) + return ret +} diff --git a/src/transfer/message/convert-send.ts b/src/transfer/message/convert-send.ts new file mode 100644 index 0000000..1c7e342 --- /dev/null +++ b/src/transfer/message/convert-send.ts @@ -0,0 +1,77 @@ +import { useLogger } from "../../common/log" +import { NTReceiveMessageType, NTSendMessageType } from "../../ntqq/message/interfaces" +import { BotMessage } from "../../onebot/contracts/message" +import { + buildFileSendElement, + buildImageInnerSendElement, + buildImageSendElement, + buildMentionInnerSendElement, + buildMentionSendElement, + buildReplyInnerSendElement, + buildReplySendElement, + buildTextInnerSendElement, + buildTextSendElement, +} from "./send-element-builders" + +const log = useLogger('Convert') + +type SendElementConverter = (msg: BotMessage.SendElement) => Promise | NTSendMessageType.MsgElement | undefined +type InnerSendElementConverter = (msg: BotMessage.SendElement, eleId: number) => Promise | NTReceiveMessageType.NTMessageElementType | undefined + +const collectConvertedElements = async ( + elems: BotMessage.SendElement[], + convert: (msg: BotMessage.SendElement, index: number) => Promise, +): Promise => { + const result: TResult[] = [] + for (const [index, ele] of elems.entries()) { + const converted = await convert(ele, index) + if (converted !== undefined) { + result.push(converted) + } + } + return result +} + +const sendElementConverters: Partial> = { + text: buildTextSendElement, + mention: buildMentionSendElement, + image: async (msg) => { + log.info('element type: image') + return buildImageSendElement(msg) + }, + file: (msg) => { + log.info('element type: file') + return buildFileSendElement(msg) + }, + reply: buildReplySendElement, +} + +const innerSendElementConverters: Partial> = { + text: buildTextInnerSendElement, + mention: buildMentionInnerSendElement, + image: async (msg, eleId) => { + log.info('element type: image') + return buildImageInnerSendElement(msg, eleId) + }, + reply: buildReplyInnerSendElement, +} + +export const convertBotMessage2NTMessage = async (elems: BotMessage.SendElement[]): Promise => { + return collectConvertedElements(elems, (ele) => convertBotMessage2NTMessageSingle(ele)) +} + +export const convertBotMessage2NTMessageSingle = async (msg: BotMessage.SendElement): Promise => { + const converter = sendElementConverters[msg.type] + return converter ? await converter(msg) : undefined +} + +export const convertBotMessage2NTInnerMessage = async (elems: BotMessage.SendElement[]): Promise => { + log.info('convert inner msg') + let eleId = Math.floor(Math.random() * 10e15) + return collectConvertedElements(elems, (ele) => convertBotMessage2NTInnerMessageSingle(ele, eleId++)) +} + +export const convertBotMessage2NTInnerMessageSingle = async (msg: BotMessage.SendElement, eleId: number): Promise => { + const converter = innerSendElementConverters[msg.type] + return converter ? await converter(msg, eleId) : undefined +} diff --git a/src/transfer/message/convert.ts b/src/transfer/message/convert.ts index 3766273..ac40021 100644 --- a/src/transfer/message/convert.ts +++ b/src/transfer/message/convert.ts @@ -1,632 +1,11 @@ -import fs from "fs"; -import { downloadFile, getImageInfo } from "../../common/file"; -import { NTReceiveMessageType } from "../../ntqq/message/interfaces"; -import { NTSendMessageType } from "../../ntqq/message/interfaces"; -import { BotMessage } from "../../onebot/common/message"; -import { useLogger } from "../../common/log"; -import { getRichMediaFilePathForGuild } from "../../ntqq/common/nt-api"; -import { copyFile, getFileType } from "../../ntqq/common/fs-api"; -import { useNTCore } from "../../ntqq/core/core"; -import { useStore } from "../../store/store"; -import { RichMediaUploadResult } from "../../ntqq/types/services/NodeIKernelMsgService"; -import { useConfigStore } from "../../store/config"; -import { BotMessageData } from "../../onebot/event/interfaces"; -import { CustomError } from "../../server/error/custom-error"; -import { ImageInfo } from "../../common/interface/file"; - -const log = useLogger('Convert') - -export const convertNTMsg2BotMsg = (msg: NTReceiveMessageType.NTMessageItemType): BotMessageData => { - const ret: BotMessageData = { - messageId: msg.msgId, - messageSeq: msg.msgSeq, - groupId: 0, - groupName: '', - senderId: parseInt(msg.senderUin), - senderUid: msg.senderUid, - senderMemberName: msg.sendMemberName, - time: parseInt(msg.msgTime), - elements: convertNTMsgElement2BotMsgElement({ - chatType: msg.chatType, - peerUid: msg.peerUid, - guildId: '' - }, msg.msgId, msg.elements), - records: [], - } - ret.records = msg.records.map(e => convertNTMsg2BotMsg(e)) - return ret -} -/** - * NTQQ的消息元素转bot消息元素 - * - * @param elems 来自NTQQ的消息 - * @returns 给bot的消息 - */ -export const convertNTMsgElement2BotMsgElement = (peer: PeerInfo, msgId: `${number}`, elems: NTReceiveMessageType.NTMessageElementType[]): BotMessage.ReceiveElement[] => { - const result: BotMessage.ReceiveElement[] = [] - for (const ele of elems) { - switch (ele.elementType) { - case 1: - // 纯文本 - { - if (ele.textElement) { - const cur = ele.textElement - if (cur.atType === 2) { - // at 特定成员 - const mention: BotMessage.ReceiveElement = { - type: 'mention', - data: { - at: { - isAll: false, - uid: cur.atNtUid as `u_${string}`, - uin: parseInt(cur.atTinyId), - name: cur.content, - } - } - } - result.push(mention) - continue - }else if (cur.atType === 1) { - // at 所有 - const mention: BotMessage.ReceiveElement = { - type: 'mention', - data: { - at: { - isAll: true, - uid: 'all', - uin: 0, - name: cur.content, - } - } - } - result.push(mention) - continue - } - const text: BotMessage.ReceiveElement = { - type: 'text', - data: { - text: ele.textElement.content - } - } - // TODO: 对@的处理 - result.push(text) - } - } - break; - case 2: - // 图片 - { - const p = ele.picElement - if (p) { - let url = p.originImageUrl - if (url) - { - if (url.startsWith('/gchatpic_new')) - { - url = `https://gchat.qpic.cn${url}` - } - else if (url.startsWith('/download')) - { - const { getConfig } = useConfigStore() - const cfg = getConfig() - url = `http://${cfg.yui.http.host}:${cfg.yui.http.port}/downloadRichMedia?file_model_id=0&down_source_type=0&trigger_type=1&msg_id=${msgId}&chat_type=${peer.chatType}&peer_uid=${peer.peerUid}&element_id=${ele.elementId}&thumb_size=0&download_type=2&file_path=${encodeURIComponent(p.sourcePath)}` - } - } - const pic: BotMessage.ReceiveElement = { - type: 'image', - data: { - pic: { - type: p.picSubType === 0 ? 'simple' : 'emoji', - width: p.picWidth, - height: p.picHeight, - size: parseInt(p.fileSize), - md5: p.md5HexStr, - uuid: p.fileUuid, - url: url, - path: p.sourcePath, - } - } - } - result.push(pic) - } - } - break; - case 5: - // 视频 - break; - case 6: - // 表情 - break; - case 7: - // 引用回复 - { - if (ele.replyElement) { - const reply: BotMessage.ReceiveElement = { - type: 'reply', - data: { - reply: { - srcMsgId: ele.replyElement.sourceMsgIdInRecords, - msgSeq: ele.replyElement.replayMsgSeq, - text: ele.replyElement.sourceMsgText, - senderUin: ele.replyElement.senderUid, - senderUid: ele.replyElement.senderUidStr, - } - } - } - result.push(reply) - } - } - break; - case 11: - // 商城表情 - break; - - default: - break; - } - } - return result -} - -/** - * bot消息转NTQQ的消息 - * - * @param elems 来自bot的消息 - * @returns 给NTQQ的消息 - */ -export const convertBotMessage2NTMessage = async (elems: BotMessage.SendElement[]): Promise => { - const result: NTSendMessageType.MsgElement[] = [] - for (const ele of elems) { - const r = await convertBotMessage2NTMessageSingle(ele) - if (r !== undefined) { - result.push(r) - } - } - return result -} - -/** - * bot消息转NTQQ的消息 - * - * @param msg 来自bot的消息 - * @returns 给NTQQ的消息 - */ -export const convertBotMessage2NTMessageSingle = async (msg: BotMessage.SendElement): Promise => { - - switch (msg.type) { - case 'text': - // 纯文本 - { - if (!msg.data.text) break - const text: NTSendMessageType.MsgElement = { - elementType: 1, - elementId: "", - textElement: { - content: msg.data.text, - atType: 0, - atUid: "", - atTinyId: "", - atNtUid: "" - } - } - return text - } - break; - case 'mention': - // At - { - if (!msg.data.at) break - const text: NTSendMessageType.MsgElement = { - elementType: 1, - elementId: "", - textElement: { - content: `@${msg.data.at.isAll ? '全体成员' : msg.data.at.name}`, - atType: msg.data.at.isAll ? 1 : 2, - atUid: `${msg.data.at.isAll ? 'all' : msg.data.at.uid}`, - atTinyId: "", - atNtUid: `${msg.data.at.isAll ? 'all' : msg.data.at.uid}`, - } - } - return text - } - break; - case 'image': - { - log.info('element type: image') - if (!msg.data.pic) break - // 获取图片基本信息 - const src = msg.data.pic - let info: ImageInfo | undefined = undefined - if (src.path == null || !fs.existsSync(src.path)) { - // 文件路径有问题,检查是否有网络地址 - if (!src.url) throw new CustomError(500, `File does not exists! ${src.path}`) - log.info(`开始从网络地址下载图片:${src.url}`) - src.path = await downloadFile(src.url) - } - log.info('获取图片信息') - info = await getImageInfo(src.path) - if (!info) { - log.info('图片信息获取失败') - throw new CustomError(500, 'Failed to get information of image') - // return undefined - } - log.info('image info:', info) - log.info('src path:', src.path) - // const fileType = await getFileType(src.path) - // log.info('file type:', fileType) - // get real storage path - const realPath = getRichMediaFilePathForGuild(info.md5, `${info.md5}.${info.ext}`) - log.info('real path:', realPath) - // copy - const ret = copyFile(src.path, realPath) - // rm temp - if (ret) { - // 删除图片 - // fs.rmSync(src.path) - src.path = realPath - } - if (!info) return undefined - - const pic: NTSendMessageType.MsgElement = { - elementType: 2, - elementId: "", - picElement: { - md5HexStr: info.md5, - picWidth: info.width, - picHeight: info.height, - fileName: `${info.md5}.${info.ext}`, - fileSize: `${info.size}`, - original: true, - picSubType: 0, - sourcePath: src.path, - thumbPath: undefined, - picType: info.ext == 'gif' ? 2000 : 1000, - fileUuid: "", - fileSubId: "", - thumbFileSize: 0, - summary: "" - }, - extBufForUI: '', - } - return pic - } - break; - case 'file': - // 文件 - { - log.info('element type: file') - if (!msg.data.file) break - - const v = msg.data.file - // getFileSize - const stat = fs.statSync(v.path) - const size = stat.size - // getFileType - const fileType = getFileType(v.path) - - // if (fileType?.ext === 'mp4'){ - // // generateThumb --- NodeIKernelMsgService/getFileThumbSavePathForSend(750, true) - // const { getWrapperSession } = useNTCore() - // const msgService = getWrapperSession().getMsgService() - // const thumbPath = msgService.getFileThumbSavePathForSend(750, true) - // // 获取视频大小 - // // NodeIKernelNodeMiscService/getVideoSize -> onVideoSizeChanged - // const miscService = getWrapperSession().getNodeMiscService() - // } - - const pic: NTSendMessageType.MsgElement = { - elementType: 3, - elementId: "", - fileElement: { - fileMd5: '', - fileName: v.name, - filePath: v.path, - fileSize: `${size}`, - picHeight: 0, - picWidth: 0, - picThumbPath: new Map(), - file10MMd5: '', - fileSha: '', - fileSha3: '', - fileUuid: '', - fileSubId: '', - fileBizId: undefined, - thumbFileSize: 750, - folderId: undefined - }, - extBufForUI: '', - } - return pic - } - break; - // case '': - // // 表情 - // break; - case 'reply': - // TODO:引用回复 - { - if (!msg.data.reply) break - const reply: NTSendMessageType.MsgElement = { - elementType: 7, - elementId: "", - replyElement: { - replayMsgId: `${msg.data.reply.msgId}`, - replayMsgSeq: `${msg.data.reply.msgSeq}`, - sourceMsgText: `${msg.data.reply.text}`, - senderUid: `${msg.data.reply.uid}`, - senderUidStr: `${msg.data.reply.uid}`, - replyMsgClientSeq: "", - replyMsgTime: "", - replyMsgRevokeType: 0, - sourceMsgTextElems: [], - sourceMsgIsIncPic: false, - sourceMsgExpired: false, - } - } - return reply - } - break; - // case 'marketFace': - // // 商城表情 - // break; - - default: - break; - } - -} - -/** - * bot消息转NTQQ的消息 - * - * @param elems 来自bot的消息 - * @returns 给NTQQ的消息 - */ -export const convertBotMessage2NTInnerMessage = async (elems: BotMessage.SendElement[]): Promise => { - const result: NTReceiveMessageType.NTMessageElementType[] = [] - log.info('convert inner msg') - let eleId = Math.floor(Math.random() * 10e15) - for (const ele of elems) { - const r = await convertBotMessage2NTInnerMessageSingle(ele, eleId++) - if (r !== undefined) { - result.push(r) - } - } - return result -} - -const uploadFile = (peerInfo: any, fileInfo: any) => { - return new Promise((resolve, reject) => { - - const { getWrapperSession } = useNTCore() - const session = getWrapperSession() - log.info('onlyUploadFile:', peerInfo, fileInfo) - - const { registerEventListener } = useStore() - let listener: undefined | { remove: () => void } = undefined - listener = registerEventListener('KernelMsgListener/onRichMediaUploadComplete', 'always', (info: RichMediaUploadResult) => { - // 同时上传,可能会识别错误,需要判定一下 - log.info('upload result:', info) - if (info.fileModelId === fileInfo.fileModelId) - { - listener?.remove() - resolve(true) - } - }) - session.getRichMediaService().onlyUploadFile(peerInfo, [fileInfo]) - - }) -} -/** - * bot消息转NTQQ的消息 - * - * @param msg 来自bot的消息 - * @returns 给NTQQ的消息 - */ -export const convertBotMessage2NTInnerMessageSingle = async (msg: BotMessage.SendElement, eleId: number): Promise => { - - const result: NTReceiveMessageType.NTMessageElementType = { - elementType: 1, - elementId: `${eleId}`, - extBufForUI: "0x" - } - switch (msg.type) { - case 'text': - // 纯文本 - { - if (!msg.data.text) break - result.textElement = { - content: msg.data.text, - atType: 0, - atUid: "0", - atTinyId: "0", - atNtUid: "", - subElementType: 0, - atChannelId: '0', - linkInfo: null, - atRoleId: '0', - atRoleColor: 0, - atRoleName: '', - needNotify: 0, - } - } - break; - case 'mention': - // At - { - if (!msg.data.at) break - result.elementType = 1 - result.textElement = { - content: `@${msg.data.at.isAll ? '全体成员' : msg.data.at.name}`, - atType: msg.data.at.isAll ? 1 : 2, - atUid: msg.data.at.isAll ? 'all' : `${msg.data.at.uin}`, - atTinyId: "", - atNtUid: `${msg.data.at.isAll ? 'all' : msg.data.at.uid}`, - subElementType: 0, - atChannelId: '0', - linkInfo: null, - atRoleId: '0', - atRoleColor: 0, - atRoleName: '', - needNotify: 0, - } - } - break; - case 'image': - { - log.info('element type: image') - if (!msg.data.pic) break - // 获取图片基本信息 - const src = msg.data.pic - let info = src.md5 ? { - width: src.width, - height: src.height, - md5: src.md5, - size: src.size, - } : undefined - if (src.path == null || !fs.existsSync(src.path)) { - // 文件路径有问题,检查是否有网络地址 - if (!src.url) throw new CustomError(500, `File does not exists! ${src.path}`) - log.info(`开始从网络地址下载图片:${src.url}`) - src.path = await downloadFile(src.url) - } - if (src.url){ - log.info('url:', src.url) - if (!src.url.startsWith('https://gchat.qpic.cn')) { - src.url = `/external-download?url=${encodeURIComponent(src.url)}` - } - else - { - src.url = src.url.replace('https://gchat.qpic.cn', '') - } - log.info('url:', src.url) - } - log.info('获取图片信息') - info = await getImageInfo(src.path) - if (!info) { - log.info('图片信息获取失败') - throw new CustomError(500, 'Failed to get information of image') - // return undefined - } - log.info('src path:', src.path) - // const fileType = await getFileType(src.path) - // log.info('file type:', fileType) - // get real storage path - const realPath = getRichMediaFilePathForGuild(info.md5, `${info.md5}.jpg`) - log.info('real path:', realPath) - // copy - const ret = copyFile(src.path, realPath) - // rm temp - if (ret) { - // 删除图片 - // fs.rmSync(src.path) - src.path = realPath - } - // const peerInfo: any = { - // chatType: 2, - // peerUid: '933286835', - // guildId: '' - // } - // const fileInfo = { - // fileName: `{${info.md5}}.jpg`, - // filePath: src.path, - // fileModelId: `${Math.floor(Math.random() * 10e9)}` as `${number}` - // } - // await uploadFile(peerInfo, fileInfo) - if (!info) return undefined - result.elementType = 2 - result.picElement = { - md5HexStr: info.md5, - picWidth: info.width, - picHeight: info.height, - fileName: `{${info.md5}}.jpg`, - fileSize: `${info.size}`, - original: true, - picSubType: 0, - sourcePath: src.path, - thumbPath: {}, - picType: 1001, - fileUuid: `${Math.floor(Math.random() * 10e10)}`, - fileSubId: "", - thumbFileSize: 0, - summary: "", - emojiFrom: null, - emojiWebUrl: null, - emojiAd:{ - url: '', - desc: '', - }, - emojiMall:{ - packageId: 0, - emojiId: 0, - }, - emojiZplan:{ - actionId: 0, - actionName: '', - actionType: 0, - playerNumber: 0, - peerUid: '1', - bytesReserveInfo: '' - }, - originImageMd5: info.md5, - - /** - * 图片网络地址 - * - * 没有host, https://gchat.qpic.cn/ - * - * /gchatpic_new/发送者QQ/群号-uuid-MD5/0 - */ - originImageUrl: src.url ?? '', - import_rich_media_context: null, - isFlashPic: false, - transferStatus: 1, - progress: 100, - invalidState: 0, - fileBizId: null, - downloadIndex: null, - } - } - break; - case 'video': - // 视频 - break; - // case '': - // // 表情 - // break; - case 'reply': - // TODO:引用回复 - { - if (!msg.data.reply) break - result.elementType = 7 - result.replyElement = { - replayMsgId: `${msg.data.reply.msgId}`, - replayMsgSeq: `${msg.data.reply.msgSeq}`, - sourceMsgText: `${msg.data.reply.text}`, - senderUid: `${msg.data.reply.uin}`, - senderUidStr: `${msg.data.reply.uid}`, - replyMsgClientSeq: "0", - replyMsgTime: `${Date.now()}`, - replyMsgRevokeType: 0, - sourceMsgTextElems: [], - sourceMsgIsIncPic: false, - sourceMsgExpired: false, - replayMsgRootSeq: '0', - replayMsgRootMsgId: '1', - replayMsgRootCommentCnt: '0', - sourceMsgIdInRecords: '1', - anonymousNickName: null, - originalMsgState: null, - } - } - break; - // case 'marketFace': - // // 商城表情 - // break; - - default: - return undefined - break; - } - return result -} \ No newline at end of file +export { + convertBotMessage2NTInnerMessage, + convertBotMessage2NTInnerMessageSingle, + convertBotMessage2NTMessage, + convertBotMessage2NTMessageSingle, +} from "./convert-send" + +export { + convertNTMsg2BotMsg, + convertNTMsgElement2BotMsgElement, +} from "./convert-receive" diff --git a/src/common/forward-converter.ts b/src/transfer/message/forward-converter.ts similarity index 93% rename from src/common/forward-converter.ts rename to src/transfer/message/forward-converter.ts index 5ff7edc..df69f22 100644 --- a/src/common/forward-converter.ts +++ b/src/transfer/message/forward-converter.ts @@ -1,19 +1,18 @@ import { statSync } from "fs"; -import { sendCustomPkgV2 } from "../native/native"; -import { CommunicationPkg } from "../ntqq/protobuf/communication"; -import { Elem } from "../ntqq/protobuf/im_msg_body"; -import { Msg } from "../ntqq/protobuf/nt_msg_common"; -import { IPAddressInfo, PicHighwayUploadExtendInfo, ReqUploadGroupPicPrepare, RspBodyField6, RspUploadGroupPicPrepare } from "../ntqq/protobuf/pic_upload"; -import { BotMessageSendElements } from "../onebot/common/message"; -import type { BotMessage } from "../onebot/common/message"; -import { useLogger } from "./log"; -import { calculateFileMd5, calculateFileSha1, getHttpTicket, int2IPNet } from "./utils"; -import { HighwayUpload } from "../ntqq/core/highway/highway"; -import { downloadFile, getImageInfo } from "./file"; -import { useNTUserStore } from "../ntqq/store/user"; -import { PicHighwayUpload } from "../ntqq/core/highway/pic-highway"; -import { getImageSizeFromPath } from "../ntqq/common/fs-api"; import { randomBytes } from "crypto"; +import { downloadFile, getImageInfo } from "../../common/file"; +import { useLogger } from "../../common/log"; +import { calculateFileMd5, calculateFileSha1, int2IPNet } from "../../common/utils"; +import { sendCustomPkgV2 } from "../../native/native"; +import { getCurrentNTUserInfo } from "../../ntqq/core/runtime-store"; +import { PicHighwayUpload } from "../../ntqq/core/highway/pic-highway"; +import { getHttpTicket } from "../../ntqq/message/http-ticket"; +import { CommunicationPkg } from "../../ntqq/protobuf/communication"; +import { Elem } from "../../ntqq/protobuf/im_msg_body"; +import { Msg } from "../../ntqq/protobuf/nt_msg_common"; +import { IPAddressInfo, ReqUploadGroupPicPrepare, RspBodyField6, RspUploadGroupPicPrepare } from "../../ntqq/protobuf/pic_upload"; +import { BotMessageSendElements } from "../../onebot/contracts/message"; +import type { BotMessage } from "../../onebot/contracts/message"; const MAX_FORWARD_PREVIEW = 4; const HEADER_TITLE = "群聊的聊天记录"; @@ -85,9 +84,11 @@ export class ForwardConverter { this.forwardData = forwardData; } async uploadImage() { - const { getUserInfo } = useNTUserStore() - const user = getUserInfo() + const user = getCurrentNTUserInfo() const log = useLogger('ForwardConverter'); + if (!user) { + throw new Error('Failed to get current user info'); + } let picElements = this.forwardData.items.flatMap(item => item.elements.filter(e => e.type === 'image' && e.data.pic).map(e => e.data.pic!)); if (picElements.length === 0) { log.info('No images to upload'); @@ -348,4 +349,4 @@ export class ForwardConverter { return ` ${HEADER_TITLE} ${titleNodes}
查看转发消息
`; } -} \ No newline at end of file +} diff --git a/src/transfer/message/friend.ts b/src/transfer/message/friend.ts index e865cfc..8f2e960 100644 --- a/src/transfer/message/friend.ts +++ b/src/transfer/message/friend.ts @@ -1,22 +1,14 @@ -import { NTSendMessage } from "../../ntqq/message/message"; -import { NTSendMessageType } from "../../ntqq/message/interfaces"; -import { BotMessage } from "../../onebot/common/message"; -import { useLogger } from "../../common/log"; -import { convertBotMessage2NTMessage } from "./convert"; +import { BotMessage } from "../../onebot/contracts/message" +import { useLogger } from "../../common/log" +import { + preparePrivateSendMessageRequest, + sendPreparedMessage, +} from "./send-message-flow" const log = useLogger('Friend') export const sendMessageToFriend = async (targetId: `u_${string}`, msg: BotMessage.SendElement[]) => { log.info('sendMessageToFriend', JSON.stringify(msg)) - const elements: NTSendMessageType.MsgElement[] = await convertBotMessage2NTMessage(msg) - return await NTSendMessage({ - msgId: "0", - peer: { - chatType: 1, - peerUid: targetId, - guildId: "" - }, - msgElements: elements, - msgAttributeInfos: new Map() - }) -} \ No newline at end of file + const request = await preparePrivateSendMessageRequest(targetId, msg) + return sendPreparedMessage(request) +} diff --git a/src/transfer/message/group.ts b/src/transfer/message/group.ts index 6f6f897..1ee3e12 100644 --- a/src/transfer/message/group.ts +++ b/src/transfer/message/group.ts @@ -1,10 +1,9 @@ -import { NTSendForwardMessage, NTSendMessage } from "../../ntqq/message/message" -import { NTSendMessageType } from "../../ntqq/message/interfaces" import { useLogger } from "../../common/log" -import { convertBotMessage2NTInnerMessage, convertBotMessage2NTMessage } from "./convert" -import { BotMessage, BotMessageSendElements } from "../../onebot/common/message" -import { getBotAccount } from "../../onebot/common/user" -import { CustomError } from "../../server/error/custom-error" +import { BotMessage } from "../../onebot/contracts/message" +import { + prepareGroupSendMessageRequest, + sendPreparedMessage, +} from "./send-message-flow" const log = useLogger('Group') @@ -17,18 +16,6 @@ const log = useLogger('Group') */ export const sendMessageToGroup = async (targetId: `${number}`, msg: BotMessage.SendElement[]) => { log.info(`sendMessage to ${targetId} with:`, JSON.stringify(msg)) - const elements: NTSendMessageType.MsgElement[] = await convertBotMessage2NTMessage(msg) - - if (elements.length == 0) throw new CustomError(500, 'Try to send msg, but size of elements is zero!') - - return await NTSendMessage({ - "msgId": "0", - "peer": { - "chatType": 2, - "peerUid": `${targetId}`, - "guildId": "" - }, - "msgElements": elements, - "msgAttributeInfos": new Map() - }) -} \ No newline at end of file + const request = await prepareGroupSendMessageRequest(targetId, msg) + return sendPreparedMessage(request) +} diff --git a/src/transfer/message/image-resource-preparer.test.ts b/src/transfer/message/image-resource-preparer.test.ts new file mode 100644 index 0000000..e2e636c --- /dev/null +++ b/src/transfer/message/image-resource-preparer.test.ts @@ -0,0 +1,50 @@ +import { afterEach, describe, expect, it, vi } from "vitest" + +const fsMocks = vi.hoisted(() => ({ + existsSync: vi.fn(), +})) + +vi.mock("fs", () => ({ + existsSync: fsMocks.existsSync, +})) + +vi.mock("../../common/file", () => ({ + downloadFile: vi.fn(), + getImageInfo: vi.fn(), +})) + +vi.mock("../../ntqq/common/nt-api", () => ({ + getRichMediaFilePathForGuild: vi.fn(), +})) + +vi.mock("../../ntqq/common/fs-api", () => ({ + copyFile: vi.fn(), +})) + +import { CustomError } from "../../common/error/custom-error" +import { prepareSendImageResource } from "./image-resource-preparer" + +describe("prepareSendImageResource", () => { + afterEach(() => { + fsMocks.existsSync.mockReset() + vi.restoreAllMocks() + }) + + it("throws when neither a local file nor a url is available", async () => { + fsMocks.existsSync.mockReturnValue(false) + + const task = prepareSendImageResource( + { + type: "simple", + width: 0, + height: 0, + size: 0, + path: "/tmp/missing-image.jpg", + }, + () => "ignored.jpg", + ) + + await expect(task).rejects.toBeInstanceOf(CustomError) + await expect(task).rejects.toThrow("File does not exists! /tmp/missing-image.jpg") + }) +}) diff --git a/src/transfer/message/image-resource-preparer.ts b/src/transfer/message/image-resource-preparer.ts new file mode 100644 index 0000000..55fa695 --- /dev/null +++ b/src/transfer/message/image-resource-preparer.ts @@ -0,0 +1,58 @@ +import * as fs from "fs" +import { downloadFile, getImageInfo } from "../../common/file" +import { ImageInfo } from "../../common/interface/file" +import { getRichMediaFilePathForGuild } from "../../ntqq/common/nt-api" +import { copyFile } from "../../ntqq/common/fs-api" +import { BotMessage } from "../../onebot/contracts/message" +import { CustomError } from "../../common/error/custom-error" + +type SendPicElement = NonNullable + +interface PrepareSendImageResourceOptions { + url?: string +} + +const resolveSendImageLocalPath = async ( + src: SendPicElement, + resolvedUrl?: string, +) => { + if (src.path && fs.existsSync(src.path)) { + return src.path + } + if (!resolvedUrl) { + throw new CustomError(500, `File does not exists! ${src.path}`) + } + return downloadFile(resolvedUrl) +} + +const resolvePreparedSendImagePath = ( + sourcePath: string, + info: ImageInfo, + fileNameBuilder: (info: ImageInfo) => string, +) => { + const realPath = getRichMediaFilePathForGuild(info.md5, fileNameBuilder(info)) + const copied = copyFile(sourcePath, realPath) + return copied ? realPath : sourcePath +} + +export const prepareSendImageResource = async ( + src: SendPicElement, + fileNameBuilder: (info: ImageInfo) => string, + options: PrepareSendImageResourceOptions = {}, +) => { + const sourceUrl = options.url ?? src.url + const localPath = await resolveSendImageLocalPath(src, sourceUrl) + + const info = await getImageInfo(localPath) + if (!info) { + throw new CustomError(500, 'Failed to get information of image') + } + + const sourcePath = resolvePreparedSendImagePath(localPath, info, fileNameBuilder) + + return { + info, + sourcePath, + sourceUrl, + } +} diff --git a/src/transfer/message/image-resource-utils.ts b/src/transfer/message/image-resource-utils.ts new file mode 100644 index 0000000..d3f7599 --- /dev/null +++ b/src/transfer/message/image-resource-utils.ts @@ -0,0 +1,16 @@ +import { ImageInfo } from "../../common/interface/file" + +export const normalizeUploadImageUrl = (url?: string) => { + if (!url) { + return url + } + if (!url.startsWith('https://gchat.qpic.cn')) { + return `/external-download?url=${encodeURIComponent(url)}` + } + return url.replace('https://gchat.qpic.cn', '') +} + +export const buildGuildImageFileName = (info: ImageInfo) => `${info.md5}.${info.ext}` + +export const buildGuildInnerImageFileName = (info: ImageInfo) => `${info.md5}.jpg` + diff --git a/src/transfer/message/send-basic-element-builders.test.ts b/src/transfer/message/send-basic-element-builders.test.ts new file mode 100644 index 0000000..2fc3be4 --- /dev/null +++ b/src/transfer/message/send-basic-element-builders.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from "vitest" + +vi.mock("../../ntqq/common/fs-api", () => ({ + getFileType: vi.fn(), +})) + +import { buildTextSendElement } from "./send-basic-element-builders" + +describe("buildTextSendElement", () => { + it("builds a text message element when text exists", () => { + const result = buildTextSendElement({ + type: "text", + data: { + text: "hello", + }, + }) + + expect(result).toEqual({ + elementType: 1, + elementId: "", + textElement: { + content: "hello", + atType: 0, + atUid: "", + atTinyId: "", + atNtUid: "", + }, + }) + }) +}) diff --git a/src/transfer/message/send-basic-element-builders.ts b/src/transfer/message/send-basic-element-builders.ts new file mode 100644 index 0000000..32245a4 --- /dev/null +++ b/src/transfer/message/send-basic-element-builders.ts @@ -0,0 +1,189 @@ +import * as fs from "fs" +import { NTReceiveMessageType, NTSendMessageType } from "../../ntqq/message/interfaces" +import { getFileType } from "../../ntqq/common/fs-api" +import { BotMessage } from "../../onebot/contracts/message" + +export const buildTextSendElement = ( + msg: BotMessage.SendElement, +): NTSendMessageType.MsgElement | undefined => { + if (!msg.data.text) { + return undefined + } + return { + elementType: 1, + elementId: "", + textElement: { + content: msg.data.text, + atType: 0, + atUid: "", + atTinyId: "", + atNtUid: "", + }, + } +} + +export const buildMentionSendElement = ( + msg: BotMessage.SendElement, +): NTSendMessageType.MsgElement | undefined => { + if (!msg.data.at) { + return undefined + } + return { + elementType: 1, + elementId: "", + textElement: { + content: `@${msg.data.at.isAll ? '全体成员' : msg.data.at.name}`, + atType: msg.data.at.isAll ? 1 : 2, + atUid: `${msg.data.at.isAll ? 'all' : msg.data.at.uid}`, + atTinyId: "", + atNtUid: `${msg.data.at.isAll ? 'all' : msg.data.at.uid}`, + }, + } +} + +export const buildReplySendElement = ( + msg: BotMessage.SendElement, +): NTSendMessageType.MsgElement | undefined => { + if (!msg.data.reply) { + return undefined + } + return { + elementType: 7, + elementId: "", + replyElement: { + replayMsgId: `${msg.data.reply.msgId}`, + replayMsgSeq: `${msg.data.reply.msgSeq}`, + sourceMsgText: `${msg.data.reply.text}`, + senderUid: `${msg.data.reply.uid}`, + senderUidStr: `${msg.data.reply.uid}`, + replyMsgClientSeq: "", + replyMsgTime: "", + replyMsgRevokeType: 0, + sourceMsgTextElems: [], + sourceMsgIsIncPic: false, + sourceMsgExpired: false, + }, + } +} + +export const buildFileSendElement = ( + msg: BotMessage.SendElement, +): NTSendMessageType.MsgElement | undefined => { + if (!msg.data.file) { + return undefined + } + const file = msg.data.file + const stat = fs.statSync(file.path) + getFileType(file.path) + return { + elementType: 3, + elementId: "", + fileElement: { + fileMd5: '', + fileName: file.name, + filePath: file.path, + fileSize: `${stat.size}`, + picHeight: 0, + picWidth: 0, + picThumbPath: new Map(), + file10MMd5: '', + fileSha: '', + fileSha3: '', + fileUuid: '', + fileSubId: '', + fileBizId: undefined, + thumbFileSize: 750, + folderId: undefined, + }, + extBufForUI: '', + } +} + +export const buildTextInnerSendElement = ( + msg: BotMessage.SendElement, + eleId: number, +): NTReceiveMessageType.NTMessageElementType | undefined => { + if (!msg.data.text) { + return undefined + } + return { + elementType: 1, + elementId: `${eleId}`, + extBufForUI: "0x", + textElement: { + content: msg.data.text, + atType: 0, + atUid: "0", + atTinyId: "0", + atNtUid: "", + subElementType: 0, + atChannelId: '0', + linkInfo: null, + atRoleId: '0', + atRoleColor: 0, + atRoleName: '', + needNotify: 0, + }, + } +} + +export const buildMentionInnerSendElement = ( + msg: BotMessage.SendElement, + eleId: number, +): NTReceiveMessageType.NTMessageElementType | undefined => { + if (!msg.data.at) { + return undefined + } + return { + elementType: 1, + elementId: `${eleId}`, + extBufForUI: "0x", + textElement: { + content: `@${msg.data.at.isAll ? '全体成员' : msg.data.at.name}`, + atType: msg.data.at.isAll ? 1 : 2, + atUid: msg.data.at.isAll ? 'all' : `${msg.data.at.uin}`, + atTinyId: "", + atNtUid: `${msg.data.at.isAll ? 'all' : msg.data.at.uid}`, + subElementType: 0, + atChannelId: '0', + linkInfo: null, + atRoleId: '0', + atRoleColor: 0, + atRoleName: '', + needNotify: 0, + }, + } +} + +export const buildReplyInnerSendElement = ( + msg: BotMessage.SendElement, + eleId: number, +): NTReceiveMessageType.NTMessageElementType | undefined => { + if (!msg.data.reply) { + return undefined + } + return { + elementType: 7, + elementId: `${eleId}`, + extBufForUI: "0x", + replyElement: { + replayMsgId: `${msg.data.reply.msgId}`, + replayMsgSeq: `${msg.data.reply.msgSeq}`, + sourceMsgText: `${msg.data.reply.text}`, + senderUid: `${msg.data.reply.uin}`, + senderUidStr: `${msg.data.reply.uid}`, + replyMsgClientSeq: "0", + replyMsgTime: `${Date.now()}`, + replyMsgRevokeType: 0, + sourceMsgTextElems: [], + sourceMsgIsIncPic: false, + sourceMsgExpired: false, + replayMsgRootSeq: '0', + replayMsgRootMsgId: '1', + replayMsgRootCommentCnt: '0', + sourceMsgIdInRecords: '1', + anonymousNickName: null, + originalMsgState: null, + }, + } +} diff --git a/src/transfer/message/send-element-builders.ts b/src/transfer/message/send-element-builders.ts new file mode 100644 index 0000000..2b01c96 --- /dev/null +++ b/src/transfer/message/send-element-builders.ts @@ -0,0 +1,13 @@ +export { + buildFileSendElement, + buildMentionInnerSendElement, + buildMentionSendElement, + buildReplyInnerSendElement, + buildReplySendElement, + buildTextInnerSendElement, + buildTextSendElement, +} from "./send-basic-element-builders" +export { + buildImageInnerSendElement, + buildImageSendElement, +} from "./send-image-element-builders" diff --git a/src/transfer/message/send-image-element-builders.ts b/src/transfer/message/send-image-element-builders.ts new file mode 100644 index 0000000..1299a4f --- /dev/null +++ b/src/transfer/message/send-image-element-builders.ts @@ -0,0 +1,110 @@ +import { NTReceiveMessageType, NTSendMessageType } from "../../ntqq/message/interfaces" +import { BotMessage } from "../../onebot/contracts/message" +import { prepareSendImageResource } from "./image-resource-preparer" +import { + buildGuildImageFileName, + buildGuildInnerImageFileName, + normalizeUploadImageUrl, +} from "./image-resource-utils" + +const buildRandomFileUuid = (): `${number}` => { + return `${Math.floor(Math.random() * 10e10)}` +} + +export const buildImageSendElement = async ( + msg: BotMessage.SendElement, +): Promise => { + if (!msg.data.pic) { + return undefined + } + const { info, sourcePath } = await prepareSendImageResource( + msg.data.pic, + buildGuildImageFileName, + ) + return { + elementType: 2, + elementId: "", + picElement: { + md5HexStr: info.md5, + picWidth: info.width, + picHeight: info.height, + fileName: `${info.md5}.${info.ext}`, + fileSize: `${info.size}`, + original: true, + picSubType: 0, + sourcePath, + thumbPath: undefined, + picType: info.ext == 'gif' ? 2000 : 1000, + fileUuid: "", + fileSubId: "", + thumbFileSize: 0, + summary: "", + }, + extBufForUI: '', + } +} + +export const buildImageInnerSendElement = async ( + msg: BotMessage.SendElement, + eleId: number, +): Promise => { + if (!msg.data.pic) { + return undefined + } + const prepared = await prepareSendImageResource( + msg.data.pic, + buildGuildInnerImageFileName, + { + url: normalizeUploadImageUrl(msg.data.pic.url), + }, + ) + const info = prepared.info + return { + elementType: 2, + elementId: `${eleId}`, + extBufForUI: "0x", + picElement: { + md5HexStr: info.md5, + picWidth: info.width, + picHeight: info.height, + fileName: `{${info.md5}}.jpg`, + fileSize: `${info.size}`, + original: true, + picSubType: 0, + sourcePath: prepared.sourcePath, + thumbPath: {}, + picType: 1001, + fileUuid: buildRandomFileUuid(), + fileSubId: "", + thumbFileSize: 0, + summary: "", + emojiFrom: null, + emojiWebUrl: null, + emojiAd: { + url: '', + desc: '', + }, + emojiMall: { + packageId: 0, + emojiId: 0, + }, + emojiZplan: { + actionId: 0, + actionName: '', + actionType: 0, + playerNumber: 0, + peerUid: '1', + bytesReserveInfo: '', + }, + originImageMd5: info.md5, + originImageUrl: prepared.sourceUrl ?? '', + import_rich_media_context: null, + isFlashPic: false, + transferStatus: 1, + progress: 100, + invalidState: 0, + fileBizId: null, + downloadIndex: null, + }, + } +} diff --git a/src/transfer/message/send-message-flow.ts b/src/transfer/message/send-message-flow.ts new file mode 100644 index 0000000..69aabc9 --- /dev/null +++ b/src/transfer/message/send-message-flow.ts @@ -0,0 +1,42 @@ +import { useLogger } from "../../common/log" +import { NTSendMessageType } from "../../ntqq/message/interfaces" +import { NTSendMessage } from "../../ntqq/message/message" +import { BotMessage } from "../../onebot/contracts/message" +import { CustomError } from "../../common/error/custom-error" +import { convertBotMessage2NTMessage } from "./convert" +import { createGroupSendRequest, createPrivateSendRequest } from "./send-request" + +const log = useLogger('Transfer/SendMessageFlow') + +export const prepareSendMessageElements = async ( + message: BotMessage.SendElement[], +) => { + const elements = await convertBotMessage2NTMessage(message) + if (elements.length === 0) { + throw new CustomError(500, 'Try to send msg, but size of elements is zero!') + } + return elements +} + +export const sendPreparedMessage = async ( + request: NTSendMessageType.SendRequest, +) => { + log.info('send prepared message:', request.peer) + return NTSendMessage(request) +} + +export const preparePrivateSendMessageRequest = async ( + peerUid: `u_${string}`, + message: BotMessage.SendElement[], +) => { + const elements = await prepareSendMessageElements(message) + return createPrivateSendRequest(peerUid, elements) +} + +export const prepareGroupSendMessageRequest = async ( + peerUid: `${number}`, + message: BotMessage.SendElement[], +) => { + const elements = await prepareSendMessageElements(message) + return createGroupSendRequest(peerUid, elements) +} diff --git a/src/transfer/message/send-request.ts b/src/transfer/message/send-request.ts new file mode 100644 index 0000000..a3e8976 --- /dev/null +++ b/src/transfer/message/send-request.ts @@ -0,0 +1,32 @@ +import { NTSendMessageType } from "../../ntqq/message/interfaces" + +const createBaseSendRequest = ( + chatType: number, + peerUid: `${number}` | `u_${string}` | '', + msgElements: NTSendMessageType.MsgElement[], +): NTSendMessageType.SendRequest => { + return { + msgId: "0", + peer: { + chatType, + peerUid, + guildId: "", + }, + msgElements, + msgAttributeInfos: new Map(), + } +} + +export const createPrivateSendRequest = ( + peerUid: `u_${string}`, + msgElements: NTSendMessageType.MsgElement[], +): NTSendMessageType.SendRequest => { + return createBaseSendRequest(1, peerUid, msgElements) +} + +export const createGroupSendRequest = ( + peerUid: `${number}`, + msgElements: NTSendMessageType.MsgElement[], +): NTSendMessageType.SendRequest => { + return createBaseSendRequest(2, peerUid, msgElements) +} diff --git a/tools/prepend.js b/tools/prepend.js index 09d5ebe..4cc54bb 100644 --- a/tools/prepend.js +++ b/tools/prepend.js @@ -1,8 +1,3 @@ -const { AsyncLocalStorage } = require('async_hooks'); - -/** @type {import('async_hooks').AsyncLocalStorage} */ -const asyncLocalStorage = new AsyncLocalStorage(); -global.asyncLocalStorage = asyncLocalStorage; ;(() => { const orgi_require = module.require const log = (...a) => { @@ -26,25 +21,12 @@ global.asyncLocalStorage = asyncLocalStorage; } return { /** - * 载入特定模块 - * @param {number} idx 模块索引 + * 载入 wrapper.node + * @param {number} idx 兼容旧调用,已忽略 * @returns */ useWrapper: (idx) => { - if (idx === undefined || idx === null) - { - throw new Error('use wrapper: idx error') - } - let targetPath = wrapperPath.replace('wrapper.node', `wrapper-${idx}.node`) - if (process.env['YUI_ACTION'] === 'ui') - { - targetPath = wrapperPath - } - if (!fs.existsSync(targetPath)) - { - fs.cpSync(wrapperPath, targetPath) - } - return orgi_require.apply(t, [targetPath]) + return orgi_require.apply(t, [wrapperPath]) } } }, @@ -73,4 +55,4 @@ global.asyncLocalStorage = asyncLocalStorage; } return orgi_require.apply(this, args) } -})(); \ No newline at end of file +})(); diff --git a/tools/yui/build.cjs b/tools/yui/build.cjs index d86e626..b28cb51 100644 --- a/tools/yui/build.cjs +++ b/tools/yui/build.cjs @@ -1,11 +1,25 @@ const path = require("path"); -const { spawn } = require("child_process"); -// rollup -c --configPlugin @rollup/plugin-typescript -w -const cmd = process.platform == 'win32' ? 'rollup.CMD' : 'rollup' -spawn(cmd, ['-c', '--configPlugin', '@rollup/plugin-typescript'], { - stdio: 'inherit', +const { spawnSync } = require("child_process"); + +const rootDir = path.resolve(__dirname, "../../"); +const cmd = process.platform === "win32" ? "vite.CMD" : "vite"; +const shell = process.platform === "win32"; + +const runBuild = (target) => { + const result = spawnSync(cmd, ["build", "--config", "vite.config.ts"], { + stdio: "inherit", + cwd: rootDir, env: { - ...process.env, + ...process.env, + YUI_BUILD_TARGET: target, }, - shell: true -}) \ No newline at end of file + shell, + }); + + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +}; + +runBuild("main"); +runBuild("ui"); diff --git a/tools/yui/dev.cjs b/tools/yui/dev.cjs index 2ca67f5..95af4e2 100644 --- a/tools/yui/dev.cjs +++ b/tools/yui/dev.cjs @@ -1,10 +1,55 @@ const path = require("path"); const { spawn } = require("child_process"); -const cmd = process.platform == 'win32' ? 'rollup.CMD' : 'rollup' -spawn(cmd, ['-c', '--configPlugin', '@rollup/plugin-typescript', '-w'], { - stdio: 'inherit', + +const rootDir = path.resolve(__dirname, "../../"); +const cmd = process.platform === "win32" ? "vite.CMD" : "vite"; +const shell = process.platform === "win32"; +const targets = ["main", "ui"]; + +let isShuttingDown = false; + +const children = targets.map((target) => + spawn(cmd, ["build", "--watch", "--config", "vite.config.ts"], { + stdio: "inherit", + cwd: rootDir, env: { - ...process.env, + ...process.env, + YUI_BUILD_TARGET: target, }, - shell: true -}) \ No newline at end of file + shell, + }) +); + +const stopChildren = (signal) => { + for (const child of children) { + if (!child.killed) { + child.kill(signal); + } + } +}; + +const shutdown = (signal, exitCode = 0) => { + if (isShuttingDown) { + return; + } + isShuttingDown = true; + stopChildren(signal); + process.exitCode = exitCode; +}; + +for (const child of children) { + child.on("exit", (code, signal) => { + if (isShuttingDown) { + return; + } + shutdown("SIGTERM", code ?? (signal ? 1 : 0)); + }); +} + +process.on("SIGINT", () => { + shutdown("SIGINT", 130); +}); + +process.on("SIGTERM", () => { + shutdown("SIGTERM", 143); +}); diff --git a/tools/yui/nodestart.cjs b/tools/yui/nodestart.cjs index 1945349..34bfc4d 100644 --- a/tools/yui/nodestart.cjs +++ b/tools/yui/nodestart.cjs @@ -2,6 +2,8 @@ const path = require("path"); const fs = require("fs"); const { spawn } = require("child_process"); +const createNodeArgs = () => ["--enable-source-maps", "./index.js"]; + fs.copyFileSync( path.resolve(__dirname, "../../yui.yaml"), path.resolve(__dirname, "../../program/yui.yaml") @@ -12,7 +14,7 @@ switch (process.platform) { { console.log("nodestart"); // cross-env YUKIHANA_LOG=true ELECTRON_RUN_AS_NODE=1 .\\ntqq\\QQ.exe .\\ntqq\\resources\\app\\app_launcher\\index.js - const pro = spawn("node", ["./index.js"], { + const pro = spawn(process.execPath, createNodeArgs(), { stdio: "inherit", env: { ...process.env, @@ -30,7 +32,7 @@ switch (process.platform) { case "linux": { // cross-env YUKIHANA_LOG=true ELECTRON_RUN_AS_NODE=1 .\\ntqq\\QQ.exe .\\ntqq\\resources\\app\\app_launcher\\index.js - spawn("node", ["./index.js"], { + spawn(process.execPath, createNodeArgs(), { stdio: "inherit", env: { ...process.env, diff --git a/tsconfig.json b/tsconfig.json index 9377187..6298d28 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "include": [ "src/**/*.ts", "tools/**/*.ts", - "rollup.config.ts" + "vite.config.ts", + "vitest.config.ts" ], -} \ No newline at end of file +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..567ca37 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,84 @@ +import fs from "node:fs" +import path from "node:path" +import { builtinModules } from "node:module" +import { fileURLToPath } from "node:url" +import { defineConfig } from "vite" + +type BuildTarget = 'main' | 'ui' + +const rootDir = path.dirname(fileURLToPath(import.meta.url)) + +const buildTargetConfigs = { + main: { + entry: path.resolve(rootDir, "src/index.ts"), + outDir: "program", + entryFileName: "index.js", + banner: fs.readFileSync(path.resolve(rootDir, "tools/prepend.js"), "utf8"), + external: ["ntwrapper", "yui-native"], + }, + ui: { + entry: path.resolve(rootDir, "src/ui.ts"), + outDir: "dist", + entryFileName: "ui.js", + banner: undefined, + external: ["electron"], + }, +} as const satisfies Record + +const nodeBuiltins = [ + ...builtinModules, + ...builtinModules.map((name) => `node:${name}`), +] + +const resolveBuildTarget = (): BuildTarget => { + return process.env["YUI_BUILD_TARGET"] === "ui" ? "ui" : "main" +} + +export default defineConfig(() => { + const target = buildTargetConfigs[resolveBuildTarget()] + const isProduction = process.env["ENVIRONMENT"] === "production" + + return { + resolve: { + conditions: ["node"], + mainFields: ["module", "jsnext:main", "jsnext", "main"], + }, + ssr: { + external: target.external, + noExternal: true, + }, + build: { + copyPublicDir: false, + emptyOutDir: false, + minify: isProduction ? "esbuild" : false, + outDir: target.outDir, + sourcemap: true, + ssr: target.entry, + rollupOptions: { + external: [...nodeBuiltins, ...target.external], + onwarn(warning, handler) { + if (warning.code === "THIS_IS_UNDEFINED") { + return + } + if (warning.code === "CIRCULAR_DEPENDENCY") { + return + } + handler(warning) + }, + output: { + banner: target.banner, + entryFileNames: target.entryFileName, + exports: "auto", + format: "cjs", + inlineDynamicImports: true, + }, + }, + }, + } +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..0f5c22b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + environment: "node", + exclude: [ + "dist/**", + "node_modules/**", + "program/**", + "tmp/**", + ], + include: ["src/**/*.test.ts"], + }, +})