├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── mergify.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .node-dev.json ├── .npmrc ├── .releaserc.cjs ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.cjs ├── examples ├── 01-example.ts └── 02-example.ts ├── jest.config.ts ├── package.json ├── pnpm-lock.yaml ├── src ├── config │ └── env.ts ├── index.ts ├── interfaces │ ├── response.ts │ ├── schema.ts │ └── send.ts ├── one.ts ├── push │ ├── custom-email.ts │ ├── dingtalk.ts │ ├── dingtalk │ │ ├── action-card.ts │ │ ├── feed-card.ts │ │ ├── link.ts │ │ ├── markdown.ts │ │ └── text.ts │ ├── discord.ts │ ├── feishu.ts │ ├── i-got.ts │ ├── ntfy.ts │ ├── one-bot.ts │ ├── push-deer.ts │ ├── push-plus.ts │ ├── qmsg.ts │ ├── server-chan-turbo.ts │ ├── server-chan-v3.ts │ ├── telegram.ts │ ├── wechat-app.ts │ ├── wechat-robot.ts │ ├── wx-pusher.ts │ └── xi-zhi.ts └── utils │ ├── ajax.ts │ ├── crypto.test.ts │ ├── crypto.ts │ ├── helper.test.ts │ ├── helper.ts │ ├── validate.test.ts │ └── validate.ts ├── tsconfig.json └── tsup.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | #启用编辑器配置 3 | root = true 4 | # 对所有文件生效 5 | [*] 6 | #编码方式 7 | charset = utf-8 8 | #缩进格式 9 | indent_style = space 10 | indent_size = 4 11 | #换行符 12 | end_of_line = lf 13 | #插入最终换行符 14 | insert_final_newline = true 15 | #修剪尾随空格 16 | trim_trailing_whitespace = true 17 | # 对后缀名为 md 的文件生效 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | [*.sh] 21 | #换行符 22 | end_of_line = lf 23 | [package.json] 24 | indent_size = 2 25 | [*.yml] 26 | indent_size = 2 27 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | #/*.js 3 | /test/unit/coverage/ 4 | /test/unit/specs/ 5 | /build/ 6 | # /test/ 7 | /node_modules/ 8 | *.min.* 9 | src/public/ 10 | /public/ 11 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // const __ERROR__ = process.env.NODE_ENV === 'production' ? 2 : 0 2 | const __WARN__ = process.env.NODE_ENV === 'production' ? 1 : 0 3 | module.exports = { 4 | root: true, 5 | globals: { 6 | globalThis: true, 7 | }, 8 | env: { 9 | }, 10 | settings: { 11 | }, 12 | extends: [ 13 | 'plugin:import/errors', 14 | 'plugin:import/warnings', 15 | 'plugin:import/typescript', 16 | 'cmyr', 17 | ], 18 | plugins: [ 19 | 'import', 20 | ], 21 | rules: { 22 | 'no-console': __WARN__, 23 | 'no-shadow': 0, 24 | '@typescript-eslint/no-shadow': 2, 25 | '@typescript-eslint/explicit-module-boundary-types': [1, { 26 | allowArgumentsExplicitlyTypedAsAny: true, 27 | }], // 要求导出函数和类的公共类方法的显式返回和参数类型 28 | '@typescript-eslint/comma-dangle': [2, 'always-multiline'], // 要求或禁止使用拖尾逗号 29 | '@typescript-eslint/prefer-as-const': 1, 30 | 'import/no-unresolved': 0, 31 | 'import/order': 1, 32 | 'require-await': 0, 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 描述 / Description 2 | 3 | 简要说明此 Pull Request 的目的和做了哪些更改 4 | 5 | Briefly describe the purpose of this Pull Request and what changes were made. 6 | 7 | ## 该 PR 相关 Issue / Involved Issue 8 | 9 | 如果此 PR 与某个 Issue 相关,请列出 Issue 编号。 10 | 11 | If this PR is related to an issue, please list the issue number. 12 | 13 | Close # 14 | 15 | 16 | ## 检查列表 / Checklist 17 | 18 | 19 | - [ ] 我已经测试了我的代码 / I have tested my code 20 | - [ ] 我已经更新了文档(如果适用) / I have updated the documentation (if applicable) 21 | - [ ] 我已经添加了测试用例(如果适用) / I have added test cases (if applicable) 22 | - [ ] 我已经检查了代码风格(lint) / I have checked the code style (lint) 23 | 24 | ## 其他信息 / Additional Information 25 | 26 | 如有任何其他需要说明的地方,请在此补充 27 | 28 | If there is any other information that needs to be explained, please supplement it here. 29 | 30 | --- 31 | > 如果你的 pull request 长时间未被处理,请在评论区 @CaoMeiYouRen 。 32 | > If your pull request has not been processed for a long time, please leave a comment at @CaoMeiYouRen. 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | open-pull-requests-limit: 20 11 | schedule: 12 | interval: "monthly" 13 | time: "05:00" 14 | timezone: "Asia/Shanghai" 15 | ignore: 16 | - dependency-name: "conventional-changelog-cli" 17 | - dependency-name: "eslint" 18 | versions: 19 | - ">= 9.0.0" 20 | - dependency-name: semantic-release 21 | versions: 22 | - ">= 21.0.1" 23 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge for Dependabot pull requests 3 | conditions: 4 | - check-success=Test 5 | - author~=^dependabot(|-preview)\[bot\]$ 6 | - label=dependencies 7 | # - base=master 8 | actions: 9 | merge: 10 | method: rebase 11 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - beta 7 | workflow_dispatch: 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | jobs: 12 | release: 13 | name: Release 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 10 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v4 22 | with: 23 | version: "latest" 24 | - name: Setup Node.js environment 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: "lts/*" 28 | cache: "pnpm" 29 | - name: Cache Dependency 30 | uses: actions/cache@v4 31 | with: 32 | path: | 33 | ~/.npm 34 | ~/.yarn 35 | ~/.cache/pnpm 36 | ~/cache 37 | !~/cache/exclude 38 | **/node_modules 39 | key: pnpm-${{ runner.os }}-${{ hashFiles('package.json') }} 40 | restore-keys: pnpm-${{ runner.os }} 41 | - run: pnpm i --frozen-lockfile 42 | - run: pnpm run lint 43 | - run: pnpm run build 44 | - run: pnpm run test 45 | - env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | run: pnpm run release 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | jobs: 7 | test: 8 | name: Test 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 10 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Setup pnpm 14 | uses: pnpm/action-setup@v4 15 | with: 16 | version: "latest" 17 | - name: Setup Node.js@lts environment 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: "lts/*" 21 | cache: "pnpm" 22 | - name: Cache Dependency 23 | uses: actions/cache@v4 24 | with: 25 | path: | 26 | ~/.npm 27 | ~/.yarn 28 | ~/.cache/pnpm 29 | ~/cache 30 | !~/cache/exclude 31 | **/node_modules 32 | key: pnpm-${{ runner.os }}-${{ hashFiles('package.json') }} 33 | restore-keys: pnpm-${{ runner.os }} 34 | - run: pnpm i --frozen-lockfile 35 | - run: pnpm run lint 36 | - run: pnpm run build 37 | - run: pnpm run test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | *.log 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | 23 | # 忽略图片 24 | *.jpg 25 | *.png 26 | *.rar 27 | 28 | .git 29 | # ssl 30 | # *.crt 31 | # *.key 32 | # *.pem 33 | sessions 34 | /test 35 | *.assets 36 | #public 37 | dist 38 | temp 39 | coverage 40 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npx --no-install commitlint --edit "$1" 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npx --no-install lint-staged 4 | -------------------------------------------------------------------------------- /.node-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "notify": false 3 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | package-lock=true 3 | -------------------------------------------------------------------------------- /.releaserc.cjs: -------------------------------------------------------------------------------- 1 | const { name } = require('./package.json') 2 | module.exports = { 3 | plugins: [ 4 | [ 5 | "@semantic-release/commit-analyzer", 6 | { 7 | "config": "conventional-changelog-cmyr-config" 8 | } 9 | ], 10 | ["@semantic-release/release-notes-generator", 11 | { 12 | "config": "conventional-changelog-cmyr-config" 13 | }], 14 | [ 15 | "@semantic-release/changelog", 16 | { 17 | "changelogFile": "CHANGELOG.md", 18 | "changelogTitle": "# " + name 19 | } 20 | ], 21 | '@semantic-release/npm', 22 | '@semantic-release/github', 23 | [ 24 | "@semantic-release/git", 25 | { 26 | "assets": [ 27 | "src", 28 | "CHANGELOG.md", 29 | "package.json" 30 | ] 31 | } 32 | ] 33 | ] 34 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # push-all-in-one 2 | 3 | ## [4.4.3](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.4.2...v4.4.3) (2025-05-27) 4 | 5 | 6 | ### 🐛 Bug 修复 7 | 8 | * **utils:** 改进颜色模块加载方式 ([0c11819](https://github.com/CaoMeiYouRen/push-all-in-one/commit/0c11819)) 9 | 10 | ## [4.4.2](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.4.1...v4.4.2) (2025-05-09) 11 | 12 | 13 | ### 🐛 Bug 修复 14 | 15 | * 添加息知推送已停止服务的弃用说明 ([af49fa7](https://github.com/CaoMeiYouRen/push-all-in-one/commit/af49fa7)) 16 | 17 | ## [4.4.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.4.0...v4.4.1) (2025-03-04) 18 | 19 | 20 | ### 🐛 Bug 修复 21 | 22 | * **wx-pusher:** 优化发送方法,支持去重用户 ID ([8337603](https://github.com/CaoMeiYouRen/push-all-in-one/commit/8337603)) 23 | 24 | # [4.4.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.3.0...v4.4.0) (2025-03-04) 25 | 26 | 27 | ### ✨ 新功能 28 | 29 | * **push:** 添加 WxPusher 推送支持 ([67de1fd](https://github.com/CaoMeiYouRen/push-all-in-one/commit/67de1fd)) 30 | * **push:** 添加 WxPusher 推送支持 ([a1ffedf](https://github.com/CaoMeiYouRen/push-all-in-one/commit/a1ffedf)) 31 | 32 | # [4.3.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.2.0...v4.3.0) (2025-02-11) 33 | 34 | 35 | ### ✨ 新功能 36 | 37 | * 添加 ntfy 推送功能及相关配置 ([540c1db](https://github.com/CaoMeiYouRen/push-all-in-one/commit/540c1db)), closes [#264](https://github.com/CaoMeiYouRen/push-all-in-one/issues/264) 38 | 39 | # [4.2.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.1.1...v4.2.0) (2025-02-10) 40 | 41 | 42 | ### ✨ 新功能 43 | 44 | * 添加飞书消息发送功能及配置验证 ([f93fc7f](https://github.com/CaoMeiYouRen/push-all-in-one/commit/f93fc7f)), closes [#285](https://github.com/CaoMeiYouRen/push-all-in-one/issues/285) 45 | 46 | ## [4.1.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.1.0...v4.1.1) (2024-11-19) 47 | 48 | 49 | ### 🐛 Bug 修复 50 | 51 | * 更新文档说明;修复 PushPlus 存在错误默认值的问题;修复 企业应用的 id 缺少默认值的问题 ([5482fee](https://github.com/CaoMeiYouRen/push-all-in-one/commit/5482fee)) 52 | 53 | # [4.1.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.0.0...v4.1.0) (2024-11-19) 54 | 55 | 56 | ### ♻ 代码重构 57 | 58 | * 优化 OneBot 和 Qmsg 的 option 校验 ([bce14a3](https://github.com/CaoMeiYouRen/push-all-in-one/commit/bce14a3)) 59 | * 优化 OneBot 和 Qmsg 的 option 校验 ([d415eac](https://github.com/CaoMeiYouRen/push-all-in-one/commit/d415eac)) 60 | * 优化 部分代码的导入风格 ([51baf2b](https://github.com/CaoMeiYouRen/push-all-in-one/commit/51baf2b)) 61 | * 优化 部分代码的导入风格 ([dc25e6b](https://github.com/CaoMeiYouRen/push-all-in-one/commit/dc25e6b)) 62 | 63 | 64 | ### ✨ 新功能 65 | 66 | * 增加 ConfigSchema 和 OptionSchema 声明;重构 Config 校验 ([b7436ed](https://github.com/CaoMeiYouRen/push-all-in-one/commit/b7436ed)) 67 | * 增加 ConfigSchema 和 OptionSchema 声明;重构 Config 校验 ([1ae4203](https://github.com/CaoMeiYouRen/push-all-in-one/commit/1ae4203)) 68 | * 增加 命名空间 声明;添加 readonly 声明 ([7aaca63](https://github.com/CaoMeiYouRen/push-all-in-one/commit/7aaca63)) 69 | * 增加 命名空间 声明;添加 readonly 声明 ([cc0b08f](https://github.com/CaoMeiYouRen/push-all-in-one/commit/cc0b08f)) 70 | * 钉钉/自定义邮件新增 配置定义 和 配置校验 ([4f7d8c3](https://github.com/CaoMeiYouRen/push-all-in-one/commit/4f7d8c3)) 71 | * 钉钉/自定义邮件新增 配置定义 和 配置校验 ([038fdcb](https://github.com/CaoMeiYouRen/push-all-in-one/commit/038fdcb)) 72 | 73 | 74 | ### 🐛 Bug 修复 75 | 76 | * 修改 DingtalkOption 的默认值 ([ce62275](https://github.com/CaoMeiYouRen/push-all-in-one/commit/ce62275)) 77 | * 修改 DingtalkOption 的默认值 ([b7329ec](https://github.com/CaoMeiYouRen/push-all-in-one/commit/b7329ec)) 78 | * 修改 部分可选字段的类型声明 ([5d46d07](https://github.com/CaoMeiYouRen/push-all-in-one/commit/5d46d07)) 79 | * 修改 部分可选字段的类型声明 ([e8a6832](https://github.com/CaoMeiYouRen/push-all-in-one/commit/e8a6832)) 80 | * 增加 PushAllInOne 导出 ([3bb1a64](https://github.com/CaoMeiYouRen/push-all-in-one/commit/3bb1a64)) 81 | * 增加 PushAllInOne 导出 ([b273034](https://github.com/CaoMeiYouRen/push-all-in-one/commit/b273034)) 82 | 83 | # [4.1.0-beta.4](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.1.0-beta.3...v4.1.0-beta.4) (2024-11-18) 84 | 85 | 86 | ### ✨ 新功能 87 | 88 | * 增加 命名空间 声明;添加 readonly 声明 ([cc0b08f](https://github.com/CaoMeiYouRen/push-all-in-one/commit/cc0b08f)) 89 | 90 | # [4.1.0-beta.3](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.1.0-beta.2...v4.1.0-beta.3) (2024-11-18) 91 | 92 | 93 | ### 🐛 Bug 修复 94 | 95 | * 增加 PushAllInOne 导出 ([b273034](https://github.com/CaoMeiYouRen/push-all-in-one/commit/b273034)) 96 | 97 | # [4.1.0-beta.2](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.1.0-beta.1...v4.1.0-beta.2) (2024-11-17) 98 | 99 | 100 | ### 🐛 Bug 修复 101 | 102 | * 修改 DingtalkOption 的默认值 ([b7329ec](https://github.com/CaoMeiYouRen/push-all-in-one/commit/b7329ec)) 103 | * 修改 部分可选字段的类型声明 ([e8a6832](https://github.com/CaoMeiYouRen/push-all-in-one/commit/e8a6832)) 104 | 105 | # [4.1.0-beta.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.0.0...v4.1.0-beta.1) (2024-11-17) 106 | 107 | 108 | ### ♻ 代码重构 109 | 110 | * 优化 OneBot 和 Qmsg 的 option 校验 ([d415eac](https://github.com/CaoMeiYouRen/push-all-in-one/commit/d415eac)) 111 | * 优化 部分代码的导入风格 ([dc25e6b](https://github.com/CaoMeiYouRen/push-all-in-one/commit/dc25e6b)) 112 | 113 | 114 | ### ✨ 新功能 115 | 116 | * 增加 ConfigSchema 和 OptionSchema 声明;重构 Config 校验 ([1ae4203](https://github.com/CaoMeiYouRen/push-all-in-one/commit/1ae4203)) 117 | * 钉钉/自定义邮件新增 配置定义 和 配置校验 ([038fdcb](https://github.com/CaoMeiYouRen/push-all-in-one/commit/038fdcb)) 118 | 119 | # [4.0.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.6.0...v4.0.0) (2024-11-16) 120 | 121 | 122 | ### ♻ 代码重构 123 | 124 | * 优化 Dingtalk/ServerChanV3 的错误提示 ([865957e](https://github.com/CaoMeiYouRen/push-all-in-one/commit/865957e)) 125 | * 修改文档;修改代码示例;优化 部分代码的类型声明 ([1f481bf](https://github.com/CaoMeiYouRen/push-all-in-one/commit/1f481bf)) 126 | * 移除 crypto-js,迁移到原生的 crypto ([0ba1b0d](https://github.com/CaoMeiYouRen/push-all-in-one/commit/0ba1b0d)) 127 | * 调整 send 接口 返回值类型 为 SendResponse ([90db419](https://github.com/CaoMeiYouRen/push-all-in-one/commit/90db419)) 128 | * 调整 自定义邮件/Discord/IGot 的接口类型声明 ([2c30bc6](https://github.com/CaoMeiYouRen/push-all-in-one/commit/2c30bc6)) 129 | * 重构 Discord 为新版接口 ([d087a64](https://github.com/CaoMeiYouRen/push-all-in-one/commit/d087a64)) 130 | * 重构 iGot 推送 为新版接口 ([7f73e1e](https://github.com/CaoMeiYouRen/push-all-in-one/commit/7f73e1e)) 131 | * 重构 OneBot 推送为 新版接口 ([b636613](https://github.com/CaoMeiYouRen/push-all-in-one/commit/b636613)) 132 | * 重构 PushDeer 推送 为新版接口 ([2c85ecf](https://github.com/CaoMeiYouRen/push-all-in-one/commit/2c85ecf)) 133 | * 重构 PushPlus 为新版接口 ([72b3457](https://github.com/CaoMeiYouRen/push-all-in-one/commit/72b3457)) 134 | * 重构 Qmsg 酱 为新版接口 ([284a56d](https://github.com/CaoMeiYouRen/push-all-in-one/commit/284a56d)) 135 | * 重构 ServerChanTurbo/ServerChanV3 到新版接口 ([3ae9c5b](https://github.com/CaoMeiYouRen/push-all-in-one/commit/3ae9c5b)) 136 | * 重构 Telegram 到新版接口 ([138cba8](https://github.com/CaoMeiYouRen/push-all-in-one/commit/138cba8)) 137 | * 重构 WechatApp/WechatRobot 到新版接口 ([8d4d7a5](https://github.com/CaoMeiYouRen/push-all-in-one/commit/8d4d7a5)) 138 | * 重构 息知推送 到新版接口 ([24ffb17](https://github.com/CaoMeiYouRen/push-all-in-one/commit/24ffb17)) 139 | * 重构 自定义邮件类 为新版接口;优化资源的释放 ([bd912f1](https://github.com/CaoMeiYouRen/push-all-in-one/commit/bd912f1)) 140 | * 重构 钉钉机器人 推送,迁移到 新版接口;优化 日志输出 ([82bfab4](https://github.com/CaoMeiYouRen/push-all-in-one/commit/82bfab4)) 141 | * 重构 钉钉机器人推送 的类型声明 ([7463ca4](https://github.com/CaoMeiYouRen/push-all-in-one/commit/7463ca4)) 142 | 143 | 144 | ### ✨ 新功能 145 | 146 | * 最低 Node.js 版本要求提升到 18,以支持原生 esm ([6d0a6d1](https://github.com/CaoMeiYouRen/push-all-in-one/commit/6d0a6d1)) 147 | * 新增 runPushAllInOne 函数 ([664ca21](https://github.com/CaoMeiYouRen/push-all-in-one/commit/664ca21)) 148 | 149 | 150 | ### 🐛 Bug 修复 151 | 152 | * qmsg 酱 增加 bot 参数 ([95b5433](https://github.com/CaoMeiYouRen/push-all-in-one/commit/95b5433)) 153 | * 修复 代理依赖升级导致的错误;优化 Server 酱³ 调用方式为 安全模式 ([40b9888](https://github.com/CaoMeiYouRen/push-all-in-one/commit/40b9888)) 154 | * 修复 接口类型未导出的问题;修复 部分类型声明的大小写问题 ([9535ebc](https://github.com/CaoMeiYouRen/push-all-in-one/commit/9535ebc)) 155 | * 升级 https-proxy-agent、socks-proxy-agent 版本 ([b9d24aa](https://github.com/CaoMeiYouRen/push-all-in-one/commit/b9d24aa)) 156 | * 移除 qs,迁移到原生 URLSearchParams;修复 Qmsg 文档链接 ([447fe60](https://github.com/CaoMeiYouRen/push-all-in-one/commit/447fe60)) 157 | * 迁移测试到 jest;修复 生成钉钉签名 错误的问题 ([4c5adc4](https://github.com/CaoMeiYouRen/push-all-in-one/commit/4c5adc4)) 158 | 159 | 160 | ### 💥 BREAKING CHANGES 161 | 162 | * 最低 Node.js 版本要求提升到 18,以支持原生 esm 163 | 164 | # [4.0.0-beta.2](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v4.0.0-beta.1...v4.0.0-beta.2) (2024-11-09) 165 | 166 | 167 | ### 🐛 Bug 修复 168 | 169 | * 修复 接口类型未导出的问题;修复 部分类型声明的大小写问题 ([9535ebc](https://github.com/CaoMeiYouRen/push-all-in-one/commit/9535ebc)) 170 | 171 | # [4.0.0-beta.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.6.0...v4.0.0-beta.1) (2024-11-09) 172 | 173 | 174 | ### ♻ 代码重构 175 | 176 | * 修改文档;修改代码示例;优化 部分代码的类型声明 ([1f481bf](https://github.com/CaoMeiYouRen/push-all-in-one/commit/1f481bf)) 177 | * 移除 crypto-js,迁移到原生的 crypto ([0ba1b0d](https://github.com/CaoMeiYouRen/push-all-in-one/commit/0ba1b0d)) 178 | * 调整 send 接口 返回值类型 为 SendResponse ([90db419](https://github.com/CaoMeiYouRen/push-all-in-one/commit/90db419)) 179 | * 调整 自定义邮件/Discord/IGot 的接口类型声明 ([2c30bc6](https://github.com/CaoMeiYouRen/push-all-in-one/commit/2c30bc6)) 180 | * 重构 Discord 为新版接口 ([d087a64](https://github.com/CaoMeiYouRen/push-all-in-one/commit/d087a64)) 181 | * 重构 iGot 推送 为新版接口 ([7f73e1e](https://github.com/CaoMeiYouRen/push-all-in-one/commit/7f73e1e)) 182 | * 重构 OneBot 推送为 新版接口 ([b636613](https://github.com/CaoMeiYouRen/push-all-in-one/commit/b636613)) 183 | * 重构 PushDeer 推送 为新版接口 ([2c85ecf](https://github.com/CaoMeiYouRen/push-all-in-one/commit/2c85ecf)) 184 | * 重构 PushPlus 为新版接口 ([72b3457](https://github.com/CaoMeiYouRen/push-all-in-one/commit/72b3457)) 185 | * 重构 Qmsg 酱 为新版接口 ([284a56d](https://github.com/CaoMeiYouRen/push-all-in-one/commit/284a56d)) 186 | * 重构 ServerChanTurbo/ServerChanV3 到新版接口 ([3ae9c5b](https://github.com/CaoMeiYouRen/push-all-in-one/commit/3ae9c5b)) 187 | * 重构 Telegram 到新版接口 ([138cba8](https://github.com/CaoMeiYouRen/push-all-in-one/commit/138cba8)) 188 | * 重构 WechatApp/WechatRobot 到新版接口 ([8d4d7a5](https://github.com/CaoMeiYouRen/push-all-in-one/commit/8d4d7a5)) 189 | * 重构 息知推送 到新版接口 ([24ffb17](https://github.com/CaoMeiYouRen/push-all-in-one/commit/24ffb17)) 190 | * 重构 自定义邮件类 为新版接口;优化资源的释放 ([bd912f1](https://github.com/CaoMeiYouRen/push-all-in-one/commit/bd912f1)) 191 | * 重构 钉钉机器人 推送,迁移到 新版接口;优化 日志输出 ([82bfab4](https://github.com/CaoMeiYouRen/push-all-in-one/commit/82bfab4)) 192 | * 重构 钉钉机器人推送 的类型声明 ([7463ca4](https://github.com/CaoMeiYouRen/push-all-in-one/commit/7463ca4)) 193 | 194 | 195 | ### ✨ 新功能 196 | 197 | * 最低 Node.js 版本要求提升到 18,以支持原生 esm ([6d0a6d1](https://github.com/CaoMeiYouRen/push-all-in-one/commit/6d0a6d1)) 198 | * 新增 runPushAllInOne 函数 ([664ca21](https://github.com/CaoMeiYouRen/push-all-in-one/commit/664ca21)) 199 | 200 | 201 | ### 🐛 Bug 修复 202 | 203 | * qmsg 酱 增加 bot 参数 ([95b5433](https://github.com/CaoMeiYouRen/push-all-in-one/commit/95b5433)) 204 | * 修复 代理依赖升级导致的错误;优化 Server 酱³ 调用方式为 安全模式 ([40b9888](https://github.com/CaoMeiYouRen/push-all-in-one/commit/40b9888)) 205 | * 升级 https-proxy-agent、socks-proxy-agent 版本 ([b9d24aa](https://github.com/CaoMeiYouRen/push-all-in-one/commit/b9d24aa)) 206 | * 移除 qs,迁移到原生 URLSearchParams;修复 Qmsg 文档链接 ([447fe60](https://github.com/CaoMeiYouRen/push-all-in-one/commit/447fe60)) 207 | * 迁移测试到 jest;修复 生成钉钉签名 错误的问题 ([4c5adc4](https://github.com/CaoMeiYouRen/push-all-in-one/commit/4c5adc4)) 208 | 209 | 210 | ### 💥 BREAKING CHANGES 211 | 212 | * 最低 Node.js 版本要求提升到 18,以支持原生 esm 213 | 214 | # [3.6.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.5.4...v3.6.0) (2024-10-04) 215 | 216 | 217 | ### ♻ 代码重构 218 | 219 | * 优化 ServerChanTurbo 的附加参数声明 ([c45d984](https://github.com/CaoMeiYouRen/push-all-in-one/commit/c45d984)) 220 | 221 | 222 | ### ✨ 新功能 223 | 224 | * 新增 Server 酱³ 支持 ([5ecc0d1](https://github.com/CaoMeiYouRen/push-all-in-one/commit/5ecc0d1)) 225 | 226 | ## [3.5.4](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.5.3...v3.5.4) (2024-07-26) 227 | 228 | 229 | ### 🐛 Bug 修复 230 | 231 | * 修复 onebot 推送渠道无法解析 CQ 码的问题 ([ee2c613](https://github.com/CaoMeiYouRen/push-all-in-one/commit/ee2c613)) 232 | 233 | ## [3.5.3](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.5.2...v3.5.3) (2024-06-13) 234 | 235 | 236 | ### 🐛 Bug 修复 237 | 238 | * 修复:在 esm 模式下, https-proxy-agent/socks-proxy-agent 的导入错误问题 ([eb68501](https://github.com/CaoMeiYouRen/push-all-in-one/commit/eb68501)), closes [#178](https://github.com/CaoMeiYouRen/push-all-in-one/issues/178) 239 | 240 | ## [3.5.2](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.5.1...v3.5.2) (2024-06-10) 241 | 242 | 243 | ### 🐛 Bug 修复 244 | 245 | * 修复 typescript 中使用找不到声明文件 ([314c051](https://github.com/CaoMeiYouRen/push-all-in-one/commit/314c051)), closes [CaoMeiYouRen/push-all-in-one#144](https://github.com/CaoMeiYouRen/push-all-in-one/issues/144) 246 | 247 | ## [3.5.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.5.0...v3.5.1) (2024-05-10) 248 | 249 | 250 | ### 🐛 Bug 修复 251 | 252 | * 回退 HTTPS_PROXY 环境变量 ([9ee2b53](https://github.com/CaoMeiYouRen/push-all-in-one/commit/9ee2b53)) 253 | 254 | # [3.5.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.4.5...v3.5.0) (2024-04-20) 255 | 256 | 257 | ### ✨ 新功能 258 | 259 | * 优化 Discord/Telegram 请求的代理配置方式 ([47aa1a4](https://github.com/CaoMeiYouRen/push-all-in-one/commit/47aa1a4)) 260 | 261 | ## [3.4.5](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.4.4...v3.4.5) (2024-01-28) 262 | 263 | 264 | ### 🐛 Bug 修复 265 | 266 | * 更新 readme ([8da3b7d](https://github.com/CaoMeiYouRen/push-all-in-one/commit/8da3b7d)) 267 | 268 | ## [3.4.4](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.4.3...v3.4.4) (2023-10-25) 269 | 270 | 271 | ### 🐛 Bug 修复 272 | 273 | * 优化文档中推荐的推送方式;增加具体的代码案例 ([0d40b2c](https://github.com/CaoMeiYouRen/push-all-in-one/commit/0d40b2c)), closes [#128](https://github.com/CaoMeiYouRen/push-all-in-one/issues/128) 274 | 275 | ## [3.4.3](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.4.2...v3.4.3) (2023-10-24) 276 | 277 | 278 | ### 🐛 Bug 修复 279 | 280 | * 增加 英文版文档;优化 NO_PROXY 逻辑 ([4dc2961](https://github.com/CaoMeiYouRen/push-all-in-one/commit/4dc2961)) 281 | 282 | ## [3.4.2](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.4.1...v3.4.2) (2023-10-22) 283 | 284 | 285 | ### 🐛 Bug 修复 286 | 287 | * 修复 https-proxy-agent 和 socks-proxy-agent 版本过高在 node12 下无法运行的问题 ([8468ce0](https://github.com/CaoMeiYouRen/push-all-in-one/commit/8468ce0)) 288 | 289 | ## [3.4.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.4.0...v3.4.1) (2023-10-22) 290 | 291 | 292 | ### 🐛 Bug 修复 293 | 294 | * 完善 Telegram 文档;优化部分逻辑 ([1361062](https://github.com/CaoMeiYouRen/push-all-in-one/commit/1361062)) 295 | 296 | # [3.4.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.3.0...v3.4.0) (2023-10-22) 297 | 298 | 299 | ### ✨ 新功能 300 | 301 | * 增加了 OneBot 推送支持 ([223184e](https://github.com/CaoMeiYouRen/push-all-in-one/commit/223184e)) 302 | 303 | # [3.3.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.2.0...v3.3.0) (2023-10-07) 304 | 305 | 306 | ### ✨ 新功能 307 | 308 | * 增加 请求代理支持 ([fc84fa6](https://github.com/CaoMeiYouRen/push-all-in-one/commit/fc84fa6)) 309 | 310 | # [3.2.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.1.1...v3.2.0) (2023-09-16) 311 | 312 | 313 | ### ✨ 新功能 314 | 315 | * **src/push:** 新增 Discord Webhook 推送 ([7ac075c](https://github.com/CaoMeiYouRen/push-all-in-one/commit/7ac075c)) 316 | * 新增 Telegram Bot 推送 ([18c292d](https://github.com/CaoMeiYouRen/push-all-in-one/commit/18c292d)) 317 | 318 | 319 | ### 🐛 Bug 修复 320 | 321 | * 修复 discord 的导出;修复 conventional-changelog-cli 的版本问题 ([edd9f25](https://github.com/CaoMeiYouRen/push-all-in-one/commit/edd9f25)) 322 | 323 | ## [3.1.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.1.0...v3.1.1) (2023-06-14) 324 | 325 | 326 | ### 🐛 Bug 修复 327 | 328 | * 修复 tsconfig.json 配置问题 ([6cfec72](https://github.com/CaoMeiYouRen/push-all-in-one/commit/6cfec72)) 329 | 330 | # [3.1.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.0.1...v3.1.0) (2023-03-12) 331 | 332 | 333 | ### ✨ 新功能 334 | 335 | * 新增 自定义邮件 支持(基于 nodemailer) ([3d6ccc8](https://github.com/CaoMeiYouRen/push-all-in-one/commit/3d6ccc8)) 336 | 337 | 338 | ### 🐛 Bug 修复 339 | 340 | * 修复 新版本的依赖和类型问题 ([8ccc2ce](https://github.com/CaoMeiYouRen/push-all-in-one/commit/8ccc2ce)) 341 | 342 | ## [3.0.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v3.0.0...v3.0.1) (2023-01-05) 343 | 344 | 345 | ### 🐛 Bug 修复 346 | 347 | * 替换colors为 @colors/colors ([e014753](https://github.com/CaoMeiYouRen/push-all-in-one/commit/e014753)) 348 | 349 | # [3.0.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v2.3.1...v3.0.0) (2023-01-05) 350 | 351 | 352 | ### ✨ 新功能 353 | 354 | * 移除 酷推、BER分邮件系统 的集成 ([6e59259](https://github.com/CaoMeiYouRen/push-all-in-one/commit/6e59259)) 355 | 356 | 357 | ### 💥 BREAKING CHANGES 358 | 359 | * 由于 酷推、BER分邮件系统 已无法登陆,故不再提供接口集成 360 | 361 | ## [2.3.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v2.3.0...v2.3.1) (2022-11-27) 362 | 363 | 364 | ### 🐛 Bug 修复 365 | 366 | * 修复 eslint 风格问题 ([e74e03e](https://github.com/CaoMeiYouRen/push-all-in-one/commit/e74e03e)) 367 | * 更新 文档说明;添加 Email、CoolPush 的弃用声明 ([bf899a0](https://github.com/CaoMeiYouRen/push-all-in-one/commit/bf899a0)) 368 | 369 | # [2.3.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v2.2.0...v2.3.0) (2022-08-01) 370 | 371 | 372 | ### ✨ 新功能 373 | 374 | * 企业微信应用推送 新增 markdown 推送支持 ([ca315b4](https://github.com/CaoMeiYouRen/push-all-in-one/commit/ca315b4)) 375 | 376 | # [2.2.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v2.1.1...v2.2.0) (2022-02-28) 377 | 378 | 379 | ### ✨ 新功能 380 | 381 | * 新增 PushDeer 推送支持 ([45444c7](https://github.com/CaoMeiYouRen/push-all-in-one/commit/45444c7)) 382 | 383 | ## [2.1.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v2.1.0...v2.1.1) (2022-02-18) 384 | 385 | 386 | ### 🐛 Bug 修复 387 | 388 | * 优化 colors 在非 Node 端的导入;优化文档说明 ([100ab96](https://github.com/CaoMeiYouRen/push-all-in-one/commit/100ab96)) 389 | 390 | # [2.1.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v2.0.4...v2.1.0) (2022-02-17) 391 | 392 | 393 | ### ✨ 新功能 394 | 395 | * 新增 Qmsg 酱推送 ([4dc8232](https://github.com/CaoMeiYouRen/push-all-in-one/commit/4dc8232)) 396 | * 新增 息知 推送 ([cfff80b](https://github.com/CaoMeiYouRen/push-all-in-one/commit/cfff80b)) 397 | 398 | ## [2.0.4](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v2.0.3...v2.0.4) (2022-02-14) 399 | 400 | 401 | ### 🐛 Bug 修复 402 | 403 | * 优化 ajax 对 form 格式的处理;优化 Debugger;更新依赖 ([5326c62](https://github.com/CaoMeiYouRen/push-all-in-one/commit/5326c62)) 404 | 405 | ## [2.0.3](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v2.0.2...v2.0.3) (2022-01-24) 406 | 407 | 408 | ### 🐛 Bug 修复 409 | 410 | * 修复 Ajax 错误 ([b35c895](https://github.com/CaoMeiYouRen/push-all-in-one/commit/b35c895)) 411 | 412 | ## [2.0.2](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v2.0.1...v2.0.2) (2022-01-24) 413 | 414 | 415 | ### 🐛 Bug 修复 416 | 417 | * 更新文档;新增 husky ([43d230b](https://github.com/CaoMeiYouRen/push-all-in-one/commit/43d230b)) 418 | 419 | ## [2.0.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v2.0.0...v2.0.1) (2021-12-24) 420 | 421 | 422 | * Merge branch 'master' of github.com:CaoMeiYouRen/push-all-in-one ([7e795e0](https://github.com/CaoMeiYouRen/push-all-in-one/commit/7e795e0)) 423 | * Update README.md ([4e03789](https://github.com/CaoMeiYouRen/push-all-in-one/commit/4e03789)) 424 | * Merge branch 'master' of github.com:CaoMeiYouRen/push-all-in-one ([f2273ae](https://github.com/CaoMeiYouRen/push-all-in-one/commit/f2273ae)) 425 | 426 | 427 | ### 🐛 Bug 修复 428 | 429 | * 更新依赖;格式化代码;更新 CI ([0dfc04a](https://github.com/CaoMeiYouRen/push-all-in-one/commit/0dfc04a)) 430 | 431 | # [2.0.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v1.3.5...v2.0.0) (2021-06-06) 432 | 433 | 434 | ### ✨ 新功能 435 | 436 | * 更新 pushplus 接口 ([4a1de7a](https://github.com/CaoMeiYouRen/push-all-in-one/commit/4a1de7a)) 437 | * 移除 旧版本 ServerChan ([a0225ee](https://github.com/CaoMeiYouRen/push-all-in-one/commit/a0225ee)) 438 | 439 | 440 | ### BREAKING CHANGES 441 | 442 | * 移除 旧版本 ServerChan 443 | 444 | ## [1.3.5](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v1.3.4...v1.3.5) (2021-03-09) 445 | 446 | 447 | ### 🐛 Bug 修复 448 | 449 | * **push-plus:** 修改 http://pushplus.hxtrip.com -> https://www.pushplus.plus ([e046788](https://github.com/CaoMeiYouRen/push-all-in-one/commit/e046788)) 450 | 451 | ## [1.3.4](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v1.3.3...v1.3.4) (2021-03-04) 452 | 453 | 454 | ### 🐛 Bug 修复 455 | 456 | * 修改 ts target 为 es2019 ([3cdfeb0](https://github.com/CaoMeiYouRen/push-all-in-one/commit/3cdfeb0)) 457 | 458 | ## [1.3.3](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v1.3.2...v1.3.3) (2021-03-04) 459 | 460 | 461 | ### 🐛 Bug 修复 462 | 463 | * **type:** 导出类型枚举 ([0ad08ce](https://github.com/CaoMeiYouRen/push-all-in-one/commit/0ad08ce)) 464 | 465 | ## [1.3.2](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v1.3.1...v1.3.2) (2021-03-04) 466 | 467 | 468 | ### 🐛 Bug 修复 469 | 470 | * **email:** 修改 addressee -> address ([509d714](https://github.com/CaoMeiYouRen/push-all-in-one/commit/509d714)) 471 | 472 | ## [1.3.1](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v1.3.0...v1.3.1) (2021-03-03) 473 | 474 | 475 | ### 🐛 Bug 修复 476 | 477 | * 修复 Dingtalk 推送错误;修复 ajax 请求 Content-Type: application/json 格式数据出错的问题 ([ffcebb4](https://github.com/CaoMeiYouRen/push-all-in-one/commit/ffcebb4)) 478 | 479 | # [1.3.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v1.2.0...v1.3.0) (2021-03-02) 480 | 481 | 482 | ### ✨ 新功能 483 | 484 | * 新增 iGot 推送 ([e53e6bb](https://github.com/CaoMeiYouRen/push-all-in-one/commit/e53e6bb)), closes [#4](https://github.com/CaoMeiYouRen/push-all-in-one/issues/4) 485 | * 新增 PushPlus 推送支持 ([299ae9f](https://github.com/CaoMeiYouRen/push-all-in-one/commit/299ae9f)) 486 | 487 | 488 | ### 🐛 Bug 修复 489 | 490 | * server-chan 新增弃用 warn ([c9a9d0d](https://github.com/CaoMeiYouRen/push-all-in-one/commit/c9a9d0d)), closes [#5](https://github.com/CaoMeiYouRen/push-all-in-one/issues/5) 491 | 492 | # [1.2.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v1.1.0...v1.2.0) (2021-02-28) 493 | 494 | 495 | ### ✨ 新功能 496 | 497 | * 完成 企业微信群机器人、企业微信应用推送 接入 ([12fa2f7](https://github.com/CaoMeiYouRen/push-all-in-one/commit/12fa2f7)) 498 | 499 | # [1.1.0](https://github.com/CaoMeiYouRen/push-all-in-one/compare/v1.0.0...v1.1.0) (2021-02-28) 500 | 501 | 502 | ### ✨ 新功能 503 | 504 | * 修改邮件推送为 BER分邮件系统 ([0b2e864](https://github.com/CaoMeiYouRen/push-all-in-one/commit/0b2e864)) 505 | 506 | # 1.0.0 (2021-02-27) 507 | 508 | 509 | ### ✨ 新功能 510 | 511 | * 完成 酷推 对接;文档编写;准备发布 ([56923c5](https://github.com/CaoMeiYouRen/push-all-in-one/commit/56923c5)) 512 | * 完成钉钉推送 ([071fb8b](https://github.com/CaoMeiYouRen/push-all-in-one/commit/071fb8b)) 513 | * 新增 email 推送支持 ([5fc9996](https://github.com/CaoMeiYouRen/push-all-in-one/commit/5fc9996)) 514 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # 贡献者公约 2 | 3 | ## 我们的承诺 4 | 5 | 身为项目成员、贡献者、负责人,我们保证参与此社区的每个人都不受骚扰,不论其年龄、体型、身体条件、民族、性征、性别认同与表现、经验水平、教育程度、社会地位、国籍、相貌、种族、宗教信仰及性取向如何。 6 | 7 | 我们承诺致力于建设开放、友善、多元、包容、健康的社区环境。 8 | 9 | ## 我们的准则 10 | 11 | 有助于促进本社区积极环境的行为包括但不限于: 12 | 13 | * 与人为善、推己及人 14 | * 尊重不同的主张、观点和经历 15 | * 积极提出、耐心接受有益批评 16 | * 面对过失,承担责任、认真道歉、从中学习 17 | * 关注社区共同诉求,而非一己私利 18 | 19 | 不当行为包括但不限于: 20 | 21 | * 发布与性有关的言论或图像,以及任何形式的献殷勤或勾引 22 | * 挑衅行为、侮辱或贬损的言论、人身及政治攻击 23 | * 公开或私下骚扰 24 | * 未获明确授权擅自发布他人的资料,如地址、电子邮箱等 25 | * 其他有理由认定为违反职业操守的不当行为 26 | 27 | ## 落实之义务 28 | 29 | 社区负责人有责任诠释何谓“妥当行为”,并据此准则,妥善公正地认定与处置不当、威胁、冒犯及有害的行为。 30 | 31 | 社区负责人有权利和义务删除、编辑、拒绝违背本公约的评论(comment)、提交(commit)、代码、维基(wiki)编辑、问题(issue)等贡献。如有必要,需告知采取措施之理由。 32 | 33 | ## 适用范围 34 | 35 | 此行为标准适用于本社区全部场合,以及在其他场合代表本社区的个人。 36 | 37 | 代表本社区的情形包括但不限于:使用官方电子邮件与社交平台、作为指定代表参与在线或线下活动。 38 | 39 | ## 贯彻落实 40 | 41 | 如遇滥用、骚扰等不当行为,请通过 [support@cmyr.dev](mailto:support@cmyr.dev) 向纪律检查委员举报。 42 | 纪委将迅速审议并调查全部投诉。 43 | 44 | 社区全体负责人有义务保密举报者信息。 45 | 46 | ## 指导方针 47 | 48 | 社区负责人将依据下列方案判断并处置违纪行为: 49 | 50 | ### 一、督促 51 | 52 | **社区影响**:用语不当、举止不符合职业道德或不受社区欢迎。 53 | 54 | **处理意见**:由社区负责人予以非公开的书面警告,阐明违纪事由、解释举止如何不妥。或将要求公开道歉。 55 | 56 | ### 二、警告 57 | 58 | **社区影响**:一起或多起事件中的违纪行为。 59 | 60 | **处理意见**:警告继续违纪之后果、违纪者在特定时间内禁止与当事人往来、不得擅自与社区执法者往来,禁令涵盖社区内外、社交网络在内的一切联络。如有违反,可致封禁乃至开除。 61 | 62 | ### 三、封禁 63 | 64 | **社区影响**:严重违纪行为,包括屡教不改。 65 | 66 | **处理意见**:违纪者在特定时间内禁止与社区的任何往来或公开联络,禁止任何与当事人公开或私下往来,不得擅自与社区执法者往来。如有违反,可致开除。 67 | 68 | ### 四、开除 69 | 70 | **社区影响**:典型违纪行为,例如屡教不改、骚扰某个人、敌对或贬低某个群体。 71 | 72 | **处理意见**:无限期禁止违纪者与项目社区的一切公开往来。 73 | 74 | ## 来源 75 | 76 | 本行为标准改编自[参与者公约][homepage]2.0 版,可在此查阅:[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0] 77 | 78 | 指导方针借鉴自[Mozilla 纪检分级][Mozilla CoC]。 79 | 80 | 此行为标准常见问题请洽:[https://www.contributor-covenant.org/faq][FAQ]。 81 | 另有诸译本:[https://www.contributor-covenant.org/translations][translations]。 82 | 83 | [homepage]:https://www.contributor-covenant.org 84 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 85 | [Mozilla CoC]: https://github.com/mozilla/diversity 86 | [FAQ]: https://www.contributor-covenant.org/faq 87 | [translations]: https://www.contributor-covenant.org/translations 88 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | 在为此存储库做出贡献时,请首先通过 issue、电子邮件或任何其他方法与此存储库的所有者讨论您希望进行的更改,然后再进行更改。 4 | 5 | **注意**: 6 | 7 | - 提问之前请三思,不要浪费我们的时间 8 | - 不要问那些你自己就能搞清楚的问题 9 | - 不要问在文档中提过的问题 10 | 11 | ## 开发环境设置 12 | 13 | 要设置开发环境,请按照以下步骤操作: 14 | 15 | 1. Clone 本项目 16 | 17 | ```sh 18 | git clone https://github.com/CaoMeiYouRen/push-all-in-one.git 19 | ``` 20 | 21 | 2. 安装依赖 22 | 23 | ```sh 24 | npm i 25 | # 或 yarn 26 | # 或 pnpm i 27 | ``` 28 | 3. 运行开发环境 29 | 30 | ```sh 31 | npm run dev 32 | ``` 33 | 34 | ## 问题和功能请求 35 | 36 | 你在源代码中发现了一个错误,文档中有一个错误,或者你想要一个新功能? 看看[GitHub 讨论](https://github.com/CaoMeiYouRen/push-all-in-one/discussions)看看它是否已经在讨论中。您可以通过[在 GitHub 上提交问题](https://github.com/CaoMeiYouRen/push-all-in-one/issues)来帮助我们。在创建问题之前,请确保搜索[问题存档](https://github.com/CaoMeiYouRen/push-all-in-one/issues?q=is%3Aissue+is%3Aclosed) - 您的问题可能已经得到解决! 37 | 38 | 请尝试创建以下错误报告: 39 | 40 | - *可重现*。包括重现问题的步骤。 41 | - *具体的*。包括尽可能多的细节:哪个版本,什么环境等。 42 | - *独特的*。不要复制现有的已打开问题。 43 | - *范围仅限于单个错误*。每个报告一个错误。 44 | 45 | **更好的是:提交带有修复或新功能的 Pull Requests!** 46 | 47 | ### 如何提交拉取请求 48 | 49 | 1. 在我们的存储库中搜索 与您的提交相关的开放或关闭的 [Pull Requests](https://github.com/CaoMeiYouRen/push-all-in-one/pulls)。你不想重复努力。 50 | 51 | 2. Fork 本项目 52 | 53 | 3. 创建您的功能分支 ( `git checkout -b feat/your_feature`) 54 | 55 | 4. 提交您的更改 56 | 57 | 本项目使用 [约定式提交](https://www.conventionalcommits.org/zh-hans/v1.0.0/),因此请遵循提交消息中的规范。 58 | 59 | git commit 将用于自动化生成日志,所以请勿直接提交 git commit。 60 | 61 | 非常建议使用 [commitizen](https://github.com/commitizen/cz-cli) 工具来生成 git commit,使用 husky 约束 git commit 62 | 63 | ```sh 64 | git add . 65 | git cz # 使用 commitizen 提交! 66 | git pull # 请合并最新代码并解决冲突后提交! 67 | #请勿直接提交git commit 68 | #若觉得修改太多也可分开提交。先 git add 一部分,执行 git cz 提交后再提交另外一部分 69 | ``` 70 | 71 | 关于选项,参考 [semantic-release](https://github.com/semantic-release/semantic-release) 的文档 72 | 73 | - 若为 BUG 修复,则选择 `fix` 74 | - 若为新增功能,则选择 `feat` 75 | - 若为性能优化,则选择 `perf` 76 | - 若为移除某些功能,则选择 `BREAKING CHANGE` 77 | - `BREAKING CHANGE` 和其他破坏性更新,若不是为了修复 BUG,原则上将拒绝该 PR 78 | 79 | 80 | 5. 推送到分支 ( `git push origin feat/your_feature`) 81 | 82 | 6. [打开一个新的 Pull Request](https://github.com/CaoMeiYouRen/push-all-in-one/compare?expand=1) 83 | 84 | *** 85 | _This CONTRIBUTING was generated with ❤️ by [cmyr-template-cli](https://github.com/CaoMeiYouRen/cmyr-template-cli)_ 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 CaoMeiYouRen(草梅友仁) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 4 |

