├── .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 = 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> {
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 {
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] "
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> {
34 | return request.get('/gateway');
35 | },
36 |
37 | /**
38 | * 获取带分片 WSS 接入点。
39 | */
40 | getGatewayBot(): Promise> {
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 ",
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 {
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 >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 {
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;
9 | keyboard?: Record;
10 | media?: {
11 | file_info: string;
12 | };
13 | ark?: Record;
14 | /**
15 | * @deprecated 暂不支持
16 | */
17 | image?: unknown;
18 | /**
19 | * 消息引用
20 | * @deprecated 暂未支持
21 | */
22 | message_reference?: Record;
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> {
73 | return request.post(`/v2/groups/${group_openid}/messages`, params);
74 | },
75 |
76 | /**
77 | * 用于撤回机器人发送在当前群的消息
78 | */
79 | recallGroupMessage(group_openid: string, message_id: string): Promise {
80 | return request.delete(`/v2/groups/${group_openid}/messages/${message_id}`);
81 | },
82 |
83 | /**
84 | * 发送富媒体消息到群。
85 | */
86 | sendGroupFile(group_openid: string, params: SendGroupFileParams): Promise> {
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 {
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 {
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;
11 | keyboard?: Record;
12 | ark?: Record;
13 | media?: {
14 | file_info: string;
15 | };
16 | /**
17 | * @deprecated 暂不支持
18 | */
19 | image?: unknown;
20 | /**
21 | * 消息引用
22 | * @deprecated 暂未支持
23 | */
24 | message_reference?: Record;
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> {
81 | return request.post(`/v2/users/${openid}/messages`, params);
82 | },
83 |
84 | /**
85 | * 用于撤回机器人发送给当前用户的消息
86 | */
87 | recallUserMessage(openid: string, message_id: string): Promise {
88 | return request.delete(`/v2/users/${openid}/messages/${message_id}`);
89 | },
90 |
91 | /**
92 | * 单独发送富媒体消息给用户。
93 | */
94 | sendUserFile(openid: string, params: SendUserMessageFileParams): Promise> {
95 | return request.post(`/v2/users/${openid}/files`, params);
96 | },
97 |
98 | /**
99 | * 获取当前机器人详情。
100 | */
101 | getUserInfo(): Promise> {
102 | return request.get(`/users/@me`);
103 | },
104 |
105 | /**
106 | * 获取用户频道列表。
107 | */
108 | getUserGuilds(params: GetUserGuildsParams): Promise> {
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;
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;
21 | /** 响应拦截器 */
22 | export type ResponseInterceptor = (result: Result) => Result | Promise;
23 |
24 | /** 结果集 */
25 | export interface Result {
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(interceptor: ResponseInterceptor): void {
53 | this.responseInterceptors.push(interceptor);
54 | }
55 |
56 | public async basis(config: RequestConfig): Promise> {
57 | if (typeof config.body === 'string') {
58 | const defaultConfig: Config = {
59 | headers: {
60 | 'Content-Type': 'application/json',
61 | },
62 | };
63 | config = 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;
91 | }
92 |
93 | public get(url: string, params?: object, config?: Config): Promise> {
94 | if (params) {
95 | url += (/\?/.test(url) ? '&' : '?') + objectToParams(params);
96 | }
97 | return this.basis({
98 | url,
99 | method: 'GET',
100 | ...config,
101 | });
102 | }
103 |
104 | public delete(url: string, params?: object, config: Config = {}): Promise> {
105 | return this.basis({
106 | url,
107 | method: 'DELETE',
108 | body: parseBody(params),
109 | ...config,
110 | });
111 | }
112 |
113 | public post(url: string, params?: object, config: Config = {}): Promise> {
114 | return this.basis({
115 | url,
116 | method: 'POST',
117 | body: parseBody(params),
118 | ...config,
119 | });
120 | }
121 |
122 | public put(url: string, params?: object, config: Config = {}): Promise> {
123 | return this.basis({
124 | url,
125 | method: 'PUT',
126 | body: parseBody(params),
127 | ...config,
128 | });
129 | }
130 |
131 | public patch(url: string, params?: object, config: Config = {}): Promise> {
132 | return this.basis({
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 |
2 |

3 |
Amesu
4 |
5 |
6 | ---
7 |
8 | 
9 | 
10 | 
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('/复读 ', 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;
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> {
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 {
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> {
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> {
124 | return request.patch(`/channels/${channel_id}`, params);
125 | },
126 |
127 | /**
128 | * 删除 channel_id 指定的子频道。
129 | */
130 | deleteChannel(channel_id: string): Promise {
131 | return request.delete(`/channels/${channel_id}`);
132 | },
133 |
134 | /**
135 | * 获取子频道在线成员数。
136 | */
137 | getChannelOnlineNum(channel_id: string): Promise> {
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> {
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 {
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> {
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 {
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> {
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 {
194 | return request.delete(`/channels/${channel_id}/pins/${message_id}`);
195 | },
196 |
197 | /**
198 | * 用于获取子频道 channel_id 内的精华消息。
199 | */
200 | getChannelPin(channel_id: string): Promise> {
201 | return request.get(`/channels/${channel_id}/pins`);
202 | },
203 |
204 | /**
205 | * 用于获取channel_id指定的子频道中当天的日程列表。
206 | */
207 | getChannelSchedule(channel_id: string, since?: number): Promise> {
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> {
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> {
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> {
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 {
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 {
250 | return request.post(`/channels/${channel_id}/audio`, params);
251 | },
252 |
253 | /**
254 | * 机器人在 channel_id 对应的语音子频道上麦。
255 | */
256 | channelMicOn(channel_id: string, params: ChannelMicParams): Promise {
257 | return request.put(`/channels/${channel_id}/mic`, params);
258 | },
259 |
260 | /**
261 | * 机器人在 channel_id 对应的语音子频道下麦。
262 | */
263 | channelMicOff(channel_id: string, params: ChannelMicParams): Promise {
264 | return request.delete(`/channels/${channel_id}/mic`, params);
265 | },
266 |
267 | /**
268 | * 该接口用于获取子频道下的帖子列表。
269 | */
270 | getChannelThread(channel_id: string): Promise> {
271 | return request.get(`/channels/${channel_id}/threads`);
272 | },
273 |
274 | /**
275 | * 该接口用于获取子频道下的帖子详情。
276 | */
277 | getChannelThreadInfo(channel_id: string, thread_id: string): Promise> {
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> {
288 | return request.put(`/channels/${channel_id}/threads`, params);
289 | },
290 |
291 | /**
292 | * 用于删除指定子频道下的某个帖子。
293 | */
294 | deleteChannelThread(channel_id: string, thread_id: string): Promise {
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;
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> {
139 | return request.get(`/guilds/${guild_id}`);
140 | },
141 |
142 | /**
143 | * 获取 guild_id 指定的频道下的子频道列表。
144 | */
145 | getGuildChannels(guild_id: string): Promise> {
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> {
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> {
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> {
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> {
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 {
203 | return request.delete(`/guilds/${guild_id}/members/${user_id}`, params);
204 | },
205 |
206 | /**
207 | * 获取 guild_id 指定的频道下的身份组列表。
208 | */
209 | getGuildRoles(guild_id: string): Promise> {
210 | return request.get(`/guilds/${guild_id}/roles`);
211 | },
212 |
213 | /**
214 | * 用于在 guild_id 指定的频道下创建一个身份组。
215 | */
216 | createGuildRole(guild_id: string, params: GuildRoleParams): Promise> {
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> {
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 {
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 {
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 {
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> {
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> {
276 | return request.post(`/guilds/${guild_id}/api_permission/demand`, params);
277 | },
278 |
279 | /**
280 | * 用于获取机器人在频道 guild_id 内的消息频率设置。
281 | */
282 | getGuildMessageSetting(guild_id: string): Promise> {
283 | return request.get(`/guilds/${guild_id}/message/setting`);
284 | },
285 |
286 | /**
287 | * 用于将频道的全体成员(非管理员)禁言。
288 | */
289 | guildMute(guild_id: string, params: GuildMuteParams): Promise {
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 {
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 {
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> {
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 {
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;
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(event: T, listener: SessionEvent[T]): this;
166 | on(event: T, listener: SessionEvent[T]): this;
167 | once(event: T, listener: SessionEvent[T]): this;
168 | removeListener(event: T, listener: SessionEvent[T]): this;
169 | off(event: T, listener: SessionEvent[T]): this;
170 | removeAllListeners(event?: T): this;
171 | listeners(event: T): Function[];
172 | rawListeners(event: T): Function[];
173 | emit(event: T, ...args: Parameters): boolean;
174 | listenerCount(event: T, listener?: SessionEvent[T]): number;
175 | prependListener(event: T, listener: SessionEvent[T]): this;
176 | prependOnceListener(event: T, listener: SessionEvent[T]): this;
177 | eventNames(): 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 {
219 | clearTimeout(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 = 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 {
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;
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;
45 |
46 | export interface Client extends EventEmitter {
47 | addListener(event: T, listener: ClientEvent[T]): this;
48 | addListener(event: string | symbol, listener: (...args: unknown[]) => void): this;
49 |
50 | on(event: T, listener: ClientEvent[T]): this;
51 | on(event: string | symbol, listener: (...args: unknown[]) => void): this;
52 |
53 | once(event: T, listener: ClientEvent[T]): this;
54 | once(event: string | symbol, listener: (...args: unknown[]) => void): this;
55 |
56 | removeListener(event: T, listener: ClientEvent[T]): this;
57 | removeListener(event: string | symbol, listener: (...args: any[]) => void): this;
58 |
59 | off(event: T, listener: ClientEvent[T]): this;
60 | off(event: string | symbol, listener: (...args: unknown[]) => void): this;
61 |
62 | removeAllListeners(event?: T): this;
63 | removeAllListeners(event?: string | symbol): this;
64 |
65 | listeners(event: T): Function[];
66 | listeners(event: string | symbol): Function[];
67 |
68 | rawListeners(event: T): Function[];
69 | rawListeners(event: string | symbol): Function[];
70 |
71 | emit(event: T, ...args: Parameters): boolean;
72 | emit(event: string | symbol, ...args: any[]): boolean;
73 |
74 | listenerCount(event: T, listener?: ClientEvent[T]): number;
75 | listenerCount(event: string | symbol, listener?: Function): number;
76 |
77 | prependListener(event: T, listener: ClientEvent[T]): this;
78 | prependListener(event: string | symbol, listener: (...args: any[]) => void): this;
79 |
80 | prependOnceListener(event: T, listener: ClientEvent[T]): this;
81 | prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this;
82 |
83 | eventNames(): T[];
84 | eventNames(): Array;
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> => {
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> => {
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> => {
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> => {
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> => {
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> => {
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 {
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 ? `${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(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>;
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>;
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>;
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>;
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>;
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>;
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>;
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 |
--------------------------------------------------------------------------------