├── .gitignore ├── src ├── bot │ ├── index.ts │ ├── token.ts │ ├── session.ts │ ├── client.ts │ └── event.ts ├── index.ts ├── utils │ ├── index.ts │ ├── logger.ts │ ├── common.ts │ └── request.ts ├── model │ ├── index.ts │ ├── user.ts │ ├── guild.ts │ ├── announce.ts │ ├── schedule.ts │ ├── audio.ts │ ├── permission.ts │ ├── member.ts │ ├── channel.ts │ ├── forum.ts │ └── message.ts └── api │ ├── index.ts │ ├── dms.ts │ ├── gateway.ts │ ├── groups.ts │ ├── users.ts │ ├── channels.ts │ └── guilds.ts ├── tsconfig.json ├── .github └── ISSUE_TEMPLATE │ ├── 2-feature-request.yaml │ └── 1-bug-report.yaml ├── package.json ├── LICENSE ├── README.md └── pnpm-lock.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /src/bot/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@/bot/client'; 2 | export * from '@/bot/event'; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@/bot'; 2 | export * from '@/model'; 3 | export * from '@/utils'; 4 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@/utils/common'; 2 | export * from '@/utils/logger'; 3 | export * from '@/utils/request'; 4 | -------------------------------------------------------------------------------- /src/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './announce'; 2 | export * from './audio'; 3 | export * from './channel'; 4 | export * from './forum'; 5 | export * from './guild'; 6 | export * from './member'; 7 | export * from './message'; 8 | export * from './permission'; 9 | export * from './schedule'; 10 | export * from './user'; 11 | -------------------------------------------------------------------------------- /src/model/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | /** 用户 id */ 3 | id: string; 4 | /** 用户名 */ 5 | username: string; 6 | /** 用户头像地址 */ 7 | avatar: string; 8 | /** 是否是机器人 */ 9 | bot: boolean; 10 | /** 特殊关联应用的 openid,需要特殊申请并配置后才会返回。 */ 11 | union_openid: string; 12 | /** 机器人关联的互联应用的用户信息,与 union_openid 关联的应用是同一个。 */ 13 | union_user_account: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/model/guild.ts: -------------------------------------------------------------------------------- 1 | export interface Guild { 2 | /** 频道 ID */ 3 | id: string; 4 | /** 频道名称 */ 5 | name: string; 6 | /** 频道头像地址 */ 7 | icon: string; 8 | /** 创建人用户 ID */ 9 | owner_id: string; 10 | /** 当前人是否是创建人 */ 11 | owner: boolean; 12 | /** 成员数 */ 13 | member_count: number; 14 | /** 最大成员数 */ 15 | max_members: number; 16 | /** 描述 */ 17 | description: string; 18 | /** 加入时间 */ 19 | joined_at: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/model/announce.ts: -------------------------------------------------------------------------------- 1 | export interface RecommendChannel { 2 | /** 子频道 id */ 3 | channel_id: string; 4 | /** 推荐语 */ 5 | introduce: string; 6 | } 7 | 8 | export interface Announce { 9 | /** 频道 id */ 10 | guild_id: string; 11 | /** 子频道 id */ 12 | channel_id: string; 13 | /** 消息 id */ 14 | message_id: string; 15 | /** 公告类别 0:成员公告 1:欢迎公告,默认成员公告 */ 16 | announces_type: number; 17 | /** 推荐子频道详情列表 */ 18 | recommend_channels: RecommendChannel[]; 19 | } 20 | -------------------------------------------------------------------------------- /src/model/schedule.ts: -------------------------------------------------------------------------------- 1 | import { Member } from '@/model/member'; 2 | 3 | export interface Schedule { 4 | /** 日程 id */ 5 | id: string; 6 | /** 日程名称 */ 7 | name: string; 8 | /** 日程描述 */ 9 | description: string; 10 | /** 日程开始时间戳(ms) */ 11 | start_timestamp: string; 12 | /** 日程结束时间戳(ms) */ 13 | end_timestamp: string; 14 | /** 创建者 */ 15 | creator: Member; 16 | /** 日程开始时跳转到的子频道 id */ 17 | jump_channel_id: string; 18 | /** 日程提醒类型,取值参考RemindType */ 19 | remind_type: string; 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["esnext"], 5 | "module": "commonjs", 6 | "rootDir": "src", 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["src/*"] 10 | }, 11 | "declaration": true, 12 | "outDir": "lib", 13 | "noEmitOnError": true, 14 | "esModuleInterop": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "strict": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { Request } from '@/utils'; 2 | 3 | import dms from '@/api/dms'; 4 | import users from '@/api/users'; 5 | import groups from '@/api/groups'; 6 | import guilds from '@/api/guilds'; 7 | import channels from '@/api/channels'; 8 | import gateway from '@/api/gateway'; 9 | 10 | export function generateApi(request: Request) { 11 | return { 12 | ...dms(request), 13 | ...users(request), 14 | ...groups(request), 15 | ...guilds(request), 16 | ...gateway(request), 17 | ...channels(request), 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature 2 | description: 请求新功能的开发 3 | title: "[FEAT] " 4 | labels: 5 | - enhancement 6 | assignees: 7 | - xueelf 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | 感谢你的问题反馈,为了方便更快的定位问题,建议尽可能多地填写以下表格。(\*^▽^\*) 13 | 14 | 该 issue 将被用于程序中的新功能开发,在这之前请先确认 amesu 是否已实现类似的功能哦。若你想要获取更多使用帮助,可以先查阅 [README](https://github.com/xueelf/amesu#readme) 说明。 15 | - type: textarea 16 | attributes: 17 | label: 功能描述 18 | description: 阐述相关功能点,最好能提供伪代码示例。 19 | - type: textarea 20 | attributes: 21 | label: 此功能的开发将会解决什么问题? 22 | validations: 23 | required: true 24 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import log4js, { Level } from 'log4js'; 2 | 3 | export type Logger = log4js.Logger; 4 | export type LogLevel = 'OFF' | 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE' | 'ALL'; 5 | 6 | const loggerMap: Map<string, Logger> = new Map(); 7 | 8 | export function createLogger(name: string, level: Level | LogLevel): Logger { 9 | const logger = log4js.getLogger(`[${name}]`); 10 | logger.level = level; 11 | 12 | loggerMap.set(name, logger); 13 | return logger; 14 | } 15 | 16 | export function getLogger(name: string): Logger { 17 | if (!loggerMap.has(name)) { 18 | throw new Error('No instance of Logger exists.'); 19 | } 20 | return loggerMap.get(name)!; 21 | } 22 | -------------------------------------------------------------------------------- /src/model/audio.ts: -------------------------------------------------------------------------------- 1 | export enum AudioStatus { 2 | /** 开始播放操作 */ 3 | Start = 0, 4 | /** 暂停播放操作 */ 5 | Pause = 1, 6 | /** 继续播放操作 */ 7 | Resume = 2, 8 | /** 停止播放操作 */ 9 | Stop = 3, 10 | } 11 | 12 | export interface AudioControl { 13 | /** 音频数据的url status为0时传 */ 14 | audio_url: string; 15 | /** 状态文本(比如:简单爱-周杰伦),可选,status为0时传,其他操作不传 */ 16 | text: string; 17 | /** 播放状态,参考 STATUS */ 18 | status: AudioStatus; 19 | } 20 | 21 | export interface AudioAction { 22 | /** 频道id */ 23 | guild_id: string; 24 | /** 子频道id */ 25 | channel_id: string; 26 | /** 音频数据的url status为0时传 */ 27 | audio_url: string; 28 | /** 状态文本(比如:简单爱-周杰伦),可选,status为0时传,其他操作不传 */ 29 | text: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/api/dms.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from '@/model/message'; 2 | import type { Request, Result } from '@/utils'; 3 | import type { SendChannelMessageParams } from '@/api/channels'; 4 | 5 | export default (request: Request) => { 6 | return { 7 | /** 8 | * 用于发送频道私信消息,前提是已经创建了私信会话。 9 | */ 10 | sendDmMessage(guild_id: string, params: SendChannelMessageParams): Promise<Result<Message>> { 11 | return request.post(`/dms/${guild_id}/messages`, params); 12 | }, 13 | 14 | /** 15 | * 撤回频道私信消息。 16 | */ 17 | recallDmMessage( 18 | guild_id: string, 19 | message_id: string, 20 | hidetip: boolean = false, 21 | ): Promise<Result> { 22 | return request.delete(`/dms/${guild_id}/messages/${message_id}?hidetip=${hidetip}`); 23 | }, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/model/permission.ts: -------------------------------------------------------------------------------- 1 | export interface ApiPermission { 2 | /** API 接口名,例如 /guilds/{guild_id}/members/{user_id} */ 3 | path: string; 4 | /** 请求方法,例如 GET */ 5 | method: string; 6 | /** API 接口名称,例如 获取频道信 */ 7 | desc: string; 8 | /** 授权状态,auth_stats 为 1 时已授权 */ 9 | auth_status: number; 10 | } 11 | 12 | export interface ApiPermissionDemandIdentify { 13 | /** API 接口名,例如 /guilds/{guild_id}/members/{user_id} */ 14 | path: string; 15 | /** 请求方法,例如 GET */ 16 | method: string; 17 | } 18 | 19 | export interface ApiPermissionDemand { 20 | /** 申请接口权限的频道 id */ 21 | guild_id: string; 22 | /** 接口权限需求授权链接发送的子频道 id */ 23 | channel_id: string; 24 | /** 权限接口唯一标识 */ 25 | api_identify: ApiPermissionDemandIdentify; 26 | /** 接口权限链接中的接口权限描述信息 */ 27 | title: string; 28 | /** 接口权限链接中的机器人可使用功能的描述信息 */ 29 | desc: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/model/member.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '@/model/user'; 2 | 3 | export interface Member { 4 | /** 用户的频道基础信息,只有成员相关接口中会填充此信息 */ 5 | user: User; 6 | /** 用户的昵称 */ 7 | nick: string; 8 | /** 用户在频道内的身份组 ID, 默认值可参考 DefaultRoles */ 9 | roles: string[]; 10 | /** 用户加入频道的时间 */ 11 | joined_at: number; 12 | } 13 | 14 | export interface MemberWithGuildID { 15 | /** 频道 id */ 16 | guild_id: string; 17 | /** 用户的频道基础信息 */ 18 | user: User; 19 | /** 用户的昵称 */ 20 | nick: string; 21 | /** 用户在频道内的身份 */ 22 | roles: string[]; 23 | /** 用户加入频道的时间 */ 24 | joined_at: number; 25 | } 26 | 27 | export interface Role { 28 | /** 身份组ID */ 29 | id: string; 30 | /** 名称 */ 31 | name: string; 32 | /** ARGB 的 HEX 十六进制颜色值转换后的十进制数值 */ 33 | color: number; 34 | /** 是否在成员列表中单独展示: 0-否, 1-是 */ 35 | hoist: number; 36 | /** 人数 */ 37 | number: number; 38 | /** 成员上限 */ 39 | member_limit: number; 40 | } 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug 2 | description: 反馈在使用过程中遇到的问题 3 | title: "[BUG] <title>" 4 | labels: 5 | - bug 6 | assignees: 7 | - xueelf 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | 感谢你的问题反馈,为了方便更快的定位问题,建议尽可能多地填写以下表格。(\*^▽^\*) 13 | 14 | 该 issue 将被用于查找程序中发现的错误和问题,在这之前,请先确认是 amesu 本身的行为导致而非 API 的问题哦。若你想要获取更多 API 使用帮助,可以先查阅 [官方文档](https://bot.q.qq.com/wiki/) 排错。 15 | - type: input 16 | attributes: 17 | label: Node.js 版本 18 | description: 你可以输入 `node -v` 查询。 19 | - type: input 20 | attributes: 21 | label: Amesu 版本 22 | description: 若不是最新版本,可以尝试升级依赖库后确认问题是否还存在。 23 | - type: textarea 24 | attributes: 25 | label: 问题描述 26 | description: 阐述相关报错的详细步骤,最好能提供代码片段。 27 | validations: 28 | required: true 29 | - type: textarea 30 | attributes: 31 | label: 控制台日志 32 | description: 如果方便的话,你可以将 `log_level` 改为 `DEBUG` 或者 `ALL` 提供完成的日志信息。 33 | -------------------------------------------------------------------------------- /src/api/gateway.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Result } from '@/utils'; 2 | 3 | export interface Gateway { 4 | /** 用于连接 `websocket` 的地址。 */ 5 | url: string; 6 | } 7 | 8 | interface SessionStartLimit { 9 | /** 每 24 小时可创建 Session 数。 */ 10 | total: number; 11 | /** 目前还可以创建的 Session 数。 */ 12 | remaining: number; 13 | /** 重置计数的剩余时间(ms)。 */ 14 | reset_after: number; 15 | /** 每 5s 可以创建的 Session 数。 */ 16 | max_concurrency: number; 17 | } 18 | 19 | export interface GatewayBot { 20 | /** WebSocket 的连接地址。 */ 21 | url: string; 22 | /** 建议的 shard 数。 */ 23 | shards: number; 24 | /** 创建 Session 限制信息。 */ 25 | session_start_limit: SessionStartLimit; 26 | } 27 | 28 | export default (request: Request) => { 29 | return { 30 | /** 31 | * 获取通用 WSS 接入点。 32 | */ 33 | getGateway(): Promise<Result<Gateway>> { 34 | return request.get('/gateway'); 35 | }, 36 | 37 | /** 38 | * 获取带分片 WSS 接入点。 39 | */ 40 | getGatewayBot(): Promise<Result<GatewayBot>> { 41 | return request.get('/gateway/bot'); 42 | }, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amesu", 3 | "version": "2.2.0", 4 | "description": "Node.js SDK for QQ Bot.", 5 | "engines": { 6 | "node": ">=18.0.0" 7 | }, 8 | "main": "lib/index.js", 9 | "typings": "lib/index.d.ts", 10 | "scripts": { 11 | "build": "tsc && tsc-alias", 12 | "build:watch": "tsc && (concurrently \"tsc -w\" \"tsc-alias -w\")" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/xueelf/amesu.git" 17 | }, 18 | "keywords": [ 19 | "amesu", 20 | "bot", 21 | "qq" 22 | ], 23 | "author": "Yuki <admin@yuki.sh>", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/xueelf/amesu/issues" 27 | }, 28 | "homepage": "https://github.com/xueelf/amesu#readme", 29 | "devDependencies": { 30 | "@types/node": "^20.12.7", 31 | "@types/ws": "^8.5.10", 32 | "concurrently": "^8.2.2", 33 | "tsc-alias": "^1.8.8", 34 | "typescript": "^5.4.5" 35 | }, 36 | "dependencies": { 37 | "log4js": "^6.9.1", 38 | "ws": "^8.16.0" 39 | }, 40 | "files": [ 41 | "lib" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yuki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/model/channel.ts: -------------------------------------------------------------------------------- 1 | // https://bot.q.qq.com/wiki/develop/api-v2/server-inter/channel/manage/channel/model.html#channel 2 | export type ChannelType = 0 | 1 | 2 | 3 | 4 | 10005 | 10006 | 10007; 3 | export type ChannelSubType = 0 | 1 | 2 | 3; 4 | export type PrivateType = 0 | 1 | 2; 5 | export type SpeakPermission = 0 | 1 | 2; 6 | type Permissions = 0x0000000001 | 0x0000000002 | 0x0000000004; 7 | 8 | export interface Channel { 9 | /** 子频道 id */ 10 | id: string; 11 | /** 频道 id */ 12 | guild_id: string; 13 | /** 子频道名 */ 14 | name: string; 15 | /** 子频道类型 */ 16 | type: ChannelType; 17 | /** 子频道子类型 */ 18 | sub_type: ChannelSubType; 19 | /** 排序值 */ 20 | position: number; 21 | /** 所属分组 id,仅对子频道有效,对 `子频道分组(ChannelType=4)` 无效 */ 22 | parent_id: string; 23 | /** 创建人 id */ 24 | owner_id: string; 25 | /** 子频道私密类型 */ 26 | private_type: PrivateType; 27 | /** 子频道发言权限 */ 28 | speak_permission: SpeakPermission; 29 | /** 用于标识应用子频道应用类型,仅应用子频道时会使用该字段 */ 30 | application_id: string; 31 | /** 用户拥有的子频道权限 */ 32 | permissions: Permissions; 33 | } 34 | 35 | export interface ChannelPermission { 36 | /** 子频道 id */ 37 | channel_id: string; 38 | /** 用户 id 或 身份组 id,只会返回其中之一 */ 39 | user_id?: string; 40 | /** 用户 id 或 身份组 id,只会返回其中之一 */ 41 | role_id?: string; 42 | /** 用户拥有的子频道权限 */ 43 | permissions: string; 44 | } 45 | -------------------------------------------------------------------------------- /src/bot/token.ts: -------------------------------------------------------------------------------- 1 | import { ClientConfig } from '@/bot/client'; 2 | import { objectToString } from '@/utils/common'; 3 | import { Logger, getLogger } from '@/utils/logger'; 4 | 5 | interface AppAccessToken { 6 | /** 获取到的凭证。 */ 7 | access_token: string; 8 | /** 凭证有效时间,单位:秒。目前是 7200 秒之内的值。 */ 9 | expires_in: number; 10 | } 11 | 12 | /** 13 | * 获取接口凭证 14 | * 15 | * @param appId - 在开放平台管理端上获得。 16 | * @param clientSecret - 在开放平台管理端上获得。 17 | */ 18 | async function getAppAccessToken(appId: string, clientSecret: string): Promise<AppAccessToken> { 19 | const response = await fetch('https://bots.qq.com/app/getAppAccessToken', { 20 | method: 'POST', 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | }, 24 | body: JSON.stringify({ 25 | appId, 26 | clientSecret, 27 | }), 28 | }); 29 | return <Promise<AppAccessToken>>response.json(); 30 | } 31 | 32 | export class Token { 33 | public value: string; 34 | /** 有效期 */ 35 | public lifespan: number; 36 | 37 | /** 记录器 */ 38 | private logger: Logger; 39 | 40 | constructor(private config: ClientConfig) { 41 | this.value = ''; 42 | this.lifespan = 0; 43 | this.logger = getLogger(config.appid); 44 | } 45 | 46 | public get authorization() { 47 | return `QQBot ${this.value}`; 48 | } 49 | 50 | private get is_expires() { 51 | return this.lifespan - Date.now() < 6000; 52 | } 53 | 54 | public async renew(): Promise<void> { 55 | if (!this.is_expires) { 56 | return; 57 | } 58 | const { appid, secret } = this.config; 59 | 60 | try { 61 | this.logger.trace('开始获取 token 令牌...'); 62 | 63 | const data = await getAppAccessToken(appid, secret); 64 | const timestamp = Date.now(); 65 | 66 | this.value = data.access_token; 67 | this.lifespan = timestamp + data.expires_in * 1000; 68 | 69 | this.logger.debug(`Token: ${objectToString(data)}`); 70 | } catch (error) { 71 | this.logger.error('获取 token 失败,请检查网络以及 appid 等参数是否有效'); 72 | throw new Error('Please check the config parameter is correct'); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/model/forum.ts: -------------------------------------------------------------------------------- 1 | export enum Format { 2 | /** 普通文本 */ 3 | Text = 1, 4 | /** HTML */ 5 | Html = 2, 6 | /** Markdown */ 7 | Markdown = 3, 8 | /** JSON */ 9 | Json = 4, 10 | } 11 | 12 | export interface ThreadInfo { 13 | /** 主帖ID */ 14 | thread_id: string; 15 | /** 帖子标题 */ 16 | title: string; 17 | /** 帖子内容 */ 18 | content: string; 19 | /** 发表时间 */ 20 | date_time: string; 21 | } 22 | 23 | export interface Thread { 24 | /** 频道ID */ 25 | guild_id: string; 26 | /** 子频道ID */ 27 | channel_id: string; 28 | /** 作者ID */ 29 | author_id: string; 30 | /** ThreadInfo 主帖内容 */ 31 | thread_info: ThreadInfo; 32 | } 33 | 34 | export interface Post { 35 | /** 频道ID */ 36 | guild_id: string; 37 | /** 子频道ID */ 38 | channel_id: string; 39 | /** 作者ID */ 40 | author_id: string; 41 | /** 帖子内容 */ 42 | post_info: PostInfo; 43 | } 44 | 45 | export interface PostInfo { 46 | /** 主题ID */ 47 | thread_id: string; 48 | /** 帖子ID */ 49 | post_id: string; 50 | /** 帖子内容 */ 51 | content: string; 52 | /** 评论时间 */ 53 | date_time: string; 54 | } 55 | 56 | export interface Reply { 57 | /** 频道ID */ 58 | guild_id: string; 59 | /** 子频道ID */ 60 | channel_id: string; 61 | /** 作者ID */ 62 | author_id: string; 63 | /** 回复内容 */ 64 | reply_info: ReplyInfo; 65 | } 66 | 67 | export interface ReplyInfo { 68 | /** 主题ID */ 69 | thread_id: string; 70 | /** 帖子ID */ 71 | post_id: string; 72 | /** 回复ID */ 73 | reply_id: string; 74 | /** 回复内容 */ 75 | content: string; 76 | /** 回复时间 */ 77 | date_time: string; 78 | } 79 | 80 | /** 审核的类型 */ 81 | export enum AuditType { 82 | /** 帖子 */ 83 | PUBLISH_THREAD = 1, 84 | /** 评论 */ 85 | PUBLISH_POST, 86 | /** 回复 */ 87 | PUBLISH_REPLY, 88 | } 89 | 90 | export interface AuditResult { 91 | /** 频道ID */ 92 | guild_id: string; 93 | /** 子频道ID */ 94 | channel_id: string; 95 | /** 作者ID */ 96 | author_id: string; 97 | /** 主题ID */ 98 | thread_id: string; 99 | /** 帖子ID */ 100 | post_id: string; 101 | /** 回复ID */ 102 | reply_id: string; 103 | /** 审核的类型 */ 104 | type: AuditType; 105 | /** 审核结果. 0:成功 1:失败 */ 106 | result: 0 | 1; 107 | /** result不为0时错误信息 */ 108 | err_msg: string; 109 | } 110 | -------------------------------------------------------------------------------- /src/api/groups.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Result } from '@/utils'; 2 | 3 | export interface SendGroupsMessageParams { 4 | /** 文本内容 */ 5 | content?: string; 6 | /** 消息类型: 0 是文本,2 是 markdown,3 ark,4 embed,7 media 富媒体 */ 7 | msg_type: 0 | 2 | 3 | 4 | 7; 8 | markdown?: Record<string, unknown>; 9 | keyboard?: Record<string, unknown>; 10 | media?: { 11 | file_info: string; 12 | }; 13 | ark?: Record<string, unknown>; 14 | /** 15 | * @deprecated 暂不支持 16 | */ 17 | image?: unknown; 18 | /** 19 | * 消息引用 20 | * @deprecated 暂未支持 21 | */ 22 | message_reference?: Record<string, unknown>; 23 | /** 24 | * 前置收到的事件 ID,用于发送被动消息 25 | * @deprecated 暂未支持 26 | */ 27 | event_id?: string; 28 | /** 前置收到的消息 ID,用于发送被动消息 */ 29 | msg_id?: string; 30 | /** 31 | * 回复消息的序号,与 msg_id 联合使用,避免相同消息 id 回复重复发送,不填默认是 1。 32 | * 相同的 msg_id + msg_seq 重复发送会失败。 33 | */ 34 | msg_seq?: number; 35 | } 36 | 37 | export interface GroupMessage { 38 | /** 消息唯一 ID */ 39 | id: string; 40 | /** 发送时间 */ 41 | timestamp: string; 42 | } 43 | 44 | export interface SendGroupFileParams { 45 | /** 媒体类型 */ 46 | file_type: number; 47 | /** 需要发送媒体资源的 url */ 48 | url: string; 49 | /** 设置 true 会直接发送消息到目标端,且会占用主动消息频次 */ 50 | srv_send_msg: boolean; 51 | /** 52 | * @deprecated 暂未支持 53 | */ 54 | file_data?: unknown; 55 | } 56 | 57 | export interface GroupFile { 58 | /** 消息唯一 ID */ 59 | id: string; 60 | /** 发送时间 */ 61 | timestamp: string; 62 | } 63 | 64 | export default (request: Request) => { 65 | return { 66 | /** 67 | * 发送消息到群。 68 | */ 69 | sendGroupMessage( 70 | group_openid: string, 71 | params: SendGroupsMessageParams, 72 | ): Promise<Result<GroupMessage>> { 73 | return request.post(`/v2/groups/${group_openid}/messages`, params); 74 | }, 75 | 76 | /** 77 | * 用于撤回机器人发送在当前群的消息 78 | */ 79 | recallGroupMessage(group_openid: string, message_id: string): Promise<Result> { 80 | return request.delete(`/v2/groups/${group_openid}/messages/${message_id}`); 81 | }, 82 | 83 | /** 84 | * 发送富媒体消息到群。 85 | */ 86 | sendGroupFile(group_openid: string, params: SendGroupFileParams): Promise<Result<GroupFile>> { 87 | return request.post(`/v2/groups/${group_openid}/files`, params); 88 | }, 89 | }; 90 | }; 91 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 对象深合并,用法与 `Object.assign()` 保持一致。 3 | * 4 | * @param target - 目标对象,接收源对象属性的对象,也是修改后的返回值。 5 | * @param sources - 源对象,包含将被合并的属性。 6 | * @returns 目标对象。 7 | */ 8 | export function deepAssign(target: any, ...sources: unknown[]): object { 9 | for (let i = 0; i < sources.length; i++) { 10 | const source = sources[i]; 11 | 12 | if (!source || typeof source !== `object`) { 13 | return target; 14 | } 15 | Object.entries(source).forEach(([key, value]) => { 16 | if (!value || typeof value !== `object`) { 17 | target[key] = value; 18 | return; 19 | } 20 | if (Array.isArray(value)) { 21 | target[key] = []; 22 | } 23 | if (typeof target[key] !== `object` || !target[key]) { 24 | target[key] = {}; 25 | } 26 | deepAssign(target[key], value); 27 | }); 28 | } 29 | return target; 30 | } 31 | 32 | export function objectToParams(object: object): string { 33 | const params = new URLSearchParams(); 34 | const keys = Object.keys(object); 35 | 36 | for (let index = 0; index < keys.length; index++) { 37 | const key = keys[index]; 38 | const value = Reflect.get(object, key); 39 | 40 | params.append(key, value); 41 | } 42 | return params.toString(); 43 | } 44 | 45 | export function parseBody(params?: object): Required<RequestInit['body']> { 46 | if (!params) { 47 | return; 48 | } 49 | const has_blob = Object.entries(params).some(([_, value]) => value instanceof Blob); 50 | 51 | if (has_blob) { 52 | const formData = new FormData(); 53 | const keys = Object.keys(params); 54 | 55 | for (let index = 0; index < keys.length; index++) { 56 | const key = keys[index]; 57 | const value = Reflect.get(params, key); 58 | 59 | formData.append(key, value); 60 | } 61 | return formData; 62 | } 63 | return JSON.stringify(params); 64 | } 65 | 66 | export function objectToString(value: unknown): string { 67 | if (typeof value === 'string') { 68 | return value; 69 | } 70 | return JSON.stringify(value, null, 2); 71 | } 72 | 73 | export async function sleep(ms: number): Promise<void> { 74 | return new Promise(resolve => setTimeout(resolve, ms)); 75 | } 76 | 77 | export function parseError(error: unknown): string { 78 | return error instanceof Error ? error.message : objectToString(error); 79 | } 80 | -------------------------------------------------------------------------------- /src/api/users.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Result } from '@/utils'; 2 | import type { User } from '@/model/user'; 3 | import type { Guild } from '@/model/guild'; 4 | 5 | export interface SendUserMessageParams { 6 | /** 文本内容 */ 7 | content?: string; 8 | /** 消息类型: 0 文本,1 图文混排 ,2 markdown 3 ark,4 embed 7 富媒体 */ 9 | msg_type: 0 | 1 | 2 | 3 | 4 | 7; 10 | markdown?: Record<string, unknown>; 11 | keyboard?: Record<string, unknown>; 12 | ark?: Record<string, unknown>; 13 | media?: { 14 | file_info: string; 15 | }; 16 | /** 17 | * @deprecated 暂不支持 18 | */ 19 | image?: unknown; 20 | /** 21 | * 消息引用 22 | * @deprecated 暂未支持 23 | */ 24 | message_reference?: Record<string, unknown>; 25 | /** 26 | * 前置收到的事件 ID,用于发送被动消息 27 | * @deprecated 暂未支持 28 | */ 29 | event_id?: string; 30 | /** 前置收到的消息 ID,用于发送被动消息 */ 31 | msg_id?: string; 32 | /** 33 | * 回复消息的序号,与 msg_id 联合使用,避免相同消息 id 回复重复发送,不填默认是 1。 34 | * 相同的 msg_id + msg_seq 重复发送会失败。 35 | */ 36 | msg_seq?: number; 37 | } 38 | 39 | export interface UserMessage { 40 | /** 消息唯一 ID */ 41 | id: string; 42 | /** 发送时间 */ 43 | timestamp: string; 44 | } 45 | 46 | export interface SendUserMessageFileParams { 47 | /** 媒体类型 */ 48 | file_type: number; 49 | /** 需要发送媒体资源的 url */ 50 | url: string; 51 | /** 设置 true 会直接发送消息到目标端,且会占用主动消息频次 */ 52 | srv_send_msg: boolean; 53 | /** 54 | * @deprecated 暂未支持 55 | */ 56 | file_data?: unknown; 57 | } 58 | 59 | export interface UserFile { 60 | /** 消息唯一 ID */ 61 | id: string; 62 | /** 发送时间 */ 63 | timestamp: string; 64 | } 65 | 66 | export interface GetUserGuildsParams { 67 | /** 读此 guild id 之前的数据 before 设置时, 先反序,再分页 */ 68 | before?: string; 69 | /** 读此 guild id 之后的数据 after 和 before 同时设置时, after 参数无效 */ 70 | after?: string; 71 | /** 每次拉取多少条数据 默认 100, 最大 100 */ 72 | limit?: number; 73 | } 74 | 75 | export default (request: Request) => { 76 | return { 77 | /** 78 | * 单独发送消息给用户。 79 | */ 80 | sendUserMessage(openid: string, params: SendUserMessageParams): Promise<Result<UserMessage>> { 81 | return request.post(`/v2/users/${openid}/messages`, params); 82 | }, 83 | 84 | /** 85 | * 用于撤回机器人发送给当前用户的消息 86 | */ 87 | recallUserMessage(openid: string, message_id: string): Promise<Result> { 88 | return request.delete(`/v2/users/${openid}/messages/${message_id}`); 89 | }, 90 | 91 | /** 92 | * 单独发送富媒体消息给用户。 93 | */ 94 | sendUserFile(openid: string, params: SendUserMessageFileParams): Promise<Result<UserFile>> { 95 | return request.post(`/v2/users/${openid}/files`, params); 96 | }, 97 | 98 | /** 99 | * 获取当前机器人详情。 100 | */ 101 | getUserInfo(): Promise<Result<User>> { 102 | return request.get(`/users/@me`); 103 | }, 104 | 105 | /** 106 | * 获取用户频道列表。 107 | */ 108 | getUserGuilds(params: GetUserGuildsParams): Promise<Result<Guild[]>> { 109 | return request.get(`/users/@me/guilds`, params); 110 | }, 111 | }; 112 | }; 113 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { deepAssign, objectToParams, parseBody, parseError } from '@/utils/common'; 2 | 3 | type Method = 'GET' | 'DELETE' | 'POST' | 'PUT' | 'PATCH'; 4 | type Config = Omit<RequestConfig, 'method' | 'url' | 'body'>; 5 | 6 | export class RequestError extends Error { 7 | constructor(message: string) { 8 | super(message); 9 | this.name = 'RequestError'; 10 | } 11 | } 12 | 13 | export interface RequestConfig extends RequestInit { 14 | method: Method; 15 | url: string; 16 | origin?: string; 17 | } 18 | 19 | /** 请求拦截器 */ 20 | export type RequestInterceptor = (config: RequestConfig) => RequestConfig | Promise<RequestConfig>; 21 | /** 响应拦截器 */ 22 | export type ResponseInterceptor<T = unknown> = (result: Result<T>) => Result | Promise<Result>; 23 | 24 | /** 结果集 */ 25 | export interface Result<T = unknown> { 26 | data: T; 27 | /** 请求配置项 */ 28 | config: RequestConfig; 29 | /** 响应状态码 */ 30 | status: number; 31 | /** 状态码消息 */ 32 | statusText: string; 33 | /** 请求头 */ 34 | headers: Headers; 35 | } 36 | 37 | export class Request { 38 | private requestInterceptors: RequestInterceptor[]; 39 | private responseInterceptors: ResponseInterceptor[]; 40 | 41 | constructor() { 42 | this.requestInterceptors = []; 43 | this.responseInterceptors = []; 44 | } 45 | 46 | /** 添加请求拦截器 */ 47 | public useRequestInterceptor(interceptor: RequestInterceptor): void { 48 | this.requestInterceptors.push(interceptor); 49 | } 50 | 51 | /** 添加响应拦截器 */ 52 | public useResponseInterceptor<T>(interceptor: ResponseInterceptor<T>): void { 53 | this.responseInterceptors.push(<ResponseInterceptor>interceptor); 54 | } 55 | 56 | public async basis<T>(config: RequestConfig): Promise<Result<T>> { 57 | if (typeof config.body === 'string') { 58 | const defaultConfig: Config = { 59 | headers: { 60 | 'Content-Type': 'application/json', 61 | }, 62 | }; 63 | config = <RequestConfig>deepAssign(defaultConfig, config); 64 | } 65 | 66 | for (const interceptor of this.requestInterceptors) { 67 | config = await interceptor(config); 68 | } 69 | const url = (config.origin ?? '') + config.url; 70 | const response = await fetch(url, config); 71 | const result: Result = { 72 | data: null, 73 | status: response.status, 74 | config, 75 | statusText: response.statusText, 76 | headers: response.headers, 77 | }; 78 | 79 | try { 80 | result.data = await response.json(); 81 | } catch (error) { 82 | if (!response.ok) { 83 | throw new RequestError(parseError(error)); 84 | } 85 | result.data = await response.text(); 86 | } 87 | for (const interceptor of this.responseInterceptors) { 88 | deepAssign(result, await interceptor(result)); 89 | } 90 | return <Result<T>>result; 91 | } 92 | 93 | public get<T>(url: string, params?: object, config?: Config): Promise<Result<T>> { 94 | if (params) { 95 | url += (/\?/.test(url) ? '&' : '?') + objectToParams(params); 96 | } 97 | return this.basis<T>({ 98 | url, 99 | method: 'GET', 100 | ...config, 101 | }); 102 | } 103 | 104 | public delete<T>(url: string, params?: object, config: Config = {}): Promise<Result<T>> { 105 | return this.basis<T>({ 106 | url, 107 | method: 'DELETE', 108 | body: parseBody(params), 109 | ...config, 110 | }); 111 | } 112 | 113 | public post<T>(url: string, params?: object, config: Config = {}): Promise<Result<T>> { 114 | return this.basis<T>({ 115 | url, 116 | method: 'POST', 117 | body: parseBody(params), 118 | ...config, 119 | }); 120 | } 121 | 122 | public put<T>(url: string, params?: object, config: Config = {}): Promise<Result<T>> { 123 | return this.basis<T>({ 124 | url, 125 | method: 'PUT', 126 | body: parseBody(params), 127 | ...config, 128 | }); 129 | } 130 | 131 | public patch<T>(url: string, params?: object, config: Config = {}): Promise<Result<T>> { 132 | return this.basis<T>({ 133 | url, 134 | method: 'PATCH', 135 | body: parseBody(params), 136 | ...config, 137 | }); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/model/message.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '@/model/user'; 2 | import type { Member } from '@/model/member'; 3 | 4 | export interface MessageEmbedThumbnail { 5 | /** 图片地址 */ 6 | url: string; 7 | } 8 | 9 | export interface MessageEmbedField { 10 | /** 字段名 */ 11 | name: string; 12 | } 13 | 14 | export interface MessageEmbed { 15 | /** 标题 */ 16 | title: string; 17 | /** 消息弹窗内容 */ 18 | prompt: string; 19 | /** 缩略图 */ 20 | thumbnail: MessageEmbedThumbnail; 21 | /** embed 字段数据 */ 22 | fields: MessageEmbedField[]; 23 | } 24 | 25 | export interface MessageArkObjKv { 26 | key: string; 27 | value: string; 28 | } 29 | 30 | export interface MessageArkObj { 31 | /** ark obj kv 列表 */ 32 | obj_kv: MessageArkObjKv[]; 33 | } 34 | 35 | export interface MessageArkKv { 36 | key: string; 37 | value: string; 38 | /** ark obj 类型的列表 */ 39 | obj: MessageArkObj[]; 40 | } 41 | 42 | export interface MessageArk { 43 | /** ark 模板 id(需要先申请) */ 44 | template_id: number; 45 | /** kv 值列表 */ 46 | kv: MessageArkKv; 47 | } 48 | 49 | export interface MessageReference { 50 | /** 需要引用回复的消息 id */ 51 | message_id: string; 52 | /** 是否忽略获取引用消息详情错误,默认否 */ 53 | ignore_get_message_error: boolean; 54 | } 55 | 56 | interface MessageMarkdownParams { 57 | /** markdown 模版 key */ 58 | key: string; 59 | /** markdown 模版 key 对应的 values ,列表长度大小为 1 代表单 value 值,长度大于 1 则为列表类型的参数 values 传参数 */ 60 | values: string[]; 61 | } 62 | 63 | export interface MessageMarkdown { 64 | /** markdown 模板 id */ 65 | template_id: number; 66 | /** markdown 模板模板参数 */ 67 | params: MessageMarkdownParams; 68 | /** 原生 markdown 内容,与 `template_id` 和 `params` 参数互斥,参数都传值将报错。 */ 69 | content: string; 70 | } 71 | 72 | export interface Message { 73 | /** 消息 id */ 74 | id: string; 75 | /** 子频道 id */ 76 | channel_id: string; 77 | /** 频道 id */ 78 | guild_id: string; 79 | /** 消息内容 */ 80 | content: string; 81 | /** 消息创建时间 */ 82 | timestamp: string; 83 | /** 消息编辑时间 */ 84 | edited_timestamp: string; 85 | /** 是否是 @ 全员消息 */ 86 | mention_everyone: boolean; 87 | /** 消息创建者 */ 88 | author: User; 89 | /** 附件 */ 90 | attachments: MessageAttachment[]; 91 | /** embed */ 92 | embeds: MessageEmbed; 93 | /** 消息中 @ 的人 */ 94 | mentions: User[]; 95 | /** 消息创建者的 member 信息 */ 96 | member: Member; 97 | /** ark 消息 */ 98 | ark: MessageArk; 99 | /** 100 | * 用于消息间的排序,seq 在同一子频道中按从先到后的顺序递增,不同的子频道之间消息无法排序。 101 | * 102 | * @deprecated 目前只在消息事件中有值,`2022 年 8 月 1 日`后续废弃。 103 | */ 104 | seq: number; 105 | /** 子频道消息 seq,用于消息间的排序,seq 在同一子频道中按从先到后的顺序递增,不同的子频道之间消息无法排序 */ 106 | seq_in_channel: string; 107 | /** 对象 引用消息对象 */ 108 | message_reference: MessageReference; 109 | } 110 | 111 | interface MessageAttachment { 112 | /** 下载地址 */ 113 | url: string; 114 | } 115 | 116 | export interface MessageSetting { 117 | /** 是否允许创建私信 */ 118 | disable_create_dm: string; 119 | /** 是否允许发主动消息 */ 120 | disable_push_msg: string; 121 | /** 子频道 id 数组 */ 122 | channel_ids: string[]; 123 | /** 每个子频道允许主动推送消息最大消息条数 */ 124 | channel_push_max_num: number; 125 | } 126 | 127 | export interface PinMessage { 128 | /** 频道 id */ 129 | guild_id: string; 130 | /** 子频道 id */ 131 | channel_id: string; 132 | /** 子频道内精华消息 id 数组 */ 133 | message_ids: string[]; 134 | } 135 | 136 | export interface MessageAudited { 137 | /** 消息审核 id */ 138 | audit_id: string; 139 | /** 消息 id,只有审核通过事件才会有值 */ 140 | audit_time: string; 141 | /** 频道 id */ 142 | channel_id: string; 143 | /** 子频道 id */ 144 | create_time: string; 145 | /** 消息审核时间 */ 146 | guild_id: string; 147 | /** 消息创建时间 */ 148 | message_id: string; 149 | /** 子频道消息 seq,用于消息间的排序,seq 在同一子频道中按从先到后的顺序递增,不同的子频道之间消息无法排序 */ 150 | seq_in_channel: string; 151 | } 152 | 153 | // enum ReactionTargetType {} 154 | 155 | export interface ReactionTarget { 156 | /** 表态对象ID */ 157 | id: string; 158 | type: number; 159 | } 160 | 161 | export interface Emoji { 162 | /** 表情ID,系统表情使用数字为ID,emoji使用emoji本身为id */ 163 | id: string; 164 | type: number; 165 | } 166 | 167 | export interface MessageReaction { 168 | /** 用户ID */ 169 | user_id: string; 170 | /** 频道ID */ 171 | guild_id: string; 172 | /** 子频道ID */ 173 | channel_id: string; 174 | /** 表态对象 */ 175 | target: ReactionTarget; 176 | /** 表态所用表情 */ 177 | emoji: Emoji; 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <div align="center"> 2 | <img src="https://vip2.loli.io/2022/11/04/AWEchfODdwszL8N.png" alt="Amesu" width="200" /> 3 | <h3>Amesu</h3> 4 | </div> 5 | 6 | --- 7 | 8 | ![package](https://img.shields.io/npm/v/amesu?label=amesu&style=flat-square&logo=npm&labelColor=FAFAFA) 9 | ![engine](https://img.shields.io/node/v/amesu?style=flat-square&logo=Node.js&labelColor=FAFAFA) 10 | ![downloads](https://img.shields.io/npm/dt/amesu?style=flat-square&logo=tinder&logoColor=FF8C00&labelColor=FAFAFA&color=616DF8) 11 | 12 | 本项目是一个在 Node.js 环境下运行的 QQ 机器人第三方 SDK。 13 | 14 | ## Introduction 15 | 16 | > 由于腾讯是近期上线的群聊 API,官方文档的内容与实际表现**有部分差异**,请勿将其用于生产环境。 17 | 18 | 项目的名字来源于 Cygames 开发和发行的游戏『公主连结 Re:Dive』中的登场角色「アメス」,其罗马音 **「a me su」** 用作了本项目的名字。 19 | 20 | ## Install 21 | 22 | ```shell 23 | npm i amesu 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```javascript 29 | const { Client } = require('amesu'); 30 | 31 | const client = new Client({ 32 | appid: '1145141919', 33 | token: '38bc73e16208135fb111c0c573a44eaa', 34 | secret: '6208135fb111c0c5', 35 | events: ['GROUP_AND_C2C_EVENT', 'PUBLIC_GUILD_MESSAGES'], 36 | }); 37 | 38 | // 监听频道消息 39 | client.on('at.message.create', async event => { 40 | // 快捷回复 41 | await event.reply({ 42 | content: 'hello world', 43 | }); 44 | }); 45 | 46 | // 监听群聊消息 47 | client.on('group.at.message.create', async event => { 48 | // API 调用 49 | await client.api.sendGroupMessage(event.group_openid, { 50 | msg_id: event.id, 51 | msg_type: 0, 52 | content: 'hello world', 53 | }); 54 | }); 55 | 56 | // 机器人上线 57 | client.online(); 58 | ``` 59 | 60 | 事件回调中的 `reply()` 函数是 `client.api` 的语法糖,会根据**消息事件**的类型指向对应消息发送的 api 函数,并自动传入 from_id 与 msg_id。 61 | 62 | ## Event 63 | 64 | 目前 socket 返回的事件全部为大写,并用下划线做分割。但是在 Node.js 的 `EventEmitter` 中,事件名一般使用小写字母。 65 | 66 | 这是因为事件名通常表示一种行为或状态,而不是一个特定的类或构造函数。根据 JavaScript 的命名约定,使用小写字母来表示这些行为或状态更为常见和推荐。 67 | 68 | 所以 Amesu 针对事件名做了以下处理: 69 | 70 | - 事件名全部转换为小写 71 | - 使用小数点替换下划线 72 | - 会话事件添加 `session` 前缀 73 | 74 | 例如 `MESSAGE_CREATE` -> `message.create`,`READY` -> `session.ready`。 75 | 76 | 你可以仅监听事件的部分前缀,例如: 77 | 78 | ```javascript 79 | const { Client } = require('amesu'); 80 | 81 | const client = new Client(); 82 | 83 | client 84 | .on('guild.member', event => { 85 | console.log(event); 86 | }) 87 | .online(); 88 | ``` 89 | 90 | 这样 `guild.member.add`、`guild.member.update`、`guild.member.remove`,三个事件将会被全部监听,这使得消息订阅更具有灵活性。 91 | 92 | ## Config 93 | 94 | ```typescript 95 | /** 客户端配置项 */ 96 | interface ClientConfig { 97 | /** 机器人 ID */ 98 | appid: string; 99 | /** 机器人令牌 */ 100 | token: string; 101 | /** 机器人密钥 */ 102 | secret: string; 103 | /** 订阅事件 */ 104 | events: IntentEvent[]; 105 | /** 是否开启沙盒,默认 `false` */ 106 | sandbox: boolean; 107 | /** 掉线重连数,默认 `3` */ 108 | max_retry?: number; 109 | /** 日志等级,默认 `'INFO'`,仅输出收到的指令信息 */ 110 | log_level?: LogLevel; 111 | } 112 | 113 | /** 日志等级,具体使用可查阅 log4js 文档 */ 114 | type LogLevel = 'OFF' | 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE' | 'ALL'; 115 | ``` 116 | 117 | ## API 118 | 119 | ### Client.api 120 | 121 | 封装了官方文档所提供的 api 接口,可直接调用。(并不是所有 api 都能返回期望的结果) 122 | 123 | ### Client.request 124 | 125 | 基于 fetch 封装,可发送自定义网络请求。 126 | 127 | ### Client.online() 128 | 129 | 机器人上线。 130 | 131 | ### Client.offline() 132 | 133 | 机器人下线。 134 | 135 | ### Client.useEventInterceptor(interceptor) 136 | 137 | 添加事件拦截器。 138 | 139 | ### Request.useRequestInterceptor(interceptor) 140 | 141 | 添加请求拦截器。 142 | 143 | ### Request.useResponseInterceptor(interceptor) 144 | 145 | 添加响应拦截器。 146 | 147 | ### Request.basis(config) 148 | 149 | 发送网络请求。(基础封装) 150 | 151 | ### Request.get(url[, params][, config]) 152 | 153 | 发送 GET 请求。 154 | 155 | ### Request.delete(url[, params][, config]) 156 | 157 | 发送 DELETE 请求。 158 | 159 | ### Request.post(url[, params][, config]) 160 | 161 | 发送 POST 请求。 162 | 163 | ### Request.put(url[, params][, config]) 164 | 165 | 发送 PUT 请求。 166 | 167 | ### Request.patch(url[, params][, config]) 168 | 169 | 发送 PATCH 请求。 170 | 171 | ## 插件开发 172 | 173 | Amesu 仅仅是一个用于帮助建立 socket 通信的 SDK,而不是一个机器人解决方案,这两者不应该耦合,所以并未原生提供插件支持。 174 | 175 | 如果你想要开发插件,建立属于自己的生态,可以直接将她作为依赖进行二次开发。她十分的轻便,没有复杂的依赖项。拥有完整类型提示的同时,仅有 120 kb 的大小,而官方 SDK 却占据了 430 kb+。 176 | 177 | 若不想手搓,可以使用 [kokkoro](https://github.com/kokkorojs/kokkoro) 框架进行机器人开发。如果不想集成框架体系,那么你也可以直接安装 `@kokkoro/core` 依赖去自定义插件。 178 | 179 | ```shell 180 | npm i @kokkoro/core 181 | ``` 182 | 183 | 插件代码示例: 184 | 185 | ```javascript 186 | import { useCommand, useEvent } from '@kokkoro/core'; 187 | 188 | /** 189 | * @type {import('@kokkoro/core').Metadata} 190 | */ 191 | export const metadata = { 192 | name: 'example', 193 | description: '插件示例', 194 | }; 195 | 196 | export default function Example() { 197 | useEvent( 198 | ctx => { 199 | ctx.logger.mark('link start'); 200 | }, 201 | ['session.ready'], 202 | ); 203 | 204 | useCommand('/测试', () => 'hello world'); 205 | useCommand('/复读 <message>', ctx => ctx.query.message); 206 | } 207 | ``` 208 | 209 | 更多示例可查看 core 的 [README](https://github.com/kokkorojs/kokkoro/blob/master/packages/core/README.md) 自述。 210 | 211 | ## FAQ 212 | 213 | ### 为什么要做这个项目? 214 | 215 | 因为官方 [频道 SDK](https://github.com/tencent-connect/bot-node-sdk) 已经有 2 年没更新了,不支持群聊而且使用体验非常糟糕。 216 | 217 | ### 为什么 config 一定要指定 events? 218 | 219 | 如果你是公域机器人,那么在使用官方 SDK 不传入 events 情况下,就会不断重连并在控制台疯狂输出。 220 | 221 | 原因是部分事件仅限私域机器人使用,如果公域机器人订阅了就会抛异常,私域机器人订阅了公域事件却不会有任何问题... 222 | 223 | 官方 SDK 的逻辑是没有传 events 就默认监听全部事件(其实也并不是全部,文档里有遗漏),这是不合理的。现在也没有任何手段知道机器人是公域还是私域,以及是否拥有群聊权限等问题,因此只能手动在 config 传入。 224 | 225 | ### 为什么部分 API 没有返回结果? 226 | 227 | 腾讯是近期才开放群聊 API 内测的,提供的文档也很不完善,目前存在字段、返回结果不一致,v1、v2 接口混用(鉴权方式不一样)等问题,所以调用某些接口可能会无法达到预期。 228 | 229 | ### 为什么 request 不使用 axios 封装? 230 | 231 | axios 太大了,基于 fetch 的封装 build 后大小仅 3 kb 不到,基本满足大部分的使用场景。如果你想要使用 axios 或者其它网络请求库,可以自行安装依赖。 232 | 233 | ### 这个 SDK 能做什么? 234 | 235 | 频道与群聊的消息收发已测试完毕,API 返回结果与 interface 不符的问题还待腾讯后续完善文档和接口统一。 236 | 237 | 当前 Amesu 已经有了完整的鉴权流程(会话保活、掉线重连、凭证刷新),并做了日志和网络请求的封装,后面没什么问题就不会再有大改了。如果有 API 缺失,在 `api` 文件内参考格式直接添加 url 就可以正常使用,也欢迎来提 pr。 238 | -------------------------------------------------------------------------------- /src/api/channels.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Result } from '@/utils'; 2 | import type { Schedule } from '@/model/schedule'; 3 | import type { AudioControl } from '@/model/audio'; 4 | import type { Format, Thread, ThreadInfo } from '@/model/forum'; 5 | import type { Channel, ChannelPermission, PrivateType, SpeakPermission } from '@/model/channel'; 6 | import type { 7 | Message, 8 | MessageArk, 9 | MessageEmbed, 10 | MessageMarkdown, 11 | MessageReference, 12 | PinMessage, 13 | } from '@/model/message'; 14 | 15 | /** 16 | * @link https://bot.q.qq.com/wiki/develop/api-v2/server-inter/message/post_messages.html#%E9%80%9A%E7%94%A8%E5%8F%82%E6%95%B0 17 | */ 18 | export interface SendChannelMessageParams { 19 | /** 消息内容 */ 20 | content?: string; 21 | /** embed 消息 */ 22 | embed?: MessageEmbed; 23 | /** ark 消息对象 */ 24 | ark?: MessageArk; 25 | /** 引用消息对象 */ 26 | message_reference?: MessageReference; 27 | /** 图片 url 地址,平台会转存该图片,用于下发图片消息 */ 28 | image?: string; 29 | /** 要回复的消息 id, 在 `at.create.message` 事件中获取。 */ 30 | msg_id?: string; 31 | /** 要回复的事件 id, 在各事件对象中获取。 */ 32 | event_id?: string; 33 | /** markdown 消息对象 */ 34 | markdown?: MessageMarkdown; 35 | /** 通过文件上传的方式发送图片。 */ 36 | file_image?: Blob; 37 | } 38 | 39 | export interface UpdateChannelMessageParams { 40 | /** 子频道名 */ 41 | name?: string; 42 | /** 排序 */ 43 | position?: number; 44 | /** 分组 id */ 45 | parent_id?: string; 46 | /** 子频道私密类型 */ 47 | private_type?: PrivateType; 48 | /** 子频道发言权限 */ 49 | speak_permission?: SpeakPermission; 50 | } 51 | 52 | export interface ChannelOnlineNum { 53 | online_nums: number; 54 | } 55 | 56 | export interface UpdateChannelPermissionParams { 57 | /** 字符串形式的位图表示赋予用户的权限 */ 58 | add: string; 59 | /** 字符串形式的位图表示删除用户的权限 */ 60 | remove: string; 61 | } 62 | 63 | export interface ChannelScheduleParams { 64 | schedule: Omit<Schedule, 'id'>; 65 | } 66 | 67 | export interface ChannelMicParams { 68 | channel_id: string; 69 | } 70 | 71 | export interface ChannelThread { 72 | /** 帖子列表对象 */ 73 | threads: Thread; 74 | /** 是否拉取完毕(0:否;1:是) */ 75 | is_finish: number; 76 | } 77 | 78 | export interface CreateChannelThreadParams { 79 | /** 帖子标题 */ 80 | title: string; 81 | /** 帖子内容 */ 82 | content: string; 83 | /** 帖子文本格式 */ 84 | format: Format; 85 | } 86 | 87 | export default (request: Request) => { 88 | return { 89 | /** 90 | * 用于向 channel_id 指定的子频道发送消息。 91 | */ 92 | sendChannelMessage( 93 | channel_id: string, 94 | params: SendChannelMessageParams, 95 | ): Promise<Result<Message>> { 96 | return request.post(`/channels/${channel_id}/messages`, params); 97 | }, 98 | 99 | /** 100 | * 用于撤回子频道 channel_id 下的消息 message_id。 101 | */ 102 | deleteChannelMessage( 103 | channel_id: string, 104 | message_id: string, 105 | hidetip: boolean = false, 106 | ): Promise<Result> { 107 | return request.delete(`/channels/${channel_id}/messages/${message_id}?hidetip=${hidetip}`); 108 | }, 109 | 110 | /** 111 | * 获取 channel_id 指定的子频道的详情。 112 | */ 113 | getChannelInfo(channel_id: string): Promise<Result<Channel>> { 114 | return request.get(`/channels/${channel_id}`); 115 | }, 116 | 117 | /** 118 | * 修改 channel_id 指定的子频道的信息。 119 | */ 120 | updateChannelInfo( 121 | channel_id: string, 122 | params: UpdateChannelMessageParams, 123 | ): Promise<Result<Channel>> { 124 | return request.patch(`/channels/${channel_id}`, params); 125 | }, 126 | 127 | /** 128 | * 删除 channel_id 指定的子频道。 129 | */ 130 | deleteChannel(channel_id: string): Promise<Result> { 131 | return request.delete(`/channels/${channel_id}`); 132 | }, 133 | 134 | /** 135 | * 获取子频道在线成员数。 136 | */ 137 | getChannelOnlineNum(channel_id: string): Promise<Result<ChannelOnlineNum>> { 138 | return request.get(`/channels/${channel_id}/online_nums`); 139 | }, 140 | 141 | /** 142 | * 获取子频道 channel_id 下用户 user_id 的权限。 143 | */ 144 | getChannelMemberPermission( 145 | channel_id: string, 146 | user_id: string, 147 | ): Promise<Result<ChannelPermission>> { 148 | return request.get(`/channels/${channel_id}/members/${user_id}/permissions`); 149 | }, 150 | 151 | /** 152 | * 用于修改子频道 channel_id 下用户 user_id 的权限。 153 | */ 154 | updateChannelMemberPermission( 155 | channel_id: string, 156 | user_id: string, 157 | params: UpdateChannelPermissionParams, 158 | ): Promise<Result> { 159 | return request.put(`/channels/${channel_id}/members/${user_id}/permissions`, params); 160 | }, 161 | 162 | /** 163 | * 获取子频道 channel_id 下身份组 role_id 的权限。 164 | */ 165 | getChannelRolePermission( 166 | channel_id: string, 167 | role_id: string, 168 | ): Promise<Result<ChannelPermission>> { 169 | return request.get(`/channels/${channel_id}/roles/${role_id}/permissions`); 170 | }, 171 | 172 | /** 173 | * 修改子频道 channel_id 下身份组 role_id 的权限。 174 | */ 175 | updateChannelRolePermission( 176 | channel_id: string, 177 | role_id: string, 178 | params: UpdateChannelPermissionParams, 179 | ): Promise<Result> { 180 | return request.put(`/channels/${channel_id}/roles/${role_id}/permissions`, params); 181 | }, 182 | 183 | /** 184 | * 用于添加子频道 channel_id 内的精华消息。 185 | */ 186 | addChannelPin(channel_id: string, message_id: string): Promise<Result<PinMessage>> { 187 | return request.put(`/channels/${channel_id}/pins/${message_id}`); 188 | }, 189 | 190 | /** 191 | * 用于删除子频道 channel_id 下指定 message_id 的精华消息。 192 | */ 193 | deleteChannelPin(channel_id: string, message_id: string): Promise<Result> { 194 | return request.delete(`/channels/${channel_id}/pins/${message_id}`); 195 | }, 196 | 197 | /** 198 | * 用于获取子频道 channel_id 内的精华消息。 199 | */ 200 | getChannelPin(channel_id: string): Promise<Result<PinMessage>> { 201 | return request.get(`/channels/${channel_id}/pins`); 202 | }, 203 | 204 | /** 205 | * 用于获取channel_id指定的子频道中当天的日程列表。 206 | */ 207 | getChannelSchedule(channel_id: string, since?: number): Promise<Result<Schedule>> { 208 | return request.get(`/channels/${channel_id}/schedules?since=${since}`); 209 | }, 210 | 211 | /** 212 | * 获取日程子频道 channel_id 下 schedule_id 指定的的日程的详情。 213 | */ 214 | getChannelScheduleInfo(channel_id: string, schedule_id: string): Promise<Result<Schedule>> { 215 | return request.get(`/channels/${channel_id}/schedules/${schedule_id}`); 216 | }, 217 | 218 | /** 219 | * 用于在 channel_id 指定的日程子频道下创建一个日程。 220 | */ 221 | createChannelSchedule( 222 | channel_id: string, 223 | params: ChannelScheduleParams, 224 | ): Promise<Result<Schedule>> { 225 | return request.post(`/channels/${channel_id}/schedules`, params); 226 | }, 227 | 228 | /** 229 | * 用于修改日程子频道 channel_id 下 schedule_id 指定的日程的详情。 230 | */ 231 | updateChannelSchedule( 232 | channel_id: string, 233 | schedule_id: string, 234 | params: ChannelScheduleParams, 235 | ): Promise<Result<Schedule>> { 236 | return request.patch(`/channels/${channel_id}/schedules/${schedule_id}`, params); 237 | }, 238 | 239 | /** 240 | * 用于删除日程子频道 channel_id 下 schedule_id 指定的日程。 241 | */ 242 | deleteChannelSchedule(channel_id: string, schedule_id: string): Promise<Result> { 243 | return request.delete(`/channels/${channel_id}/schedules/${schedule_id}`); 244 | }, 245 | 246 | /** 247 | * 用于控制子频道 channel_id 下的音频。 248 | */ 249 | channelAudioControl(channel_id: string, params: AudioControl): Promise<Result> { 250 | return request.post(`/channels/${channel_id}/audio`, params); 251 | }, 252 | 253 | /** 254 | * 机器人在 channel_id 对应的语音子频道上麦。 255 | */ 256 | channelMicOn(channel_id: string, params: ChannelMicParams): Promise<Result> { 257 | return request.put(`/channels/${channel_id}/mic`, params); 258 | }, 259 | 260 | /** 261 | * 机器人在 channel_id 对应的语音子频道下麦。 262 | */ 263 | channelMicOff(channel_id: string, params: ChannelMicParams): Promise<Result> { 264 | return request.delete(`/channels/${channel_id}/mic`, params); 265 | }, 266 | 267 | /** 268 | * 该接口用于获取子频道下的帖子列表。 269 | */ 270 | getChannelThread(channel_id: string): Promise<Result<ChannelThread>> { 271 | return request.get(`/channels/${channel_id}/threads`); 272 | }, 273 | 274 | /** 275 | * 该接口用于获取子频道下的帖子详情。 276 | */ 277 | getChannelThreadInfo(channel_id: string, thread_id: string): Promise<Result<ThreadInfo>> { 278 | return request.get(`/channels/${channel_id}/threads/${thread_id}`); 279 | }, 280 | 281 | /** 282 | * 发表帖子。 283 | */ 284 | createChannelThread( 285 | channel_id: string, 286 | params: CreateChannelThreadParams, 287 | ): Promise<Result<ChannelThread>> { 288 | return request.put(`/channels/${channel_id}/threads`, params); 289 | }, 290 | 291 | /** 292 | * 用于删除指定子频道下的某个帖子。 293 | */ 294 | deleteChannelThread(channel_id: string, thread_id: string): Promise<Result> { 295 | return request.delete(`/channels/${channel_id}/threads/${thread_id}`); 296 | }, 297 | }; 298 | }; 299 | -------------------------------------------------------------------------------- /src/api/guilds.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Result } from '@/utils'; 2 | import type { Guild } from '@/model/guild'; 3 | import type { Member, Role } from '@/model/member'; 4 | import type { 5 | ApiPermission, 6 | ApiPermissionDemand, 7 | ApiPermissionDemandIdentify, 8 | } from '@/model/permission'; 9 | import type { 10 | Channel, 11 | ChannelSubType, 12 | ChannelType, 13 | PrivateType, 14 | SpeakPermission, 15 | } from '@/model/channel'; 16 | import { MessageSetting } from '@/model/message'; 17 | import { Announce, RecommendChannel } from '@/model/announce'; 18 | 19 | export interface CreateGuildChannelParams { 20 | /** 子频道名称 */ 21 | name: string; 22 | /** 子频道类型 */ 23 | type: ChannelType; 24 | /** 子频道子类型 */ 25 | sub_type: ChannelSubType; 26 | /** 子频道排序,必填;当子频道类型为 子频道分组(ChannelType=4)时,必须大于等于 2 */ 27 | position: number; 28 | /** 子频道所属分组ID */ 29 | parent_id: string; 30 | /** 子频道私密类型 */ 31 | private_type: PrivateType; 32 | /** 子频道私密类型成员 ID */ 33 | private_user_ids: string[]; 34 | /** 子频道发言权限 */ 35 | speak_permission: SpeakPermission; 36 | /** 应用类型子频道应用 AppID,仅应用子频道需要该字段 */ 37 | application_id: string; 38 | } 39 | 40 | export interface GetGuildMembersParams { 41 | /** 将上一次回包中next填入, 如果是第一次请求填 0,默认为 0 */ 42 | start_index: string; 43 | /** 分页大小,1-400,默认是 1。成员较多的频道尽量使用较大的 limit 值,以减少请求数 */ 44 | limit: number; 45 | } 46 | 47 | export interface GuildRoleMembers { 48 | data: Member[]; 49 | next: string; 50 | } 51 | 52 | export interface DeleteGuildUserMemberParams { 53 | /** 删除成员的同时,将该用户添加到频道黑名单中 */ 54 | add_blacklist: boolean; 55 | /** 删除成员的同时,撤回该成员的消息,可以指定撤回消息的时间范围 */ 56 | delete_history_msg_days: number; 57 | } 58 | 59 | export interface GuildRoles { 60 | /** 频道 ID */ 61 | guild_id: string; 62 | /** 一组频道身份组对象 */ 63 | roles: Role[]; 64 | /** 默认分组上限 */ 65 | role_num_limit: string; 66 | } 67 | 68 | export interface GuildRoleParams { 69 | /** 名称 */ 70 | name?: string; 71 | /** ARGB 的 HEX 十六进制颜色值转换后的十进制数值 */ 72 | color?: number; 73 | /** 在成员列表中单独展示: 0-否, 1-是 */ 74 | hoist?: number; 75 | } 76 | 77 | export interface CreateGuildRole { 78 | /** 身份组 ID */ 79 | role_id: string; 80 | /** 所创建的频道身份组对象 */ 81 | role: Role[]; 82 | } 83 | 84 | export interface UpdateGuildRole { 85 | /** 频道 ID */ 86 | guild_id: string; 87 | /** 身份组 ID */ 88 | role_id: string; 89 | /** 所创建的频道身份组对象 */ 90 | role: Role[]; 91 | } 92 | 93 | export interface GuildMemberRoleParams { 94 | channel: Pick<Channel, 'id'>; 95 | } 96 | 97 | export interface GuildApiPermissions { 98 | apis: ApiPermission[]; 99 | } 100 | 101 | export interface SendGuildApiPermissionDemandParams { 102 | /** 授权链接发送的子频道 id */ 103 | channel_id: string; 104 | /** api 权限需求标识对象 */ 105 | api_identify: ApiPermissionDemandIdentify[]; 106 | /** 机器人申请对应的 API 接口权限后可以使用功能的描述 */ 107 | desc: string; 108 | } 109 | 110 | export interface GuildMuteParams { 111 | /** 禁言到期时间戳,绝对时间戳,单位:秒(与 mute_seconds 字段同时赋值的话,以该字段为准) */ 112 | mute_end_timestamp?: string; 113 | /** 禁言多少秒(两个字段二选一,默认以 mute_end_timestamp 为准) */ 114 | mute_seconds?: string; 115 | } 116 | 117 | export interface GuildMembersMuteParams extends GuildMuteParams { 118 | /** 禁言成员的 user_id 列表,即 User 的 id */ 119 | user_ids: string[]; 120 | } 121 | 122 | export interface CreateGuildAnnounceParams { 123 | /** 选填,消息 id,message_id 有值则优选将某条消息设置为成员公告 */ 124 | message_id: string; 125 | /** 选填,子频道 id,message_id 有值则为必填。 */ 126 | channel_id: string; 127 | /** 选填,公告类别 0:成员公告,1:欢迎公告,默认为成员公告 */ 128 | announces_type: number; 129 | /** 选填,推荐子频道列表,会一次全部替换推荐子频道列表 */ 130 | recommend_channels: RecommendChannel[]; 131 | } 132 | 133 | export default (request: Request) => { 134 | return { 135 | /** 136 | * 获取 `guild_id` 指定的频道的详情。 137 | */ 138 | getGuildInfo(guild_id: string): Promise<Result<Guild>> { 139 | return request.get(`/guilds/${guild_id}`); 140 | }, 141 | 142 | /** 143 | * 获取 guild_id 指定的频道下的子频道列表。 144 | */ 145 | getGuildChannels(guild_id: string): Promise<Result<Channel[]>> { 146 | return request.get(`/guilds/${guild_id}/channels`); 147 | }, 148 | 149 | /** 150 | * 在 guild_id 指定的频道下创建一个子频道。 151 | */ 152 | createGuildChannel( 153 | guild_id: string, 154 | params: CreateGuildChannelParams, 155 | ): Promise<Result<Channel>> { 156 | return request.post(`/guilds/${guild_id}/channels`, params); 157 | }, 158 | 159 | /** 160 | * 获取 guild_id 指定的频道中所有成员的详情列表,支持分页。 161 | */ 162 | getGuildMembers( 163 | guild_id: string, 164 | after: string = '0', 165 | limit: number = 1, 166 | ): Promise<Result<Member[]>> { 167 | return request.get(`/guilds/${guild_id}/members?after=${after}&limit=${limit}`); 168 | }, 169 | 170 | /** 171 | * 获取 guild_id 频道中指定 role_id 身份组下所有成员的详情列表,支持分页。 172 | */ 173 | getGuildRoleMembers( 174 | guild_id: string, 175 | role_id: string, 176 | params: GetGuildMembersParams, 177 | ): Promise<Result<GuildRoleMembers>> { 178 | const defaultParams: GetGuildMembersParams = { 179 | start_index: '0', 180 | limit: 1, 181 | }; 182 | return request.get(`/guilds/${guild_id}/roles/${role_id}/members`, { 183 | ...defaultParams, 184 | ...params, 185 | }); 186 | }, 187 | 188 | /** 189 | * 获取 guild_id 指定的频道中 user_id 对应成员的详细信息。 190 | */ 191 | getGuildUserMember(guild_id: string, user_id: string): Promise<Result<Member>> { 192 | return request.get(`/guilds/${guild_id}/members/${user_id}`); 193 | }, 194 | 195 | /** 196 | * 删除 guild_id 指定的频道下的成员 user_id。 197 | */ 198 | deleteGuildUserMember( 199 | guild_id: string, 200 | user_id: string, 201 | params: DeleteGuildUserMemberParams, 202 | ): Promise<Result> { 203 | return request.delete(`/guilds/${guild_id}/members/${user_id}`, params); 204 | }, 205 | 206 | /** 207 | * 获取 guild_id 指定的频道下的身份组列表。 208 | */ 209 | getGuildRoles(guild_id: string): Promise<Result<GuildRoles>> { 210 | return request.get(`/guilds/${guild_id}/roles`); 211 | }, 212 | 213 | /** 214 | * 用于在 guild_id 指定的频道下创建一个身份组。 215 | */ 216 | createGuildRole(guild_id: string, params: GuildRoleParams): Promise<Result<CreateGuildRole>> { 217 | return request.post(`/guilds/${guild_id}/roles`, params); 218 | }, 219 | 220 | /** 221 | * 修改频道 guild_id 下 role_id 指定的身份组。 222 | */ 223 | updateGuildRole( 224 | guild_id: string, 225 | role_id: string, 226 | params: GuildRoleParams, 227 | ): Promise<Result<UpdateGuildRole>> { 228 | return request.patch(`/guilds/${guild_id}/roles/${role_id}`, params); 229 | }, 230 | 231 | /** 232 | * 删除频道 guild_id下 role_id 对应的身份组。 233 | */ 234 | deleteGuildRole(guild_id: string, role_id: string): Promise<Result> { 235 | return request.delete(`/guilds/${guild_id}/roles/${role_id}`); 236 | }, 237 | 238 | /** 239 | * 将频道 guild_id 下的用户 user_id 添加到身份组 role_id 。 240 | */ 241 | addGuildMemberRole( 242 | guild_id: string, 243 | user_id: string, 244 | role_id: string, 245 | params?: GuildMemberRoleParams, 246 | ): Promise<Result> { 247 | return request.put(`/guilds/${guild_id}/members/${user_id}/roles/${role_id}`, params); 248 | }, 249 | 250 | /** 251 | * 将用户 user_id 从 频道 guild_id 的 role_id 身份组中移除。 252 | */ 253 | deleteGuildMemberRole( 254 | guild_id: string, 255 | user_id: string, 256 | role_id: string, 257 | params?: GuildMemberRoleParams, 258 | ): Promise<Result> { 259 | return request.delete(`/guilds/${guild_id}/members/${user_id}/roles/${role_id}`, params); 260 | }, 261 | 262 | /** 263 | * 用于获取机器人在频道 guild_id 内可以使用的权限列表。 264 | */ 265 | getGuildApiPermissions(guild_id: string): Promise<Result<GuildApiPermissions>> { 266 | return request.get(`/guilds/${guild_id}/api_permission`); 267 | }, 268 | 269 | /** 270 | * 发送机器人在频道接口权限的授权链接。 271 | */ 272 | sendGuildApiPermissionDemand( 273 | guild_id: string, 274 | params: SendGuildApiPermissionDemandParams, 275 | ): Promise<Result<ApiPermissionDemand>> { 276 | return request.post(`/guilds/${guild_id}/api_permission/demand`, params); 277 | }, 278 | 279 | /** 280 | * 用于获取机器人在频道 guild_id 内的消息频率设置。 281 | */ 282 | getGuildMessageSetting(guild_id: string): Promise<Result<MessageSetting>> { 283 | return request.get(`/guilds/${guild_id}/message/setting`); 284 | }, 285 | 286 | /** 287 | * 用于将频道的全体成员(非管理员)禁言。 288 | */ 289 | guildMute(guild_id: string, params: GuildMuteParams): Promise<Result> { 290 | return request.patch(`/guilds/${guild_id}/mute`, params); 291 | }, 292 | 293 | /** 294 | * 用于禁言频道 guild_id 下的成员 user_id。 295 | */ 296 | guildMemberMute(guild_id: string, user_id: string, params: GuildMuteParams): Promise<Result> { 297 | return request.patch(`/guilds/${guild_id}/members/${user_id}/mute`, params); 298 | }, 299 | 300 | /** 301 | * 用于将频道的指定批量成员(非管理员)禁言。 302 | */ 303 | guildMembersMute(guild_id: string, params: GuildMembersMuteParams): Promise<Result> { 304 | return request.patch(`/guilds/${guild_id}/mute`, params); 305 | }, 306 | 307 | /** 308 | * 用于创建频道全局公告,公告类型分为 消息类型的频道公告 和 推荐子频道类型的频道公告 。 309 | */ 310 | createGuildAnnounce( 311 | guild_id: string, 312 | params: CreateGuildAnnounceParams, 313 | ): Promise<Result<Announce>> { 314 | return request.post(`/guilds/${guild_id}/announces`, params); 315 | }, 316 | 317 | /** 318 | * 用于删除频道 guild_id 下指定 message_id 的全局公告。 319 | */ 320 | deleteGuildAnnounce(guild_id: string, message_id: string): Promise<Result> { 321 | return request.delete(`/guilds/${guild_id}/announces/${message_id}`); 322 | }, 323 | }; 324 | }; 325 | -------------------------------------------------------------------------------- /src/bot/session.ts: -------------------------------------------------------------------------------- 1 | import type { Token } from '@/bot/token'; 2 | 3 | import { RawData, WebSocket } from 'ws'; 4 | import { EventEmitter } from 'node:events'; 5 | import { ClientConfig } from '@/bot/client'; 6 | import { Logger, getLogger } from '@/utils/logger'; 7 | import { objectToString, sleep } from '@/utils/common'; 8 | 9 | enum OpCode { 10 | /** 服务端进行消息推送 */ 11 | Dispatch = 0, 12 | /** 客户端或服务端发送心跳 */ 13 | Heartbeat = 1, 14 | /** 客户端发送鉴权 */ 15 | Identify = 2, 16 | /** 客户端恢复连接 */ 17 | Resume = 6, 18 | /** 服务端通知客户端重新连接 */ 19 | Reconnect = 7, 20 | /** 当 Identify 或 Resume 的时候,如果参数有错,服务端会返回该消息 */ 21 | InvalidSession = 9, 22 | /** 当客户端与网关建立 ws 连接之后,网关下发的第一条消息 */ 23 | Hello = 10, 24 | /** 当发送心跳成功之后,就会收到该消息 */ 25 | HeartbeatAck = 11, 26 | /** 仅用于 http 回调模式的回包,代表机器人收到了平台推送的数据 */ 27 | HttpCallbackAck = 12, 28 | } 29 | 30 | enum DispatchType { 31 | READY = 'READY', 32 | RESUMED = 'RESUMED', 33 | } 34 | 35 | /** 事件类型 */ 36 | enum Intent { 37 | GUILDS = 1 << 0, 38 | GUILD_MEMBERS = 1 << 1, 39 | GUILD_MESSAGES = 1 << 9, 40 | GUILD_MESSAGE_REACTIONS = 1 << 10, 41 | DIRECT_MESSAGE = 1 << 12, 42 | GROUP_AND_C2C_EVENT = 1 << 25, 43 | INTERACTION = 1 << 26, 44 | MESSAGE_AUDIT = 1 << 27, 45 | FORUMS_EVENT = 1 << 28, 46 | AUDIO_ACTION = 1 << 29, 47 | PUBLIC_GUILD_MESSAGES = 1 << 30, 48 | } 49 | 50 | /** 消息推送数据 */ 51 | interface DispatchPayload { 52 | op: OpCode.Dispatch; 53 | /** 消息序列号 */ 54 | s: number; 55 | } 56 | 57 | export interface ReadyData { 58 | version: number; 59 | session_id: string; 60 | user: { 61 | id: string; 62 | username: string; 63 | bot: boolean; 64 | status: number; 65 | }; 66 | shard: number[]; 67 | } 68 | 69 | interface ReadyDispatchPayload extends DispatchPayload { 70 | t: DispatchType.READY; 71 | d: ReadyData; 72 | } 73 | 74 | export type ResumedData = ''; 75 | 76 | interface ResumedDispatchPayload extends DispatchPayload { 77 | t: DispatchType.RESUMED; 78 | d: ResumedData; 79 | } 80 | 81 | /** 消息推送 */ 82 | type AllDispatchPayload = ReadyDispatchPayload | ResumedDispatchPayload; 83 | 84 | /** 心跳数据 */ 85 | interface HeartbeatPayload { 86 | op: OpCode.Heartbeat; 87 | /** 客户端收到的最新的消息的 s,如果是首次连接,值为 `null` */ 88 | d: number | null; 89 | } 90 | 91 | /** 鉴权数据 */ 92 | interface IdentifyPayload { 93 | op: OpCode.Identify; 94 | d: { 95 | token: string; 96 | intents: number; 97 | shard: number[]; 98 | properties: Record<string, unknown>; 99 | }; 100 | } 101 | 102 | /** 恢复连接数据 */ 103 | interface ResumePayload { 104 | op: OpCode.Resume; 105 | d: { 106 | seq: number; 107 | session_id: string; 108 | token: string; 109 | }; 110 | } 111 | 112 | /** 等待重连数据 */ 113 | interface ReconnectPayload { 114 | op: OpCode.Reconnect; 115 | } 116 | 117 | /** 参数错误数据 */ 118 | interface InvalidSessionPayload { 119 | op: OpCode.InvalidSession; 120 | d: boolean; 121 | } 122 | 123 | /** 首次连接数据 */ 124 | interface HelloPayload { 125 | op: OpCode.Hello; 126 | d: { 127 | heartbeat_interval: number; 128 | }; 129 | } 130 | 131 | /** 心跳回包数据 */ 132 | interface HeartbeatAckPayload { 133 | op: OpCode.HeartbeatAck; 134 | } 135 | 136 | type Payload = 137 | | AllDispatchPayload 138 | | HeartbeatPayload 139 | | IdentifyPayload 140 | | ResumePayload 141 | | ReconnectPayload 142 | | InvalidSessionPayload 143 | | HelloPayload 144 | | HeartbeatAckPayload; 145 | 146 | class SessionError extends Error { 147 | constructor(message: string) { 148 | super(message); 149 | this.name = 'SessionError'; 150 | } 151 | } 152 | 153 | export type IntentEvent = keyof typeof Intent; 154 | 155 | export interface DispatchData { 156 | t: string; 157 | d: any; 158 | } 159 | 160 | interface SessionEvent { 161 | dispatch: (data: DispatchData) => void; 162 | } 163 | 164 | export interface Session extends EventEmitter { 165 | addListener<T extends keyof SessionEvent>(event: T, listener: SessionEvent[T]): this; 166 | on<T extends keyof SessionEvent>(event: T, listener: SessionEvent[T]): this; 167 | once<T extends keyof SessionEvent>(event: T, listener: SessionEvent[T]): this; 168 | removeListener<T extends keyof SessionEvent>(event: T, listener: SessionEvent[T]): this; 169 | off<T extends keyof SessionEvent>(event: T, listener: SessionEvent[T]): this; 170 | removeAllListeners<T extends keyof SessionEvent>(event?: T): this; 171 | listeners<T extends keyof SessionEvent>(event: T): Function[]; 172 | rawListeners<T extends keyof SessionEvent>(event: T): Function[]; 173 | emit<T extends keyof SessionEvent>(event: T, ...args: Parameters<SessionEvent[T]>): boolean; 174 | listenerCount<T extends keyof SessionEvent>(event: T, listener?: SessionEvent[T]): number; 175 | prependListener<T extends keyof SessionEvent>(event: T, listener: SessionEvent[T]): this; 176 | prependOnceListener<T extends keyof SessionEvent>(event: T, listener: SessionEvent[T]): this; 177 | eventNames<T extends keyof SessionEvent>(): T[]; 178 | } 179 | 180 | export class Session extends EventEmitter { 181 | private ackTimeout: NodeJS.Timeout | null; 182 | /** 心跳间隔 */ 183 | private heartbeat_interval!: number; 184 | /** 是否重连 */ 185 | private is_reconnect: boolean; 186 | /** 记录器 */ 187 | private logger: Logger; 188 | /** 重连计数 */ 189 | private retry: number; 190 | /** 最大重连数 */ 191 | private max_retry: number; 192 | /** 消息序列号 */ 193 | private seq: number; 194 | /** 会话 id */ 195 | private session_id: string | null; 196 | private ws: WebSocket | null; 197 | 198 | constructor(private config: ClientConfig, private token: Token) { 199 | super(); 200 | 201 | this.ackTimeout = null; 202 | this.is_reconnect = false; 203 | this.logger = getLogger(config.appid); 204 | this.retry = 0; 205 | this.max_retry = config.max_retry!; 206 | this.seq = 0; 207 | this.session_id = null; 208 | this.ws = null; 209 | } 210 | 211 | private onOpen(): void { 212 | if (this.retry) { 213 | this.retry = 0; 214 | } 215 | this.logger.debug('连接 socket 成功'); 216 | } 217 | 218 | private async onClose(code: number): Promise<void> { 219 | clearTimeout(<NodeJS.Timeout | undefined>this.ackTimeout); 220 | this.ackTimeout = null; 221 | this.ws!.removeAllListeners(); 222 | this.logger.debug(`Session Exit Code: ${code}.`); 223 | 224 | if (!this.is_reconnect) { 225 | this.ws = null; 226 | this.logger.info('会话连接已关闭'); 227 | return; 228 | } 229 | this.logger.warn('会话连接已被中断'); 230 | await this.token.renew(); 231 | this.reconnect(); 232 | } 233 | 234 | private onError(error: Error): void { 235 | this.logger.fatal(error); 236 | } 237 | 238 | private onMessage(data: RawData): void { 239 | const payload = <Payload>JSON.parse(data.toString()); 240 | this.logger.debug(`收到 payload 数据: ${objectToString(payload)}`); 241 | 242 | switch (payload.op) { 243 | case OpCode.Dispatch: 244 | this.onDispatch(payload); 245 | break; 246 | case OpCode.Reconnect: 247 | this.logger.info('当前会话已失效,等待断开后自动重连'); 248 | this.ws!.close(); 249 | break; 250 | case OpCode.InvalidSession: 251 | this.logger.error('发送的 payload 参数有误'); 252 | throw new SessionError('The Payload parameter sent is incorrect.'); 253 | case OpCode.Hello: 254 | this.heartbeat_interval = payload.d.heartbeat_interval; 255 | this.is_reconnect ? this.sendResumePayload() : this.sendAuthPayload(); 256 | break; 257 | case OpCode.HeartbeatAck: 258 | this.ackTimeout = setTimeout(() => this.heartbeat(), this.heartbeat_interval); 259 | break; 260 | } 261 | } 262 | 263 | private onDispatch(payload: AllDispatchPayload): void { 264 | const { d, s, t } = payload; 265 | this.seq = s; 266 | 267 | switch (t) { 268 | case DispatchType.READY: 269 | const { session_id } = d; 270 | 271 | this.session_id = session_id; 272 | this.logger.mark(`Hello, ${d.user.username}`); 273 | case DispatchType.RESUMED: 274 | this.logger.trace('开始发送心跳...'); 275 | this.heartbeat(); 276 | break; 277 | } 278 | const dispatch: DispatchData = { 279 | t, 280 | d, 281 | }; 282 | 283 | this.emit('dispatch', dispatch); 284 | } 285 | 286 | private heartbeat(): void { 287 | const payload: HeartbeatPayload = { 288 | op: OpCode.Heartbeat, 289 | d: this.seq, 290 | }; 291 | this.sendPayload(payload); 292 | } 293 | 294 | private sendPayload(payload: Payload): void { 295 | try { 296 | const data = objectToString(payload); 297 | 298 | this.ws!.send(data); 299 | this.logger.debug(`发送 payload 数据: ${data}`); 300 | } catch (error) { 301 | this.logger.error(error); 302 | } 303 | } 304 | 305 | private getIntents(): number { 306 | const events = this.config.events; 307 | const intents = events.reduce((previous, current) => previous | Intent[current], 0); 308 | 309 | return intents; 310 | } 311 | 312 | private sendAuthPayload(): void { 313 | const payload: IdentifyPayload = { 314 | op: OpCode.Identify, 315 | d: { 316 | token: this.token.authorization, 317 | intents: this.getIntents(), 318 | shard: this.config.shard!, 319 | // TODO: /人◕ ‿‿ ◕人\ 320 | properties: {}, 321 | }, 322 | }; 323 | 324 | this.is_reconnect = true; 325 | this.sendPayload(payload); 326 | } 327 | 328 | private sendResumePayload(): void { 329 | const payload: ResumePayload = { 330 | op: OpCode.Resume, 331 | d: { 332 | token: this.token.authorization, 333 | seq: this.seq, 334 | session_id: this.session_id!, 335 | }, 336 | }; 337 | this.sendPayload(payload); 338 | } 339 | 340 | private async reconnect(): Promise<void> { 341 | if (this.retry === this.max_retry) { 342 | this.logger.error('重连失败,请检查网络和配置。'); 343 | throw new SessionError('Reached the maximum number of reconnection attempts.'); 344 | } 345 | this.retry++; 346 | 347 | try { 348 | this.logger.info(`尝试重连... x${this.retry}`); 349 | await sleep(this.retry * 3000); 350 | this.connect(this.ws!.url); 351 | } catch (error) { 352 | this.reconnect(); 353 | } 354 | } 355 | 356 | public connect(url: string): void { 357 | if (this.ws && this.ws.readyState === WebSocket.OPEN) { 358 | this.logger.warn('已建立会话通信,不要重复连接。'); 359 | return; 360 | } 361 | this.logger.trace('开始建立 ws 通信...'); 362 | 363 | const ws = new WebSocket(url); 364 | 365 | ws.on('open', () => this.onOpen()); 366 | ws.on('close', code => this.onClose(code)); 367 | ws.on('error', error => this.onError(error)); 368 | ws.on('message', data => this.onMessage(data)); 369 | 370 | this.ws = ws; 371 | } 372 | 373 | public disconnect(): void { 374 | if (!this.ws) { 375 | this.logger.warn('未建立会话通信,无效的操作。'); 376 | return; 377 | } 378 | this.is_reconnect = false; 379 | 380 | this.logger.trace('正在断开 ws 通信...'); 381 | this.ws.close(); 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /src/bot/client.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from '@/model/message'; 2 | import type { ClientEvent, C2cMessageCreate, GroupAtMessageCreate } from '@/bot/event'; 3 | 4 | import { EventEmitter } from 'node:events'; 5 | import { generateApi } from '@/api'; 6 | import { UserMessage, SendUserMessageParams } from '@/api/users'; 7 | import { GroupMessage, SendGroupsMessageParams } from '@/api/groups'; 8 | import { SendChannelMessageParams } from '@/api/channels'; 9 | import { Token } from '@/bot/token'; 10 | import { DispatchData, IntentEvent, Session } from '@/bot/session'; 11 | import { deepAssign, objectToString, parseError } from '@/utils/common'; 12 | import { Request, RequestError, Result } from '@/utils/request'; 13 | import { LogLevel, Logger, createLogger } from '@/utils/logger'; 14 | 15 | /** 客户端配置项 */ 16 | export interface ClientConfig { 17 | /** 机器人 ID */ 18 | appid: string; 19 | /** 机器人令牌 */ 20 | token: string; 21 | /** 机器人密钥 */ 22 | secret: string; 23 | /** 分片,默认 `[0, 1]` */ 24 | shard?: number[]; 25 | /** 订阅事件 */ 26 | events: IntentEvent[]; 27 | /** 是否开启沙盒,默认 `false` */ 28 | sandbox?: boolean; 29 | /** 掉线重连数,默认 `3` */ 30 | max_retry?: number; 31 | /** 日志等级,默认 `'INFO'` */ 32 | log_level?: LogLevel; 33 | } 34 | 35 | type Api = ReturnType<typeof generateApi>; 36 | 37 | class ClientError extends Error { 38 | constructor(message: string) { 39 | super(message); 40 | this.name = 'ClientError'; 41 | } 42 | } 43 | 44 | type EventInterceptor = (dispatch: DispatchData) => DispatchData | Promise<DispatchData>; 45 | 46 | export interface Client extends EventEmitter { 47 | addListener<T extends keyof ClientEvent>(event: T, listener: ClientEvent[T]): this; 48 | addListener(event: string | symbol, listener: (...args: unknown[]) => void): this; 49 | 50 | on<T extends keyof ClientEvent>(event: T, listener: ClientEvent[T]): this; 51 | on(event: string | symbol, listener: (...args: unknown[]) => void): this; 52 | 53 | once<T extends keyof ClientEvent>(event: T, listener: ClientEvent[T]): this; 54 | once(event: string | symbol, listener: (...args: unknown[]) => void): this; 55 | 56 | removeListener<T extends keyof ClientEvent>(event: T, listener: ClientEvent[T]): this; 57 | removeListener(event: string | symbol, listener: (...args: any[]) => void): this; 58 | 59 | off<T extends keyof ClientEvent>(event: T, listener: ClientEvent[T]): this; 60 | off(event: string | symbol, listener: (...args: unknown[]) => void): this; 61 | 62 | removeAllListeners<T extends keyof ClientEvent>(event?: T): this; 63 | removeAllListeners(event?: string | symbol): this; 64 | 65 | listeners<T extends keyof ClientEvent>(event: T): Function[]; 66 | listeners(event: string | symbol): Function[]; 67 | 68 | rawListeners<T extends keyof ClientEvent>(event: T): Function[]; 69 | rawListeners(event: string | symbol): Function[]; 70 | 71 | emit<T extends keyof ClientEvent>(event: T, ...args: Parameters<ClientEvent[T]>): boolean; 72 | emit(event: string | symbol, ...args: any[]): boolean; 73 | 74 | listenerCount<T extends keyof ClientEvent>(event: T, listener?: ClientEvent[T]): number; 75 | listenerCount(event: string | symbol, listener?: Function): number; 76 | 77 | prependListener<T extends keyof ClientEvent>(event: T, listener: ClientEvent[T]): this; 78 | prependListener(event: string | symbol, listener: (...args: any[]) => void): this; 79 | 80 | prependOnceListener<T extends keyof ClientEvent>(event: T, listener: ClientEvent[T]): this; 81 | prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; 82 | 83 | eventNames<T extends keyof ClientEvent>(): T[]; 84 | eventNames(): Array<string | symbol>; 85 | } 86 | 87 | export class Client extends EventEmitter { 88 | public logger: Logger; 89 | public api: Api; 90 | public request: Request; 91 | 92 | private token: Token; 93 | private session: Session; 94 | private eventInterceptors: EventInterceptor[]; 95 | 96 | constructor(public config: ClientConfig) { 97 | super(); 98 | 99 | config.sandbox ??= false; 100 | config.shard ??= [0, 1]; 101 | config.max_retry ??= 3; 102 | config.log_level ??= 'INFO'; 103 | 104 | this.logger = createLogger(config.appid, config.log_level); 105 | this.checkConfig(); 106 | this.logger.mark('Client is initializing...'); 107 | 108 | this.api = this.createApi(); 109 | this.request = this.createRequest(); 110 | this.token = new Token(config); 111 | this.session = new Session(config, this.token); 112 | this.eventInterceptors = []; 113 | 114 | this.useEventInterceptor(dispatch => { 115 | const { t, d } = dispatch; 116 | 117 | switch (t) { 118 | case 'GUILD_MEMBER_ADD': 119 | case 'GUILD_MEMBER_UPDATE': 120 | case 'GUILD_MEMBER_REMOVE': 121 | case 'MESSAGE_REACTION_ADD': 122 | case 'MESSAGE_REACTION_REMOVE': 123 | case 'FORUM_THREAD_CREATE': 124 | case 'FORUM_THREAD_UPDATE': 125 | case 'FORUM_THREAD_DELETE': 126 | case 'FORUM_POST_CREATE': 127 | case 'FORUM_POST_DELETE': 128 | case 'FORUM_REPLY_CREATE': 129 | case 'FORUM_REPLY_DELETE': 130 | d.reply = (params: SendChannelMessageParams): Promise<Result<Message>> => { 131 | return this.api.sendChannelMessage(d.channel_id, { 132 | event_id: d.id, 133 | ...params, 134 | }); 135 | }; 136 | break; 137 | // case 'INTERACTION_CREATE': 138 | case 'GROUP_ADD_ROBOT': 139 | case 'GROUP_MSG_RECEIVE': 140 | d.reply = (params: SendGroupsMessageParams): Promise<Result<GroupMessage>> => { 141 | return this.api.sendGroupMessage(d.group_openid, { 142 | event_id: d.id, 143 | ...params, 144 | }); 145 | }; 146 | break; 147 | case 'MESSAGE_CREATE': 148 | case 'AT_MESSAGE_CREATE': 149 | d.reply = (params: SendChannelMessageParams): Promise<Result<Message>> => { 150 | return this.api.sendChannelMessage(d.channel_id, { 151 | msg_id: d.id, 152 | ...params, 153 | }); 154 | }; 155 | break; 156 | case 'DIRECT_MESSAGE_CREATE': 157 | d.reply = (params: SendChannelMessageParams): Promise<Result<Message>> => { 158 | return this.api.sendDmMessage(d.guild_id, { 159 | msg_id: d.id, 160 | ...params, 161 | }); 162 | }; 163 | break; 164 | case 'GROUP_AT_MESSAGE_CREATE': 165 | d.reply = (params: SendGroupsMessageParams): Promise<Result<GroupMessage>> => { 166 | return this.api.sendGroupMessage(d.group_openid, { 167 | msg_id: d.id, 168 | ...params, 169 | }); 170 | }; 171 | break; 172 | case 'C2C_MESSAGE_CREATE': 173 | d.reply = (params: SendUserMessageParams): Promise<Result<UserMessage>> => { 174 | return this.api.sendUserMessage(d.author.user_openid, { 175 | msg_id: d.id, 176 | ...params, 177 | }); 178 | }; 179 | break; 180 | } 181 | return dispatch; 182 | }); 183 | } 184 | 185 | /** 186 | * 机器人上线。 187 | */ 188 | public async online(): Promise<void> { 189 | const { data } = await this.api.getGateway(); 190 | 191 | this.session.connect(data.url); 192 | this.session.on('dispatch', data => this.onDispatch(data)); 193 | 194 | this.on('c2c.message.create', this.onMessage); 195 | this.on('group.at.message.create', this.onMessage); 196 | this.on('direct.message.create', this.onMessage); 197 | this.on('at.message.create', this.onMessage); 198 | } 199 | 200 | /** 201 | * 机器人下线。 202 | */ 203 | public offline(): void { 204 | this.session.disconnect(); 205 | this.session.removeAllListeners('dispatch'); 206 | 207 | this.off('c2c.message.create', this.onMessage); 208 | this.off('group.at.message.create', this.onMessage); 209 | this.off('direct.message.create', this.onMessage); 210 | this.off('at.message.create', this.onMessage); 211 | } 212 | 213 | /** 214 | * 添加事件拦截器。 215 | */ 216 | public useEventInterceptor(interceptor: EventInterceptor) { 217 | this.eventInterceptors.push(interceptor); 218 | } 219 | 220 | private async onDispatch(dispatch: DispatchData) { 221 | for (const interceptor of this.eventInterceptors) { 222 | try { 223 | dispatch = await interceptor(dispatch); 224 | } catch (error) { 225 | const message = parseError(error); 226 | 227 | this.logger.error(message); 228 | throw new ClientError(message); 229 | } 230 | } 231 | const { t, d } = dispatch; 232 | const data = { 233 | t, 234 | ...d, 235 | }; 236 | const events = t.toLowerCase().split('_'); 237 | 238 | // 不存在下划线就是 session 自身的事件,例如 READY、RESUMED 239 | if (events.length === 1) { 240 | events.unshift('session'); 241 | } 242 | 243 | do { 244 | const event = events.join('.'); 245 | 246 | this.emit(event, data); 247 | this.logger.debug(`推送 "${event}" 事件`); 248 | events.pop(); 249 | } while (events.length); 250 | } 251 | 252 | private onMessage(message: C2cMessageCreate | GroupAtMessageCreate | Message) { 253 | const { attachments, content } = message; 254 | const text = attachments ? `<attachment>${content}` : content; 255 | 256 | this.logger.info(`Received message: "${text}"`); 257 | } 258 | 259 | private checkConfig() { 260 | if (!this.config.events.length) { 261 | const wiki = 262 | 'https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/event-emit.html#%E4%BA%8B%E4%BB%B6%E8%AE%A2%E9%98%85Intents'; 263 | 264 | this.logger.error(`检测到 events 为空,请查阅相关文档:${wiki}`); 265 | throw new ClientError('Events cannot be empty.'); 266 | } else if ( 267 | !Array.isArray(this.config.shard) || 268 | this.config.shard.length !== 2 || 269 | this.config.shard[0] >= this.config.shard[1] 270 | ) { 271 | const wiki = 272 | 'https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/event-emit.html'; 273 | 274 | this.logger.error(`检测到 shard 配置错误,请查阅相关文档:${wiki}`); 275 | throw new ClientError('Shard configuration is incorrect.'); 276 | } 277 | } 278 | 279 | private createApi(): Api { 280 | const request = new Request(); 281 | const origin = this.config.sandbox 282 | ? 'https://sandbox.api.sgroup.qq.com' 283 | : 'https://api.sgroup.qq.com'; 284 | 285 | request.useRequestInterceptor(async config => { 286 | this.logger.trace('开始调用接口请求...'); 287 | await this.token.renew(); 288 | 289 | deepAssign(config, { 290 | origin, 291 | headers: { 292 | 'Authorization': this.token.authorization, 293 | 'X-Union-Appid': this.config.appid, 294 | }, 295 | }); 296 | this.logger.debug(`API Request: ${objectToString(config)}`); 297 | 298 | return config; 299 | }); 300 | 301 | request.useResponseInterceptor<undefined | { code: number; message: string }>(result => { 302 | const { data } = result; 303 | this.logger.debug(`API Response: ${objectToString(data)}`); 304 | 305 | if (data?.code) { 306 | this.logger.error(`API Code: ${data.code}, ${data.message}.`); 307 | throw new RequestError(`Code ${data.code}, ${data.message}.`); 308 | } 309 | return result; 310 | }); 311 | return generateApi(request); 312 | } 313 | 314 | private createRequest(): Request { 315 | const request = new Request(); 316 | 317 | request.useRequestInterceptor(config => { 318 | this.logger.trace('开始发起网络请求...'); 319 | this.logger.debug(`HTTP Request: ${objectToString(config)}`); 320 | 321 | return config; 322 | }); 323 | 324 | request.useResponseInterceptor(result => { 325 | this.logger.debug(`HTTP Response: ${objectToString(result.data)}`); 326 | return result; 327 | }); 328 | return request; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/bot/event.ts: -------------------------------------------------------------------------------- 1 | import type { ReadyData, ResumedData } from '@/bot/session'; 2 | import type { Guild } from '@/model/guild'; 3 | import type { Channel } from '@/model/channel'; 4 | import type { AudioAction } from '@/model/audio'; 5 | import type { MemberWithGuildID } from '@/model/member'; 6 | import type { AuditResult, Post, Reply, Thread } from '@/model/forum'; 7 | import type { Message, MessageAudited, MessageReaction } from '@/model/message'; 8 | import { Result } from '@/utils/request'; 9 | import { SendChannelMessageParams } from '@/api/channels'; 10 | import { GroupMessage, SendGroupsMessageParams } from '@/api/groups'; 11 | import { UserMessage, SendUserMessageParams } from '@/api/users'; 12 | 13 | export type SessionReady = ReadyData & { t: 'READY' }; 14 | export type SessionResumed = ResumedData & { t: 'RESUMED' }; 15 | 16 | export type GuildCreate = Guild & { 17 | t: 'GUILD_CREATE'; 18 | op_user_id: string; 19 | }; 20 | export type GuildUpdate = Guild & { 21 | t: 'GUILD_UPDATE'; 22 | op_user_id: string; 23 | }; 24 | export type GuildDelete = Guild & { 25 | t: 'GUILD_DELETE'; 26 | op_user_id: string; 27 | }; 28 | export type ChannelCreate = Channel & { 29 | t: 'CHANNEL_CREATE'; 30 | op_user_id: string; 31 | }; 32 | export type ChannelUpdate = Channel & { 33 | t: 'CHANNEL_UPDATE'; 34 | op_user_id: string; 35 | }; 36 | export type ChannelDelete = Channel & { 37 | t: 'CHANNEL_DELETE'; 38 | op_user_id: string; 39 | }; 40 | 41 | export type GuildMemberAdd = MemberWithGuildID & { 42 | t: 'GUILD_MEMBER_ADD'; 43 | op_user_id: string; 44 | }; 45 | export type GuildMemberUpdate = MemberWithGuildID & { 46 | t: 'GUILD_MEMBER_UPDATE'; 47 | op_user_id: string; 48 | }; 49 | export type GuildMemberRemove = MemberWithGuildID & { 50 | t: 'GUILD_MEMBER_REMOVE'; 51 | op_user_id: string; 52 | }; 53 | 54 | export type MessageCreate = Message & { 55 | /** 事件类型 */ 56 | t: 'MESSAGE_CREATE'; 57 | /** 快捷回复 */ 58 | reply: (params: SendChannelMessageParams) => Promise<Result<Message>>; 59 | }; 60 | export type MessageDelete = { t: 'MESSAGE_DELETE'; [key: string]: unknown }; 61 | 62 | export type MessageReactionAdd = { t: 'MESSAGE_REACTION_ADD'; [key: string]: unknown }; 63 | export type MessageReactionRemove = MessageReaction & { t: 'MESSAGE_REACTION_REMOVE' }; 64 | 65 | export type DirectMessageCreate = Message & { 66 | /** 事件类型 */ 67 | t: 'DIRECT_MESSAGE_CREATE'; 68 | /** 快捷回复 */ 69 | reply: (params: SendChannelMessageParams) => Promise<Result<Message>>; 70 | }; 71 | export type DirectMessageDelete = { t: 'DIRECT_MESSAGE_DELETE'; [key: string]: unknown }; 72 | 73 | export interface InteractionCreate { 74 | /** 事件类型 */ 75 | t: 'INTERACTION_CREATE'; 76 | /** 平台方事件 ID,可以用于被动消息发送 */ 77 | id: string; 78 | /** 按钮事件固定是 11 */ 79 | type: 11; 80 | /** 消息内容 */ 81 | // TODO: /人◕ ‿‿ ◕人\ 文档这里写错了,咱也不知道是啥字段 82 | // 'chat_type': number; 83 | /** 消息生产时间 */ 84 | timestamp: string; 85 | /** 频道的 openid */ 86 | guild_id: string; 87 | /** 文字子频道的 openid */ 88 | channel_id: string; 89 | /** 群聊的 openid */ 90 | group_open_id: string; 91 | /** 目前只有群和单聊有该字段,1 群聊,2 单聊,后续加入 3 频道 */ 92 | chat_type: 1 | 2 | 3; 93 | data: { 94 | resolved: { 95 | /** 操作按钮的 data 字段值【在发送按钮时规划】 */ 96 | button_data: string; 97 | /** 操作按钮的 id 字段值【在发送按钮时规划】 */ 98 | 99 | button_id: string; 100 | /** 操作的用户 openid */ 101 | user_id: string; 102 | /** 操作的消息 id */ 103 | 104 | message_id: string; 105 | }; 106 | }; 107 | /** 默认 1 */ 108 | version: number; 109 | /** 机器人的 appid */ 110 | application_id: string; 111 | } 112 | 113 | export type MessageAuditPass = MessageAudited & { t: 'MESSAGE_AUDIT_PASS' }; 114 | export type MessageAuditReject = MessageAudited & { t: 'MESSAGE_AUDIT_REJECT' }; 115 | 116 | export type AudioStart = AudioAction & { t: 'AUDIO_START' }; 117 | export type AudioFinish = AudioAction & { t: 'AUDIO_FINISH' }; 118 | export type AudioOnMic = AudioAction & { t: 'AUDIO_ON_MIC' }; 119 | export type AudioOffMic = AudioAction & { t: 'AUDIO_OFF_MIC' }; 120 | 121 | export type ForumThreadCreate = Thread & { t: 'FORUM_THREAD_CREATE' }; 122 | export type ForumThreadUpdate = Thread & { t: 'FORUM_THREAD_UPDATE' }; 123 | export type ForumThreadDelete = Thread & { t: 'FORUM_THREAD_DELETE' }; 124 | export type ForumPostCreate = Post & { t: 'FORUM_POST_CREATE' }; 125 | export type ForumPostDelete = Post & { t: 'FORUM_POST_DELETE' }; 126 | export type ForumReplyCreate = Reply & { t: 'FORUM_REPLY_CREATE' }; 127 | export type ForumReplyDelete = Reply & { t: 'FORUM_REPLY_DELETE' }; 128 | export type ForumPublishAuditResult = AuditResult & { t: 'FORUM_PUBLISH_AUDIT_RESULT' }; 129 | 130 | export type AtMessageCreate = Message & { 131 | /** 事件类型 */ 132 | t: 'AT_MESSAGE_CREATE'; 133 | /** 快捷回复 */ 134 | reply: (params: SendChannelMessageParams) => Promise<Result<Message>>; 135 | }; 136 | export type PublicMessageDelete = { t: 'PUBLIC_MESSAGE_DELETE'; [key: string]: unknown }; 137 | 138 | export interface GroupAddRobot { 139 | /** 事件类型 */ 140 | t: 'GROUP_ADD_ROBOT'; 141 | /** 加入的时间戳 */ 142 | timestamp: string; 143 | /** 加入群的群 openid */ 144 | group_openid: string; 145 | /** 操作添加机器人进群的群成员 openid */ 146 | op_member_openid: string; 147 | /** 快捷回复 */ 148 | reply: (params: SendGroupsMessageParams) => Promise<Result<GroupMessage>>; 149 | } 150 | 151 | export interface GroupDelRobot { 152 | /** 事件类型 */ 153 | t: 'GROUP_DEL_ROBOT'; 154 | /** 移除的时间戳 */ 155 | timestamp: string; 156 | /** 移除群的群 openid */ 157 | group_openid: string; 158 | /** 操作移除机器人退群的群成员 openid */ 159 | op_member_openid: string; 160 | /** 快捷回复 */ 161 | reply: (params: SendGroupsMessageParams) => Promise<Result<GroupMessage>>; 162 | } 163 | 164 | export interface GroupMessageReject { 165 | /** 事件类型 */ 166 | t: 'GROUP_MSG_REJECT'; 167 | /** 操作的时间戳 */ 168 | timestamp: string; 169 | /** 操作群的群 openid */ 170 | group_openid: string; 171 | /** 操作群成员的 openid */ 172 | op_member_openid: string; 173 | } 174 | 175 | export interface GroupMessageReceive { 176 | /** 事件类型 */ 177 | t: 'GROUP_MSG_RECEIVE'; 178 | /** 操作的时间戳 */ 179 | timestamp: string; 180 | /** 操作群的群 openid */ 181 | group_openid: string; 182 | /** 操作群成员的 openid */ 183 | op_member_openid: string; 184 | } 185 | 186 | export interface FriendAdd { 187 | /** 事件类型 */ 188 | t: 'FRIEND_ADD'; 189 | /** 添加时间戳 */ 190 | timestamp: string; 191 | /** 用户 openid */ 192 | openid: string; 193 | } 194 | 195 | export interface FriendDel { 196 | /** 事件类型 */ 197 | t: 'FRIEND_DEL'; 198 | /** 删除时间戳 */ 199 | timestamp: string; 200 | /** 用户 openid */ 201 | openid: string; 202 | } 203 | 204 | export interface C2cMsgReject { 205 | /** 事件类型 */ 206 | t: 'C2C_MSG_REJECT'; 207 | /** 操作时间戳 */ 208 | timestamp: string; 209 | /** 用户 openid */ 210 | openid: string; 211 | } 212 | 213 | export interface C2cMsgReceive { 214 | /** 事件类型 */ 215 | t: 'C2C_MSG_RECEIVE'; 216 | /** 操作时间戳 */ 217 | timestamp: string; 218 | /** 用户 openid */ 219 | openid: string; 220 | } 221 | 222 | export interface C2cMessageCreate { 223 | /** 事件类型 */ 224 | t: 'C2C_MESSAGE_CREATE'; 225 | /** 平台方消息ID,可以用于被动消息发送 */ 226 | id: string; 227 | /** 发送者 */ 228 | author: { 229 | /** 用户 openid */ 230 | user_openid: string; 231 | }; 232 | /** 文本消息内容 */ 233 | content: string; 234 | /** 消息生产时间 */ 235 | timestamp: string; 236 | /** 富媒体文件附件 */ 237 | attachments: object[]; 238 | /** 快捷回复 */ 239 | reply: (params: SendUserMessageParams) => Promise<Result<UserMessage>>; 240 | } 241 | 242 | export interface GroupAtMessageCreate { 243 | /** 事件类型 */ 244 | t: 'GROUP_AT_MESSAGE_CREATE'; 245 | /** 平台方消息ID,可以用于被动消息发送 */ 246 | id: string; 247 | /** 发送者 */ 248 | author: { 249 | id: string; 250 | /** 用户 openid */ 251 | member_openid: string; 252 | }; 253 | /** 消息内容 */ 254 | content: string; 255 | /** 消息生产时间 */ 256 | timestamp: string; 257 | /** 群聊的 openid */ 258 | group_openid: string; 259 | /** 富媒体文件附件 */ 260 | attachments: object[]; 261 | /** 快捷回复 */ 262 | reply: (params: SendGroupsMessageParams) => Promise<Result<GroupMessage>>; 263 | } 264 | 265 | export interface ClientEvent { 266 | //#region GUILDS 267 | /** 当机器人加入新 guild 时 */ 268 | 'guild.create': (event: GuildCreate) => void; 269 | /** 当 guild 资料发生变更时 */ 270 | 'guild.update': (event: GuildUpdate) => void; 271 | /** 当机器人退出 guild 时 */ 272 | 'guild.delete': (event: GuildDelete) => void; 273 | /** 当 channel 被创建时 */ 274 | 'channel.create': (event: ChannelCreate) => void; 275 | /** 当 channel 被更新时 */ 276 | 'channel.update': (event: ChannelUpdate) => void; 277 | /** 当 channel 被删除时 */ 278 | 'channel.delete': (event: ChannelDelete) => void; 279 | //#endregion 280 | 281 | //#region GUILD_MEMBERS 282 | /** 当成员加入时 */ 283 | 'guild.member.add': (event: GuildMemberAdd) => void; 284 | /** 当成员资料变更时 */ 285 | 'guild.member.update': (event: GuildMemberUpdate) => void; 286 | /** 当成员被移除时 */ 287 | 'guild.member.remove': (event: GuildMemberRemove) => void; 288 | //#endregion 289 | 290 | //#region GUILD_MESSAGES 291 | /** **仅私域**,发送消息事件,代表频道内的全部消息,而不只是 at 机器人的消息。内容与 AT_MESSAGE_CREATE 相同 */ 292 | 'message.create': (event: MessageCreate) => void; 293 | /** **仅私域**,删除(撤回)消息事件 */ 294 | 'message.delete': (event: MessageDelete) => void; 295 | //#endregion 296 | 297 | //#region GUILD_MESSAGE_REACTIONS 298 | /** 为消息添加表情表态 */ 299 | 'message.reaction.add': (event: MessageReactionAdd) => void; 300 | /** 为消息删除表情表态 */ 301 | 'message.reaction.remove': (event: MessageReactionRemove) => void; 302 | //#endregion 303 | 304 | //#region DIRECT_MESSAGE 305 | /** 当收到用户发给机器人的私信消息时 */ 306 | 'direct.message.create': (event: DirectMessageCreate) => void; 307 | /** 删除(撤回)消息事件 */ 308 | 'direct.message.delete': (event: DirectMessageDelete) => void; 309 | //#endregion 310 | 311 | //#region INTERACTION 312 | /** 用户点击了消息体的回调按钮 */ 313 | 'interaction.create': (event: InteractionCreate) => void; 314 | //#endregion 315 | 316 | //#region MESSAGE_AUDIT 317 | /** 消息审核通过 */ 318 | 'message.audit.pass': (event: MessageAuditPass) => void; 319 | /** 消息审核不通过 */ 320 | 'message.audit.reject': (event: MessageAuditReject) => void; 321 | //#endregion 322 | 323 | //#region FORUMS_EVENT 324 | /** **仅私域**,当用户创建主题时 */ 325 | 'forum.thread.create': (event: ForumThreadCreate) => void; 326 | /** **仅私域**,当用户更新主题时 */ 327 | 'forum.thread.update': (event: ForumThreadUpdate) => void; 328 | /** **仅私域**,当用户删除主题时 */ 329 | 'forum.thread.delete': (event: ForumThreadDelete) => void; 330 | 331 | /** **仅私域**,当用户创建帖子时 */ 332 | 'forum.post.create': (event: ForumPostCreate) => void; 333 | /** **仅私域**,当用户删除帖子时 */ 334 | 'forum.post.delete': (event: ForumPostDelete) => void; 335 | 336 | /** **仅私域**,当用户回复评论时 */ 337 | 'forum.reply.create': (event: ForumReplyCreate) => void; 338 | /** **仅私域**,当用户回复评论时 */ 339 | 'forum.reply.delete': (event: ForumReplyDelete) => void; 340 | 341 | /** **仅私域**,当用户发表审核通过时 */ 342 | 'forum.publish.audit.result': (event: ForumPublishAuditResult) => void; 343 | //#endregion 344 | 345 | //#region AUDIO_ACTION 346 | /** 音频开始播放时 */ 347 | 'audio.start': (event: AudioStart) => void; 348 | /** 音频播放结束时 */ 349 | 'audio.finish': (event: AudioFinish) => void; 350 | /** 上麦时 */ 351 | 'audio.on.mic': (event: AudioOnMic) => void; 352 | /** 下麦时 */ 353 | 'audio.off.mic': (event: AudioOffMic) => void; 354 | //#endregion 355 | 356 | //#region PUBLIC_GUILD_MESSAGES 357 | /** **仅公域**,当收到 at 机器人的消息时 */ 358 | 'at.message.create': (event: AtMessageCreate) => void; 359 | /** **仅公域**,当频道的消息被删除时 */ 360 | 'public.message.delete': (event: PublicMessageDelete) => void; 361 | //#endregion 362 | 363 | //#region GROUP_AND_C2C_EVENT 364 | /** 机器人被添加到群聊 */ 365 | 'group.add.robot': (event: GroupAddRobot) => void; 366 | /** 机器人被移出群聊 */ 367 | 'group.del.robot': (event: GroupDelRobot) => void; 368 | 369 | /** 群管理员主动在机器人资料页操作关闭通知 */ 370 | 'group.msg.reject': (event: GroupMessageReject) => void; 371 | /** 群管理员主动在机器人资料页操作开启通知 */ 372 | 'group.msg.receive': (event: GroupMessageReceive) => void; 373 | 374 | /** 用户在群聊 at 机器人发送消息 */ 375 | 'group.at.message.create': (event: GroupAtMessageCreate) => void; 376 | 377 | /** 用户添加机器人'好友'到消息列表 */ 378 | 'friend.add': (event: FriendAdd) => void; 379 | /** 用户删除机器人'好友' */ 380 | 'friend.del': (event: FriendDel) => void; 381 | 382 | /** 用户在机器人资料卡手动关闭 "主动消息" 推送 */ 383 | 'c2c.msg.reject': (event: C2cMsgReject) => void; 384 | /** 用户在机器人资料卡手动开启 "主动消息" 推送开关 */ 385 | 'c2c.msg.receive': (event: C2cMsgReceive) => void; 386 | /** 用户在单聊发送消息给机器人 */ 387 | 'c2c.message.create': (event: C2cMessageCreate) => void; 388 | //#endregion 389 | 390 | // TODO: /人◕ ‿‿ ◕人\ SESSION 文档没提供类型,我暂时只遇到过这俩,待补充 391 | //#region SESSION 392 | /** 连接会话通信 */ 393 | 'session.ready': (event: SessionReady) => void; 394 | /** 重连会话 */ 395 | 'session.resumed': (event: SessionResumed) => void; 396 | //#endregion 397 | } 398 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | log4js: 9 | specifier: ^6.9.1 10 | version: 6.9.1 11 | ws: 12 | specifier: ^8.16.0 13 | version: 8.16.0 14 | 15 | devDependencies: 16 | '@types/node': 17 | specifier: ^20.12.7 18 | version: 20.12.7 19 | '@types/ws': 20 | specifier: ^8.5.10 21 | version: 8.5.10 22 | concurrently: 23 | specifier: ^8.2.2 24 | version: 8.2.2 25 | tsc-alias: 26 | specifier: ^1.8.8 27 | version: 1.8.8 28 | typescript: 29 | specifier: ^5.4.5 30 | version: 5.4.5 31 | 32 | packages: 33 | 34 | /@babel/runtime@7.24.4: 35 | resolution: {integrity: sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==} 36 | engines: {node: '>=6.9.0'} 37 | dependencies: 38 | regenerator-runtime: 0.14.1 39 | dev: true 40 | 41 | /@nodelib/fs.scandir@2.1.5: 42 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 43 | engines: {node: '>= 8'} 44 | dependencies: 45 | '@nodelib/fs.stat': 2.0.5 46 | run-parallel: 1.2.0 47 | dev: true 48 | 49 | /@nodelib/fs.stat@2.0.5: 50 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 51 | engines: {node: '>= 8'} 52 | dev: true 53 | 54 | /@nodelib/fs.walk@1.2.8: 55 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 56 | engines: {node: '>= 8'} 57 | dependencies: 58 | '@nodelib/fs.scandir': 2.1.5 59 | fastq: 1.17.1 60 | dev: true 61 | 62 | /@types/node@20.12.7: 63 | resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} 64 | dependencies: 65 | undici-types: 5.26.5 66 | dev: true 67 | 68 | /@types/ws@8.5.10: 69 | resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} 70 | dependencies: 71 | '@types/node': 20.12.7 72 | dev: true 73 | 74 | /ansi-regex@5.0.1: 75 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 76 | engines: {node: '>=8'} 77 | dev: true 78 | 79 | /ansi-styles@4.3.0: 80 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 81 | engines: {node: '>=8'} 82 | dependencies: 83 | color-convert: 2.0.1 84 | dev: true 85 | 86 | /anymatch@3.1.3: 87 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 88 | engines: {node: '>= 8'} 89 | dependencies: 90 | normalize-path: 3.0.0 91 | picomatch: 2.3.1 92 | dev: true 93 | 94 | /array-union@2.1.0: 95 | resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 96 | engines: {node: '>=8'} 97 | dev: true 98 | 99 | /binary-extensions@2.3.0: 100 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 101 | engines: {node: '>=8'} 102 | dev: true 103 | 104 | /braces@3.0.2: 105 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 106 | engines: {node: '>=8'} 107 | dependencies: 108 | fill-range: 7.0.1 109 | dev: true 110 | 111 | /chalk@4.1.2: 112 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 113 | engines: {node: '>=10'} 114 | dependencies: 115 | ansi-styles: 4.3.0 116 | supports-color: 7.2.0 117 | dev: true 118 | 119 | /chokidar@3.6.0: 120 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 121 | engines: {node: '>= 8.10.0'} 122 | dependencies: 123 | anymatch: 3.1.3 124 | braces: 3.0.2 125 | glob-parent: 5.1.2 126 | is-binary-path: 2.1.0 127 | is-glob: 4.0.3 128 | normalize-path: 3.0.0 129 | readdirp: 3.6.0 130 | optionalDependencies: 131 | fsevents: 2.3.3 132 | dev: true 133 | 134 | /cliui@8.0.1: 135 | resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} 136 | engines: {node: '>=12'} 137 | dependencies: 138 | string-width: 4.2.3 139 | strip-ansi: 6.0.1 140 | wrap-ansi: 7.0.0 141 | dev: true 142 | 143 | /color-convert@2.0.1: 144 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 145 | engines: {node: '>=7.0.0'} 146 | dependencies: 147 | color-name: 1.1.4 148 | dev: true 149 | 150 | /color-name@1.1.4: 151 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 152 | dev: true 153 | 154 | /commander@9.5.0: 155 | resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} 156 | engines: {node: ^12.20.0 || >=14} 157 | dev: true 158 | 159 | /concurrently@8.2.2: 160 | resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} 161 | engines: {node: ^14.13.0 || >=16.0.0} 162 | hasBin: true 163 | dependencies: 164 | chalk: 4.1.2 165 | date-fns: 2.30.0 166 | lodash: 4.17.21 167 | rxjs: 7.8.1 168 | shell-quote: 1.8.1 169 | spawn-command: 0.0.2 170 | supports-color: 8.1.1 171 | tree-kill: 1.2.2 172 | yargs: 17.7.2 173 | dev: true 174 | 175 | /date-fns@2.30.0: 176 | resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} 177 | engines: {node: '>=0.11'} 178 | dependencies: 179 | '@babel/runtime': 7.24.4 180 | dev: true 181 | 182 | /date-format@4.0.14: 183 | resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==} 184 | engines: {node: '>=4.0'} 185 | dev: false 186 | 187 | /debug@4.3.4: 188 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 189 | engines: {node: '>=6.0'} 190 | peerDependencies: 191 | supports-color: '*' 192 | peerDependenciesMeta: 193 | supports-color: 194 | optional: true 195 | dependencies: 196 | ms: 2.1.2 197 | dev: false 198 | 199 | /dir-glob@3.0.1: 200 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 201 | engines: {node: '>=8'} 202 | dependencies: 203 | path-type: 4.0.0 204 | dev: true 205 | 206 | /emoji-regex@8.0.0: 207 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 208 | dev: true 209 | 210 | /escalade@3.1.2: 211 | resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} 212 | engines: {node: '>=6'} 213 | dev: true 214 | 215 | /fast-glob@3.3.2: 216 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 217 | engines: {node: '>=8.6.0'} 218 | dependencies: 219 | '@nodelib/fs.stat': 2.0.5 220 | '@nodelib/fs.walk': 1.2.8 221 | glob-parent: 5.1.2 222 | merge2: 1.4.1 223 | micromatch: 4.0.5 224 | dev: true 225 | 226 | /fastq@1.17.1: 227 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 228 | dependencies: 229 | reusify: 1.0.4 230 | dev: true 231 | 232 | /fill-range@7.0.1: 233 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 234 | engines: {node: '>=8'} 235 | dependencies: 236 | to-regex-range: 5.0.1 237 | dev: true 238 | 239 | /flatted@3.3.1: 240 | resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} 241 | dev: false 242 | 243 | /fs-extra@8.1.0: 244 | resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} 245 | engines: {node: '>=6 <7 || >=8'} 246 | dependencies: 247 | graceful-fs: 4.2.11 248 | jsonfile: 4.0.0 249 | universalify: 0.1.2 250 | dev: false 251 | 252 | /fsevents@2.3.3: 253 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 254 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 255 | os: [darwin] 256 | requiresBuild: true 257 | dev: true 258 | optional: true 259 | 260 | /get-caller-file@2.0.5: 261 | resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 262 | engines: {node: 6.* || 8.* || >= 10.*} 263 | dev: true 264 | 265 | /glob-parent@5.1.2: 266 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 267 | engines: {node: '>= 6'} 268 | dependencies: 269 | is-glob: 4.0.3 270 | dev: true 271 | 272 | /globby@11.1.0: 273 | resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} 274 | engines: {node: '>=10'} 275 | dependencies: 276 | array-union: 2.1.0 277 | dir-glob: 3.0.1 278 | fast-glob: 3.3.2 279 | ignore: 5.3.1 280 | merge2: 1.4.1 281 | slash: 3.0.0 282 | dev: true 283 | 284 | /graceful-fs@4.2.11: 285 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 286 | dev: false 287 | 288 | /has-flag@4.0.0: 289 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 290 | engines: {node: '>=8'} 291 | dev: true 292 | 293 | /ignore@5.3.1: 294 | resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} 295 | engines: {node: '>= 4'} 296 | dev: true 297 | 298 | /is-binary-path@2.1.0: 299 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 300 | engines: {node: '>=8'} 301 | dependencies: 302 | binary-extensions: 2.3.0 303 | dev: true 304 | 305 | /is-extglob@2.1.1: 306 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 307 | engines: {node: '>=0.10.0'} 308 | dev: true 309 | 310 | /is-fullwidth-code-point@3.0.0: 311 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 312 | engines: {node: '>=8'} 313 | dev: true 314 | 315 | /is-glob@4.0.3: 316 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 317 | engines: {node: '>=0.10.0'} 318 | dependencies: 319 | is-extglob: 2.1.1 320 | dev: true 321 | 322 | /is-number@7.0.0: 323 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 324 | engines: {node: '>=0.12.0'} 325 | dev: true 326 | 327 | /jsonfile@4.0.0: 328 | resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 329 | optionalDependencies: 330 | graceful-fs: 4.2.11 331 | dev: false 332 | 333 | /lodash@4.17.21: 334 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 335 | dev: true 336 | 337 | /log4js@6.9.1: 338 | resolution: {integrity: sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==} 339 | engines: {node: '>=8.0'} 340 | dependencies: 341 | date-format: 4.0.14 342 | debug: 4.3.4 343 | flatted: 3.3.1 344 | rfdc: 1.3.1 345 | streamroller: 3.1.5 346 | transitivePeerDependencies: 347 | - supports-color 348 | dev: false 349 | 350 | /merge2@1.4.1: 351 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 352 | engines: {node: '>= 8'} 353 | dev: true 354 | 355 | /micromatch@4.0.5: 356 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 357 | engines: {node: '>=8.6'} 358 | dependencies: 359 | braces: 3.0.2 360 | picomatch: 2.3.1 361 | dev: true 362 | 363 | /ms@2.1.2: 364 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 365 | dev: false 366 | 367 | /mylas@2.1.13: 368 | resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} 369 | engines: {node: '>=12.0.0'} 370 | dev: true 371 | 372 | /normalize-path@3.0.0: 373 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 374 | engines: {node: '>=0.10.0'} 375 | dev: true 376 | 377 | /path-type@4.0.0: 378 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 379 | engines: {node: '>=8'} 380 | dev: true 381 | 382 | /picomatch@2.3.1: 383 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 384 | engines: {node: '>=8.6'} 385 | dev: true 386 | 387 | /plimit-lit@1.6.1: 388 | resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} 389 | engines: {node: '>=12'} 390 | dependencies: 391 | queue-lit: 1.5.2 392 | dev: true 393 | 394 | /queue-lit@1.5.2: 395 | resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} 396 | engines: {node: '>=12'} 397 | dev: true 398 | 399 | /queue-microtask@1.2.3: 400 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 401 | dev: true 402 | 403 | /readdirp@3.6.0: 404 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 405 | engines: {node: '>=8.10.0'} 406 | dependencies: 407 | picomatch: 2.3.1 408 | dev: true 409 | 410 | /regenerator-runtime@0.14.1: 411 | resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} 412 | dev: true 413 | 414 | /require-directory@2.1.1: 415 | resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 416 | engines: {node: '>=0.10.0'} 417 | dev: true 418 | 419 | /reusify@1.0.4: 420 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 421 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 422 | dev: true 423 | 424 | /rfdc@1.3.1: 425 | resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} 426 | dev: false 427 | 428 | /run-parallel@1.2.0: 429 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 430 | dependencies: 431 | queue-microtask: 1.2.3 432 | dev: true 433 | 434 | /rxjs@7.8.1: 435 | resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} 436 | dependencies: 437 | tslib: 2.6.2 438 | dev: true 439 | 440 | /shell-quote@1.8.1: 441 | resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} 442 | dev: true 443 | 444 | /slash@3.0.0: 445 | resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 446 | engines: {node: '>=8'} 447 | dev: true 448 | 449 | /spawn-command@0.0.2: 450 | resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} 451 | dev: true 452 | 453 | /streamroller@3.1.5: 454 | resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} 455 | engines: {node: '>=8.0'} 456 | dependencies: 457 | date-format: 4.0.14 458 | debug: 4.3.4 459 | fs-extra: 8.1.0 460 | transitivePeerDependencies: 461 | - supports-color 462 | dev: false 463 | 464 | /string-width@4.2.3: 465 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 466 | engines: {node: '>=8'} 467 | dependencies: 468 | emoji-regex: 8.0.0 469 | is-fullwidth-code-point: 3.0.0 470 | strip-ansi: 6.0.1 471 | dev: true 472 | 473 | /strip-ansi@6.0.1: 474 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 475 | engines: {node: '>=8'} 476 | dependencies: 477 | ansi-regex: 5.0.1 478 | dev: true 479 | 480 | /supports-color@7.2.0: 481 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 482 | engines: {node: '>=8'} 483 | dependencies: 484 | has-flag: 4.0.0 485 | dev: true 486 | 487 | /supports-color@8.1.1: 488 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 489 | engines: {node: '>=10'} 490 | dependencies: 491 | has-flag: 4.0.0 492 | dev: true 493 | 494 | /to-regex-range@5.0.1: 495 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 496 | engines: {node: '>=8.0'} 497 | dependencies: 498 | is-number: 7.0.0 499 | dev: true 500 | 501 | /tree-kill@1.2.2: 502 | resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 503 | hasBin: true 504 | dev: true 505 | 506 | /tsc-alias@1.8.8: 507 | resolution: {integrity: sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==} 508 | hasBin: true 509 | dependencies: 510 | chokidar: 3.6.0 511 | commander: 9.5.0 512 | globby: 11.1.0 513 | mylas: 2.1.13 514 | normalize-path: 3.0.0 515 | plimit-lit: 1.6.1 516 | dev: true 517 | 518 | /tslib@2.6.2: 519 | resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} 520 | dev: true 521 | 522 | /typescript@5.4.5: 523 | resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} 524 | engines: {node: '>=14.17'} 525 | hasBin: true 526 | dev: true 527 | 528 | /undici-types@5.26.5: 529 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 530 | dev: true 531 | 532 | /universalify@0.1.2: 533 | resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} 534 | engines: {node: '>= 4.0.0'} 535 | dev: false 536 | 537 | /wrap-ansi@7.0.0: 538 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 539 | engines: {node: '>=10'} 540 | dependencies: 541 | ansi-styles: 4.3.0 542 | string-width: 4.2.3 543 | strip-ansi: 6.0.1 544 | dev: true 545 | 546 | /ws@8.16.0: 547 | resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} 548 | engines: {node: '>=10.0.0'} 549 | peerDependencies: 550 | bufferutil: ^4.0.1 551 | utf-8-validate: '>=5.0.2' 552 | peerDependenciesMeta: 553 | bufferutil: 554 | optional: true 555 | utf-8-validate: 556 | optional: true 557 | dev: false 558 | 559 | /y18n@5.0.8: 560 | resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 561 | engines: {node: '>=10'} 562 | dev: true 563 | 564 | /yargs-parser@21.1.1: 565 | resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} 566 | engines: {node: '>=12'} 567 | dev: true 568 | 569 | /yargs@17.7.2: 570 | resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} 571 | engines: {node: '>=12'} 572 | dependencies: 573 | cliui: 8.0.1 574 | escalade: 3.1.2 575 | get-caller-file: 2.0.5 576 | require-directory: 2.1.1 577 | string-width: 4.2.3 578 | y18n: 5.0.8 579 | yargs-parser: 21.1.1 580 | dev: true 581 | --------------------------------------------------------------------------------