push-all-in-one

5 |

6 | 7 | Version 8 | 9 | 10 | npm publish 11 | 12 | 13 | GitHub Workflow Status 14 | 15 | Node Current 16 | 17 | Documentation 18 | 19 | 20 | Maintenance 21 | 22 | 23 | License: MIT 24 | 25 |

26 | 27 | > Push All In One!支持 Server 酱(以及 Server 酱³)、自定义邮件、钉钉机器人、企业微信机器人、企业微信应用、飞书、pushplus、WxPusher、iGot 、Qmsg、息知、PushDeer、Discord、OneBot、Telegram、ntfy 等多种推送方式。 28 | > 29 | > Push All In One! Supports multiple push methods including Server Chan (and Server Chan³), custom email, DingTalk robot, WeChat Work robot, WeChat Work application, Feishu, pushplus, WxPusher, iGot, Qmsg, XiZhi, PushDeer, Discord, OneBot, Telegram, ntfy and more. 30 | > 31 | > 温馨提示:出于安全考虑, **所有** 推送方式请在 **服务端** 使用!请勿在 **客户端(网页端)** 使用! 32 | > 33 | > Friendly Reminder: For security reasons, **all** push methods should be used on the **server side**! Do not use them on the **client side (web page)**! 34 | > 35 | > 基于 push-all-in-one 和 hono 开发的云函数推送服务——[push-all-in-cloud](https://github.com/CaoMeiYouRen/push-all-in-cloud) 。支持 nodejs/docker/vercel 等部署方式 ,可一键部署到 vercel 。 36 | 37 | **重大更新提示:** `push-all-in-one` v4 版本不兼容 v3 及以下低版本,请查看 [CHANGELOG](./CHANGELOG.md) 了解改动。 38 | 39 | **BREAKING CHANGES**: `push-all-in-one` v4 version is not compatible with v3 and lower versions. Please refer to [CHANGELOG](./CHANGELOG.md) for changes. 40 | 41 | 建议根据 TypeScript 的类型提示进行修改。 42 | 43 | Suggest modifying according to TypeScript's type prompts. 44 | 45 | ## 🏠 主页 46 | 47 | [https://github.com/CaoMeiYouRen/push-all-in-one#readme](https://github.com/CaoMeiYouRen/push-all-in-one#readme) 48 | 49 | ## ✨ Demo 50 | 51 | [https://github.com/CaoMeiYouRen/push-all-in-one/tree/master/examples](https://github.com/CaoMeiYouRen/push-all-in-one/tree/master/examples) 52 | 53 | ## 📦 依赖要求/Requirements 54 | 55 | 56 | - node >=18 57 | 58 | ## 🚀 安装/Installation 59 | 60 | ```sh 61 | npm i push-all-in-one -S 62 | ``` 63 | 64 | ## 👨‍💻 使用/Usage 65 | 66 | 所有推送方式均实现了 `send(title: string, desp?: string, options?: any):` 方法。 67 | 68 | `title` 为 `消息标题`,`desp` 为 `消息描述`,`options` 为该推送方式的`额外推送选项`,具体请参考各个推送渠道的注释。 69 | 70 | > 不知道如何设置配置?请前往 [push-all-in-cloud 配置生成器](https://push.cmyr.dev/) 在线生成 `push-all-in-one` 和 `push-all-in-cloud` 通用配置。 71 | 72 | 调用方式举例: 73 | 74 | ```ts 75 | import { ServerChanTurbo, ServerChanV3, CustomEmail, Dingtalk, WechatRobot, WechatApp, PushPlus, WxPusher, IGot, Qmsg, XiZhi, PushDeer, Discord, OneBot, Telegram, Feishu, Ntfy, runPushAllInOne } from 'push-all-in-one' 76 | 77 | // 通过 runPushAllInOne 统一调用 78 | runPushAllInOne('测试推送', '测试推送', { 79 | type: 'ServerChanTurbo', 80 | config: { 81 | SERVER_CHAN_TURBO_SENDKEY: '', 82 | }, 83 | option: { 84 | }, 85 | }) 86 | 87 | 88 | // Server酱·Turbo。官方文档:https://sct.ftqq.com/r/13172 89 | const SCTKEY = 'SCTxxxxxxxxxxxxxxxxxxx' 90 | const serverChanTurbo = new ServerChanTurbo({ 91 | SERVER_CHAN_TURBO_SENDKEY: SCTKEY, 92 | }) 93 | serverChanTurbo.send('你好', '你好,我很可爱 - Server酱·Turbo', {}) 94 | 95 | // 【推荐】Server酱³ 96 | // Server酱3。官方文档:https://sc3.ft07.com/doc 97 | const SERVER_CHAN_V3_SENDKEY = 'sctpXXXXXXXXXXXXXXXXXXXXXXXX' 98 | const serverChanV3 = new ServerChanV3({ 99 | SERVER_CHAN_V3_SENDKEY, 100 | }) 101 | serverChanV3.send('你好', '你好,我很可爱 - Server酱³', {}) 102 | 103 | // 【推荐】自定义邮件,基于 nodemailer 实现,官方文档: https://github.com/nodemailer/nodemailer 104 | const customEmail = new CustomEmail({ 105 | EMAIL_TYPE: 'text', 106 | EMAIL_TO_ADDRESS: 'xxxxx@qq.com', 107 | EMAIL_AUTH_USER: 'yyyyy@qq.com', 108 | EMAIL_AUTH_PASS: '123456', 109 | EMAIL_HOST: 'smtp.qq.com', 110 | EMAIL_PORT: 465, 111 | }) 112 | customEmail.send('你好', '你好,我很可爱 - 自定义邮件', {}) 113 | 114 | // 【推荐】钉钉机器人。官方文档:https://developers.dingtalk.com/document/app/custom-robot-access 115 | const DINGTALK_ACCESS_TOKEN = 'xxxxxxxxxxxxxxxxxx' 116 | const DINGTALK_SECRET = 'SECxxxxxxxxxxxxxxxx' 117 | const dingtalk = new Dingtalk({ 118 | DINGTALK_ACCESS_TOKEN, 119 | DINGTALK_SECRET, 120 | }) 121 | dingtalk.send('你好', '你好,我很可爱 - 钉钉机器人', { msgtype: 'markdown' }) 122 | 123 | // 企业微信群机器人。官方文档:https://developer.work.weixin.qq.com/document/path/91770 124 | // 企业微信群机器人的使用需要两人以上加入企业,如果个人使用微信推送建议使用 企业微信应用+微信插件 推送。虽然需要配置的内容更多了,但是无需下载企业微信,网页端即可完成操作。 125 | const WECHAT_ROBOT_KEY = 'xxxxxxxxxxxxxxxxxxxxxxx' 126 | const wechatRobot = new WechatRobot({ 127 | WECHAT_ROBOT_KEY, 128 | }) 129 | wechatRobot.send('你好,我很可爱- 企业微信群机器人', '', { msgtype: 'text' }) 130 | 131 | // 【推荐】企业微信应用推送,官方文档:https://developer.work.weixin.qq.com/document/path/90664 132 | // 微信插件 https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin 133 | // 参数的介绍请参考:https://developer.work.weixin.qq.com/document/path/90665 134 | // 支持 text 和 markdown 格式,但 markdown 格式仅可在企业微信中查看 135 | const wechatApp = new WechatApp({ 136 | WECHAT_APP_CORPID: 'wwxxxxxxxxxxxxxxxxxxxx', 137 | WECHAT_APP_AGENTID: 10001, // 请更换为自己的 AGENTID 138 | WECHAT_APP_SECRET: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 139 | }) 140 | wechatApp.send('你好,我很可爱 - 企业微信应用推送', '', { 141 | msgtype: 'text', 142 | touser: '@all', 143 | }) 144 | 145 | // 【推荐】飞书 推送。官方文档:https://open.feishu.cn/document/home/index 146 | const feishu = new Feishu({ 147 | FEISHU_APP_ID: 'xxxxxxx', 148 | FEISHU_APP_SECRET: 'yyyyyyyy', 149 | }) 150 | feishu.send('你好,我很可爱 - 飞书', '', { 151 | receive_id_type: 'open_id', 152 | receive_id: 'zzzzzzzzzzzzzzzz', 153 | msg_type: 'text', 154 | }) 155 | 156 | // pushplus 推送,官方文档:https://www.pushplus.plus/doc/ 157 | const PUSH_PLUS_TOKEN = 'xxxxxxxxxxxxxxxxxxxxx' 158 | const pushplus = new PushPlus({ PUSH_PLUS_TOKEN }) 159 | pushplus.send('你好', '你好,我很可爱 - PushPlus', { 160 | template: 'html', 161 | channel: 'wechat', 162 | }) 163 | 164 | // iGot 推送,官方文档:http://hellyw.com/#/ 165 | const I_GOT_KEY = 'xxxxxxxxxx' 166 | const iGot = new IGot({ I_GOT_KEY }) 167 | iGot.send('你好', '你好,我很可爱 - iGot', { 168 | url: 'https://github.com/CaoMeiYouRen/push-all-in-one', 169 | topic: 'push-all-in-one', 170 | }) 171 | 172 | // Qmsg 酱 推送,官方文档:https://qmsg.zendee.cn 173 | const QMSG_KEY = 'xxxxxxxxxxxx' 174 | const qmsg = new Qmsg({ QMSG_KEY }) 175 | qmsg.send('你好,我很可爱 - Qmsg', '', { 176 | type: 'send', 177 | qq: '123456,654321', 178 | }) // msg:要推送的消息内容;qq:指定要接收消息的QQ号或者QQ群,多个以英文逗号分割,例如:12345,12346 179 | 180 | 181 | // 息知 推送,官方文档:https://xz.qqoq.net/#/index 182 | const XI_ZHI_KEY = 'xxxxxxxxxxxxx' 183 | const xiZhi = new XiZhi({ XI_ZHI_KEY }) 184 | xiZhi.send('你好', '你好,我很可爱 - XiZhi') 185 | 186 | // PushDeer 推送,官方文档:https://github.com/easychen/pushdeer 187 | const PUSH_DEER_PUSH_KEY = 'xxxxxxxxxx' 188 | const pushDeer = new PushDeer({ PUSH_DEER_PUSH_KEY }) 189 | pushDeer.send('你好', '你好,我很可爱 - PushDeer', { 190 | type: 'markdown', 191 | }) 192 | 193 | // 【推荐】Discord Webhook 推送,官方文档:https://support.discord.com/hc/zh-tw/articles/228383668-%E4%BD%BF%E7%94%A8%E7%B6%B2%E7%B5%A1%E9%89%A4%E6%89%8B-Webhooks- 194 | // [Recommended] Discord Webhook push. Official documentation: https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks 195 | const DISCORD_WEBHOOK = 'https://discord.com/api/webhooks/xxxxxxxxxxxxxxxxxxxxxxxxxxx' 196 | const DISCORD_USERNAME = 'My Bot' 197 | const PROXY_URL = 'http://127.0.0.1:8101' 198 | const discord = new Discord({ DISCORD_WEBHOOK, PROXY_URL }) 199 | // Discord 也支持以下方式添加代理地址 200 | // Discord also supports adding proxy addresses in the following ways 201 | // discord.proxyUrl = 'http://127.0.0.1:8101' 202 | discord.send('你好,我很可爱 - Discord', '', { 203 | username: DISCORD_USERNAME, 204 | }) 205 | 206 | // 【推荐】Telegram Bot 推送。官方文档:https://core.telegram.org/bots/api#making-requests 207 | // [Recommended] Telegram Bot push. Official documentation: https://core.telegram.org/bots/api#making-requests 208 | const telegram = new Telegram({ 209 | TELEGRAM_BOT_TOKEN: '111111:xxxxxxxxxxxxxx', 210 | TELEGRAM_CHAT_ID: 100000, 211 | // PROXY_URL: 'http://127.0.0.1:8101', 212 | }) 213 | // Telegram 也支持以下方式添加代理地址 214 | // Telegram also supports adding proxy addresses in the following ways 215 | // telegram.proxyUrl = 'http://127.0.0.1:8101' 216 | telegram.send('你好,我很可爱 - Telegram', '', { 217 | disable_notification: true, 218 | }) 219 | 220 | // OneBot 推送。官方文档:https://github.com/botuniverse/onebot-11 221 | // 本项目实现的版本为 OneBot 11 222 | // 在 mirai 环境下实现的插件版本可参考:https://github.com/yyuueexxiinngg/onebot-kotlin 223 | const ONE_BOT_BASE_URL = 'http://127.0.0.1:5700' 224 | const ONE_BOT_ACCESS_TOKEN = 'xxxxxxxxxxx' 225 | const oneBot = new OneBot({ ONE_BOT_BASE_URL, ONE_BOT_ACCESS_TOKEN }) 226 | oneBot.send('你好,我很可爱 - OneBot 11', '', { 227 | message_type: 'private', 228 | user_id: 123456789, 229 | }) 230 | 231 | // 【推荐】Ntfy 推送。官方文档:https://ntfy.sh/docs/publish/ 232 | const ntfy = new Ntfy({ 233 | NTFY_URL: 'https://ntfy.sh', 234 | NTFY_TOPIC: 'push_all_in_one_test', 235 | }) 236 | await ntfy.send('Ntfy - 标题支持中文', '你好,我很可爱 - Ntfy', { 237 | }) 238 | 239 | // WxPusher 推送。官方文档:https://wxpusher.zjiecode.com/docs 240 | // WxPusher 是一个开源的微信消息推送平台,支持多种消息格式,包括文本、HTML、Markdown 241 | // 使用前需要: 242 | // 1. 在 https://wxpusher.zjiecode.com/admin/main/app/appToken 申请 appToken 243 | // 2. 在 https://wxpusher.zjiecode.com/admin/main/wxuser/list 获取接收消息用户的 uid 244 | const WX_PUSHER_APP_TOKEN = 'xxxxxxxxxxxxxxxxxx' 245 | const WX_PUSHER_UID = 'yyyyyyyyyyyyyyyyyyy' 246 | const wxPusher = new WxPusher({ 247 | WX_PUSHER_APP_TOKEN, 248 | WX_PUSHER_UID, 249 | }) 250 | 251 | // 基础用法 252 | wxPusher.send('你好', '你好,我很可爱 - WxPusher') 253 | 254 | // 高级用法 255 | wxPusher.send('你好', '你好,我很可爱 - WxPusher', { 256 | contentType: 3, // 内容类型:1=文本,2=HTML,3=Markdown,默认为1 257 | summary: '消息摘要', // 显示在微信聊天页面的消息摘要,限制长度20,不传则自动截取content 258 | url: 'https://wxpusher.zjiecode.com', // 点击消息时打开的链接,可选 259 | topicIds: [123], // 发送目标的主题ID数组,可以实现群发,可选 260 | save: 1, // 是否保存消息:0=不保存,1=保存,默认0 261 | verifyPayload: 'test', // 验证负载,仅针对text消息类型有效,可选 262 | }) 263 | 264 | // HTML 格式示例 265 | wxPusher.send('HTML 消息', '

标题

红色文字

', { 266 | contentType: 2, 267 | summary: 'HTML示例', 268 | }) 269 | 270 | // Markdown 格式示例 271 | wxPusher.send('Markdown 消息', '## 二级标题\n- 列表项1\n- 列表项2', { 272 | contentType: 3, 273 | summary: 'Markdown示例', 274 | }) 275 | 276 | // 群发示例 277 | wxPusher.send('群发消息', '这是一条群发消息', { 278 | contentType: 1, 279 | topicIds: [123, 456], // 可以发送给多个主题 280 | uids: ['UID_1', 'UID_2'], // 可以同时发送给多个用户 281 | }) 282 | 283 | 更多例子请参考 [examples](https://github.com/CaoMeiYouRen/push-all-in-one/tree/master/examples) 284 | 285 | **代理支持** 286 | 287 | | 环境变量 | 作用 | 例子 | 288 | | ----------- | ------------------------------------------ | ---------------------- | 289 | | NO_PROXY | 设置是否禁用代理 | true | 290 | | HTTP_PROXY | 设置 http/https 代理 | http://127.0.0.1:8101 | 291 | | HTTPS_PROXY | 设置 http/https 代理 | http://127.0.0.1:8101 | 292 | | SOCKS_PROXY | 通过 socks/socks5 协议设置 http/https 代理 | socks://127.0.0.1:8100 | 293 | 294 | 本项目通过环境变量来支持请求代理 295 | 296 | ```ts 297 | // 在 nodejs 项目中可通过直接设置环境变量来设置代理 298 | process.env.HTTP_PROXY = 'http://127.0.0.1:8101' // 当请求是 http/https 的时候走 HTTP_PROXY 299 | process.env.HTTPS_PROXY = 'http://127.0.0.1:8101' // 当请求是 http/https 的时候走 HTTPS_PROXY,HTTPS_PROXY 优先 300 | process.env.SOCKS_PROXY = 'socks://127.0.0.1:8100' // 当 HTTP_PROXY 设置时走 SOCKS_PROXY 301 | // process.env.NO_PROXY = true // 设置 NO_PROXY 可禁用代理 302 | ``` 303 | 304 | 在命令行中可手动设置环境变量 305 | 306 | ```sh 307 | set HTTP_PROXY='http://127.0.0.1:8101' # Windows 308 | export HTTP_PROXY='http://127.0.0.1:8101' # Linux 309 | cross-env HTTP_PROXY='http://127.0.0.1:8101' # 通过 cross-env 这个包来跨平台 310 | ``` 311 | 312 | ## 🛠️ 开发/Development 313 | 314 | 本项目采用 TypeScript 开发,使用 tsup 打包,可以完美实现类型提示和摇树优化,对于未使用到的模块,会在编译阶段去除。 315 | 316 | ```sh 317 | npm run dev 318 | ``` 319 | 320 | ## 🐛 debug 321 | 322 | 本项目使用 `debug` 这个包来 debug ,如果要开启调试则设置环境变量为 `DEBUG=push:*` 即可,例如 323 | 324 | ```sh 325 | cross-env DEBUG=push:* NODE_ENV=development ts-node-dev test/index.test.ts # 因为一些原因该文件未上传,可自行编写测试用例 326 | ``` 327 | 328 | ## 🔧 编译/Build 329 | 330 | ```sh 331 | npm run build 332 | ``` 333 | 334 | ## 🔍 Lint 335 | 336 | ```sh 337 | npm run lint 338 | ``` 339 | 340 | ## 💾 Commit 341 | 342 | ```sh 343 | npm run commit 344 | ``` 345 | 346 | ## 👤 作者/Author 347 | 348 | **CaoMeiYouRen** 349 | 350 | * Website: [https://blog.cmyr.ltd/](https://blog.cmyr.ltd/) 351 | * GitHub: [@CaoMeiYouRen](https://github.com/CaoMeiYouRen) 352 | 353 | ## 🤝 贡献/Contribution 354 | 355 | 欢迎 贡献、提问或提出新功能!
如有问题请查看 [issues page](https://github.com/CaoMeiYouRen/push-all-in-one/issues).
贡献或提出新功能可以查看[contributing guide](https://github.com/CaoMeiYouRen/push-all-in-one/blob/master/CONTRIBUTING.md). 356 | 357 | Welcome to contribute, ask questions or propose new features!
If you have any questions, please check the [issues page](https://github.com/CaoMeiYouRen/push-all-in-one/issues).
For contributions or new feature proposals, please refer to the [contributing guide](https://github.com/CaoMeiYouRen/push-all-in-one/blob/master/CONTRIBUTING.md). 358 | 359 | ## 💰 支持/Support 360 | 361 | 如果觉得这个项目有用的话请给一颗⭐️,非常感谢。 362 | 363 | If you find this project useful, please give it a ⭐️. Thank you very much. 364 | 365 | 366 | 在爱发电支持我 367 | 368 | 369 | 370 | become a patreon 371 | 372 | 373 | ## 🌟 Star History 374 | 375 | [![Star History Chart](https://api.star-history.com/svg?repos=CaoMeiYouRen/push-all-in-one&type=Date)](https://star-history.com/#CaoMeiYouRen/push-all-in-one&Date) 376 | 377 | ## 📝 License 378 | 379 | Copyright © 2022 [CaoMeiYouRen](https://github.com/CaoMeiYouRen).
380 | This project is [MIT](https://github.com/CaoMeiYouRen/push-all-in-one/blob/master/LICENSE) licensed. 381 | 382 | *** 383 | _This README was generated with ❤️ by [cmyr-template-cli](https://github.com/CaoMeiYouRen/cmyr-template-cli)_ 384 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [2, 'always', [ 5 | 'feat', 6 | 'fix', 7 | 'docs', 8 | 'style', 9 | 'refactor', 10 | 'perf', 11 | 'test', 12 | 'build', 13 | 'ci', 14 | 'chore', 15 | 'revert', 16 | ]], 17 | 'subject-full-stop': [0, 'never'], 18 | 'subject-case': [0, 'never'], 19 | 'body-max-line-length': [0, 'never'], 20 | 'footer-max-line-length': [0, 'never'], 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /examples/01-example.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios' 2 | import colors from '@colors/colors' 3 | import { ServerChanTurbo, ServerChanV3, CustomEmail, Dingtalk, WechatRobot, WechatApp, PushPlus, IGot, Qmsg, XiZhi, PushDeer, Discord, OneBot, Telegram, WxPusher, PushPlusTemplateType, CustomEmailType, WechatRobotMsgType, WechatAppMsgType, PushPlusChannelType } from '../src' 4 | import { warn } from '../src/utils/helper' 5 | import { SendResponse } from '../src/interfaces/response' 6 | 7 | export function info(text: any): void { 8 | // eslint-disable-next-line no-console 9 | console.info(colors.cyan(text)) 10 | } 11 | 12 | /** 13 | * 14 | * 从环境变量读取配置并批量推送 15 | * @author CaoMeiYouRen 16 | * @date 2023-10-25 17 | * @export 18 | * @param title 19 | * @param [desp] 20 | */ 21 | export async function batchPushAllInOne(title: string, desp?: string): Promise>[]> { 22 | const env = process.env 23 | const pushs: Promise>[] = [] 24 | if (env.SERVER_CHAN_TURBO_SENDKEY) { 25 | // Server酱。官方文档:https://sct.ftqq.com/ 26 | const serverChanTurbo = new ServerChanTurbo({ 27 | SERVER_CHAN_TURBO_SENDKEY: env.SERVER_CHAN_TURBO_SENDKEY, 28 | }) 29 | pushs.push(serverChanTurbo.send(title, desp, {})) 30 | info('Server酱·Turbo 已加入推送队列') 31 | } else { 32 | info('未配置 Server酱·Turbo,已跳过') 33 | } 34 | 35 | if (env.SERVER_CHAN_V3_SENDKEY) { 36 | const serverChanV3 = new ServerChanV3({ 37 | SERVER_CHAN_V3_SENDKEY: env.SERVER_CHAN_V3_SENDKEY, 38 | }) 39 | pushs.push(serverChanV3.send(title, desp, {})) 40 | info('Server酱³ 已加入推送队列') 41 | } else { 42 | info('未配置 Server酱³,已跳过') 43 | } 44 | 45 | if (env.EMAIL_AUTH_USER && env.EMAIL_AUTH_PASS && env.EMAIL_HOST && env.EMAIL_PORT && env.EMAIL_TO_ADDRESS) { 46 | // 自定义邮件,基于 nodemailer 实现,官方文档: https://github.com/nodemailer/nodemailer 47 | const customEmail = new CustomEmail({ 48 | EMAIL_TYPE: env.EMAIL_TYPE as CustomEmailType, 49 | EMAIL_TO_ADDRESS: env.EMAIL_TO_ADDRESS, 50 | EMAIL_AUTH_USER: env.EMAIL_AUTH_USER, 51 | EMAIL_AUTH_PASS: env.EMAIL_AUTH_PASS, 52 | EMAIL_HOST: env.EMAIL_HOST, 53 | EMAIL_PORT: Number(env.EMAIL_PORT), 54 | }) 55 | pushs.push(customEmail.send(title, desp)) 56 | info('自定义邮件 已加入推送队列') 57 | } else { 58 | info('未配置 自定义邮件,已跳过') 59 | } 60 | 61 | if (env.DINGTALK_ACCESS_TOKEN) { 62 | // 钉钉机器人。官方文档:https://developers.dingtalk.com/document/app/custom-robot-access 63 | const dingtalk = new Dingtalk({ 64 | DINGTALK_ACCESS_TOKEN: env.DINGTALK_ACCESS_TOKEN, 65 | DINGTALK_SECRET: env.DINGTALK_SECRET, 66 | }) 67 | pushs.push(dingtalk.send(title, desp, { msgtype: 'markdown' })) 68 | info('钉钉机器人 已加入推送队列') 69 | } else { 70 | info('未配置 钉钉机器人,已跳过') 71 | } 72 | 73 | if (env.WECHAT_ROBOT_KEY) { 74 | // 企业微信群机器人。官方文档:https://work.weixin.qq.com/help?person_id=1&doc_id=13376 75 | // 企业微信群机器人的使用需要两人以上加入企业,如果个人使用微信推送建议使用 企业微信应用+微信插件 推送 76 | const wechatRobot = new WechatRobot({ 77 | WECHAT_ROBOT_KEY: env.WECHAT_ROBOT_KEY, 78 | }) 79 | pushs.push(wechatRobot.send(title, desp, { 80 | msgtype: env.WECHAT_ROBOT_MSG_TYPE as WechatRobotMsgType, 81 | })) 82 | info('企业微信群机器人 已加入推送队列') 83 | } else { 84 | info('未配置 企业微信群机器人,已跳过') 85 | } 86 | 87 | if (env.WECHAT_APP_CORPID && env.WECHAT_APP_AGENTID && env.WECHAT_APP_SECRET) { 88 | // 企业微信应用推送,官方文档:https://work.weixin.qq.com/api/doc/90000/90135/90664 89 | const wechatApp = new WechatApp({ 90 | WECHAT_APP_CORPID: env.WECHAT_APP_CORPID, 91 | WECHAT_APP_AGENTID: Number(env.WECHAT_APP_AGENTID), 92 | WECHAT_APP_SECRET: env.WECHAT_APP_SECRET, 93 | }) 94 | pushs.push(wechatApp.send(title, desp, { 95 | msgtype: env.WECHAT_APP_MSG_TYPE as WechatAppMsgType, 96 | touser: env.WECHAT_APP_USERID || '@all', 97 | })) 98 | info('企业微信应用推送 已加入推送队列') 99 | } else { 100 | info('未配置 企业微信应用推送,已跳过') 101 | } 102 | 103 | if (env.PUSH_PLUS_TOKEN) { 104 | // pushplus 推送,官方文档:http://pushplus.hxtrip.com/doc/ 105 | const pushplus = new PushPlus({ 106 | PUSH_PLUS_TOKEN: env.PUSH_PLUS_TOKEN, 107 | }) 108 | pushs.push(pushplus.send(title, desp, { 109 | template: env.PUSH_PLUS_TEMPLATE_TYPE as PushPlusTemplateType || 'html', 110 | channel: env.PUSH_PLUS_CHANNEL_TYPE as PushPlusChannelType || 'wechat', 111 | })) 112 | info('pushplus 推送 已加入推送队列') 113 | } else { 114 | info('未配置 pushplus 推送,已跳过') 115 | } 116 | 117 | if (env.I_GOT_KEY) { 118 | // iGot 推送,官方文档:https://wahao.github.io/Bark-MP-helper 119 | const iGot = new IGot({ 120 | I_GOT_KEY: env.I_GOT_KEY, 121 | }) 122 | pushs.push(iGot.send(title, desp, {})) 123 | info('iGot 推送 已加入推送队列') 124 | } else { 125 | info('未配置 iGot 推送,已跳过') 126 | } 127 | 128 | if (env.QMSG_KEY) { 129 | // Qmsg 酱 推送,官方文档:https://qmsg.zendee.cn 130 | const qmsg = new Qmsg({ 131 | QMSG_KEY: env.QMSG_KEY, 132 | }) 133 | pushs.push(qmsg.send(title, desp || '', { 134 | type: 'send', 135 | qq: env.QMSG_QQ || '', 136 | })) 137 | info('Qmsg 推送 已加入推送队列') 138 | } else { 139 | info('未配置 Qmsg 推送,已跳过') 140 | } 141 | 142 | if (env.XI_ZHI_KEY) { 143 | // 息知 推送,官方文档:https://xz.qqoq.net/#/index 144 | const xiZhi = new XiZhi({ 145 | XI_ZHI_KEY: env.XI_ZHI_KEY, 146 | }) 147 | pushs.push(xiZhi.send(title, desp)) 148 | info('XiZhi 推送 已加入推送队列') 149 | } else { 150 | info('未配置 XiZhi 推送,已跳过') 151 | } 152 | 153 | if (env.PUSH_DEER_PUSH_KEY) { 154 | // 【推荐】PushDeer 推送,官方文档:https://github.com/easychen/pushdeer 155 | const pushDeer = new PushDeer({ 156 | PUSH_DEER_PUSH_KEY: env.PUSH_DEER_PUSH_KEY, 157 | }) 158 | pushs.push(pushDeer.send(title, desp, { 159 | type: 'markdown', 160 | })) 161 | info('PushDeer 推送 已加入推送队列') 162 | } else { 163 | info('未配置 PushDeer 推送,已跳过') 164 | } 165 | 166 | if (env.DISCORD_WEBHOOK) { 167 | // 【推荐】Discord Webhook 推送,官方文档:https://support.discord.com/hc/zh-tw/articles/228383668-%E4%BD%BF%E7%94%A8%E7%B6%B2%E7%B5%A1%E9%89%A4%E6%89%8B-Webhooks- 168 | const discord = new Discord({ 169 | DISCORD_WEBHOOK: env.DISCORD_WEBHOOK, 170 | }) 171 | pushs.push(discord.send(title, desp, { 172 | username: env.DISCORD_USERNAME, 173 | })) 174 | info('Discord 推送 已加入推送队列') 175 | } else { 176 | info('未配置 Discord 推送,已跳过') 177 | } 178 | 179 | if (env.TELEGRAM_BOT_TOKEN && env.TELEGRAM_CHAT_ID) { 180 | // 【推荐】Telegram Bot 推送。官方文档:https://core.telegram.org/bots/api#making-requests 181 | const telegram = new Telegram({ 182 | TELEGRAM_BOT_TOKEN: env.TELEGRAM_BOT_TOKEN, 183 | TELEGRAM_CHAT_ID: Number(env.TELEGRAM_CHAT_ID), 184 | }) 185 | pushs.push(telegram.send(title, desp, { 186 | disable_notification: false, 187 | })) 188 | info('Telegram 推送 已加入推送队列') 189 | } else { 190 | info('未配置 Telegram 推送,已跳过') 191 | } 192 | 193 | if (env.ONE_BOT_BASE_URL && env.ONE_BOT_ACCESS_TOKEN) { 194 | // OneBot 推送。官方文档:https://github.com/botuniverse/onebot-11 195 | // 本项目实现的版本为 OneBot 11 196 | // 在 mirai 环境下实现的插件版本可参考:https://github.com/yyuueexxiinngg/onebot-kotlin 197 | const oneBot = new OneBot({ 198 | ONE_BOT_BASE_URL: env.ONE_BOT_BASE_URL, 199 | ONE_BOT_ACCESS_TOKEN: env.ONE_BOT_ACCESS_TOKEN, 200 | }) 201 | pushs.push(oneBot.send(title, desp || '', { 202 | message_type: 'private', 203 | user_id: Number(env.ONE_BOT_USER_ID), 204 | })) 205 | info('OneBot 推送 已加入推送队列') 206 | } else { 207 | info('未配置 OneBot 推送,已跳过') 208 | } 209 | 210 | if (env.WX_PUSHER_APP_TOKEN && env.WX_PUSHER_UID) { 211 | // WxPusher 推送。官方文档:https://wxpusher.zjiecode.com/docs 212 | const wxPusher = new WxPusher({ 213 | WX_PUSHER_APP_TOKEN: env.WX_PUSHER_APP_TOKEN, 214 | WX_PUSHER_UID: env.WX_PUSHER_UID, 215 | }) 216 | pushs.push(wxPusher.send(title, desp, { 217 | contentType: 3, // 使用 markdown 格式 218 | })) 219 | info('WxPusher 推送 已加入推送队列') 220 | } else { 221 | info('未配置 WxPusher 推送,已跳过') 222 | } 223 | 224 | if (pushs.length === 0) { 225 | warn('未配置任何推送,请检查推送配置的环境变量!') 226 | return [] 227 | } 228 | 229 | const results = await Promise.allSettled(pushs) 230 | const success = results.filter((e) => e.status === 'fulfilled') 231 | const fail = results.filter((e) => e.status === 'rejected') 232 | 233 | info(`本次共推送 ${results.length} 个,成功 ${success.length} 个,失败 ${fail.length} 个`) 234 | 235 | return results 236 | } 237 | -------------------------------------------------------------------------------- /examples/02-example.ts: -------------------------------------------------------------------------------- 1 | import { runPushAllInOne } from '../src' 2 | 3 | runPushAllInOne('测试推送', '测试推送', { 4 | type: 'ServerChanTurbo', 5 | config: { 6 | SERVER_CHAN_TURBO_SENDKEY: '', 7 | }, 8 | option: { 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import type { Config } from 'jest' 3 | 4 | const config: Config = { 5 | testTimeout: 20000, 6 | moduleNameMapper: { 7 | '^@/(.*)$': '/src/$1', 8 | }, 9 | moduleFileExtensions: [ 10 | 'js', 11 | 'json', 12 | 'ts', 13 | ], 14 | rootDir: '.', 15 | testRegex: '.(test|spec).ts$', 16 | transform: { 17 | '^.+\\.(t|j)s$': 'ts-jest', 18 | }, 19 | coverageDirectory: path.resolve('./coverage'), 20 | testEnvironment: 'node', 21 | } 22 | 23 | export default config 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "push-all-in-one", 3 | "version": "4.4.3", 4 | "description": "Push All In One!支持 Server酱(以及 Server 酱³)、自定义邮件、钉钉机器人、企业微信机器人、企业微信应用、pushplus、iGot 、Qmsg、息知、PushDeer、Discord、OneBot、Telegram 等多种推送方式", 5 | "author": "CaoMeiYouRen", 6 | "license": "MIT", 7 | "main": "dist/index.cjs", 8 | "module": "dist/index.mjs", 9 | "types": "dist/index.d.ts", 10 | "type": "module", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "import": "./dist/index.mjs", 15 | "require": "./dist/index.cjs" 16 | } 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "engines": { 22 | "node": ">=20" 23 | }, 24 | "keywords": [ 25 | "push", 26 | "server-chan", 27 | "serverchan", 28 | "server-chan-turbo", 29 | "server-chan-v3", 30 | "email", 31 | "custom-email", 32 | "nodemailer", 33 | "dingtalk", 34 | "weixin", 35 | "wechat", 36 | "pushplus", 37 | "push+", 38 | "iGot", 39 | "Qmsg", 40 | "xi-zhi", 41 | "PushDeer", 42 | "pushdeer", 43 | "Discord", 44 | "OneBot", 45 | "Telegram", 46 | "WxPusher" 47 | ], 48 | "scripts": { 49 | "lint": "cross-env NODE_ENV=production eslint src examples --fix --ext .ts,.js,.cjs,.mjs", 50 | "prebuild": "rimraf dist", 51 | "build": "cross-env NODE_ENV=production tsup", 52 | "analyzer": "cross-env NODE_ENV=production ANALYZER=true rollup -c", 53 | "dev": "cross-env NODE_ENV=development tsx watch src/index.ts", 54 | "dev:tsup": "cross-env NODE_ENV=development tsup --watch", 55 | "rm": "rimraf node_modules", 56 | "start": "node ./dist/index.mjs", 57 | "release": "semantic-release", 58 | "commit": "git add . && git cz", 59 | "test": "cross-env DEBUG=push:* NODE_ENV=development jest", 60 | "test:cov": "cross-env DEBUG=push:* NODE_ENV=development jest --coverage", 61 | "prepare": "husky install" 62 | }, 63 | "devDependencies": { 64 | "@commitlint/cli": "^19.0.1", 65 | "@commitlint/config-conventional": "^19.0.3", 66 | "@semantic-release/changelog": "^6.0.1", 67 | "@semantic-release/git": "^10.0.1", 68 | "@types/crypto-js": "^4.1.0", 69 | "@types/debug": "^4.1.5", 70 | "@types/jest": "^29.5.14", 71 | "@types/lodash": "^4.14.168", 72 | "@types/mocha": "^10.0.1", 73 | "@types/module-alias": "^2.0.0", 74 | "@types/node": "^22.0.0", 75 | "@types/nodemailer": "^6.4.7", 76 | "@typescript-eslint/eslint-plugin": "7.18.0", 77 | "@typescript-eslint/parser": "7.18.0", 78 | "commitizen": "^4.2.3", 79 | "conventional-changelog-cli": "2.2.2", 80 | "conventional-changelog-cmyr-config": "2.1.2", 81 | "cross-env": "^7.0.3", 82 | "cz-conventional-changelog": "^3.3.0", 83 | "cz-conventional-changelog-cmyr": "^1.1.0", 84 | "eslint": "^8.42.0", 85 | "eslint-config-cmyr": "^1.1.30", 86 | "eslint-plugin-import": "^2.25.4", 87 | "husky": "^9.0.5", 88 | "jest": "^29.7.0", 89 | "lint-staged": "^16.1.0", 90 | "lodash": "^4.17.21", 91 | "mocha": "^11.0.1", 92 | "module-alias": "^2.2.2", 93 | "rimraf": "^6.0.0", 94 | "semantic-release": "21.1.2", 95 | "should": "^13.2.3", 96 | "ts-jest": "^29.2.5", 97 | "ts-node": "^10.5.0", 98 | "ts-node-dev": "^2.0.0", 99 | "tslib": "^2.6.2", 100 | "tsup": "^8.5.0", 101 | "tsx": "^4.19.4", 102 | "typescript": "^5.8.3", 103 | "validate-commit-msg": "^2.14.0" 104 | }, 105 | "dependencies": { 106 | "@colors/colors": "^1.5.0", 107 | "axios": "^1.2.1", 108 | "debug": "^4.3.1", 109 | "https-proxy-agent": "7.0.6", 110 | "nodemailer": "^7.0.3", 111 | "socks-proxy-agent": "^8.0.4" 112 | }, 113 | "config": { 114 | "commitizen": { 115 | "path": "./node_modules/cz-conventional-changelog-cmyr" 116 | } 117 | }, 118 | "changelog": { 119 | "language": "zh" 120 | }, 121 | "husky": { 122 | "hooks": { 123 | "pre-commit": "lint-staged", 124 | "commit-msg": "validate-commit-msg" 125 | } 126 | }, 127 | "gitHooks": { 128 | "pre-commit": "lint-staged" 129 | }, 130 | "lint-staged": { 131 | "*.{js,ts}": [ 132 | "npm run lint", 133 | "git add" 134 | ] 135 | }, 136 | "pnpm": { 137 | "onlyBuiltDependencies": [ 138 | "esbuild" 139 | ] 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/config/env.ts: -------------------------------------------------------------------------------- 1 | export const __PROD__ = process.env.NODE_ENV === 'production' 2 | export const __DEV__ = process.env.NODE_ENV === 'development' 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './push/custom-email' 2 | export * from './push/dingtalk' 3 | export * from './push/discord' 4 | export * from './push/feishu' 5 | export * from './push/i-got' 6 | export * from './push/ntfy' 7 | export * from './push/one-bot' 8 | export * from './push/push-deer' 9 | export * from './push/push-plus' 10 | export * from './push/qmsg' 11 | export * from './push/server-chan-turbo' 12 | export * from './push/server-chan-v3' 13 | export * from './push/telegram' 14 | export * from './push/wechat-app' 15 | export * from './push/wechat-robot' 16 | export * from './push/xi-zhi' 17 | export * from './push/wx-pusher' 18 | 19 | export * from './interfaces/response' 20 | export * from './interfaces/schema' 21 | export * from './interfaces/send' 22 | export * from './one' 23 | 24 | -------------------------------------------------------------------------------- /src/interfaces/response.ts: -------------------------------------------------------------------------------- 1 | export interface SendResponse { 2 | headers?: any 3 | status: number 4 | statusText: string 5 | data: T 6 | } 7 | -------------------------------------------------------------------------------- /src/interfaces/schema.ts: -------------------------------------------------------------------------------- 1 | // 是否是联合类型 2 | type IsUnion = T extends U ? ([U] extends [T] ? false : true) : never 3 | /** 4 | * 判断类型是否相同 5 | */ 6 | type Equal = 7 | (() => U extends Left ? 1 : 0) extends (() => U extends Right ? 1 : 0) ? true : false 8 | 9 | /** 10 | * 判断字段是否必填 11 | */ 12 | type IsRequired = Equal, T> 13 | 14 | export type Config = { 15 | [key: string]: any 16 | } 17 | 18 | /** 19 | * 配置 Schema 20 | * 如果字段的类型是 string,则生成的 Schema 类型为 string 21 | * 如果字段的类型是 number,则生成的 Schema 类型为 number 22 | * 如果字段的类型是 boolean,则生成的 Schema 类型为 boolean 23 | * 如果字段的类型是 object,则生成的 Schema 类型为 object 24 | * 如果字段的类型是 array,则生成的 Schema 类型为 array 25 | * 如果字段的类型是 联合 number 类型(1 | 2 | 3),则生成的 Schema 类型为 select 26 | * 如果字段的类型是 联合 string 类型('text' | 'html'),则生成的 Schema 类型为 select 27 | * (IsUnion extends true ? 'select' : never) 28 | */ 29 | export type ConfigSchema = { 30 | [K in keyof T]: { 31 | // 字段类型 32 | type: T[K] extends boolean ? 'boolean' : ( 33 | IsUnion extends true ? 'select' : ( 34 | T[K] extends string ? 'string' : ( 35 | T[K] extends number ? 'number' : ( 36 | T[K] extends any[] ? 'array' : ( 37 | T[K] extends object ? 'object' : ( 38 | 'select' 39 | ) 40 | ) 41 | ) 42 | ) 43 | ) 44 | ) 45 | 46 | // 字段名称 47 | title?: string 48 | // 字段描述 49 | description?: string 50 | // 字段是否必填 51 | required: boolean // IsRequired> 52 | // 字段默认值 53 | default?: T[K] 54 | // 字段选项,仅当字段类型为 select 时有效 55 | options?: { 56 | // 选项名称 57 | label: string 58 | // 选项值 59 | value: T[K] // 选项值的类型跟字段的类型一致 60 | }[] 61 | } 62 | } 63 | 64 | // type ConfigA = { 65 | // name: string 66 | // age?: number 67 | // isActive: boolean 68 | // content?: 'text' | 'html' 69 | // status: 1 | 2 | 3 70 | // } 71 | 72 | // type ConfigSchemaA = ConfigSchema 73 | 74 | // const a: ConfigSchemaA = { 75 | // name: { 76 | // type: 'string', 77 | // title: '', 78 | // description: '', 79 | // required: true, 80 | // default: '', 81 | // }, 82 | // age: { 83 | // type: 'number', 84 | // title: '', 85 | // description: '', 86 | // required: false, 87 | // default: 0, 88 | // }, 89 | // isActive: { 90 | // type: 'boolean', 91 | // title: '', 92 | // description: '', 93 | // required: true, 94 | // default: false, 95 | // options: [ 96 | // { 97 | // label: '是', 98 | // value: true, 99 | // }, 100 | // { 101 | // label: '否', 102 | // value: false, 103 | // }, 104 | // ], 105 | // }, 106 | // content: { 107 | // type: 'string', 108 | // title: '', 109 | // description: '', 110 | // required: false, 111 | // default: 'text', 112 | // options: [ 113 | // { 114 | // label: '文本', 115 | // value: 'text', 116 | // }, 117 | // { 118 | // label: 'HTML', 119 | // value: 'html', 120 | // }, 121 | // ], 122 | // }, 123 | // status: { 124 | // type: 'number', 125 | // title: '', 126 | // description: '', 127 | // required: true, 128 | // default: 1, 129 | // options: [ 130 | // { 131 | // label: '1', 132 | // value: 1, 133 | // }, 134 | // { 135 | // label: '2', 136 | // value: 2, 137 | // }, 138 | // { 139 | // label: '3', 140 | // value: 3, 141 | // }, 142 | // ], 143 | // }, 144 | // } 145 | 146 | export type Option = { 147 | [key: string]: any 148 | } 149 | 150 | export type OptionSchema = ConfigSchema 151 | 152 | -------------------------------------------------------------------------------- /src/interfaces/send.ts: -------------------------------------------------------------------------------- 1 | import { SendResponse } from './response' 2 | 3 | /** 4 | * 要求所有 push 方法都至少实现了 send 接口 5 | * 6 | * @author CaoMeiYouRen 7 | * @date 2021-02-27 8 | * @export 9 | * @interface Send 10 | */ 11 | export interface Send { 12 | /** 13 | * 代理地址。支持 http/https/socks/socks5 协议。例如 http://127.0.0.1:8080 14 | * 15 | * @author CaoMeiYouRen 16 | * @date 2024-04-20 17 | */ 18 | proxyUrl?: string 19 | /** 20 | * 发送消息 21 | * 22 | * @author CaoMeiYouRen 23 | * @date 2024-11-09 24 | * @param title 消息标题 25 | * @param [desp] 消息描述 26 | * @param [options] 发送选项 27 | */ 28 | send(title: string, desp?: string, options?: any): Promise> 29 | } 30 | -------------------------------------------------------------------------------- /src/one.ts: -------------------------------------------------------------------------------- 1 | import { CustomEmail, Dingtalk, Discord, Feishu, IGot, Ntfy, OneBot, PushDeer, PushPlus, Qmsg, ServerChanTurbo, ServerChanV3, Telegram, WechatApp, WechatRobot, XiZhi, WxPusher } from './index' 2 | import { SendResponse } from '@/interfaces/response' 3 | 4 | export const PushAllInOne = { 5 | CustomEmail, 6 | Dingtalk, 7 | Discord, 8 | Feishu, 9 | IGot, 10 | Ntfy, 11 | OneBot, 12 | PushDeer, 13 | PushPlus, 14 | Qmsg, 15 | ServerChanTurbo, 16 | ServerChanV3, 17 | Telegram, 18 | WechatApp, 19 | WechatRobot, 20 | WxPusher, 21 | XiZhi, 22 | } as const 23 | 24 | export type IPushAllInOne = typeof PushAllInOne 25 | 26 | export type PushType = keyof IPushAllInOne 27 | 28 | export type MetaPushConfig = { 29 | type: T 30 | config: ConstructorParameters[0] 31 | option: Parameters[2] 32 | } 33 | 34 | /** 35 | * 从传入变量中读取配置,并选择一个渠道推送 36 | * 37 | * @author CaoMeiYouRen 38 | * @date 2024-11-09 39 | * @export 40 | * @template T 41 | * @param title 推送标题 42 | * @param desp 推送内容 43 | * @param pushConfig 推送配置 44 | */ 45 | export async function runPushAllInOne(title: string, desp: string, pushConfig: MetaPushConfig): Promise> { 46 | const { type, config, option } = pushConfig 47 | if (PushAllInOne[type]) { 48 | const push = new PushAllInOne[type](config as any) 49 | return push.send(title, desp, option as any) 50 | } 51 | throw new Error('未匹配到任何推送方式!') 52 | } 53 | -------------------------------------------------------------------------------- /src/push/custom-email.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import nodemailer from 'nodemailer' 3 | import SMTPTransport from 'nodemailer/lib/smtp-transport' 4 | import Mail from 'nodemailer/lib/mailer' 5 | import { Send } from '@/interfaces/send' 6 | import { SendResponse } from '@/interfaces/response' 7 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 8 | import { validate } from '@/utils/validate' 9 | 10 | const Debugger = debug('push:custom-email') 11 | 12 | export type CustomEmailType = 'text' | 'html' 13 | export interface CustomEmailConfig { 14 | /** 15 | * 邮件类型 16 | */ 17 | EMAIL_TYPE: CustomEmailType 18 | /** 19 | * 收件邮箱 20 | */ 21 | EMAIL_TO_ADDRESS: string 22 | /** 23 | * 发件邮箱 24 | */ 25 | EMAIL_AUTH_USER: string 26 | /** 27 | * 发件授权码(或密码) 28 | */ 29 | EMAIL_AUTH_PASS: string 30 | /** 31 | * 发件域名 32 | */ 33 | EMAIL_HOST: string 34 | /** 35 | * 发件端口 36 | */ 37 | EMAIL_PORT: number 38 | } 39 | 40 | export type CustomEmailConfigSchema = ConfigSchema 41 | 42 | export const customEmailConfigSchema: CustomEmailConfigSchema = { 43 | EMAIL_TYPE: { 44 | type: 'select', 45 | title: '邮件类型', 46 | description: '邮件类型', 47 | required: true, 48 | default: 'text', 49 | options: [ 50 | { 51 | label: '文本', 52 | value: 'text', 53 | }, 54 | { 55 | label: 'HTML', 56 | value: 'html', 57 | }, 58 | ], 59 | }, 60 | EMAIL_TO_ADDRESS: { 61 | type: 'string', 62 | title: '收件邮箱', 63 | description: '收件邮箱', 64 | required: true, 65 | default: '', 66 | }, 67 | EMAIL_AUTH_USER: { 68 | type: 'string', 69 | title: '发件邮箱', 70 | description: '发件邮箱', 71 | required: true, 72 | default: '', 73 | }, 74 | EMAIL_AUTH_PASS: { 75 | type: 'string', 76 | title: '发件授权码(或密码)', 77 | description: '发件授权码(或密码)', 78 | required: true, 79 | default: '', 80 | }, 81 | EMAIL_HOST: { 82 | type: 'string', 83 | title: '发件域名', 84 | description: '发件域名', 85 | required: true, 86 | default: '', 87 | }, 88 | EMAIL_PORT: { 89 | type: 'number', 90 | title: '发件端口', 91 | description: '发件端口', 92 | required: true, 93 | default: 465, 94 | }, 95 | } as const 96 | 97 | export type CustomEmailOption = Mail.Options 98 | 99 | type OptionalCustomEmailOption = Pick 100 | 101 | /** 102 | * 由于 CustomEmailOption 的配置太多,所以不提供完整的 Schema,只提供部分配置 schema。 103 | * 如需使用完整的配置,请查看官方文档 104 | */ 105 | export type CustomEmailOptionSchema = OptionSchema<{ 106 | [K in keyof OptionalCustomEmailOption]: string 107 | }> 108 | 109 | export const customEmailOptionSchema: CustomEmailOptionSchema = { 110 | to: { 111 | type: 'string', 112 | title: '收件邮箱', 113 | description: '收件邮箱', 114 | required: false, 115 | default: '', 116 | }, 117 | from: { 118 | type: 'string', 119 | title: '发件邮箱', 120 | description: '发件邮箱', 121 | required: false, 122 | default: '', 123 | }, 124 | subject: { 125 | type: 'string', 126 | title: '邮件主题', 127 | description: '邮件主题', 128 | required: false, 129 | default: '', 130 | }, 131 | text: { 132 | type: 'string', 133 | title: '邮件内容', 134 | description: '邮件内容', 135 | required: false, 136 | default: '', 137 | }, 138 | html: { 139 | type: 'string', 140 | title: '邮件内容', 141 | description: '邮件内容', 142 | required: false, 143 | default: '', 144 | }, 145 | } as const 146 | 147 | /** 148 | * 自定义邮件。官方文档: https://github.com/nodemailer/nodemailer 149 | * 150 | * @author CaoMeiYouRen 151 | * @date 2023-03-12 152 | * @export 153 | * @class CustomEmail 154 | */ 155 | export class CustomEmail implements Send { 156 | // 命名空间 157 | static readonly namespace = '自定义邮件' 158 | 159 | static readonly configSchema = customEmailConfigSchema 160 | 161 | static readonly optionSchema = customEmailOptionSchema 162 | 163 | private config: CustomEmailConfig 164 | 165 | private transporter: nodemailer.Transporter 166 | 167 | constructor(config: CustomEmailConfig) { 168 | this.config = config 169 | Debugger('CustomEmailConfig: %o', config) 170 | // 根据 configSchema 验证 config 171 | validate(config, CustomEmail.configSchema) 172 | const { EMAIL_AUTH_USER, EMAIL_AUTH_PASS, EMAIL_HOST, EMAIL_PORT } = this.config 173 | this.transporter = nodemailer.createTransport({ 174 | host: EMAIL_HOST, 175 | port: Number(EMAIL_PORT), 176 | auth: { 177 | user: EMAIL_AUTH_USER, 178 | pass: EMAIL_AUTH_PASS, 179 | }, 180 | }) 181 | } 182 | 183 | /** 184 | * 释放资源(需要支持 Symbol.dispose) 185 | * 186 | * @author CaoMeiYouRen 187 | * @date 2024-11-08 188 | */ 189 | [Symbol.dispose](): void { 190 | if (this.transporter) { 191 | this.transporter.close() 192 | } 193 | } 194 | 195 | /** 196 | * 197 | * @author CaoMeiYouRen 198 | * @date 2024-11-08 199 | * @param title 消息的标题 200 | * @param [desp] 消息的内容,支持 html 201 | * @param [option] 额外选项 202 | */ 203 | async send(title: string, desp?: string, option?: CustomEmailOption): Promise> { 204 | Debugger('title: "%s", desp: "%s", option: %o', title, desp, option) 205 | const { EMAIL_TYPE, EMAIL_TO_ADDRESS, EMAIL_AUTH_USER } = this.config 206 | if (!await this.transporter.verify()) { 207 | throw new Error('自定义邮件的发件配置无效') 208 | } 209 | const { to: _to, ...args } = option || {} 210 | const from = EMAIL_AUTH_USER 211 | const to = _to || EMAIL_TO_ADDRESS 212 | const type = EMAIL_TYPE 213 | const response = await this.transporter.sendMail({ 214 | from, 215 | to, 216 | subject: title, 217 | [type]: desp, 218 | ...args, 219 | }) 220 | if (typeof Symbol.dispose === 'undefined') { // 如果不支持 Symbol.dispose ,则手动释放 221 | this.transporter.close() 222 | } 223 | Debugger('CustomEmail Response: %o', response) 224 | if (response.response?.includes('250 OK')) { 225 | return { 226 | status: 200, 227 | statusText: 'OK', 228 | data: response, 229 | headers: {}, 230 | } 231 | } 232 | return { 233 | status: 500, 234 | statusText: 'Internal Server Error', 235 | data: response, 236 | headers: {}, 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/push/dingtalk.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios' 2 | import debug from 'debug' 3 | import { Markdown } from './dingtalk/markdown' 4 | import { Text } from './dingtalk/text' 5 | import { Link } from './dingtalk/link' 6 | import { FeedCard } from './dingtalk/feed-card' 7 | import { ActionCard, IndependentJump, OverallJump } from './dingtalk/action-card' 8 | import { Send } from '@/interfaces/send' 9 | import { warn } from '@/utils/helper' 10 | import { ajax } from '@/utils/ajax' 11 | import { generateSignature } from '@/utils/crypto' 12 | import { SendResponse } from '@/interfaces/response' 13 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 14 | import { validate } from '@/utils/validate' 15 | 16 | const Debugger = debug('push:dingtalk') 17 | 18 | export type DingtalkMsgType = 'text' | 'markdown' | 'link' | 'actionCard' | 'feedCard' 19 | 20 | export interface DingtalkConfig { 21 | /** 22 | * 钉钉机器人 access_token。官方文档:https://developers.dingtalk.com/document/app/custom-robot-access 23 | */ 24 | DINGTALK_ACCESS_TOKEN: string 25 | /** 26 | * 加签安全秘钥(HmacSHA256) 27 | */ 28 | DINGTALK_SECRET?: string 29 | } 30 | 31 | export type DingtalkConfigSchema = ConfigSchema 32 | 33 | export const dingtalkConfigSchema: DingtalkConfigSchema = { 34 | DINGTALK_ACCESS_TOKEN: { 35 | type: 'string', 36 | title: '钉钉机器人 access_token', 37 | description: '钉钉机器人 access_token', 38 | required: true, 39 | default: '', 40 | }, 41 | DINGTALK_SECRET: { 42 | type: 'string', 43 | title: '加签安全秘钥(HmacSHA256)', 44 | required: false, 45 | default: '', 46 | }, 47 | } as const 48 | 49 | export type DingtalkOption = Partial<(Text | Markdown | Link | FeedCard | ActionCard)> 50 | 51 | type TempDingtalkOption = { 52 | msgtype?: DingtalkOption['msgtype'] 53 | text?: Partial 54 | markdown?: Partial 55 | link?: Partial 56 | actionCard?: Partial<{ 57 | // 首屏会话透出的展示内容 58 | title: string 59 | // markdown 格式的消息内容 60 | text: string 61 | // 0:按钮竖直排列;1:按钮横向排列 62 | btnOrientation?: '0' | '1' 63 | }> & Partial & Partial 64 | feedCard?: Partial 65 | 66 | at?: Text['at'] 67 | [key: string]: any 68 | } 69 | 70 | export type DingtalkOptionSchema = OptionSchema 71 | 72 | export const dingtalkOptionSchema: DingtalkOptionSchema = { 73 | msgtype: { 74 | type: 'select', 75 | title: '消息类型', 76 | description: '消息类型', 77 | required: false, 78 | default: 'text', 79 | options: [ 80 | { 81 | label: '文本', 82 | value: 'text', 83 | }, 84 | { 85 | label: 'Markdown', 86 | value: 'markdown', 87 | }, 88 | { 89 | label: '链接', 90 | value: 'link', 91 | }, 92 | { 93 | label: '按钮', 94 | value: 'actionCard', 95 | }, 96 | { 97 | label: 'FeedCard', 98 | value: 'feedCard', 99 | }, 100 | ], 101 | }, 102 | text: { 103 | type: 'object', 104 | title: '文本', 105 | description: '文本', 106 | required: false, 107 | default: {}, 108 | }, 109 | markdown: { 110 | type: 'object', 111 | title: 'Markdown', 112 | description: 'Markdown', 113 | required: false, 114 | default: {}, 115 | }, 116 | link: { 117 | type: 'object', 118 | title: '链接', 119 | description: '链接', 120 | required: false, 121 | default: {}, 122 | }, 123 | actionCard: { 124 | type: 'object', 125 | title: '动作卡片', 126 | description: '动作卡片', 127 | required: false, 128 | default: {}, 129 | }, 130 | feedCard: { 131 | type: 'object', 132 | title: '订阅卡片', 133 | description: '订阅卡片', 134 | required: false, 135 | default: {}, 136 | }, 137 | } as const 138 | 139 | export interface DingtalkResponse { 140 | errcode: number 141 | errmsg: string 142 | } 143 | 144 | /** 145 | * 钉钉机器人推送 146 | * 在 [dingtalk-robot-sdk](https://github.com/ineo6/dingtalk-robot-sdk) 的基础上重构了一下,用法几乎完全一致。 147 | * @author CaoMeiYouRen 148 | * @date 2021-02-27 149 | * @export 150 | * @class Dingtalk 151 | */ 152 | export class Dingtalk implements Send { 153 | 154 | static readonly namespace = '钉钉' 155 | 156 | static readonly configSchema = dingtalkConfigSchema 157 | 158 | static readonly optionSchema = dingtalkOptionSchema 159 | 160 | private ACCESS_TOKEN: string 161 | /** 162 | * 加签安全秘钥(HmacSHA256) 163 | * 164 | * @private 165 | */ 166 | private SECRET?: string 167 | private webhook: string = 'https://oapi.dingtalk.com/robot/send' 168 | 169 | /** 170 | * 参考文档 [钉钉开放平台 - 自定义机器人接入](https://developers.dingtalk.com/document/app/custom-robot-access) 171 | * @author CaoMeiYouRen 172 | * @date 2024-11-08 173 | * @param config 174 | */ 175 | constructor(config: DingtalkConfig) { 176 | const { DINGTALK_ACCESS_TOKEN, DINGTALK_SECRET } = config 177 | this.ACCESS_TOKEN = DINGTALK_ACCESS_TOKEN 178 | this.SECRET = DINGTALK_SECRET 179 | Debugger('DINGTALK_ACCESS_TOKEN: %s , DINGTALK_SECRET: %s', this.ACCESS_TOKEN, this.SECRET) 180 | // 根据 configSchema 验证 config 181 | validate(config, Dingtalk.configSchema) 182 | if (!this.SECRET) { 183 | warn('未提供 DINGTALK_SECRET !') 184 | } 185 | } 186 | 187 | private getSign(timeStamp: number): string { 188 | let signStr = '' 189 | if (this.SECRET) { 190 | signStr = generateSignature(timeStamp, this.SECRET, this.SECRET) 191 | Debugger('Sign string is %s, result is %s', `${timeStamp}\n${this.SECRET}`, signStr) 192 | } 193 | return signStr 194 | } 195 | 196 | private async push(data: DingtalkOption): Promise> { 197 | const timestamp = Date.now() 198 | const sign = this.getSign(timestamp) 199 | const result = await ajax({ 200 | url: this.webhook, 201 | method: 'POST', 202 | headers: { 203 | 'Content-Type': 'application/json', 204 | }, 205 | query: { 206 | timestamp, 207 | sign, 208 | access_token: this.ACCESS_TOKEN, 209 | }, 210 | data, 211 | }) 212 | Debugger('Result is %s, %s。', result.data.errcode, result.data.errmsg) 213 | if (result.data.errcode === 310000) { 214 | console.error('Send Failed:', result.data) 215 | Debugger('Please check safe config : %O', result.data) 216 | } 217 | return result 218 | } 219 | 220 | /** 221 | * 222 | * 223 | * @author CaoMeiYouRen 224 | * @date 2024-11-08 225 | * @param title 消息的标题 226 | * @param [desp] 消息的内容,支持 Markdown 227 | * @returns 228 | */ 229 | async send(title: string, desp?: string, option?: DingtalkOption): Promise> { 230 | Debugger('title: "%s", desp: "%s", option: %O', title, desp, option) 231 | switch (option.msgtype) { 232 | case 'text': 233 | return this.push({ 234 | msgtype: 'text', 235 | text: { 236 | content: `${title}${desp ? `\n${desp}` : ''}`, 237 | }, 238 | ...option, 239 | }) 240 | case 'markdown': 241 | return this.push({ 242 | msgtype: 'markdown', 243 | markdown: { 244 | title, 245 | text: `# ${title}${desp ? `\n\n${desp}` : ''}`, 246 | }, 247 | ...option, 248 | }) 249 | case 'link': 250 | return this.push({ 251 | msgtype: 'link', 252 | link: { 253 | title, 254 | text: desp || '', 255 | picUrl: option?.link?.picUrl || '', 256 | messageUrl: option.link?.messageUrl || '', 257 | }, 258 | ...option, 259 | }) 260 | case 'actionCard': 261 | return this.push({ 262 | msgtype: 'actionCard', 263 | actionCard: { 264 | title, 265 | text: desp || '', 266 | btnOrientation: option?.actionCard?.btnOrientation || '0', 267 | btns: (option?.actionCard as any)?.btns, 268 | singleTitle: (option?.actionCard as any)?.singleTitle, 269 | singleURL: (option?.actionCard as any)?.singleURL, 270 | }, 271 | ...option, 272 | }) 273 | case 'feedCard': 274 | return this.push({ 275 | msgtype: 'feedCard', 276 | feedCard: { 277 | links: option?.feedCard?.links || [], 278 | }, 279 | ...option, 280 | }) 281 | default: 282 | throw new Error('msgtype is required!') 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/push/dingtalk/action-card.ts: -------------------------------------------------------------------------------- 1 | // 整体跳转 2 | export type OverallJump = { 3 | // 单个按钮的标题。设置此项和 singleURL 后,btns无效。 4 | singleTitle: string 5 | // 点击消息跳转的URL 6 | singleURL: string 7 | } 8 | 9 | // 独立跳转 10 | export type IndependentJump = { 11 | btns: { 12 | // 按钮的标题 13 | title: string 14 | // 点击按钮触发的URL 15 | actionURL: string 16 | }[] 17 | } 18 | 19 | /** 20 | * 动作卡片消息 21 | * 22 | * @author CaoMeiYouRen 23 | * @date 2024-11-09 24 | * @export 25 | * @interface ActionCard 26 | */ 27 | export interface ActionCard { 28 | msgtype: 'actionCard' 29 | actionCard: { 30 | // 首屏会话透出的展示内容 31 | title: string 32 | // markdown 格式的消息内容 33 | text: string 34 | // 0:按钮竖直排列;1:按钮横向排列 35 | btnOrientation?: '0' | '1' 36 | } & (OverallJump | IndependentJump) 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/push/dingtalk/feed-card.ts: -------------------------------------------------------------------------------- 1 | export interface FeedCardLink { 2 | title: string 3 | messageURL: string 4 | picURL: string 5 | } 6 | /** 7 | * 订阅卡片消息 8 | * 9 | * @author CaoMeiYouRen 10 | * @date 2024-11-09 11 | * @export 12 | * @interface FeedCard 13 | */ 14 | export interface FeedCard { 15 | msgtype: 'feedCard' 16 | feedCard: { 17 | links: FeedCardLink[] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/push/dingtalk/link.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 链接消息 3 | * 4 | * @author CaoMeiYouRen 5 | * @date 2024-11-09 6 | * @export 7 | * @interface Link 8 | */ 9 | export interface Link { 10 | msgtype: 'link' 11 | link: { 12 | text: string 13 | title: string 14 | picUrl?: string 15 | messageUrl: string 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/push/dingtalk/markdown.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 钉钉 markdown 消息 3 | * 4 | * @author CaoMeiYouRen 5 | * @date 2024-11-09 6 | * @export 7 | * @interface Markdown 8 | */ 9 | export interface Markdown { 10 | msgtype: 'markdown' 11 | markdown: { 12 | title: string 13 | text: string 14 | } 15 | at?: { 16 | atMobiles?: string[] 17 | atUserIds?: string[] 18 | isAtAll?: boolean 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/push/dingtalk/text.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 文本消息 3 | * 4 | * @author CaoMeiYouRen 5 | * @date 2024-11-09 6 | * @export 7 | * @interface Text 8 | */ 9 | export interface Text { 10 | msgtype: 'text' 11 | text: { 12 | content: string 13 | } 14 | at?: { 15 | atMobiles?: string[] 16 | atUserIds?: string[] 17 | isAtAll?: boolean 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/push/discord.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | 8 | const Debugger = debug('push:discord') 9 | 10 | export interface DiscordConfig { 11 | /** 12 | * Webhook Url 可在服务器设置 -> 整合 -> Webhook -> 创建 Webhook 中获取 13 | */ 14 | DISCORD_WEBHOOK: string 15 | 16 | /** 17 | * 代理地址 18 | */ 19 | PROXY_URL?: string 20 | } 21 | 22 | export type DiscordConfigSchema = ConfigSchema 23 | 24 | export const discordConfigSchema: DiscordConfigSchema = { 25 | DISCORD_WEBHOOK: { 26 | type: 'string', 27 | title: 'Webhook Url', 28 | description: 'Webhook Url 可在服务器设置 -> 整合 -> Webhook -> 创建 Webhook 中获取', 29 | required: true, 30 | }, 31 | PROXY_URL: { 32 | type: 'string', 33 | title: '代理地址', 34 | description: '代理地址', 35 | required: false, 36 | }, 37 | } as const 38 | 39 | /** 40 | * Discord 额外选项 41 | * 由于参数过多,因此请参考官方文档进行配置 42 | */ 43 | export type DiscordOption = { 44 | /** 45 | * 机器人显示的名称 46 | */ 47 | username?: string 48 | /** 49 | * 机器人头像的 Url 50 | */ 51 | avatar_url?: string 52 | [key: string]: any 53 | } 54 | 55 | export type DiscordOptionSchema = OptionSchema 56 | 57 | export const discordOptionSchema: DiscordOptionSchema = { 58 | username: { 59 | type: 'string', 60 | title: '机器人显示的名称', 61 | description: '机器人显示的名称', 62 | required: false, 63 | }, 64 | avatar_url: { 65 | type: 'string', 66 | title: '机器人头像的 Url', 67 | description: '机器人头像的 Url', 68 | required: false, 69 | }, 70 | } as const 71 | 72 | export interface DiscordResponse { } 73 | 74 | /** 75 | * Discord Webhook 推送 76 | * 77 | * @author CaoMeiYouRen 78 | * @date 2023-09-17 79 | * @export 80 | * @class Discord 81 | */ 82 | export class Discord implements Send { 83 | 84 | static readonly namespace = 'Discord' 85 | static readonly configSchema = discordConfigSchema 86 | static readonly optionSchema = discordOptionSchema 87 | 88 | /** 89 | * Webhook Url 可在服务器设置 -> 整合 -> Webhook -> 创建 Webhook 中获取 90 | * 91 | * @author CaoMeiYouRen 92 | * @date 2023-09-17 93 | * @private 94 | */ 95 | private DISCORD_WEBHOOK: string 96 | 97 | proxyUrl: string 98 | 99 | /** 100 | * 创建 Discord 实例 101 | * @author CaoMeiYouRen 102 | * @date 2024-11-08 103 | * @param config 配置 104 | */ 105 | constructor(config: DiscordConfig) { 106 | const { DISCORD_WEBHOOK, PROXY_URL } = config 107 | Debugger('DISCORD_WEBHOOK: %s, PROXY_URL: %s', DISCORD_WEBHOOK, PROXY_URL) 108 | this.DISCORD_WEBHOOK = DISCORD_WEBHOOK 109 | if (PROXY_URL) { 110 | this.proxyUrl = PROXY_URL 111 | } 112 | // 根据 configSchema 验证 config 113 | validate(config, Discord.configSchema) 114 | } 115 | 116 | /** 117 | * 发送消息 118 | * 119 | * @author CaoMeiYouRen 120 | * @date 2024-11-08 121 | * @param title 消息的标题 122 | * @param [desp] 消息的描述。最多 2000 个字符 123 | * @param [option] 额外选项 124 | */ 125 | async send(title: string, desp?: string, option?: DiscordOption): Promise> { 126 | Debugger('title: "%s", desp: "%s", option: %o', title, desp, option) 127 | const { username, avatar_url, ...args } = option || {} 128 | const proxyUrl = this.proxyUrl 129 | const content = `${title}${desp ? `\n${desp}` : ''}` 130 | return ajax({ 131 | url: this.DISCORD_WEBHOOK, 132 | method: 'POST', 133 | proxyUrl, 134 | data: { 135 | username, 136 | content, 137 | avatar_url, 138 | ...args, 139 | }, 140 | }) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/push/feishu.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | 8 | const Debugger = debug('push:feishu') 9 | 10 | export interface FeishuConfig { 11 | /** 12 | * 飞书应用 ID。官方文档:https://open.feishu.cn/document/server-docs/api-call-guide/terminology#b047be0c 13 | */ 14 | FEISHU_APP_ID: string 15 | /** 16 | * 飞书应用密钥。官方文档:https://open.feishu.cn/document/server-docs/api-call-guide/terminology#1b5fb6cd 17 | */ 18 | FEISHU_APP_SECRET: string 19 | } 20 | 21 | export type FeishuConfigSchema = ConfigSchema 22 | 23 | export const feishuConfigSchema: FeishuConfigSchema = { 24 | FEISHU_APP_ID: { 25 | type: 'string', 26 | title: '飞书应用 ID', 27 | description: '飞书应用 ID', 28 | required: true, 29 | default: '', 30 | }, 31 | FEISHU_APP_SECRET: { 32 | type: 'string', 33 | title: '飞书应用密钥', 34 | description: '飞书应用密钥', 35 | required: true, 36 | default: '', 37 | }, 38 | } 39 | 40 | export type FeishuOption = { 41 | // 用户 ID 类型 42 | receive_id_type: 'open_id' | 'union_id' | 'user_id' | 'email' | 'chat_id' 43 | // 消息接收者的 ID,ID 类型与查询参数 receive_id_type 的取值一致。 44 | receive_id: string 45 | // 消息类型。 46 | msg_type: 'text' | 'post' | 'image' | 'file' | 'audio' | 'media' | 'sticker' | 'interactive' | 'share_chat' | 'share_user' | 'system' 47 | // 消息内容,JSON 结构序列化后的字符串。该参数的取值与 msg_type 对应,例如 msg_type 取值为 text,则该参数需要传入文本类型的内容。 48 | content?: string 49 | // 自定义设置的唯一字符串序列,用于在发送消息时请求去重。持有相同 uuid 的请求,在 1 小时内至多成功发送一条消息。 50 | uuid?: string 51 | } 52 | 53 | export type FeishuOptionSchema = OptionSchema 54 | 55 | export const feishuOptionSchema: FeishuOptionSchema = { 56 | receive_id_type: { 57 | type: 'select', 58 | title: '用户 ID 类型', 59 | description: '用户 ID 类型', 60 | required: true, 61 | options: [ 62 | { 63 | label: 'open_id', 64 | value: 'open_id', 65 | }, 66 | { 67 | label: 'union_id', 68 | value: 'union_id', 69 | }, 70 | { 71 | label: 'user_id', 72 | value: 'user_id', 73 | }, 74 | { 75 | label: 'email', 76 | value: 'email', 77 | }, 78 | { 79 | label: 'chat_id', 80 | value: 'chat_id', 81 | }, 82 | ], 83 | }, 84 | receive_id: { 85 | type: 'string', 86 | title: '消息接收者的 ID', 87 | description: '消息接收者的 ID,ID 类型与查询参数 receive_id_type 的取值一致。', 88 | required: true, 89 | }, 90 | msg_type: { 91 | type: 'select', 92 | title: '消息类型', 93 | description: '消息类型', 94 | required: true, 95 | options: [ 96 | { 97 | label: '文本', 98 | value: 'text', 99 | }, 100 | { 101 | label: '富文本', 102 | value: 'post', 103 | }, 104 | { 105 | label: '图片', 106 | value: 'image', 107 | }, 108 | { 109 | label: '文件', 110 | value: 'file', 111 | }, 112 | { 113 | label: '语音', 114 | value: 'audio', 115 | }, 116 | { 117 | label: '视频', 118 | value: 'media', 119 | }, 120 | { 121 | label: '表情包', 122 | value: 'sticker', 123 | }, 124 | { 125 | label: '卡片', 126 | value: 'interactive', 127 | }, 128 | { 129 | label: '分享群名片', 130 | value: 'share_chat', 131 | }, 132 | { 133 | label: '分享个人名片', 134 | value: 'share_user', 135 | }, 136 | { 137 | label: '系统消息', 138 | value: 'system', 139 | }, 140 | ], 141 | }, 142 | content: { 143 | type: 'string', 144 | title: '消息内容', 145 | description: '消息内容,JSON 结构序列化后的字符串。该参数的取值与 msg_type 对应,例如 msg_type 取值为 text,则该参数需要传入文本类型的内容。', 146 | required: false, 147 | }, 148 | uuid: { 149 | type: 'string', 150 | title: '自定义设置的唯一字符串序列', 151 | description: '自定义设置的唯一字符串序列,用于在发送消息时请求去重。持有相同 uuid 的请求,在 1 小时内至多成功发送一条消息。', 152 | required: false, 153 | }, 154 | } 155 | 156 | /** 157 | * 飞书。官方文档:https://open.feishu.cn/document/home/index 158 | * 159 | * @author CaoMeiYouRen 160 | * @date 2025-02-10 161 | * @export 162 | * @class Feishu 163 | */ 164 | export class Feishu implements Send { 165 | 166 | static readonly namespace = '飞书' 167 | 168 | static readonly configSchema = feishuConfigSchema 169 | 170 | static readonly optionSchema = feishuOptionSchema 171 | 172 | private readonly config: FeishuConfig 173 | 174 | /** 175 | * accessToken 的过期时间(时间戳) 176 | */ 177 | private expiresTime: number 178 | 179 | private accessToken: string 180 | 181 | constructor(config: FeishuConfig) { 182 | this.config = config 183 | // 根据 configSchema 验证 config 184 | validate(config, Feishu.configSchema) 185 | } 186 | 187 | private async getAccessToken() { 188 | const { FEISHU_APP_ID, FEISHU_APP_SECRET } = this.config 189 | const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal' 190 | const data = { 191 | app_id: FEISHU_APP_ID, 192 | app_secret: FEISHU_APP_SECRET, 193 | } 194 | const result = await ajax({ 195 | url, 196 | method: 'POST', 197 | headers: { 198 | 'Content-Type': 'application/json; charset=utf-8', 199 | }, 200 | data, 201 | }) 202 | const { code, msg, tenant_access_token, expire } = result.data 203 | if (code !== 0) { // 出错返回码,为0表示成功,非0表示调用失败 204 | throw new Error(msg || '获取 tenant_access_token 失败!') 205 | } 206 | this.expiresTime = Date.now() + expire * 1000 207 | Debugger('获取 tenant_access_token 成功: %s', tenant_access_token) 208 | return tenant_access_token as string 209 | } 210 | 211 | async send(title: string, desp?: string, option?: FeishuOption): Promise { 212 | Debugger('title: "%s", desp: "%s", option: %O', title, desp, option) 213 | if (!this.accessToken || Date.now() >= this.expiresTime) { 214 | this.accessToken = await this.getAccessToken() 215 | } 216 | const { receive_id_type = 'open_id', receive_id, msg_type = 'text', content, uuid } = option 217 | const data = { receive_id, msg_type, content, uuid } 218 | if (!data.content) { 219 | switch (msg_type) { 220 | case 'text': 221 | data.content = JSON.stringify({ 222 | text: `${title}${desp ? `\n${desp}` : ''}`, 223 | }) 224 | break 225 | case 'post': 226 | data.content = JSON.stringify({ 227 | post: { 228 | zh_cn: { 229 | title, 230 | content: [ 231 | [ 232 | { 233 | tag: 'text', 234 | text: desp, 235 | }, 236 | ], 237 | ], 238 | }, 239 | }, 240 | }) 241 | break 242 | default: 243 | throw new Error('msg_type is required!') 244 | } 245 | } 246 | const result = await ajax({ 247 | url: 'https://open.feishu.cn/open-apis/im/v1/messages', 248 | method: 'POST', 249 | headers: { 250 | 'Content-Type': 'application/json; charset=utf-8', 251 | Authorization: `Bearer ${this.accessToken}`, 252 | }, 253 | data, 254 | query: { 255 | receive_id_type: receive_id_type || 'open_id', 256 | }, 257 | }) 258 | return result 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/push/i-got.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | 8 | const Debugger = debug('push:i-got') 9 | 10 | export interface IGotConfig { 11 | /** 12 | * 微信搜索小程序“iGot”获取推送key 13 | */ 14 | I_GOT_KEY: string 15 | } 16 | 17 | export type IGotConfigSchema = ConfigSchema 18 | 19 | export const iGotConfigSchema: IGotConfigSchema = { 20 | I_GOT_KEY: { 21 | type: 'string', 22 | title: 'iGot 推送key', 23 | description: 'iGot 推送key', 24 | required: true, 25 | default: '', 26 | }, 27 | } as const 28 | 29 | export interface IGotOption { 30 | /** 31 | * 链接; 点开消息后会主动跳转至此地址 32 | */ 33 | url?: string 34 | /** 35 | * 是否自动复制; 为1自动复制 36 | */ 37 | automaticallyCopy?: number 38 | /** 39 | * 紧急消息,为1表示紧急。此消息将置顶在小程序内, 同时会在推送的消息内做一定的特殊标识 40 | */ 41 | urgent?: number 42 | /** 43 | * 需要自动复制的文本内容 44 | */ 45 | copy?: string 46 | /** 47 | * 主题; 订阅链接下有效;对推送内容分类,用户可选择性订阅 48 | */ 49 | topic?: string 50 | [key: string]: any 51 | } 52 | 53 | export type IGotOptionSchema = OptionSchema 54 | 55 | export const iGotOptionSchema: IGotOptionSchema = { 56 | url: { 57 | type: 'string', 58 | title: '链接', 59 | description: '链接; 点开消息后会主动跳转至此地址', 60 | required: false, 61 | default: '', 62 | }, 63 | automaticallyCopy: { 64 | type: 'number', 65 | title: '是否自动复制', 66 | description: '是否自动复制; 为1自动复制', 67 | required: false, 68 | default: 0, 69 | }, 70 | urgent: { 71 | type: 'number', 72 | title: '紧急消息', 73 | description: '紧急消息,为1表示紧急。此消息将置顶在小程序内, 同时会在推送的消息内做一定的特殊标识', 74 | required: false, 75 | default: 0, 76 | }, 77 | copy: { 78 | type: 'string', 79 | title: '需要自动复制的文本内容', 80 | description: '需要自动复制的文本内容', 81 | required: false, 82 | default: '', 83 | }, 84 | topic: { 85 | type: 'string', 86 | title: '主题', 87 | description: '主题; 订阅链接下有效;对推送内容分类,用户可选择性订阅', 88 | required: false, 89 | default: '', 90 | }, 91 | } as const 92 | 93 | export interface IGotResponse { 94 | /** 95 | * 状态码; 0为正常 96 | */ 97 | ret: number 98 | /** 99 | * 响应结果 100 | */ 101 | data: { 102 | /** 103 | * 消息记录,后期开放其他接口用 104 | * */ 105 | id: string 106 | } 107 | /** 108 | * 结果描述 109 | */ 110 | errMsg: string 111 | } 112 | 113 | /** 114 | * iGot 推送,官方文档:http://hellyw.com 115 | * 116 | * @author CaoMeiYouRen 117 | * @date 2021-03-03 118 | * @export 119 | * @class IGot 120 | */ 121 | export class IGot implements Send { 122 | static readonly namespace = 'iGot' 123 | static readonly configSchema = iGotConfigSchema 124 | static readonly optionSchema = iGotOptionSchema 125 | /** 126 | * 微信搜索小程序“iGot”获取推送key 127 | * 128 | * @private 129 | */ 130 | private I_GOT_KEY: string 131 | /** 132 | * @author CaoMeiYouRen 133 | * @date 2024-11-08 134 | * @param config 微信搜索小程序“iGot”获取推送key 135 | */ 136 | constructor(config: IGotConfig) { 137 | const { I_GOT_KEY } = config 138 | this.I_GOT_KEY = I_GOT_KEY 139 | Debugger('set I_GOT_KEY: "%s"', I_GOT_KEY) 140 | // 根据 configSchema 验证 config 141 | validate(config, IGot.configSchema) 142 | } 143 | /** 144 | * 145 | * 146 | * @author CaoMeiYouRen 147 | * @date 2024-11-08 148 | * @param title 消息标题 149 | * @param [desp] 消息正文 150 | * @param [option] 额外选项 151 | * @returns 152 | */ 153 | send(title: string, desp?: string, option?: IGotOption): Promise> { 154 | Debugger('title: "%s", desp: "%s", option: "%o"', title, desp, option) 155 | return ajax({ 156 | url: `https://push.hellyw.com/${this.I_GOT_KEY}`, 157 | method: 'POST', 158 | headers: { 159 | 'Content-Type': 'application/json', 160 | }, 161 | data: { 162 | title, 163 | content: desp || title, 164 | automaticallyCopy: 0, // 关闭自动复制 165 | ...option, 166 | }, 167 | }) 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/push/ntfy.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | import { rfc2047Encode } from '@/utils/crypto' 8 | 9 | const Debugger = debug('push:ntfy') 10 | 11 | export interface NtfyConfig { 12 | /** 13 | * 推送地址 14 | */ 15 | NTFY_URL: string 16 | 17 | /** 18 | * 主题 19 | * 用于区分不同的推送目标。 20 | * 主题本质上是一个密码,所以请选择不容易猜到的东西。 21 | * 例如:`my-topic` 22 | */ 23 | NTFY_TOPIC: string 24 | 25 | /** 26 | * 认证参数。 27 | * 支持 Basic Auth、Bearer Token。 28 | * Basic Auth 示例:"Basic dGVzdDpwYXNz" 29 | * Bearer Token 示例:"Bearer tk_..." 30 | */ 31 | NTFY_AUTH?: string 32 | } 33 | 34 | export type NtfyConfigSchema = ConfigSchema 35 | export const ntfyConfigSchema: NtfyConfigSchema = { 36 | NTFY_URL: { 37 | type: 'string', 38 | title: '推送地址', 39 | description: '推送地址', 40 | required: true, 41 | default: '', 42 | }, 43 | NTFY_TOPIC: { 44 | type: 'string', 45 | title: '主题', 46 | description: '主题', 47 | required: true, 48 | default: '', 49 | }, 50 | NTFY_AUTH: { 51 | type: 'string', 52 | title: '认证参数', 53 | description: '支持 Basic Auth、Bearer Token。\n' + 54 | 'Basic Auth 示例:"Basic dGVzdDpwYXNz"\n' + 55 | 'Bearer Token 示例:"Bearer tk_..."', 56 | required: false, 57 | default: '', 58 | }, 59 | } as const 60 | 61 | export interface NtfyOption { 62 | /** 63 | * 通知中显示的标题 64 | */ 65 | title?: string 66 | /** 67 | * 通知中显示的消息正文 68 | */ 69 | message?: string 70 | /** 71 | * 消息正文 72 | */ 73 | body?: string 74 | /** 75 | * 消息优先级(1-5,1最低,5最高) 76 | */ 77 | priority?: number 78 | /** 79 | * 标签列表(逗号分隔),支持Emoji短代码 80 | */ 81 | tags?: string 82 | /** 83 | * 启用Markdown格式化(设为`true`或`yes`) 84 | */ 85 | markdown?: boolean 86 | /** 87 | * 延迟发送时间(支持时间戳、自然语言如`tomorrow 10am`) 88 | */ 89 | delay?: string 90 | /** 91 | * 点击通知时打开的URL 92 | */ 93 | click?: string 94 | /** 95 | * 附加文件的URL 96 | */ 97 | attach?: string 98 | /** 99 | * 附件的显示文件名 100 | */ 101 | filename?: string 102 | /** 103 | * 通知图标的URL(仅支持JPEG/PNG) 104 | */ 105 | icon?: string 106 | /** 107 | * 定义通知的操作按钮(JSON或简写格式) 108 | */ 109 | actions?: string 110 | /** 111 | * 设为`no`禁止服务器缓存消息 112 | */ 113 | cache?: boolean 114 | /** 115 | * 设为`no`禁止转发到Firebase(仅影响Android推送) 116 | */ 117 | firebase?: boolean 118 | /** 119 | * 设为`1`启用UnifiedPush模式(用于Matrix网关) 120 | */ 121 | unifiedPush?: boolean 122 | /** 123 | * 将通知转发到指定邮箱 124 | */ 125 | email?: string 126 | /** 127 | * 发送语音呼叫(需验证手机号,仅限认证用户) 128 | */ 129 | call?: string 130 | /** 131 | * 设为`text/markdown`启用Markdown 132 | */ 133 | contentType?: string 134 | /** 135 | * 直接上传文件作为附件(需设置`X-Filename`) 136 | */ 137 | file?: File 138 | } 139 | 140 | export type NtfyOptionSchema = OptionSchema 141 | 142 | export const ntfyOptionSchema: NtfyOptionSchema = { 143 | title: { 144 | type: 'string', 145 | title: '标题', 146 | description: '标题', 147 | required: false, 148 | default: '', 149 | }, 150 | body: { 151 | type: 'string', 152 | title: '消息正文', 153 | description: '消息正文', 154 | required: false, 155 | default: '', 156 | }, 157 | priority: { 158 | type: 'number', 159 | title: '消息优先级', 160 | description: '消息优先级(1-5,1最低,5最高)', 161 | required: false, 162 | default: 3, 163 | }, 164 | tags: { 165 | type: 'string', 166 | title: '标签列表', 167 | description: '标签列表(逗号分隔),支持Emoji短代码', 168 | required: false, 169 | default: '', 170 | }, 171 | markdown: { 172 | type: 'boolean', 173 | title: '启用Markdown格式', 174 | description: '启用Markdown格式(设为`true`或`yes`)', 175 | required: false, 176 | default: false, 177 | }, 178 | delay: { 179 | type: 'string', 180 | title: '延迟发送时间', 181 | description: '延迟发送时间(支持时间戳、自然语言如`tomorrow 10am`)', 182 | required: false, 183 | default: '', 184 | }, 185 | click: { 186 | type: 'string', 187 | title: '点击通知时打开的URL', 188 | description: '点击通知时打开的URL', 189 | required: false, 190 | default: '', 191 | }, 192 | attach: { 193 | type: 'string', 194 | title: '附加文件的URL', 195 | description: '附加文件的URL', 196 | required: false, 197 | default: '', 198 | }, 199 | filename: { 200 | type: 'string', 201 | title: '附件的显示文件名', 202 | description: '附件的显示文件名', 203 | required: false, 204 | default: '', 205 | }, 206 | icon: { 207 | type: 'string', 208 | title: '通知图标的URL', 209 | description: '通知图标的URL(仅支持JPEG/PNG)', 210 | required: false, 211 | default: '', 212 | }, 213 | actions: { 214 | type: 'string', 215 | title: '定义通知的操作按钮', 216 | description: '定义通知的操作按钮(JSON或简写格式)', 217 | required: false, 218 | default: '', 219 | }, 220 | cache: { 221 | type: 'boolean', 222 | title: '禁止服务器缓存消息', 223 | description: '设为`no`禁止服务器缓存消息', 224 | required: false, 225 | default: false, 226 | }, 227 | firebase: { 228 | type: 'boolean', 229 | title: '禁止转发到Firebase', 230 | description: '设为`no`禁止转发到Firebase(仅影响Android推送)', 231 | required: false, 232 | default: false, 233 | }, 234 | unifiedPush: { 235 | type: 'boolean', 236 | title: '启用UnifiedPush模式', 237 | description: '设为`1`启用UnifiedPush模式(用于Matrix网关)', 238 | required: false, 239 | default: false, 240 | }, 241 | email: { 242 | type: 'string', 243 | title: '邮箱', 244 | description: '将通知转发到指定邮箱', 245 | required: false, 246 | default: '', 247 | }, 248 | call: { 249 | type: 'string', 250 | title: '发送语音呼叫', 251 | description: '发送语音呼叫(需验证手机号,仅限认证用户)', 252 | required: false, 253 | default: '', 254 | }, 255 | contentType: { 256 | type: 'string', 257 | title: '编码格式', 258 | description: '设为`text/markdown`启用Markdown', 259 | required: false, 260 | default: '', 261 | }, 262 | file: { 263 | type: 'object', 264 | title: '附件', 265 | description: '直接上传文件作为附件(需设置`X-Filename`)', 266 | required: false, 267 | }, 268 | } as const 269 | 270 | export interface NtfyResponse { 271 | /** 272 | * 消息ID 273 | */ 274 | id: string 275 | /** 276 | * 消息发布时间(Unix时间戳) 277 | */ 278 | time: number 279 | /** 280 | * 消息过期时间(Unix时间戳) 281 | */ 282 | expires: number 283 | /** 284 | * 事件类型 285 | */ 286 | event: string 287 | /** 288 | * 主题 289 | */ 290 | topic: string 291 | /** 292 | * 消息内容 293 | */ 294 | message: string 295 | } 296 | 297 | /** 298 | * ntfy推送。 299 | * 官方文档:https://ntfy.sh/docs/publish/ 300 | * 301 | * @author CaoMeiYouRen 302 | * @date 2025-02-11 303 | * @export 304 | * @class Ntfy 305 | */ 306 | export class Ntfy implements Send { 307 | static readonly namespace = 'ntfy' 308 | static readonly configSchema = ntfyConfigSchema 309 | static readonly optionSchema = ntfyOptionSchema 310 | /** 311 | * 推送地址 312 | */ 313 | private NTFY_URL: string 314 | /** 315 | * 认证参数。 316 | * 支持 Basic Auth、Bearer Token。 317 | * Basic Auth 示例:"Basic dGVzdDpwYXNz" 318 | * Bearer Token 示例:"Bearer tk_..." 319 | */ 320 | private NTFY_AUTH?: string 321 | 322 | /** 323 | * 主题 324 | * 用于区分不同的推送目标。 325 | * 主题本质上是一个密码,所以请选择不容易猜到的东西。 326 | * 例如:`my-topic` 327 | */ 328 | private NTFY_TOPIC: string 329 | 330 | constructor(config: NtfyConfig) { 331 | const { NTFY_URL, NTFY_AUTH, NTFY_TOPIC } = config 332 | this.NTFY_URL = NTFY_URL 333 | this.NTFY_TOPIC = NTFY_TOPIC 334 | this.NTFY_AUTH = NTFY_AUTH 335 | Debugger('set NTFY_URL: "%s", NTFY_TOPIC: "%s", NTFY_AUTH: "%s"', NTFY_URL, NTFY_TOPIC, NTFY_AUTH) 336 | // 根据 configSchema 验证 config 337 | validate(config, Ntfy.configSchema) 338 | } 339 | 340 | async send(title: string, desp: string, option?: NtfyOption): Promise> { 341 | Debugger('option: "%o"', option) 342 | const { message, body, priority, tags, markdown, delay, click, attach, filename, icon, actions, cache, firebase, unifiedPush, email, call, contentType, file } = option || {} 343 | const headers: any = {} 344 | if (this.NTFY_AUTH) { 345 | headers['Authorization'] = this.NTFY_AUTH 346 | } 347 | if (contentType) { 348 | headers['Content-Type'] = contentType 349 | } 350 | const xTitle = title || option.title 351 | if (xTitle) { 352 | headers['X-Title'] = rfc2047Encode(xTitle) 353 | } 354 | if (message) { 355 | headers['X-Message'] = rfc2047Encode(message) 356 | } 357 | if (priority) { 358 | headers['X-Priority'] = priority.toString() 359 | } 360 | if (tags) { 361 | headers['X-Tags'] = tags 362 | } 363 | if (markdown) { 364 | headers['X-Markdown'] = markdown.toString() 365 | } 366 | if (delay) { 367 | headers['X-Delay'] = delay 368 | } 369 | if (click) { 370 | headers['X-Click'] = click 371 | } 372 | if (attach) { 373 | headers['X-Attach'] = attach 374 | } 375 | if (filename) { 376 | headers['X-Filename'] = filename 377 | } 378 | if (icon) { 379 | headers['X-Icon'] = icon 380 | } 381 | if (actions) { 382 | headers['X-Actions'] = actions 383 | } 384 | if (cache) { 385 | headers['X-Cache'] = cache ? 'yes' : 'no' 386 | } 387 | if (firebase) { 388 | headers['X-Firebase'] = firebase ? 'yes' : 'no' 389 | } 390 | if (unifiedPush) { 391 | headers['X-UnifiedPush'] = unifiedPush ? '1' : '0' 392 | } 393 | if (email) { 394 | headers['X-Email'] = email 395 | } 396 | if (call) { 397 | headers['X-Call'] = call 398 | } 399 | if (file) { 400 | headers['X-Filename'] = file.name 401 | headers['Content-Type'] = 'application/octet-stream' 402 | headers['Content-Length'] = file.size 403 | headers['Content-Disposition'] = `attachment; filename="${file.name}"` 404 | } 405 | Debugger('headers: "%o"', headers) 406 | const data = desp || body || message 407 | Debugger('data: "%s"', data) 408 | const url = new URL(this.NTFY_TOPIC, this.NTFY_URL).toString() 409 | const response = await ajax({ 410 | url, 411 | method: 'POST', 412 | headers, 413 | data, 414 | }) 415 | return response 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /src/push/one-bot.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { warn } from '@/utils/helper' 5 | import { SendResponse } from '@/interfaces/response' 6 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 7 | import { validate } from '@/utils/validate' 8 | 9 | const Debugger = debug('push:one-bot') 10 | 11 | export interface OneBotConfig { 12 | /** 13 | * OneBot HTTP 基础路径 14 | */ 15 | ONE_BOT_BASE_URL: string 16 | /** 17 | * OneBot AccessToken 18 | * 出于安全原因,请务必设置 AccessToken 19 | */ 20 | ONE_BOT_ACCESS_TOKEN?: string 21 | } 22 | 23 | export type OneBotConfigSchema = ConfigSchema 24 | export const oneBotConfigSchema: OneBotConfigSchema = { 25 | ONE_BOT_BASE_URL: { 26 | type: 'string', 27 | title: 'OneBot HTTP 基础路径', 28 | description: 'OneBot HTTP 基础路径', 29 | required: true, 30 | }, 31 | ONE_BOT_ACCESS_TOKEN: { 32 | type: 'string', 33 | title: 'OneBot AccessToken', 34 | description: '出于安全原因,请务必设置 AccessToken', 35 | required: false, 36 | }, 37 | } as const 38 | 39 | export interface OneBotPrivateMsgOption { 40 | /** 41 | * 消息类型 42 | */ 43 | message_type: 'private' 44 | /** 45 | * 对方 QQ 号 46 | */ 47 | user_id: number 48 | } 49 | 50 | export interface OneBotGroupMsgOption { 51 | /** 52 | * 消息类型 53 | */ 54 | message_type: 'group' 55 | /** 56 | * 群号 57 | */ 58 | group_id: number 59 | 60 | } 61 | 62 | export type OneBotOption = (OneBotPrivateMsgOption | OneBotGroupMsgOption) & { 63 | /** 64 | * 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 message 字段是字符串时有效 65 | */ 66 | auto_escape?: boolean 67 | } 68 | 69 | export type OneBotMsgType = OneBotOption['message_type'] 70 | 71 | export type OneBotOptionSchema = OptionSchema<{ 72 | // 消息类型,private 或 group 73 | message_type: OneBotMsgType 74 | // 如果为 private,对方 QQ 号 75 | user_id?: number 76 | // 如果为 group,群号 77 | group_id?: number 78 | // 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 message 字段是字符串时有效 79 | auto_escape?: boolean 80 | }> 81 | 82 | export const oneBotOptionSchema: OneBotOptionSchema = { 83 | message_type: { 84 | type: 'select', 85 | title: '消息类型', 86 | description: '消息类型,private 或 group,默认为 private', 87 | required: true, 88 | default: 'private', 89 | options: [ 90 | { 91 | label: '私聊', 92 | value: 'private', 93 | }, 94 | { 95 | label: '群聊', 96 | value: 'group', 97 | }, 98 | ], 99 | }, 100 | user_id: { 101 | type: 'number', 102 | title: ' QQ 号', 103 | description: '对方 QQ 号。仅私聊有效。', 104 | required: false, 105 | }, 106 | group_id: { 107 | type: 'number', 108 | title: '群号', 109 | description: '群号。仅群聊有效。', 110 | required: false, 111 | }, 112 | auto_escape: { 113 | type: 'boolean', 114 | title: '消息内容是否作为纯文本发送(即不解析 CQ 码),只在 message 字段是字符串时有效', 115 | description: '消息内容是否作为纯文本发送(即不解析 CQ 码),只在 message 字段是字符串时有效', 116 | required: false, 117 | }, 118 | } as const 119 | 120 | export interface OneBotData { 121 | ClassType: string 122 | // 消息 ID 123 | message_id: number 124 | } 125 | 126 | export interface OneBotResponse { 127 | status: string 128 | retcode: number 129 | data: OneBotData 130 | echo?: any 131 | } 132 | 133 | /** 134 | * OneBot。官方文档:https://github.com/botuniverse/onebot-11 135 | * 本项目实现的版本为 OneBot 11 136 | * @author CaoMeiYouRen 137 | * @date 2023-10-22 138 | * @export 139 | * @class OneBot 140 | */ 141 | export class OneBot implements Send { 142 | static readonly namespace = 'OneBot' 143 | static readonly configSchema = oneBotConfigSchema 144 | static readonly optionSchema = oneBotOptionSchema 145 | 146 | /** 147 | * OneBot 协议版本号 148 | * 149 | * @author CaoMeiYouRen 150 | * @date 2023-10-22 151 | * @static 152 | */ 153 | static version = 11 154 | 155 | /** 156 | * OneBot HTTP 基础路径 157 | * 158 | * @author CaoMeiYouRen 159 | * @date 2023-10-22 160 | * @private 161 | * @example http://127.0.0.1 162 | */ 163 | private ONE_BOT_BASE_URL: string 164 | /** 165 | * OneBot AccessToken 166 | * 出于安全原因,请务必设置 AccessToken 167 | * @author CaoMeiYouRen 168 | * @date 2023-10-22 169 | * @private 170 | */ 171 | private ONE_BOT_ACCESS_TOKEN?: string 172 | 173 | /** 174 | * 创建 OneBot 实例 175 | * @author CaoMeiYouRen 176 | * @date 2024-11-08 177 | * @param config OneBot 配置 178 | */ 179 | constructor(config: OneBotConfig) { 180 | const { ONE_BOT_BASE_URL, ONE_BOT_ACCESS_TOKEN } = config 181 | this.ONE_BOT_BASE_URL = ONE_BOT_BASE_URL 182 | this.ONE_BOT_ACCESS_TOKEN = ONE_BOT_ACCESS_TOKEN 183 | Debugger('set ONE_BOT_BASE_URL: "%s", ONE_BOT_ACCESS_TOKEN: "%s"', ONE_BOT_BASE_URL, ONE_BOT_ACCESS_TOKEN) 184 | // 根据 configSchema 验证 config 185 | validate(config, OneBot.configSchema) 186 | if (!this.ONE_BOT_ACCESS_TOKEN) { 187 | warn('未提供 ONE_BOT_ACCESS_TOKEN !出于安全原因,请务必设置 AccessToken!') 188 | } 189 | } 190 | 191 | /** 192 | * 193 | * 194 | * @author CaoMeiYouRen 195 | * @date 2024-11-08 196 | * @param title 消息标题 197 | * @param desp 消息正文 198 | * @param option 额外推送选项 199 | */ 200 | async send(title: string, desp: string, option: OneBotOption): Promise> { 201 | Debugger('title: "%s", desp: "%s", option: "%o"', title, desp, option) 202 | // !由于 OneBot 的 option 中带有必填项,所以需要校验 203 | // 根据 optionSchema 验证 option 204 | validate(option, OneBot.optionSchema as OptionSchema) 205 | if (option.message_type === 'private' && !option.user_id) { 206 | throw new Error('OneBot 私聊消息类型必须提供 user_id') 207 | } 208 | if (option.message_type === 'group' && !option.group_id) { 209 | throw new Error('OneBot 群聊消息类型必须提供 group_id') 210 | } 211 | const { message_type = 'private', ...args } = option || {} 212 | const message = `${title}${desp ? `\n${desp}` : ''}` 213 | return ajax({ 214 | baseURL: this.ONE_BOT_BASE_URL, 215 | url: '/send_msg', 216 | method: 'POST', 217 | headers: { 218 | 'Content-Type': 'application/json', 219 | Authorization: `Bearer ${this.ONE_BOT_ACCESS_TOKEN}`, 220 | }, 221 | data: { 222 | auto_escape: false, 223 | message_type, 224 | message, 225 | ...args, 226 | }, 227 | }) 228 | } 229 | 230 | } 231 | -------------------------------------------------------------------------------- /src/push/push-deer.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | 8 | const Debugger = debug('push:push-deer') 9 | 10 | export type PushDeerPushType = 'markdown' | 'text' | 'image' 11 | 12 | export interface PushDeerConfig { 13 | /** 14 | * pushkey。请参考 https://github.com/easychen/pushdeer 获取 15 | */ 16 | PUSH_DEER_PUSH_KEY: string 17 | 18 | /** 19 | * 使用自架版时的服务器端地址。例如 http://127.0.0.1:8800。默认为 https://api2.pushdeer.com 20 | */ 21 | PUSH_DEER_ENDPOINT?: string 22 | } 23 | 24 | export type PushDeerConfigSchema = ConfigSchema 25 | 26 | export const pushDeerConfigSchema: PushDeerConfigSchema = { 27 | PUSH_DEER_PUSH_KEY: { 28 | type: 'string', 29 | title: 'pushkey', 30 | description: '请参考 https://github.com/easychen/pushdeer 获取', 31 | required: true, 32 | }, 33 | PUSH_DEER_ENDPOINT: { 34 | type: 'string', 35 | title: '使用自架版时的服务器端地址', 36 | description: '例如 http://127.0..1:8800。默认为 https://api2.pushdeer.com', 37 | required: false, 38 | default: 'https://api2.pushdeer.com', 39 | }, 40 | } as const 41 | 42 | export interface PushDeerOption { 43 | /** 44 | * 格式。文本=text,markdown,图片=image,默认为markdown。type 为 image 时,text 中为要发送图片的URL 45 | */ 46 | type?: PushDeerPushType 47 | } 48 | 49 | export type PushDeerOptionSchema = OptionSchema 50 | export const pushDeerOptionSchema: PushDeerOptionSchema = { 51 | type: { 52 | type: 'select', 53 | title: '格式', 54 | description: '文本=text,markdown,图片=image,默认为markdown。type 为 image 时,text 中为要发送图片的URL', 55 | required: false, 56 | default: 'markdown', 57 | options: [ 58 | { 59 | label: '文本', 60 | value: 'text', 61 | }, 62 | { 63 | label: 'Markdown', 64 | value: 'markdown', 65 | }, 66 | { 67 | label: '图片', 68 | value: 'image', 69 | }, 70 | ], 71 | }, 72 | } as const 73 | 74 | export interface PushDeerResponse { 75 | /** 76 | * 正确为0,错误为非0 77 | */ 78 | code: number 79 | /** 80 | * 错误信息。无错误时无此字段 81 | */ 82 | error: string 83 | /** 84 | * 消息内容,错误时无此字段 85 | */ 86 | content: { 87 | result: string[] 88 | } 89 | } 90 | 91 | /** 92 | * PushDeer 推送。 官方文档 https://github.com/easychen/pushdeer 93 | * 94 | * @author CaoMeiYouRen 95 | * @date 2022-02-28 96 | * @export 97 | * @class PushDeer 98 | */ 99 | export class PushDeer implements Send { 100 | 101 | static readonly namespace = 'PushDeer' 102 | static readonly configSchema = pushDeerConfigSchema 103 | static readonly optionSchema = pushDeerOptionSchema 104 | 105 | /** 106 | * pushkey,请参考 https://github.com/easychen/pushdeer 获取 107 | * 108 | * @author CaoMeiYouRen 109 | * @date 2022-02-28 110 | * @private 111 | */ 112 | private PUSH_DEER_PUSH_KEY: string 113 | 114 | /** 115 | * 使用自架版时的服务器端地址。例如 http://127.0.0.1:8800 116 | * 117 | * @author CaoMeiYouRen 118 | * @date 2022-02-28 119 | * @private 120 | */ 121 | private PUSH_DEER_ENDPOINT: string 122 | 123 | /** 124 | * 创建 PushDeer 实例 125 | * @author CaoMeiYouRen 126 | * @date 2024-11-08 127 | * @param config 配置 128 | */ 129 | constructor(config: PushDeerConfig) { 130 | const { PUSH_DEER_PUSH_KEY, PUSH_DEER_ENDPOINT } = config 131 | this.PUSH_DEER_PUSH_KEY = PUSH_DEER_PUSH_KEY 132 | this.PUSH_DEER_ENDPOINT = PUSH_DEER_ENDPOINT || 'https://api2.pushdeer.com' 133 | Debugger('set PUSH_DEER_PUSH_KEY: "%s", PUSH_DEER_ENDPOINT: "%s"', PUSH_DEER_PUSH_KEY, PUSH_DEER_ENDPOINT) 134 | // 根据 configSchema 验证 config 135 | validate(config, PushDeer.configSchema) 136 | } 137 | 138 | /** 139 | * @author CaoMeiYouRen 140 | * @date 2024-11-08 141 | * @param text 推送消息内容 142 | * @param [desp=''] 消息内容第二部分 143 | * @param [option={}] 额外推送选项 144 | */ 145 | async send(title: string, desp: string = '', option?: PushDeerOption): Promise> { 146 | Debugger('title: "%s", desp: "%s", option: "%o"', title, desp, option) 147 | const { type = 'markdown' } = option || {} 148 | return ajax({ 149 | baseURL: this.PUSH_DEER_ENDPOINT, 150 | url: '/message/push', 151 | method: 'POST', 152 | headers: { 153 | 'Content-Type': 'application/x-www-form-urlencoded', 154 | }, 155 | data: { 156 | text: title, 157 | desp, 158 | pushkey: this.PUSH_DEER_PUSH_KEY, 159 | type, 160 | }, 161 | }) 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /src/push/push-plus.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | 8 | const Debugger = debug('push:push-plus') 9 | /** 10 | html 默认模板,支持html文本 11 | txt 纯文本展示,不转义html 12 | json 内容基于json格式展示 13 | markdown 内容基于markdown格式展示 14 | cloudMonitor 阿里云监控报警定制模板 15 | jenkins jenkins插件定制模板 16 | route 路由器插件定制模板 */ 17 | export type PushPlusTemplateType = 'html' | 'txt' | 'json' | 'markdown' | 'cloudMonitor' | 'jenkins' | 'route' 18 | /** 19 | wechat 免费 微信公众号 20 | webhook 免费 第三方webhook;企业微信、钉钉、飞书、server酱;webhook机器人推送 21 | cp 免费 企业微信应用;具体参考企业微信应用推送 22 | mail 免费 邮箱;具体参考邮件渠道使用说明 23 | sms 收费 短信,未开放 24 | */ 25 | export type PushPlusChannelType = 'wechat' | 'webhook' | 'cp' | 'sms' | 'mail' 26 | 27 | export interface PushPlusConfig { 28 | /** 29 | * 请前往 https://www.pushplus.plus 领取 30 | */ 31 | PUSH_PLUS_TOKEN: string 32 | } 33 | 34 | export type PushPlusConfigSchema = ConfigSchema 35 | 36 | export const pushPlusConfigSchema: PushPlusConfigSchema = { 37 | PUSH_PLUS_TOKEN: { 38 | type: 'string', 39 | title: 'PushPlus Token', 40 | description: '请前往 https://www.pushplus.plus/ 领取', 41 | required: true, 42 | }, 43 | } 44 | 45 | export interface PushPlusOption { 46 | /** 47 | * 模板类型 48 | */ 49 | template?: PushPlusTemplateType 50 | /** 51 | * 渠道类型 52 | */ 53 | channel?: PushPlusChannelType 54 | /** 55 | * 群组编码,不填仅发送给自己;channel为webhook时无效 56 | */ 57 | topic?: string 58 | /** 59 | * webhook编码,仅在channel使用webhook渠道和CP渠道时需要填写 60 | */ 61 | webhook?: string 62 | /** 63 | * 发送结果回调地址 64 | */ 65 | callbackUrl?: string 66 | /** 67 | * 毫秒时间戳。格式如:1632993318000。服务器时间戳大于此时间戳,则消息不会发送 68 | */ 69 | timestamp?: number 70 | } 71 | 72 | export type PushPlusOptionSchema = OptionSchema 73 | 74 | export const pushPlusOptionSchema: PushPlusOptionSchema = { 75 | template: { 76 | type: 'select', 77 | title: '模板类型', 78 | description: 'html,txt,json,markdown,cloudMonitor,jenkins,route', 79 | required: false, 80 | default: 'html', 81 | options: [ 82 | { 83 | label: 'HTML', 84 | value: 'html', 85 | }, 86 | { 87 | label: '文本', 88 | value: 'txt', 89 | }, 90 | { 91 | label: 'JSON', 92 | value: 'json', 93 | }, 94 | { 95 | label: 'Markdown', 96 | value: 'markdown', 97 | }, 98 | { 99 | label: '阿里云监控', 100 | value: 'cloudMonitor', 101 | }, 102 | { 103 | label: 'Jenkins', 104 | value: 'jenkins', 105 | }, 106 | { 107 | label: '路由器', 108 | value: 'route', 109 | }, 110 | ], 111 | }, 112 | channel: { 113 | type: 'select', 114 | title: '渠道类型', 115 | description: 'wechat,webhook,cp,sms,mail', 116 | required: false, 117 | default: 'wechat', 118 | options: [ 119 | { 120 | label: '微信', 121 | value: 'wechat', 122 | }, 123 | { 124 | label: 'Webhook', 125 | value: 'webhook', 126 | }, 127 | { 128 | label: '企业微信', 129 | value: 'cp', 130 | }, 131 | { 132 | label: '邮件', 133 | value: 'mail', 134 | }, 135 | { 136 | label: '短信', 137 | value: 'sms', 138 | }, 139 | ], 140 | }, 141 | topic: { 142 | type: 'string', 143 | title: '群组编码', 144 | description: '不填仅发送给自己;channel为webhook时无效', 145 | required: false, 146 | default: '', 147 | }, 148 | webhook: { 149 | type: 'string', 150 | title: 'webhook编码', 151 | description: '仅在channel使用webhook渠道和CP渠道时需要填写', 152 | required: false, 153 | default: '', 154 | }, 155 | callbackUrl: { 156 | type: 'string', 157 | title: '发送结果回调地址', 158 | description: '发送结果回调地址', 159 | required: false, 160 | default: '', 161 | }, 162 | timestamp: { 163 | type: 'number', 164 | title: '毫秒时间戳', 165 | description: '格式如:1632993318000。服务器时间戳大于此时间戳,则消息不会发送', 166 | required: false, 167 | // default: 0, 168 | }, 169 | } 170 | 171 | export interface PushPlusResponse { 172 | // 200 为正确 173 | code: number 174 | msg: string 175 | data: any 176 | } 177 | 178 | /** 179 | * pushplus 推送加开放平台,仅支持一对一推送。官方文档:https://www.pushplus.plus/doc/ 180 | * 181 | * @author CaoMeiYouRen 182 | * @date 2021-03-03 183 | * @export 184 | * @class PushPlus 185 | */ 186 | export class PushPlus implements Send { 187 | static readonly namespace = 'PushPlus' 188 | static readonly configSchema = pushPlusConfigSchema 189 | static readonly optionSchema = pushPlusOptionSchema 190 | /** 191 | * 请前往 https://www.pushplus.plus 领取 192 | * 193 | * @private 194 | */ 195 | private PUSH_PLUS_TOKEN: string 196 | 197 | /** 198 | * 199 | * @author CaoMeiYouRen 200 | * @date 2024-11-08 201 | * @param config 请前往 https://www.pushplus.plus 领取 202 | */ 203 | constructor(config: PushPlusConfig) { 204 | const { PUSH_PLUS_TOKEN } = config 205 | this.PUSH_PLUS_TOKEN = PUSH_PLUS_TOKEN 206 | Debugger('set PUSH_PLUS_TOKEN: "%s"', PUSH_PLUS_TOKEN) 207 | // 根据 configSchema 验证 config 208 | validate(config, PushPlus.configSchema) 209 | } 210 | 211 | /** 212 | * 发送消息 213 | * 214 | * @author CaoMeiYouRen 215 | * @date 2024-11-08 216 | * @param title 消息标题 217 | * @param [desp=''] 消息内容 218 | * @param [option] 额外推送选项 219 | */ 220 | send(title: string, desp: string = '', option?: PushPlusOption): Promise> { 221 | Debugger('title: "%s", desp: "%s", option: "%o"', title, desp, option) 222 | const { template = 'html', channel = 'wechat', ...args } = option || {} 223 | const content = desp || title 224 | return ajax({ 225 | url: 'http://www.pushplus.plus/send', 226 | method: 'POST', 227 | headers: { 228 | 'Content-Type': 'application/json', 229 | }, 230 | data: { 231 | token: this.PUSH_PLUS_TOKEN, 232 | title, 233 | content: content || title, 234 | template, 235 | channel, 236 | ...args, 237 | }, 238 | }) 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /src/push/qmsg.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | 8 | const Debugger = debug('push:qmsg') 9 | 10 | /** 11 | * 推送类型,见 [Qmsg](https://qmsg.zendee.cn/docs/api)。 12 | */ 13 | export type QmsgPushType = 'send' | 'group' 14 | 15 | export interface QmsgConfig { 16 | /** 17 | * 推送的 key。在 [Qmsg 酱管理台](https://qmsg.zendee.cn/user) 查看 18 | */ 19 | QMSG_KEY: string 20 | } 21 | export type QmsgConfigSchema = ConfigSchema 22 | 23 | export const qmsgConfigSchema: QmsgConfigSchema = { 24 | QMSG_KEY: { 25 | type: 'string', 26 | title: '推送的 key', 27 | description: '在 [Qmsg 酱管理台](https://qmsg.zendee.cn/user) 查看', 28 | required: true, 29 | }, 30 | } as const 31 | 32 | export interface QmsgPrivateMsgOption { 33 | /** 34 | * send 表示发送消息给指定的QQ号,group 表示发送消息给指定的QQ群。默认为 send 35 | */ 36 | type: 'send' 37 | /** 38 | * 指定要接收消息的QQ号或者QQ群。多个以英文逗号分割,例如:12345,12346 39 | */ 40 | qq: string 41 | } 42 | 43 | export interface QmsgGroupMsgOption { 44 | /** 45 | * send 表示发送消息给指定的QQ号,group 表示发送消息给指定的QQ群。默认为 send 46 | */ 47 | type: 'group' 48 | /** 49 | * 指定要接收消息的QQ号或者QQ群。多个以英文逗号分割,例如:12345,12346 50 | */ 51 | qq: string 52 | 53 | } 54 | 55 | export type QmsgOption = (QmsgPrivateMsgOption | QmsgGroupMsgOption) & { 56 | /** 57 | * 机器人的QQ号。指定使用哪个机器人来发送消息,不指定则会自动随机选择一个在线的机器人发送消息。该参数仅私有云有效 58 | */ 59 | bot?: string 60 | } 61 | 62 | export type QmsgOptionSchema = OptionSchema<{ 63 | // 消息类型 64 | type: 'send' | 'group' 65 | // 指定要接收消息的QQ号或者QQ群。多个以英文逗号分割,例如:12345,12346 66 | qq: string 67 | // 机器人的QQ号。指定使用哪个机器人来发送消息,不指定则会自动随机选择一个在线的机器人发送消息。该参数仅私有云有效 68 | bot?: string 69 | }> 70 | 71 | export const qmsgOptionSchema: QmsgOptionSchema = { 72 | type: { 73 | type: 'select', 74 | title: '消息类型', 75 | description: 'send 表示发送消息给指定的QQ号,group 表示发送消息给指定的QQ群。默认为 send', 76 | required: true, 77 | default: 'send', 78 | options: [ 79 | { 80 | label: '私聊', 81 | value: 'send', 82 | }, 83 | { 84 | label: '群聊', 85 | value: 'group', 86 | }, 87 | ], 88 | }, 89 | qq: { 90 | type: 'string', 91 | title: '指定要接收消息的QQ号或者QQ群', 92 | description: '多个以英文逗号分割,例如:12345,12346', 93 | required: true, 94 | }, 95 | bot: { 96 | type: 'string', 97 | title: '机器人的QQ号', 98 | description: '指定使用哪个机器人来发送消息,不指定则会自动随机选择一个在线的机器人发送消息。该参数仅私有云有效', 99 | required: false, 100 | }, 101 | } as const 102 | 103 | export interface QmsgResponse { 104 | /** 105 | * 本次请求是否成功 106 | */ 107 | success: boolean 108 | /** 109 | * 本次请求结果描述 110 | */ 111 | reason: string 112 | /** 113 | * 错误代码。错误代码目前不可靠,如果要判断是否成功请使用success 114 | */ 115 | code: number 116 | info: any 117 | } 118 | 119 | /** 120 | * Qmsg酱。使用说明见 [Qmsg酱](https://qmsg.zendee.cn/docs) 121 | * 122 | * @author CaoMeiYouRen 123 | * @date 2022-02-17 124 | * @export 125 | * @class Qmsg 126 | */ 127 | export class Qmsg implements Send { 128 | 129 | static readonly namespace = 'Qmsg酱' 130 | static readonly configSchema = qmsgConfigSchema 131 | static readonly optionSchema = qmsgOptionSchema 132 | 133 | private QMSG_KEY: string 134 | 135 | constructor(config: QmsgConfig) { 136 | const { QMSG_KEY } = config 137 | this.QMSG_KEY = QMSG_KEY 138 | Debugger('set QMSG_KEY: "%s"', QMSG_KEY) 139 | // 根据 configSchema 验证 config 140 | validate(config, Qmsg.configSchema) 141 | } 142 | 143 | /** 144 | * 145 | * 发送消息 146 | * @author CaoMeiYouRen 147 | * @date 2024-11-08 148 | * @param title 消息标题 149 | * @param [desp] 消息描述 150 | * @param [option] QmsgOption 选项 151 | */ 152 | async send(title: string, desp: string, option: QmsgOption): Promise> { 153 | Debugger('title: "%s", desp: "%s", option: "%o"', title, desp, option) 154 | // !由于 Qmsg 酱的 option 中带有必填项,所以需要校验 155 | // 根据 optionSchema 验证 option 156 | validate(option, Qmsg.optionSchema) 157 | const { qq, type = 'send', bot } = option || {} 158 | const msg = `${title}${desp ? `\n${desp}` : ''}` 159 | return ajax({ 160 | url: `https://qmsg.zendee.cn/${type}/${this.QMSG_KEY}`, 161 | headers: { 162 | 'Content-Type': 'application/x-www-form-urlencoded', 163 | }, 164 | method: 'POST', 165 | data: { msg, qq, bot }, 166 | }) 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /src/push/server-chan-turbo.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | 8 | const Debugger = debug('push:server-chan-turbo') 9 | 10 | export type ChannelValue = 98 | 66 | 1 | 2 | 3 | 8 | 0 | 88 | 18 | 9 11 | 12 | export type Channel = `${ChannelValue}` | `${ChannelValue}|${ChannelValue}` 13 | 14 | export interface ServerChanTurboConfig { 15 | /** 16 | * Server酱 Turbo 的 SCTKEY 17 | * 请前往 https://sct.ftqq.com/sendkey 领取 18 | */ 19 | SERVER_CHAN_TURBO_SENDKEY: string 20 | } 21 | 22 | export type ServerChanTurboConfigSchema = ConfigSchema 23 | export const serverChanTurboConfigSchema: ServerChanTurboConfigSchema = { 24 | SERVER_CHAN_TURBO_SENDKEY: { 25 | type: 'string', 26 | title: 'SCTKEY', 27 | description: 'Server酱 Turbo 的 SCTKEY。请前往 https://sct.ftqq.com/sendkey 领取', 28 | required: true, 29 | }, 30 | } as const 31 | 32 | /** 33 | * 附加参数 34 | */ 35 | export type ServerChanTurboOption = { 36 | /** 37 | * 消息卡片内容,选填。最大长度 64。如果不指定,将自动从 desp 中截取生成。 38 | */ 39 | short?: string 40 | /** 41 | * 是否隐藏调用 IP,选填。如果不指定,则显示;为 1 则隐藏。 42 | */ 43 | noip?: '1' | 1 | true 44 | /** 45 | * 动态指定本次推送使用的消息通道,选填。如不指定,则使用网站上的消息通道页面设置的通道。支持最多两个通道,多个通道值用竖线 "|" 隔开。 46 | * 通道对应的值如下: 47 | * 官方Android版·β=98 48 | * 企业微信应用消息=66 49 | * 企业微信群机器人=1 50 | * 钉钉群机器人=2 51 | * 飞书群机器人=3 52 | * Bark iOS=8 53 | * 测试号=0 54 | * 自定义=88 55 | * PushDeer=18 56 | * 方糖服务号=9 57 | */ 58 | channel?: Channel 59 | /** 60 | * 消息抄送的 openid,选填。只支持测试号和企业微信应用消息通道。多个 openid 用 "," 隔开。企业微信应用消息通道的 openid 参数,内容为接收人在企业微信中的 UID,多个人请 "|" 隔开。 61 | */ 62 | openid?: string 63 | } 64 | 65 | export type ServerChanTurboOptionSchema = OptionSchema<{ 66 | short?: string 67 | openid?: string 68 | channel?: string 69 | noip?: boolean 70 | }> 71 | export const serverChanTurboOptionSchema: ServerChanTurboOptionSchema = { 72 | short: { 73 | type: 'string', 74 | title: '消息卡片内容', 75 | description: '选填。最大长度 64。如果不指定,将自动从 desp 中截取生成。', 76 | required: false, 77 | }, 78 | noip: { 79 | type: 'boolean', 80 | title: '是否隐藏调用 IP', 81 | description: '选填。如果不指定,则显示;为 1/true 则隐藏。', 82 | required: false, 83 | }, 84 | channel: { 85 | type: 'string', 86 | title: '消息通道', 87 | description: '选填。动态指定本次推送使用的消息通道,支持最多两个通道,多个通道值用竖线 "|" 隔开。', 88 | required: false, 89 | }, 90 | openid: { 91 | type: 'string', 92 | title: '消息抄送的 openid', 93 | description: '选填。只支持测试号和企业微信应用消息通道。多个 openid 用 "," 隔开。企业微信应用消息通道的 openid 参数,内容为接收人在企业微信中的 UID,多个人请 "|" 隔开。', 94 | required: false, 95 | }, 96 | } 97 | 98 | export interface ServerChanTurboResponse { 99 | // 0 表示成功,其他值表示失败 100 | code: number 101 | message: string 102 | data: { 103 | // 推送消息的 ID 104 | pushid: string 105 | // 推送消息的阅读凭证 106 | readkey: string 107 | error: string 108 | errno: number 109 | } 110 | } 111 | 112 | /** 113 | * Server 酱·Turbo 114 | * 文档 https://sct.ftqq.com/ 115 | * 116 | * @author CaoMeiYouRen 117 | * @date 2021-02-27 118 | * @export 119 | * @class ServerChanTurbo 120 | */ 121 | export class ServerChanTurbo implements Send { 122 | 123 | static readonly namespace = 'Server酱·Turbo' 124 | static readonly configSchema = serverChanTurboConfigSchema 125 | static readonly optionSchema = serverChanTurboOptionSchema 126 | 127 | /** 128 | * 129 | * @author CaoMeiYouRen 130 | * @date 2024-11-08 131 | * @param config 请前往 https://sct.ftqq.com/sendkey 领取 132 | */ 133 | constructor(config: ServerChanTurboConfig) { 134 | const { SERVER_CHAN_TURBO_SENDKEY } = config 135 | this.SCTKEY = SERVER_CHAN_TURBO_SENDKEY 136 | Debugger('set SCTKEY: "%s"', this.SCTKEY) 137 | // 根据 configSchema 验证 config 138 | validate(config, ServerChanTurbo.configSchema) 139 | } 140 | /** 141 | * 142 | * 143 | * @private 请前往 https://sct.ftqq.com/sendkey 领取 144 | */ 145 | private SCTKEY: string 146 | 147 | /** 148 | * 发送消息 149 | * 150 | * @author CaoMeiYouRen 151 | * @date 2024-11-08 152 | * @param title 消息的标题 153 | * @param [desp=''] 消息的内容,支持 Markdown 154 | * @param [option={}] 额外发送选项 155 | */ 156 | async send(title: string, desp: string = '', option: ServerChanTurboOption = {}): Promise> { 157 | Debugger('title: "%s", desp: "%s", option: %O', title, desp, option) 158 | if (option.noip === 1 || option.noip === true) { 159 | option.noip = '1' 160 | } 161 | const data = { 162 | text: title, 163 | desp, 164 | ...option, 165 | } 166 | return ajax({ 167 | url: `https://sctapi.ftqq.com/${this.SCTKEY}.send`, 168 | method: 'POST', 169 | headers: { 170 | 'Content-Type': 'application/x-www-form-urlencoded', 171 | }, 172 | data, 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/push/server-chan-v3.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | 8 | const Debugger = debug('push:server-chan-v3') 9 | 10 | export interface ServerChanV3Config { 11 | /** 12 | * 请前往 https://sc3.ft07.com/sendkey 领取 13 | */ 14 | SERVER_CHAN_V3_SENDKEY: string 15 | } 16 | 17 | export type ServerChanV3ConfigSchema = ConfigSchema 18 | export const serverChanV3ConfigSchema: ServerChanV3ConfigSchema = { 19 | SERVER_CHAN_V3_SENDKEY: { 20 | type: 'string', 21 | title: 'SENDKEY', 22 | description: '请前往 https://sc3.ft07.com/sendkey 领取', 23 | required: true, 24 | }, 25 | } as const 26 | 27 | /** 28 | * 附加参数 29 | */ 30 | export type ServerChanV3Option = { 31 | tags?: string | string[] // 标签列表,多个标签使用竖线分隔;也可以用数组格式,数组格式下不要加竖线 32 | short?: string // 推送消息的简短描述,用于指定消息卡片的内容部分,尤其是在推送markdown的时候 33 | } 34 | 35 | export type ServerChanV3OptionSchema = OptionSchema<{ 36 | tags?: string[] 37 | short?: string 38 | }> 39 | export const serverChanV3OptionSchema: ServerChanV3OptionSchema = { 40 | tags: { 41 | type: 'array', 42 | title: '标签列表', 43 | description: '多个标签用数组格式', 44 | required: false, 45 | }, 46 | short: { 47 | type: 'string', 48 | title: '推送消息的简短描述', 49 | description: '用于指定消息卡片的内容部分,尤其是在推送markdown的时候', 50 | required: false, 51 | }, 52 | } as const 53 | 54 | export interface ServerChanV3Response { 55 | // 0 表示成功,其他值表示失败 56 | code: number 57 | message: string 58 | errno: number 59 | data: { 60 | // 推送消息的 ID 61 | pushid: string 62 | meta: { 63 | android: any 64 | devices: any[] 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Server酱³ 71 | * 文档:https://sc3.ft07.com/doc 72 | * @author CaoMeiYouRen 73 | * @date 2024-10-04 74 | * @export 75 | * @class ServerChanV3 76 | */ 77 | export class ServerChanV3 implements Send { 78 | 79 | static readonly namespace = 'Server酱³' 80 | static readonly configSchema = serverChanV3ConfigSchema 81 | static readonly optionSchema = serverChanV3OptionSchema 82 | 83 | /** 84 | * 请前往 https://sc3.ft07.com/sendkey 领取 85 | * 86 | * @author CaoMeiYouRen 87 | * @date 2024-10-04 88 | * @private 89 | */ 90 | private sendkey: string 91 | 92 | private uid: string = '' 93 | 94 | /** 95 | * 创建 ServerChanV3 实例 96 | * @author CaoMeiYouRen 97 | * @date 2024-11-08 98 | * @param config 请前往 https://sc3.ft07.com/sendkey 领取 99 | */ 100 | constructor(config: ServerChanV3Config) { 101 | const { SERVER_CHAN_V3_SENDKEY } = config 102 | const sendkey = SERVER_CHAN_V3_SENDKEY 103 | this.sendkey = sendkey 104 | Debugger('set sendkey: "%s"', sendkey) 105 | // 根据 configSchema 验证 config 106 | validate(config, ServerChanV3.configSchema) 107 | this.uid = this.sendkey.match(/^sctp(\d+)t/)?.[1] 108 | if (!this.uid) { 109 | throw new Error('SERVER_CHAN_V3_SENDKEY 不合法!') 110 | } 111 | } 112 | 113 | /** 114 | * 发送消息 115 | * 116 | * @author CaoMeiYouRen 117 | * @date 2024-11-08 118 | * @param title 消息的标题 119 | * @param [desp=''] 消息的内容,支持 Markdown 120 | * @param [option={}] 额外发送选项 121 | */ 122 | async send(title: string, desp: string = '', option: ServerChanV3Option = {}): Promise> { 123 | Debugger('title: "%s", desp: "%s", option: %O', title, desp, option) 124 | if (Array.isArray(option.tags)) { 125 | option.tags = option.tags.join('|') 126 | } 127 | const data = { 128 | text: title, 129 | desp, 130 | ...option, 131 | } 132 | return ajax({ 133 | url: `https://${this.uid}.push.ft07.com/send/${this.sendkey}.send`, 134 | method: 'POST', 135 | headers: { 136 | 'Content-Type': 'application/json', 137 | }, 138 | data, 139 | }) 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/push/telegram.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | 8 | const Debugger = debug('push:telegram') 9 | 10 | export interface TelegramConfig { 11 | /** 12 | * 机器人令牌 13 | * 您可以从 https://t.me/BotFather 获取 Token。 14 | * @author CaoMeiYouRen 15 | * @date 2023-10-22 16 | */ 17 | TELEGRAM_BOT_TOKEN: string 18 | /** 19 | * 支持对话/群组/频道的 Chat ID 20 | * 您可以转发消息到 https://t.me/JsonDumpBot 获取 Chat ID 21 | * @author CaoMeiYouRen 22 | * @date 2023-10-22 23 | */ 24 | TELEGRAM_CHAT_ID: number 25 | /** 26 | * 代理地址 27 | */ 28 | PROXY_URL?: string 29 | } 30 | 31 | export type TelegramConfigSchema = ConfigSchema 32 | export const telegramConfigSchema: TelegramConfigSchema = { 33 | TELEGRAM_BOT_TOKEN: { 34 | type: 'string', 35 | title: '机器人令牌', 36 | description: '您可以从 https://t.me/BotFather 获取 Token。', 37 | required: true, 38 | }, 39 | TELEGRAM_CHAT_ID: { 40 | type: 'number', 41 | title: '支持对话/群组/频道的 Chat ID', 42 | description: '您可以转发消息到 https://t.me/JsonDumpBot 获取 Chat ID', 43 | required: true, 44 | }, 45 | PROXY_URL: { 46 | type: 'string', 47 | title: '代理地址', 48 | description: '代理地址', 49 | required: false, 50 | }, 51 | } as const 52 | 53 | /** 54 | * 参考 https://core.telegram.org/bots/api#sendmessage 55 | * 56 | * @author CaoMeiYouRen 57 | * @date 2024-11-09 58 | * @export 59 | * @interface TelegramOption 60 | */ 61 | export interface TelegramOption { 62 | /** 63 | * 静默发送 64 | * 静默地发送消息。消息发布后用户会收到无声通知。 65 | */ 66 | disable_notification?: boolean 67 | /** 68 | * 阻止转发/保存 69 | * 如果启用,Telegram 中的机器人消息将受到保护,不会被转发和保存。 70 | */ 71 | protect_content?: boolean 72 | /** 73 | * 话题 ID 74 | * 可选的唯一标识符,用以向该标识符对应的话题发送消息,仅限启用了话题功能的超级群组可用 75 | */ 76 | message_thread_id?: string 77 | [key: string]: any 78 | } 79 | 80 | export type TelegramOptionSchema = OptionSchema 81 | 82 | export const telegramOptionSchema: TelegramOptionSchema = { 83 | disable_notification: { 84 | type: 'boolean', 85 | title: '静默发送', 86 | description: '静默地发送消息。消息发布后用户会收到无声通知。', 87 | required: false, 88 | }, 89 | protect_content: { 90 | type: 'boolean', 91 | title: '阻止转发/保存', 92 | description: '如果启用,Telegram 中的机器人消息将受到保护,不会被转发和保存。', 93 | required: false, 94 | }, 95 | message_thread_id: { 96 | type: 'string', 97 | title: '话题 ID', 98 | description: '可选的唯一标识符,用以向该标识符对应的话题发送消息,仅限启用了话题功能的超级群组可用', 99 | required: false, 100 | }, 101 | } as const 102 | 103 | interface From { 104 | id: number 105 | is_bot: boolean 106 | first_name: string 107 | username: string 108 | } 109 | interface Chat { 110 | id: number 111 | first_name: string 112 | last_name: string 113 | username: string 114 | type: string 115 | } 116 | interface Result { 117 | message_id: number 118 | from: From 119 | chat: Chat 120 | date: number 121 | text: string 122 | } 123 | export interface TelegramResponse { 124 | ok: boolean 125 | result: Result 126 | } 127 | 128 | /** 129 | * Telegram Bot 推送。 130 | * 官方文档:https://core.telegram.org/bots/api#making-requests 131 | * 132 | * @author CaoMeiYouRen 133 | * @date 2023-09-16 134 | * @export 135 | * @class Telegram 136 | */ 137 | export class Telegram implements Send { 138 | 139 | static readonly namespace = 'Telegram' 140 | static readonly configSchema = telegramConfigSchema 141 | static readonly optionSchema = telegramOptionSchema 142 | 143 | /** 144 | * 机器人令牌 145 | * 您可以从 https://t.me/BotFather 获取 Token。 146 | * @author CaoMeiYouRen 147 | * @date 2023-10-22 148 | * @private 149 | */ 150 | private TELEGRAM_BOT_TOKEN: string 151 | /** 152 | * 支持对话/群组/频道的 Chat ID 153 | * 您可以转发消息到 https://t.me/JsonDumpBot 获取 Chat ID 154 | * @author CaoMeiYouRen 155 | * @date 2023-10-22 156 | * @private 157 | */ 158 | private TELEGRAM_CHAT_ID: number 159 | 160 | proxyUrl?: string 161 | 162 | constructor(config: TelegramConfig) { 163 | Debugger('config: %O', config) 164 | Object.assign(this, config) 165 | // 根据 configSchema 验证 config 166 | validate(config, Telegram.configSchema) 167 | if (config.PROXY_URL) { 168 | this.proxyUrl = config.PROXY_URL 169 | } 170 | } 171 | 172 | /** 173 | * 发送消息 174 | * 175 | * @author CaoMeiYouRen 176 | * @date 2024-11-09 177 | * @param title 消息标题 178 | * @param [desp] 消息正文,和 title 相加后不超过 4096 个字符 179 | * @param [option] 其他参数 180 | */ 181 | async send(title: string, desp?: string, option?: TelegramOption): Promise> { 182 | const url = `https://api.telegram.org/bot${this.TELEGRAM_BOT_TOKEN}/sendMessage` 183 | Debugger('title: "%s", desp: "%s", option: %O', title, desp, option) 184 | const text = `${title}${desp ? `\n${desp}` : ''}` 185 | return ajax({ 186 | url, 187 | method: 'POST', 188 | proxyUrl: this.proxyUrl, 189 | data: { 190 | chat_id: this.TELEGRAM_CHAT_ID, 191 | text, 192 | }, 193 | }) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/push/wechat-app.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { warn } from '@/utils/helper' 4 | import { ajax } from '@/utils/ajax' 5 | import { SendResponse } from '@/interfaces/response' 6 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 7 | import { validate } from '@/utils/validate' 8 | 9 | const Debugger = debug('push:wechat-app') 10 | export type WechatAppMsgType = 'text' | 'markdown' | 'voice' | 'file' | 'image' | 'voice' | 'video' | 'textcard' | 'news' | 'mpnews' | 'miniprogram_notice' | 'template_card' 11 | export interface WechatAppConfig { 12 | /** 13 | * 企业ID,获取方式参考:[术语说明-corpid](https://work.weixin.qq.com/api/doc/90000/90135/91039#14953/corpid) 14 | * 15 | */ 16 | WECHAT_APP_CORPID: string 17 | /** 18 | * 应用的凭证密钥,获取方式参考:[术语说明-secret](https://work.weixin.qq.com/api/doc/90000/90135/91039#14953/secret) 19 | * 20 | */ 21 | WECHAT_APP_SECRET: string 22 | /** 23 | * 企业应用的id。企业内部开发,可在应用的设置页面查看 24 | * 25 | */ 26 | WECHAT_APP_AGENTID: number 27 | } 28 | 29 | export type WechatAppConfigSchema = ConfigSchema 30 | 31 | export const wechatAppConfigSchema: WechatAppConfigSchema = { 32 | WECHAT_APP_CORPID: { 33 | type: 'string', 34 | title: '企业ID', 35 | description: '企业ID,获取方式参考:[术语说明-corpid](https://work.weixin.qq.com/api/doc/90000/90135/91039#14953/corpid)', 36 | required: true, 37 | default: '', 38 | }, 39 | WECHAT_APP_SECRET: { 40 | type: 'string', 41 | title: '应用的凭证密钥', 42 | description: '应用的凭证密钥,获取方式参考:[术语说明-secret](https://work.weixin.qq.com/api/doc/90000/90135/91039#14953/secret)', 43 | required: true, 44 | default: '', 45 | }, 46 | WECHAT_APP_AGENTID: { 47 | type: 'number', 48 | title: '企业应用的id', 49 | description: '企业应用的id。企业内部开发,可在应用的设置页面查看', 50 | required: true, 51 | default: 0, 52 | }, 53 | } as const 54 | 55 | export type WechatAppOption = { 56 | // 消息类型 57 | msgtype: WechatAppMsgType 58 | // 表示是否是保密消息,0表示可对外分享,1表示不能,默认0。 59 | safe?: 0 | 1 60 | // 表示是否开启id转译,0表示否,1表示是,默认0。 61 | enable_id_trans?: 0 | 1 62 | // 表示是否开启重复消息检查,0表示否,1表示是,默认0 63 | enable_duplicate_check?: 0 | 1 64 | // 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时 65 | duplicate_check_interval?: number 66 | [key: string]: any 67 | // 指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个)。 68 | // 特殊情况:指定为"@all",则向该企业应用的全部成员发送 69 | touser?: string 70 | } & ({ 71 | // 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。 72 | // 当touser为"@all"时忽略本参数 73 | toparty?: string 74 | } | { 75 | // 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。 76 | // 当touser为"@all"时忽略本参数 77 | totag?: string 78 | }) 79 | 80 | export type WechatAppOptionSchema = OptionSchema 81 | export const wechatAppOptionSchema: WechatAppOptionSchema = { 82 | msgtype: { 83 | type: 'select', 84 | title: '消息类型', 85 | description: '消息类型', 86 | required: true, 87 | options: [ 88 | { 89 | label: '文本', 90 | value: 'text', 91 | }, 92 | { 93 | label: 'Markdown', 94 | value: 'markdown', 95 | }, 96 | { 97 | label: '语音', 98 | value: 'voice', 99 | }, 100 | { 101 | label: '文件', 102 | value: 'file', 103 | }, 104 | { 105 | label: '图片', 106 | value: 'image', 107 | }, 108 | { 109 | label: '视频', 110 | value: 'video', 111 | }, 112 | { 113 | label: '图文', 114 | value: 'news', 115 | }, 116 | { 117 | label: '小程序通知', 118 | value: 'miniprogram_notice', 119 | }, 120 | { 121 | label: '模板卡片', 122 | value: 'template_card', 123 | }, 124 | ], 125 | }, 126 | safe: { 127 | type: 'select', 128 | title: '是否是保密消息', 129 | description: '表示是否是保密消息,0表示可对外分享,1表示不能', 130 | required: false, 131 | options: [ 132 | { 133 | label: '否', 134 | value: 0, 135 | }, 136 | { 137 | label: '是', 138 | value: 1, 139 | }, 140 | ], 141 | }, 142 | enable_id_trans: { 143 | type: 'select', 144 | title: '是否开启id转译', 145 | description: '表示是否开启id转译,0表示否,1表示是,默认0。', 146 | required: false, 147 | options: [ 148 | { 149 | label: '否', 150 | value: 0, 151 | }, 152 | { 153 | label: '是', 154 | value: 1, 155 | }, 156 | ], 157 | }, 158 | enable_duplicate_check: { 159 | type: 'select', 160 | title: '是否开启重复消息检查', 161 | description: '表示是否开启重复消息检查,0表示否,1表示是,默认', 162 | required: false, 163 | options: [ 164 | { 165 | label: '否', 166 | value: 0, 167 | }, 168 | { 169 | label: '是', 170 | value: 1, 171 | }, 172 | ], 173 | }, 174 | duplicate_check_interval: { 175 | type: 'number', 176 | title: '重复消息检查的时间间隔', 177 | description: '表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时', 178 | required: false, 179 | }, 180 | touser: { 181 | type: 'string', 182 | title: '指定接收消息的成员', 183 | description: '指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个)。', 184 | required: false, 185 | }, 186 | toparty: { 187 | type: 'string', 188 | title: '指定接收消息的部门', 189 | description: '指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。', 190 | required: false, 191 | }, 192 | totag: { 193 | type: 'string', 194 | title: '指定接收消息的标签', 195 | description: '指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。', 196 | required: false, 197 | }, 198 | } as const 199 | 200 | export interface WechatAppResponse { 201 | // 企业微信返回的错误码,为0表示成功,非0表示调用失败 202 | errcode: number 203 | errmsg: string 204 | invaliduser?: string 205 | invalidparty?: string 206 | invalidtag?: string 207 | unlicenseduser?: string 208 | msgid: string 209 | response_code?: string 210 | } 211 | /** 212 | * 企业微信应用推送,文档:https://developer.work.weixin.qq.com/document/path/90664 213 | * 214 | * @author CaoMeiYouRen 215 | * @date 2021-02-28 216 | * @export 217 | * @class WechatApp 218 | */ 219 | export class WechatApp implements Send { 220 | 221 | static readonly namespace = '企业微信应用' 222 | static readonly configSchema = wechatAppConfigSchema 223 | static readonly optionSchema = wechatAppOptionSchema 224 | 225 | private ACCESS_TOKEN: string 226 | 227 | private WECHAT_APP_CORPID: string 228 | private WECHAT_APP_SECRET: string 229 | private WECHAT_APP_AGENTID: number 230 | 231 | /** 232 | * ACCESS_TOKEN 的过期时间(时间戳) 233 | * 234 | * @private 235 | */ 236 | private expiresTime: number 237 | 238 | constructor(config: WechatAppConfig) { 239 | Debugger('config: %O', config) 240 | Object.assign(this, config) 241 | // 根据 configSchema 验证 config 242 | validate(config, WechatApp.configSchema) 243 | } 244 | 245 | private async getAccessToken(): Promise { 246 | const { data } = await ajax({ 247 | url: 'https://qyapi.weixin.qq.com/cgi-bin/gettoken', 248 | query: { 249 | corpid: this.WECHAT_APP_CORPID, 250 | corpsecret: this.WECHAT_APP_SECRET, 251 | }, 252 | }) 253 | if (data?.errcode !== 0) { // 出错返回码,为0表示成功,非0表示调用失败 254 | throw new Error(data?.errmsg || '获取 access_token 失败!') 255 | } 256 | const { access_token, expires_in = 7200 } = data 257 | Debugger('获取 access_token 成功: %s', access_token) 258 | this.extendexpiresTime(expires_in) 259 | return access_token 260 | } 261 | /** 262 | * 延长过期时间 263 | * 264 | * @author CaoMeiYouRen 265 | * @date 2021-03-03 266 | * @private 267 | * @param expiresIn 延长的秒数 268 | */ 269 | private extendexpiresTime(expiresIn: number): void { 270 | this.expiresTime = Date.now() + expiresIn * 1000 // 设置过期时间 271 | } 272 | 273 | /** 274 | * 发送消息 275 | * 276 | * @author CaoMeiYouRen 277 | * @date 2024-11-08 278 | * @param title 消息标题 279 | * @param [desp] 消息内容,最长不超过2048个字节,超过将截断(支持id转译) 280 | * @param [option] 额外推送选项 281 | */ 282 | async send(title: string, desp?: string, option?: WechatAppOption): Promise> { 283 | Debugger('title: "%s", desp: "%s", option: %O', title, desp, option) 284 | if (!this.ACCESS_TOKEN || Date.now() >= this.expiresTime) { 285 | this.ACCESS_TOKEN = await this.getAccessToken() 286 | } 287 | const { msgtype = 'text', touser: _touser, ...args } = option || {} 288 | if (!_touser) { 289 | warn('未指定 touser,将使用 "@all" 向全体成员推送') 290 | } 291 | const sep = msgtype === 'markdown' ? '\n\n' : '\n' 292 | const content = `${title}${desp ? `${sep}${desp}` : ''}` 293 | const touser = _touser || '@all' // 如果没有指定 touser,则使用全体成员 294 | return ajax({ 295 | url: 'https://qyapi.weixin.qq.com/cgi-bin/message/send', 296 | method: 'POST', 297 | headers: { 298 | 'Content-Type': 'application/json', 299 | }, 300 | query: { 301 | access_token: this.ACCESS_TOKEN, 302 | }, 303 | data: { 304 | touser, 305 | msgtype, 306 | agentid: this.WECHAT_APP_AGENTID, 307 | [msgtype]: { 308 | content, 309 | }, 310 | ...args, 311 | }, 312 | }) 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/push/wechat-robot.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | 8 | const Debugger = debug('push:wechat-robot') 9 | 10 | export type WechatRobotMsgType = 'text' | 'markdown' | 'image' | 'news' | 'file' | 'voice' | 'template_card' 11 | 12 | export interface WechatRobotConfig { 13 | // 企业微信机器人的key 14 | WECHAT_ROBOT_KEY: string 15 | } 16 | export type WechatRobotConfigSchema = ConfigSchema 17 | export const wechatRobotConfigSchema: WechatRobotConfigSchema = { 18 | WECHAT_ROBOT_KEY: { 19 | type: 'string', 20 | title: '企业微信机器人的key', 21 | description: '企业微信机器人的key', 22 | required: true, 23 | }, 24 | } as const 25 | 26 | export interface WechatRobotOption { 27 | msgtype?: WechatRobotMsgType 28 | [key: string]: any 29 | } 30 | export type WechatRobotOptionSchema = OptionSchema 31 | export const wechatRobotOptionSchema: WechatRobotOptionSchema = { 32 | msgtype: { 33 | type: 'select', 34 | title: '消息类型', 35 | description: '消息类型', 36 | options: [ 37 | { 38 | label: '文本', 39 | value: 'text', 40 | }, 41 | { 42 | label: 'Markdown', 43 | value: 'markdown', 44 | }, 45 | { 46 | label: '图片', 47 | value: 'image', 48 | }, 49 | { 50 | label: '图文', 51 | value: 'news', 52 | }, 53 | { 54 | label: '文件', 55 | value: 'file', 56 | }, 57 | { 58 | label: '语音', 59 | value: 'voice', 60 | }, 61 | { 62 | label: '模板卡片', 63 | value: 'template_card', 64 | }, 65 | ], 66 | required: false, 67 | default: 'text', 68 | }, 69 | } as const 70 | 71 | export interface WechatRobotResponse { 72 | // 企业微信机器人返回的错误码,为0表示成功,非0表示调用失败 73 | errcode: number 74 | errmsg: string 75 | } 76 | 77 | /** 78 | * 企业微信群机器人。文档: [如何使用群机器人](https://developer.work.weixin.qq.com/document/path/91770) 79 | * 80 | * @author CaoMeiYouRen 81 | * @date 2021-02-28 82 | * @export 83 | * @class WechatRobot 84 | */ 85 | export class WechatRobot implements Send { 86 | 87 | static readonly namespace = '企业微信群机器人' 88 | static readonly configSchema = wechatRobotConfigSchema 89 | static readonly optionSchema = wechatRobotOptionSchema 90 | 91 | private WECHAT_ROBOT_KEY: string 92 | 93 | constructor(config: WechatRobotConfig) { 94 | const { WECHAT_ROBOT_KEY } = config 95 | this.WECHAT_ROBOT_KEY = WECHAT_ROBOT_KEY 96 | Debugger('set WECHAT_ROBOT_KEY: "%s"', WECHAT_ROBOT_KEY) 97 | // 根据 configSchema 验证 config 98 | validate(config, WechatRobot.configSchema) 99 | } 100 | 101 | /** 102 | * 103 | * 104 | * @author CaoMeiYouRen 105 | * @date 2024-11-08 106 | * @param title 消息标题 107 | * @param [desp] 消息内容。text内容,最长不超过2048个字节;markdown内容,最长不超过4096个字节;必须是utf8编码 108 | * @param [option] 额外推送选项 109 | */ 110 | async send(title: string, desp?: string, option?: WechatRobotOption): Promise> { 111 | Debugger('title: "%s", desp: "%s", option: %O', title, desp, option) 112 | const { msgtype = 'text', ...args } = option || {} 113 | const sep = msgtype === 'markdown' ? '\n\n' : '\n' 114 | const content = `${title}${desp ? `${sep}${desp}` : ''}` 115 | return ajax({ 116 | url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send', 117 | headers: { 118 | 'Content-Type': 'application/json', 119 | }, 120 | method: 'POST', 121 | query: { key: this.WECHAT_ROBOT_KEY }, 122 | data: { 123 | msgtype, 124 | [msgtype]: { content }, 125 | ...args, 126 | }, 127 | }) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/push/wx-pusher.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | import { uniq } from '@/utils/helper' 8 | 9 | const Debugger = debug('push:wx-pusher') 10 | 11 | export interface WxPusherConfig { 12 | /** 13 | * WxPusher 的 appToken。在 https://wxpusher.zjiecode.com/admin/main/app/appToken 申请 14 | */ 15 | WX_PUSHER_APP_TOKEN: string 16 | /** 17 | * WxPusher 的 uid。在 https://wxpusher.zjiecode.com/admin/main/wxuser/list 查看 18 | */ 19 | WX_PUSHER_UID: string 20 | } 21 | 22 | export type WxPusherConfigSchema = ConfigSchema 23 | 24 | export const wxPusherConfigSchema: WxPusherConfigSchema = { 25 | WX_PUSHER_APP_TOKEN: { 26 | type: 'string', 27 | title: 'appToken', 28 | description: '在 https://wxpusher.zjiecode.com/admin/main/app/appToken 申请', 29 | required: true, 30 | }, 31 | WX_PUSHER_UID: { 32 | type: 'string', 33 | title: 'uid', 34 | description: '在 https://wxpusher.zjiecode.com/admin/main/wxuser/list 查看', 35 | required: true, 36 | }, 37 | } as const 38 | 39 | export interface WxPusherOption { 40 | /** 41 | * 消息摘要,显示在微信聊天页面或者模版消息卡片上,限制长度20,可以不传,不传默认截取content前面的内容。 42 | */ 43 | summary?: string 44 | /** 45 | * 内容类型 1表示文字 2表示html(只发送body标签内部的数据即可,不包括body标签) 3表示markdown 46 | * @default 1 47 | */ 48 | contentType?: 1 | 2 | 3 49 | /** 50 | * 是否保存发送内容,1保存,0不保存 51 | * @default 0 52 | */ 53 | save?: 0 | 1 54 | /** 55 | * 主题ID,可以根据主题ID发送消息,可以在主题管理中查看主题ID 56 | */ 57 | topicIds?: number[] 58 | /** 59 | * 发送目标的UID,是一个数组。注意uids和topicIds可以同时填写,也可以只填写一个。 60 | */ 61 | uids?: string[] 62 | /** 63 | * 发送url,可以不传,如果传了,则根据url弹出通知 64 | */ 65 | url?: string 66 | /** 67 | * 验证负载,仅针对text消息类型有效 68 | */ 69 | verifyPayload?: string 70 | } 71 | 72 | export type WxPusherOptionSchema = OptionSchema 73 | 74 | export const wxPusherOptionSchema: WxPusherOptionSchema = { 75 | summary: { 76 | type: 'string', 77 | title: '消息摘要', 78 | description: '显示在微信聊天页面或者模版消息卡片上,限制长度20,可以不传,不传默认截取content前面的内容。', 79 | required: false, 80 | }, 81 | contentType: { 82 | type: 'select', 83 | title: '内容类型', 84 | description: '内容类型', 85 | required: false, 86 | default: 1, 87 | options: [ 88 | { 89 | label: '文本', 90 | value: 1, 91 | }, 92 | { 93 | label: 'HTML', 94 | value: 2, 95 | }, 96 | { 97 | label: 'Markdown', 98 | value: 3, 99 | }, 100 | ], 101 | }, 102 | save: { 103 | type: 'select', 104 | title: '是否保存发送内容', 105 | description: '是否保存发送内容,1保存,0不保存,默认0', 106 | required: false, 107 | default: 0, 108 | options: [ 109 | { 110 | label: '不保存', 111 | value: 0, 112 | }, 113 | { 114 | label: '保存', 115 | value: 1, 116 | }, 117 | ], 118 | }, 119 | topicIds: { 120 | type: 'array', 121 | title: '主题ID', 122 | description: '主题ID,可以根据主题ID发送消息,可以在主题管理中查看主题ID', 123 | required: false, 124 | }, 125 | uids: { 126 | type: 'array', 127 | title: '用户ID', 128 | description: '发送目标的UID,是一个数组。注意uids和topicIds可以同时填写,也可以只填写一个。', 129 | required: false, 130 | }, 131 | url: { 132 | type: 'string', 133 | title: '发送url', 134 | description: '发送url,可以不传,如果传了,则根据url弹出通知', 135 | required: false, 136 | }, 137 | verifyPayload: { 138 | type: 'string', 139 | title: '验证负载', 140 | description: '仅针对text消息类型有效', 141 | required: false, 142 | }, 143 | } as const 144 | 145 | export interface WxPusherResponse { 146 | /** 147 | * 请求是否成功 148 | */ 149 | success: boolean 150 | /** 151 | * 请求返回码 152 | */ 153 | code: number 154 | /** 155 | * 请求返回消息 156 | */ 157 | msg: string 158 | /** 159 | * 请求返回数据 160 | */ 161 | data: { 162 | /** 163 | * 消息ID 164 | */ 165 | messageId: number 166 | /** 167 | * 消息编码 168 | */ 169 | code: string 170 | } 171 | } 172 | 173 | /** 174 | * WxPusher 推送。官方文档:https://wxpusher.zjiecode.com/docs 175 | * 176 | * @author CaoMeiYouRen 177 | * @date 2024-11-09 178 | * @export 179 | * @class WxPusher 180 | */ 181 | export class WxPusher implements Send { 182 | static readonly namespace = 'WxPusher' 183 | static readonly configSchema = wxPusherConfigSchema 184 | static readonly optionSchema = wxPusherOptionSchema 185 | 186 | private WX_PUSHER_APP_TOKEN: string 187 | private WX_PUSHER_UID: string 188 | 189 | constructor(config: WxPusherConfig) { 190 | const { WX_PUSHER_APP_TOKEN, WX_PUSHER_UID } = config 191 | this.WX_PUSHER_APP_TOKEN = WX_PUSHER_APP_TOKEN 192 | this.WX_PUSHER_UID = WX_PUSHER_UID 193 | Debugger('set WX_PUSHER_APP_TOKEN: "%s", WX_PUSHER_UID: "%s"', WX_PUSHER_APP_TOKEN, WX_PUSHER_UID) 194 | // 根据 configSchema 验证 config 195 | validate(config, WxPusher.configSchema) 196 | } 197 | 198 | async send(title: string, desp?: string, option?: WxPusherOption): Promise> { 199 | Debugger('title: "%s", desp: "%s", option: %O', title, desp, option) 200 | const { contentType = 1, ...args } = option || {} 201 | const uids = uniq([...option.uids || [], this.WX_PUSHER_UID]) 202 | const content = `${title}${desp ? `\n${desp}` : ''}` 203 | return ajax({ 204 | url: 'https://wxpusher.zjiecode.com/api/send/message', 205 | method: 'POST', 206 | headers: { 207 | 'Content-Type': 'application/json', 208 | }, 209 | data: { 210 | ...args, 211 | appToken: this.WX_PUSHER_APP_TOKEN, 212 | content, 213 | contentType, 214 | uids, 215 | }, 216 | }) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/push/xi-zhi.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import { Send } from '@/interfaces/send' 3 | import { ajax } from '@/utils/ajax' 4 | import { SendResponse } from '@/interfaces/response' 5 | import { ConfigSchema, OptionSchema } from '@/interfaces/schema' 6 | import { validate } from '@/utils/validate' 7 | 8 | const Debugger = debug('push:xi-zhi') 9 | 10 | export interface XiZhiConfig { 11 | // 息知的 key,前往 https://xz.qqoq.net/#/index 获取 12 | XI_ZHI_KEY: string 13 | } 14 | 15 | export type XiZhiConfigSchema = ConfigSchema 16 | export const xiZhiConfigSchema: XiZhiConfigSchema = { 17 | XI_ZHI_KEY: { 18 | type: 'string', 19 | title: '息知的 key', 20 | description: '前往 https://xz.qqoq.net/#/index 获取', 21 | required: true, 22 | }, 23 | } as const 24 | 25 | export interface XiZhiOption { 26 | } 27 | 28 | export type XiZhiOptionSchema = OptionSchema 29 | export const xiZhiOptionSchema: XiZhiOptionSchema = { 30 | } as const 31 | 32 | export interface XiZhiResponse { 33 | // 状态码,200 表示成功 34 | code: number 35 | msg: string 36 | } 37 | 38 | /** 39 | * 息知 推送,官方文档:https://xz.qqoq.net/#/index 40 | * 41 | * @author CaoMeiYouRen 42 | * @date 2022-02-18 43 | * @export 44 | * @class XiZhi 45 | */ 46 | export class XiZhi implements Send { 47 | 48 | static readonly namespace = '息知' 49 | static readonly configSchema = xiZhiConfigSchema 50 | static readonly optionSchema = xiZhiOptionSchema 51 | 52 | private XI_ZHI_KEY: string 53 | 54 | constructor(config: XiZhiConfig) { 55 | const { XI_ZHI_KEY } = config 56 | this.XI_ZHI_KEY = XI_ZHI_KEY 57 | Debugger('set XI_ZHI_KEY: "%s"', XI_ZHI_KEY) 58 | // 根据 configSchema 验证 config 59 | validate(config, XiZhi.configSchema) 60 | } 61 | 62 | async send(title: string, desp?: string, option?: XiZhiOption): Promise> { 63 | Debugger('title: "%s", desp: "%s"', title, desp) 64 | return ajax({ 65 | url: `https://xizhi.qqoq.net/${this.XI_ZHI_KEY}.send`, 66 | method: 'POST', 67 | headers: { 68 | 'Content-Type': 'application/json', 69 | }, 70 | data: { 71 | title, 72 | content: desp, 73 | }, 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/ajax.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse, Method, AxiosRequestHeaders } from 'axios' 2 | import debug from 'debug' 3 | import { HttpsProxyAgent } from 'https-proxy-agent' 4 | import { SocksProxyAgent } from 'socks-proxy-agent' 5 | import { isHttpURL, isSocksUrl, logger } from './helper' 6 | 7 | const Debugger = debug('push:ajax') 8 | 9 | interface AjaxConfig { 10 | url: string 11 | query?: Record 12 | data?: Record | string | Buffer | ArrayBuffer 13 | method?: Method 14 | headers?: Record 15 | baseURL?: string 16 | proxyUrl?: string 17 | } 18 | 19 | /** 20 | * axios 接口封装 21 | * 22 | * @author CaoMeiYouRen 23 | * @date 2021-02-27 24 | * @export 25 | * @param config 26 | * @returns 27 | */ 28 | export async function ajax(config: AjaxConfig): Promise> { 29 | try { 30 | Debugger('ajax config: %O', config) 31 | const { url, query = {}, method = 'GET', baseURL = '', proxyUrl } = config 32 | const headers = (config.headers || {}) as AxiosRequestHeaders 33 | let { data = {} } = config 34 | 35 | if (headers['Content-Type'] === 'application/x-www-form-urlencoded' && typeof data === 'object') { 36 | data = new URLSearchParams(data as Record).toString() 37 | } 38 | 39 | let httpAgent = null 40 | Debugger('NO_PROXY: %s', process.env.NO_PROXY) 41 | if (process.env.NO_PROXY !== 'true') { 42 | Debugger('HTTP_PROXY: %s', process.env.HTTP_PROXY) 43 | Debugger('HTTPS_PROXY: %s', process.env.HTTPS_PROXY) 44 | Debugger('SOCKS_PROXY: %s', process.env.SOCKS_PROXY) 45 | if (isHttpURL(proxyUrl)) { 46 | httpAgent = new HttpsProxyAgent(proxyUrl) 47 | } else if (isSocksUrl(proxyUrl)) { 48 | httpAgent = new SocksProxyAgent(proxyUrl) 49 | } else if (process.env.HTTPS_PROXY) { 50 | httpAgent = new HttpsProxyAgent(process.env.HTTPS_PROXY) 51 | } else if (process.env.HTTP_PROXY) { 52 | httpAgent = new HttpsProxyAgent(process.env.HTTP_PROXY) 53 | } else if (process.env.SOCKS_PROXY) { 54 | httpAgent = new SocksProxyAgent(process.env.SOCKS_PROXY) 55 | } 56 | } 57 | const response = await axios(url, { 58 | baseURL, 59 | method, 60 | headers, 61 | params: query, 62 | data, 63 | timeout: 60000, 64 | httpAgent, 65 | httpsAgent: httpAgent, 66 | proxy: false, 67 | }) 68 | Debugger('response data: %O', response.data) 69 | return response 70 | } catch (error) { 71 | if (error?.response) { 72 | logger.error(error.response) 73 | return error.response 74 | } 75 | throw error 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/utils/crypto.test.ts: -------------------------------------------------------------------------------- 1 | import { generateSignature } from './crypto' 2 | // 生成 generateSignature 的jest 测试用例 3 | describe('generateSignature', () => { 4 | it('should generate correct signature', () => { 5 | const timestamp = '1604000000' 6 | const suiteTicket = 'suite_ticket' 7 | const suiteSecret = 'suite_secret' 8 | const expectedSignature = 'g6zSsTYaHijVbTCIDP2ypYviTry0T0m27zfbJfMQ++U=' 9 | const signature = generateSignature(timestamp, suiteTicket, suiteSecret) 10 | expect(signature).toBe(expectedSignature) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | 3 | /** 4 | * 生成钉钉签名 5 | * 6 | * @author CaoMeiYouRen 7 | * @date 2024-10-30 8 | * @export 9 | * @param timestamp 10 | * @param suiteTicket 11 | * @param suiteSecret 12 | */ 13 | export function generateSignature(timestamp: string | number, suiteTicket: string, suiteSecret: crypto.BinaryLike | crypto.KeyObject): string { 14 | // 创建要签名的字符串 15 | const stringToSign = `${timestamp}\n${suiteTicket}` 16 | 17 | // 创建 HMAC 实例 18 | const hmac = crypto.createHmac('sha256', suiteSecret) 19 | 20 | // 更新 HMAC 实例的数据 21 | hmac.update(stringToSign, 'utf8') 22 | 23 | // 计算 HMAC 签名并进行 Base64 编码 24 | const signature = hmac.digest('base64') 25 | 26 | return signature 27 | } 28 | 29 | export function base64Encode(str: string): string { 30 | return Buffer.from(str).toString('base64') 31 | } 32 | 33 | export function rfc2047Encode(str: string): string { 34 | return `=?utf-8?B?${base64Encode(str)}?=` 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/helper.test.ts: -------------------------------------------------------------------------------- 1 | import { isHttpURL, isSocksUrl, isNil, isNotNil, isEmpty, isNotEmpty } from './helper' 2 | 3 | describe('helper', () => { 4 | it('isHttpURL', () => { 5 | expect(isHttpURL('http://127.0.0.1')).toBe(true) 6 | expect(isHttpURL('https://127.0.0.1')).toBe(true) 7 | expect(isHttpURL('test')).toBe(false) 8 | }) 9 | it('isSocksUrl', () => { 10 | expect(isSocksUrl('socks5://127.0.0.1')).toBe(true) 11 | expect(isSocksUrl('socks://127.0.0.1')).toBe(true) 12 | expect(isSocksUrl('test')).toBe(false) 13 | }) 14 | it('isNil', () => { 15 | expect(isNil(null)).toBe(true) 16 | expect(isNil(undefined)).toBe(true) 17 | expect(isNil({})).toBe(false) 18 | }) 19 | it('isNotNil', () => { 20 | expect(isNotNil(null)).toBe(false) 21 | expect(isNotNil(undefined)).toBe(false) 22 | expect(isNotNil({})).toBe(true) 23 | }) 24 | it('isEmpty', () => { 25 | expect(isEmpty([])).toBe(false) 26 | expect(isEmpty({})).toBe(false) 27 | expect(isEmpty('')).toBe(true) 28 | expect(isEmpty(0)).toBe(false) 29 | expect(isEmpty(null)).toBe(true) 30 | expect(isEmpty(undefined)).toBe(true) 31 | expect(isEmpty({ a: 1 })).toBe(false) 32 | expect(isEmpty([1])).toBe(false) 33 | expect(isEmpty('test')).toBe(false) 34 | expect(isEmpty(1)).toBe(false) 35 | }) 36 | it('isNotEmpty', () => { 37 | expect(isNotEmpty([])).toBe(true) 38 | expect(isNotEmpty({})).toBe(true) 39 | expect(isNotEmpty('')).toBe(false) 40 | expect(isNotEmpty(0)).toBe(true) 41 | expect(isNotEmpty(null)).toBe(false) 42 | expect(isNotEmpty(undefined)).toBe(false) 43 | expect(isNotEmpty({ a: 1 })).toBe(true) 44 | expect(isNotEmpty([1])).toBe(true) 45 | expect(isNotEmpty('test')).toBe(true) 46 | expect(isNotEmpty(1)).toBe(true) 47 | }) 48 | }) 49 | 50 | -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | let colors: any 2 | 3 | if (globalThis.process && typeof globalThis.process.on === 'function') { 4 | try { 5 | colors = require('@colors/colors') 6 | } catch { 7 | import('@colors/colors').then((value) => { 8 | colors = value.default 9 | }).catch(console.error) 10 | } 11 | } 12 | 13 | export function warn(text: any): void { 14 | if (colors) { 15 | text = colors.yellow(text) 16 | } 17 | console.warn(text) 18 | } 19 | 20 | export function error(text: any): void { 21 | if (colors) { 22 | text = colors.red(text) 23 | } 24 | console.error(text) 25 | } 26 | 27 | export const logger = { 28 | warn, 29 | error, 30 | } 31 | 32 | /** 33 | * 检测是否为 http/https 开头的 url 34 | * @param url 35 | * @returns 36 | */ 37 | export const isHttpURL = (url: string): boolean => /^(https?:\/\/)/.test(url) 38 | 39 | /** 40 | * 检测是否为 socks/socks5 开头的 url 41 | * @param url 42 | * @returns 43 | */ 44 | export const isSocksUrl = (url: string): boolean => /^(socks5?:\/\/)/.test(url) 45 | 46 | /** 47 | * 判断是否为 null 或 undefined 48 | * @param value 49 | * @returns 50 | */ 51 | export function isNil(value: unknown): boolean { 52 | return value === null || value === undefined 53 | } 54 | 55 | /** 56 | * 判断是否不为 null 且不为 undefined 57 | * @param value 58 | * @returns 59 | */ 60 | export function isNotNil(value: unknown): boolean { 61 | return !isNil(value) 62 | } 63 | 64 | /** 65 | * 判断是否为 null 或 undefined 或 空字符串 66 | * @param value 67 | * @returns 68 | */ 69 | export function isEmpty(value: unknown): boolean { 70 | return value === null || value === undefined || value === '' 71 | } 72 | /** 73 | * 判断是否不为 null 且不为 undefined 且不为 空字符串 74 | * @param value 75 | * @returns 76 | */ 77 | export function isNotEmpty(value: unknown): boolean { 78 | return !isEmpty(value) 79 | } 80 | 81 | /** 82 | * 数组去重 83 | * 84 | * @author CaoMeiYouRen 85 | * @date 2025-03-05 86 | * @export 87 | * @template T 88 | * @param arr 89 | */ 90 | export function uniq(arr: T[]): T[] { 91 | return Array.from(new Set(arr)) 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/validate.test.ts: -------------------------------------------------------------------------------- 1 | import { validate } from './validate' 2 | import { ConfigSchema } from '@/interfaces/schema' 3 | 4 | describe('validate', () => { 5 | type TestConfigSchema = ConfigSchema<{ 6 | name: string 7 | age?: number 8 | isActive: boolean 9 | content?: 'text' | 'html' 10 | status: 1 | 2 | 3 11 | settings?: object 12 | list?: any[] 13 | }> 14 | // 定义一个示例配置和对应的 Schema 15 | const configSchema: TestConfigSchema = { 16 | name: { 17 | type: 'string', 18 | title: 'Name', 19 | description: 'User name', 20 | required: true, 21 | }, 22 | age: { 23 | type: 'number', 24 | title: 'Age', 25 | description: 'User age', 26 | required: false, 27 | }, 28 | isActive: { 29 | type: 'boolean', 30 | title: 'Active', 31 | description: 'Is user active', 32 | required: true, 33 | }, 34 | content: { 35 | type: 'select', 36 | title: 'Content', 37 | description: 'Content type', 38 | required: false, 39 | options: [ 40 | { label: 'Text', value: 'text' }, 41 | { label: 'HTML', value: 'html' }, 42 | ], 43 | }, 44 | status: { 45 | type: 'select', 46 | title: 'Status', 47 | description: 'User status', 48 | required: true, 49 | options: [ 50 | { label: '1', value: 1 }, 51 | { label: '2', value: 2 }, 52 | { label: '3', value: 3 }, 53 | ], 54 | }, 55 | settings: { 56 | type: 'object', 57 | title: 'Settings', 58 | description: 'Settings object', 59 | required: false, 60 | }, 61 | list: { 62 | type: 'array', 63 | title: 'List', 64 | description: 'List of items', 65 | required: false, 66 | default: [], 67 | }, 68 | } 69 | 70 | // 测试必填字段 71 | it('should throw an error if a required field is missing', () => { 72 | const config = { 73 | age: 25, 74 | isActive: true, 75 | content: 'text', 76 | status: 1, 77 | } 78 | 79 | expect(() => validate(config as any, configSchema as any)).toThrow('"name" 字段是必须的!') 80 | }) 81 | 82 | // 测试非必填字段 83 | it('should not throw an error if an optional field is missing', () => { 84 | const config = { 85 | name: 'John', 86 | isActive: true, 87 | status: 1, 88 | } 89 | 90 | expect(() => validate(config as any, configSchema as any)).not.toThrow() 91 | }) 92 | 93 | // 测试字段类型验证 94 | it('should throw an error if a field type is incorrect', () => { 95 | const config = { 96 | name: 'John', 97 | age: '25', // 应该是 number 98 | isActive: true, 99 | status: 1, 100 | } 101 | 102 | expect(() => validate(config as any, configSchema as any)).toThrow('"age" 字段必须是数字!') 103 | }) 104 | 105 | // 测试联合类型验证 106 | it('should throw an error if a select field value is not in options', () => { 107 | const config = { 108 | name: 'John', 109 | age: 25, 110 | isActive: true, 111 | content: 'xml', // 应该是 'text' 或 'html' 112 | status: 1, 113 | } 114 | 115 | expect(() => validate(config as any, configSchema as any)).toThrow('"content" 字段必须是以下选项之一:text,html') 116 | }) 117 | 118 | // 测试数组类型验证 119 | it('should throw an error if an array field is not an array', () => { 120 | const config = { 121 | name: 'John', 122 | age: 25, 123 | isActive: true, 124 | content: 'text', 125 | status: 1, 126 | list: 'not an array', // 应该是数组 127 | } 128 | 129 | expect(() => validate(config as any, configSchema as any)).toThrow('"list" 字段必须是数组!') 130 | }) 131 | 132 | // 测试对象类型验证 133 | it('should throw an error if an object field is not an object', () => { 134 | const config = { 135 | name: 'John', 136 | age: 25, 137 | isActive: true, 138 | content: 'text', 139 | status: 1, 140 | list: [], 141 | settings: 'not an object', // 应该是对象 142 | } 143 | 144 | expect(() => validate(config as any, configSchema as any)).toThrow('"settings" 字段必须是对象!') 145 | }) 146 | 147 | // 测试布尔类型验证 148 | it('should throw an error if a boolean field is not a boolean', () => { 149 | const config = { 150 | name: 'John', 151 | age: 25, 152 | isActive: 'true', // 应该是布尔值 153 | status: 1, 154 | } 155 | 156 | expect(() => validate(config as any, configSchema as any)).toThrow('"isActive" 字段必须是布尔值!') 157 | }) 158 | 159 | // 测试字符串类型验证 160 | it('should throw an error if a string field is not a string', () => { 161 | const config = { 162 | name: 123, // 应该是字符串 163 | age: 25, 164 | isActive: true, 165 | status: 1, 166 | } 167 | 168 | expect(() => validate(config as any, configSchema as any)).toThrow('"name" 字段必须是字符串!') 169 | }) 170 | }) 171 | -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from './helper' 2 | import { Config, ConfigSchema } from '@/interfaces/schema' 3 | 4 | /** 5 | * 验证配置是否符合 Schema 规则,如果不符合则抛出错误 6 | * 7 | * @author CaoMeiYouRen 8 | * @date 2024-11-17 9 | * @export 10 | * @template T 11 | * @param config 12 | * @param schema 13 | */ 14 | export function validate(config: T, schema: ConfigSchema): void { 15 | Object.keys(schema).forEach((key) => { 16 | const item = schema[key] 17 | const value = config[key] 18 | if (!item.required && isEmpty(value)) { 19 | return 20 | } 21 | if (item.required && isEmpty(value)) { 22 | throw new Error(`"${key}" 字段是必须的!`) 23 | } 24 | if (item.type === 'select') { 25 | const { options } = item as any 26 | if (!options.map((e) => e.value).includes(value)) { 27 | throw new Error(`"${key}" 字段必须是以下选项之一:${options.map((e) => e.value).join(',')}`) 28 | } 29 | return 30 | } 31 | if (item.type === 'string') { 32 | if (typeof value !== 'string') { 33 | throw new Error(`"${key}" 字段必须是字符串!`) 34 | } 35 | return 36 | } 37 | if (item.type === 'number') { 38 | if (typeof value !== 'number') { 39 | throw new Error(`"${key}" 字段必须是数字!`) 40 | } 41 | return 42 | } 43 | if (item.type === 'boolean') { 44 | if (typeof value !== 'boolean') { 45 | throw new Error(`"${key}" 字段必须是布尔值!`) 46 | } 47 | return 48 | } 49 | if (item.type === 'array') { 50 | if (!Array.isArray(value)) { 51 | throw new Error(`"${key}" 字段必须是数组!`) 52 | } 53 | return 54 | } 55 | if (item.type === 'object') { 56 | if (typeof value !== 'object') { 57 | throw new Error(`"${key}" 字段必须是对象!`) 58 | } 59 | return 60 | } 61 | throw new Error(`"${key}" 字段类型不支持!`) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", //指定生成哪个模块系统代码 4 | "target": "esnext", //目标代码类型 5 | "noImplicitAny": false, //在表达式和声明上有隐含的'any'类型时报错。 6 | "allowJs": true, //允许编译js文件 7 | "checkJs": true, //在 .js文件中报告错误。与 --allowJs配合使用。 8 | "sourceMap": false, //用于debug 9 | "rootDir": "./src", //仅用来控制输出的目录结构--outDir。 10 | "outDir": "./dist", //重定向输出目录。 11 | "declaration": true, //生成类型文件 12 | "importHelpers": true, 13 | "esModuleInterop": true, 14 | "moduleResolution": "node", 15 | "strictPropertyInitialization": false, 16 | "allowSyntheticDefaultImports": true, 17 | "experimentalDecorators": true, //开启装饰器 18 | "removeComments": false, 19 | "baseUrl": ".", 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ], 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "src/*.ts", 34 | "src/**/*.ts", 35 | ], 36 | "exclude": [ 37 | "node_modules", 38 | "**/*.spec.ts", 39 | "**/node_modules/*" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | platform: 'node', // 目标平台 5 | entry: ['src/index.ts'], 6 | format: ['cjs', 'esm'], 7 | outExtension({ format }) { 8 | switch (format) { 9 | case 'cjs': 10 | return { 11 | js: '.cjs', 12 | dts: '.d.ts', 13 | } 14 | case 'esm': 15 | return { 16 | js: '.mjs', 17 | dts: '.d.ts', 18 | } 19 | case 'iife': 20 | return { 21 | js: '.global.js', 22 | dts: '.d.ts', 23 | } 24 | default: 25 | break 26 | } 27 | return { 28 | js: '.js', 29 | dts: '.d.ts', 30 | } 31 | }, 32 | splitting: false, // 代码拆分 33 | sourcemap: true, 34 | clean: true, 35 | dts: true, 36 | minify: false, // 缩小输出 37 | shims: true, // 注入 cjs 和 esm 填充代码,解决 import.meta.url 和 __dirname 的兼容问题 38 | esbuildOptions(options, context) { // 设置编码格式 39 | options.charset = 'utf8' 40 | }, 41 | // external: [], // 排除的依赖项 42 | // noExternal: [/(.*)/], // 将依赖打包到一个文件中 43 | // bundle: true, 44 | }) 45 | --------------------------------------------------------------------------------