├── .husky ├── .gitignore └── pre-commit ├── docs ├── public │ ├── CNAME │ ├── logo.png │ ├── images │ │ └── ebg │ │ │ └── hierarchy.jpg │ ├── favicon.svg │ └── manifest.json ├── README.md ├── dev │ └── index.md ├── shims-vue.d.ts ├── guide │ ├── faq.md │ ├── logger.md │ ├── napcat │ │ └── index.md │ ├── database.md │ └── extend.md ├── api │ ├── status.md │ ├── index.md │ └── utils.md ├── .vitepress │ ├── theme │ │ ├── index.ts │ │ └── custom.scss │ └── components │ │ ├── ChatAvatar.vue │ │ ├── ChatPanel.vue │ │ └── ChatMessage.vue ├── package.json └── index.md ├── packages ├── create-app │ ├── .npmignore │ ├── .gitignore │ ├── template-ts │ │ ├── _gitignore │ │ ├── nodemon.json │ │ ├── plugins │ │ │ ├── README.md │ │ │ └── test │ │ │ │ ├── options.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── el.config.ts │ │ ├── README.md │ │ ├── tsconfig.json │ │ └── package.json │ ├── tsup.config.ts │ ├── README.md │ └── package.json ├── el-bot │ ├── plugins │ │ ├── rss │ │ │ ├── rss.schema.ts │ │ │ ├── template.ts │ │ │ └── package.json │ │ ├── README.md │ │ ├── index.ts │ │ ├── report │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ └── index.ts │ │ ├── tqfs │ │ │ ├── package.json │ │ │ └── index.ts │ │ ├── jrmsn │ │ │ ├── package.json │ │ │ └── index.ts │ │ ├── ping │ │ │ ├── package.json │ │ │ └── index.ts │ │ ├── search │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ └── index.ts │ │ ├── limit │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── options.ts │ │ │ └── index.ts │ │ ├── qrcode │ │ │ ├── README.md │ │ │ ├── options.ts │ │ │ ├── package.json │ │ │ └── index.ts │ │ ├── github │ │ │ ├── index.ts │ │ │ └── package.json │ │ ├── blacklist │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── egg │ │ │ ├── index.ts │ │ │ └── package.json │ │ ├── forward │ │ │ ├── types.ts │ │ │ ├── package.json │ │ │ └── index.ts │ │ ├── teach │ │ │ ├── utils.ts │ │ │ ├── options.ts │ │ │ ├── package.json │ │ │ ├── teach.schema.ts │ │ │ └── index.ts │ │ ├── dev │ │ │ ├── package.json │ │ │ └── index.ts │ │ ├── memo │ │ │ ├── package.json │ │ │ ├── memo.schema.ts │ │ │ └── utils.ts │ │ ├── admin │ │ │ ├── package.json │ │ │ └── index.ts │ │ ├── answer │ │ │ ├── package.json │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── workflow │ │ │ ├── package.json │ │ │ └── index.ts │ │ ├── nbnhhsh │ │ │ ├── package.json │ │ │ ├── README.md │ │ │ └── index.ts │ │ └── counter │ │ │ ├── package.json │ │ │ ├── counter.schema.ts │ │ │ └── index.ts │ ├── core │ │ ├── db │ │ │ ├── friend │ │ │ │ └── friend.service.ts │ │ │ ├── schemas │ │ │ │ ├── group.schema.ts │ │ │ │ └── friend.schema.ts │ │ │ ├── index.ts │ │ │ └── analytics.ts │ │ ├── bot │ │ │ ├── cli │ │ │ │ ├── README.md │ │ │ │ ├── utils.ts │ │ │ │ └── jobs.ts │ │ │ ├── platform │ │ │ │ ├── index.ts │ │ │ │ └── qq.ts │ │ │ ├── plugins │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── logger │ │ │ │ ├── consola.ts │ │ │ │ ├── index.ts │ │ │ │ └── winston.ts │ │ │ ├── command │ │ │ │ ├── utils.ts │ │ │ │ └── index.ts │ │ │ ├── user.ts │ │ │ ├── sender.ts │ │ │ └── status.ts │ │ ├── nest │ │ │ ├── README.md │ │ │ ├── index.ts │ │ │ ├── service.ts │ │ │ └── module.ts │ │ ├── composition-api │ │ │ ├── index.ts │ │ │ ├── README.md │ │ │ ├── test.ts │ │ │ ├── lifecycle.ts │ │ │ └── hooks.ts │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── target.ts │ │ │ ├── message.ts │ │ │ ├── error.ts │ │ │ ├── misc.ts │ │ │ ├── decorators.ts │ │ │ ├── helper.ts │ │ │ └── config.ts │ │ ├── index.ts │ │ ├── shared │ │ │ └── index.ts │ │ ├── node │ │ │ └── utils.ts │ │ ├── napcat │ │ │ └── index.ts │ │ └── config │ │ │ ├── bot.ts │ │ │ └── index.ts │ ├── node │ │ ├── README.md │ │ ├── index.ts │ │ ├── types │ │ │ └── index.ts │ │ ├── server │ │ │ ├── index.ts │ │ │ ├── webhook │ │ │ │ ├── types.ts │ │ │ │ ├── github-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── octokit.ts │ │ │ └── hono.ts │ │ └── cli │ │ │ ├── options.ts │ │ │ ├── index.ts │ │ │ ├── utils.ts │ │ │ └── commands │ │ │ └── dev.ts │ ├── TODO.md │ ├── typedoc.json │ ├── index.ts │ ├── bin │ │ └── el-bot.ts │ ├── types │ │ ├── index.ts │ │ └── config.ts │ ├── .gitignore │ ├── templates │ │ └── plugin-example │ │ │ ├── package.json │ │ │ └── index.ts │ ├── bump.config.ts │ ├── scripts │ │ ├── utils.ts │ │ └── plugins.ts │ ├── tsconfig.json │ └── README.md ├── onebot-adapter-koishi │ └── index.ts ├── @el-bot │ ├── README.md │ └── plugin-niubi │ │ ├── tsconfig.json │ │ ├── package.json │ │ ├── src │ │ ├── options.ts │ │ └── index.ts │ │ └── README.md ├── qq-sdk │ ├── src │ │ ├── types │ │ │ ├── index.ts │ │ │ └── thread.ts │ │ ├── client │ │ │ ├── api.ts │ │ │ ├── channels │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── constants │ │ │ └── index.ts │ │ └── index.ts │ ├── README.md │ ├── build.config.ts │ └── package.json └── cli │ ├── package.json │ ├── README.md │ └── src │ ├── index.ts │ ├── install │ ├── index.ts │ └── repo.ts │ └── start │ └── index.ts ├── plugins ├── koishi-plugin-nbnhhsh │ └── src │ │ └── index.ts ├── feeder │ ├── README.md │ ├── package.json │ └── src │ │ └── feeder.scheme.ts ├── twitter │ ├── package.json │ └── index.ts ├── niubi │ ├── tsup.config.ts │ ├── package.json │ ├── README.md │ └── src │ │ └── index.ts ├── setu │ ├── tsup.config.ts │ ├── package.json │ ├── README.md │ └── src │ │ └── index.ts └── search-image │ ├── package.json │ ├── README.md │ └── src │ └── index.ts ├── demo ├── README.md ├── .env.example ├── bot │ ├── README.md │ ├── plugins │ │ ├── qq │ │ │ ├── config.ts │ │ │ ├── package.json │ │ │ ├── constants.ts │ │ │ └── index.ts │ │ └── ping.ts │ └── index.ts ├── pm2.config.cjs ├── package.json └── el-bot.config.ts ├── examples ├── nestjs-demo │ ├── .npmrc │ ├── .gitignore │ ├── .env.example │ ├── config │ │ ├── README.md │ │ ├── qq.ts │ │ └── bot.ts │ ├── nest-cli.json │ ├── src │ │ ├── plugins │ │ │ ├── webhook │ │ │ │ ├── package.json │ │ │ │ └── index.ts │ │ │ ├── command.ts │ │ │ ├── dev.ts │ │ │ └── card.ts │ │ ├── core │ │ │ ├── core.module.ts │ │ │ └── core.controller.ts │ │ ├── app.service.ts │ │ ├── bots │ │ │ ├── bots.service.ts │ │ │ └── bots.module.ts │ │ ├── app.module.ts │ │ └── main.ts │ ├── tsconfig.build.json │ ├── README.md │ ├── el-bot.config.ts │ ├── tsconfig.json │ └── package.json └── simple │ ├── .env.example │ ├── bot │ ├── README.md │ └── plugins │ │ └── ping.ts │ ├── README.md │ ├── src │ └── index.ts │ ├── tsup.config.ts │ ├── el-bot.config.ts │ ├── tsconfig.json │ └── package.json ├── .npmrc ├── CHANGELOG.md ├── bun.lockb ├── .env.example ├── .editorconfig ├── bump.config.ts ├── netlify.toml ├── eslint.config.mjs ├── .github ├── workflows │ ├── ci.yml │ ├── api.yml │ └── release.yml └── FUNDING.yml ├── tsconfig.json ├── test └── webhook.test.ts ├── .gitignore ├── .vscode └── settings.json ├── package.json └── config.yml /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /docs/public/CNAME: -------------------------------------------------------------------------------- 1 | docs.bot.elpsy.cn -------------------------------------------------------------------------------- /packages/create-app/.npmignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/rss/rss.schema.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/rss/template.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/onebot-adapter-koishi/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/koishi-plugin-nbnhhsh/src/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | Just for dev. 4 | -------------------------------------------------------------------------------- /packages/el-bot/core/db/friend/friend.service.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nestjs-demo/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /packages/create-app/.gitignore: -------------------------------------------------------------------------------- 1 | el-bot-template 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # @el-bot/docs 2 | 3 | el-bot 使用文档 4 | -------------------------------------------------------------------------------- /examples/nestjs-demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/cli/README.md: -------------------------------------------------------------------------------- 1 | # CLI for Bot 2 | -------------------------------------------------------------------------------- /packages/el-bot/core/nest/README.md: -------------------------------------------------------------------------------- 1 | # Nest.JS Module 2 | -------------------------------------------------------------------------------- /packages/@el-bot/README.md: -------------------------------------------------------------------------------- 1 | # @el-bot 2 | 3 | el-bot 相关插件 4 | -------------------------------------------------------------------------------- /packages/qq-sdk/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './thread' 2 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/platform/index.ts: -------------------------------------------------------------------------------- 1 | export * from './qq' 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | ignore-workspace-root-check=true 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## V1 2 | 3 | Fully ESM & TS, based on `vite-node`. 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/el-bot/HEAD/bun.lockb -------------------------------------------------------------------------------- /examples/simple/.env.example: -------------------------------------------------------------------------------- 1 | QQ_BOT_APP_ID= 2 | QQ_BOT_APP_TOKEN= 3 | -------------------------------------------------------------------------------- /packages/el-bot/node/README.md: -------------------------------------------------------------------------------- 1 | # Node API 2 | 3 | 依赖于 Node.js 的 API。 4 | -------------------------------------------------------------------------------- /packages/qq-sdk/src/client/api.ts: -------------------------------------------------------------------------------- 1 | export class ClientAPI { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /docs/dev/index.md: -------------------------------------------------------------------------------- 1 | # 开发 2 | 3 | ```bash 4 | pnpm i 5 | 6 | pnpm start 7 | ``` 8 | -------------------------------------------------------------------------------- /packages/create-app/template-ts/_gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | -------------------------------------------------------------------------------- /packages/create-app/template-ts/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "js,ts,yml" 3 | } 4 | -------------------------------------------------------------------------------- /packages/el-bot/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cli' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /packages/qq-sdk/src/client/channels/index.ts: -------------------------------------------------------------------------------- 1 | export class Channels { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /examples/nestjs-demo/.env.example: -------------------------------------------------------------------------------- 1 | BOT_QQ= 2 | BOT_DB_URI= 3 | EL_DB_ENABLE=false 4 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/README.md: -------------------------------------------------------------------------------- 1 | # plugins 2 | 3 | Internal plugins for the bot. 4 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export { default as answerPlugin } from './answer' 2 | -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/el-bot/HEAD/docs/public/logo.png -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/cli", 3 | "version": "0.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /packages/el-bot/TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - remove useless dependencies 4 | - commander 5 | -------------------------------------------------------------------------------- /packages/el-bot/core/nest/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module' 2 | export * from './service' 3 | -------------------------------------------------------------------------------- /plugins/feeder/README.md: -------------------------------------------------------------------------------- 1 | # feeder 2 | 3 | ```sh 4 | yarn add rss-feed-emitter 5 | ``` 6 | -------------------------------------------------------------------------------- /packages/el-bot/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["index.ts"], 3 | "out": "docs" 4 | } 5 | -------------------------------------------------------------------------------- /packages/el-bot/core/composition-api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks' 2 | export * from './lifecycle' 3 | -------------------------------------------------------------------------------- /packages/el-bot/node/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * only for node types 3 | */ 4 | export type A = string 5 | -------------------------------------------------------------------------------- /examples/nestjs-demo/config/README.md: -------------------------------------------------------------------------------- 1 | # el 机器人配置 2 | 3 | - `el-bot.config.ts` 全局配置 4 | - `bot.ts` 机器人及插件相关配置 5 | -------------------------------------------------------------------------------- /packages/el-bot/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core' 2 | export * from './plugins' 3 | export * from './types' 4 | -------------------------------------------------------------------------------- /demo/.env.example: -------------------------------------------------------------------------------- 1 | # https://q.qq.com/qqbot/#/developer/developer-setting 2 | QQ_BOT_APP_ID= 3 | QQ_BOT_APP_TOKEN= 4 | -------------------------------------------------------------------------------- /examples/nestjs-demo/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /demo/bot/README.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Auto load plugins from the `plugins` directory. 4 | 5 | 自动加载 `plugins` 目录下的插件。 6 | -------------------------------------------------------------------------------- /packages/el-bot/bin/el-bot.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env vite-node --script 2 | 3 | import { run } from '../node' 4 | 5 | run() 6 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/report/README.md: -------------------------------------------------------------------------------- 1 | # Report 2 | 3 | > 须先启用 `webhook` 以接收 POST 信息。 4 | 5 | 根据收到的 POST 信息,进行对应报告。 6 | -------------------------------------------------------------------------------- /packages/el-bot/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from '../core' 2 | 3 | export * from './config' 4 | export type { Bot } 5 | -------------------------------------------------------------------------------- /docs/public/images/ebg/hierarchy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/el-bot/HEAD/docs/public/images/ebg/hierarchy.jpg -------------------------------------------------------------------------------- /packages/el-bot/core/bot/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './class' 2 | export * from './types' 3 | export * from './utils' 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # rettiwt-api https://github.com/Rishikant181/Rettiwt-API#a-for-chromechromium-based-browsers 2 | RETTIWT_API_KEY= 3 | -------------------------------------------------------------------------------- /demo/bot/plugins/qq/config.ts: -------------------------------------------------------------------------------- 1 | export const ylfTestGuildID = '8023676110463580325' 2 | export const xyLabSubGuildId = '665652769' 3 | -------------------------------------------------------------------------------- /examples/simple/bot/README.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Auto load plugins from the `plugins` directory. 4 | 5 | 自动加载 `plugins` 目录下的插件。 6 | -------------------------------------------------------------------------------- /packages/el-bot/.gitignore: -------------------------------------------------------------------------------- 1 | # for typedoc 2 | docs/ 3 | dist/ 4 | node_modules/ 5 | package-lock.json 6 | yarn.lock 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /demo/bot/plugins/qq/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qq", 3 | "version": "0.0.1", 4 | "description": "QQ plugin for my custom bot." 5 | } 6 | -------------------------------------------------------------------------------- /packages/create-app/template-ts/plugins/README.md: -------------------------------------------------------------------------------- 1 | # plugins 2 | 3 | 插件目录 4 | 5 | ## Todo 6 | 7 | - `autoload: true` 提供自动加载该目录下所有插件的配置项 8 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/tqfs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/plugin-tqfs", 3 | "version": "0.0.1", 4 | "description": "" 5 | } 6 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/logger/consola.ts: -------------------------------------------------------------------------------- 1 | import { createConsola } from 'consola' 2 | 3 | export const consola = createConsola().withTag('🤖') 4 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/jrmsn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/plugin-jrmsn", 3 | "version": "0.0.1", 4 | "description": "今日美少女" 5 | } 6 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/ping/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/ping", 3 | "version": "0.0.1", 4 | "description": "Ping Command" 5 | } 6 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/plugins/webhook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook", 3 | "private": "true", 4 | "description": "测试 Webhook 插件" 5 | } 6 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # 使用 QQ官方机器人的 Demo 2 | 3 | [使用hunyuan-lite回复消息](https://github.com/tencentyun/serverless-demo/tree/master/Go1-QQBotDemo) 4 | -------------------------------------------------------------------------------- /packages/create-app/template-ts/index.ts: -------------------------------------------------------------------------------- 1 | import Bot from 'el-bot' 2 | import el from './el.config' 3 | 4 | const bot = new Bot(el) 5 | bot.start() 6 | -------------------------------------------------------------------------------- /packages/create-app/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | }) 6 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # cli 2 | 3 | ## Install 4 | 5 | ```sh 6 | npm link 7 | ``` 8 | 9 | ## Usage 10 | 11 | ```sh 12 | el start 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/el-bot/core/composition-api/README.md: -------------------------------------------------------------------------------- 1 | # Composables for Bot 2 | 3 | > 抹平 SDK 差异 4 | 5 | ```ts 6 | import { onMessage } from 'el-bot' 7 | ``` 8 | -------------------------------------------------------------------------------- /packages/el-bot/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * as config from './config' 2 | export * from './error' 3 | export * from './helper' 4 | export * from './message' 5 | -------------------------------------------------------------------------------- /packages/el-bot/node/server/index.ts: -------------------------------------------------------------------------------- 1 | import { createHonoServer } from './hono' 2 | 3 | export * from './hono' 4 | export const createServer = createHonoServer 5 | -------------------------------------------------------------------------------- /packages/el-bot/templates/plugin-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/plugin-example", 3 | "version": "0.0.1", 4 | "description": "插件实力" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@el-bot/plugin-niubi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/search/README.md: -------------------------------------------------------------------------------- 1 | # search 搜索 2 | 3 | 你不会百度吗? 4 | 5 | ```md 6 | 百度 云游君 7 | 8 | 回复: 9 | https://www.baidu.com/s?wd=el-bot 10 | ``` 11 | -------------------------------------------------------------------------------- /plugins/twitter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/plugin-twitter", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "rettiwt-api": "^4.2.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/bot/index.ts: -------------------------------------------------------------------------------- 1 | import { createBot } from 'el-bot' 2 | 3 | export async function main() { 4 | const bot = await createBot() 5 | await bot.start() 6 | } 7 | 8 | main() 9 | -------------------------------------------------------------------------------- /packages/create-app/README.md: -------------------------------------------------------------------------------- 1 | # @el-bot/create-app 2 | 3 | ```sh 4 | yarn create @el-bot/app 5 | ``` 6 | 7 | It will clone . 8 | -------------------------------------------------------------------------------- /docs/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/limit/README.md: -------------------------------------------------------------------------------- 1 | # 消息频率限制 limit 2 | 3 | [消息频率限制](https://docs.bot.elpsy.cn/plugins/default.html#%E6%B6%88%E6%81%AF%E9%A2%91%E7%8E%87%E9%99%90%E5%88%B6) 4 | -------------------------------------------------------------------------------- /examples/simple/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createBot } from 'el-bot' 2 | 3 | export async function main() { 4 | const bot = await createBot() 5 | await bot.start() 6 | } 7 | 8 | main() 9 | -------------------------------------------------------------------------------- /packages/el-bot/bump.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'bumpp' 2 | 3 | export default defineConfig({ 4 | all: false, 5 | commit: false, 6 | tag: false, 7 | push: false, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/el-bot/core/index.ts: -------------------------------------------------------------------------------- 1 | // export * from './nest' 2 | export * from './bot' 3 | export * from './composition-api' 4 | 5 | export * from './config' 6 | export * as utils from './utils' 7 | -------------------------------------------------------------------------------- /packages/el-bot/core/shared/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line ts/no-unsafe-function-type 2 | export function isFunction(val: unknown): val is Function { 3 | return typeof val === 'function' 4 | } 5 | -------------------------------------------------------------------------------- /demo/pm2.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'el-bot-demo', // Name of your application 3 | script: 'bot/index.ts', // Entry point of your application 4 | interpreter: 'bun', // Path to the Bun interpreter 5 | } 6 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/qrcode/README.md: -------------------------------------------------------------------------------- 1 | # Qrcode 2 | 3 | 如果您需要使用 qrcode 插件,由于 mirai 目前只支持相对路径发送图片,您可能需要配置 `package.json`。 4 | 5 | ```json 6 | { 7 | "mcl": { 8 | "folder": "你的 mcl 的相对路径" 9 | } 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/qq-sdk/README.md: -------------------------------------------------------------------------------- 1 | # QQ SDK 2 | 3 | > [QQ 官方 API 接口](https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/error-trace/websocket.html) 4 | 5 | - [qq-guide-bot](https://www.npmjs.com/package/qq-guild-bot) 许久没有维护,不支持群聊消息发送。 6 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { CoreController } from './core.controller' 3 | 4 | @Module({ 5 | controllers: [CoreController], 6 | }) 7 | export class CoreModule {} 8 | -------------------------------------------------------------------------------- /packages/el-bot/core/utils/target.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 广播对象 3 | */ 4 | export class BroadcastTarget { 5 | /** 6 | * 好友 7 | */ 8 | friends = new Set() 9 | /** 10 | * 群聊 11 | */ 12 | groups = new Set() 13 | } 14 | -------------------------------------------------------------------------------- /packages/qq-sdk/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | 'src/index', 6 | ], 7 | declaration: 'node16', 8 | clean: true, 9 | }) 10 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Get, Injectable } from '@nestjs/common' 2 | 3 | @Injectable() 4 | export class AppService { 5 | @Get('hello') 6 | getHello(): string { 7 | return 'World!' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/simple/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | clean: true, 6 | // dts: true, 7 | format: ['esm'], 8 | shims: false, 9 | }) 10 | -------------------------------------------------------------------------------- /examples/nestjs-demo/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | // do not build el-bot in dist 5 | "paths": {} 6 | }, 7 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/github/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | 3 | // interface GitHubOptions {} 4 | 5 | export default function (ctx: Bot) { 6 | const { cli } = ctx 7 | cli.command('github').description('GitHub 小助手') 8 | } 9 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/core/core.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common' 2 | 3 | @Controller('') 4 | export class CoreController { 5 | @Get() 6 | index(): string { 7 | return 'Hello, Demo Bot!' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/niubi/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | clean: true, 5 | dts: true, 6 | minify: true, 7 | entryPoints: ['src/index.ts'], 8 | external: ['el-bot', 'axios', 'mirai-ts'], 9 | }) 10 | -------------------------------------------------------------------------------- /plugins/setu/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | clean: true, 5 | dts: true, 6 | minify: true, 7 | entryPoints: ['src/index.ts'], 8 | external: ['el-bot', 'axios', 'mirai-ts'], 9 | }) 10 | -------------------------------------------------------------------------------- /packages/create-app/template-ts/plugins/test/options.ts: -------------------------------------------------------------------------------- 1 | export interface TestOptions { 2 | /** 3 | * 帮助信息 4 | */ 5 | help: string 6 | } 7 | 8 | const testOptions: TestOptions = { 9 | help: '测试一下帮助信息', 10 | } 11 | 12 | export default testOptions 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/qrcode/options.ts: -------------------------------------------------------------------------------- 1 | export interface QRCodeOptions { 2 | /** 3 | * 每次运行时自动清除缓存 4 | */ 5 | autoClearCache: boolean 6 | } 7 | 8 | const qrcodeOptions: QRCodeOptions = { 9 | autoClearCache: true, 10 | } 11 | 12 | export default qrcodeOptions 13 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/plugins/webhook/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import consola from 'consola' 3 | 4 | export default function (ctx: Bot) { 5 | ctx.webhook?.on('ok', (data: any) => { 6 | consola.info('Get type OK!') 7 | consola.info(data) 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/blacklist/README.md: -------------------------------------------------------------------------------- 1 | # 黑名单 2 | 3 | 用户封禁后,将不再对用户所发送的消息进行反馈。 4 | 5 | 群封禁后,将不再对群内消息进行反馈。(须在其他群进行解封操作) 6 | 7 | ```sh 8 | el block 9 | el block qq 114514 10 | el block group 666666 11 | el unblock qq 114514 12 | el unblock group 666666 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/egg/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | 3 | export default function (ctx: Bot) { 4 | const mirai = ctx.mirai 5 | 6 | mirai.on('message', (msg) => { 7 | if (msg.plain.toLowerCase() === 'el psy congroo') 8 | msg.reply('这一切都是命运石之门的选择……') 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /examples/nestjs-demo/config/qq.ts: -------------------------------------------------------------------------------- 1 | export const groups = { 2 | first: { 3 | name: '机器人测试群', 4 | id: 120117362, 5 | }, 6 | second: { 7 | name: '测试群二号', 8 | id: 275834309, 9 | }, 10 | } 11 | 12 | export const plugins = { 13 | jrmsn: { 14 | groups: [389401003], 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/bots/bots.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit } from '@nestjs/common' 2 | import consola from 'consola' 3 | 4 | @Injectable() 5 | export class BotsService implements OnModuleInit { 6 | onModuleInit(): void { 7 | consola.info('BotsService initialized') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/plugins/command.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import consola from 'consola' 3 | 4 | export default function (ctx: Bot) { 5 | ctx 6 | .command('命令') 7 | .description('一个测试用的命令') 8 | .action((options) => { 9 | consola.info(options) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /examples/nestjs-demo/README.md: -------------------------------------------------------------------------------- 1 | # bot 2 | 3 | 这只是一个开发用的测试机器人。 4 | 5 | 如果你需要一个模版,你应该参考 [el-bot-template](https://github.com/ElpsyCN/el-bot-template)。 6 | 7 | > Demo with nestjs 8 | 9 | ## Usage 10 | 11 | 启动 cqhttp 12 | 13 | ```bash 14 | pnpm cqhttp 15 | ``` 16 | 17 | ```bash 18 | # dev 19 | pnpm dev 20 | ``` 21 | -------------------------------------------------------------------------------- /bump.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'bumpp' 2 | 3 | const packages = [ 4 | 'el-bot', 5 | ] 6 | 7 | export default defineConfig({ 8 | all: true, 9 | commit: true, 10 | tag: true, 11 | push: true, 12 | 13 | files: [ 14 | ...packages.map(pkg => `packages/${pkg}/package.json`), 15 | ], 16 | }) 17 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/plugins/dev.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import consola from 'consola' 3 | 4 | export default async function (ctx: Bot) { 5 | const mirai = ctx.mirai 6 | consola.info(ctx.el.path) 7 | 8 | const friendList = await mirai.api.friendList() 9 | consola.info(friendList) 10 | } 11 | -------------------------------------------------------------------------------- /docs/guide/faq.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | [![GitHub issues](https://img.shields.io/github/issues/ElpsyCN/el-bot)](https://github.com/ElpsyCN/el-bot/issues) 4 | 5 | 这里将会列出一些常见的容易犯错的问题,请尽量通过 [el-bot Issues](https://github.com/ElpsyCN/el-bot/issues) 进行反馈。 6 | 7 | ## 机器人私聊正常回复,群聊无法回复 8 | 9 | 可能是受到腾讯风控。先使用自己手机登陆并发条消息(刷新 Token),再重新使用自己的服务器登陆。 10 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/command/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Command } from './index' 2 | 3 | export type CommandList = Map 4 | 5 | export function getHelpContent(list: CommandList) { 6 | let content = '帮助指令:' 7 | for (const [key, command] of list) 8 | content += `\n ${key}: ${command.desc || '没有说明'}` 9 | 10 | return content 11 | } 12 | -------------------------------------------------------------------------------- /packages/el-bot/node/cli/options.ts: -------------------------------------------------------------------------------- 1 | import type { Argv } from 'yargs' 2 | 3 | /** 4 | * set common options for cli 5 | * @param args 6 | */ 7 | export function commonOptions(args: Argv) { 8 | return args.positional('root', { 9 | default: '.', 10 | type: 'string', 11 | describe: 'root folder of your source files', 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /packages/el-bot/core/utils/message.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 渲染 ES6 字符串 3 | * @param template 字符串模版 4 | * @param data 数据 5 | * @param name 参数名称 6 | */ 7 | function renderString(template: string, data: any, name: string) { 8 | // eslint-disable-next-line no-new-func 9 | return new Function(name, `return \`${template}\``)(data) 10 | } 11 | 12 | export { renderString } 13 | -------------------------------------------------------------------------------- /packages/el-bot/scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import fs from 'fs-extra' 4 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | 7 | export function getAllPlugins() { 8 | const plugins = fs.readdirSync(path.resolve(__dirname, '../src/plugins')) 9 | return plugins 10 | } 11 | -------------------------------------------------------------------------------- /docs/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "El Bot", 3 | "name": "El Bot Docs", 4 | "icons": [ 5 | { 6 | "src": "favicon.svg", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#4b5cc4", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/forward/types.ts: -------------------------------------------------------------------------------- 1 | import { ListenTarget, Target } from '../../types' 2 | 3 | export type BaseListenType = 'all' | 'master' | 'admin' | 'friend' | 'group' 4 | 5 | export interface ForwardItem { 6 | listen: ListenTarget 7 | target: Target 8 | } 9 | 10 | export type ForwardOptions = ForwardItem[] 11 | 12 | export type AllMessageList = Record 13 | -------------------------------------------------------------------------------- /plugins/search-image/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/plugin-search-image", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "以图搜图", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "license": "AGPL-3.0", 12 | "dependencies": { 13 | "sagiri": "^4.3.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/teach/utils.ts: -------------------------------------------------------------------------------- 1 | import { Teach } from './teach.schema' 2 | 3 | /** 4 | * 展示当前的问答列表 5 | */ 6 | export async function displayList() { 7 | const list = await Teach.find() 8 | let listContent = '问答列表:' 9 | list.forEach((qa) => { 10 | listContent += '\n----------' 11 | listContent += `\nQ: 「${qa.question}\nA: 「${qa.answer}」` 12 | }) 13 | return listContent 14 | } 15 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/egg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egg", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "彩蛋", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "开发输出", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/memo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "备忘录", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "通用的管理员功能", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/answer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "answer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "自动应答", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/forward/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forward", 3 | "version": "0.0.2", 4 | "private": true, 5 | "description": "消息转发", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/report/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "report", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "消息报告", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/rss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rss", 3 | "version": "0.0.2", 4 | "private": true, 5 | "description": "订阅 RSS 信息", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/workflow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workflow", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "工作流", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/qq-sdk/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const QQAvailableIntentsEvents = { 2 | /** 3 | * 用户在单聊发送消息给机器人 4 | * @see https://bot.q.qq.com/wiki/develop/api-v2/server-inter/message/send-receive/event.html#%E5%8D%95%E8%81%8A%E6%B6%88%E6%81%AF 5 | */ 6 | C2C_MESSAGE_CREATE: 'C2C_MESSAGE_CREATE', 7 | /** 8 | * 用户在群聊@机器人发送消息 9 | */ 10 | GROUP_AT_MESSAGE_CREATE: 'GROUP_AT_MESSAGE_CREATE', 11 | } 12 | -------------------------------------------------------------------------------- /examples/simple/el-bot.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'el-bot' 2 | 3 | export default defineConfig({ 4 | bot: { 5 | 6 | }, 7 | 8 | napcat: { 9 | protocol: 'ws', 10 | host: '127.0.0.1', 11 | port: 3001, 12 | accessToken: 'yunyoujun', 13 | 14 | // ↓ 自动重连(可选) 15 | reconnection: { 16 | enable: true, 17 | attempts: 10, 18 | delay: 5000, 19 | }, 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/github/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "GitHub 小助手", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/nbnhhsh/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nbnhhsh", 3 | "version": "0.0.3", 4 | "private": true, 5 | "description": "能不能好好说话?", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/dev/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | 3 | export default async function (ctx: Bot) { 4 | const { mirai } = ctx 5 | // const config = ctx.el.config; 6 | // mirai.api.sendFriendMessage("咳咳……麦克风测试,麦克风测试……", config.master[0]); 7 | 8 | ctx.logger.info('on message') 9 | mirai.on('message', (msg) => { 10 | // msg.reply(msg.plain); 11 | ctx.logger.debug(msg) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/limit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/plugin-limit", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "限制消息频率", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/qrcode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/plugin-qrcode", 3 | "version": "0.0.3", 4 | "private": true, 5 | "description": "二维码生成器", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./index.mjs", 14 | "require": "./index.js" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/teach/options.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'el-bot' 2 | 3 | export interface TeachOptions { 4 | listen: Config.Listen 5 | /** 6 | * 回复 7 | */ 8 | reply: string 9 | /** 10 | * 没有权限时的回复 11 | */ 12 | else: string 13 | } 14 | 15 | const teachOptions: TeachOptions = { 16 | listen: ['master', 'admin'], 17 | reply: '我学会了!', 18 | else: '你在教我做事?', 19 | } 20 | 21 | export default teachOptions 22 | -------------------------------------------------------------------------------- /plugins/feeder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/plugin-feeder", 3 | "version": "0.0.1", 4 | "description": "动态订阅 RSS Feed 信息(与 rss 相区别)", 5 | "author": { 6 | "name": "YunYouJun", 7 | "url": "https://www.yunyoujun.cn", 8 | "email": "me@yunyoujun.cn" 9 | }, 10 | "main": "dist/index.js", 11 | "el-bot": { 12 | "db": true 13 | }, 14 | "dependencies": { 15 | "rss-feed-emitter": "^3.2.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugins/search-image/README.md: -------------------------------------------------------------------------------- 1 | # 以图搜图 2 | 3 | > 使用 [SauceNAO](https://saucenao.com/) 搜图。 4 | 5 | 请前往 [search-api | SauceNAO](https://saucenao.com/user.php?page=search-api) 申请 api key。 6 | 7 | ```sh 8 | yarn add sagiri 9 | ``` 10 | 11 | ## 配置 12 | 13 | ```yaml 14 | search-image: 15 | token: xxx 16 | options: 17 | # 返回多少个结果 18 | results: 3 19 | ``` 20 | 21 | More options in [Sagiri](https://github.com/ClarityCafe/Sagiri). 22 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/search/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "search", 3 | "version": "0.0.5", 4 | "private": true, 5 | "description": "引擎搜索", 6 | "author": { 7 | "name": "YunYouJun", 8 | "url": "https://www.yunyoujun.cn", 9 | "email": "me@yunyoujun.cn" 10 | }, 11 | "license": "AGPL-3.0", 12 | "exports": { 13 | ".": { 14 | "import": "./index.mjs", 15 | "require": "./index.js" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/create-app/template-ts/el.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'el-bot' 2 | 3 | export default defineConfig({ 4 | qq: 123, 5 | // 你可以直接解析你的 mirai/mcl 中 mirai-api-http 的配置 6 | // 你应当将其修改为你的相对路径或绝对路径 7 | setting: './mcl/config/net.mamoe.mirai-api-http/setting.yml', 8 | bot: { 9 | master: [910426929], 10 | plugins: { 11 | default: ['answer'], 12 | custom: ['./src/plugins/test'], 13 | }, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /packages/create-app/template-ts/plugins/test/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import type { TestOptions } from './options' 3 | 4 | /** 5 | * 这是一个测试插件 6 | */ 7 | export default (ctx: Bot, options: TestOptions) => { 8 | const { mirai } = ctx 9 | ctx.logger.info(options) 10 | mirai.on('message', async (msg) => { 11 | ctx.logger.info(msg) 12 | if (msg.plain === 'test') 13 | msg.reply('Link Start!') 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/logger/index.ts: -------------------------------------------------------------------------------- 1 | // import colors from 'picocolors' 2 | import { createLogger } from './winston' 3 | 4 | export * from './consola' 5 | export * from './winston' 6 | 7 | const defaultLogger = createLogger() 8 | /** 9 | * Bot 日志 10 | */ 11 | export const logger = defaultLogger.child({ label: '🤖' }) 12 | export const botLogger = logger 13 | /** 14 | * 插件日志 15 | */ 16 | export const pluginLogger = logger.child({ label: '🔌' }) 17 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/teach/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teach", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "问答学习", 6 | "el-bot": { 7 | "db": true 8 | }, 9 | "author": { 10 | "name": "YunYouJun", 11 | "url": "https://www.yunyoujun.cn", 12 | "email": "me@yunyoujun.cn" 13 | }, 14 | "exports": { 15 | ".": { 16 | "import": "./index.mjs", 17 | "require": "./index.js" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/@el-bot/plugin-niubi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/plugin-niubi", 3 | "version": "0.2.3", 4 | "description": "夸人牛逼", 5 | "author": "YunYouJun", 6 | "license": "AGPL-3.0", 7 | "exports": { 8 | ".": "./dist/index.js", 9 | "./options": "./dist/options.js", 10 | "./package.json": "./package.json" 11 | }, 12 | "main": "dist/index.js", 13 | "scripts": { 14 | "build": "tsc", 15 | "test": "echo '@某人 nb'" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "计数器", 6 | "el-bot": { 7 | "db": true 8 | }, 9 | "author": { 10 | "name": "YunYouJun", 11 | "url": "https://www.yunyoujun.cn", 12 | "email": "me@yunyoujun.cn" 13 | }, 14 | "exports": { 15 | ".": { 16 | "import": "./index.mjs", 17 | "require": "./index.js" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "docs/.vitepress/dist" 3 | command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run docs:build" 4 | 5 | [build.environment] 6 | # bypass npm auto install 7 | NPM_FLAGS = "--version" 8 | NODE_VERSION = "16" 9 | 10 | [[redirects]] 11 | from = "/*" 12 | to = "/index.html" 13 | status = 200 14 | 15 | [[headers]] 16 | for = "/manifest.webmanifest" 17 | 18 | [headers.values] 19 | Content-Type = "application/manifest+json" 20 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/blacklist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blacklist", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "黑名单", 6 | "el-bot": { 7 | "db": true 8 | }, 9 | "author": { 10 | "name": "YunYouJun", 11 | "url": "https://www.yunyoujun.cn", 12 | "email": "me@yunyoujun.cn" 13 | }, 14 | "exports": { 15 | ".": { 16 | "import": "./index.mjs", 17 | "require": "./index.js" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/qq-sdk/src/types/thread.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 帖子格式 3 | */ 4 | export enum THREAD_FORMAT { 5 | /** 6 | * 普通文本 7 | */ 8 | FORMAT_TEXT = 1, 9 | /** 10 | * HTML 11 | */ 12 | FORMAT_HTML = 2, 13 | /** 14 | * Markdown 15 | */ 16 | FORMAT_MARKDOWN = 3, 17 | /** 18 | * JSON(content参数可参照 [RichText](https://bot.q.qq.com/wiki/develop/api-v2/server-inter/channel/content/forum/model.html#richtext) 结构) 19 | */ 20 | FORMAT_JSON = 4, 21 | } 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | ignores: ['node_modules/', '**/node_modules/**/', 'dist/', '**/dist/**/', 'logs/', '**/logs/**/', 'docs/', '**/docs/**/', 'coverage', '**/coverage/**', 'mcl', '**/mcl/**', 'go-cqhttp', '**/go-cqhttp/**'], 5 | formatters: true, 6 | unocss: true, 7 | vue: true, 8 | }, { 9 | rules: { 10 | // for nest import 11 | 'ts/consistent-type-imports': 'off', 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /examples/simple/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "emitDecoratorMetadata": false, 6 | "rootDir": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler", 9 | "resolveJsonModule": true, 10 | "strict": true, 11 | "strictNullChecks": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "skipDefaultLibCheck": true, 15 | "skipLibCheck": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/el-bot/core/utils/error.ts: -------------------------------------------------------------------------------- 1 | import { logger, pluginLogger } from '../bot' 2 | import { isDev } from './misc' 3 | 4 | /** 5 | * 通用的异常处理 6 | */ 7 | export function handleError( 8 | e: any | Error, 9 | type: '' | 'plugin' = '', 10 | ) { 11 | if (!e) 12 | return 13 | 14 | if (isDev) 15 | console.error(e) 16 | 17 | if (e.message) { 18 | if (type) 19 | pluginLogger.error(e.message) 20 | else 21 | logger.error(e.message) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/qq-sdk/src/client/index.ts: -------------------------------------------------------------------------------- 1 | import { GetWsParam } from 'qq-guild-bot' 2 | import { Channels } from './channels' 3 | 4 | export * from './channels' 5 | 6 | /** 7 | * crete qq client api 8 | * 官方的 qq-guild-bot 很多类型与文档不符,且未更新如帖子等接口 9 | * 新的 qq-sdk 会提供更好的类型支持 10 | * 11 | * 在未来,将会发布为 qq-sdk npm 包 12 | */ 13 | export function createQQApi(options: GetWsParam) { 14 | const channels = new Channels(options) 15 | 16 | return { 17 | channels, 18 | // TODO 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/guide/logger.md: -------------------------------------------------------------------------------- 1 | # 日志系统 2 | 3 | 日志系统使用 [winston](https://github.com/winstonjs/winston) 实现。 4 | 5 | ```js 6 | import { Bot } from "el-bot"; 7 | /* 8 | * @param {Bot} ctx 9 | */ 10 | export default (ctx: Bot) => { 11 | ctx.logger.success("整挺好!"); 12 | }; 13 | ``` 14 | 15 | ```bash 16 | # 输出消息 17 | [el-bot] [success] 整挺好! 18 | # [SUCCESS] 为绿色 19 | ``` 20 | 21 | ## 类型 22 | 23 | - success: 成功信息 24 | - warning: 警告信息 25 | - error: 错误信息 26 | - info: 提示信息 27 | - debug: Debug 信息 28 | 29 | ## Todo 30 | 31 | 日志分级显示 32 | -------------------------------------------------------------------------------- /examples/simple/bot/plugins/ping.ts: -------------------------------------------------------------------------------- 1 | import { defineBotPlugin, pluginLogger } from 'el-bot' 2 | import { Structs } from 'node-napcat-ts' 3 | 4 | export default defineBotPlugin({ 5 | pkg: { 6 | name: 'ping', 7 | }, 8 | setup: (ctx) => { 9 | pluginLogger.info('ping 自己的插件日志') 10 | 11 | const { napcat } = ctx 12 | napcat.on('message', (msg) => { 13 | if (msg.raw_message === '捅死你') { 14 | ctx.reply(msg, [ 15 | Structs.text('我爱你'), 16 | ]) 17 | } 18 | }) 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/create-app/template-ts/README.md: -------------------------------------------------------------------------------- 1 | # template-ts 2 | 3 | el-bot 的 TypeScript 模版 4 | 5 | ## Start 6 | 7 | 在 [el.config.ts](./el.config.ts) 中配置你的机器人 8 | 9 | 在 [plugins](./plugins) 目录中配置你的插件 10 | 11 | ```sh 12 | yarn 13 | yarn start 14 | ``` 15 | 16 | ## Link MCL 17 | 18 | 如果你使用 [mcl](https://github.com/iTXTech/mirai-console-loader) 启动你的 mirai,你可以将其放置于当前目录下的 mcl 文件夹或链接至此处。 19 | 20 | (因为 mirai-api-http 1.x 尚未支持绝对路径发送图片/音频文件。) 21 | 22 | 如: 23 | 24 | ```sh 25 | ln -s /Users/yunyou/github/org/elpsycn/xiao-yun/mcl ./mcl 26 | ``` 27 | -------------------------------------------------------------------------------- /packages/@el-bot/plugin-niubi/src/options.ts: -------------------------------------------------------------------------------- 1 | import type { check } from 'mirai-ts' 2 | 3 | export interface NiubiOptions { 4 | /** 5 | * API URL 6 | */ 7 | url: string 8 | match: check.Match[] 9 | } 10 | 11 | const niubiOptions: NiubiOptions = { 12 | url: 'https://el-bot-api.vercel.app/api/words/niubi', 13 | match: [ 14 | { 15 | re: { 16 | pattern: '来点(\\S*)笑话', 17 | flags: 'i', 18 | }, 19 | }, 20 | { 21 | is: 'nb', 22 | }, 23 | ], 24 | } 25 | 26 | export default niubiOptions 27 | -------------------------------------------------------------------------------- /packages/qq-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qq-sdk", 3 | "version": "1.0.0", 4 | "exports": { 5 | ".": "./dist/index.mjs", 6 | "./package.json": "./package.json" 7 | }, 8 | "main": "./dist/index.mjs", 9 | "module": "./dist/index.mjs", 10 | "types": "./dist/index.d.mts", 11 | "scripts": { 12 | "build": "unbuild", 13 | "dev": "unbuild --stub" 14 | }, 15 | "dependencies": { 16 | "axios": "^1.8.4", 17 | "qq-guild-bot": "^2.9.5" 18 | }, 19 | "devDependencies": { 20 | "unbuild": "^3.5.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/api/status.md: -------------------------------------------------------------------------------- 1 | # 状态 status 2 | 3 | 你他娘的就是劳资的 Master 吗? 4 | 5 | status 命名空间下提供了几个辅助函数。 6 | 7 | ## isListening 是否监听 8 | 9 | 如果你的配置遵循了 [监听与目标](/guide/config.html#监听与目标),那么你可以调用它来快速判断是否处于监听状态。 10 | 11 | 譬如: 12 | 13 | ```yml 14 | test: 15 | listen: master 16 | ``` 17 | 18 | ```js 19 | export function(ctx, options) { 20 | const { mirai, status } = ctx.mirai; 21 | miria.on('message', (msg) => { 22 | if (status.isListening(msg.sender.id, options.listen)) { 23 | msg.reply('你他娘的就是劳资的 Master 吗?'); 24 | } 25 | }) 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/bots/bots.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common' 2 | import consola from 'consola' 3 | import { ElBotModule } from 'el-bot' 4 | import { BotsService } from './bots.service' 5 | // import { ElModule } from './el/el.module' 6 | 7 | @Module({ 8 | imports: [ 9 | // ElModule, 10 | ElBotModule.forRoot(), 11 | ], 12 | providers: [BotsService], 13 | }) 14 | export class BotsModule implements OnModuleInit { 15 | onModuleInit(): void { 16 | consola.info('BotsModule initialized') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/el-bot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "baseUrl": ".", 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "paths": { 9 | "el-bot": ["index.ts"] 10 | }, 11 | "resolveJsonModule": true, 12 | "types": [ 13 | "vite/client" 14 | ], 15 | "strict": true, 16 | "strictNullChecks": true, 17 | "noEmit": true, 18 | "esModuleInterop": true, 19 | "skipDefaultLibCheck": true, 20 | "skipLibCheck": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/el-bot/types/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 监听类型 3 | * - all: 所有人 4 | * - master: 主人 5 | * - admin: 管理员 6 | * - friend: 好友 7 | * - group: 群聊 8 | */ 9 | export type BaseListenType = 'all' | 'master' | 'admin' | 'friend' | 'group' 10 | 11 | /** 12 | * 目标对象 13 | */ 14 | export interface Target { 15 | /** 16 | * 好友 17 | */ 18 | friend?: number[] 19 | /** 20 | * 群聊 21 | */ 22 | group?: number[] 23 | } 24 | 25 | /** 26 | * 监听对象 27 | * @example 'master' 监听主人 28 | */ 29 | export type ListenTarget = Target | (BaseListenType | number)[] 30 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { VPTheme } from "vitepress-theme-you"; 2 | 3 | import "./custom.scss"; 4 | 5 | import ChatAvatar from "../components/ChatAvatar.vue"; 6 | import ChatMessage from "../components/ChatMessage.vue"; 7 | import ChatPanel from "../components/ChatPanel.vue"; 8 | 9 | import 'uno.css' 10 | 11 | export default Object.assign({}, VPTheme, { 12 | enhanceApp({ app }) { 13 | app.component("ChatAvatar", ChatAvatar); 14 | app.component("ChatMessage", ChatMessage); 15 | app.component("ChatPanel", ChatPanel); 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /docs/guide/napcat/index.md: -------------------------------------------------------------------------------- 1 | # Napcat 2 | 3 | ## Start 4 | 5 | ```bash 6 | # curl -o napcat.sh https://nclatest.znin.net/NapNeko/NapCat-Installer/main/script/install.sh && sudo bash napcat.sh 7 | 8 | sudo docker run -d -e ACCOUNT=996955042 -e WS_ENABLE=true -e NAPCAT_GID=0 -e NAPCAT_UID=0 -p 3001:3001 -p 6099:6099 --name napcat --restart=always docker.1panel.dev/mlikiowa/napcat-docker:latest 9 | ``` 10 | 11 | ## 升级 Docker 12 | 13 | ```bash 14 | # see https://github.com/NapNeko/NapCat-Docker 15 | docker pull docker.1panel.dev/mlikiowa/napcat-docker:latest 16 | ``` 17 | -------------------------------------------------------------------------------- /plugins/niubi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/plugin-niubi", 3 | "version": "0.3.1", 4 | "description": "夸人牛逼", 5 | "author": "YunYouJun", 6 | "license": "AGPL-3.0", 7 | "exports": { 8 | ".": "./dist/index.js" 9 | }, 10 | "main": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "prepublishOnly": "npm run build", 17 | "build": "tsup", 18 | "test": "echo '@某人 nb'" 19 | }, 20 | "devDependencies": { 21 | "el-bot": "workspace:*" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugins/setu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/plugin-setu", 3 | "version": "0.2.0", 4 | "description": "发点色图", 5 | "author": "YunYouJun", 6 | "license": "AGPL-3.0", 7 | "exports": { 8 | ".": "./dist/index.js" 9 | }, 10 | "main": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "scripts": { 13 | "prepublishOnly": "npm run build", 14 | "build": "tsup", 15 | "test": "echo '来点色图'" 16 | }, 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "devDependencies": { 21 | "el-bot": "workspace:*" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/ping/index.ts: -------------------------------------------------------------------------------- 1 | // export const name = 'ping' 2 | 3 | // export function apply(ctx) { 4 | // // 如果收到“天王盖地虎”,就回应“宝塔镇河妖” 5 | // ctx.middleware(async (session, next) => { 6 | // const replyMap: Record = { 7 | // '(': ')', 8 | // '(': ')', 9 | // '[': ']', 10 | // '[': ']', 11 | // '【': '】', 12 | // 'ping': 'pong', 13 | // } 14 | // let reply 15 | // if (session.content) 16 | // reply = replyMap[session.content] 17 | 18 | // return reply || next() 19 | // }) 20 | // } 21 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/plugins/types.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from '../index' 2 | 3 | export interface BotPlugin { 4 | /** 5 | * 插件信息 6 | */ 7 | pkg?: { 8 | /** 9 | * 若不填写,则默认从 package.json 中读取 10 | * 若 package.json 中不存在,则使用文件名 11 | */ 12 | name?: string 13 | version?: string 14 | description?: string 15 | keywords?: string[] 16 | } 17 | 18 | /** 19 | * 在插件加载时执行 20 | */ 21 | setup: (ctx: Bot, ...options: any[]) => Promise | void 22 | 23 | /** 24 | * 扩展 CLI 25 | */ 26 | extendCli?: (cli: Bot['cli']) => void 27 | } 28 | -------------------------------------------------------------------------------- /packages/el-bot/core/db/schemas/group.schema.ts: -------------------------------------------------------------------------------- 1 | import type { Document } from 'mongoose' 2 | import mongoose from 'mongoose' 3 | 4 | export interface IGroup extends Document { 5 | /** 6 | * 群名 7 | */ 8 | name: string 9 | /** 10 | * 群号 11 | */ 12 | groupId: number 13 | /** 14 | * 是否被封禁 15 | */ 16 | block?: boolean 17 | } 18 | 19 | export const groupSchema = new mongoose.Schema({ 20 | name: String, 21 | groupId: { type: Number, unique: true }, 22 | block: Boolean, 23 | }) 24 | 25 | export const Group = mongoose.model('Group', groupSchema) 26 | -------------------------------------------------------------------------------- /packages/create-app/template-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "allowJs": true, 8 | "strict": true, 9 | "outDir": "dist", 10 | "sourceMap": true, 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true 13 | }, 14 | "include": [ 15 | "index.ts", 16 | "package.json", 17 | "plugins/**/*", 18 | "plugins/**/*/package.json" 19 | ], 20 | "exclude": ["node_modules", "dist", "mcl"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/el-bot/core/composition-api/test.ts: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { createHooks } from 'hookable' 3 | 4 | // Create a hookable instance 5 | const hooks = createHooks() 6 | 7 | // Hook on 'hello' 8 | hooks.addHooks({ 9 | hello: () => { 10 | consola.info('Hello World 1') 11 | }, 12 | }) 13 | 14 | hooks.addHooks({ 15 | hello: () => { 16 | consola.info('Hello World 2') 17 | }, 18 | }) 19 | 20 | hooks.addHooks({ 21 | hello: () => { 22 | consola.info('Hello World 3') 23 | }, 24 | }) 25 | 26 | // Call 'hello' hook 27 | hooks.callHook('hello') 28 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/tqfs/index.ts: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { defineBotPlugin, onNapcatMessage } from 'el-bot' 3 | import colors from 'picocolors' 4 | 5 | export default defineBotPlugin({ 6 | setup(ctx) { 7 | const { napcat } = ctx 8 | onNapcatMessage(async (msg) => { 9 | consola.info('napcat message', msg) 10 | 11 | if (msg.raw_message === 'Get Login Info') { 12 | const data = await napcat.get_login_info() 13 | consola.info('当前登录账号:', `${colors.yellow(data.nickname)}(${colors.cyan(data.user_id)})`) 14 | } 15 | }) 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/memo/memo.schema.ts: -------------------------------------------------------------------------------- 1 | import type { Document } from 'mongoose' 2 | import mongoose from 'mongoose' 3 | 4 | export interface IMemo extends Document { 5 | time: string | Date 6 | /** 7 | * 内容 8 | */ 9 | content: string 10 | /** 11 | * 群 12 | */ 13 | group?: number 14 | /** 15 | * 好友 16 | */ 17 | friend?: number 18 | } 19 | 20 | export const memoSchema = new mongoose.Schema({ 21 | time: Date || String, 22 | content: String, 23 | group: Number, 24 | friend: Number, 25 | }) 26 | 27 | export const Memo = mongoose.model('Memo', memoSchema) 28 | -------------------------------------------------------------------------------- /packages/el-bot/templates/plugin-example/index.ts: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { defineBotPlugin, onNapcatMessage } from 'el-bot' 3 | import colors from 'picocolors' 4 | 5 | export default defineBotPlugin({ 6 | setup(ctx) { 7 | const { napcat } = ctx 8 | onNapcatMessage(async (msg) => { 9 | consola.info('napcat message', msg) 10 | 11 | if (msg.raw_message === 'Get Login Info') { 12 | const data = await napcat.get_login_info() 13 | consola.info('当前登录账号:', `${colors.yellow(data.nickname)}(${colors.cyan(data.user_id)})`) 14 | } 15 | }) 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /packages/el-bot/core/nest/service.ts: -------------------------------------------------------------------------------- 1 | import type { ElConfig } from '../config' 2 | 3 | import { Injectable, Logger, OnModuleInit } from '@nestjs/common' 4 | import { loadConfig } from 'c12' 5 | import { Bot } from '../bot' 6 | 7 | @Injectable() 8 | export class ElBotService implements OnModuleInit { 9 | public bot: Bot | undefined 10 | 11 | async onModuleInit() { 12 | const { config: elConfig } = await loadConfig({ 13 | configFile: 'el-bot.config', 14 | }) 15 | 16 | this.bot = new Bot(elConfig) 17 | 18 | // await this.bot.start() 19 | Logger.debug('Bot start') 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --c-brand: steelblue; 3 | --c-brand-light: #747bff; 4 | } 5 | 6 | * { 7 | scrollbar-color: var(--c-divider-light) var(--c-bg); 8 | } 9 | 10 | ::-webkit-scrollbar { 11 | width: 6px; 12 | } 13 | 14 | ::-webkit-scrollbar:horizontal { 15 | height: 6px; 16 | } 17 | 18 | ::-webkit-scrollbar-track { 19 | background: var(--c-bg); 20 | border-radius: 10px; 21 | } 22 | 23 | ::-webkit-scrollbar-thumb { 24 | background: var(--c-divider-light); 25 | border-radius: 10px; 26 | } 27 | 28 | ::-webkit-scrollbar-thumb:hover { 29 | background: var(--c-divider-dark); 30 | } 31 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/teach/teach.schema.ts: -------------------------------------------------------------------------------- 1 | import type { Document } from 'mongoose' 2 | import mongoose from 'mongoose' 3 | 4 | export interface ITeach extends Document { 5 | /** 6 | * 问题 7 | */ 8 | question: string 9 | /** 10 | * 回答 11 | */ 12 | answer: string 13 | /** 14 | * 更新时间 15 | */ 16 | updatedAt?: Date 17 | } 18 | 19 | export const teachSchema = new mongoose.Schema({ 20 | question: { type: String, required: true, unique: true }, 21 | answer: { type: String, required: true }, 22 | updatedAt: Date, 23 | }) 24 | 25 | export const Teach = mongoose.model('Teach', teachSchema) 26 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common' 2 | import { AppService } from './app.service' 3 | // import { ElBotModule } from 'el-bot' 4 | import { BotsModule } from './bots/bots.module' 5 | import { CoreModule } from './core/core.module' 6 | 7 | @Module({ 8 | imports: [ 9 | BotsModule, 10 | CoreModule, 11 | // ElBotModule.forRoot(), 12 | ], 13 | providers: [AppService], 14 | }) 15 | export class AppModule implements OnModuleInit { 16 | onModuleInit(): void { 17 | // eslint-disable-next-line no-console 18 | console.log('AppModule initialized') 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/main.ts: -------------------------------------------------------------------------------- 1 | // import { ValidationPipe } from '@nestjs/common' 2 | import { NestFactory } from '@nestjs/core' 3 | import consola from 'consola' 4 | import pkg from '../package.json' 5 | import { AppModule } from './app.module' 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule) 9 | // app.useGlobalPipes(new ValidationPipe()) 10 | 11 | app.enableShutdownHooks() 12 | await app.listen(3000) 13 | 14 | // const url = await app.getUrl() 15 | // consola.info(`Application is running on: ${url}`) 16 | consola.info(`Version: ${pkg.version}`) 17 | } 18 | 19 | bootstrap() 20 | -------------------------------------------------------------------------------- /plugins/niubi/README.md: -------------------------------------------------------------------------------- 1 | # 牛逼 niubi 2 | 3 | - 作者:[@YunYouJun](https://github.com/YunYouJun) 4 | - 简介:夸人牛逼 5 | - 触发条件:艾特人且文本中包含 `nb`,新人进群时 6 | 7 | 例如: 8 | 9 | ```md 10 | 云游君: @Niubi nb 11 | 12 | Bot: 世界上没有定理,只有 Niubi 允许其正确的命题。 13 | 14 | 云游君: 来点群主笑话 15 | 16 | Bot: 世界上没有定理,只有 群主 允许其正确的命题。 17 | ``` 18 | 19 | ## 描述 20 | 21 | 数据来源自 Jeff 笑话、高斯笑话。 22 | 23 | ## 配置 24 | 25 | - `url`: API 地址 或本地 JSON 数据的文件所在路径 26 | - `match` 见[配置讲解](https://docs.bot.elpsy.cn/config.html)。 27 | 28 | ```yaml 29 | niubi: 30 | url: https://el-bot-api.vercel.app/api/words/niubi 31 | match: 32 | - re: 33 | pattern: 来点(\S*)笑话 34 | - is: nb 35 | ``` 36 | -------------------------------------------------------------------------------- /packages/el-bot/core/nest/module.ts: -------------------------------------------------------------------------------- 1 | import type { DynamicModule } from '@nestjs/common' 2 | import type { ElConfig } from '../config' 3 | import { Module } from '@nestjs/common' 4 | import consola from 'consola' 5 | import { ElBotService } from './service' 6 | 7 | /** 8 | * @see https://docs.nestjs.com/modules#dynamic-modules 9 | */ 10 | @Module({ 11 | providers: [ElBotService], 12 | }) 13 | export class ElBotModule { 14 | static forRoot(_options?: ElConfig): DynamicModule { 15 | // const elConfig: ElConfig = {} 16 | consola.info('ElBotModule.forRoot') 17 | 18 | return { 19 | module: ElBotModule, 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/example-simple", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "private": true, 6 | "description": "A simple demo for el-bot.", 7 | "scripts": { 8 | "dev": "el-bot", 9 | "dev:w": "vite-node -w ../../packages/el-bot/bin/el-bot.ts", 10 | "build": "tsup", 11 | "start": "vite-node src/index.ts", 12 | "start:prod": "nodemon dist/index.js" 13 | }, 14 | "dependencies": { 15 | "axios": "^1.8.4" 16 | }, 17 | "devDependencies": { 18 | "el-bot": "workspace:*", 19 | "nodemon": "^3.1.9", 20 | "qq-sdk": "workspace:*", 21 | "tsup": "^8.4.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/nestjs-demo/src/plugins/card.ts: -------------------------------------------------------------------------------- 1 | import type { MessageType } from 'mirai-ts' 2 | import { Message, template } from 'mirai-ts' 3 | 4 | export function card(msg: MessageType.ChatMessage) { 5 | if (msg.plain === '卡片') { 6 | msg.reply([ 7 | Message.Xml( 8 | template.card({ 9 | type: 'bilibili', 10 | url: 'https://www.bilibili.com/video/BV1bs411b7aE', 11 | cover: 12 | 'https://cdn.jsdelivr.net/gh/YunYouJun/cdn/img/meme/love-er-ci-yuan-is-sick.jpg', 13 | summary: '咱是摘要', 14 | title: '咱是标题', 15 | brief: '咱是简介', 16 | }), 17 | ), 18 | ]) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/el-bot/node/cli/index.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import yargs from 'yargs' 3 | import { hideBin } from 'yargs/helpers' 4 | 5 | import { version } from '../../package.json' 6 | import { registerDevCommand } from './commands/dev' 7 | 8 | export const cli = yargs(hideBin(process.argv)) 9 | .scriptName('el') 10 | .scriptName('el-bot') 11 | .usage('Usage: $0 [options]') 12 | .version(version) 13 | .showHelpOnFail(true) 14 | .alias('h', 'help') 15 | .alias('v', 'version') 16 | 17 | registerDevCommand(cli) 18 | 19 | /** 20 | * run cli for bin 21 | */ 22 | export function run() { 23 | cli.help().parse() 24 | } 25 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { spawn } from 'node:child_process' 4 | import process from 'node:process' 5 | import { Command } from 'commander' 6 | import pkg from '../package.json' 7 | import registerInstallCommand from './install' 8 | import registerStartCommand from './start' 9 | 10 | const cli = new Command('el') 11 | cli.version(pkg.version) 12 | 13 | registerInstallCommand(cli) 14 | registerStartCommand(cli) 15 | 16 | // default 17 | cli 18 | .command('bot') 19 | .description('等价于 el start bot') 20 | .action(() => { 21 | spawn('el', ['start', 'bot'], { stdio: 'inherit' }) 22 | }) 23 | 24 | cli.parse(process.argv) 25 | -------------------------------------------------------------------------------- /packages/create-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/create-app", 3 | "version": "0.0.3", 4 | "description": "Create El Bot", 5 | "author": { 6 | "name": "云游君", 7 | "email": "me@yunyoujun.cn", 8 | "url": "https://www.yunyoujun.cn" 9 | }, 10 | "license": "AGPL-3.0", 11 | "homepage": "https://github.com/YunYouJun/el-bot/blob/dev/packages/create-app#readme", 12 | "main": "index.js", 13 | "bin": { 14 | "create-el-bot": "dist/index.js" 15 | }, 16 | "scripts": { 17 | "build": "tsup" 18 | }, 19 | "dependencies": { 20 | "enquirer": "^2.4.1", 21 | "picocolors": "^1.1.1" 22 | }, 23 | "devDependencies": { 24 | "tsup": "^8.4.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/el-bot/core/node/utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import fs from 'fs-extra' 3 | 4 | /** 5 | * 寻找文件 6 | * @param dir 7 | * @param formats 8 | * @param pathOnly 9 | */ 10 | export function lookupFile( 11 | dir: string, 12 | formats: string[], 13 | pathOnly = false, 14 | ): string | undefined { 15 | for (const format of formats) { 16 | const fullPath = path.join(dir, format) 17 | if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) 18 | return pathOnly ? fullPath : fs.readFileSync(fullPath, 'utf-8') 19 | } 20 | const parentDir = path.dirname(dir) 21 | if (parentDir !== dir) 22 | return lookupFile(parentDir, formats, pathOnly) 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v3 18 | with: 19 | run_install: true 20 | 21 | - name: Use Node.js LTS 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 'lts/*' 25 | registry-url: https://registry.npmjs.org/ 26 | cache: pnpm 27 | 28 | - name: Lint 29 | run: pnpm lint 30 | 31 | - name: TypeCheck 32 | run: pnpm typecheck 33 | -------------------------------------------------------------------------------- /plugins/feeder/src/feeder.scheme.ts: -------------------------------------------------------------------------------- 1 | import type { Document } from 'mongoose' 2 | import mongoose from 'mongoose' 3 | 4 | /** 5 | * 订阅者 6 | */ 7 | export interface Subscriber { 8 | qq: number 9 | groupId?: number 10 | } 11 | 12 | export interface IFeeder extends Document { 13 | /** 14 | * 链接 15 | */ 16 | url: string 17 | /** 18 | * 刷新时间 s 默认 1000s 19 | */ 20 | refresh: number 21 | /** 22 | * 目标对象 23 | */ 24 | targets: Subscriber[] 25 | } 26 | 27 | export const feederSchema = new mongoose.Schema({ 28 | url: { type: String, unique: true }, 29 | refresh: Number, 30 | target: Array, 31 | }) 32 | 33 | export const Feeder = mongoose.model('Feeder', feederSchema) 34 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/demo", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "description": "A dev demo for el-bot.", 6 | "scripts": { 7 | "dev": "bun run watch", 8 | "dev:hmr": "bun --hot bot/index.ts", 9 | "dev:v": "vite-node -w ../packages/el-bot/bin/el-bot.ts", 10 | "build": "tsup", 11 | "pm2": "pm2 start pm2.config.cjs", 12 | "start": "vite-node bot/index.ts", 13 | "start:cli": "el-bot", 14 | "start:prod": "nodemon dist/index.js", 15 | "watch": "bun --watch bot/index.ts" 16 | }, 17 | "devDependencies": { 18 | "el-bot": "workspace:*", 19 | "nodemon": "^3.1.9", 20 | "qq-sdk": "workspace:*", 21 | "tsup": "^8.4.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/bot/plugins/ping.ts: -------------------------------------------------------------------------------- 1 | import { defineBotPlugin, onMessage, pluginLogger } from 'el-bot' 2 | import { Structs } from 'node-napcat-ts' 3 | 4 | export default defineBotPlugin({ 5 | pkg: { 6 | name: 'ping', 7 | version: '0.0.1', 8 | }, 9 | setup: (ctx) => { 10 | onMessage(async (msg) => { 11 | pluginLogger 12 | .child({ plugin: 'ping' }) 13 | .info('这是 ping 自己的插件日志') 14 | 15 | if (msg.raw_message === 'ping') { 16 | await ctx.reply(msg, [ 17 | Structs.text('pong'), 18 | ]) 19 | } 20 | if (msg.raw_message === '1') { 21 | await ctx.reply(msg, [ 22 | Structs.text('2'), 23 | ]) 24 | } 25 | }) 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /packages/el-bot/core/napcat/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { AllHandlers, NCWebsocket } from 'node-napcat-ts' 3 | 4 | function handler(context: AllHandlers['message']) { 5 | console.log(context.message) 6 | } 7 | 8 | async function main() { 9 | const napcat = new NCWebsocket({ 10 | // 3001 11 | protocol: 'ws', 12 | host: '127.0.0.1', 13 | port: 3001, 14 | accessToken: 'yunyoujun', 15 | }, true) 16 | 17 | console.log(napcat) 18 | // napcat.connect() 19 | napcat 20 | .on('message.group', handler) 21 | 22 | napcat.connect() 23 | // await napcat.send_msg({ 24 | // user_id: 910426929, 25 | // message: [Structs.text('Hello')], 26 | // }) 27 | } 28 | 29 | main() 30 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/counter/counter.schema.ts: -------------------------------------------------------------------------------- 1 | import type { check } from 'mirai-ts' 2 | import type { Document } from 'mongoose' 3 | import mongoose from 'mongoose' 4 | 5 | export interface ICounter extends Document { 6 | /** 7 | * 匹配规则 8 | */ 9 | match: check.Match 10 | /** 11 | * 今日出现次数 12 | */ 13 | today?: number 14 | /** 15 | * 至今出现次数 16 | */ 17 | total: number 18 | } 19 | 20 | export const counterSchema = new mongoose.Schema({ 21 | match: { type: Object, unique: true }, 22 | today: Number, 23 | total: Number, 24 | }) 25 | 26 | // auto generate createdAt & updatedAt 27 | counterSchema.set('timestamps', true) 28 | 29 | export const Counter = mongoose.model('Counter', counterSchema) 30 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@el-bot/docs", 4 | "description": "Docs for el-bot.", 5 | "repository": "https://github.com/YunYouJun/el-bot/docs", 6 | "homepage": "https://docs.bot.elpsy.cn", 7 | "author": { 8 | "name": "云游君", 9 | "email": "me@yunyoujun.cn", 10 | "url": "https://www.yunyoujun.cn" 11 | }, 12 | "scripts": { 13 | "dev": "vitepress dev . --open --host", 14 | "build": "vitepress build", 15 | "serve": "vitepress serve" 16 | }, 17 | "license": "AGPL-3.0", 18 | "dependencies": { 19 | "sass": "^1.86.0", 20 | "vitepress": "^1.6.3", 21 | "vitepress-theme-you": "^0.1.0-alpha.1" 22 | }, 23 | "devDependencies": { 24 | "@iconify-json/ri": "^1.2.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/nestjs-demo/el-bot.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import process from 'node:process' 3 | import dotenv from 'dotenv' 4 | import { defineConfig } from 'el-bot' 5 | import botConfig from './config/bot' 6 | 7 | dotenv.config({ 8 | path: path.resolve(process.cwd(), '.env'), 9 | }) 10 | 11 | export default defineConfig({ 12 | qq: Number.parseInt(process.env.BOT_QQ || ''), 13 | setting: './mcl/config/net.mamoe.mirai-api-http/setting.yml', 14 | db: { 15 | enable: process.env.EL_DB_ENABLE === 'true', 16 | uri: process.env.BOT_DB_URI, 17 | analytics: true, 18 | }, 19 | bot: botConfig, 20 | webhook: { 21 | enable: true, 22 | path: '/webhook', 23 | port: 7777, 24 | secret: 'el-psy-congroo', 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /packages/el-bot/core/composition-api/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from '../../types' 2 | 3 | /** 4 | * Current bot instance 5 | * @internal 6 | */ 7 | // eslint-disable-next-line import/no-mutable-exports 8 | export let currentInstance: Bot | null = null 9 | 10 | /** 11 | * @internal 12 | */ 13 | export function setCurrentInstance(instance: Bot | null) { 14 | currentInstance = instance 15 | } 16 | 17 | /** 18 | * @internal 19 | */ 20 | export function unsetCurrentInstance() { 21 | currentInstance = null 22 | } 23 | 24 | /** 25 | * @internal 26 | */ 27 | export function getCurrentInstance() { 28 | if (!currentInstance) { 29 | throw new Error('getCurrentInstance called when no active instance.') 30 | } 31 | return currentInstance 32 | } 33 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/nbnhhsh/README.md: -------------------------------------------------------------------------------- 1 | # nbnhhsh 2 | 3 | > 能不能好好说话? 4 | 5 | 参考自 [itorr/nbnhhsh](https://github.com/itorr/nbnhhsh)。 6 | 7 | - 网页版本: 8 | - GitHub: 9 | - API: 10 | 11 | ```sh 12 | POST https://lab.magiconch.com/api/nbnhhsh/guess {text: "nb"} 13 | ``` 14 | 15 | ```json 16 | [ 17 | { 18 | "name": "nb", 19 | "trans": [ 20 | "牛逼", 21 | "Nice Boat", 22 | "脓包", 23 | "南北", 24 | "nice body", 25 | "新百伦", 26 | "no bomb", 27 | "嫩逼", 28 | "nobody", 29 | "内部", 30 | "那边" 31 | ] 32 | } 33 | ] 34 | ``` 35 | 36 | ## Usage 37 | 38 | ```sh 39 | el nbnhhsh nb 40 | ``` 41 | -------------------------------------------------------------------------------- /packages/el-bot/scripts/plugins.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import fs from 'fs-extra' 4 | 5 | import { getAllPlugins } from './utils' 6 | 7 | const plugins = getAllPlugins() 8 | 9 | const __dirname = dirname(fileURLToPath(import.meta.url)) 10 | 11 | // 批量设置 type = module 12 | plugins.forEach((item) => { 13 | const source = path.resolve(__dirname, '../src/plugins/', item, 'package.json') 14 | const pkg = JSON.parse(fs.readFileSync(source, 'utf-8')) 15 | delete pkg.type 16 | 17 | console.log(pkg) 18 | pkg.exports = { 19 | '.': { 20 | require: './index.js', 21 | import: './index.mjs', 22 | }, 23 | } 24 | fs.writeFileSync(source, JSON.stringify(pkg, null, 2)) 25 | }) 26 | -------------------------------------------------------------------------------- /plugins/setu/README.md: -------------------------------------------------------------------------------- 1 | # 色图 setu 2 | 3 | - 作者:[@YunYouJun](https://github.com/YunYouJun) 4 | - 简介:发点色图 5 | 6 | 例如: 7 | 8 | ```md 9 | 云游君: 来点色图 10 | 11 | Bot: [假装我是一张真正的色图] 12 | ``` 13 | 14 | - `url`: API 地址 或本地 JSON 数据的文件所在路径 15 | - `proxy`: 图片链接代理(可能因为种种原因,你的 API 获取的图片链接需要代理) 16 | - `match` 见[配置讲解](https://docs.bot.elpsy.cn/config.html)。 17 | - `reply`: 默认的回复文本消息 18 | 19 | > 不需要自定义时,不需要配置,默认使用 `https://el-bot-api.vercel.app/api/setu`。 20 | 21 | 另一个 [色图 API](https://api.lolicon.app/#/setu):https://api.lolicon.app/setu/ 22 | 23 | ```yaml 24 | setu: 25 | url: https://el-bot-api.vercel.app/api/setu 26 | # proxy: https://images.weserv.nl/?url= 27 | match: 28 | - is: 不够色 29 | - includes: 30 | - 来点 31 | - 色图 32 | reply: 让我找找 33 | ``` 34 | -------------------------------------------------------------------------------- /packages/@el-bot/plugin-niubi/README.md: -------------------------------------------------------------------------------- 1 | # 牛逼 niubi 2 | 3 | 移植自 [niubi | el-bot-plugins](https://github.com/ElpsyCN/el-bot-plugins/blob/master/packages/niubi/),使用 TS 重写。 4 | 5 | - 作者:[@YunYouJun](https://github.com/YunYouJun) 6 | - 简介:夸人牛逼 7 | - 触发条件: 8 | - 艾特人且文本中包含 `nb` 9 | - 新人进群时 10 | 11 | 例如: 12 | 13 | ```md 14 | 云游君: @Niubi nb 15 | 16 | Bot: 世界上没有定理,只有 Niubi 允许其正确的命题。 17 | 18 | 云游君: 来点群主笑话 19 | 20 | Bot: 世界上没有定理,只有 群主 允许其正确的命题。 21 | ``` 22 | 23 | ## 描述 24 | 25 | 数据来源自 Jeff 笑话、高斯笑话。 26 | 27 | ## 配置 28 | 29 | - `url`: API 地址 或本地 JSON 数据的文件所在路径 30 | - `match` 见[配置讲解](https://docs.bot.elpsy.cn/config.html)。 31 | 32 | ```yaml 33 | niubi: 34 | url: https://el-bot-api.vercel.app/api/words/niubi 35 | match: 36 | - re: 37 | pattern: 来点(\S*)笑话 38 | - is: nb 39 | ``` 40 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: YunYouJun # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://sponsors.yunyoujun.cn # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/api.yml: -------------------------------------------------------------------------------- 1 | name: api 2 | on: 3 | push: 4 | branches: 5 | - dev 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Install pnpm 14 | uses: pnpm/action-setup@v3 15 | with: 16 | run_install: true 17 | 18 | - name: Setup Node 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: lts/* 22 | 23 | # TODO: fix type 24 | # - name: Build API 25 | # run: pnpm run build:api 26 | 27 | # - name: Deploy 28 | # uses: peaceiris/actions-gh-pages@v3 29 | # with: 30 | # github_token: ${{ secrets.GITHUB_TOKEN }} 31 | # publish_dir: ./packages/el-bot/docs 32 | # force_orphan: true 33 | -------------------------------------------------------------------------------- /plugins/twitter/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://github.com/Rishikant181/Rettiwt-API 3 | */ 4 | 5 | import process from 'node:process' 6 | import { consola } from 'el-bot' 7 | import { Rettiwt } from 'rettiwt-api' 8 | 9 | if (!process.env.RETTIWT_API_KEY) { 10 | consola.error('Please set RETTIWT_API_KEY in .env') 11 | process.exit(1) 12 | } 13 | 14 | const rettiwt = new Rettiwt({ 15 | apiKey: process.env.RETTIWT_API_KEY, 16 | }) 17 | 18 | // const username = 'YunYouJun' 19 | const username = 'yyjmoe' 20 | rettiwt.user.details(username) 21 | .then((details) => { 22 | consola.info(details) 23 | }) 24 | 25 | rettiwt.tweet.search({ 26 | fromUsers: [username], 27 | }) 28 | .then((tweets) => { 29 | consola.info(tweets) 30 | }) 31 | .catch((err) => { 32 | consola.error(err) 33 | }) 34 | -------------------------------------------------------------------------------- /packages/el-bot/core/db/schemas/friend.schema.ts: -------------------------------------------------------------------------------- 1 | import type { Document, Model } from 'mongoose' 2 | import mongoose from 'mongoose' 3 | 4 | export interface IFriend extends Document { 5 | /** 6 | * 备注姓名 7 | */ 8 | name: string 9 | /** 10 | * 触发者 QQ 11 | */ 12 | qq: number 13 | /** 14 | * 总计触发次数 15 | */ 16 | total: number 17 | /** 18 | * 上次触发时间 19 | */ 20 | lastTriggerTime: Date 21 | /** 22 | * 是否被封禁 23 | */ 24 | block?: boolean 25 | } 26 | 27 | export const friendSchema = new mongoose.Schema({ 28 | name: String, 29 | qq: { type: Number, unique: true }, 30 | total: Number, 31 | lastTriggerTime: Date, 32 | block: Boolean, 33 | }) 34 | 35 | export const Friend: Model 36 | = mongoose.models.Friend || mongoose.model('Friend', friendSchema) 37 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | # 核心 API 2 | 3 | [![api](https://github.com/YunYouJun/el-bot/workflows/api/badge.svg)](https://www.yunyoujun.cn/el-bot/) 4 | 5 | el-bot 的 [API 文档](https://www.yunyoujun.cn/el-bot/) 已通过 [typedoc](https://typedoc.org/) 自动生成。 6 | 7 | 为了更好地帮助你了解及进行示例展示,将会对一些较为有用地 API 方法进行介绍。 8 | 9 | ## Context 上下文 10 | 11 | 机器人所有的相关内容均被绑定于 `ctx` 上,它是 `el-bot` 实例化后的自身。这也是开发机器人插件时你默认所能获得到的内容。 12 | 13 | `mirai` 本身便是 `ctx` 中的一个属性,即实例化后的 [mirai-ts](https://github.com/YunYouJun/mirai-ts)。 14 | 15 | 因此你可以借助它来实现与 mirai-api-http 的一切交互。这也意味着除此之外的便是 `el-bot` 的扩展功能(及其存在的意义)。 16 | 17 | ```ts 18 | import { Bot } from "el-bot"; 19 | const bot = new Bot(); 20 | 21 | function test(ctx) { 22 | const { mirai } = ctx; 23 | mirai.on("message", (msg) => { 24 | console.log(msg); 25 | }); 26 | } 27 | 28 | bot.use(test); 29 | ``` 30 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/memo/utils.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | // eslint-disable-next-line prefer-regex-literals 4 | const timeRegExp = new RegExp('^(\\d+d)?(\\d+h)?(\\d+m)?$', 'i') 5 | 6 | /** 7 | * 解析时间 8 | * @param time example: 1d23h50m 9 | */ 10 | export function parseTime(time: string) { 11 | const matches = timeRegExp.exec(time) 12 | if (matches) { 13 | return { 14 | day: Number.parseInt(matches[1]) || 0, 15 | hour: Number.parseInt(matches[2]) || 0, 16 | minute: Number.parseInt(matches[3]) || 0, 17 | } 18 | } 19 | else { 20 | return null 21 | } 22 | } 23 | 24 | /** 25 | * 检查时间是否超过限额(不得超过一年) 26 | * @param time 27 | */ 28 | export function checkTime(time: Date) { 29 | const maxTime = dayjs().add(1, 'year') 30 | return time.valueOf() < maxTime.valueOf() 31 | } 32 | -------------------------------------------------------------------------------- /examples/nestjs-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2020", 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "baseUrl": ".", 8 | "module": "ESNext", 9 | "moduleResolution": "Node", 10 | "paths": { 11 | "el-bot": ["../packages/el-bot/src/index.ts"] 12 | }, 13 | "strict": true, 14 | "declaration": true, 15 | "outDir": "./dist", 16 | "removeComments": true, 17 | "esModuleInterop": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "skipLibCheck": true 20 | }, 21 | "include": [ 22 | "src/**/*.ts", 23 | "**/*/package.json", 24 | "src/plugins/**/*", 25 | "el-bot.config.ts" 26 | ], 27 | "exclude": ["dist", "node_modules", "test", "**/*spec.ts"] 28 | } 29 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/limit/options.ts: -------------------------------------------------------------------------------- 1 | export interface LimitOptions { 2 | /** 3 | * 间隔 4 | */ 5 | interval: number 6 | /** 7 | * 数量 8 | */ 9 | count: number 10 | /** 11 | * 发送者 12 | */ 13 | sender: { 14 | /** 15 | * 超过时间清空记录 16 | */ 17 | interval: number 18 | /** 19 | * 连续次数 20 | */ 21 | maximum: number 22 | /** 23 | * 提示 24 | */ 25 | tooltip: string 26 | /** 27 | * 禁言时间 28 | */ 29 | time: number 30 | } 31 | } 32 | 33 | /** 34 | * 默认配置 35 | */ 36 | const limitOptions: LimitOptions = { 37 | interval: 30000, 38 | count: 20, 39 | sender: { 40 | // 超过十分钟清空记录 41 | interval: 600000, 42 | // 连续次数 43 | maximum: 5, 44 | tooltip: '我生气了', 45 | // 禁言时间 46 | time: 600, 47 | }, 48 | } 49 | 50 | export default limitOptions 51 | -------------------------------------------------------------------------------- /packages/qq-sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export * from './client' 4 | export * from './constants' 5 | export * from './types' 6 | 7 | export const DOMAINS = { 8 | /** 9 | * 获取调用凭证 10 | * 不区分正式环境、沙箱环境 11 | */ 12 | TOKEN: 'https://bots.qq.com', 13 | /** 14 | * 正式环境 15 | */ 16 | PRODUCTION: 'https://api.sgroup.qq.com', 17 | /** 18 | * 沙箱环境地址只会收到在开发者平台配置的沙箱频道、沙箱私信QQ号、沙箱群、沙箱单聊QQ号的事件,且调用openapi仅能操作沙箱环境 19 | */ 20 | SANDBOX: 'https://sandbox.api.sgroup.qq.com', 21 | } 22 | 23 | /** 24 | * 获取调用凭证 25 | */ 26 | export async function getAppAccessToken(params: { 27 | appId: string 28 | clientSecret: string 29 | }) { 30 | const { data } = await axios.post(`${DOMAINS.TOKEN}/app/getAppAccessToken`, { 31 | appId: params.appId, 32 | clientSecret: params.clientSecret, 33 | }) 34 | return data 35 | } 36 | -------------------------------------------------------------------------------- /packages/el-bot/core/db/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import type { dbConfig } from '../config/el' 3 | import mongoose from 'mongoose' 4 | import { analytics } from './analytics' 5 | 6 | export async function connectDb(bot: Bot, dbConfig: dbConfig): Promise { 7 | if (!dbConfig.enable) 8 | return 9 | 10 | const uri = dbConfig.uri || 'mongodb://localhost:27017/el-bot' 11 | 12 | const dbName = 'MongoDB 数据库' 13 | bot.logger.info(`开始连接 ${dbName}`) 14 | 15 | mongoose.connect(uri) 16 | 17 | const db = mongoose.connection 18 | bot.db = db 19 | 20 | db.on('error', () => { 21 | bot.logger.error(`${dbName}连接失败`) 22 | }) 23 | db.once('open', () => { 24 | bot.logger.success(`${dbName}连接成功`) 25 | }) 26 | 27 | if (!db) 28 | return 29 | 30 | // 分析统计 31 | if (dbConfig.analytics) 32 | analytics(bot) 33 | } 34 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/report/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import type * as Config from '../../types/config' 3 | import { renderString } from '../../utils' 4 | 5 | interface ReportOptions { 6 | /** 7 | * 报告事件类型 8 | */ 9 | type: string 10 | /** 11 | * 报告对象 12 | */ 13 | target: Config.Target 14 | /** 15 | * 报告消息模版 16 | */ 17 | content: string 18 | } 19 | 20 | export default function (ctx: Bot, options: ReportOptions[] = []) { 21 | if (!ctx.webhook) { 22 | ctx.logger.error('[report] 您须先开启 webhook') 23 | return 24 | } 25 | options.forEach((option) => { 26 | ctx.webhook?.on(option.type, (data: any) => { 27 | ctx.logger.debug(data) 28 | const text = renderString(option.content, data, 'data') 29 | if (option.target) 30 | ctx.sender.sendMessageByConfig(text, option.target) 31 | }) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /docs/api/utils.md: -------------------------------------------------------------------------------- 1 | # 辅助工具 utils 2 | 3 | 一些开发时的辅助函数 4 | 5 | > v0.7.0-alpha.2 新增,尚未稳定 6 | 7 | ## 内部模式 8 | 9 | 你可以使用它来控制是否进入插件的内部模式。 10 | 11 | 所谓的内部模式,即当你简单的编写回复逻辑时,机器人不会对当前的群或好友进行判断。 12 | 这就会导致你在 A 群发送搜图,在 B 群发送图片,机器人会在 B 群返回搜图的结果。 13 | 14 | 使用内部模式后,即你在 A 群发送搜图,机器人只在 A 群存在搜图模式,与 B 群互不影响。 15 | 16 | 示例如下: 17 | 18 | ```js 19 | import { utils } from "el-bot"; 20 | const innerMode = new utils.InnerMode(); 21 | 22 | export default async function() { 23 | mirai.on("message", (msg) => { 24 | // 传入当前的消息 25 | innerMode.setMsg(msg); 26 | 27 | // 进入搜图模式 28 | if (msg.plain === "搜图") { 29 | // 进入内部 30 | innerMode.enter(); 31 | msg.reply("我准备好了!"); 32 | } 33 | 34 | // 如果当前为内部模式 35 | if (innerMode.getStatus()) { 36 | // 做搜图操作 37 | fakeSearchImage(); 38 | 39 | // 退出搜图模式 40 | innerMode.exit(); 41 | } 42 | }); 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /packages/el-bot/core/config/bot.ts: -------------------------------------------------------------------------------- 1 | // import type { AnswerOptions } from '../plugins/answer' 2 | // import type { ForwardOptions } from '../plugins/forward' 3 | 4 | import { BotPlugin } from '../bot' 5 | 6 | export interface BotConfig { 7 | /** 8 | * 机器人名 9 | */ 10 | name: string 11 | 12 | /** 13 | * 是否自动加载 plugins 文件夹下的自定义插件,默认目录为 ['plugins'] 14 | */ 15 | autoloadPlugins: boolean 16 | /** 17 | * 默认自动加载插件的目录 18 | * @default 'bot/plugins' 19 | */ 20 | pluginDir: string 21 | 22 | /** 23 | * 插件配置 24 | */ 25 | plugins: BotPlugin[] 26 | 27 | /** 28 | * 主人(超级管理员) 29 | */ 30 | master: number[] 31 | /** 32 | * 管理员 33 | */ 34 | admin: number[] 35 | 36 | /** 37 | * 开发测试群 38 | */ 39 | devGroup: number 40 | 41 | /** 42 | * 其他插件配置 43 | */ 44 | [propName: string]: any 45 | } 46 | 47 | export type BotUserConfig = Partial 48 | -------------------------------------------------------------------------------- /packages/create-app/template-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "el-bot-typescript-starter", 3 | "version": "0.0.1", 4 | "author": { 5 | "name": "YunYouJun", 6 | "email": "me@yunyoujun.cn", 7 | "url": "https://www.yunyoujun.cn" 8 | }, 9 | "license": "AGPL-3.0", 10 | "scripts": { 11 | "prebuild": "npm run clean", 12 | "build": "tsc", 13 | "clean": "rm -rf dist", 14 | "dev": "run-p watch start", 15 | "lint": "eslint \"**/*.{ts,js}\"", 16 | "lint:fix": "eslint \"**/*.{ts,js}\" --fix", 17 | "start": "cd dist && nodemon index.js", 18 | "start:bot": "el start bot", 19 | "start:prod": "npm run build && npm run start", 20 | "watch": "tsc -w --incremental" 21 | }, 22 | "dependencies": { 23 | "el-bot": "workspace:^0.9.0-beta.5" 24 | }, 25 | "devDependencies": { 26 | "nodemon": "^3.1.9", 27 | "npm-run-all": "^4.1.5", 28 | "typescript": "^5.8.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/el-bot/node/server/webhook/types.ts: -------------------------------------------------------------------------------- 1 | import { createNodeMiddleware } from '@octokit/webhooks' 2 | 3 | /** 4 | * GitHub Webhooks Specific 5 | * @see https://github.com/octokit/webhooks 6 | */ 7 | export interface OctokitOptions { 8 | /** 9 | * used when `new Webhooks({ secret })` 10 | * Required. Secret as configured in GitHub Settings. 11 | * @see https://github.com/user/repo/settings/hooks/new 12 | */ 13 | secret: string 14 | /** 15 | * path @default /api/github/webhooks 16 | */ 17 | middlewareOptions?: Parameters[1] 18 | } 19 | 20 | export interface WebhooksOptions { 21 | /** 22 | * 是否启用 23 | */ 24 | enable?: boolean 25 | port?: number 26 | /** 27 | * GitHub Webhooks Specific 28 | * @see https://github.com/octokit/webhooks 29 | */ 30 | octokit: OctokitOptions 31 | /** 32 | * 回调函数 33 | */ 34 | callback?: (webhook: any) => void 35 | } 36 | -------------------------------------------------------------------------------- /packages/cli/src/install/index.ts: -------------------------------------------------------------------------------- 1 | import { logger } from 'packages/el-bot' 2 | 3 | /** 4 | * @deprecated 5 | */ 6 | export default function (cli: any) { 7 | cli 8 | .command('install [project]') 9 | .description('安装依赖') 10 | .alias('i') 11 | .action((project: string) => { 12 | if (project === 'mirai') 13 | installMirai() 14 | }) 15 | } 16 | 17 | /** 18 | * 安装 mirai 19 | * @deprecated 20 | */ 21 | function installMirai() { 22 | logger.warning( 23 | '这只是 mirai-api-http 辅助的安装脚本,你完全可以自行启动 mirai 而无需使用它。(本脚本默认已将 mirai-console-loader 放置于当前 mcl 文件夹中。)', 24 | ) 25 | logger.info( 26 | '推荐使用官方启动器 mirai-console-loader ( https://github.com/iTXTech/mirai-console-loader )', 27 | ) 28 | logger.info( 29 | '\nel-bot 基于 mirai-api-http 且专注于机器人本身逻辑,但不提供任何关于如何下载启动 mirai 的解答,你应该自行掌握如何使用 mirai。\n在使用 el-bot 过程中遇到的问题,欢迎提 ISSUE,或加入我们的 QQ群 : 707408530 / TG群: https://t.me/elpsy_cn。', 30 | ) 31 | 32 | // @deprecated mirai prompt 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | id-token: write 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v3 22 | with: 23 | run_install: true 24 | 25 | # after pnpm 26 | - name: Use Node.js LTS 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 'lts/*' 30 | registry-url: https://registry.npmjs.org/ 31 | cache: pnpm 32 | 33 | - run: cd packages/el-bot && npm publish --access public 34 | env: 35 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 36 | NPM_CONFIG_PROVENANCE: true 37 | 38 | - run: npx changelogithub 39 | env: 40 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 41 | -------------------------------------------------------------------------------- /packages/el-bot/README.md: -------------------------------------------------------------------------------- 1 | # el-bot 2 | 3 | [![docs](https://github.com/ElpsyCN/el-bot-docs/workflows/docs/badge.svg)](https://docs.bot.elpsy.cn/) 4 | [![api](https://github.com/YunYouJun/el-bot/workflows/api/badge.svg)](https://www.yunyoujun.cn/el-bot/) 5 | [![npm](https://img.shields.io/npm/v/el-bot?logo=npm)](https://www.npmjs.com/package/el-bot) 6 | [![GitHub package.json dependency version (subfolder of monorepo)](https://img.shields.io/github/package-json/dependency-version/YunYouJun/el-bot/mirai-ts?filename=packages%2Fel-bot%2Fpackage.json&logo=typescript)](https://github.com/YunYouJun/mirai-ts) 7 | ![node-current](https://img.shields.io/node/v/el-bot) 8 | 9 | More info see [README.md](https://github.com/YunYouJun/el-bot#readme). 10 | 11 | ## Napcat 12 | 13 | > [NapCatQQ](https://napneko.github.io/zh-CN/) 14 | 15 | ```bash 16 | # https://napneko.github.io/zh-CN/guide/getting-started 17 | curl -o napcat.sh https://nclatest.znin.net/NapNeko/NapCat-Installer/main/script/install.sh && sudo bash napcat.sh 18 | ``` 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | "target": "ESNext", 7 | "lib": [ 8 | "ESNext" 9 | ], 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "baseUrl": ".", 13 | "module": "ESNext", 14 | "moduleResolution": "Bundler", 15 | "paths": { 16 | "el-bot": ["packages/el-bot/index.ts"], 17 | "qq-sdk": ["packages/qq-sdk/src/index.ts"] 18 | }, 19 | // custom 20 | "resolveJsonModule": true, 21 | "allowJs": true, 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "declaration": true, 25 | "outDir": "./dist", 26 | "removeComments": true, 27 | "allowSyntheticDefaultImports": true, 28 | "esModuleInterop": true, 29 | "forceConsistentCasingInFileNames": true, 30 | "skipLibCheck": true 31 | }, 32 | "include": ["**/*.ts"], 33 | "exclude": ["dist", "node_modules", "**/*.spec.ts"] 34 | } 35 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/counter/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import type { MessageType } from 'mirai-ts' 3 | import type { ICounter } from './counter.schema' 4 | import { check } from 'mirai-ts' 5 | import { Counter } from './counter.schema' 6 | 7 | /** 8 | * 根据匹配规则统计关键字 9 | * @param msg 10 | * @param option 11 | */ 12 | async function countKeyword(msg: MessageType.ChatMessage, option: ICounter) { 13 | if (check.match(msg.plain, option.match)) { 14 | await Counter.findOneAndUpdate( 15 | { 16 | match: option.match, 17 | }, 18 | { 19 | $inc: { 20 | total: 1, 21 | }, 22 | }, 23 | { 24 | upsert: true, 25 | }, 26 | ) 27 | } 28 | } 29 | 30 | /** 31 | * 计数器 32 | * @param ctx 33 | */ 34 | export default async (ctx: Bot) => { 35 | const { mirai } = ctx 36 | const options = await Counter.find() 37 | 38 | mirai.on('message', (msg) => { 39 | options.forEach((option) => { 40 | countKeyword(msg, option) 41 | }) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/user.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from '.' 2 | import consola from 'consola' 3 | 4 | export class User { 5 | constructor(public ctx: Bot) {} 6 | 7 | /** 8 | * 是否是主人 9 | * @param qq 10 | */ 11 | isMaster(qq: number) { 12 | return this.ctx.el.bot.master.includes(qq) 13 | } 14 | 15 | /** 16 | * 是否是管理员 17 | * @param qq 18 | */ 19 | isAdmin(qq: number) { 20 | return this.ctx.el.bot.admin?.includes(qq) 21 | } 22 | 23 | /** 24 | * 是否拥有权限 25 | * @param qq 用户 QQ,若未传入,则取当前消息发送者 26 | * @param reply 是否回复 27 | * @param content 提示内容 28 | */ 29 | isAllowed(qq = 0, reply = false, content = '您没有操作权限') { 30 | if ( 31 | !qq 32 | // && this.ctx.mirai.curMsg 33 | // && check.isChatMessage(this.ctx.mirai.curMsg) 34 | ) { 35 | // qq = this.ctx.mirai.curMsg.sender.id 36 | } 37 | 38 | const allowFlag = this.isMaster(qq) || this.isAdmin(qq) 39 | if (!allowFlag && reply) { 40 | // TODO reply 41 | consola.warn(content) 42 | } 43 | 44 | return allowFlag 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/webhook.test.ts: -------------------------------------------------------------------------------- 1 | import { sign } from '@octokit/webhooks-methods' 2 | import fs from 'fs-extra' 3 | import { beforeAll, describe, expect, it } from 'vitest' 4 | 5 | const pushEventPayload = fs.readFileSync( 6 | 'test/fixtures/push-payload.json', 7 | 'utf-8', 8 | ) 9 | let signatureSha256: string 10 | 11 | describe('github webhook', async () => { 12 | beforeAll(async () => { 13 | signatureSha256 = await sign('mySecret', pushEventPayload) 14 | }) 15 | 16 | it('local github webhook', async () => { 17 | const port = 7777 18 | const response = await fetch( 19 | `http://localhost:${port}/api/github/webhooks`, 20 | { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | 'X-GitHub-Delivery': '123e4567-e89b-12d3-a456-426655440000', 25 | 'X-GitHub-Event': 'push', 26 | 'X-Hub-Signature-256': signatureSha256, 27 | }, 28 | body: pushEventPayload, 29 | }, 30 | ) 31 | 32 | expect(response.status).toEqual(200) 33 | expect(await response.text()).toContain('ok') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/cli/utils.ts: -------------------------------------------------------------------------------- 1 | import type commander from 'commander' 2 | 3 | /** 4 | * 返回关于信息 5 | */ 6 | export function aboutInfo(pkg: any) { 7 | let about = '' 8 | about += `GitHub: ${pkg.repository.url}\n` 9 | about += `Docs: ${pkg.homepage}\n` 10 | about += `SDK: ${pkg.directories.lib}\n` 11 | about += 'Author: ' + `${pkg.author.name} <${pkg.author.url}>` + '\n' 12 | about += 'Copyright: @ElpsyCN' 13 | return about 14 | } 15 | 16 | /** 17 | * 清理全局选项 18 | */ 19 | export function cleanOptions(program: commander.Command) { 20 | const options = program.opts() 21 | 22 | // reset option 23 | Object.keys(options).forEach((key) => { 24 | delete options[key] 25 | 26 | // 重新设置默认值 27 | if (options.length > 0) { 28 | options.forEach((option: any) => { 29 | if ( 30 | option.defaultValue 31 | && (option.long === `--${key}` || option.short === `-${key}`) 32 | ) { 33 | options[key] = option.defaultValue 34 | } 35 | }) 36 | } 37 | }) 38 | 39 | if (program.commands.length > 0) { 40 | program.commands.forEach((command) => { 41 | cleanOptions(command) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/admin/index.ts: -------------------------------------------------------------------------------- 1 | // import type { Bot } from 'el-bot' 2 | // import type { EventType } from 'mirai-ts' 3 | // import { Message } from 'mirai-ts' 4 | 5 | // export default function (ctx: Bot) { 6 | // const { mirai } = ctx 7 | // const masters = ctx.el.bot.master 8 | 9 | // const messageListMap = new Map< 10 | // number, 11 | // EventType.BotInvitedJoinGroupRequestEvent 12 | // >() 13 | 14 | // mirai.on('BotInvitedJoinGroupRequestEvent', (msg) => { 15 | // const content = [ 16 | // Message.Plain( 17 | // `好友 ${msg.nick}(${msg.fromId}) 邀请您加入 ${msg.groupName}(${msg.groupId})\n引用回复该信息,0 同意邀请,1 拒绝邀请`, 18 | // ), 19 | // ] 20 | // if (masters) { 21 | // masters.forEach(async (target) => { 22 | // const { messageId } = await mirai.api.sendFriendMessage(content, target) 23 | // messageListMap.set(messageId, msg) 24 | // }) 25 | // } 26 | // }) 27 | 28 | // mirai.on('FriendMessage', (msg) => { 29 | // const quoteMsg = msg.get('Quote') 30 | // if (quoteMsg && messageListMap.get(quoteMsg.id)) 31 | // messageListMap.get(quoteMsg.id)?.respond(Number.parseInt(msg.plain)) 32 | // }) 33 | // } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # custom 2 | tmp 3 | *.out 4 | # .vscode 5 | .idea 6 | 7 | # mirai 8 | mirai 9 | mcl* 10 | 11 | # go-cqhttp 12 | go-cqhttp 13 | data/ 14 | device.json 15 | 16 | .DS_Store 17 | yarn.lock 18 | package-lock.json 19 | # pnpm-lock.yaml 20 | 21 | ### Node ### 22 | # Logs 23 | logs 24 | *.log 25 | 26 | # Runtime data 27 | pids 28 | *.pid 29 | *.seed 30 | *.pid.lock 31 | 32 | # Directory for instrumented libs generated by jscoverage/JSCover 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | coverage 37 | *.lcov 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | .env.test 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | dist 75 | -------------------------------------------------------------------------------- /examples/nestjs-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@el-bot/example-nestjs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "description": "这只是一个开发用的测试机器人。", 7 | "author": { 8 | "name": "YunYouJun", 9 | "email": "me@yunyoujun.cn", 10 | "url": "https://www.yunyoujun.cn" 11 | }, 12 | "main": "src/index.ts", 13 | "scripts": { 14 | "prebuild": "rm -rf dist", 15 | "start": "nest start", 16 | "start:dev": "nest start --watch", 17 | "start:debug": "nest start --debug --watch", 18 | "start:prod": "nodemon dist/main", 19 | "build": "nest build", 20 | "lint": "eslint .", 21 | "dev": "npm run start:dev", 22 | "test": "ts-node src/index.ts" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^11.0.12", 26 | "@nestjs/core": "^11.0.12", 27 | "@nestjs/platform-express": "^11.0.12", 28 | "class-transformer": "^0.5.1", 29 | "class-validator": "^0.14.1", 30 | "el-bot": "workspace:*", 31 | "reflect-metadata": "^0.2.2" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/cli": "^11.0.5", 35 | "nodemon": "^3.1.9", 36 | "npm-run-all": "^4.1.5", 37 | "ts-loader": "^9.5.2", 38 | "typescript": "^5.8.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroIconClass: i-ri-robot-line 4 | heroText: el-bot 5 | tagline: 基于 Mirai 的可配置 QQ 机器人框架 6 | actions: 7 | - text: 快速上手 → 8 | link: /guide/ 9 | features: 10 | - title: Element 11 | details: 元素,无尽的世界线 12 | - title: Elegrant 13 | details: 优雅,不死的凤凰鸟 14 | - title: Electronic 15 | details: 电子,闪光的指压师 16 | footer: 17 | license: AGPL-3.0 Licensed 18 | since: 2020 19 | author: 20 | name: YunYouJun 21 | url: https://www.yunyoujun.cn 22 | --- 23 | 24 | > 一个优雅的电子机器人,通过简单的配置便可以实现你的大部分需求。 25 | 26 | [el-bot 使用文档](https://docs.bot.elpsy.cn) 27 | 28 | # Project 29 | 30 | - [el-bot](https://github.com/ElpsyCN/el-bot):机器人主体,基于 [mirai-ts](https://github.com/YunYouJun/mirai-ts) 实现的配置型 QQ 机器人框架。(开发中...,又不是不能用.jpg) 31 | - [el-bot-web](https://github.com/ElpsyCN/el-bot-web):控制面板,基于 [mirai-api-http](https://github.com/mamoe/mirai-api-http) 实现的 QQ 机器人网站控制台,通过网站观测与控制你的 QQ 机器人。(开发中...) 32 | 33 | ## 其他 34 | 35 | - [el-bot-api](https://github.com/ElpsyCN/el-bot-api): 提供一些插件的默认 API 36 | - [el-bot-plugins](https://github.com/ElpsyCN/el-bot-plugins): el-bot 的官方插件集中地(你也可以提交 PR 或一些自己的插件链接到 README 里打广告) 37 | - [el-bot-template](https://github.com/ElpsyCN/el-bot-template):机器人模版(你可以直接使用它来生成你的机器人) 38 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/platform/qq.ts: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { AvailableIntentsEventsEnum, createOpenAPI, createWebsocket, GetWsParam, IMessage, SessionEvents } from 'qq-guild-bot' 3 | import { createQQApi } from 'qq-sdk' 4 | 5 | export type EventType = keyof typeof SessionEvents | keyof typeof AvailableIntentsEventsEnum 6 | 7 | export interface BaseEventType { 8 | eventType: EventType 9 | eventId: string 10 | } 11 | 12 | export interface EventTypesMap { 13 | GUILD_MESSAGES: BaseEventType & { 14 | msg: IMessage 15 | } 16 | [key: string]: BaseEventType 17 | } 18 | 19 | /** 20 | * fix qq-guild-bot types 21 | */ 22 | export interface QQWebsocketClient extends ReturnType { 23 | on: (eventName: T, callback: (data: EventTypesMap[T]) => void) => void 24 | } 25 | 26 | /** 27 | * QQ 机器人平台 28 | */ 29 | export function createQQSDK(qqConfig: GetWsParam) { 30 | const client = createOpenAPI(qqConfig) 31 | consola.success('🐧 已创建 QQ Client') 32 | 33 | const api = createQQApi(qqConfig) 34 | const ws = createWebsocket(qqConfig) as QQWebsocketClient 35 | 36 | ws.on('READY', (_data) => { 37 | consola.success('🐧 QQ Websocket 已连接') 38 | }) 39 | consola.success('🐧 已创建 QQ Websocket 链接') 40 | 41 | return { 42 | api, 43 | client, 44 | ws, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/jrmsn/index.ts: -------------------------------------------------------------------------------- 1 | // import dayjs from 'dayjs' 2 | 3 | // export const name = 'jrmsn' 4 | 5 | // export interface JrmsnOptions { 6 | // groups: number[] 7 | // } 8 | 9 | // /** 10 | // * 随机选中一位群友担任今日美少女 11 | // * @param ctx 12 | // */ 13 | // // eslint-disable-next-line unused-imports/no-unused-vars 14 | // export function apply(ctx: Context, options: JrmsnOptions) { 15 | // const msnMap = new Map() 20 | 21 | // ctx.middleware(async (session, next) => { 22 | // if (session.content === '今日美少女' && session.guildId) { 23 | // const list = await session.bot.getGuildMemberList(session.guildId) 24 | // const today = dayjs().format('YYYY-MM-DD') 25 | // if (!msnMap.has(session.guildId) || today !== msnMap.get(session.guildId)?.lastUpdated) { 26 | // const len = list.length 27 | // const member = list[Math.floor(Math.random() * len)] 28 | // msnMap.set(session.guildId, { 29 | // lastUpdated: today, 30 | // member, 31 | // }) 32 | // } 33 | // return `今日担任美少女的是:${segment.at(msnMap.get(session.guildId)!.member.userId)}!` 34 | // } 35 | // else { 36 | // return next() 37 | // } 38 | // }) 39 | // } 40 | -------------------------------------------------------------------------------- /packages/el-bot/core/db/analytics.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | // import { Friend } from './schemas/friend.schema' 3 | 4 | /** 5 | * 记录触发信息 6 | */ 7 | export async function recordTriggerInfo() { 8 | // if (mirai.curMsg && mirai.curMsg.type === 'GroupMessage') { 9 | // const msg = mirai.curMsg 10 | 11 | // Friend.findOneAndUpdate( 12 | // { 13 | // qq: msg.sender.id, 14 | // lastTriggerTime: new Date(), 15 | // }, 16 | // { 17 | // $inc: { 18 | // total: 1, 19 | // }, 20 | // $setOnInsert: { 21 | // total: 0, 22 | // }, 23 | // }, 24 | // { 25 | // upsert: true, 26 | // }, 27 | // ) 28 | // } 29 | } 30 | 31 | /** 32 | * 分析统计 33 | * @param bot 34 | */ 35 | export async function analytics(bot: Bot) { 36 | if (!bot.db) { 37 | bot.logger.error('[analytics] 您必须先启用数据库。') 38 | // return 39 | } 40 | 41 | // const { mirai } = bot 42 | 43 | // const sendGroupMessage = mirai.api.sendGroupMessage 44 | // 重载消息发送函数 45 | // mirai.api.sendGroupMessage = async (messageChain, target, quote) => { 46 | // recordTriggerInfo(mirai) 47 | 48 | // const data = await sendGroupMessage.apply(mirai.api, [ 49 | // messageChain, 50 | // target, 51 | // quote, 52 | // ]) 53 | 54 | // return data 55 | // } 56 | } 57 | -------------------------------------------------------------------------------- /packages/el-bot/core/utils/misc.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import consola from 'consola' 3 | 4 | import gradient from 'gradient-string' 5 | import nodeNapcatTsPkg from 'node-napcat-ts/package.json' 6 | import colors from 'picocolors' 7 | 8 | import pkg from '../../package.json' 9 | 10 | /** 11 | * 是否为开发模式 12 | */ 13 | export const isDev = process.env.NODE_ENV !== 'production' 14 | 15 | /** 16 | * 休眠 17 | * @param ms 18 | */ 19 | export async function sleep(ms: number): Promise { 20 | return new Promise(resolve => setTimeout(resolve, ms)) 21 | } 22 | 23 | const cyanBlue = gradient('cyan', 'blue') 24 | 25 | /** 26 | * 声明 27 | */ 28 | export function statement() { 29 | const asciis = [ 30 | ' _____ _ ____ _', 31 | '| ____| | | __ ) ___ | |_', 32 | '| _| | | | _ \\ / _ \\| __|', 33 | '| |___| | | |_) | (_) | |_', 34 | '|_____|_| |____/ \\___/ \\__|', 35 | ] 36 | // eslint-disable-next-line no-console 37 | console.log(cyanBlue(asciis.join('\n'))) 38 | // console gradient 39 | consola.log('') 40 | consola.debug(`${cyanBlue('Docs: ')} ${colors.dim(pkg.homepage)}`) 41 | consola.debug(`GitHub: ${colors.dim(pkg.repository.url)}`) 42 | consola.info(`el-bot: ${colors.cyan(`v${pkg.version}`)}`) 43 | consola.info(`napcat-node-ts: ${colors.cyan(`v${nodeNapcatTsPkg.version}`)}`) 44 | // eslint-disable-next-line no-console 45 | console.log('') 46 | } 47 | -------------------------------------------------------------------------------- /docs/.vitepress/components/ChatAvatar.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 43 | 44 | 56 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/nbnhhsh/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import type { MessageType } from 'mirai-ts' 3 | import axios from 'axios' 4 | import { handleError } from '../../utils/error' 5 | 6 | async function guess(text: string) { 7 | const API_URL = 'https://lab.magiconch.com/api/nbnhhsh/guess' 8 | return axios.post(API_URL, { 9 | text, 10 | }) 11 | } 12 | 13 | /** 14 | * 能不能好好说话? 15 | * @param ctx 16 | */ 17 | export default function (ctx: Bot) { 18 | const { cli } = ctx 19 | cli 20 | .command('nbnhhsh ') 21 | .description('能不能好好说话?') 22 | .action(async (text: string[]) => { 23 | const msg = ctx.mirai.curMsg as MessageType.ChatMessage 24 | try { 25 | const { data } = await guess(text.join(',')) 26 | if (data.length) { 27 | data.forEach((result: any) => { 28 | let content = `${result.name} 理解不能` 29 | if (result.trans && result.trans.length > 0) { 30 | content = `${result.name} 的含义:${result.trans.join(',')}` 31 | } 32 | else if (result.inputting && result.inputting.length > 0) { 33 | content = `${result.name} 有可能是:${result.inputting.join( 34 | ',', 35 | )}` 36 | } 37 | msg.reply(content) 38 | }) 39 | } 40 | } 41 | catch (e: any) { 42 | if (e) { 43 | msg.reply(e.message) 44 | handleError(e) 45 | } 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /packages/el-bot/node/cli/utils.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import * as readline from 'node:readline' 3 | import { blue, bold, dim, underline } from 'picocolors' 4 | import { version } from '../../package.json' 5 | 6 | export function printInfo() { 7 | console.log() 8 | console.log(` ${bold('🤖 El Bot')} ${blue(`v${version}`)}`) 9 | console.log() 10 | // console.log(` ${dim('📁')} ${dim(path.resolve(options.userRoot))}`) 11 | console.log() 12 | const restart = `${underline('r')}${dim('estart')}` 13 | const edit = `${underline('e')}${dim('dit')}` 14 | const divider = `${dim(' | ')}` 15 | console.log(`${dim(' shortcuts ')} > ${restart}${divider}${edit}`) 16 | console.log() 17 | } 18 | 19 | /** 20 | * bind shortcut for terminal 21 | */ 22 | export function bindShortcut(SHORTCUTS: { name: string, fullName: string, action: () => void }[]) { 23 | process.stdin.resume() 24 | process.stdin.setEncoding('utf8') 25 | readline.emitKeypressEvents(process.stdin) 26 | if (process.stdin.isTTY) 27 | process.stdin.setRawMode(true) 28 | 29 | process.stdin.on('keypress', (str, key) => { 30 | if (key.ctrl && key.name === 'c') { 31 | process.exit() 32 | } 33 | else { 34 | const [sh] = SHORTCUTS.filter(item => item.name === str) 35 | if (sh) { 36 | try { 37 | sh.action() 38 | } 39 | catch (err) { 40 | console.error(`Failed to execute shortcut ${sh.fullName}`, err) 41 | } 42 | } 43 | } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /examples/nestjs-demo/config/bot.ts: -------------------------------------------------------------------------------- 1 | import { defineBotConfig } from 'el-bot' 2 | 3 | export const config = { 4 | rules: [ 5 | { 6 | match: '在吗', 7 | reply: '爪巴', 8 | }, 9 | { 10 | match: /^(.+)一时爽$/, 11 | reply: (_: any, str: string) => `一直${str}一直爽`, 12 | }, 13 | ], 14 | } 15 | 16 | export default defineBotConfig({ 17 | 'name': '嘿', 18 | 'autoloadPlugins': true, 19 | 20 | // plugins 21 | // default: [ 22 | // // # - dev 23 | // 'admin', 24 | // 'answer', 25 | // // "blacklist", 26 | // // "counter", 27 | // // "feeder", 28 | // // "forward", 29 | // // "limit", 30 | // // "memo", 31 | // 'nbnhhsh', 32 | // 'qrcode', 33 | // // "rss", 34 | // // "report", 35 | // // "search", 36 | // // "search-image", 37 | // 'teach', 38 | // // "workflow", 39 | // ], 40 | // custom: ['./plugins/webhook', './plugins/command'], 41 | 42 | 'master': [910426929], 43 | 44 | 'answer': [ 45 | { 46 | is: '在吗', 47 | reply: 'Yes, Master!', 48 | help: '在不在测试', 49 | }, 50 | ], 51 | 52 | 'search-image': { 53 | token: '0306dd15aa139efd8c0e2820e07249aef0c9361b', 54 | options: { 55 | results: 3, 56 | }, 57 | }, 58 | 59 | 'report': [ 60 | { 61 | type: 'jdy-report', 62 | // eslint-disable-next-line no-template-curly-in-string 63 | content: '简道云每日填报情况:${data.msg}', 64 | target: { 65 | group: [120117362], 66 | }, 67 | }, 68 | ], 69 | }) 70 | -------------------------------------------------------------------------------- /packages/el-bot/core/utils/decorators.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@yunyoujun/logger' 2 | 3 | const logger = new Logger({ prefix: '[decorators]' }) 4 | 5 | export function displayCall( 6 | target: any, 7 | propertyName: string, 8 | propertyDescriptor: PropertyDescriptor, 9 | ) { 10 | const method = propertyDescriptor.value 11 | 12 | propertyDescriptor.value = function (...args: any[]) { 13 | // 将 greet 的参数列表转换为字符串 14 | const params = args.map(a => JSON.stringify(a)).join() 15 | // 调用 greet() 并获取其返回值 16 | const result = method.apply(this, args) 17 | // 转换结尾为字符串 18 | const r = JSON.stringify(result) 19 | // 在终端显示函数调用细节 20 | logger.info(`Call: ${propertyName}(${params}) => ${r}`) 21 | // 返回调用函数的结果 22 | return result 23 | } 24 | 25 | return propertyDescriptor 26 | } 27 | 28 | // 类捕获异常 29 | export function tryCatch(errorHandler?: (error?: Error) => void) { 30 | return function ( 31 | target: any, 32 | propertyKey: string, 33 | descriptor: PropertyDescriptor, 34 | ) { 35 | const func = descriptor.value 36 | 37 | return { 38 | get() { 39 | return (...args: any[]) => { 40 | return Promise.resolve(func.apply(this, args)).catch((error) => { 41 | if (errorHandler) 42 | errorHandler(error) 43 | else 44 | logger.error(`调用 ${propertyKey} 出了问题`) 45 | }) 46 | } 47 | }, 48 | set(newValue: any) { 49 | return newValue 50 | }, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/el-bot/node/server/webhook/github-handler.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import type { EventEmitter } from 'node:events' 3 | import type { IncomingMessage, ServerResponse } from 'node:http' 4 | 5 | import * as octokit from '@octokit/webhooks' 6 | import * as shell from 'shelljs' 7 | 8 | // github handler 9 | export interface handler extends EventEmitter { 10 | ( 11 | req: IncomingMessage, 12 | res: ServerResponse, 13 | callback: (err: Error) => void 14 | ): void 15 | } 16 | 17 | /** 18 | * Setup github webhook handler 19 | * @see https://github.com/octokit/webhooks 20 | */ 21 | export function githubHandler(ctx: Bot) { 22 | const config = { 23 | secret: ctx.el.webhook?.secret || 'el-psy-congroo', 24 | } 25 | 26 | const handler = new octokit.Webhooks(config) 27 | const middleware = octokit.createNodeMiddleware(handler, { 28 | path: ctx.el.webhook?.path || '/webhook', 29 | }) 30 | 31 | handler.onError((err) => { 32 | ctx.logger.error(`Error: ${err}`) 33 | }) 34 | 35 | // 处理 36 | handler.on('push', (event) => { 37 | ctx.logger.info( 38 | `Received a push event for ${event.payload.repository.name} to ${event.payload.ref}`, 39 | ) 40 | 41 | // git pull repo 42 | if (shell.exec('git pull').code !== 0) { 43 | ctx.logger.error('Git 拉取失败,请检查默认分支。') 44 | } 45 | else { 46 | ctx.logger.info('安装依赖...') 47 | shell.exec('yarn') 48 | } 49 | }) 50 | 51 | return { 52 | handler, 53 | middleware, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/@el-bot/plugin-niubi/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import type { NiubiOptions } from './options' 3 | import axios from 'axios' 4 | import { utils } from 'el-bot' 5 | import { check } from 'mirai-ts' 6 | 7 | async function getRandomSentence(url: string, name: string) { 8 | let sentence = '' 9 | const { data } = await axios.get(url) 10 | sentence = utils.renderString(data[0], name, 'name') 11 | return sentence 12 | } 13 | 14 | export default function (ctx: Bot, options: NiubiOptions) { 15 | const { mirai } = ctx 16 | 17 | // 覆盖默认配置 18 | mirai.on('message', (msg) => { 19 | let name = '我' 20 | 21 | options.match.forEach(async (option) => { 22 | const str = check.match(msg.plain.toLowerCase(), option) 23 | if (!str) 24 | return 25 | else if (Array.isArray(str) && str[1]) 26 | name = str[1] 27 | 28 | msg.messageChain.some((singleMessage) => { 29 | if (singleMessage.type === 'At' && singleMessage.display) { 30 | name = `「${singleMessage.display.slice(1)}」` 31 | return true 32 | } 33 | return false 34 | }) 35 | 36 | const sentence = await getRandomSentence(options.url, name) 37 | msg.reply(sentence) 38 | }) 39 | }) 40 | 41 | // 进群时 42 | mirai.on('MemberJoinEvent', async (msg) => { 43 | const sentence = await getRandomSentence( 44 | options.url, 45 | msg.member.memberName, 46 | ) 47 | mirai.api.sendGroupMessage(sentence, msg.member.group.id) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /packages/el-bot/core/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import type { MessageType } from 'mirai-ts' 2 | 3 | /** 4 | * 判断是否为 URL 链接 5 | * @param url 6 | */ 7 | export function isUrl(url: string) { 8 | return /^https?:\/\/.+/.test(url) 9 | } 10 | 11 | /** 12 | * 内部模式 13 | */ 14 | export class InnerMode { 15 | msg: MessageType.ChatMessage | undefined 16 | friendSet = new Set() 17 | groupSet = new Set() 18 | constructor(msg?: MessageType.ChatMessage) { 19 | if (msg) 20 | this.msg = msg 21 | } 22 | 23 | setMsg(msg: MessageType.ChatMessage) { 24 | this.msg = msg 25 | } 26 | 27 | /** 28 | * 进入 29 | */ 30 | enter() { 31 | const msg = this.msg 32 | if (!msg) 33 | return 34 | if (msg.type === 'FriendMessage') 35 | this.friendSet.add(msg.sender.id) 36 | else if (msg.type === 'GroupMessage') 37 | this.groupSet.add(msg.sender.group.id) 38 | } 39 | 40 | /** 41 | * 当前状态 42 | * 是否已进入内部 43 | */ 44 | getStatus() { 45 | const msg = this.msg 46 | if (!msg) 47 | return 48 | if (msg.type === 'FriendMessage') 49 | return this.friendSet.has(msg.sender.id) 50 | else if (msg.type === 'GroupMessage') 51 | return this.groupSet.has(msg.sender.group.id) 52 | } 53 | 54 | /** 55 | * 退出 56 | */ 57 | exit() { 58 | const msg = this.msg 59 | if (!msg) 60 | return 61 | if (msg.type === 'FriendMessage') 62 | this.friendSet.delete(msg.sender.id) 63 | else if (msg.type === 'GroupMessage') 64 | this.groupSet.delete(msg.sender.group.id) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/el-bot/node/server/hono.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @documentation 3 | * 为什么不使用 [Elysia](https://elysiajs.com)? 4 | * Elysia 是 Bun First 框架,它使用了 Bun 的 API 来创建 Server。 5 | * 而其 Server 类型与 @octokit/webhooks 的 Server 类型不同,所以无法直接使用现有生态的中间件。 6 | * 7 | * 由于生态问题,改为使用 Hono(同样兼容 Bun)。 8 | */ 9 | 10 | import { HttpBindings, serve } from '@hono/node-server' 11 | import consola from 'consola' 12 | import { Hono } from 'hono' 13 | 14 | import { cors } from 'hono/cors' 15 | import { logger } from 'hono/logger' 16 | import { poweredBy } from 'hono/powered-by' 17 | 18 | import colors from 'picocolors' 19 | import { BotServerOptions } from '../../core' 20 | import { createWebhooks } from './webhook' 21 | 22 | export type Bindings = HttpBindings 23 | export type BotServer = Hono<{ 24 | Bindings: Bindings 25 | }> 26 | 27 | /** 28 | * @see https://hono.dev 29 | */ 30 | export function createHonoServer(options: BotServerOptions) { 31 | const app = new Hono<{ 32 | Bindings: Bindings 33 | }>() 34 | 35 | app.use('/api/*', cors()) 36 | app.use(logger()) 37 | app.use(poweredBy()) 38 | 39 | // github webhooks: /api/github/webhooks 40 | if (options.webhooks?.enable) 41 | createWebhooks(app, options.webhooks) 42 | 43 | app.get('/', (c) => { 44 | return c.text('Hono is running! I\'m el-bot server!') 45 | }) 46 | 47 | const port = options.port || 7777 48 | serve({ 49 | fetch: app.fetch, 50 | port, 51 | }) 52 | const url = `http://localhost:${port}` 53 | consola.success(`🔥 Hono is running: ${colors.green(url)}`) 54 | 55 | return app 56 | } 57 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/qrcode/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from '../../core' 2 | import type { QRCodeOptions } from './options' 3 | import QRCode from 'qrcode' 4 | import qrcodeOptions from './options' 5 | 6 | /** 7 | * 生成二维码 8 | * @param text 9 | * @param folder 目标文件夹 10 | */ 11 | export async function generateQR(text: string, folder: string) { 12 | const timestamp = new Date().valueOf() 13 | const filename = `${timestamp}.png` 14 | await QRCode.toFile(`${folder}/${filename}`, text) 15 | return filename 16 | } 17 | 18 | export default function (ctx: Bot, _options: QRCodeOptions = qrcodeOptions) { 19 | // const { cli } = ctx 20 | // const name = 'qrcode' 21 | // TODO 22 | // const folder = resolve('', name) 23 | 24 | // if (!fs.existsSync(folder)) 25 | // fs.mkdirSync(folder, { recursive: true }) 26 | 27 | // if (options.autoClearCache) 28 | // fs.rmSync(folder, { recursive: true }) 29 | 30 | // cli 31 | // .command('qrcode ') 32 | // .description('生成二维码') 33 | // .action(async (text: string[]) => { 34 | // const msg = ctx.mirai.curMsg as MessageType.ChatMessage 35 | // try { 36 | // const filename = await generateQR(text.join(' '), folder) 37 | // consola.info(`${folder}/${name}/${filename}`) 38 | // const chain = [Message.Image(null, null, `${folder}/${filename}`)] 39 | // msg.reply(chain) 40 | // } 41 | // catch (e: any) { 42 | // if (e) 43 | // msg.reply(e.message) 44 | 45 | // utils.handleError(e) 46 | // } 47 | // }) 48 | } 49 | -------------------------------------------------------------------------------- /packages/cli/src/start/index.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import process from 'node:process' 3 | import { createLogger, utils } from 'el-bot' 4 | import shell from 'shelljs' 5 | 6 | // 实例目录下的 package.json 7 | // eslint-disable-next-line ts/no-require-imports 8 | const pkg = require(getAbsolutePath('./package.json')) 9 | 10 | const logger = createLogger().child({ label: '🚀' }) 11 | 12 | /** 13 | * 获取当前目录下的绝对路径 14 | * @param file 文件名 15 | */ 16 | function getAbsolutePath(file: string) { 17 | return resolve(process.cwd(), file) 18 | } 19 | 20 | /** 21 | * @deprecated 22 | * TODO: refactor for napcat 23 | */ 24 | export default async function (cli: any) { 25 | /** 26 | * 启动机器人 27 | */ 28 | function startBot() { 29 | // el-bot 30 | } 31 | 32 | /** 33 | * 启动 MCL 34 | * @deprecated mirai 35 | */ 36 | function startMcl(folder?: string) { 37 | // 先进入目录 38 | try { 39 | shell.cd(folder || (pkg.mcl ? pkg.mcl.folder : 'mcl')) 40 | } 41 | catch (err) { 42 | utils.handleError(err) 43 | logger.error('mcl 目录不存在') 44 | } 45 | 46 | // remove mcl 47 | // glob('./mcl', {}) 48 | } 49 | 50 | // 启动 51 | cli 52 | .command('start [project]') 53 | .description('启动 el-bot') 54 | .option('-f --folder', 'mirai/mcl 所在目录') 55 | .action((project: string, options: { folder: string | undefined }) => { 56 | if (!project || project === 'bot') 57 | startBot() 58 | else if (project === 'mcl') 59 | startMcl(options.folder) 60 | else 61 | logger.error('不存在该指令') 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /packages/el-bot/node/cli/commands/dev.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'node:child_process' 2 | import consola from 'consola' 3 | import { Argv } from 'yargs' 4 | import { Bot, createBot } from '../../../core' 5 | import { commonOptions } from '../options' 6 | 7 | import { bindShortcut } from '../utils' 8 | 9 | let bot: Bot 10 | 11 | /** 12 | * el-bot dev . 13 | * @param cli 14 | */ 15 | export function registerDevCommand(cli: Argv) { 16 | cli.command( 17 | '* [root]', 18 | '启动开发模式', 19 | args => 20 | commonOptions(args) 21 | .option('port', { 22 | alias: 'p', 23 | type: 'number', 24 | describe: 'port', 25 | }) 26 | .strict() 27 | .help(), 28 | async ({ root }) => { 29 | consola.start('Link Start ...') 30 | consola.log('') 31 | 32 | // set root dir 33 | bot = await createBot(root) 34 | await bot.start() 35 | 36 | const SHORTCUTS = [ 37 | { 38 | name: 'r', 39 | fullName: 'restart', 40 | async action() { 41 | await bot.stop() 42 | await bot.start() 43 | }, 44 | }, 45 | { 46 | name: 'e', 47 | fullName: 'edit', 48 | action() { 49 | exec(`code "${root}"`) 50 | }, 51 | }, 52 | ] 53 | bindShortcut(SHORTCUTS) 54 | }, 55 | ) 56 | } 57 | 58 | // for vite hmr 59 | if (import.meta.hot) { 60 | consola.success('[el-bot] HMR') 61 | const close = async () => { 62 | await bot?.stop() 63 | 64 | // hmr 65 | consola.success('[el-bot] HMR') 66 | } 67 | import.meta.hot.on('vite:beforeFullReload', close) 68 | import.meta.hot.dispose(close) 69 | } 70 | -------------------------------------------------------------------------------- /docs/.vitepress/components/ChatPanel.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | 27 | 76 | -------------------------------------------------------------------------------- /packages/el-bot/node/server/webhook/index.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer' 2 | import { createNodeMiddleware } from '@octokit/webhooks' 3 | import consola from 'consola' 4 | import { BotServer } from '../hono' 5 | import { createOctokitWebhooks } from './octokit' 6 | import { WebhooksOptions } from './types' 7 | 8 | // import * as octokit from '@octokit/webhooks' 9 | // import { githubHandler } from './github-handler' 10 | import colors from 'picocolors' 11 | 12 | export * from './types' 13 | 14 | /** 15 | * create webhook 16 | * - github 17 | * @param app 18 | */ 19 | export function createWebhooks(app: BotServer, options: WebhooksOptions) { 20 | const path = options.octokit.middlewareOptions?.path || '/api/github/webhooks' 21 | consola.success(`🪝 Webhooks enabled: ${colors.green(`http://localhost:${options.port}${path}`)}`) 22 | 23 | const webhooks = createOctokitWebhooks({ 24 | secret: options.octokit.secret, 25 | }) 26 | 27 | /** 28 | * ref https://github.com/octokit/webhooks.js/blob/b22596fe031aa89873ba7bad0ff4329c0b882832/test/integration/node-middleware.test.ts#L73 29 | */ 30 | const middleware = createNodeMiddleware(webhooks, options.octokit.middlewareOptions) 31 | // for post return 32 | app.post('/api/github/webhooks', async (ctx) => { 33 | const req = ctx.env.incoming 34 | const res = ctx.env.outgoing 35 | // console.log('before ctx.body', ctx.body) 36 | // if (await middleware(req, res)) { 37 | // // ctx.header('Content-Type', '') 38 | // ctx.body('ok') 39 | // } 40 | // To fix ERR_STREAM_WRITE_AFTER_END 41 | if (await middleware(req, res)) { 42 | ctx.header('Content-Length', Buffer.byteLength('ok').toString()) 43 | return ctx.body('GitHub Webhook') 44 | } 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "cqhttp", 4 | "el-bot", 5 | "jrmsn", 6 | "koishi", 7 | "koishijs", 8 | "mamoe", 9 | "mirai", 10 | "Napcat", 11 | "Onebot", 12 | "QQSDK" 13 | ], 14 | // Disable the default formatter, use eslint instead 15 | "prettier.enable": false, 16 | "editor.formatOnSave": false, 17 | 18 | // Auto fix 19 | "editor.codeActionsOnSave": { 20 | "source.fixAll.eslint": "explicit", 21 | "source.organizeImports": "never" 22 | }, 23 | 24 | // Silent the stylistic rules in you IDE, but still auto fix them 25 | "eslint.rules.customizations": [ 26 | { "rule": "style/*", "severity": "off", "fixable": true }, 27 | { "rule": "format/*", "severity": "off", "fixable": true }, 28 | { "rule": "*-indent", "severity": "off", "fixable": true }, 29 | { "rule": "*-spacing", "severity": "off", "fixable": true }, 30 | { "rule": "*-spaces", "severity": "off", "fixable": true }, 31 | { "rule": "*-order", "severity": "off", "fixable": true }, 32 | { "rule": "*-dangle", "severity": "off", "fixable": true }, 33 | { "rule": "*-newline", "severity": "off", "fixable": true }, 34 | { "rule": "*quotes", "severity": "off", "fixable": true }, 35 | { "rule": "*semi", "severity": "off", "fixable": true } 36 | ], 37 | 38 | // Enable eslint for all supported languages 39 | "eslint.validate": [ 40 | "javascript", 41 | "javascriptreact", 42 | "typescript", 43 | "typescriptreact", 44 | "vue", 45 | "html", 46 | "markdown", 47 | "json", 48 | "jsonc", 49 | "yaml", 50 | "toml", 51 | "xml", 52 | "gql", 53 | "graphql", 54 | "astro", 55 | "css", 56 | "less", 57 | "scss", 58 | "pcss", 59 | "postcss" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /docs/guide/database.md: -------------------------------------------------------------------------------- 1 | # 数据系统 2 | 3 | > 其实就是数据库,叫数据系统当然是为了对仗。 4 | 5 | 默认使用 [MongoDB](https://www.mongodb.com/) 数据库,搭配 [mongoose](https://github.com/Automattic/mongoose)。 6 | 7 | > 0.7.0 之后的版本使用 Mongoose,此前的版本仍为原生的 MongoDB。 8 | 9 | 理由如下: 10 | 11 | - MongoDB 是最为流行的数据库之一。(~~MySQL: 你说什么?~~) 12 | - MongoDB 有官方支持的 Node.js 库,且数据形式便是 JSON,与 JavaScript 有天生的亲和性。 13 | - MongoDB 官方奉行数据库即服务,允许所有用户免费创建 512MB 以内的数据库。(~~这才是重点吧!~~) 14 | - 这可以免除额外搭建或迁移数据库的时间成本。 15 | - 此前我也考虑过 LeanCloud,但其对请求次数有限制。 16 | - SQLite / LokiJS 更适合无须联网的小型程序,轻量但功能有限,迁移不便,而机器人本身必然需要网络与服务器支持,直接使用 MongoDB 云数据库仍旧不影响机器人本身的轻量而更为方便,可以远程连接并实现线上线下数据统一,也更便于日后扩展。 17 | 18 | 我也考虑过将数据库作为插件实现,但统一一个默认类型的数据库可以避免一些处理上的混乱,也避免浪费一些额外的精力。 19 | 20 | > 譬如 A 插件使用 MongoDB,B 插件使用 MySQL,想要同时使用 A 插件和 B 插件,还需要依赖两个数据库显然是不恰当的,不如从开始便限制默认使用一个类型的数据库。 21 | 22 | ## 使用 23 | 24 | 添加环境变量,因为这是敏感信息,你最好不要直接书写在你的文件中。 25 | 26 | ```bash 27 | # .env 28 | BOT_DB_URI=mongodb+srv://你的用户名:你的密码@你的地址/el-bot 29 | ``` 30 | 31 | 配置数据库配置项 32 | 33 | - `enable`: 是否启用 34 | - `uri`: 你的 MongoDB 链接(包括数据库名称),注意是 uri(因为我看官方示例都是用这个) 35 | - `analytics`: 是否开启统计(当前只有简单的统计用户触发次数与上一次的触发时间) 36 | 37 | ```js 38 | // el/index.js 39 | module.exports = { 40 | // ... 41 | db: { 42 | enable: true, 43 | uri: process.env.BOT_DB_URI, 44 | analytics: true, 45 | }, 46 | }; 47 | ``` 48 | 49 | - `ctx.db`: `mongoose.connection` 50 | 51 | 用法与 Mongoose 一致,更多请参见 [Mongoose 文档](https://mongoosejs.com/docs/guide.html)。 52 | 53 | [user.schema.ts](https://github.com/YunYouJun/el-bot/blob/dev/src/db/schemas/user.schema.ts) 54 | 55 | ```js 56 | import { User } from "../schemas/user.schema.ts"; 57 | export function getUsers() { 58 | const users = User.find(); 59 | return users; 60 | } 61 | ``` 62 | 63 | - [The Official MongoDB Node.js Driver](https://github.com/mongodb/node-mongodb-native) 64 | - [Mongoose Docs](https://mongoosejs.com/) 65 | -------------------------------------------------------------------------------- /demo/el-bot.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { answerPlugin, defineConfig } from 'el-bot' 3 | import { AvailableIntentsEventsEnum } from 'qq-guild-bot' 4 | 5 | const answer = answerPlugin({ 6 | list: [ 7 | { 8 | listen: 'master', 9 | receivedText: ['在吗'], 10 | reply: 'Hello, master!', 11 | else: '爪巴', 12 | }, 13 | ], 14 | }) 15 | 16 | export default defineConfig({ 17 | bot: { 18 | plugins: [ 19 | answer, 20 | ], 21 | }, 22 | 23 | server: { 24 | webhooks: { 25 | octokit: { 26 | secret: 'mySecret', 27 | }, 28 | }, 29 | }, 30 | 31 | napcat: { 32 | debug: false, 33 | 34 | protocol: 'ws', 35 | host: '127.0.0.1', 36 | port: 3001, 37 | accessToken: '', 38 | 39 | // ↓ 自动重连(可选) 40 | reconnection: { 41 | enable: true, 42 | attempts: 10, 43 | delay: 5000, 44 | }, 45 | }, 46 | 47 | qq: { 48 | appID: process.env.QQ_BOT_APP_ID || '', // 申请机器人时获取到的机器人 BotAppID 49 | token: process.env.QQ_BOT_APP_TOKEN || '', // 申请机器人时获取到的机器人 BotToken 50 | /** 51 | * 事件订阅,用于开启可接收的消息类型 52 | * 传递了无权限的 intents,websocket 会报错 53 | */ 54 | intents: [ 55 | // @see https://bot.q.qq.com/wiki/develop/api-v2/server-inter/message/send-receive/event.html#%E5%8D%95%E8%81%8A%E6%B6%88%E6%81%AF 56 | 57 | // 默认权限 58 | AvailableIntentsEventsEnum.GUILDS, 59 | AvailableIntentsEventsEnum.GUILD_MEMBERS, 60 | AvailableIntentsEventsEnum.PUBLIC_GUILD_MESSAGES, 61 | // only for 私域机器人 62 | AvailableIntentsEventsEnum.FORUMS_EVENT, 63 | AvailableIntentsEventsEnum.GUILD_MESSAGES, 64 | AvailableIntentsEventsEnum.GUILD_MESSAGE_REACTIONS, 65 | AvailableIntentsEventsEnum.FORUMS_EVENT, 66 | ], // 67 | sandbox: true, // 沙箱支持,可选,默认false. v2.7.0+ 68 | }, 69 | }) 70 | -------------------------------------------------------------------------------- /docs/guide/extend.md: -------------------------------------------------------------------------------- 1 | # 扩展功能 2 | 3 | 请参见 [el-bot-template/package.json](https://github.com/ElpsyCN/el-bot-template/blob/master/package.json) `scripts` 字段。 4 | 5 | ## 全局安装 6 | 7 | 你也可以通过全局安装 el-bot 的方式以使用 el-bot 的命令行。 8 | 9 | ```bash 10 | npm install -g el-bot 11 | # yarn global add el-bot 12 | ``` 13 | 14 | ```bash 15 | # 安装 mirai 16 | el install mirai 17 | 18 | # 启动 mirai 19 | el start mirai 20 | 21 | # 启动机器人 22 | el bot 23 | ``` 24 | 25 | ## Webhook 26 | 27 | ::: danger 28 | 29 | 此部分现已集成至 el-bot 本身。 30 | 31 | 想要使用该功能,请确保您已放通服务器的对应端口。 32 | 33 | ::: 34 | 35 | 譬如当我们推送自己的机器人代码到 GitHub 上时,服务器将监听 GitHub 仓库消息,并拉取代码重启机器人。 36 | 37 | ### 配置 38 | 39 | 你需要将你的配置放在 git 仓库中,并设置 Webhooks。 40 | 41 | > `https://github.com/用户名/仓库/settings/hooks` 42 | 43 | - `Payload URL`: 填写你的服务器地址加端口号加 `/webhook`(默认 7777) 44 | > 例如:`http://1.2.3.4:7777/webhook` 45 | - `Content type`: application/json 46 | 47 | 收到推送后,将会自动拉取新的配置并重启机器人。 48 | 49 | 你可以通过以下配置决定是否启用它。 50 | 51 | ```js 52 | // el/index.js 53 | module.exports = { 54 | webhook: { 55 | enable: true, 56 | path: "/webhook", 57 | port: 7777, 58 | secret: "el-psy-congroo" 59 | } 60 | }; 61 | ``` 62 | 63 | ### 自定义 64 | 65 | 你可以在插件中通过 `ctx.webhook.githubHandler` 来获得 githubHandler 并进一步对其扩展。 66 | 67 | > [github-webhook-handler](https://github.com/rvagg/github-webhook-handler) 68 | 69 | #### 监听 70 | 71 | 通过 `ctx.webhook.on` 监听对应类型的消息。 72 | 73 | 使用其他程序发送 POST 或 GET 请求。 74 | 75 | ```json 76 | POST http://你服务器的IP地址:7777 77 | Content-Type: application/json 78 | { "type": "is-teacher-here", "isHere": 1 } 79 | ``` 80 | 81 | 或 82 | 83 | ```bash 84 | GET http://你的服务器IP地址:7777?type=is-teacher-here&isHere=1 85 | ``` 86 | 87 | 监听该请求,并进行响应。 88 | 89 | ```js 90 | module.exports = (ctx) => { 91 | const mirai = ctx.mirai; 92 | ctx.webhook.on("is-teacher-here", (data) => { 93 | const status = data.isHere ? "在" : "不在"; 94 | mirai.api.sendGroupMessage(status, 群号); 95 | }); 96 | }; 97 | ``` 98 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/search/index.ts: -------------------------------------------------------------------------------- 1 | // import type { Bot } from 'el-bot' 2 | 3 | /** 4 | * 搜索引擎 5 | */ 6 | interface SearchEngine { 7 | keywords: string[] 8 | url: string 9 | } 10 | 11 | /** 12 | * 搜索引擎列表 13 | */ 14 | type SearchEngineList = Record 15 | 16 | const engineList: SearchEngineList = { 17 | baidu: { 18 | keywords: ['百度', '度娘', 'baidu'], 19 | url: 'https://www.baidu.com/s?wd=', 20 | }, 21 | google: { 22 | keywords: ['谷歌', 'google'], 23 | url: 'https://www.google.com/search?q=', 24 | }, 25 | bing: { 26 | keywords: ['bing', '必应'], 27 | url: 'https://cn.bing.com/search?q=', 28 | }, 29 | buhuibaidu: { 30 | keywords: ['不会百度'], 31 | url: 'https://buhuibaidu.me/?s=', 32 | }, 33 | } 34 | 35 | /** 36 | * 根据搜索引擎返回对应链接 37 | * @param name engine name 38 | * @param keyword 39 | */ 40 | export function getLinkByEngine(name: string, keyword: string) { 41 | keyword = encodeURI(keyword) 42 | if (engineList[name]) { 43 | return engineList[name].url + keyword 44 | } 45 | else { 46 | for (const engine in engineList) { 47 | if (engineList[engine].keywords.includes(name)) 48 | return engineList[engine].url + keyword 49 | } 50 | return '' 51 | } 52 | } 53 | 54 | // export default function (ctx: Bot) { 55 | // // const mirai = ctx.mirai 56 | // // mirai.on('message', (msg) => { 57 | // // const engineString = msg.plain.split(' ')[0] 58 | // // let keyword = msg.plain.slice(engineString.length).trim() 59 | // // const buhuibaidu = msg.plain.match(/不会百度(.*)吗/) 60 | // // if (buhuibaidu) { 61 | // // keyword = buhuibaidu[1].trim() 62 | // // msg.reply(getLinkByEngine('buhuibaidu', keyword)) 63 | // // } 64 | // // else { 65 | // // const content = getLinkByEngine(engineString, keyword) 66 | // // if (content) 67 | // // msg.reply(content) 68 | // // } 69 | // // }) 70 | // } 71 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/logger/winston.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import dayjs from 'dayjs' 3 | import winston from 'winston' 4 | 5 | const customLevels = { 6 | levels: { 7 | error: 0, 8 | warn: 1, 9 | warning: 1, 10 | success: 2, 11 | info: 3, 12 | debug: 4, 13 | }, 14 | colors: { 15 | error: 'red', 16 | warn: 'yellow', 17 | warning: 'yellow', 18 | success: 'green', 19 | info: 'blue', 20 | debug: 'cyan', 21 | }, 22 | } 23 | 24 | export interface Logger extends winston.Logger { 25 | success: winston.LeveledLogMethod 26 | } 27 | 28 | /** 29 | * 创建日志工具,基于 winston 30 | */ 31 | export function createLogger() { 32 | const logger = winston.createLogger({ 33 | levels: customLevels.levels, 34 | transports: [ 35 | new winston.transports.Console(), 36 | new winston.transports.File({ 37 | filename: 'logs/error.log', 38 | level: 'error', 39 | }), 40 | ], 41 | format: winston.format.combine( 42 | winston.format((info) => { 43 | info.level = info.level.toUpperCase() 44 | return info 45 | })(), 46 | winston.format.padLevels({ 47 | levels: customLevels.levels, 48 | }), 49 | winston.format.colorize({ 50 | colors: customLevels.colors, 51 | }), 52 | winston.format.timestamp(), 53 | winston.format.printf(({ level, message, label, timestamp, plugin }) => { 54 | const namespace = `${chalk.cyan(`${label}`)}` 55 | const pluginPrefix = plugin ? chalk.magenta(`[${plugin}] `) : '' 56 | const printedMessage = message instanceof Object ? JSON.stringify(message, null, 2) : message 57 | const content = [ 58 | namespace, 59 | chalk.yellow(`[${dayjs(timestamp).format('HH:mm:ss')}]`), 60 | `${pluginPrefix}[${level}]${printedMessage}`, 61 | ] 62 | return content.join(' ') 63 | }), 64 | ), 65 | }) 66 | return logger as Logger 67 | } 68 | -------------------------------------------------------------------------------- /plugins/setu/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import axios from 'axios' 3 | import { utils } from 'el-bot' 4 | import { check, Message } from 'mirai-ts' 5 | 6 | /** 7 | * 色图格式 8 | */ 9 | export interface SetuImage { 10 | url: string 11 | } 12 | 13 | /** 14 | * 色图配置项 15 | */ 16 | export interface SetuOptions { 17 | url: string 18 | proxy: string 19 | match: check.Match[] 20 | reply: string 21 | } 22 | 23 | const setu = { 24 | url: 'https://el-bot-api.vercel.app/api/setu', 25 | proxy: 'https://images.weserv.nl/?url=', 26 | match: [ 27 | { 28 | is: '不够色', 29 | }, 30 | { 31 | includes: ['来', '色图'], 32 | }, 33 | ], 34 | reply: '让我找找', 35 | } 36 | 37 | /** 38 | * 获取随机图片 39 | */ 40 | function getRandomImage(image: SetuImage[]) { 41 | const index = Math.floor(Math.random() * image.length) 42 | return image[index] 43 | } 44 | 45 | /** 46 | * 47 | * @param {Bot} ctx 48 | * @param {SetuOptions} options 49 | */ 50 | export default function (ctx: Bot, options: SetuOptions) { 51 | const mirai = ctx.mirai 52 | utils.config.merge(setu, options) 53 | 54 | let image: any 55 | if (setu.url) { 56 | mirai.on('message', (msg) => { 57 | setu.match.forEach(async (obj) => { 58 | if (check.match(msg.plain.toLowerCase(), obj)) { 59 | if (setu.reply) 60 | msg.reply(setu.reply) 61 | 62 | if (utils.isUrl(setu.url)) { 63 | const { data } = await axios.get(setu.url) 64 | image = data 65 | if (!image.url) 66 | image = data.data[0] 67 | } 68 | else { 69 | const setuJson: any = await import(setu.url) 70 | image = getRandomImage(setuJson.image) 71 | } 72 | 73 | // 图片链接设置代理 74 | if (setu.proxy) 75 | image.url = setu.proxy + image.url 76 | msg.reply([Message.Image('', image.url)]) 77 | } 78 | }) 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /plugins/niubi/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import axios from 'axios' 3 | import { utils } from 'el-bot' 4 | import { check } from 'mirai-ts' 5 | 6 | let niubiJson: any = null 7 | 8 | const niubi = { 9 | url: 'https://el-bot-api.vercel.app/api/words/niubi', 10 | match: [ 11 | { 12 | re: '来点(\\S*)笑话', 13 | }, 14 | { 15 | is: 'nb', 16 | }, 17 | ], 18 | } 19 | 20 | /** 21 | * 获取随机句子 22 | * @param name 23 | */ 24 | async function getRandomSentence(name: string) { 25 | let sentence = '' 26 | if (niubiJson) { 27 | const index = Math.floor(Math.random() * niubiJson.length) 28 | sentence = utils.renderString(niubiJson[index], name, 'name') 29 | } 30 | else { 31 | const { data } = await axios.get(niubi.url) 32 | sentence = utils.renderString(data[0], name, 'name') 33 | } 34 | return sentence 35 | } 36 | 37 | export interface NiubiOptions {} 38 | 39 | export default function (ctx: Bot, _options: NiubiOptions = {}) { 40 | const { mirai } = ctx 41 | 42 | // 覆盖默认配置 43 | mirai.on('message', async (msg) => { 44 | let name = '我' 45 | 46 | if (!utils.isUrl(niubi.url)) 47 | niubiJson = await import(niubi.url) 48 | 49 | niubi.match.forEach(async (obj) => { 50 | const str = check.match(msg.plain.toLowerCase(), obj) 51 | if (!str) 52 | return 53 | 54 | else if (Array.isArray(str) && str[1]) 55 | name = str[1] 56 | 57 | msg.messageChain.some((singleMessage) => { 58 | if (singleMessage.type === 'At' && singleMessage.display) { 59 | name = `「${singleMessage.display.slice(1)}」` 60 | return true 61 | } 62 | return false 63 | }) 64 | 65 | const sentence = await getRandomSentence(name) 66 | msg.reply(sentence) 67 | }) 68 | }) 69 | 70 | // 进群时 71 | mirai.on('MemberJoinEvent', async (msg) => { 72 | const sentence = await getRandomSentence(msg.member.memberName) 73 | mirai.api.sendGroupMessage(sentence, msg.member.group.id) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/cli/jobs.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import * as shell from 'shelljs' 3 | 4 | /** 5 | * 任务 6 | */ 7 | interface Job { 8 | name: string 9 | do: string[] 10 | } 11 | 12 | /** 13 | * 步骤 14 | */ 15 | interface Step { 16 | cmd: string 17 | async: boolean 18 | } 19 | 20 | /** 21 | * cli 配置项 22 | */ 23 | export interface CliOptions { 24 | jobs: Job[] 25 | } 26 | 27 | /** 28 | * 执行对应任务 29 | * @param jobs 30 | * @param name 31 | */ 32 | function doJobByName(jobs: Job[], name: string) { 33 | jobs.forEach((job: Job) => { 34 | if (job.name && job.name === name && job.do) { 35 | job.do.forEach((step: string | Step) => { 36 | let cmd = '' 37 | let async = false 38 | if (typeof step === 'string') { 39 | cmd = step 40 | } 41 | else { 42 | cmd = step.cmd 43 | async = step.async 44 | } 45 | if (cmd.includes('el run ')) { 46 | name = cmd.slice(7) 47 | doJobByName(jobs, name) 48 | } 49 | shell.exec(cmd, { 50 | async, 51 | }) 52 | }) 53 | } 54 | }) 55 | } 56 | 57 | /** 58 | * 为 program 添加初始指令 59 | * @param ctx 60 | * @param options 61 | */ 62 | export function initProgram(ctx: Bot, options: CliOptions, qq: number) { 63 | const program = ctx.cli 64 | 65 | // 任务 66 | program 67 | .command('jobs') 68 | .description('任务列表') 69 | .action(async () => { 70 | if (!ctx.user.isAllowed(qq, true)) 71 | return 72 | 73 | let content = '任务列表:' 74 | options.jobs.forEach((job: Job) => { 75 | if (job.name) 76 | content += `\n- ${job.name}` 77 | }) 78 | ctx.reply(content) 79 | }) 80 | 81 | // 自定义任务 82 | program 83 | .command('run ') 84 | .description('运行自定义任务') 85 | .action(async (name: string) => { 86 | if (!ctx.user.isAllowed(qq, true)) 87 | return 88 | 89 | if (options && options.jobs && options.jobs.length > 0) 90 | doJobByName(options.jobs, name) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/teach/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import type { MessageType } from 'mirai-ts' 3 | import type { TeachOptions } from './options' 4 | import { Teach } from './teach.schema' 5 | import { displayList } from './utils' 6 | 7 | // implement the autoloadback referenced in loki constructor 8 | export default async function teach(ctx: Bot, options: TeachOptions) { 9 | if (!ctx.db) 10 | return 11 | 12 | const { mirai } = ctx 13 | 14 | // register command 15 | // 显示当前已有的问答列表 16 | ctx.cli 17 | .command('teach') 18 | .description('问答教学') 19 | .option('-l, --list', '当前列表') 20 | .action(async (options) => { 21 | if (options.list) 22 | ctx.reply(await displayList()) 23 | }) 24 | 25 | // 检测学习关键词 26 | // Q: xxx 27 | // A: xxx 28 | mirai.on('message', async (msg: MessageType.ChatMessage) => { 29 | // 私聊或被艾特时 30 | const qa = msg.plain.match(/Q:(.*)\nA:(.*)/) 31 | if ( 32 | qa 33 | && (msg.type === 'FriendMessage' 34 | || (msg.type === 'GroupMessage' && msg.isAt())) 35 | ) { 36 | // 没有权限时 37 | if (!ctx.status.getListenStatusByConfig(msg.sender, options)) { 38 | msg.reply(options.else) 39 | return 40 | } 41 | 42 | // 学习应答 43 | ctx.logger.info(`[teach] ${msg.plain}`) 44 | const question = qa[1].trim() 45 | const answer = qa[2].trim() 46 | 47 | const result = await Teach.findOneAndUpdate( 48 | { 49 | question, 50 | }, 51 | { 52 | answer, 53 | }, 54 | { 55 | upsert: true, 56 | }, 57 | ) 58 | if (result) { 59 | msg.reply( 60 | `存在重复,已覆盖旧值:\nQ: ${result.question}\nA: ${result.answer}`, 61 | ) 62 | } 63 | else { 64 | msg.reply(options.reply) 65 | } 66 | } 67 | else { 68 | // 查找应答 69 | const result = await Teach.findOne({ 70 | question: msg.plain, 71 | }) 72 | if (result) 73 | msg.reply(result.answer) 74 | } 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/command/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from '../../bot' 2 | // import { check } from 'mirai-ts' 3 | import consola from 'consola' 4 | 5 | /** 6 | * 面向用户的指令 7 | */ 8 | export class Command { 9 | /** 10 | * 指令名称 11 | */ 12 | name = '' 13 | /** 14 | * 指令描述 15 | */ 16 | desc = '' 17 | /** 18 | * 回调函数 19 | */ 20 | callback = (options: string[]) => { 21 | consola.info(options) 22 | } 23 | 24 | children = new Map() 25 | 26 | constructor(public ctx: Bot) {} 27 | 28 | /** 29 | * 注册指令 30 | * @param name 31 | */ 32 | command(name: string) { 33 | this.name = name 34 | 35 | return this 36 | } 37 | 38 | /** 39 | * 命令描述 40 | * @param desc 41 | */ 42 | description(desc: string) { 43 | this.desc = desc 44 | return this 45 | } 46 | 47 | /** 48 | * 只有有回调执行操作的命令才会被加入 command 列表 49 | * @param callback 50 | */ 51 | action(callback: (options: string[]) => any) { 52 | this.callback = callback 53 | if (this.children.has(this.name)) { 54 | this.ctx.logger.error(`指令【${this.name}】已存在`) 55 | } 56 | else { 57 | this.children.set(this.name, this) 58 | this.name = '' 59 | this.desc = '' 60 | } 61 | return this 62 | } 63 | 64 | parse(text: string) { 65 | const cmds = text.split(' ') 66 | const filteredCmds = cmds.filter(cmd => cmd !== '') 67 | 68 | const cmdKey = filteredCmds[0] 69 | if (this.children.has(cmdKey)) { 70 | const command = this.children.get(cmdKey) 71 | if (command) 72 | command.callback(filteredCmds.slice(1)) 73 | else 74 | this.ctx.logger.warn(`指令【${cmdKey}】的行为未定义`) 75 | } 76 | } 77 | 78 | listen() { 79 | // const { mirai } = this.ctx 80 | // mirai.on('message', (msg) => { 81 | // // if (check.isAt(msg, this.ctx.el.qq) && msg.plain.trim() === '帮助') { 82 | // // const content = getHelpContent(this.children) 83 | // // msg.reply(content) 84 | // // return 85 | // // } 86 | 87 | // this.parse(msg.plain.trim()) 88 | // }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/forward/index.ts: -------------------------------------------------------------------------------- 1 | import type { EventType, Mirai } from 'mirai-ts' 2 | 3 | import { AllMessageList } from './types' 4 | 5 | /** 6 | * 撤回消息对应转发群中的消息 7 | * @param mirai 8 | * @param msg 9 | * @param allMessageList 10 | */ 11 | export function recallByList( 12 | mirai: Mirai, 13 | msg: EventType.FriendRecallEvent | EventType.GroupRecallEvent, 14 | allMessageList: AllMessageList, 15 | ) { 16 | if (allMessageList && msg.messageId in allMessageList) { 17 | allMessageList[msg.messageId].forEach((messageId: number) => { 18 | // @ts-expect-error null 19 | mirai.api.recall(messageId) 20 | }) 21 | allMessageList[msg.messageId] = [] 22 | } 23 | } 24 | 25 | // export default function (ctx: Bot, options: ForwardOptions) { 26 | // const mirai = ctx.mirai 27 | // /** 28 | // * 原消息和被转发的各消息 Id 关系列表 29 | // */ 30 | // const allMessageList: AllMessageList = {} 31 | // mirai.on('message', async (msg: MessageType.ChatMessage) => { 32 | // if (!msg.sender || !msg.messageChain) 33 | // return 34 | 35 | // if (options) { 36 | // await Promise.all( 37 | // options.map(async (item: ForwardItem) => { 38 | // // const canForward = ctx.status.getListenStatusByConfig( 39 | // // msg.sender, 40 | // // item, 41 | // // ) 42 | 43 | // // if (canForward) { 44 | // // // remove source 45 | // // const sourceMessageId: number = msg.messageChain[0].id 46 | // // allMessageList[ 47 | // // sourceMessageId 48 | // // ] = await ctx.sender.sendMessageByConfig( 49 | // // msg.messageChain.slice(1), 50 | // // item.target, 51 | // // ) 52 | // // } 53 | // }), 54 | // ) 55 | // } 56 | // }) 57 | 58 | // // 消息撤回 59 | // mirai.on('FriendRecallEvent', (msg: EventType.FriendRecallEvent) => { 60 | // recallByList(mirai, msg, allMessageList) 61 | // }) 62 | 63 | // mirai.on('GroupRecallEvent', (msg: EventType.GroupRecallEvent) => { 64 | // recallByList(mirai, msg, allMessageList) 65 | // }) 66 | // } 67 | -------------------------------------------------------------------------------- /packages/el-bot/core/utils/config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import yaml from 'js-yaml' 3 | 4 | /** 5 | * 单纯 typeof [] 会返回 object 6 | * @deprecated 7 | * @param item 8 | */ 9 | function isObject(item: any) { 10 | return typeof item === 'object' && !Array.isArray(item) 11 | } 12 | 13 | /** 14 | * https://www.npmjs.com/package/js-yaml 15 | * @param path 配置文件名 16 | */ 17 | export function parseYaml(path: string) { 18 | return yaml.load(fs.readFileSync(path, 'utf8')) 19 | } 20 | 21 | /** 22 | * 合并配置 23 | * @deprecated 24 | * @param target 目标配置 25 | * @param source 源配置 26 | */ 27 | export function merge(target: any, source: any): any { 28 | for (const key in source) { 29 | if (isObject(target[key]) && isObject(source[key])) 30 | merge(target[key], source[key]) 31 | else 32 | target[key] = source[key] 33 | } 34 | return target 35 | } 36 | 37 | function mergeConfigRecursively( 38 | defaults: Record, 39 | overrides: Record, 40 | rootPath: string, 41 | ) { 42 | const merged: Record = { ...defaults } 43 | for (const key in overrides) { 44 | const value = overrides[key] 45 | if (value == null) 46 | continue 47 | 48 | const existing = merged[key] 49 | 50 | if (existing == null) { 51 | merged[key] = value 52 | continue 53 | } 54 | 55 | if (Array.isArray(existing) || Array.isArray(value)) { 56 | merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])] 57 | continue 58 | } 59 | if (isObject(existing) && isObject(value)) { 60 | merged[key] = mergeConfigRecursively( 61 | existing, 62 | value, 63 | rootPath ? `${rootPath}.${key}` : key, 64 | ) 65 | continue 66 | } 67 | 68 | merged[key] = value 69 | } 70 | return merged 71 | } 72 | 73 | export function mergeConfig( 74 | defaults: Record, 75 | overrides: Record, 76 | isRoot = true, 77 | ): Record { 78 | return mergeConfigRecursively(defaults, overrides, isRoot ? '' : '.') 79 | } 80 | 81 | function arraify(target: T | T[]): T[] { 82 | return Array.isArray(target) ? target : [target] 83 | } 84 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/blacklist/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import type { EventType, MessageType } from 'mirai-ts' 3 | import { check } from 'mirai-ts' 4 | import { block, displayList, initBlacklist, unBlock } from './utils' 5 | 6 | export default async function (ctx: Bot) { 7 | if (!ctx.db) 8 | return 9 | const { mirai, cli } = ctx 10 | 11 | const blacklist = await initBlacklist() 12 | 13 | mirai.beforeListener.push( 14 | (msg: MessageType.ChatMessage | EventType.Event) => { 15 | const isFriendBlocked 16 | = check.isChatMessage(msg) && blacklist.friends.has(msg.sender.id) 17 | const isGroupBlocked 18 | = msg.type === 'GroupMessage' 19 | && blacklist.groups.has(msg.sender.group.id) 20 | 21 | if (isFriendBlocked || isGroupBlocked) 22 | mirai.active = false 23 | else 24 | mirai.active = true 25 | }, 26 | ) 27 | 28 | // register command 29 | // 显示当前已有的黑名单 30 | cli 31 | .command('blacklist') 32 | .description('黑名单') 33 | .option('-l, --list ', '当前列表', 'all') 34 | .action(async (options) => { 35 | if (!ctx.user.isAllowed(undefined, true)) 36 | return 37 | const listType = options.list 38 | if (listType) { 39 | if (['friend', 'user', 'all'].includes(listType)) 40 | ctx.reply(`当前用户黑名单:${displayList(blacklist.friends)}`) 41 | 42 | if (['group', 'all'].includes(listType)) 43 | ctx.reply(`当前群聊黑名单:${displayList(blacklist.groups)}`) 44 | } 45 | }) 46 | 47 | cli 48 | .command('block [id]') 49 | .description('封禁') 50 | .action(async (type, id) => { 51 | if (!ctx.user.isAllowed(undefined, true)) 52 | return 53 | const msg = ctx.mirai.curMsg 54 | if (await block(type, Number.parseInt(id))) { 55 | const info = `[blacklist] 封禁 ${type} ${id}` 56 | msg!.reply!(info) 57 | } 58 | }) 59 | 60 | cli 61 | .command('unblock [id]') 62 | .description('解封') 63 | .action(async (type, id) => { 64 | if (!ctx.user.isAllowed(undefined, true)) 65 | return 66 | const msg = ctx.mirai.curMsg 67 | if (await unBlock(type, Number.parseInt(id))) { 68 | const info = `[blacklist] 解封 ${type} ${id}` 69 | msg!.reply!(info) 70 | } 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /packages/el-bot/core/composition-api/hooks.ts: -------------------------------------------------------------------------------- 1 | // import { } from "node-napcat-ts"; 2 | 3 | import { GroupMessage, PrivateFriendMessage, PrivateGroupMessage } from 'node-napcat-ts' 4 | import { currentInstance } from './lifecycle' 5 | 6 | export type NapcatMessage = GroupMessage | PrivateFriendMessage | PrivateGroupMessage 7 | export type CommonMessage = NapcatMessage 8 | 9 | export interface LiteCycleHook { 10 | onMessage: (msg: any) => void | Promise 11 | onNapcatMessage: (msg: any) => void | Promise 12 | onGroupMessage: (msg: any) => void | Promise 13 | onPrivateMessage: (msg: any) => void | Promise 14 | onPrivateFriendMessage: (msg: any) => void | Promise 15 | onPrivateGroupMessage: (msg: any) => void | Promise 16 | } 17 | 18 | /** 19 | * listen to all message 20 | * - napcat 21 | * - TODO: other platform 22 | * @param handler 23 | */ 24 | export function onMessage( 25 | handler: (msg: NapcatMessage) => void | Promise, 26 | ) { 27 | // consola.info('onMessage', handler) 28 | currentInstance?.hooks.addHooks({ 29 | onMessage: handler, 30 | }) 31 | } 32 | 33 | /** 34 | * only listen to napcat message 35 | * @param handler 36 | */ 37 | export function onNapcatMessage( 38 | handler: (msg: NapcatMessage) => void | Promise, 39 | ) { 40 | currentInstance?.hooks.addHooks({ 41 | onNapcatMessage: handler, 42 | }) 43 | } 44 | 45 | /** 46 | * listen to private message 47 | * @param handler 48 | */ 49 | export function onPrivateFriendMessage( 50 | handler: (msg: PrivateFriendMessage) => void | Promise, 51 | ) { 52 | currentInstance?.hooks.addHooks({ 53 | onPrivateFriendMessage: handler, 54 | }) 55 | } 56 | 57 | /** 58 | * listen to private message 59 | * @param handler 60 | */ 61 | export function onPrivateGroupMessage( 62 | handler: (msg: PrivateFriendMessage) => void | Promise, 63 | ) { 64 | currentInstance?.hooks.addHooks({ 65 | onPrivateGroupMessage: handler, 66 | }) 67 | } 68 | 69 | export function onPrivateMessage( 70 | handler: (msg: PrivateFriendMessage) => void | Promise, 71 | ) { 72 | currentInstance?.hooks.addHooks({ 73 | onPrivateMessage: handler, 74 | }) 75 | } 76 | 77 | /** 78 | * listen to group message 79 | * @param handler 80 | */ 81 | export function onGroupMessage( 82 | handler: (msg: GroupMessage) => void | Promise, 83 | ) { 84 | currentInstance?.hooks.addHooks({ 85 | onGroupMessage: handler, 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /demo/bot/plugins/qq/constants.ts: -------------------------------------------------------------------------------- 1 | import { MessageToCreate } from 'qq-guild-bot' 2 | 3 | /** 4 | * @see https://bot.q.qq.com/wiki/develop/nodesdk/message/message_template.html#list-%E7%BB%93%E6%9E%84 5 | */ 6 | export const arkTemplateMessage: MessageToCreate = { 7 | ark: { 8 | template_id: 23, 9 | kv: [ 10 | { 11 | key: '#DESC#', 12 | value: 'descaaaaaa', 13 | }, 14 | { 15 | key: '#PROMPT#', 16 | value: 'promptaaaa', 17 | }, 18 | { 19 | key: '#LIST#', 20 | obj: [ 21 | { 22 | obj_kv: [ 23 | { 24 | key: 'desc', 25 | value: '需求标题:UI问题解决', 26 | }, 27 | ], 28 | }, 29 | { 30 | obj_kv: [ 31 | { 32 | key: 'desc', 33 | value: '当前状态"体验中"点击下列动作直接扭转状态到:', 34 | }, 35 | ], 36 | }, 37 | { 38 | obj_kv: [ 39 | { 40 | key: 'desc', 41 | value: '已评审', 42 | }, 43 | { 44 | key: 'link', 45 | // value: 'https://yunyoujun.cn/about', 46 | }, 47 | ], 48 | }, 49 | { 50 | obj_kv: [ 51 | { 52 | key: 'desc', 53 | value: '已排期', 54 | }, 55 | { 56 | key: 'link', 57 | // value: 'https://yunyoujun.cn/about', 58 | }, 59 | ], 60 | }, 61 | { 62 | obj_kv: [ 63 | { 64 | key: 'desc', 65 | value: '开发中', 66 | }, 67 | { 68 | key: 'link', 69 | // value: 'https://yunyoujun.cn/about', 70 | }, 71 | ], 72 | }, 73 | { 74 | obj_kv: [ 75 | { 76 | key: 'desc', 77 | value: '增量测试中', 78 | }, 79 | { 80 | key: 'link', 81 | // value: 'https://yunyoujun.cn/about', 82 | }, 83 | ], 84 | }, 85 | { 86 | obj_kv: [ 87 | { 88 | key: 'desc', 89 | value: '请关注', 90 | }, 91 | ], 92 | }, 93 | ], 94 | }, 95 | ], 96 | } as any, 97 | } 98 | -------------------------------------------------------------------------------- /packages/cli/src/install/repo.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import download from 'download' 3 | import fs from 'fs-extra' 4 | import { createLogger } from 'packages/el-bot' 5 | import ProgressBar from 'progress' 6 | 7 | const logger = createLogger().child({ label: '📦' }) 8 | 9 | /** 10 | * Repo 类 11 | */ 12 | export default class Repo { 13 | /** 14 | * Latest Release 信息链接 15 | * https://developer.github.com/v3/repos/releases/#get-the-latest-release 16 | */ 17 | url: string 18 | /** 19 | * 版本 20 | */ 21 | version: string 22 | /** 23 | * release 下载链接 24 | */ 25 | browser_download_url: string 26 | constructor(public owner: string, public repo: string) { 27 | this.url = `https://api.github.com/repos/${owner}/${repo}/releases/latest` 28 | this.version = '' 29 | this.browser_download_url = '' 30 | } 31 | 32 | async getLatestVersion() { 33 | const browser_download_url = await axios 34 | .get(this.url) 35 | .then((res) => { 36 | this.version = res.data.tag_name 37 | this.browser_download_url = res.data.assets[0].browser_download_url 38 | logger.info(`Latest Version: ${this.version}`) 39 | return this.browser_download_url 40 | }) 41 | .catch((err) => { 42 | logger.error(err.message) 43 | logger.error('获取最新版本失败') 44 | }) 45 | return browser_download_url 46 | } 47 | 48 | async downloadLatestRelease(dest = '.') { 49 | if (!this.browser_download_url) { 50 | const lastestVersion = await this.getLatestVersion() 51 | if (!lastestVersion) 52 | return 53 | } 54 | 55 | const filename = this.browser_download_url.split('/').pop() 56 | const path = `${dest}/${filename}` 57 | 58 | if (fs.existsSync(path)) { 59 | logger.error(`${path} 已存在!`) 60 | return 61 | } 62 | 63 | try { 64 | download(this.browser_download_url, path) 65 | .on('response', (res) => { 66 | const bar = new ProgressBar( 67 | `下载至 ${dest} [:bar] :percent (:rate KB/s :total KB) :etas`, 68 | { 69 | complete: '=', 70 | incomplete: ' ', 71 | width: 20, 72 | total: 0, 73 | }, 74 | ) 75 | 76 | bar.total = Number.parseInt(res.headers['content-length'] || '', 10) / 1000 77 | res.on('data', (data: any) => bar.tick(data.length / 8000)) 78 | }) 79 | .then(() => logger.success('下载完成')) 80 | } 81 | catch (err: any) { 82 | logger.error(err.message) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/el-bot/core/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { BotConfig, BotUserConfig } from './bot' 2 | import type { ElUserConfig } from './el' 3 | import path from 'node:path' 4 | import process from 'node:process' 5 | import fs from 'fs-extra' 6 | import { createLogger } from '../bot/logger/winston' 7 | 8 | export * from './bot' 9 | export * from './el' 10 | 11 | // config 12 | const logger = createLogger().child({ label: '⚙️' }) 13 | 14 | /** 15 | * 从文件加载配置 16 | */ 17 | export async function loadConfigFromFile( 18 | configFile?: string, 19 | configRoot: string = process.cwd(), 20 | ) { 21 | let resolvedPath: string | undefined 22 | // let isTS = false; 23 | // let isMjs = false; 24 | 25 | const configName = 'el' 26 | 27 | // check package.json for type: "module" and set `isMjs` to true 28 | // try { 29 | // const pkg = lookupFile(configRoot, ["package.json"]); 30 | // if (pkg && JSON.parse(pkg).type === "module") { 31 | // isMjs = true; 32 | // } 33 | // } catch (e) {} 34 | 35 | if (configFile) { 36 | // explicit config path is always resolved from cwd 37 | resolvedPath = path.resolve(configFile) 38 | } 39 | else { 40 | // implicit config file loaded from inline root (if present) 41 | // otherwise from cwd 42 | const jsconfigFile = path.resolve(configRoot, `${configName}.config.js`) 43 | if (fs.existsSync(jsconfigFile)) 44 | resolvedPath = jsconfigFile 45 | 46 | if (!resolvedPath) { 47 | const mjsconfigFile = path.resolve( 48 | configRoot, 49 | `${configName}.config.mjs`, 50 | ) 51 | if (fs.existsSync(mjsconfigFile)) 52 | resolvedPath = mjsconfigFile 53 | // isMjs = true; 54 | } 55 | 56 | if (!resolvedPath) { 57 | const tsconfigFile = path.resolve(configRoot, `${configName}.config.ts`) 58 | if (fs.existsSync(tsconfigFile)) 59 | resolvedPath = tsconfigFile 60 | // isTS = true; 61 | } 62 | 63 | if (!resolvedPath) { 64 | logger.debug('no config file found.') 65 | return null 66 | } 67 | 68 | try { 69 | const botConfig: BotConfig | undefined = await import(resolvedPath) 70 | return botConfig 71 | } 72 | catch (e) { 73 | logger.error(`无法正确加载配置文件:${resolvedPath}`) 74 | throw e 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * el-bot.config.ts 81 | * 机器人全局配置 82 | * @param config 83 | */ 84 | export function defineConfig(config: ElUserConfig): ElUserConfig { 85 | return config 86 | } 87 | 88 | /** 89 | * Bot 配置 90 | * @param config 91 | */ 92 | export function defineBotConfig(config: BotUserConfig): BotUserConfig { 93 | return config 94 | } 95 | -------------------------------------------------------------------------------- /packages/el-bot/node/server/webhook/octokit.ts: -------------------------------------------------------------------------------- 1 | import { Webhooks } from '@octokit/webhooks' 2 | import consola from 'consola' 3 | import { Structs } from 'node-napcat-ts' 4 | import colors from 'picocolors' 5 | import { getCurrentInstance } from '../../../core' 6 | import { OctokitOptions } from './types' 7 | 8 | /** 9 | * @see https://github.com/octokit/webhooks.js#local-development 10 | * @see https://smee.io/ Start a new channel 11 | */ 12 | export function localDevForWebhook(webhooks: Webhooks) { 13 | const webhookProxyUrl = 'https://smee.io/IrqK0nopGAOc847' // replace with your own Webhook Proxy URL 14 | const source = new EventSource(webhookProxyUrl) 15 | source.onmessage = (event) => { 16 | const webhookEvent = JSON.parse(event.data) 17 | webhooks 18 | .verifyAndReceive({ 19 | id: webhookEvent['x-request-id'], 20 | name: webhookEvent['x-github-event'], 21 | signature: webhookEvent['x-hub-signature'], 22 | payload: JSON.stringify(webhookEvent.body), 23 | }) 24 | .catch(console.error) 25 | } 26 | } 27 | 28 | /** 29 | * create octokit webhooks 30 | */ 31 | export function createOctokitWebhooks(octokitOptions: OctokitOptions) { 32 | const webhooks = new Webhooks({ 33 | secret: octokitOptions.secret || 'el-psy-congroo', 34 | }) 35 | 36 | webhooks.onAny(({ id, name, payload }) => { 37 | consola.debug(payload) 38 | consola.box(`🪝 ${colors.green(name)} event received: ${colors.dim(id)}`) 39 | }) 40 | 41 | webhooks.on('push', ({ payload }) => { 42 | const compareDiffCommit = payload.compare.split('/').pop() 43 | const messages = [ 44 | `🔗: ${`${colors.dim(payload.repository.url)}/compare/${colors.yellow(compareDiffCommit)}`}`, 45 | `🤺: ${colors.green(payload.pusher.username || payload.pusher.name)}<${colors.dim(payload.pusher.email)}>`, 46 | `📦: ${colors.cyan(payload.repository.url)}`, 47 | `💬: ${payload.head_commit?.message} ${colors.dim('by')} ${colors.dim(payload.head_commit?.author.username)}`, 48 | ] 49 | consola.box(messages.join('\n')) 50 | 51 | // TODO: send message to qq 52 | const ctx = getCurrentInstance() 53 | const { napcat } = ctx 54 | const plainMessages = [ 55 | `🔗: ${payload.repository.url}/compare/${compareDiffCommit}`, 56 | `🤺: ${payload.pusher.username || payload.pusher.name}<${payload.pusher.email}>`, 57 | `📦: ${payload.repository.url}`, 58 | `💬: ${payload.head_commit?.message} by ${payload.head_commit?.author.username}`, 59 | ] 60 | napcat.send_group_msg({ 61 | group_id: 120117362, 62 | message: [ 63 | Structs.text(plainMessages.join('\n')), 64 | ], 65 | }) 66 | }) 67 | 68 | return webhooks 69 | } 70 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/answer/index.ts: -------------------------------------------------------------------------------- 1 | import type { AnswerOptions } from './utils' 2 | // import axios from 'axios' 3 | // import * as nodeSchdule from 'node-schedule' 4 | import { defineBotPlugin, onNapcatMessage } from '../../core' 5 | // import { displayAnswerList, renderString } from './utils' 6 | import pkg from './package.json' 7 | 8 | export * from './utils' 9 | 10 | export default defineBotPlugin((options) => { 11 | return { 12 | pkg, 13 | // extendCli: (cli) => { 14 | // cli 15 | // .command('answer') 16 | // .option('-l --list') 17 | // .action((opts) => { 18 | // if (opts.list) { 19 | // const answerList = displayAnswerList(options) 20 | // ctx.reply(answerList) 21 | // } 22 | // }) 23 | // }, 24 | 25 | setup(ctx) { 26 | // 设置定时 27 | // options.forEach((ans) => { 28 | // if (ans.cron) { 29 | // nodeSchdule.scheduleJob(ans.cron, async () => { 30 | // if (!ans.target) 31 | // return 32 | // const replyContent = ans.api 33 | // ? await renderStringByApi(ans.api, ans.reply) 34 | // : ans.reply 35 | // ctx.sender.sendMessageByConfig(replyContent, ans.target) 36 | // }) 37 | // } 38 | // }) 39 | 40 | // 应答 41 | onNapcatMessage(async (msg) => { 42 | // use async in some 43 | // https://advancedweb.hu/how-to-use-async-functions-with-array-some-and-every-in-javascript/ 44 | for await (const ans of options.list) { 45 | // const replyContent = null 46 | // if (ans.at) { 47 | // if (!(msg.type === 'GroupMessage' && msg.isAt())) 48 | // return 49 | // } 50 | 51 | if (ans.receivedText?.includes(msg.raw_message)) { 52 | await ctx.reply(msg, ans.reply) 53 | break 54 | } 55 | 56 | // if (msg.plain && check.match(msg.plain, ans)) { 57 | // // 默认监听所有 58 | // if (ctx.status.getListenStatusByConfig(msg.sender, ans)) { 59 | // replyContent = ans.api 60 | // ? await renderStringByApi(ans.api, ans.reply) 61 | // : ans.reply 62 | // } 63 | // else if (ans.else) { 64 | // // 后续可以考虑用监听白名单、黑名单优化 65 | // replyContent = ans.api 66 | // ? await renderStringByApi(ans.api, ans.else) 67 | // : ans.else 68 | // } 69 | 70 | // if (replyContent) { 71 | // await msg.reply(replyContent, ans.quote) 72 | // // 有一个满足即跳出 73 | // break 74 | // } 75 | } 76 | }) 77 | }, 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /plugins/search-image/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import type { MessageType } from 'mirai-ts' 3 | import { utils } from 'el-bot' 4 | import { Message } from 'mirai-ts' 5 | import * as sagiri from 'sagiri' 6 | 7 | /** 8 | * 搜图设置 9 | */ 10 | interface SearchImageOptions { 11 | token: string 12 | options?: sagiri.Options 13 | } 14 | 15 | /** 16 | * 17 | * @param result 格式化结果 18 | */ 19 | function formatResult(result: sagiri.SagiriResult): MessageType.MessageChain { 20 | if (!result) 21 | return [] 22 | const msgChain = [ 23 | Message.Plain('\n------------------\n'), 24 | Message.Image(null, result.thumbnail), 25 | Message.Plain(`\n相似度:${result.similarity}`), 26 | Message.Plain(`\n站点:${result.site}`), 27 | Message.Plain(`\n链接:${result.url}`), 28 | Message.Plain(`\n作者:${result.authorName || '未知'}`), 29 | ] 30 | return msgChain 31 | } 32 | 33 | /** 34 | * 搜图 [SauceNAO](https://saucenao.com/) 35 | */ 36 | export default async function searchImage( 37 | ctx: Bot, 38 | options: SearchImageOptions, 39 | ) { 40 | const { mirai } = ctx 41 | const client = sagiri.default(options.token, options.options) 42 | 43 | const innerMode = new utils.InnerMode() 44 | 45 | mirai.on('message', (msg) => { 46 | innerMode.setMsg(msg) 47 | 48 | if (msg.plain === '搜图') { 49 | innerMode.enter() 50 | msg.reply('我准备好了!') 51 | } 52 | 53 | if (innerMode.getStatus()) { 54 | msg.messageChain.forEach(async (singleMessage) => { 55 | if (singleMessage.type === 'Image' && singleMessage.url) { 56 | let replyContent: MessageType.MessageChain = [] 57 | 58 | try { 59 | const results = await client(singleMessage.url) 60 | if (results.length === 0) { 61 | replyContent.push(Message.Plain('未搜索到相关图片')) 62 | } 63 | else { 64 | const length = Math.min( 65 | options.options?.results || 3, 66 | results.length, 67 | ) 68 | replyContent.push(Message.Plain(`返回 ${length} 个结果`)) 69 | for (let i = 0; i < length; i++) { 70 | const result = results[i] 71 | const formatContent = formatResult(result) 72 | replyContent = replyContent.concat(formatContent) 73 | } 74 | } 75 | msg.reply(replyContent) 76 | 77 | // 退出搜图模式 78 | innerMode.exit() 79 | } 80 | catch (err: any) { 81 | if (err) { 82 | utils.handleError(err) 83 | if (err.message) 84 | ctx.logger.error('[search-image]', err.message) 85 | } 86 | } 87 | } 88 | }) 89 | } 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "private": true, 4 | "packageManager": "bun@1.2.7", 5 | "workspaces": [ 6 | "packages/*", 7 | "examples/*", 8 | "plugins/*", 9 | "demo", 10 | "docs" 11 | ], 12 | "author": { 13 | "name": "云游君", 14 | "email": "me@yunyoujun.cn", 15 | "url": "https://www.yunyoujun.cn" 16 | }, 17 | "license": "AGPL-3.0", 18 | "homepage": "https://docs.bot.elpsy.cn/", 19 | "repository": "https://github.com/YunYouJun/el-bot", 20 | "bugs": { 21 | "url": "https://github.com/YunYouJun/el-bot/issues" 22 | }, 23 | "engines": { 24 | "node": ">=12.0.0" 25 | }, 26 | "ecosystem": { 27 | "el-bot-api": "https://github.com/ElpsyCN/el-bot-api", 28 | "el-bot-plugins": "https://github.com/ElpsyCN/el-bot-plugins", 29 | "el-bot-docs": "https://github.com/ElpsyCN/el-bot-docs", 30 | "el-bot-template": "https://github.com/ElpsyCN/el-bot-template", 31 | "el-bot-web": "https://github.com/ElpsyCN/el-bot-web" 32 | }, 33 | "scripts": { 34 | "build": "", 35 | "build:lib": "pnpm -C packages/el-bot run build", 36 | "build:demo": "pnpm run build -C demo", 37 | "build:api": "pnpm -C packages/el-bot run build:api", 38 | "demo": "pnpm -C demo run start", 39 | "demo:dev": "pnpm -C demo run dev", 40 | "example:simple": "pnpm -C examples/simple run dev", 41 | "dev": "npm run demo:dev", 42 | "dev:bot": "npm run dev:simple", 43 | "dev:simple": "pnpm -C examples/simple run dev", 44 | "docs:dev": "pnpm -C docs dev", 45 | "docs:build": "pnpm -C docs run build", 46 | "lint": "eslint .", 47 | "lint:fix": "eslint \"**/*.{ts,js}\" --fix", 48 | "cqhttp": "cd go-cqhttp && ./go-cqhttp faststart", 49 | "cqhttp:update": "cd go-cqhttp && ./go-cqhttp update", 50 | "start": "pnpm run start:demo", 51 | "release": "bumpp", 52 | "start:demo": "pnpm -C demo start", 53 | "test": "vitest", 54 | "typecheck": "tsc --noEmit" 55 | }, 56 | "dependencies": { 57 | "axios": "^1.8.4" 58 | }, 59 | "devDependencies": { 60 | "@antfu/eslint-config": "^4.11.0", 61 | "@types/bun": "^1.2.8", 62 | "@types/fs-extra": "^11.0.4", 63 | "@types/node": "^22.13.14", 64 | "@unocss/eslint-plugin": "^66.1.0-beta.7", 65 | "bumpp": "^10.1.0", 66 | "eslint": "^9.23.0", 67 | "eslint-plugin-format": "^1.0.1", 68 | "husky": "9.1.4", 69 | "lint-staged": "^15.5.0", 70 | "nodemon": "^3.1.9", 71 | "npm-run-all": "^4.1.5", 72 | "pm2": "^6.0.5", 73 | "tsx": "^4.19.3", 74 | "typescript": "^5.8.2", 75 | "vitest": "^3.0.9" 76 | }, 77 | "pnpm": { 78 | "overrides": { 79 | "el-bot": "workspace:*" 80 | } 81 | }, 82 | "lint-staged": { 83 | "*.{js,ts}": [ 84 | "eslint --fix" 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/sender.ts: -------------------------------------------------------------------------------- 1 | // import consola from 'consola' 2 | // import type * as Config from '../types/config' 3 | import type { Bot } from './index' 4 | 5 | export class Sender { 6 | constructor(public ctx: Bot) {} 7 | 8 | // /** 9 | // * 根据 QQ 号数组列表发送消息 10 | // * @param messageChain 11 | // * @param array qq 列表 12 | // */ 13 | // sendFriendMessageByArray( 14 | // messageChain: string | MessageType.MessageChain, 15 | // array: number[], 16 | // messageList: number[], 17 | // ) { 18 | // const mirai = this.ctx.mirai 19 | // return Promise.all( 20 | // array.map(async (qq) => { 21 | // const { messageId } = await mirai.api.sendFriendMessage( 22 | // messageChain, 23 | // qq, 24 | // ) 25 | // messageList.push(messageId) 26 | // }), 27 | // ) 28 | // } 29 | 30 | // /** 31 | // * 通过配置发送消息 32 | // * @param messageChain 33 | // * @param target 34 | // */ 35 | // async sendMessageByConfig( 36 | // messageChain: string | MessageType.MessageChain, 37 | // target: Config.Target, 38 | // ): Promise { 39 | // const mirai = this.ctx.mirai 40 | // const botConfig = this.ctx.el.bot 41 | // const messageList: number[] = [] 42 | 43 | // if (Array.isArray(messageChain)) { 44 | // messageChain.forEach((msg) => { 45 | // if (msg.type === 'Image') 46 | // msg.imageId = '' 47 | // }) 48 | // } 49 | 50 | // if (Array.isArray(target) || typeof target === 'string') { 51 | // if (target.includes('master')) { 52 | // await this.sendFriendMessageByArray( 53 | // messageChain, 54 | // botConfig.master, 55 | // messageList, 56 | // ) 57 | // } 58 | 59 | // if (target.includes('admin') && botConfig.admin) { 60 | // await this.sendFriendMessageByArray( 61 | // messageChain, 62 | // botConfig.admin, 63 | // messageList, 64 | // ) 65 | // } 66 | // } 67 | 68 | // if (target.group) { 69 | // await Promise.all( 70 | // target.group.map(async (qq: number) => { 71 | // const { messageId } = await mirai.api.sendGroupMessage( 72 | // messageChain, 73 | // qq, 74 | // ) 75 | // messageList.push(messageId) 76 | // }), 77 | // ) 78 | // } 79 | 80 | // if (target.friend) { 81 | // try { 82 | // await this.sendFriendMessageByArray( 83 | // messageChain, 84 | // target.friend, 85 | // messageList, 86 | // ) 87 | // } 88 | // catch (err: any) { 89 | // this.ctx.logger.error('发送失败:可能是由于 mirai 私聊暂不支持长文本') 90 | // consola.error(err) 91 | // } 92 | // } 93 | 94 | // return messageList 95 | // } 96 | } 97 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/answer/utils.ts: -------------------------------------------------------------------------------- 1 | import type { MessageType } from 'mirai-ts' 2 | import type nodeSchdule from 'node-schedule' 3 | import type * as Config from '../../types/config' 4 | import axios from 'axios' 5 | import { Send } from 'node-napcat-ts' 6 | import { renderString } from '../../core/utils' 7 | 8 | export type ReplyContent = string | Send[keyof Send][] 9 | 10 | interface BaseAnswerOptions { 11 | /** 12 | * 监听 13 | */ 14 | listen?: string | Config.ListenTarget 15 | /** 16 | * 不监听 17 | */ 18 | unListen?: Config.ListenTarget 19 | /** 20 | * 定时任务 21 | */ 22 | cron?: nodeSchdule.RecurrenceRule 23 | /** 24 | * 定时发送的对象 25 | */ 26 | target?: Config.Target 27 | /** 28 | * API 地址,存在时,自动渲染字符串 29 | */ 30 | api?: string 31 | 32 | /** 33 | * 是否命中 用于自定义判断 34 | * @example 35 | * ```ts 36 | * hit: (msg) => msg.raw_message === 'ping' 37 | * ``` 38 | */ 39 | hit?: (msg: string) => boolean 40 | 41 | /** 42 | * 接收到的文本 完全匹配 时回复 43 | * @example 44 | * ```ts 45 | * { 46 | * receivedText: 'ping', 47 | * reply: 'pong' 48 | * } 49 | */ 50 | receivedText?: string[] 51 | reply: ReplyContent 52 | /** 53 | * 只有被 @ 时回复 54 | */ 55 | at?: boolean 56 | /** 57 | * 回复时是否引用消息 58 | */ 59 | quote?: boolean 60 | else?: ReplyContent 61 | /** 62 | * 帮助信息 63 | */ 64 | help?: string 65 | } 66 | 67 | /** 68 | * @internal 69 | * @description Answer 插件配置 70 | * @example 71 | * ```ts 72 | * // el-bot.config.ts 73 | * import { answerPlugin, defineConfig } from 'el-bot' 74 | * export default defineConfig({ 75 | * bot: { 76 | * plugins: [ 77 | * answerPlugin({ 78 | * list: [] 79 | * }) 80 | * ] 81 | * }, 82 | * }) 83 | * ``` 84 | */ 85 | export interface AnswerOptions { 86 | list: BaseAnswerOptions[] 87 | } 88 | 89 | /** 90 | * 输出回答列表 91 | */ 92 | export function displayAnswerList(options: AnswerOptions) { 93 | let content = '回答列表:' 94 | options.list.forEach((option) => { 95 | if (option.help) 96 | content += `\n- ${option.help}` 97 | }) 98 | return content 99 | } 100 | 101 | /** 102 | * 根据 API 返回的内容渲染字符串 103 | * @param api 104 | * @param content 105 | */ 106 | export async function renderStringByApi( 107 | api: string, 108 | content: ReplyContent, 109 | ) { 110 | const { data } = await axios.get(api) 111 | if (typeof content === 'string') { 112 | return renderString(content, data, 'data') 113 | } 114 | else { 115 | if (!content) 116 | return 117 | (content as any).forEach((msg: MessageType.SingleMessage) => { 118 | if (msg.type === 'Plain') 119 | msg.text = renderString(msg.text, data, 'data') 120 | }) 121 | return content 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/workflow/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from 'el-bot' 2 | import fs from 'fs-extra' 3 | 4 | /** 5 | * ref github actions 6 | * https://docs.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow 7 | */ 8 | 9 | import type { EventType, MessageType } from 'mirai-ts' 10 | import schedule from 'node-schedule' 11 | import * as shelljs from 'shelljs' 12 | import { parseYaml } from '../../utils/config' 13 | import { handleError } from '../../utils/error' 14 | 15 | interface step { 16 | name?: string 17 | run?: string 18 | reply: string | MessageType.MessageChain 19 | } 20 | 21 | interface Job { 22 | name?: string 23 | steps: step[] 24 | } 25 | 26 | type Jobs = Record 27 | 28 | type MessageAndEventType = 29 | | 'message' 30 | | EventType.EventType 31 | | MessageType.ChatMessageType 32 | 33 | /** 34 | * 定时格式 35 | */ 36 | interface Schedule { 37 | cron: string 38 | } 39 | 40 | interface On { 41 | schedule: [Schedule] 42 | } 43 | 44 | interface WorkflowConfig { 45 | name: string 46 | on: On | MessageAndEventType | MessageAndEventType[] 47 | jobs: Jobs 48 | } 49 | 50 | /** 51 | * config a workflow 52 | */ 53 | function createWorkflow(ctx: Bot, workflow: WorkflowConfig) { 54 | const mirai = ctx.mirai 55 | if (!workflow.on) 56 | return 57 | 58 | if (Array.isArray(workflow.on)) { 59 | workflow.on.forEach((on) => { 60 | trigger(on) 61 | }) 62 | } 63 | else if (typeof workflow.on === 'string') { 64 | trigger(workflow.on) 65 | } 66 | else if ((workflow.on as On).schedule) { 67 | (workflow.on as On).schedule.forEach((singleSchedule) => { 68 | schedule.scheduleJob(singleSchedule.cron, () => { 69 | doJobs(workflow.jobs) 70 | }) 71 | }) 72 | } 73 | 74 | /** 75 | * 触发 76 | * @param type 77 | */ 78 | function trigger(type: MessageAndEventType) { 79 | mirai.on(type, (msg) => { 80 | Object.keys(workflow.jobs).forEach((name) => { 81 | const job = workflow.jobs[name] 82 | job.steps.forEach((step) => { 83 | if (msg.reply) 84 | msg.reply(step.reply) 85 | }) 86 | }) 87 | }) 88 | } 89 | 90 | /** 91 | * 运行 jobs 中终端命令 92 | */ 93 | function doJobs(jobs: Jobs) { 94 | Object.keys(jobs).forEach((name) => { 95 | const job = jobs[name] 96 | job.steps.forEach((step) => { 97 | if (step.run) 98 | shelljs.exec(step.run) 99 | }) 100 | }) 101 | } 102 | } 103 | 104 | export default function workflow(ctx: Bot) { 105 | try { 106 | const folder = './el/workflows' 107 | const files = fs.readdirSync(folder) 108 | files.forEach((file) => { 109 | const workflow = parseYaml(`${folder}/${file}`) 110 | if (workflow) 111 | createWorkflow(ctx, workflow as WorkflowConfig) 112 | }) 113 | } 114 | catch (err: any) { 115 | // 不是 文件不存在 的错误时,才打印出错信息 116 | if (err && err.code !== 'ENOENT') 117 | handleError(err) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /docs/.vitepress/components/ChatMessage.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 72 | 73 | 128 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/limit/index.ts: -------------------------------------------------------------------------------- 1 | // import type { Bot } from '../../core' 2 | // import type { LimitOptions } from './options' 3 | 4 | export interface GroupInfo { 5 | /** 6 | * 上一位发送者的 QQ 7 | */ 8 | lastSenderId: number 9 | /** 10 | * 发言次数 11 | */ 12 | count: number 13 | } 14 | 15 | export type GroupList = Record 16 | 17 | // export default function limit(ctx: Bot, options: LimitOptions) { 18 | // // const { mirai } = ctx 19 | 20 | // let count = 0 21 | // let startTime = new Date().getTime() 22 | // let now = startTime 23 | 24 | // /** 25 | // * 是否被限制 26 | // */ 27 | // function isLimited() { 28 | // now = new Date().getTime() 29 | // if (now - startTime > options.interval) { 30 | // count = 0 31 | // startTime = now 32 | // } 33 | // return count > options.count 34 | // } 35 | 36 | // /** 37 | // * 发送者连续触发次数是否超过限额 38 | // */ 39 | // let lastList: GroupList = {} 40 | // async function isMaxCountForSender(): Promise { 41 | // let msg 42 | // if (mirai.curMsg && mirai.curMsg.type === 'GroupMessage') 43 | // msg = mirai.curMsg 44 | // else 45 | // return false 46 | 47 | // // 如果超过间隔时间,则重置历史记录 48 | // now = new Date().getTime() 49 | // if (now - startTime > options.sender.interval) 50 | // lastList = {} 51 | 52 | // const senderId = msg.sender.id 53 | // const groupId = msg.sender.group.id 54 | // if (lastList[groupId]) { 55 | // if (lastList[groupId].lastSenderId === senderId) { 56 | // lastList[groupId].count += 1 57 | // } 58 | // else { 59 | // lastList[groupId].lastSenderId = senderId 60 | // lastList[groupId].count = 1 61 | // } 62 | // } 63 | // else { 64 | // lastList[groupId] = { 65 | // lastSenderId: senderId, 66 | // count: 1, 67 | // } 68 | // } 69 | 70 | // // 同一个用户连续调用多次(不限制有机器人管理权限的人) 71 | // if ( 72 | // lastList[groupId].count > options.sender.maximum 73 | // && !ctx.user.isAllowed(senderId) 74 | // ) { 75 | // lastList[groupId].count = 0 76 | 77 | // await msg.reply(options.sender.tooltip) 78 | // await mirai.api.mute(groupId, senderId, options.sender.time) 79 | // return true 80 | // } 81 | // return false 82 | // } 83 | 84 | // // 只限制群消息 85 | // const sendGroupMessage = mirai.api.sendGroupMessage 86 | 87 | // mirai.api.sendGroupMessage = async (messageChain, target, quote) => { 88 | // let data = { 89 | // code: -1, 90 | // msg: 'fail', 91 | // messageId: 0, 92 | // } 93 | 94 | // const isMax = await isMaxCountForSender() 95 | // if (isMax) 96 | // return data 97 | 98 | // if (!isLimited()) { 99 | // count += 1 100 | // data = await sendGroupMessage.apply(mirai.api, [ 101 | // messageChain, 102 | // target, 103 | // quote, 104 | // ]) 105 | // return data 106 | // } 107 | // else { 108 | // ctx.logger.error('[limit] 群消息发送太频繁啦!') 109 | // } 110 | // return data 111 | // } 112 | // } 113 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # go-cqhttp 默认配置文件 2 | 3 | account: # 账号相关 4 | uin: 1233456 # QQ账号 5 | password: '' # 密码为空时使用扫码登录 6 | encrypt: false # 是否开启密码加密 7 | status: 0 # 在线状态 请参考 https://docs.go-cqhttp.org/guide/config.html#在线状态 8 | relogin: # 重连设置 9 | delay: 3 # 首次重连延迟, 单位秒 10 | interval: 3 # 重连间隔 11 | max-times: 0 # 最大重连次数, 0为无限制 12 | 13 | # 是否使用服务器下发的新地址进行重连 14 | # 注意, 此设置可能导致在海外服务器上连接情况更差 15 | use-sso-address: true 16 | 17 | heartbeat: 18 | # 心跳频率, 单位秒 19 | # -1 为关闭心跳 20 | interval: 5 21 | 22 | message: 23 | # 上报数据类型 24 | # 可选: string,array 25 | post-format: string 26 | # 是否忽略无效的CQ码, 如果为假将原样发送 27 | ignore-invalid-cqcode: false 28 | # 是否强制分片发送消息 29 | # 分片发送将会带来更快的速度 30 | # 但是兼容性会有些问题 31 | force-fragment: false 32 | # 是否将url分片发送 33 | fix-url: false 34 | # 下载图片等请求网络代理 35 | proxy-rewrite: '' 36 | # 是否上报自身消息 37 | report-self-message: false 38 | # 移除服务端的Reply附带的At 39 | remove-reply-at: false 40 | # 为Reply附加更多信息 41 | extra-reply-data: false 42 | # 跳过 Mime 扫描, 忽略错误数据 43 | skip-mime-scan: false 44 | 45 | output: 46 | # 日志等级 trace,debug,info,warn,error 47 | log-level: warn 48 | # 日志时效 单位天. 超过这个时间之前的日志将会被自动删除. 设置为 0 表示永久保留. 49 | log-aging: 15 50 | # 是否在每次启动时强制创建全新的文件储存日志. 为 false 的情况下将会在上次启动时创建的日志文件续写 51 | log-force-new: true 52 | # 是否启用日志颜色 53 | log-colorful: true 54 | # 是否启用 DEBUG 55 | debug: false # 开启调试模式 56 | 57 | # 默认中间件锚点 58 | default-middlewares: &default 59 | # 访问密钥, 强烈推荐在公网的服务器设置 60 | access-token: '' 61 | # 事件过滤器文件目录 62 | filter: '' 63 | # API限速设置 64 | # 该设置为全局生效 65 | # 原 cqhttp 虽然启用了 rate_limit 后缀, 但是基本没插件适配 66 | # 目前该限速设置为令牌桶算法, 请参考: 67 | # https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin 68 | rate-limit: 69 | enabled: false # 是否启用限速 70 | frequency: 1 # 令牌回复频率, 单位秒 71 | bucket: 1 # 令牌桶大小 72 | 73 | database: # 数据库相关设置 74 | leveldb: 75 | # 是否启用内置leveldb数据库 76 | # 启用将会增加10-20MB的内存占用和一定的磁盘空间 77 | # 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能 78 | enable: true 79 | 80 | # 媒体文件缓存, 删除此项则使用缓存文件(旧版行为) 81 | cache: 82 | image: data/image.db 83 | video: data/video.db 84 | 85 | # 连接服务列表 86 | servers: 87 | # 添加方式,同一连接方式可添加多个,具体配置说明请查看文档 88 | # - http: # http 通信 89 | # - ws: # 正向 Websocket 90 | # - ws-reverse: # 反向 Websocket 91 | # - pprof: #性能分析服务器 92 | # HTTP 通信设置 93 | - http: 94 | # 服务端监听地址 95 | host: 127.0.0.1 96 | # 服务端监听端口 97 | port: 5700 98 | # 反向HTTP超时时间, 单位秒 99 | # 最小值为5,小于5将会忽略本项设置 100 | timeout: 5 101 | # 长轮询拓展 102 | long-polling: 103 | # 是否开启 104 | enabled: false 105 | # 消息队列大小,0 表示不限制队列大小,谨慎使用 106 | max-queue-size: 2000 107 | middlewares: 108 | <<: *default # 引用默认中间件 109 | # 反向HTTP POST地址列表 110 | post: 111 | # - url: '' # 地址 112 | # secret: '' # 密钥 113 | # - url: 127.0.0.1:5701 # 地址 114 | # secret: '' # 密钥 115 | # 正向WS设置 116 | - ws: 117 | # 正向WS服务器监听地址 118 | host: 127.0.0.1 119 | # 正向WS服务器监听端口 120 | port: 6700 121 | middlewares: 122 | <<: *default # 引用默认中间件 123 | -------------------------------------------------------------------------------- /packages/el-bot/plugins/blacklist/utils.ts: -------------------------------------------------------------------------------- 1 | import { Friend } from '../../db/schemas/friend.schema' 2 | // import mongoose from "mongoose"; 3 | // const Friend = mongoose.models.Friend; 4 | import { Group } from '../../db/schemas/group.schema' 5 | 6 | type BlockType = 'qq' | 'user' | 'friend' | 'group' 7 | 8 | const blacklist = { 9 | friends: new Set(), 10 | groups: new Set(), 11 | } 12 | 13 | /** 14 | * 初始化黑名单 15 | * 同步维护 减少查询 16 | */ 17 | export async function initBlacklist() { 18 | const blockedFriends = await Friend.find({ 19 | block: true, 20 | }) 21 | const blockedGroups = await Group.find({ 22 | block: true, 23 | }) 24 | 25 | blockedFriends.forEach((friend) => { 26 | blacklist.friends.add(friend.qq) 27 | }) 28 | blockedGroups.forEach((group) => { 29 | blacklist.groups.add(group.groupId) 30 | }) 31 | 32 | return blacklist 33 | } 34 | 35 | export function displayList(blacklist: Set) { 36 | let content = '' 37 | blacklist.forEach((qq) => { 38 | content += `\n- ${qq}` 39 | }) 40 | return content 41 | } 42 | 43 | const friendAlias = ['user', 'qq', 'friend'] 44 | 45 | /** 46 | * 封禁 47 | * @param type 类型 48 | * @param id 49 | */ 50 | export async function block(type: BlockType, id: number) { 51 | if (!Number.isInteger(id)) 52 | return false 53 | if (friendAlias.includes(type)) { 54 | await blockFriend(id) 55 | return true 56 | } 57 | else if (type === 'group') { 58 | await blockGroup(id) 59 | return true 60 | } 61 | } 62 | 63 | /** 64 | * 解封 65 | * @param type 类型 66 | * @param id 67 | */ 68 | export async function unBlock(type: BlockType, id: number) { 69 | if (!Number.isInteger(id)) 70 | return false 71 | if (friendAlias.includes(type)) { 72 | await unBlockFriend(id) 73 | return true 74 | } 75 | else if (type === 'group') { 76 | await unBlockGroup(id) 77 | return true 78 | } 79 | } 80 | 81 | export async function blockGroup(groupId: number) { 82 | await Group.updateOne( 83 | { 84 | groupId, 85 | }, 86 | { 87 | $set: { 88 | block: true, 89 | }, 90 | }, 91 | { 92 | upsert: true, 93 | }, 94 | ) 95 | blacklist.groups.add(groupId) 96 | } 97 | 98 | export async function unBlockGroup(groupId: number) { 99 | await Group.updateOne( 100 | { 101 | groupId, 102 | }, 103 | { 104 | $set: { 105 | block: false, 106 | }, 107 | }, 108 | { 109 | upsert: true, 110 | }, 111 | ) 112 | blacklist.groups.delete(groupId) 113 | } 114 | 115 | export async function blockFriend(qq: number) { 116 | await Friend.updateOne( 117 | { 118 | qq, 119 | }, 120 | { 121 | $set: { 122 | block: true, 123 | }, 124 | }, 125 | { 126 | upsert: true, 127 | }, 128 | ) 129 | blacklist.friends.add(qq) 130 | } 131 | 132 | export async function unBlockFriend(qq: number) { 133 | await Friend.updateOne( 134 | { 135 | qq, 136 | }, 137 | { 138 | $set: { 139 | block: false, 140 | }, 141 | }, 142 | { 143 | upsert: true, 144 | }, 145 | ) 146 | blacklist.friends.delete(qq) 147 | } 148 | -------------------------------------------------------------------------------- /packages/el-bot/core/bot/status.ts: -------------------------------------------------------------------------------- 1 | import type { Bot } from '.' 2 | 3 | export class Status { 4 | constructor(public ctx: Bot) {} 5 | 6 | // /** 7 | // * 是否监听发送者 8 | // * @param {object} sender 9 | // */ 10 | // isListening(sender: Contact.User, listen: Config.Listen) { 11 | // if (typeof listen === 'string') { 12 | // let listenFlag = false 13 | // switch (listen as Config.BaseListenType) { 14 | // // 监听所有 15 | // case 'all': 16 | // listenFlag = true 17 | // break 18 | 19 | // // 监听 master 20 | // case 'master': 21 | // listenFlag = this.ctx.user.isMaster(sender.id) 22 | // break 23 | 24 | // // 监听管理员 25 | // case 'admin': 26 | // listenFlag = Boolean(this.ctx.user.isAdmin(sender.id)) 27 | // break 28 | 29 | // // 只监听好友 30 | // case 'friend': 31 | // // 群不存在 32 | // listenFlag = !(sender as Contact.Member).group 33 | // break 34 | 35 | // // 监听群 36 | // case 'group': 37 | // // 群存在 38 | // listenFlag = Boolean((sender as Contact.Member).group) 39 | // break 40 | 41 | // default: 42 | // break 43 | // } 44 | 45 | // return listenFlag 46 | // } 47 | // else { 48 | // // 语法糖 49 | // if (Array.isArray(listen)) { 50 | // // 无论 QQ 号还是 QQ 群号 51 | // if ( 52 | // listen.includes(sender.id) 53 | // || ((sender as Contact.Member).group 54 | // && listen.includes((sender as Contact.Member).group.id)) 55 | // ) { 56 | // return true 57 | // } 58 | 59 | // if (listen.includes('master') && this.ctx.user.isMaster(sender.id)) 60 | // return true 61 | 62 | // if (listen.includes('admin') && this.ctx.user.isAdmin(sender.id)) 63 | // return true 64 | 65 | // // 只监听好友 66 | // if (listen.includes('friend') && !(sender as Contact.Member).group) 67 | // return true 68 | 69 | // if (listen.includes('group') && (sender as Contact.Member).group) 70 | // return true 71 | // } 72 | // else { 73 | // // 指定 QQ 74 | // if (listen.friend && listen.friend.includes(sender.id)) 75 | // return true 76 | 77 | // if ((sender as Contact.Member).group) { 78 | // // 群 79 | // if ( 80 | // listen.group 81 | // && listen.group.includes((sender as Contact.Member).group.id) 82 | // ) { 83 | // return true 84 | // } 85 | // } 86 | // } 87 | // } 88 | 89 | // return false 90 | // } 91 | 92 | // /** 93 | // * 从配置直接获取监听状态(包括判断 listen 与 unlisten) 94 | // * @param sender 发送者 95 | // * @param config 配置 96 | // */ 97 | // getListenStatusByConfig(sender: Contact.User, config: any): boolean { 98 | // let listenFlag = true 99 | // if (config.listen) 100 | // listenFlag = this.isListening(sender, config.listen || 'all') 101 | // else if (config.unlisten) 102 | // listenFlag = !this.isListening(sender, config.unlisten || 'all') 103 | 104 | // return listenFlag 105 | // } 106 | } 107 | -------------------------------------------------------------------------------- /demo/bot/plugins/qq/index.ts: -------------------------------------------------------------------------------- 1 | import { consola, defineBotPlugin } from 'el-bot' 2 | import colors from 'picocolors' 3 | import { THREAD_FORMAT } from 'qq-sdk' 4 | 5 | export default defineBotPlugin({ 6 | setup: async (ctx) => { 7 | const { qq } = ctx 8 | if (!qq) 9 | return 10 | 11 | const { client, ws } = qq 12 | // 不阻塞插件加载 13 | setTimeout(async () => { 14 | const { data } = await client.meApi.me() 15 | consola.info('QQ 频道机器人', colors.green(data.username), colors.dim(data.union_openid)) 16 | }, 1) 17 | 18 | // const channelsRes = await client.channelApi.channels(ylfTestGuildID) 19 | // const channels = channelsRes.data as IChannel[] 20 | 21 | ws.on('GUILD_MESSAGES', (data) => { 22 | consola.info('GUILD_MESSAGES', data) 23 | 24 | if (data.msg.content === '1') { 25 | const channelId = data.msg.channel_id 26 | // 主动消息不能在00:00:00 - 05:59:59 推送 27 | client.messageApi 28 | .postMessage(channelId, { 29 | content: '2', 30 | msg_id: data.msg.id, 31 | }) 32 | } 33 | 34 | if (data.msg.content === '发帖') { 35 | const channelId = data.msg.channel_id 36 | // client.channelApi.channel(channelId) 37 | client.request({ 38 | method: 'GET', 39 | url: '/channels/:channelID/threads', 40 | rest: { 41 | channelID: channelId, 42 | }, 43 | data: { 44 | title: '你所热爱的', 45 | content: '就是你的生活', 46 | format: THREAD_FORMAT.FORMAT_MARKDOWN, 47 | }, 48 | }) 49 | } 50 | 51 | // 链接、文本列表模板 52 | // client.messageApi.postMessage(channelId, { 53 | // ark: arkTemplateMessage.ark, 54 | // }) 55 | 56 | // keyboard 57 | // 无 markdown 权限 58 | // client.messageApi.postMessage(channelId, { 59 | // markdown: { 60 | // template_id: 1, 61 | // params: [ 62 | // { 63 | // key: 'title', 64 | // value: ['标题'], 65 | // }, 66 | // ], 67 | // }, 68 | // msg_id: 'xxxxxx', 69 | // keyboard: { 70 | // content: { 71 | // rows: [ 72 | // { 73 | // buttons: [ 74 | // { 75 | // id: '1', 76 | // render_data: { 77 | // label: 'AtBot-按钮1', 78 | // visited_label: '点击后按钮1上文字', 79 | // }, 80 | // action: { 81 | // type: 2, 82 | // permission: { 83 | // type: 2, 84 | // specify_role_ids: ['1', '2', '3'], 85 | // }, 86 | // click_limit: 10, 87 | // unsupport_tips: '编辑-兼容文本', 88 | // data: '/搜索', 89 | // at_bot_show_channel_list: true, 90 | // }, 91 | // }, 92 | // ], 93 | // }, 94 | // ], 95 | // bot_appid: 123123123, 96 | // }, 97 | // }, 98 | // }) 99 | }) 100 | }, 101 | }) 102 | --------------------------------------------------------------------------------