├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
371 |
372 |
373 | ## 🌟 Star History
374 |
375 | [](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 |
--------------------------------------------------------------------------------