├── .nvmrc ├── src ├── shadow │ ├── guard.wasm │ ├── const.js │ ├── guard.js │ └── index.js ├── notifier │ ├── vendor │ │ ├── lark.js │ │ ├── server-chan.js │ │ ├── telegram.js │ │ ├── pushplus.js │ │ ├── qmsg.js │ │ ├── wxpusher.js │ │ ├── bark.js │ │ ├── dingtalk.js │ │ └── work-wechat.js │ ├── util.js │ └── index.js ├── coupons │ ├── const.js │ ├── index.js │ ├── gundam.js │ ├── wxfwh.js │ └── lottery.js ├── util │ └── index.js ├── update-notifier.js ├── user.js ├── template.js └── request.js ├── .prettierrc.yml ├── .gitignore ├── vite.config.js ├── docs ├── notify │ ├── Server酱.md │ ├── Pushplus.md │ ├── Qmsg酱.md │ ├── WxPusher.md │ ├── Bark.md │ ├── 钉钉.md │ ├── Telegram.md │ ├── 飞书.md │ └── 企业微信.md ├── 更新.md ├── 获取token.md ├── 本地运行.md ├── 部署.md ├── 通知.md └── token配置.md ├── test ├── update.js ├── notify.js ├── template.js ├── shadow.js ├── payload.js ├── coupons.js └── user.js ├── eslint.config.js ├── .editorconfig ├── CONTRIBUTING.md ├── LICENSE ├── package.json ├── .github └── workflows │ └── grab-coupon.yml ├── sync-upstream.js ├── CHANGELOG.md ├── README.md ├── index.js └── pnpm-lock.yaml /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /src/shadow/guard.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vv314/actions-mtz-coupons/HEAD/src/shadow/guard.wasm -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | semi: false 2 | tabWidth: 2 3 | printWidth: 80 4 | singleQuote: true 5 | trailingComma: none 6 | -------------------------------------------------------------------------------- /src/shadow/const.js: -------------------------------------------------------------------------------- 1 | export const yodaReady = 'h5' 2 | export const csecPlatform = 4 3 | export const guardVersion = '2.4.0' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | npm-debug.log* 3 | yarn-debug.log* 4 | yarn-error.log* 5 | .DS_Store 6 | node_modules/ 7 | .env 8 | .env.test 9 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | setupFiles: ['dotenv/config'], 6 | include: ['test/**/*.[jt]s'] 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /docs/notify/Server酱.md: -------------------------------------------------------------------------------- 1 | # Server 酱 2 | 3 | [Server 酱](https://sct.ftqq.com) 是一款从服务器、路由器等设备上推消息到手机的工具。 4 | 5 | ## 获取 SendKey 6 | 7 | 打开 Server 酱 [SendKey](https://sct.ftqq.com/sendkey) 页面,获取 `SendKey` 8 | 9 | 10 | 11 | ## 全局通知配置 12 | 13 | 进入项目 "Settings" → "Secrets" 配置页,点击 `New repository secret` 14 | 15 | - 新建 `SC_SEND_KEY` 项,填入 `SendKey` 16 | -------------------------------------------------------------------------------- /docs/更新.md: -------------------------------------------------------------------------------- 1 | # 更新 2 | 3 | 此项目将长期维护,为了确保副本能够及时享受到上游更新,请定期执行同步操作。 4 | 5 | ## 一键同步 6 | 7 | 推荐使用项目内置的 `sync` 脚本同步上游变更。 8 | 9 | 执行以下命令一键同步: 10 | 11 | ```bash 12 | pnpm run sync 13 | ``` 14 | 15 | 脚本执行后会拉取上游仓库的最新主分支代码,与本地主分支进行合并,最后合并结果同步到远程仓库。 16 | 17 | ## 手动同步 18 | 19 | 参考 Github 官方文档 [同步复刻](https://docs.github.com/cn/github/collaborating-with-issues-and-pull-requests/syncing-a-fork) 20 | -------------------------------------------------------------------------------- /docs/notify/Pushplus.md: -------------------------------------------------------------------------------- 1 | # pushplus 2 | 3 | [pushplus](https://www.pushplus.plus/) 推送加。是一个集成了微信、企业微信、钉钉、短信、邮件等渠道的信息推送平台。 4 | 5 | ## 获取 pushplus token 6 | 7 | 进入 [pushplus 官网](https://www.pushplus.plus/push1.html),登录后获取 pushplus `token` 8 | 9 | 10 | 11 | ## 全局通知配置 12 | 13 | 进入项目 "Settings" → "Secrets" 配置页,点击 `New repository secret` 14 | 15 | - 新建 `PUSHPLUS_TOKEN` 项,填入 `token` 16 | -------------------------------------------------------------------------------- /test/update.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import updateNotifier from '../src/update-notifier.js' 3 | 4 | test('Update', () => { 5 | const timeout = 5000 6 | 7 | return expect(updateNotifier(timeout)).resolves.toBeDefined() 8 | }) 9 | 10 | test('Update Timeout', () => { 11 | const timeout = 1 12 | 13 | return expect(updateNotifier(timeout)).rejects.toMatch('请求超时') 14 | }) 15 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | 4 | export default [ 5 | js.configs.recommended, 6 | { 7 | languageOptions: { 8 | globals: { 9 | ...globals.nodeBuiltin, 10 | Javy: 'readonly' 11 | } 12 | }, 13 | rules: { 14 | 'no-unused-vars': 'warn' 15 | } 16 | }, 17 | { 18 | files: ['test/**'] 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | 17 | [*.yml] 18 | indent_style = space 19 | 20 | [package.json] 21 | indent_style = space 22 | indent_size = 2 23 | insert_final_newline = false -------------------------------------------------------------------------------- /src/notifier/vendor/lark.js: -------------------------------------------------------------------------------- 1 | import { doPost } from '../util.js' 2 | 3 | async function sendLark({ title = '', content = '', webhook }) { 4 | const data = { 5 | mtz: { 6 | title, 7 | content 8 | } 9 | } 10 | 11 | return doPost(webhook, data) 12 | .then((res) => { 13 | if (res.code) throw res.msg 14 | }) 15 | .then(() => ({ success: true, msg: '飞书推送成功' })) 16 | .catch((e) => ({ success: false, msg: `飞书推送失败: ${e}` })) 17 | } 18 | 19 | export default sendLark 20 | -------------------------------------------------------------------------------- /src/notifier/vendor/server-chan.js: -------------------------------------------------------------------------------- 1 | import { doPost } from '../util.js' 2 | 3 | async function sendServerChan({ title = '', content = '', token }) { 4 | const api = token.includes('SCT') ? 'sctapi' : 'sc' 5 | const url = `https://${api}.ftqq.com/${token}.send` 6 | const data = { title, desp: content } 7 | 8 | return doPost(url, data, 'form') 9 | .then((res) => ({ success: true, msg: 'Server 酱推送成功' })) 10 | .catch((e) => ({ success: false, msg: `Server 酱推送失败: ${e}` })) 11 | } 12 | 13 | export default sendServerChan 14 | -------------------------------------------------------------------------------- /src/notifier/vendor/telegram.js: -------------------------------------------------------------------------------- 1 | import { doPost } from '../util.js' 2 | 3 | async function sendServerChan({ title = '', content = '', botToken, user }) { 4 | const url = `https://api.telegram.org/bot${botToken}/sendMessage` 5 | const data = { 6 | chat_id: user, 7 | text: `【${title}】\n${content}` 8 | } 9 | 10 | return doPost(url, data, 'form') 11 | .then((res) => ({ success: true, msg: 'Telegram 推送成功' })) 12 | .catch((e) => ({ success: false, msg: `Telegram 推送失败: ${e}` })) 13 | } 14 | 15 | export default sendServerChan 16 | -------------------------------------------------------------------------------- /src/notifier/vendor/pushplus.js: -------------------------------------------------------------------------------- 1 | import { doPost } from '../util.js' 2 | 3 | async function sendPushplus({ title = '', content = '', token }) { 4 | const url = `https://www.pushplus.plus/send` 5 | const data = { token, title, content: content, template: 'txt' } 6 | 7 | return doPost(url, data) 8 | .then((res) => { 9 | if (res.code != 200) throw res.msg 10 | 11 | return { success: true, msg: 'pushplus 推送成功' } 12 | }) 13 | .catch((e) => ({ success: false, msg: `pushplus 推送失败: ${e}` })) 14 | } 15 | 16 | export default sendPushplus 17 | -------------------------------------------------------------------------------- /src/notifier/vendor/qmsg.js: -------------------------------------------------------------------------------- 1 | import { doPost } from '../util.js' 2 | 3 | async function sendPushplus({ token, title = '', content = '', qq }) { 4 | const url = `https://qmsg.zendee.cn/send/${token}` 5 | const data = { 6 | qq, 7 | msg: `【${title}】\n${content}` 8 | } 9 | 10 | return doPost(url, data, 'form') 11 | .then((res) => { 12 | if (!res.success) throw res.reason 13 | 14 | return { success: true, msg: 'qmsg 推送成功' } 15 | }) 16 | .catch((e) => ({ success: false, msg: `qmsg 推送失败: ${e}` })) 17 | } 18 | 19 | export default sendPushplus 20 | -------------------------------------------------------------------------------- /docs/notify/Qmsg酱.md: -------------------------------------------------------------------------------- 1 | # Qmsg 酱 2 | 3 | [Qmsg 酱](https://qmsg.zendee.cn/) 是一个 QQ 消息推送平台。 4 | 5 | ## 获取 qmsg key 6 | 7 | 进入 Qmsg 酱[管理台](https://qmsg.zendee.cn/me.html)。登录后获取推送 `key` 8 | 9 | 10 | 11 | ## 添加 qmsg key 12 | 13 | 进入项目 "Settings" → "Secrets" 配置页,点击 `New repository secret` 14 | 15 | - 新建 `QMSG_KEY` 项,填入 `key` 16 | 17 | ## 用户通知配置 18 | 19 | > [!NOTE] 20 | > 需使用 [JSON Token 配置格式](./token配置.md)。 21 | 22 | `TOKEN` 中添加 `qq` 属性,填入用户 QQ 号。 23 | 24 | ## 全局通知配置 25 | 26 | 进入项目 "Settings" → "Secrets" 配置页,点击 `New repository secret` 27 | 28 | - 新建 `QMSG_ADMIN` 项,填入 qq 号 29 | -------------------------------------------------------------------------------- /docs/notify/WxPusher.md: -------------------------------------------------------------------------------- 1 | # wxpusher 通知 2 | 3 | [wxpusher](https://wxpusher.zjiecode.com/docs/#/) 是一个微信的消息推送平台。 4 | 5 | ## 后台微信登录后创建应用 6 | 7 | 进入 [wxpusher 官网](https://wxpusher.zjiecode.com/admin/login),扫码登录后创建一个应用 8 | 9 | ## 获取 wxpusher token 10 | 11 | 在你创建应用的过程中,你应该已经看到appToken,如果没有保存,可以通过侧边栏的菜单编辑应用重制它。 12 | 13 | ## 获取 topicId 14 | 15 | 创建完应用后,为你的应用创建一个主题(topic),然后就可以获取到 topicId。 16 | 17 | 18 | 19 | ## 全局通知配置 20 | 21 | 进入项目 "Settings" → "Secrets" 配置页,点击 `New repository secret` 22 | 23 | - 新建 `WXPUSHER_TOKEN` 项,填入 `token` 24 | - 新建 `WXPUSHER_TOPICID` 项,填入 `topicId` 25 | -------------------------------------------------------------------------------- /src/notifier/vendor/wxpusher.js: -------------------------------------------------------------------------------- 1 | import { doPost } from '../util.js' 2 | 3 | async function sendWxPusher({ title = '', content = '', token, topicId }) { 4 | const url = `https://wxpusher.zjiecode.com/api/send/message` 5 | const data = { 6 | appToken: token, 7 | content: content, 8 | summary: title, 9 | contentType: 1, 10 | topicIds: [topicId + ''] 11 | } 12 | 13 | return doPost(url, data) 14 | .then((res) => { 15 | return res 16 | }) 17 | .catch((e) => ({ success: false, msg: `wxpusher 推送失败: ${e}` })) 18 | } 19 | 20 | export default sendWxPusher 21 | -------------------------------------------------------------------------------- /docs/获取token.md: -------------------------------------------------------------------------------- 1 | # 获取 Token 2 | 3 | 打开[美团个人主页](https://i.meituan.com/mttouch/page/account),登录后使用 Chrome 开发者工具,或任意抓包工具获取 cookie 信息,提取 `token` 字段值。 4 | 5 | ## 使用 Chrome 开发者工具 6 | 7 | 1. 打开 Chrome 开发者工具,切换至移动设备调试模式 8 | 2. 打开[美团个人主页](https://i.meituan.com/mttouch/page/account),登录账号 9 | 3. 在 Chrome 开发者工具中点击 “应用” - “Cookie”,搜索 “token” 10 | 11 | ![Chrome DevTools](https://github.com/vv314/actions-mtz-coupons/assets/7637375/6677e9a8-95b8-4b96-83c2-33e443e26e36) 12 | 13 | **示例:** 14 | 15 | ``` 16 | Js3xxxxFyy_Aq-rOnxMtw6vKPV4AAAAA6QwAADgqRBSfcmNqyu777Q7JDL7xxxxNGbfF7tPNV5347_ANLcydac_MTWMTTL_xx 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/notify/Bark.md: -------------------------------------------------------------------------------- 1 | # Bark 2 | 3 | [Bark](https://apps.apple.com/cn/app/id1403753865) 是一款可以接收自定义通知的 iOS 应用 4 | 5 | ## 获取推送 key 6 | 7 | 打开 Bark App 查看推送 url: 8 | 9 | ``` 10 | URL 组成:host/:key/:body 11 | 示例: https://api.day.app/kkWwxxxq5NpWx/推送内容... 12 | 13 | host: 服务域名 14 | key: 推送 key,设备唯一标识 15 | body: 自定义推送内容 16 | ``` 17 | 18 | 提取推送 `key`,本例为 `kkWwxxxq5NpWx` 19 | 20 | ## 用户通知配置 21 | 22 | > [!NOTE] 23 | > 需使用 [JSON Token 配置格式](./token配置.md)。 24 | 25 | `TOKEN` Secret 配置为 JSON 格式,添加 `barkKey` 属性,填入推送 key。 26 | 27 | ## 全局通知配置 28 | 29 | 进入项目 "Settings" → "Secrets" 配置页,点击 `New repository secret` 30 | 31 | - 新建 `BARK_KEY` 项,填入推送 key 32 | -------------------------------------------------------------------------------- /docs/notify/钉钉.md: -------------------------------------------------------------------------------- 1 | # 钉钉 2 | 3 | [钉钉](https://www.dingtalk.com/)是由阿里巴巴集团开发的智能移动办公平台,用于商务沟通和工作协同。 4 | 5 | **注意:当设置 “加签” 时,需按照`secret\|webhook` 的格式将 secret 拼接至 webhook 之前(两者以 `|` 分隔)** 6 | 7 | ## 创建钉钉机器人 8 | 9 | 1. [创建](https://oa.dingtalk.com/register_new.htm)一个团队(群聊) 10 | 2. 打开 PC 端钉钉,在群设置中选择 “智能群助手” → “添加机器人” → “自定义” 11 | 3. 填写机器人自定义名称,配置安全设置。安全设置支持以下两种方式: 12 | 1. 自定义关键词:填入 `外卖` 13 | 2. 加签 14 | 4. 复制 webhook 地址 15 | 16 | 17 | 18 | ## 用户通知配置 19 | 20 | > [!NOTE] 21 | > 需使用 [JSON Token 配置格式](./token配置.md)。 22 | 23 | `TOKEN` 中添加 `dtWebhook` 属性,填入`webhook 地址`。 24 | 25 | ## 全局通知配置 26 | 27 | 进入项目 "Settings" → "Secrets" 配置页,点击 `New repository secret` 28 | 29 | - 新建 `DINGTALK_WEBHOOK` 项,填入`webhook 地址` 30 | -------------------------------------------------------------------------------- /src/notifier/vendor/bark.js: -------------------------------------------------------------------------------- 1 | import { doGet } from '../util.js' 2 | 3 | async function sendBark({ title = '', content = '', link, pushKey }) { 4 | const url = `https://api.day.app/${pushKey}/` 5 | const path = [title, content].map(encodeURIComponent).join('/') 6 | const icon = 7 | 'https://github-production-user-asset-6210df.s3.amazonaws.com/7637375/291613192-73636f3d-7271-4802-b0fe-328c479c1e35.png' 8 | let data 9 | 10 | if (link) { 11 | data = { url: link } 12 | } 13 | 14 | return doGet(`${url}${path}?icon=${icon}`, data) 15 | .then((res) => ({ success: true, msg: 'Bark 推送成功' })) 16 | .catch((e) => ({ success: false, msg: `Bark 推送失败: ${e}` })) 17 | } 18 | 19 | export default sendBark 20 | -------------------------------------------------------------------------------- /src/coupons/const.js: -------------------------------------------------------------------------------- 1 | const ECODE = { 2 | SUCC: 0, 3 | AUTH: 1, 4 | API: 2, 5 | NETWOEK: 3, 6 | RUNTIME: 4 7 | } 8 | 9 | const mainActConf = { gid: '2KAWnD', name: '外卖红包天天领' } 10 | 11 | // 抽奖活动 12 | const lotteryActConfs = [ 13 | { gid: '1VlhFT', name: '美团神会员' } 14 | // { gid: '2NjTfR', name: '公众号周三外卖节专属福利' } 15 | ] 16 | 17 | // 神券活动 18 | const gundamActConfs = [ 19 | { gid: '4luWGh', name: '品质优惠天天领' }, 20 | { gid: '4JZIgf', name: '冬日美食季' } 21 | ] 22 | 23 | // 社群活动 24 | const wxfwhActConfs = [ 25 | // { gid: '1C0wLz', name: '天天神券服务号专属福利' }, 26 | { gid: '1I9uL6', name: '社群专属福利' }, 27 | { gid: '1HgnjG', name: '神奇福利社' } 28 | ] 29 | 30 | export { ECODE, gundamActConfs, mainActConf, wxfwhActConfs, lotteryActConfs } 31 | -------------------------------------------------------------------------------- /src/notifier/util.js: -------------------------------------------------------------------------------- 1 | import timeoutSignal from 'timeout-signal' 2 | 3 | function doGet(url, data) { 4 | const params = new URLSearchParams(data) 5 | 6 | return fetch(`${url}?${params.toString()}`, { 7 | signal: timeoutSignal(10000) 8 | }).then((res) => res.json()) 9 | } 10 | 11 | function doPost(url, data, type = 'json') { 12 | let cType, body 13 | 14 | if (type == 'json') { 15 | cType = 'application/json' 16 | body = JSON.stringify(data) 17 | } else { 18 | const params = new URLSearchParams(data) 19 | 20 | cType = 'application/x-www-form-urlencoded' 21 | body = params.toString() 22 | } 23 | 24 | return fetch(url, { 25 | method: 'POST', 26 | body: body, 27 | headers: { 28 | 'Content-Type': cType 29 | }, 30 | signal: timeoutSignal(10000) 31 | }).then((res) => res.json()) 32 | } 33 | 34 | export { doGet, doPost } 35 | -------------------------------------------------------------------------------- /docs/本地运行.md: -------------------------------------------------------------------------------- 1 | # 本地运行 2 | 3 | ## 配置文件 4 | 5 | 项目使用 [dotenv](https://github.com/motdotla/dotenv) 储存本地配置。 6 | 7 | 在项目根目录下创建 `.env` 文件,填入 `Secrets` 信息。 8 | 9 | **示例:** 10 | 11 | ```bash 12 | # 美团 cookie token 13 | TOKEN=token=Js3xxxxFyy_Aq-rOnxMte6vKPV4AAAAA6QwAADgqRBSfcmNqyuG8CQ7JDL7xxxxNGbfF7tPNV5347_ANLcydua_JHCSRj0_xxxg9xx; 14 | # bark 推送 key 15 | BARK_KEY=kkWwxxxq5NpWx 16 | # telegram bot token 17 | TG_BOT_TOKEN=1689581149:AAGYVVjEHsaNxxxT8eQxxxshwr2o4Pxxxu86 18 | # telegram 用户 ID 19 | TG_USER_ID=100000000 20 | # server 酱 SendKey 21 | SC_SEND_KEY=SCTxxxxxTPIvAYxxxxxXjGGzvCfUxxxxxx 22 | # 企业微信配置 23 | QYWX_SEND_CONF={"agentId": "1000002", "corpId": "wwxxxe9ddxxxc50xxx", "corpSecret": "12Qxxxo4hxxxyedtxxxdyfVxxxCqh6xxxF0zg3xxxNI", "toUser": "@all"} 24 | # 钉钉 webhook (加签) 25 | SEC69162axxxf59sdss23|https://oapi.dingtalk.com/robot/send?access_token=09bsdfa66xxxa608bsds 26 | ``` 27 | 28 | ### 运行命令 29 | 30 | 项目根目录下执行: 31 | 32 | ```bash 33 | pnpm start:local 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/部署.md: -------------------------------------------------------------------------------- 1 | # 部署 2 | 3 | ## 使用 [GitHub Actions](https://docs.github.com/cn/actions) 部署 4 | 5 | ## 1. Fork 源项目 6 | 7 | 1. 访问 [actions-mtz-coupons](https://github.com/vv314/actions-mtz-coupons) 源仓库 8 | 2. 点击右上角 `Star` 按钮 ;) 9 | 3. 点击右上角 `Fork` 按钮 10 | 11 | ## 2. 添加 Actions secrets 12 | 13 | 1. 导航到 Fork 后的仓库主页面 14 | 2. 在仓库菜单栏中,点击 `⚙️Settings` 15 | 3. 点击侧边栏 `Secrets and variables - Actions`条目 16 | 4. 点击 `New repository secret` 创建仓库密码 17 | 1. 在 `Name` 输入框中填入 `TOKEN` 18 | 2. 在 `Secret` 输入框中填入从 cookie 中提取的 token 值(详见下文 TOKEN 配置) 19 | 5. 点击 `Add secret` 保存配置 20 | 21 | _Fork 后的项目可执行 `npm run sync` 同步上游更新,详细参考【脚本更新】章节。_ 22 | 23 | ## 脚本触发方式 24 | 25 | Github Actions 工作流支持**手动**与**自动**两种触发方式。 26 | 27 | ### 自动触发 28 | 29 | 每日 `11:00` 前定时执行(已开启)。 30 | 31 | ### 手动触发 32 | 33 | - [在项目主页上调用](https://docs.github.com/cn/actions/managing-workflow-runs/manually-running-a-workflow#) 34 | - [使用 REST API 调用](https://docs.github.com/cn/rest/reference/actions#create-a-workflow-dispatch-event) 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 参与贡献 2 | 3 | 1. [Fork 项目](https://help.github.com/articles/fork-a-repo/) 4 | 2. 安装依赖 (`pnpm install`) 5 | 3. 创建你的特性分支 (`git checkout -b my-new-feature`) 6 | 4. 提交你的修改 (`git commit -am 'Added some feature'`) 7 | 5. 测试你的修改 (`pnpm test`) 8 | 6. 推送分支 (`git push origin my-new-feature`) 9 | 7. [创建 Pull Request](https://help.github.com/articles/creating-a-pull-request/) 10 | 11 | ## 开发 12 | 13 | ### 环境准备 14 | 15 | - 使用 [Node.js](https://nodejs.org/) 18+ 运行环境 16 | - 使用 [pnpm@8.x](https://pnpm.io/) 作为包管理工具 17 | - 使用 [ECMAScript](https://nodejs.org/api/esm.html#modules-ecmascript-modules) 模块规范 18 | - 推荐使用 [corepack](https://github.com/nodejs/corepack) 匹配包管理器版本 19 | - 在 `.env` 文件中写入配置(参考 [本地运行](./docs/本地运行.md)) 20 | 21 | ### 初始化安装 22 | 23 | ```sh 24 | pnpm bootstrap 25 | ``` 26 | 27 | ## 测试 28 | 29 | 使用 [Jest](https://github.com/facebook/jest) 作为测试套件。测试命令: 30 | 31 | ```sh 32 | pnpm test 33 | ``` 34 | 35 | ## 编码规范 36 | 37 | 使用 [ESLint](https://eslint.org/) 和 [editorconfig](http://editorconfig.org) 保持代码风格和最佳实践。请确保你的 PR 符合指南的要求,检测命令: 38 | 39 | ```sh 40 | pnpm lint 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/notify/Telegram.md: -------------------------------------------------------------------------------- 1 | # Telegram 2 | 3 | [Telegram](https://telegram.org) 是一款跨平台的专注于安全和速度的聊天软件。通过创建 Telegram Bot,可发送自定义通知。 4 | 5 | ## 创建 Telegram Bot 6 | 7 | 1. Telegram 搜索 [@BotFather](https://t.me/botfather),点击 `/start` 启用 bot 8 | 2. 点击 `/newbot` 创建自定义 bot 9 | 1. 输入 bot 昵称 10 | 2. 输入 bot id(需全局唯一),以 `_bot` 结尾,例:`test233_bot` 11 | 3. 创建成功后,将会返回你的 bot token(例:`1689581149:AAGYVVjEHsaNxxxT8eQxxxshwr2o4Pxxxu86`) 12 | 4. Telegram 搜索刚刚创建的 bot id(本例: `test_bot`),点击 `/start` 启用 bot 13 | 14 | ### 获取 Bot Token 15 | 16 | Telegram 搜索 [@BotFather](https://t.me/botfather),点击 `/mybots`,获取 bot token 17 | 18 | ### 获取用户 ID 19 | 20 | Telegram 搜索 [@userinfobot](https://t.me/useridinfobot),点击 `/start`,获取用户 ID。 21 | 22 | ## 添加 Bot Token 23 | 24 | 进入项目 "Settings" → "Secrets" 配置页,点击 `New repository secret` 25 | 26 | - 新建 `TG_BOT_TOKEN` 项,填入 bot token 27 | 28 | ## 用户通知配置 29 | 30 | > [!NOTE] 31 | > 需使用 [JSON Token 配置格式](./token配置.md)。 32 | 33 | `TOKEN` 中添加 `tgUid` 属性,填入用户 ID。 34 | 35 | ## 全局通知配置 36 | 37 | 进入项目 "Settings" → "Secrets" 配置页,点击 `New repository secret` 38 | 39 | - 新建 `TG_USER_ID` 项,填入用户 ID 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vincent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | 3 | const require = createRequire(import.meta.url) 4 | 5 | function readPkgJson() { 6 | return require('../../package.json') 7 | } 8 | 9 | // 对手机号脱敏处理 10 | function replacePhoneNumber(str) { 11 | return str.replace(/1[3456789]\d{9}/, (match) => 12 | match.replace(/^(\d{3})\d{4}(\d+)/, '$1****$2') 13 | ) 14 | } 15 | 16 | function groupBy(arr, key) { 17 | return arr.reduce((acc, cur) => { 18 | const k = cur[key] 19 | 20 | acc[k] = acc[k] || [] 21 | acc[k].push(cur) 22 | 23 | return acc 24 | }, {}) 25 | } 26 | 27 | function dateFormat(date) { 28 | return new Date(date).toLocaleString('zh-CN', { 29 | hour12: false, 30 | timeZone: 'Asia/Shanghai' 31 | }) 32 | } 33 | 34 | function maskNickName(nickName) { 35 | return nickName.replace(/^(.).*(.)$/, '$1***$2') 36 | } 37 | 38 | function removePhoneRestriction(text) { 39 | return text.replace(/限登录手机号为\d{3}\*\*\*\*\d{4}使用。/, '') 40 | } 41 | 42 | export { 43 | dateFormat, 44 | groupBy, 45 | readPkgJson, 46 | replacePhoneNumber, 47 | maskNickName, 48 | removePhoneRestriction 49 | } 50 | -------------------------------------------------------------------------------- /test/notify.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import Notifier from '../src/notifier/index.js' 3 | 4 | const notifier = new Notifier({ 5 | barkKey: process.env.BARK_KEY, 6 | larkWebhook: process.env.LARK_WEBHOOK, 7 | workWechat: process.env.QYWX_SEND_CONF, 8 | serverChanToken: process.env.SC_SEND_KEY, 9 | pushplusToken: process.env.PUSHPLUS_TOKEN, 10 | dingTalkWebhook: process.env.DINGTALK_WEBHOOK, 11 | telegram: { 12 | botToken: process.env.TG_BOT_TOKEN, 13 | userId: process.env.TG_USER_ID 14 | }, 15 | qmsg: { 16 | token: process.env.QMSG_KEY, 17 | qq: process.env.QMSG_ADMIN 18 | } 19 | }) 20 | 21 | const date = new Date() 22 | const title = '推送测试' 23 | const time = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}` 24 | const content = [ 25 | '- ¥12 (满20可用 - 外卖节红包)', 26 | '- ¥10 (满39可用 - 外卖节红包)', 27 | '- ¥25 (满69可用 - 水果大额满减券)', 28 | '- ¥15 (满59可用 - 便利店满减红包)' 29 | ].join('\n') 30 | 31 | test('Notifier', async () => { 32 | const res = await Promise.all( 33 | notifier.notify(title, `账号 X:\n${content}\n- Time ${time}`) 34 | ) 35 | 36 | expect(res.filter((e) => e.success).length).toBe(res.length) 37 | }) 38 | -------------------------------------------------------------------------------- /docs/notify/飞书.md: -------------------------------------------------------------------------------- 1 | # 飞书 2 | 3 | [飞书](https://www.feishu.cn/)是字节跳动旗下先进企业协作与管理平台,提供一站式的无缝办公协作能力。 4 | 5 | ## 创建飞书捷径 6 | 7 | 1. 打开[飞书应用目录](https://app.feishu.cn/),选择 "企业服务" → "连接器" → "[飞书捷径](https://app.feishu.cn/app/cli_9c2e4621576f1101)",点击“获取”(使用)按钮安装应用 8 | 2. 打开[飞书捷径](https://applink.feishu.cn/client/app_share/open?appId=cli_9c2e4621576f1101)应用,在 “按应用查看模板” 栏目筛选 “webhook”,选择使用 “webhook 收到请求时通知” 9 | 3. 配置 webhook 捷径 10 | 1. 点击 "Catch hook" 卡片 11 | - 复制保存 `webhook 地址`(例:https://www.feishu.cn/flow/api/trigger-webhook/3391dxxxxx60a2d5xxxxx2073b3xxxxx) 12 | - 在`参数`项,填入以下内容: 13 | ```json 14 | { 15 | "mtz": { 16 | "title": "外卖神券天天领", 17 | "content": "hello world!" 18 | } 19 | } 20 | ``` 21 | 2. 点击“通过飞书捷径机器人发送消息”卡片 22 | - 在 `消息标题` 项,清空已有内容,点击右侧加号按钮选择 `mtz.title` 23 | - 在 `消息内容` 项,清空已有内容,点击右侧加号选择 `mtz.content` 24 | 3. 点击“保存”按钮应用配置 25 | 26 | ## 用户通知配置 27 | 28 | > [!NOTE] 29 | > 需使用 [JSON Token 配置格式](./token配置.md)。 30 | 31 | `TOKEN` 中添加 `larkWebhook` 属性,填入`webhook 地址`。 32 | 33 | ## 全局通知配置 34 | 35 | 进入项目 "Settings" → "Secrets" 配置页,点击 `New repository secret` 36 | 37 | - 新建 `LARK_WEBHOOK` 项,填入`webhook 地址` 38 | -------------------------------------------------------------------------------- /test/template.js: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test } from 'vitest' 2 | import { getTemplateData, getRenderList } from '../src/template.js' 3 | import ShadowGuard from '../src/shadow/index.js' 4 | import gundam from '../src/coupons/gundam.js' 5 | import { createMTCookie, parseToken } from '../src/user.js' 6 | import { wxfwhActConfs } from '../src/coupons/const.js' 7 | 8 | const guard = new ShadowGuard() 9 | const tokens = parseToken(process.env.TOKEN) 10 | const cookie = createMTCookie(tokens[0].token) 11 | 12 | beforeAll(() => guard.init(gundam.getActUrl(wxfwhActConfs[0].gid))) 13 | 14 | test('GetTemplateData', async () => { 15 | const res = await getTemplateData(cookie, wxfwhActConfs[0].gid, guard) 16 | 17 | return expect(res).toMatchObject({ 18 | pageId: expect.any(Number), 19 | gdId: expect.any(Number), 20 | actName: expect.any(String), 21 | appJs: expect.stringMatching(/app[^"]*\.js/) 22 | }) 23 | }) 24 | 25 | test('GetRenderList', async () => { 26 | const renderList = await getRenderList( 27 | cookie, 28 | { gundamViewId: '1I9uL6', pageId: 544003, gdId: 466632 }, 29 | guard 30 | ) 31 | 32 | return expect(renderList).toEqual(expect.any(Array)) 33 | }) 34 | -------------------------------------------------------------------------------- /src/notifier/vendor/dingtalk.js: -------------------------------------------------------------------------------- 1 | import crypto from 'node:crypto' 2 | import { doPost } from '../util.js' 3 | 4 | function signFn(secret, content) { 5 | const str = crypto 6 | .createHmac('sha256', secret) 7 | .update(content) 8 | .digest() 9 | .toString('base64') 10 | 11 | return encodeURIComponent(str) 12 | } 13 | 14 | async function sendDingTalk({ title = '', content = '', webhook }) { 15 | const timestamp = Date.now() 16 | let secret = '' 17 | 18 | if (webhook.startsWith('SEC')) { 19 | const [sec, url] = webhook.split('|') 20 | 21 | secret = sec 22 | webhook = url 23 | } 24 | 25 | const sign = signFn(secret, `${timestamp}\n${secret}`) 26 | const hookUrl = `${webhook}×tamp=${timestamp}&sign=${sign}` 27 | const data = { 28 | msgtype: 'text', 29 | text: { 30 | content: `【${title}】\n${content}` 31 | } 32 | } 33 | 34 | return doPost(hookUrl, data) 35 | .then((res) => { 36 | if (res.errcode == 0) return 37 | 38 | if (res.errcode == 310000) { 39 | throw 'secret 不匹配' 40 | } else if (res.errcode == 300001) { 41 | throw 'access_token 不匹配' 42 | } else { 43 | throw res.errmsg 44 | } 45 | }) 46 | .then(() => ({ success: true, msg: '钉钉推送成功' })) 47 | .catch((e) => ({ success: false, msg: `钉钉推送失败: ${e}` })) 48 | } 49 | 50 | export default sendDingTalk 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actions-mtz-coupons", 3 | "description": "外卖神券天天领", 4 | "version": "2.2.0", 5 | "keywords": [ 6 | "外卖", 7 | "红包" 8 | ], 9 | "main": "index.js", 10 | "type": "module", 11 | "author": "Vincent ", 12 | "scripts": { 13 | "bootstrap": "corepack enable && corepack pnpm i", 14 | "start": "node index.js", 15 | "start:local": "node -r dotenv/config index.js", 16 | "lint": "eslint .", 17 | "test": "vitest run", 18 | "sync": "node ./sync-upstream.js" 19 | }, 20 | "dependencies": { 21 | "@wasmer/wasi": "^1.2.2", 22 | "https-proxy-agent": "^7.0.5", 23 | "p-limit": "^6.1.0", 24 | "semver": "^7.6.3", 25 | "timeout-signal": "^2.0.0", 26 | "tough-cookie": "^5.0.0" 27 | }, 28 | "devDependencies": { 29 | "dotenv": "^16.4.5", 30 | "eslint": "^9.13.0", 31 | "globals": "^15.10.0", 32 | "vitest": "^2.1.3" 33 | }, 34 | "private": true, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/vv314/actions-mtwm-coupons.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/vv314/actions-mtwm-coupons/issues" 41 | }, 42 | "homepage": "https://github.com/vv314/actions-mtwm-coupons#readme", 43 | "license": "MIT", 44 | "packageManager": "pnpm@8.15.9+sha512.499434c9d8fdd1a2794ebf4552b3b25c0a633abcee5bb15e7b5de90f32f47b513aca98cd5cfd001c31f0db454bc3804edccd578501e4ca293a6816166bbd9f81" 45 | } 46 | -------------------------------------------------------------------------------- /src/update-notifier.js: -------------------------------------------------------------------------------- 1 | import semver from 'semver' 2 | import { readPkgJson } from './util/index.js' 3 | import request from './request.js' 4 | 5 | const { version: currentVersion } = readPkgJson() 6 | 7 | async function getLatestRelease(timeout = 5000) { 8 | const res = await request.get( 9 | 'https://api.github.com/repos/vv314/actions-mtz-coupons/releases', 10 | { 11 | timeout: timeout 12 | } 13 | ) 14 | 15 | const info = res.filter((e) => !e.draft || !e.prerelease)[0] 16 | 17 | return { 18 | tag: info.tag_name, 19 | version: info.name.replace('v', ''), 20 | date: info.published_at.substr(0, 10), 21 | url: info.html_url, 22 | message: info.body.replace(/\r\n/g, '\n') 23 | } 24 | } 25 | 26 | function getNotifyMessage(release) { 27 | const message = [ 28 | `新版本就绪 ${currentVersion} → ${release.version}`, 29 | '', 30 | release.message, 31 | '', 32 | '执行 `npm run sync` 同步复刻' 33 | ].join('\n') 34 | 35 | return message 36 | } 37 | 38 | async function checkUpdate(timeout) { 39 | let release 40 | 41 | try { 42 | release = await getLatestRelease(timeout) 43 | } catch (e) { 44 | let errMsg = e.msg ?? e.message 45 | 46 | if (e.code === request.ECODE.TIMEOUT) { 47 | errMsg = '请求超时' 48 | } 49 | 50 | throw '检查更新失败: ' + errMsg 51 | } 52 | 53 | if (semver.gt(release.version, currentVersion)) { 54 | return getNotifyMessage(release) 55 | } 56 | 57 | return '' 58 | } 59 | 60 | export default checkUpdate 61 | -------------------------------------------------------------------------------- /docs/通知.md: -------------------------------------------------------------------------------- 1 | # 消息通知 2 | 3 | 程序执行完毕后,支持调用第三方通知服务,推送运行结果。 4 | 5 | ## 通知类型 6 | 7 | 按照受众群体不同,通知划分为“**用户通知**”和“**全局通知**”两大类。 8 | 9 | ### 用户通知 10 | 11 | > [!NOTE] 12 | > 用户通知需使用 [JSON Token 配置格式](./token配置.md)。 13 | 14 | 一对一推送,适用于多账户内的“乘客”。 15 | 16 | ### 全局通知 17 | 18 | 推送所有任务的执行情况,适用于程序管理者。 19 | 20 | ## 支持平台 21 | 22 | | | 用户通知 | 全局通知 | 备注 | 23 | | --------------------------------- | :------: | :------: | ------------------------------------------------ | 24 | | [Bark](./notify/Bark.md) | ✅ | ✅ | 仅 iOS 支持 | 25 | | [飞书](./notify/飞书.md) | ✅ | ✅ | | 26 | | [钉钉](./notify/钉钉.md) | ✅ | ✅ | | 27 | | [Telegram](./notify/Telegram.md) | ✅ | ✅ | | 28 | | [企业微信](./notify/企业微信.md) | ✅ | ✅ | | 29 | | [Server 酱](./notify/Server酱.md) | | ✅ | | 30 | | [pushplus](./notify/Pushplus.md) | | ✅ 31 | | [wxpusher](./notify/WxPusher.md) | | ✅ | 32 | | [Qmsg 酱](./notify/Qmsg酱.md) | ☑️ | ✅ | 平台方对非捐赠版有频次限制,将影响多账户通知通能 | 33 | 34 | ### 消息示例 35 | 36 | ``` 37 | 【外卖神券天天领😋】 38 | 账号 xxx: 39 | - ¥5(满20可用) 40 | - ¥7(满35可用) 41 | - ¥3(满20可用) 42 | 43 | 账号 xxx: 44 | - ¥5(满20可用) 45 | - ¥7(满35可用) 46 | ... 47 | ``` 48 | -------------------------------------------------------------------------------- /src/notifier/vendor/work-wechat.js: -------------------------------------------------------------------------------- 1 | import { doGet, doPost } from '../util.js' 2 | 3 | /** 4 | * 获取企业微信 accessToken 5 | * https://work.weixin.qq.com/api/doc/90000/90135/91039 6 | * 7 | * @param {[type]} corpId 企业 id 8 | * @param {[type]} corpSecret 企业应用密匙 9 | * @return {Promise} 结果 10 | */ 11 | async function getQywxAccessToken(corpId, corpSecret) { 12 | const res = await doGet('https://qyapi.weixin.qq.com/cgi-bin/gettoken', { 13 | corpid: corpId, 14 | corpsecret: corpSecret 15 | }) 16 | 17 | if (res.errcode != 0) { 18 | throw new Error('AccessToken 获取失败:' + res.errmsg) 19 | } 20 | 21 | // 提前十分钟过期 22 | const expiresTime = Date.now() + res.expires_in * 1000 - 10 * 60 * 1000 23 | 24 | return { 25 | token: res.access_token, 26 | expires: expiresTime 27 | } 28 | } 29 | 30 | async function sendWorkWechat({ 31 | title = '', 32 | content = '', 33 | accessToken, 34 | agentId, 35 | user 36 | }) { 37 | try { 38 | const res = await doPost( 39 | `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}`, 40 | { 41 | touser: user, 42 | msgtype: 'text', 43 | agentid: agentId, 44 | text: { 45 | content: `【${title}】\n${content}` 46 | } 47 | } 48 | ) 49 | 50 | if (res.errcode != 0) { 51 | throw res.errmsg 52 | } 53 | } catch (e) { 54 | return { success: false, msg: `企业微信推送失败: ${e}` } 55 | } 56 | 57 | return { success: true, msg: '企业微信推送成功' } 58 | } 59 | 60 | export { sendWorkWechat, getQywxAccessToken } 61 | -------------------------------------------------------------------------------- /test/shadow.js: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test } from 'vitest' 2 | import ShadowGuard from '../src/shadow/index.js' 3 | import gundam from '../src/coupons/gundam.js' 4 | import { mainActConf } from '../src/coupons/const.js' 5 | 6 | const guard = new ShadowGuard({ 7 | dfpId: '8v0111yz74185w7deu38vz71222u80w981zvylws4779123469xu4399' 8 | }) 9 | 10 | beforeAll(() => guard.init(gundam.getActUrl(mainActConf.gid))) 11 | 12 | test('Generate Session', () => expect(guard.meta.sessionId).toHaveLength(32)) 13 | 14 | test('Generate Meta', () => expect(guard.meta).toBeTruthy()) 15 | 16 | test('Web FpId', async () => { 17 | const dfpId = await guard.getWebDfpId(guard.fingerprint) 18 | 19 | return expect(dfpId).toHaveLength(56) 20 | }) 21 | 22 | test('MtgSig', async () => { 23 | guard.context.timestamp = 1702734030440 24 | guard.context.runtimeKey = 'r0ejVfUUFC1DvZh3L/0z' 25 | guard.context.siua = 26 | 'hs1.4A7RoRP0dbIKmoIPl+WUiTN8BQHkire5xDBjSCt4mtv1Ww6RzbqF4jv3nTk50BKzxmnmkBvEGU1suLA5Q1YoDrhGZ49LeB+Ze/XUZsCN6OhE=' 27 | 28 | const { reqSig } = await guard.getReqSig({ 29 | url: 'https://mediacps.meituan.com/gundam/gundamLogin', 30 | method: 'POST' 31 | }) 32 | const mtgSig = await guard.getMtgSig(reqSig) 33 | 34 | expect(mtgSig).toBeTruthy() 35 | }) 36 | 37 | test('Base Signature', async () => { 38 | const sig = await guard.getReqSig({ 39 | url: 'https://mediacps.meituan.com/gundam/gundamLogin', 40 | method: 'POST' 41 | }) 42 | 43 | expect(sig).toBeTruthy() 44 | }) 45 | 46 | test('H5 Fingerprint', () => { 47 | return expect(guard.h5fp.length).toBeTruthy() 48 | }) 49 | -------------------------------------------------------------------------------- /docs/notify/企业微信.md: -------------------------------------------------------------------------------- 1 | # 企业微信 2 | 3 | [企业微信](https://work.weixin.qq.com) 是微信团队出品的企业通讯与办公应用,具有与微信互联的能力。 4 | 5 | ## 创建企业微信应用 6 | 7 | 1. PC 端打开[企业微信官网](https://work.weixin.qq.com/),注册一个企业 8 | 2. 注册完成后,进入“[应用管理](https://work.weixin.qq.com/wework_admin/frame#apps)” → “应用” → “自建”,点击 `➕创建应用` 9 | 3. 完善应用名称与 logo 信息,可见范围选择公司名 10 | 11 | ### 获取应用信息 12 | 13 | 进入企业微信管理后台: 14 | 15 | 1. “[我的企业](https://work.weixin.qq.com/wework_admin/frame#profile)” → “企业信息” 下获取 “企业 ID” 16 | 2. “[应用管理](https://work.weixin.qq.com/wework_admin/frame#apps)” → “应用” → “自建”,点进目标应用,获取 `AgentId`(应用 ID) 17 | 3. “[应用管理](https://work.weixin.qq.com/wework_admin/frame#apps)” → “应用” → “自建”,点进目标应用,获取 `Secret`(应用钥匙) 18 | 19 | ## 添加企业微信通知 20 | 21 | 进入项目 "Settings" → "Secrets" 配置页,点击 `New repository secret` 22 | 23 | - 新建 `QYWX_SEND_CONF` 项,填入 **\** 24 | 25 | **JSON 配置**字段说明: 26 | 27 | | 属性名 | 类型 | 默认值 | 必填 | 说明 | 28 | | ---------- | ------ | ----------------- | ---- | ------------------------- | 29 | | corpId | string | | 是 | 企业 ID | 30 | | agentId | string | | 是 | 应用 ID | 31 | | corpSecret | string | | 是 | 应用密匙 | 32 | | toUser | string | @all (推送所有) | 否 | 用户 ID,多用户以 \| 分割 | 33 | 34 | 示例: 35 | 36 | ```json 37 | { 38 | "corpId": "wwxxxe9ddxxxc50xxx", 39 | "agentId": "1000002", 40 | "corpSecret": "12Qxxxo4hxxxyedtxxxdyfVxxxCqh6xxxF0zg3xxxNI", 41 | "toUser": "@all" 42 | } 43 | ``` 44 | 45 | ## 用户通知配置 46 | 47 | > [!NOTE] 48 | > 需使用 [JSON Token 配置格式](./token配置.md)。 49 | 50 | `TOKEN` 中添加 `qywxUid` 属性,填入企业微信用户 ID 51 | 52 | ## 全局通知配置 53 | 54 | `QYWX_SEND_CONF` Secret 设置 `toUser` 属性,填入企业微信用户 ID 55 | -------------------------------------------------------------------------------- /test/payload.js: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test } from 'vitest' 2 | import { getTemplateData } from '../src/template.js' 3 | import ShadowGuard from '../src/shadow/index.js' 4 | import gundam from '../src/coupons/gundam.js' 5 | import wxfwh from '../src/coupons/wxfwh.js' 6 | import { mainActConf, wxfwhActConfs } from '../src/coupons/const.js' 7 | import { createMTCookie, parseToken } from '../src/user.js' 8 | 9 | const guard = new ShadowGuard() 10 | const tokens = parseToken(process.env.TOKEN) 11 | const cookie = createMTCookie(tokens[0].token) 12 | 13 | beforeAll(() => guard.init(gundam.getActUrl(mainActConf.gid))) 14 | 15 | test('Main Payload', async () => { 16 | const tmplData = await getTemplateData(cookie, mainActConf.gid, guard) 17 | const payload = await gundam.getPayload(tmplData, guard) 18 | 19 | return expect(payload).toMatchObject({ 20 | actualLatitude: 0, 21 | actualLongitude: 0, 22 | app: -1, 23 | platform: 3, 24 | couponConfigIdOrderCommaString: expect.any(String), 25 | couponAllConfigIdOrderString: expect.any(String), 26 | gundamId: tmplData.gdId, 27 | needTj: expect.any(Boolean), 28 | instanceId: expect.any(String), 29 | h5Fingerprint: expect.any(String) 30 | }) 31 | }) 32 | 33 | // test('Wxfwh Payload', async () => { 34 | // const tmplData = await getTemplateData(cookie, wxfwhActConfs[0].gid, guard) 35 | // const payload = await wxfwh.getPayload(cookie, tmplData, guard) 36 | 37 | // return expect(payload).toMatchObject({ 38 | // ctype: 'wm_wxapp', 39 | // fpPlatform: 13, 40 | // wxOpenId: '', 41 | // appVersion: '', 42 | // gdId: tmplData.gdId, 43 | // pageId: tmplData.pageId, 44 | // tabs: expect.any(Array), 45 | // activityViewId: expect.any(String), 46 | // instanceId: expect.any(String), 47 | // mtFingerprint: expect.any(String) 48 | // }) 49 | // }) 50 | -------------------------------------------------------------------------------- /test/coupons.js: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test } from 'vitest' 2 | 3 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' 4 | 5 | import ShadowGuard from '../src/shadow/index.js' 6 | import { createMTCookie, parseToken } from '../src/user.js' 7 | import { grabCoupons, ECODE } from '../src/coupons/index.js' 8 | import { mainActConf, wxfwhActConfs } from '../src/coupons/const.js' 9 | import gundam from '../src/coupons/gundam.js' 10 | import wxfwh from '../src/coupons/wxfwh.js' 11 | import lottery from '../src/coupons/lottery.js' 12 | 13 | const guard = new ShadowGuard() 14 | const tokens = parseToken(process.env.TOKEN) 15 | const cookie = createMTCookie(tokens[0].token) 16 | 17 | beforeAll(() => guard.init(gundam.getActUrl(mainActConf.gid))) 18 | 19 | test('Main Grab', async () => { 20 | const res = await gundam.grabCoupon(cookie, mainActConf.gid, guard) 21 | 22 | return expect(res.length).toBeGreaterThan(0) 23 | }) 24 | 25 | test('Token Error', async () => { 26 | const res = await grabCoupons('invalid token', { 27 | // proxy: 'http://127.0.0.1:8887' 28 | }) 29 | 30 | return expect(res.code).toBe(ECODE.AUTH) 31 | }) 32 | 33 | // test('Wxfwh grab', async () => { 34 | // const res = await wxfwh.grabCoupon(cookie, wxfwhActConfs[0].gid, guard) 35 | 36 | // return expect(res).toBeTruthy() 37 | // }) 38 | 39 | // test('Wxfwh Result', async () => { 40 | // const res = await wxfwh.getCouponList(cookie, 'I5r2SYd5kTN1l1AkMhwCNA') 41 | 42 | // return expect(res.length).toBeGreaterThan(0) 43 | // }) 44 | 45 | // test('Lottery Result', async () => { 46 | // const tmplData = await lottery.getTemplateData(cookie, '1VlhFT', guard) 47 | // const ticketConfig = await lottery.getTicketConfig( 48 | // tmplData.gdId, 49 | // tmplData.appJs 50 | // ) 51 | // const res = await lottery.getCouponList(cookie, [ 52 | // ticketConfig.channelUrl ?? '' 53 | // ]) 54 | 55 | // return expect(res).toBeTruthy() 56 | // }) 57 | -------------------------------------------------------------------------------- /.github/workflows/grab-coupon.yml: -------------------------------------------------------------------------------- 1 | name: 领红包 2 | 3 | # https://docs.github.com/cn/actions/reference/events-that-trigger-workflows#workflow_dispatch 4 | on: 5 | # 手动触发 6 | workflow_dispatch: 7 | # 定时任务 8 | schedule: 9 | # https://crontab.guru/ 10 | # UTC 时间,中国时区应减 8 11 | # ┌────────── minute (0 - 59) 12 | # │ ┌──────── hour (0 - 23) 13 | # │ │ ┌────── day of the month (1 - 31) 14 | # │ │ │ ┌──── month (1 - 12 or JAN-DEC) 15 | # │ │ │ │ ┌── day of the week (0 - 6 or SUN-SAT) 16 | # │ │ │ │ │ 17 | - cron: '45 2 * * *' 18 | 19 | jobs: 20 | start: 21 | name: grab coupon 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Checkout codes 26 | uses: actions/checkout@v4 27 | 28 | - uses: pnpm/action-setup@v4 29 | name: Install pnpm 30 | with: 31 | run_install: false 32 | 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version-file: '.nvmrc' 37 | cache: 'pnpm' 38 | 39 | - name: Install dependencies 40 | run: pnpm install 41 | 42 | - name: Run app 43 | run: pnpm start 44 | env: 45 | TOKEN: ${{ secrets.TOKEN }} 46 | BARK_KEY: ${{ secrets.BARK_KEY }} 47 | TG_USER_ID: ${{ secrets.TG_USER_ID }} 48 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }} 49 | SC_SEND_KEY: ${{ secrets.SC_SEND_KEY }} 50 | QMSG_KEY: ${{ secrets.QMSG_KEY }} 51 | QMSG_ADMIN: ${{ secrets.QMSG_ADMIN }} 52 | QYWX_SEND_CONF: ${{ secrets.QYWX_SEND_CONF }} 53 | LARK_WEBHOOK: ${{ secrets.LARK_WEBHOOK }} 54 | PUSHPLUS_TOKEN: ${{ secrets.PUSHPLUS_TOKEN }} 55 | WXPUSHER_TOKEN: ${{ secrets.WXPUSHER_TOKEN }} 56 | WXPUSHER_TOPICID: ${{ secrets.WXPUSHER_TOPICID }} 57 | DINGTALK_WEBHOOK: ${{ secrets.DINGTALK_WEBHOOK }} 58 | -------------------------------------------------------------------------------- /sync-upstream.js: -------------------------------------------------------------------------------- 1 | import { spawn } from 'node:child_process' 2 | 3 | function gitExec(opt) { 4 | const pull = spawn('git', opt.split(' ')) 5 | 6 | return new Promise((resolve, reject) => { 7 | let res = '' 8 | 9 | pull.stdout.on('data', (buffer) => { 10 | res += buffer.toString() 11 | }) 12 | 13 | pull.stderr.on('data', (buffer) => { 14 | res += buffer.toString() 15 | }) 16 | 17 | pull.on('close', (code) => { 18 | code == 0 ? resolve(res) : reject(res) 19 | }) 20 | }) 21 | } 22 | 23 | async function getRemoteUrls() { 24 | const urls = await gitExec('remote -v') 25 | 26 | return urls.split('\n') 27 | } 28 | 29 | function addUpstream(upstreamUrl) { 30 | return gitExec(`remote add upstream ${upstreamUrl}`) 31 | } 32 | 33 | function setUpstream(upstreamUrl) { 34 | return gitExec(`remote set-url upstream ${upstreamUrl}`) 35 | } 36 | 37 | function pullUpstream() { 38 | return gitExec('pull --rebase upstream main:main') 39 | } 40 | 41 | function pushOrigin() { 42 | return gitExec('push origin main:main') 43 | } 44 | 45 | async function main() { 46 | const upstreamUrl = 'git@github.com:vv314/actions-mtz-coupons.git' 47 | 48 | console.log('———— [1/4] 获取上游仓库信息 ————') 49 | const urls = await getRemoteUrls() 50 | const exist = urls.some((url) => url.startsWith('upstream')) 51 | 52 | if (!exist) { 53 | console.log('———— [2/4] 添加上游仓库 ————') 54 | await addUpstream(upstreamUrl) 55 | } else { 56 | console.log('———— [2/4] 设置上游仓库 ————') 57 | await setUpstream(upstreamUrl) 58 | } 59 | 60 | try { 61 | console.log('———— [3/4] 拉取上游仓库 ————') 62 | const pullRes = await pullUpstream() 63 | console.log(pullRes) 64 | 65 | console.log('———— [4/4] 推送更新 ————') 66 | const pushRes = await pushOrigin() 67 | console.log(pushRes) 68 | 69 | console.log('同步成功') 70 | } catch (e) { 71 | console.log(e) 72 | console.log('同步失败') 73 | } 74 | } 75 | 76 | main() 77 | -------------------------------------------------------------------------------- /src/user.js: -------------------------------------------------------------------------------- 1 | import request, { createCookieJar } from './request.js' 2 | import { ECODE } from './coupons/const.js' 3 | 4 | function tokenFormat(token, index = 0) { 5 | const defToken = { 6 | token: '', 7 | alias: '', 8 | index: index + 1, 9 | tgUid: '', 10 | qywxUid: '', 11 | barkKey: '', 12 | larkWebhook: '', 13 | qq: '' 14 | } 15 | 16 | if (typeof token == 'string') { 17 | token = { token } 18 | } 19 | 20 | return Object.assign({}, defToken, token) 21 | } 22 | 23 | function parseToken(token) { 24 | if (!token) throw '请配置 TOKEN' 25 | 26 | const likeJson = ['{', '['].includes(token.trim()[0]) 27 | 28 | if (!likeJson) return [tokenFormat(token)] 29 | 30 | try { 31 | token = JSON.parse(token) 32 | } catch (e) { 33 | throw `TOKEN 解析错误: ${e.message}` 34 | } 35 | 36 | return [].concat(token).map(tokenFormat) 37 | } 38 | 39 | async function getMTUerId() { 40 | const rep = await request('https://h5.waimai.meituan.com/waimai/mindex/home') 41 | 42 | const repCookie = rep.headers.get('set-cookie') || '' 43 | const matchArr = repCookie.match(/userId=(\w+)/) || [] 44 | 45 | return matchArr[1] || '' 46 | } 47 | 48 | async function getUserInfo(cookie, guard) { 49 | const res = await request.post( 50 | 'https://mediacps.meituan.com/gundam/gundamLogin', 51 | null, 52 | { 53 | cookie: cookie, 54 | guard: guard 55 | } 56 | ) 57 | 58 | if (res.code == 0) return res.data 59 | 60 | if (res.code == 3) { 61 | throw { code: ECODE.AUTH, api: 'gundamLogin', msg: res.msg || res.message } 62 | } 63 | 64 | throw { code: ECODE.API, api: 'gundamLogin', msg: res.msg || res.message } 65 | } 66 | 67 | function createMTCookie(token) { 68 | const cookieJar = createCookieJar(token) 69 | const domain = 'Domain=.meituan.com' 70 | const path = 'Path=/' 71 | const http = 'HttpOnly' 72 | const expire = 'Max-Age=3600' 73 | const content = token.startsWith('token=') ? token : `token=${token}` 74 | const cookieStr = [content, domain, path, http, expire].join(';') 75 | 76 | cookieJar.setCookie(cookieStr, 'https://mediacps.meituan.com') 77 | 78 | return cookieJar 79 | } 80 | 81 | export { createMTCookie, getUserInfo, getMTUerId, parseToken } 82 | -------------------------------------------------------------------------------- /src/shadow/guard.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import fs from 'fs/promises' 4 | import { init, WASI } from '@wasmer/wasi' 5 | 6 | await init() 7 | 8 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 9 | const wasmBuffer = await fs.readFile(path.resolve(__dirname, './guard.wasm')) 10 | const wasmModule = await WebAssembly.compile(wasmBuffer) 11 | 12 | async function wbus(input) { 13 | const wasi = new WASI({}) 14 | 15 | await wasi.instantiate(wasmModule, {}) 16 | 17 | wasi.setStdinString(JSON.stringify(input)) 18 | 19 | const exitCode = wasi.start() 20 | const stdout = wasi.getStdoutString() 21 | const result = JSON.parse(stdout) 22 | 23 | // console.log(`[CODE: ${exitCode}]`, result) 24 | 25 | return result 26 | } 27 | 28 | function formatUrl(url) { 29 | if (!url) return url 30 | 31 | try { 32 | if (typeof url === 'object' && url instanceof URL) { 33 | return url.toString() 34 | } 35 | 36 | url = url.trim() 37 | 38 | if (url.startsWith('//')) { 39 | return 'https:' + url 40 | } 41 | 42 | if (url.startsWith('/')) { 43 | return 'https://market.waimai.meituan.com' + url 44 | } 45 | 46 | return url 47 | } catch { 48 | return url 49 | } 50 | } 51 | 52 | async function genMetaData(actUrl, version) { 53 | const { data } = await wbus({ 54 | method: 'genMetaData', 55 | args: [actUrl, version] 56 | }) 57 | 58 | return data 59 | } 60 | 61 | async function getH5Dfp(metaData, version) { 62 | const { data } = await wbus({ 63 | method: 'getH5Dfp', 64 | args: [metaData, version] 65 | }) 66 | 67 | return data 68 | } 69 | 70 | async function getH5Fp(url) { 71 | const { data } = await wbus({ 72 | method: 'getH5Fp', 73 | args: [url] 74 | }) 75 | 76 | return data 77 | } 78 | 79 | async function getReqSig(reqOpt) { 80 | const { data } = await wbus({ 81 | method: 'getReqSig', 82 | args: [reqOpt] 83 | }) 84 | 85 | return data 86 | } 87 | 88 | async function getMtgSig(reqSig, guardCtx) { 89 | try { 90 | const { data } = await wbus({ 91 | method: 'getMtgSig', 92 | args: [reqSig, guardCtx] 93 | }) 94 | 95 | return data 96 | } catch (e) { 97 | console.log(e) 98 | } 99 | } 100 | 101 | export { formatUrl, getH5Fp, getH5Dfp, genMetaData, getMtgSig, getReqSig } 102 | -------------------------------------------------------------------------------- /src/shadow/index.js: -------------------------------------------------------------------------------- 1 | import request from '../request.js' 2 | import { 3 | formatUrl, 4 | genMetaData, 5 | getH5Fp, 6 | getH5Dfp, 7 | getMtgSig, 8 | getReqSig 9 | } from './guard.js' 10 | import { guardVersion, csecPlatform, yodaReady } from './const.js' 11 | 12 | class ShadowGuard { 13 | version = guardVersion 14 | h5fp = '' 15 | fingerprint = '' 16 | meta = {} 17 | 18 | constructor(opts) { 19 | this.context = { 20 | dfpId: opts?.dfpId, 21 | version: this.version 22 | } 23 | } 24 | 25 | async init(actUrl) { 26 | actUrl = actUrl instanceof URL ? actUrl.toString() : actUrl 27 | 28 | this.meta = await genMetaData(actUrl, this.version) 29 | this.fingerprint = await getH5Dfp(this.meta, this.version) 30 | this.h5fp = await getH5Fp(actUrl) 31 | 32 | if (!this.context.dfpId) { 33 | this.context.dfpId = await this.getWebDfpId(this.fingerprint) 34 | } 35 | 36 | this.meta.k3 = this.context.dfpId 37 | this.context.meta = this.meta 38 | 39 | return this 40 | } 41 | 42 | async getWebDfpId(fingerprint) { 43 | const res = await request.post( 44 | 'https://appsec-mobile.meituan.com/v1/webdfpid', 45 | { 46 | data: fingerprint 47 | } 48 | ) 49 | 50 | return res.data.dfp 51 | } 52 | 53 | async getReqSig(reqOpt) { 54 | const guardURL = new URL(formatUrl(reqOpt.url || '')) 55 | 56 | guardURL.searchParams.append('gdBs', '') 57 | guardURL.searchParams.append('yodaReady', yodaReady) 58 | guardURL.searchParams.append('csecplatform', csecPlatform) 59 | guardURL.searchParams.append('csecversion', this.version) 60 | reqOpt.url = guardURL.toString() 61 | 62 | const reqSig = await getReqSig(reqOpt) 63 | 64 | return { guardURL, reqSig } 65 | } 66 | 67 | async getMtgSig(reqSig, signType = 'url') { 68 | return getMtgSig(reqSig, { 69 | ...this.context, 70 | signType 71 | }) 72 | } 73 | 74 | /** 75 | * @param {FetchOptions} reqOpt 76 | * @param {'url' | 'header'} signType 77 | * @returns 78 | */ 79 | async sign(reqOpt, signType) { 80 | if (!reqOpt) return reqOpt 81 | 82 | const { guardURL, reqSig } = await this.getReqSig(reqOpt) 83 | const res = await this.getMtgSig(reqSig, signType) 84 | const mtgSig = JSON.stringify(res.data) 85 | const headers = {} 86 | 87 | if (signType === 'header') { 88 | headers.mtgsig = mtgSig 89 | } else { 90 | guardURL.searchParams.append('mtgsig', mtgSig) 91 | } 92 | 93 | return { url: guardURL.toString(), headers } 94 | } 95 | } 96 | 97 | export default ShadowGuard 98 | -------------------------------------------------------------------------------- /docs/token配置.md: -------------------------------------------------------------------------------- 1 | # TOKEN 配置 2 | 3 | ## 配置格式 4 | 5 | `TOKEN` Secret 支持 `String` 和 `JSON` 对象两种配置格式。 6 | 7 | ### String 配置 8 | 9 | 快捷配置,值为从 `cookie` 中提取的 `token` 信息。 10 | 11 | **示例:** 12 | 13 | ``` 14 | Js3xxxxFyy_Aq-rOnxMte6vKPV4AAAAA6QwAADgqRBSfcmNqyuG8CQ7JDL7xxxxNGbfF7tPNV5347_ANLcydua_JHCSRj0_xxxg9xx 15 | ``` 16 | 17 | ### JSON 配置 18 | 19 | 高级配置,适用于用户通知以及多账户支持。 20 | 21 | #### 参数 22 | 23 | | 属性名 | 类型 | 默认值 | 必填 | 说明 | 24 | | ----------- | ------ | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------- | 25 | | token | string | | 是 | 账号 token | 26 | | alias | string | | 否 | 账号别名,便于区分多账户 | 27 | | qywxUid | string | | 否 | 企业微信通知,用户 id | 28 | | tgUid | string | | 否 | Telegram 通知,用户 id | 29 | | barkKey | string | | 否 | Bark 通知,推送 Key | 30 | | larkWebhook | string | | 否 | 飞书通知,webhook 链接 | 31 | | dtWebhook | string | | 否 | 钉钉通知,webhook 链接。当设置**加签**时,需按照`secret\|webhook` 的格式将 secret 拼接至 webhook 之前(两者以 `\|` 分隔) | 32 | | qq | string | | 否 | Qmsg 通知,qq 号 | 33 | 34 | _注意:企业微信通知需配置 `QYWX_SEND_CONF` Secret,Telegram 通知需配置 `TG_BOT_TOKEN` Secret,详见【消息通知】章节_ 35 | 36 | **示例:** 37 | 38 | ```json 39 | { 40 | "token": "Js3xxxxFyy_Aq-rOnxMte6vKPV4AAAAA6QwAADgqRBSfcmNqyuG8CQ7JDL7xxxxNGbfF7tPNV5347_ANLcydua_JHCSRj0_xxxg9xx", 41 | "qywxUid": "Vincent", 42 | "barkKey": "kkWwxxxq5NpWx" 43 | } 44 | ``` 45 | 46 | ## 多账户配置 47 | 48 | 当 `TOKEN` 指定为数组时,代表启用账户配置。每个配置成员均支持 `String` 和 `JSON` 格式。 49 | 50 | **示例:** 51 | 52 | ```json 53 | [ 54 | "Js3xxxxFyy_Aq-rOnxMte6vKPV4AAAAA6QwAADgqRBSfcmNqyuG8CQ7JDL7xxxxNGbfF7tPNV5347_ANLcydua_JHCSRj0_xxxg9xx", 55 | { 56 | "token": "3R2xxxxxUqS_Aq-rOnxMte6vKPV4AAAAA6QwAADgqRBSfcmNqyuG8CQ7JDL7xxxxNGbfF7tPNV5347_ANLcydua_JHCSRj0_xxxg9xx", 57 | "alias": "fish", 58 | "barkKey": "kkWwxxxq5NpWx", 59 | "qywxUid": "Vincent" 60 | } 61 | ] 62 | ``` 63 | -------------------------------------------------------------------------------- /src/template.js: -------------------------------------------------------------------------------- 1 | import request from './request.js' 2 | 3 | function extractAppJsUrl(text) { 4 | const regex = /https:\/\/[^"]*\/app[^"]*\.js/ 5 | const match = text.match(regex) 6 | 7 | return match ? match[0] : null 8 | } 9 | 10 | async function getTemplateData(cookie, gundamId, guard) { 11 | const text = await request( 12 | `https://market.waimai.meituan.com/api/template/get?env=current&el_biz=waimai&el_page=gundam.loader&gundam_id=${gundamId}` 13 | ).then((rep) => rep.text()) 14 | const matchGlobal = text.match(/globalData: ({.+})/) 15 | const appJs = extractAppJsUrl(text) 16 | 17 | try { 18 | const globalData = JSON.parse(matchGlobal[1]) 19 | const renderList = await getRenderList(cookie, globalData, guard) 20 | 21 | return { 22 | gundamId, 23 | gdId: globalData.gdId, 24 | actName: globalData.pageInfo.title, 25 | appJs: appJs, 26 | pageId: globalData.pageId, 27 | renderList: renderList 28 | } 29 | } catch (e) { 30 | throw new Error(`活动配置数据获取失败: gdId: ${gundamId}, ${e}`) 31 | } 32 | } 33 | 34 | function getLocalRenderList(renderInfo) { 35 | if (renderInfo.status != 0 || !renderInfo?.componentRenderInfos) return [] 36 | 37 | return filterRenderableKeys(renderInfo.componentRenderInfos) 38 | } 39 | 40 | function filterRenderableKeys(renderInfo) { 41 | return Object.entries(renderInfo) 42 | .filter(([_, v]) => v.render) 43 | .map(([k]) => k) 44 | } 45 | 46 | // 通过接口获取真实的渲染列表 47 | async function getRenderList( 48 | cookie, 49 | { gundamViewId, gdId, pageId, renderInfo }, 50 | guard 51 | ) { 52 | let data 53 | 54 | if (renderInfo) { 55 | const renderList = getLocalRenderList(renderInfo) 56 | 57 | if (renderList.length > 0) { 58 | return renderList 59 | } 60 | } 61 | 62 | try { 63 | const res = await request.get( 64 | 'https://market.waimai.meituan.com/gd/zc/renderinfo', 65 | { 66 | params: { 67 | el_biz: 'waimai', 68 | el_page: 'gundam.loader', 69 | gundam_id: gundamViewId, 70 | gdId: gdId, 71 | pageId: pageId, 72 | tenant: 'gundam' 73 | }, 74 | cookie, 75 | guard 76 | } 77 | ) 78 | 79 | data = res.data 80 | } catch (e) { 81 | console.log(e) 82 | 83 | throw new Error('renderinfo 接口调用失败:' + e.message) 84 | } 85 | 86 | return filterRenderableKeys(data) 87 | } 88 | 89 | function matchMoudleData(text, start, end) { 90 | const reg = new RegExp(`${start}.+?(?=${end})`) 91 | 92 | const res = text.match(reg) 93 | 94 | if (!res) return null 95 | 96 | const data = eval(`({moduleId:"${res[0]})`) 97 | 98 | return data 99 | } 100 | 101 | export { getTemplateData, getRenderList, matchMoudleData } 102 | -------------------------------------------------------------------------------- /test/user.js: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test } from 'vitest' 2 | import tough from 'tough-cookie' 3 | import ShadowGuard from '../src/shadow/index.js' 4 | import mainGrab from '../src/coupons/gundam.js' 5 | import { createMTCookie, getUserInfo, parseToken } from '../src/user.js' 6 | import { mainActConf } from '../src/coupons/const.js' 7 | 8 | const guard = new ShadowGuard() 9 | 10 | beforeAll(() => guard.init(mainGrab.getActUrl(mainActConf.gid))) 11 | 12 | test('Token Undefined', () => { 13 | expect(() => parseToken()).toThrow('请配置 TOKEN') 14 | }) 15 | 16 | test('String Token', () => { 17 | expect(parseToken('aaa')).toContainEqual({ 18 | token: 'aaa', 19 | alias: '', 20 | index: 1, 21 | tgUid: '', 22 | qywxUid: '', 23 | barkKey: '', 24 | larkWebhook: '', 25 | qq: '' 26 | }) 27 | }) 28 | 29 | test('JSON Token', () => { 30 | const token = ` 31 | { 32 | "token": "aaa" 33 | } 34 | ` 35 | 36 | expect(parseToken(token)).toContainEqual({ 37 | token: 'aaa', 38 | alias: '', 39 | index: 1, 40 | tgUid: '', 41 | qywxUid: '', 42 | barkKey: '', 43 | larkWebhook: '', 44 | qq: '' 45 | }) 46 | }) 47 | 48 | test('Multiple Token', () => { 49 | expect(parseToken('["aaa", {"token": "bbb", "alias": "jeff"}]')).toEqual( 50 | expect.arrayContaining([ 51 | { 52 | token: 'aaa', 53 | alias: '', 54 | index: 1, 55 | tgUid: '', 56 | qywxUid: '', 57 | barkKey: '', 58 | larkWebhook: '', 59 | qq: '' 60 | }, 61 | { 62 | token: 'bbb', 63 | alias: 'jeff', 64 | index: 2, 65 | tgUid: '', 66 | qywxUid: '', 67 | barkKey: '', 68 | larkWebhook: '', 69 | qq: '' 70 | } 71 | ]) 72 | ) 73 | }) 74 | 75 | test('JSON Token Verification', () => { 76 | expect(() => parseToken('{token: "aaa"}')).toThrow('TOKEN 解析错误') 77 | }) 78 | 79 | test('Multiple Token Verification', () => { 80 | expect(() => parseToken('["aaa", {token: "bbb"}]')).toThrow('TOKEN 解析错误') 81 | }) 82 | 83 | test('Cookie', () => { 84 | const tokens = parseToken(process.env.TOKEN) 85 | const cookie = createMTCookie(tokens[0].token) 86 | 87 | expect(cookie).toBeInstanceOf(tough.CookieJar) 88 | }) 89 | 90 | test('Login', async () => { 91 | const tokens = parseToken(process.env.TOKEN) 92 | const cookie = createMTCookie(tokens[0].token) 93 | const userInfo = await getUserInfo(cookie) 94 | 95 | expect(userInfo).toBeTruthy() 96 | }) 97 | 98 | test('Login With Guard', async () => { 99 | const tokens = parseToken(process.env.TOKEN) 100 | const cookie = createMTCookie(tokens[0].token) 101 | const userInfo = await getUserInfo(cookie, guard) 102 | 103 | expect(userInfo).toBeTruthy() 104 | }) 105 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.2.0 (2024-10-06) 2 | 3 | #### :rocket: New Feature 4 | 5 | - 适配最新 Guard 6 | 7 | #### :house: Internal 8 | 9 | - 升级 Eslint 9 10 | - 升级 Github Actions 依赖 11 | 12 | ## 2.1.0 (2024-01-01) 13 | 14 | HAPPY NEW YEAR! 🎉 15 | 16 | #### :rocket: New Feature 17 | 18 | - 支持社群红包 19 | - 支持服务号红包 20 | - 丰富通知信息 21 | - bark 通知添加 logo 22 | 23 | #### :house: Internal 24 | 25 | - 内部模块重构 26 | - 完善错误信息 27 | 28 | ## 2.0.0 (2023-12-20) 29 | 30 | #### ⚠️ Breaking changes 31 | 32 | - 依赖 Node.js 18+ 33 | - 依赖 pnpm 8+ 34 | - source 目录调整为 src 35 | 36 | #### :rocket: New Feature 37 | 38 | - 添加参数签名 39 | 40 | #### :house: Internal 41 | 42 | - 内部模块重构 43 | - 提升稳定性 44 | 45 | ## v1.9.0 (2023-09-04) 46 | 47 | #### :rocket: New Feature 48 | 49 | - 适配新版规则 50 | 51 | ## 1.8.1 (2023-01-15) 52 | 53 | #### :bug: Bug Fix 54 | 55 | - 修复 payload 获取失败问题 56 | - 修复多账号模式 57 | 58 | #### :house: Internal 59 | 60 | - 迁移 ECMAScript module 61 | - 稳定性提升 62 | 63 | ## 1.8.0 (2022-04-03) 64 | 65 | #### :rocket: New Feature 66 | 67 | - 支持 QQ 通知(Qmsg 酱) 68 | 69 | #### :bug: Bug Fix 70 | 71 | - 修复脚本失败问题 72 | 73 | ## 1.7.0 (2021-12-20) 74 | 75 | #### :rocket: New Feature 76 | 77 | - 使用动态载荷 78 | - 支持钉钉推送 79 | - 支持 pushplus 推送 80 | - 支持格式化的 JSON 配置 [@Code2qing, #11] 81 | 82 | #### :bug: Bug Fix 83 | 84 | - 输出接口错误信息 85 | 86 | #### :house: Internal 87 | 88 | - 提升稳定性 89 | 90 | ## 1.6.0 (2021-11-23) 91 | 92 | #### :rocket: New Feature 93 | 94 | - 支持飞书 bot 推送 95 | - 推送信息展示用户昵称 96 | 97 | #### :bug: Bug Fix 98 | 99 | - 升级领券接口 100 | 101 | #### :house: Internal 102 | 103 | - 提升稳定性 104 | 105 | ## 1.5.0 (2021-04-23) 106 | 107 | #### :rocket: New Feature 108 | 109 | - 并行化执行任务 110 | - 添加异常重试机制 111 | 112 | #### :bug: Bug Fix 113 | 114 | - 任务失败时的错误处理 115 | - unhandled rejection 处理 116 | 117 | #### :house: Internal 118 | 119 | - 调整定时触发时间为 11 点前 120 | - 日志添加项目 banner 121 | - 仅在大于当前版本时提示更新 122 | 123 | ## 1.4.0 (2021-04-18) 124 | 125 | #### :rocket: New Feature 126 | 127 | - 支持为 token 备注别名 128 | 129 | #### :bug: Bug Fix 130 | 131 | - 日志脱敏,防止隐私泄露 132 | 133 | #### :house: Internal 134 | 135 | - 调整定时触发时间 136 | 137 | ## 1.3.0 (2021-04-18) 138 | 139 | #### :rocket: New Feature 140 | 141 | - 升级至 **gundam API** 142 | - 添加 Release 更新提醒 143 | - 添加 sync 同步副本命令 144 | 145 | #### :house: Internal 146 | 147 | - 模块结构调整 148 | - 完善错误详情 149 | 150 | ## 1.2.0 (2021-04-01) 151 | 152 | #### :rocket: New Feature 153 | 154 | - TOKEN 支持多账户配置 155 | - 支持用户通知和全局通知 156 | 157 | #### :house: Internal 158 | 159 | - 异步输出通知结果 160 | 161 | ## 1.1.0 (2021-03-28) 162 | 163 | #### :rocket: New Feature 164 | 165 | - 支持 Server 酱推送 166 | - 支持 企业微信推送 167 | - 支持 Telegram 推送 168 | 169 | #### :house: Internal 170 | 171 | - 模块结构调整 172 | 173 | ## 1.0.0 (2021-03-28) 174 | 175 | #### :rocket: Feature 176 | 177 | - 定时领红包 178 | - 支持 Bark 推送 179 | -------------------------------------------------------------------------------- /src/coupons/index.js: -------------------------------------------------------------------------------- 1 | import request from '../request.js' 2 | import ShadowGuard from '../shadow/index.js' 3 | import { createMTCookie, getUserInfo } from '../user.js' 4 | import { mainActConf, gundamActConfs, wxfwhActConfs, ECODE } from './const.js' 5 | import gundam from './gundam.js' 6 | import wxfwh from './wxfwh.js' 7 | 8 | async function runTask(cookie, guard) { 9 | try { 10 | // 优先检测登录状态 11 | const userInfo = await getUserInfo(cookie) 12 | 13 | // 主线任务,失败时向外抛出异常 14 | const results = await gundam.grabCoupon(cookie, mainActConf.gid, guard) 15 | 16 | // 支线任务,并行执行提升效率 17 | const asyncResults = await Promise.all([ 18 | ...gundamActConfs.map((conf) => 19 | gundam.grabCoupon(cookie, conf.gid, guard).catch(() => []) 20 | ) 21 | // 微信服务号活动 22 | // ...wxfwhActConfs.map((conf) => 23 | // wxfwh.grabCoupon(cookie, conf.gid, guard).catch(() => []) 24 | // ) 25 | ]) 26 | 27 | results.push(...asyncResults.flat()) 28 | 29 | return { 30 | code: ECODE.SUCC, 31 | data: { 32 | userInfo, 33 | coupons: results 34 | }, 35 | msg: '成功' 36 | } 37 | } catch (e) { 38 | let code, msg 39 | 40 | // console.log('grabCoupon error', e) 41 | 42 | switch (e.code) { 43 | case ECODE.AUTH: 44 | code = ECODE.AUTH 45 | msg = '登录过期' 46 | break 47 | case request.ECODE.FETCH: 48 | code = ECODE.API 49 | msg = '接口异常' 50 | break 51 | case request.ECODE.TIMEOUT: 52 | case request.ECODE.NETWOEK: 53 | code = ECODE.NETWOEK 54 | msg = '网络异常' 55 | break 56 | default: 57 | code = ECODE.RUNTIME 58 | msg = '程序异常' 59 | } 60 | 61 | return { code, msg, error: e } 62 | } 63 | } 64 | 65 | /** 66 | * 领取优惠券 67 | * @param {String} token 用户 token 68 | * @param {Number} maxRetry 最大重试次数 69 | * @return {Promise()} 结果 70 | */ 71 | async function grabCoupons(token, { maxRetry = 0, proxy }) { 72 | if (!token) { 73 | return { 74 | code: ECODE.RUNTIME, 75 | msg: '请设置 token', 76 | error: '' 77 | } 78 | } 79 | 80 | // 优先设置代理 81 | if (proxy) { 82 | request.setProxyAgent(proxy) 83 | } 84 | 85 | const cookieJar = createMTCookie(token) 86 | 87 | // 复用 guard 88 | const guard = await new ShadowGuard().init(gundam.getActUrl(mainActConf.gid)) 89 | 90 | async function main(retryTimes = 0) { 91 | const result = await runTask(cookieJar, guard) 92 | const needRetry = [request.ECODE.NETWOEK, request.ECODE.API].includes( 93 | result.code 94 | ) 95 | 96 | // 标记重试次数 97 | result['retryTimes'] = retryTimes 98 | 99 | if (!needRetry || retryTimes >= maxRetry) return result 100 | 101 | return main(++retryTimes) 102 | } 103 | 104 | return main() 105 | } 106 | 107 | export { grabCoupons, ECODE } 108 | -------------------------------------------------------------------------------- /src/coupons/gundam.js: -------------------------------------------------------------------------------- 1 | import request from '../request.js' 2 | import { dateFormat, removePhoneRestriction } from '../util/index.js' 3 | import { getTemplateData, matchMoudleData } from '../template.js' 4 | import { ECODE } from './const.js' 5 | 6 | function resolveRedMod(text, renderList) { 7 | try { 8 | for (const instanceId of renderList) { 9 | const data = matchMoudleData( 10 | text, 11 | `gdc-fx-v2-netunion-red-envelope-${instanceId}`, 12 | ',directives' 13 | ) 14 | 15 | if (data) { 16 | data.instanceID = instanceId 17 | 18 | return data 19 | } 20 | } 21 | } catch { 22 | // ignore 23 | } 24 | 25 | return null 26 | } 27 | 28 | async function getPayload({ gundamId, gdId, appJs, renderList }, guard) { 29 | const jsText = await request(appJs).then((res) => res.text()) 30 | const data = resolveRedMod(jsText, renderList) 31 | 32 | if (!data) { 33 | throw new Error(`[${gundamId}] Gundam Payload 生成失败`) 34 | } 35 | 36 | return { 37 | actualLatitude: 0, 38 | actualLongitude: 0, 39 | ctype: 'h5', 40 | app: -1, 41 | platform: 3, 42 | couponAllConfigIdOrderString: data.expandCouponIds.keys.join(','), 43 | couponConfigIdOrderCommaString: data.priorityCouponIds.keys.join(','), 44 | // 这里取 number 类型的 gdId 45 | gundamId: gdId, 46 | instanceId: data.instanceID, 47 | h5Fingerprint: guard.h5fp, 48 | needTj: data.isStopTJCoupon 49 | } 50 | } 51 | 52 | function getActUrl(gundamId) { 53 | return new URL( 54 | `https://market.waimai.meituan.com/gd/single.html?el_biz=waimai&el_page=gundam.loader&gundam_id=${gundamId}` 55 | ) 56 | } 57 | 58 | function formatCoupons(coupons, actName) { 59 | function extractNumber(text) { 60 | const match = text.match(/满(\d+)可用/) 61 | 62 | return match ? parseInt(match[1], 10) : 0 63 | } 64 | 65 | return coupons.map((item) => { 66 | const etime = 67 | typeof item.etime === 'number' ? dateFormat(item.etime) : item.etime 68 | const amountLimit = extractNumber(item.amountLimit) 69 | 70 | return { 71 | name: item.couponName, 72 | etime, 73 | amount: item.couponAmount, 74 | amountLimit, 75 | useCondition: removePhoneRestriction(item.useCondition), 76 | actName: actName 77 | } 78 | }) 79 | } 80 | 81 | async function grabCoupon(cookie, gundamId, guard) { 82 | const actUrl = getActUrl(gundamId) 83 | const tmplData = await getTemplateData(cookie, gundamId, guard) 84 | const payload = await getPayload(tmplData, guard) 85 | const res = await request.post( 86 | 'https://mediacps.meituan.com/gundam/gundamGrabV4', 87 | payload, 88 | { 89 | cookie, 90 | headers: { 91 | Origin: actUrl.origin, 92 | Referer: actUrl.origin + '/' 93 | }, 94 | signType: 'header', 95 | guard 96 | } 97 | ) 98 | 99 | if (res.code == 0) { 100 | return formatCoupons(res.data.coupons, tmplData.actName) 101 | } 102 | 103 | const apiInfo = { 104 | api: 'gundamGrabV4', 105 | name: tmplData.actName, 106 | msg: res.msg || res.message 107 | } 108 | 109 | if (res.code == 3) { 110 | throw { code: ECODE.AUTH, ...apiInfo } 111 | } 112 | 113 | throw { code: ECODE.API, ...apiInfo } 114 | } 115 | 116 | export default { 117 | grabCoupon, 118 | getActUrl, 119 | getPayload 120 | } 121 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | import tough from 'tough-cookie' 2 | import timeoutSignal from 'timeout-signal' 3 | import HttpsProxyAgent from 'https-proxy-agent' 4 | 5 | const cookieJarMap = new Map() 6 | const UA = 7 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1' 8 | 9 | const ECODE = { 10 | FETCH: 'FETCH_ERROR', 11 | NETWOEK: 'NETWOEK_ERROR', 12 | TIMEOUT: 'TIMEOUT' 13 | } 14 | 15 | async function request(url, opts = {}) { 16 | const cookieJar = opts.cookie 17 | const existCookie = cookieJar?.getCookieStringSync?.(url) 18 | const optCookie = opts.headers?.cookie || '' 19 | const defHeader = { 20 | // 重要:需设置 UA 21 | 'User-Agent': UA, 22 | Connection: 'keep-alive' 23 | } 24 | let urlObj = new URL(url) 25 | let res 26 | 27 | if (opts.timeout) { 28 | opts.signal = timeoutSignal(opts.timeout) 29 | } 30 | 31 | if (existCookie) { 32 | defHeader.cookie = existCookie + ';' + optCookie 33 | } 34 | 35 | if (fetch._proxyAgent) { 36 | opts.agent = fetch._proxyAgent 37 | } else if (opts.proxy) { 38 | opts.agent = new HttpsProxyAgent(opts.proxy) 39 | } 40 | 41 | opts.headers = { ...defHeader, ...opts.headers } 42 | 43 | if (opts.params) { 44 | Object.keys(opts.params).forEach((key) => { 45 | urlObj.searchParams.append(key, opts.params?.[key]) 46 | }) 47 | } 48 | 49 | delete opts.cookie 50 | delete opts.timeout 51 | delete opts.params 52 | 53 | if (opts.guard) { 54 | const { url, headers } = await opts.guard.sign( 55 | { 56 | url: urlObj, 57 | method: opts.method, 58 | body: opts.body 59 | }, 60 | opts.signType 61 | ) 62 | 63 | urlObj = url 64 | opts.headers = { ...opts.headers, ...headers } 65 | } 66 | 67 | try { 68 | res = await fetch(urlObj, opts) 69 | } catch (e) { 70 | if (opts.signal?.aborted) { 71 | throw { code: ECODE.TIMEOUT, req: urlObj, msg: e } 72 | } 73 | 74 | throw { code: ECODE.FETCH, req: urlObj, msg: res.statusText } 75 | } 76 | 77 | const setCookies = res.headers['set-cookie'] 78 | 79 | if (setCookies) { 80 | setCookies.map((cookie) => 81 | cookieJar?.setCookieSync(cookie, res.url, { ignoreError: true }) 82 | ) 83 | } 84 | 85 | return res 86 | } 87 | 88 | async function doGet(url, opts = {}) { 89 | const res = await request(url, { 90 | ...opts, 91 | timeout: opts.timeout ?? 10000 92 | }) 93 | 94 | if (res.ok) return res.json() 95 | 96 | throw { code: ECODE.FETCH, url: url, msg: res.statusText } 97 | } 98 | 99 | async function doPost(url, data, opts = {}) { 100 | const payloadType = opts.type 101 | let cType, body 102 | 103 | if (payloadType == 'form') { 104 | const formData = new URLSearchParams(data) 105 | 106 | cType = 'application/x-www-form-urlencoded' 107 | body = formData.toString() 108 | } else { 109 | cType = 'application/json' 110 | body = data ? JSON.stringify(data) : '' 111 | } 112 | 113 | const res = await request(url, { 114 | ...opts, 115 | method: 'POST', 116 | body: body, 117 | headers: Object.assign({}, opts.headers, { 118 | 'Content-Type': cType 119 | }), 120 | timeout: opts.timeout ?? 10000 121 | }) 122 | 123 | if (res.ok) return res.json() 124 | 125 | throw { code: ECODE.FETCH, url: url, msg: res.statusText } 126 | } 127 | 128 | request.ECODE = ECODE 129 | request.get = doGet 130 | request.post = doPost 131 | request.setProxyAgent = (url) => { 132 | request._proxyAgent = new HttpsProxyAgent(url) 133 | } 134 | 135 | export function createCookieJar(id) { 136 | const cookieJar = new tough.CookieJar() 137 | 138 | cookieJarMap.set(id, cookieJar) 139 | 140 | return cookieJar 141 | } 142 | 143 | export default request 144 | -------------------------------------------------------------------------------- /src/coupons/wxfwh.js: -------------------------------------------------------------------------------- 1 | import request from '../request.js' 2 | import { dateFormat, groupBy } from '../util/index.js' 3 | import { getTemplateData, matchMoudleData } from '../template.js' 4 | import { ECODE } from './const.js' 5 | 6 | /* 服务号专属活动 */ 7 | 8 | function getActUrl(gundamId) { 9 | return new URL( 10 | `https://market.waimai.meituan.com/gd/single.html?el_biz=waimai&el_page=gundam.loader&gundam_id=${gundamId}` 11 | ) 12 | } 13 | 14 | function formatCoupons(coupons, actName) { 15 | return coupons.map((item) => { 16 | const etime = 17 | typeof item.couponEndTime === 'number' 18 | ? dateFormat(item.couponEndTime) 19 | : item.couponEndTime 20 | 21 | return { 22 | name: item.couponName, 23 | etime: etime, 24 | amount: item.couponValue, 25 | amountLimit: item.priceLimit, 26 | useCondition: '', 27 | actName: actName ?? '' 28 | } 29 | }) 30 | } 31 | 32 | async function getCouponList(cookie, viewId) { 33 | const res = await request.get( 34 | 'https://promotion.waimai.meituan.com/playcenter/generalcoupon/info', 35 | { 36 | params: { 37 | activityViewId: viewId 38 | }, 39 | cookie 40 | } 41 | ) 42 | 43 | if (res.code != 0) { 44 | throw new Error('服务号红包信息获取失败') 45 | } 46 | 47 | return res.data.couponList 48 | } 49 | 50 | async function getPayloadTabs(cookie, viewId) { 51 | const couponList = await getCouponList(cookie, viewId) 52 | const tabs = Object.entries(groupBy(couponList, 'planCode')).map( 53 | ([planCode, coupons]) => ({ 54 | planCode, 55 | rightCodes: coupons 56 | .filter((item) => item.status === 0) 57 | .map((item) => item.rightCode) 58 | }) 59 | ) 60 | 61 | return tabs.filter((item) => item.rightCodes.length) 62 | } 63 | 64 | async function getPayload( 65 | cookie, 66 | { gundamId, gdId, pageId, renderList, appJs }, 67 | guard 68 | ) { 69 | const jsText = await request(appJs).then((res) => res.text()) 70 | let data = null 71 | 72 | try { 73 | for (const instanceId of renderList) { 74 | const modData = matchMoudleData( 75 | jsText, 76 | `gdc-gd-cross-ticket-wall-${instanceId}`, 77 | 'materialConfig' 78 | ) 79 | 80 | if (modData) { 81 | data = { 82 | instanceId, 83 | viewId: modData.ticketConfig.playActivityId 84 | } 85 | 86 | break 87 | } 88 | } 89 | } catch (e) { 90 | // ignore 91 | } 92 | 93 | if (!data) { 94 | throw new Error(`[${gundamId}] wxfwh Payload 生成失败`) 95 | } 96 | 97 | const tabs = await getPayloadTabs(cookie, data.viewId) 98 | 99 | return { 100 | ctype: 'wm_wxapp', 101 | fpPlatform: 13, 102 | wxOpenId: '', 103 | appVersion: '', 104 | activityViewId: data.viewId, 105 | tabs: tabs, 106 | gdId: gdId, 107 | pageId: pageId, 108 | instanceId: data.instanceId, 109 | mtFingerprint: guard.fingerprint 110 | } 111 | } 112 | 113 | async function grabCoupon(cookie, gundamId, guard) { 114 | const actUrl = getActUrl(gundamId) 115 | const tmplData = await getTemplateData(cookie, gundamId, guard) 116 | const payload = await getPayload(cookie, tmplData, guard) 117 | 118 | if (!payload.tabs.length) { 119 | return [] 120 | } 121 | 122 | const res = await request.post( 123 | 'https://promotion.waimai.meituan.com/playcenter/generalcoupon/fetch', 124 | payload, 125 | { 126 | cookie, 127 | params: { 128 | isMini: 1, 129 | ctype: 'wm_wxapp', 130 | isInDpEnv: 0, 131 | pageId: payload.pageId, 132 | gdPageId: payload.gdId, 133 | instanceId: payload.instanceId 134 | }, 135 | headers: { 136 | Origin: actUrl.origin, 137 | Referer: actUrl.origin + '/' 138 | }, 139 | guard 140 | } 141 | ) 142 | 143 | if (res.code == 0) { 144 | return formatCoupons(res.data.couponList, tmplData.actName) 145 | } 146 | 147 | const apiInfo = { 148 | api: 'generalcoupon/fetch', 149 | name: tmplData.actName, 150 | msg: res.msg || res.message 151 | } 152 | 153 | if (res.code == 3) { 154 | throw { code: ECODE.AUTH, ...apiInfo } 155 | } 156 | 157 | throw { code: ECODE.API, ...apiInfo } 158 | } 159 | 160 | export default { 161 | grabCoupon, 162 | getActUrl, 163 | getPayload, 164 | getCouponList 165 | } 166 | -------------------------------------------------------------------------------- /src/coupons/lottery.js: -------------------------------------------------------------------------------- 1 | import request from '../request.js' 2 | import { getTemplateData } from '../template.js' 3 | 4 | const fetchStatus = { 5 | CAN_FETCH: 0, 6 | FETCH_ALREADY: 1, 7 | OUT_OF_STOCK: 2, 8 | INVALID_USER_TYPE: 3, 9 | CANNOT_FETCH: 4, 10 | FETCHED_BY_UUID: 5 11 | } 12 | 13 | function getActUrl(gundamId) { 14 | return new URL( 15 | `https://market.waimai.meituan.com/gd2/single.html?el_biz=waimai&el_page=gundam.loader&tenant=gundam&gundam_id=${gundamId}` 16 | ) 17 | } 18 | 19 | function resolveMetadata(renderList, jsText) { 20 | try { 21 | for (const instanceId of renderList) { 22 | const regRedMod = new RegExp( 23 | `gdc-new-ticket-wall-${instanceId}.+?(?=openUserCheck)` 24 | ) 25 | const res = jsText.match(regRedMod) 26 | 27 | if (res) { 28 | const data = eval(`({moduleId:"${res[0]}})`) 29 | 30 | data.instanceId = instanceId 31 | 32 | return data 33 | } 34 | } 35 | } catch (e) { 36 | return null 37 | } 38 | 39 | return null 40 | } 41 | 42 | async function getTicketConfig(appJs, renderList) { 43 | const jsText = await request(appJs).then((res) => res.text()) 44 | const data = resolveMetadata(renderList, jsText) 45 | const ticketConfig = data.ticketConfig.makeOptions1.ticketInfo1 46 | 47 | return { 48 | ...ticketConfig, 49 | instanceId: data.instanceId 50 | } 51 | } 52 | 53 | async function getPayload({ referId, gdId, pageId, instanceId }, guard) { 54 | const query = { 55 | couponReferId: referId, 56 | actualLng: 0, 57 | actualLat: 0, 58 | geoType: 2, 59 | version: 1, 60 | isInDpEnv: 0, 61 | gdPageId: gdId, 62 | pageId: pageId, 63 | instanceId: instanceId ?? '', 64 | sceneId: 1 65 | } 66 | const body = { 67 | appVersion: '', 68 | cType: 'wm_wxapp', 69 | fpPlatform: 3, 70 | mtFingerprint: guard.fingerprint, 71 | wxOpenId: '' 72 | } 73 | 74 | return { body, query } 75 | } 76 | 77 | function formatCoupons(coupons, info) { 78 | return coupons.map((item) => ({ 79 | name: item.couponName, 80 | etime: item.couponEndTime, 81 | amount: item.couponValue, 82 | amountLimit: item.priceLimit, 83 | useCondition: info.useCondition ?? '', 84 | actName: info.actName 85 | })) 86 | } 87 | 88 | async function getCouponList(cookie, couponIds) { 89 | const res = await request.get( 90 | `https://promotion.waimai.meituan.com/lottery/couponcomponent/info/v2`, 91 | { 92 | cookie, 93 | params: { 94 | couponReferIds: couponIds.join(','), 95 | actualLng: 0, 96 | actualLat: 0, 97 | geoType: 2, 98 | sceneId: 1, 99 | isInDpEnv: 0, 100 | cType: 'wm_wxapp' 101 | } 102 | } 103 | ) 104 | 105 | return res.data.couponList 106 | } 107 | 108 | async function grabCoupon(cookie, gundamId, guard) { 109 | const actUrl = getActUrl(gundamId) 110 | const tmplData = await getTemplateData(cookie, gundamId) 111 | const ticketConfig = await getTicketConfig( 112 | tmplData.appJs, 113 | tmplData.renderList 114 | ) 115 | const couponList = await getCouponList(cookie, [ 116 | ticketConfig.channelUrl ?? '' 117 | ]) 118 | const availCoupons = couponList.filter((coupon) => 119 | [fetchStatus.CAN_FETCH].includes(coupon.status) 120 | ) 121 | const results = Promise.all( 122 | availCoupons 123 | .map(async (coupon) => { 124 | const payload = getPayload( 125 | { 126 | referId: coupon.couponReferId, 127 | gdId: tmplData.gdId, 128 | pageId: tmplData.pageId, 129 | instanceId: ticketConfig.instanceId 130 | }, 131 | guard 132 | ) 133 | const res = await request.post( 134 | `https://promotion.waimai.meituan.com/lottery/couponcomponent/fetchcomponentcoupon/v2`, 135 | payload.body, 136 | { 137 | cookie, 138 | params: payload.query, 139 | headers: { 140 | mtgsig: '{}', 141 | Origin: actUrl.origin, 142 | Referer: actUrl.origin + '/' 143 | } 144 | } 145 | ) 146 | 147 | if (res.code == 0) { 148 | return coupon 149 | } 150 | 151 | return null 152 | }) 153 | .filter(Boolean) 154 | ) 155 | 156 | return formatCoupons(results, { 157 | actName: tmplData.actName, 158 | useCondition: ticketConfig.desc 159 | }) 160 | } 161 | 162 | export default { 163 | grabCoupon, 164 | getActUrl, 165 | getCouponList, 166 | getPayload, 167 | getTicketConfig, 168 | getTemplateData 169 | } 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 |
7 |

8 | workflow 9 | release 10 | update 11 | forks 12 | forks 13 |

14 |
15 | 16 | # 🧧 外卖神券天天领 Ver.2 17 | 18 |

外卖神券天天领,超值红包享不停;以自动化的方式领取外卖红包。


19 | 20 | > [!NOTE] 21 | > ★ 专注领劵,不搞杂七杂八
22 | > ★ 多帐号支持,全村都能配上
23 | > ★ 并行化任务,数管齐下更有效率
24 | > ★ 异常重试,一次不行再来一次
25 | > ★ 多路消息通知,总有一个到达你
26 | > ★ Github Actions 部署,操作如此简单 27 | 28 |
29 | 30 | ## 👨‍🍳 指南 31 | 32 | ### 环境要求 33 | 34 | - [Node.js](https://nodejs.org/) v20.0 及以上 35 | - [pnpm](https://pnpm.io/) v8.0 及以上 36 | 37 | ### 获取账号 Token 38 | 39 | [使用 Chrome DevTools 获取账号 Token](./docs/获取token.md) 40 | 41 | **示例:** 42 | 43 | ``` 44 | Js3xxxxFyy_Aq-rOnxMte6vKPV4AAAAA6QwAADgqRBSfcmNqyuG8CQ7JDL7xxxxNGbfF7tPNV5347_ANLcydua_JHCSRj0_xx 45 | ``` 46 | 47 | > [!IMPORTANT] 48 | > 账号 Token 仅限官方接口身份认证,本项目([vv314/actions-mtz-coupons](https://github.com/vv314/actions-mtz-coupons) )**不会存储和发送给第三方**。 49 | 50 | ### [GitHub Actions](https://docs.github.com/cn/actions) 部署 51 | 52 | #### 1. Fork 源项目 53 | 54 | 1. 访问 [vv314/actions-mtz-coupons](https://github.com/vv314/actions-mtz-coupons) 源仓库 55 | 2. 点击右上角 `Fork` 按钮 56 | 3. 点击右上角 `Star` 按钮 57 | 58 | 如果本项目对你有帮助,就让 ⭐️ 闪耀吧 ;) 59 | 60 | > [!TIP] 61 | > Fork 后的项目可执行 `npm run sync` 同步上游更新,详情参考 [一键同步](./docs/更新.md)。 62 | 63 | #### 2. 配置 Actions secrets 64 | 65 | 1. 导航到你的仓库主页面,点击 ⚙️**Settings** 66 | 2. 在边栏的 "Security" 部分中选择 **Secrets and variables**、然后单击 **Actions** 67 | 3. 选中 **Secrets** tab,点击 **New repository secret** 创建仓库密码 68 | 1. 在 `Name` 表单项填入 "TOKEN" 69 | 2. 在 `Secret` 表单项填入 Token 值(参考 [获取 Token](./docs/获取token.md)) 70 | 4. 点击 **Add secret** 保存配置 71 | 72 | #### 3. 启用 Actions 73 | 74 | 1. 导航到你的仓库主页面,点击 **Actions** 75 | 2. 在左侧边栏中,点击 **领红包** 76 | ![启用](https://github.com/vv314/actions-mtz-coupons/assets/7637375/7a1fb38d-8489-4d1a-9318-f1f2e5fab878) 77 | 3. 点击 **Enable workflow** 启用 Actions 78 | 79 | ### 脚本触发方式 80 | 81 | Github Actions 工作流支持**手动**与**自动**两种触发方式。 82 | 83 | #### 定时触发(默认开启) 84 | 85 | 每日 `11:00` 前定时执行。 86 | 87 | #### 手动触发 88 | 89 | - [在项目主页上调用](https://docs.github.com/cn/actions/managing-workflow-runs/manually-running-a-workflow#) 90 | - [使用 REST API 调用](https://docs.github.com/cn/rest/reference/actions#create-a-workflow-dispatch-event) 91 | 92 | ## 🤹‍♂️ 进阶用法 93 | 94 | - [使用 JSON Token](./docs/token配置.md) 95 | - [添加消息通知](./docs/通知.md) 96 | - [使用多账户配置](./docs/token配置.md) 97 | - [脚本更新](./docs/更新.md) 98 | - [本地运行](./docs/本地运行.md) 99 | 100 | ## 🍕 参与贡献 101 | 102 | 请参阅:[CONTRIBUTING.md](https://github.com/vv314/actions-mtz-coupons/blob/main/CONTRIBUTING.md) 103 | 104 | ## 📜 声明 105 | 106 | 本项目仅供学习与研究之用,请勿用于商业或非法用途。原作者不能完全保证项目的合法性,准确性和安全性,因使用不当造成的任何损失与损害,与原作者无关。请仔细阅读此声明,一旦您使用并复制了本项目,则视为已接受此声明。 107 | 108 | ## Star History 109 | 110 | [![Star History Chart](https://api.star-history.com/svg?repos=vv314/actions-mtz-coupons&type=Date)](https://star-history.com/#vv314/actions-mtz-coupons&Date) 111 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | process.on('unhandledRejection', (e) => { 2 | console.log('程序执行异常:', e) 3 | }) 4 | 5 | import pLimit from 'p-limit' 6 | import Notifier from './src/notifier/index.js' 7 | import { parseToken } from './src/user.js' 8 | import updateNotifier from './src/update-notifier.js' 9 | import { grabCoupons } from './src/coupons/index.js' 10 | import { maskNickName, readPkgJson } from './src/util/index.js' 11 | 12 | const { version: currentVersion } = readPkgJson() 13 | 14 | const TOKEN = process.env.TOKEN 15 | const notifier = new Notifier({ 16 | barkKey: process.env.BARK_KEY, 17 | larkWebhook: process.env.LARK_WEBHOOK, 18 | workWechat: process.env.QYWX_SEND_CONF, 19 | serverChanToken: process.env.SC_SEND_KEY, 20 | pushplusToken: process.env.PUSHPLUS_TOKEN, 21 | wxpusher: { 22 | token: process.env.WXPUSHER_TOKEN, 23 | topicId: process.env.WXPUSHER_TOPICID 24 | }, 25 | dingTalkWebhook: process.env.DINGTALK_WEBHOOK, 26 | telegram: { 27 | botToken: process.env.TG_BOT_TOKEN, 28 | userId: process.env.TG_USER_ID 29 | }, 30 | qmsg: { 31 | token: process.env.QMSG_KEY, 32 | qq: process.env.QMSG_ADMIN 33 | } 34 | }) 35 | 36 | const NOTIFY_TITLE = '外卖神券天天领' 37 | const MAX_RETRY_COUNT = 2 38 | const CHECK_UPDATE_TIMEOUT = 5000 39 | 40 | console.log(` 41 | ─────────────────────────────────────── 42 | actions-mtwm-coupons 43 | 外卖神券天天领 44 | ──────────────────────── 45 | 46 | Ver. ${currentVersion} 47 | 48 | Github @vv314\n`) 49 | 50 | function stringifyCoupons(coupons) { 51 | return coupons 52 | .map( 53 | (item) => 54 | `- ¥${item.amount} (${ 55 | item.amountLimit ? `满${item.amountLimit}可用` : '无门槛' 56 | } - ${item.name})` 57 | ) 58 | .join('\n') 59 | } 60 | 61 | function sendUserNotify({ status, message, account, userInfo }) { 62 | const result = [] 63 | const userName = userInfo.nickName 64 | const title = `${NOTIFY_TITLE}${status == 'success' ? '😋' : '😥'}` 65 | 66 | if (account.barkKey) { 67 | const qywxRes = notifier 68 | .sendBark(title, message, { key: account.barkKey }) 69 | .then((res) => `@${userName} ${res.msg}`) 70 | 71 | result.push(qywxRes) 72 | } 73 | 74 | if (account.qywxUid) { 75 | const qywxRes = notifier 76 | .sendWorkWechat(title, message, { 77 | uid: account.qywxUid 78 | }) 79 | .then((res) => `@${userName} ${res.msg}`) 80 | 81 | result.push(qywxRes) 82 | } 83 | 84 | if (account.larkWebhook) { 85 | const larkRes = notifier 86 | .sendLark(title, message, { 87 | webhook: account.larkWebhook 88 | }) 89 | .then((res) => `@${userName} ${res.msg}`) 90 | 91 | result.push(larkRes) 92 | } 93 | 94 | if (account.dtWebhook) { 95 | const dtRes = notifier 96 | .sendDingTalk(title, message, { 97 | webhook: account.dtWebhook 98 | }) 99 | .then((res) => `@${userName} ${res.msg}`) 100 | 101 | result.push(dtRes) 102 | } 103 | 104 | if (account.tgUid) { 105 | const tgRes = notifier 106 | .sendTelegram(title, message, { uid: account.tgUid }) 107 | .then((res) => `@${userName} ${res.msg}`) 108 | 109 | result.push(tgRes) 110 | } 111 | 112 | if (account.qq) { 113 | const tgRes = notifier 114 | .sendQmsg(title, message, { qq: account.qq }) 115 | .then((res) => `@${userName} ${res.msg}`) 116 | 117 | result.push(tgRes) 118 | } 119 | 120 | return result.map((p) => p.then((r) => `[用户通知] ${r}`)) 121 | } 122 | 123 | function sendGlobalNotify(tasks) { 124 | const message = tasks.map((t) => `账号 ${t.user}:\n${t.data}`).join('\n\n') 125 | const errorTasks = tasks.filter((t) => t.status == 'error') 126 | const allFailed = tasks.length && errorTasks.length === tasks.length 127 | const title = `${NOTIFY_TITLE}${ 128 | allFailed 129 | ? '😥' 130 | : errorTasks.length 131 | ? `[${tasks.length - errorTasks.length}/${tasks.length}]` 132 | : '😋' 133 | }` 134 | 135 | return notifier 136 | .notify(title, message) 137 | .map((p) => p.then((res) => `[全局通知] ${res.msg}`)) 138 | } 139 | 140 | function parseAccountName(account, userInfo = {}) { 141 | return account.alias || userInfo.nickName || `token${account.index}` 142 | } 143 | 144 | async function doJob(account, progress) { 145 | const res = await grabCoupons(account.token, { maxRetry: MAX_RETRY_COUNT }) 146 | const accountName = parseAccountName(account) 147 | 148 | console.log( 149 | `\n────────── [${progress.mark()}] 账号: ${accountName} ──────────\n` 150 | ) 151 | 152 | if (res.code != 0) { 153 | console.log(res.msg, res.error) 154 | 155 | res.retryTimes && console.log(`重试: ${res.retryTimes} 次`) 156 | 157 | console.log('\n😦 领取失败', `(v${currentVersion})`) 158 | 159 | return { 160 | status: 'error', 161 | user: accountName, 162 | data: `领取失败: ${res.msg}`, 163 | pushQueue: [] 164 | } 165 | } 166 | 167 | const { coupons, userInfo } = res.data 168 | 169 | console.log(...coupons) 170 | console.log(`\n红包已放入账号:${maskNickName(userInfo.nickName)}`) 171 | console.log(`\n🎉 领取成功!`) 172 | 173 | const message = stringifyCoupons(coupons) 174 | const pushQueue = sendUserNotify({ message, account, userInfo }) 175 | 176 | return { 177 | status: 'success', 178 | // 结合 userInfo 重新解析 userName 179 | user: parseAccountName(account, userInfo), 180 | data: message, 181 | pushQueue 182 | } 183 | } 184 | 185 | async function runTaskQueue(tokenList) { 186 | const asyncPool = pLimit(5) 187 | const progress = { 188 | count: 0, 189 | mark() { 190 | return `${++this.count}/${tokenList.length}` 191 | } 192 | } 193 | 194 | return Promise.all( 195 | tokenList.map((account) => asyncPool(doJob, account, progress)) 196 | ) 197 | } 198 | 199 | async function printNotifyResult(pushQueue) { 200 | if (pushQueue.length) { 201 | console.log(`\n────────── 推送通知 ──────────\n`) 202 | 203 | // 异步打印结果 204 | pushQueue.forEach((p) => p.then((res) => console.log(res))) 205 | } 206 | 207 | return Promise.all(pushQueue) 208 | } 209 | 210 | async function checkUpdate(timeout) { 211 | let message 212 | 213 | try { 214 | message = await updateNotifier(timeout) 215 | } catch (e) { 216 | console.log('\n', e) 217 | } 218 | 219 | if (!message) return 220 | 221 | console.log(`\n────────── 更新提醒 ──────────\n`) 222 | console.log(message) 223 | } 224 | 225 | async function main() { 226 | const tokens = parseToken(TOKEN) 227 | const tasks = await runTaskQueue(tokens) 228 | 229 | const globalPushQueue = sendGlobalNotify(tasks) 230 | const userPushQueue = tasks.map((res) => res.pushQueue).flat() 231 | // 打印通知结果,用户通知优先 232 | await printNotifyResult(userPushQueue.concat(globalPushQueue)) 233 | 234 | checkUpdate(CHECK_UPDATE_TIMEOUT) 235 | } 236 | 237 | main() 238 | -------------------------------------------------------------------------------- /src/notifier/index.js: -------------------------------------------------------------------------------- 1 | import sendBark from './vendor/bark.js' 2 | import sendLark from './vendor/lark.js' 3 | import sendTelegram from './vendor/telegram.js' 4 | import sendServerChan from './vendor/server-chan.js' 5 | import sendPushplus from './vendor/pushplus.js' 6 | import sendDingTalk from './vendor/dingtalk.js' 7 | import sendQmsg from './vendor/qmsg.js' 8 | import sendWxPusher from './vendor/wxpusher.js' 9 | import { sendWorkWechat, getQywxAccessToken } from './vendor/work-wechat.js' 10 | 11 | class Notifier { 12 | constructor(options) { 13 | this.barkKey = options.barkKey 14 | this.larkWebhook = options.larkWebhook 15 | this.serverChanToken = options.serverChanToken 16 | this.pushplusToken = options.pushplusToken 17 | this.wxpusher = options.wxpusher 18 | this.telegram = options.telegram 19 | this.dingTalkWebhook = options.dingTalkWebhook 20 | this.qmsg = options.qmsg 21 | this.workWechat = null 22 | this.qywxAccessToken = null 23 | 24 | if (options.workWechat) { 25 | try { 26 | this.workWechat = JSON.parse(options.workWechat) 27 | } catch (e) { 28 | throw new Error('企业微信配置 JSON 语法错误: ' + e) 29 | } 30 | } 31 | } 32 | 33 | async _getQywxAccessToken() { 34 | const { corpId, corpSecret } = this.workWechat 35 | const token = this.qywxAccessToken 36 | 37 | if (token && token.expires > Date.now()) { 38 | return token.value 39 | } 40 | 41 | const res = await getQywxAccessToken(corpId, corpSecret) 42 | 43 | this.qywxAccessToken = { 44 | value: res.token, 45 | expires: res.expires 46 | } 47 | 48 | return res.token 49 | } 50 | 51 | /** 52 | * Bark 通知 53 | * https://github.com/Finb/Bark 54 | * 55 | * @param {String} title 标题 56 | * @param {String} content 内容 57 | * @param {Object} options 配置 58 | * @return {Promise} 推送结果 59 | */ 60 | async sendBark(title = '', content = '', { link, key }) { 61 | const barkKey = key || this.barkKey 62 | 63 | if (!barkKey) { 64 | return { 65 | success: false, 66 | msg: `Bark 推送失败: 请设置 bark key` 67 | } 68 | } 69 | 70 | return sendBark({ title, content, link, pushKey: barkKey }) 71 | } 72 | 73 | /** 74 | * 飞书通知 75 | * https://www.feishu.cn/hc/zh-CN/articles/360040566333 76 | * 77 | * @param {String} title 标题 78 | * @param {String} content 内容 79 | * @param {Object} options 配置 80 | * @return {Promise} 推送结果 81 | */ 82 | async sendLark(title = '', content = '', { webhook }) { 83 | const larkWebhook = webhook || this.larkWebhook 84 | 85 | if (!larkWebhook) { 86 | return { 87 | success: false, 88 | msg: `飞书推送失败: 请设置 Webhook` 89 | } 90 | } 91 | 92 | return sendLark({ title, content, webhook: larkWebhook }) 93 | } 94 | 95 | /** 96 | * telegram bot 通知 97 | * https://core.telegram.org/bots/api#sendmessage 98 | * 99 | * @param {String} title 标题 100 | * @param {String} content 内容 101 | * @param {Object} options 配置 102 | * @return {Promise} 推送结果 103 | */ 104 | async sendTelegram(title = '', content = '', { uid = '' }) { 105 | const botToken = this.telegram && this.telegram.botToken 106 | const user = uid || this.telegram.userId 107 | 108 | if (!botToken) { 109 | return { 110 | success: false, 111 | msg: `Telegram 推送失败: 请设置 bot token` 112 | } 113 | } 114 | 115 | return sendTelegram({ 116 | botToken, 117 | content, 118 | title, 119 | user 120 | }) 121 | } 122 | 123 | /** 124 | * Server 酱通知 125 | * https://sct.ftqq.com/sendkey 126 | * 127 | * @param {String} title 标题 128 | * @param {String} content 内容 129 | * @return {Promise} 推送结果 130 | */ 131 | async sendServerChan(title = '', content = '') { 132 | return sendServerChan({ title, content, token: this.serverChanToken }) 133 | } 134 | 135 | /** 136 | * pushplus 通知 137 | * https://www.pushplus.plus/doc/guide/api.html 138 | * 139 | * @param {String} title 标题 140 | * @param {String} content 内容 141 | * @return {Promise} 推送结果 142 | */ 143 | async sendPushplus(title = '', content = '') { 144 | return sendPushplus({ title, content, token: this.pushplusToken }) 145 | } 146 | 147 | /** 148 | * wxpusher 通知 149 | * https://wxpusher.zjiecode.com/docs/#/ 150 | * 151 | * @param {String} title 标题 152 | * @param {String} content 内容 153 | * @return {Promise} 推送结果 154 | */ 155 | async sendWxPusher(title = '', content = '') { 156 | return sendWxPusher({ 157 | title, 158 | content, 159 | token: this.wxpusher.token, 160 | topicId: this.wxpusher.topicId 161 | }) 162 | } 163 | 164 | /** 165 | * 企业微信应用消息 166 | * https://work.weixin.qq.com/api/doc/90000/90135/90236 167 | * 168 | * @param {String} title 标题 169 | * @param {String} content 内容 170 | * @param {Object} options 配置 171 | * @return {Promise} 推送结果 172 | */ 173 | async sendWorkWechat(title = '', content = '', { uid = '' }) { 174 | if (!this.workWechat) { 175 | return { 176 | success: false, 177 | msg: `企业微信推送失败: 缺少 workWechat 配置` 178 | } 179 | } 180 | 181 | const accessToken = await this._getQywxAccessToken() 182 | const { agentId, toUser } = this.workWechat 183 | const user = uid || toUser || '@all' 184 | 185 | return sendWorkWechat({ 186 | accessToken, 187 | agentId, 188 | content, 189 | title, 190 | user 191 | }) 192 | } 193 | 194 | /** 195 | * 钉钉通知 196 | * https://open.dingtalk.com/document/robots/custom-robot-access 197 | * 198 | * @param {String} title 标题 199 | * @param {String} content 内容 200 | * @param {Object} options 配置 201 | * @return {Promise} 推送结果 202 | */ 203 | async sendDingTalk(title = '', content = '', { webhook }) { 204 | const dingTalkWebhook = webhook || this.dingTalkWebhook 205 | 206 | if (!dingTalkWebhook) { 207 | return { 208 | success: false, 209 | msg: `钉钉推送失败: 请设置 Webhook` 210 | } 211 | } 212 | 213 | return sendDingTalk({ title, content, webhook: dingTalkWebhook }) 214 | } 215 | 216 | /** 217 | * Qmsg 酱 218 | * https://qmsg.zendee.cn/api.html 219 | * 220 | * @param {String} title 标题 221 | * @param {String} content 内容 222 | * @param {Object} options 配置 223 | * @param {String} options.qq qq 号 224 | * @return {Promise} 推送结果 225 | */ 226 | async sendQmsg(title = '', content = '', { qq }) { 227 | const token = this.qmsg && this.qmsg.token 228 | const user = qq || this.qmsg.qq 229 | 230 | if (!token) { 231 | return { 232 | success: false, 233 | msg: `Qmsg 推送失败: 请设置 Qmsg token` 234 | } 235 | } 236 | 237 | return sendQmsg({ 238 | token, 239 | title, 240 | content, 241 | qq: user 242 | }) 243 | } 244 | 245 | /** 246 | * 聚合通知 247 | * 248 | * @param {String} title 标题 249 | * @param {String} content 内容 250 | * @param {Object} options 配置 251 | * @return {Promise} 推送结果 252 | */ 253 | notify(title, content, options = {}) { 254 | const result = [] 255 | 256 | if (this.barkKey) { 257 | result.push(this.sendBark(title, content, options)) 258 | } 259 | 260 | if (this.larkWebhook) { 261 | result.push(this.sendLark(title, content, options)) 262 | } 263 | 264 | if (this.dingTalkWebhook) { 265 | result.push(this.sendDingTalk(title, content, options)) 266 | } 267 | 268 | if (this.telegram && this.telegram.botToken) { 269 | result.push(this.sendTelegram(title, content, options)) 270 | } 271 | 272 | if (this.workWechat) { 273 | result.push(this.sendWorkWechat(title, content, options)) 274 | } 275 | 276 | if (this.serverChanToken) { 277 | result.push(this.sendServerChan(title, content, options)) 278 | } 279 | 280 | if (this.pushplusToken) { 281 | result.push(this.sendPushplus(title, content, options)) 282 | } 283 | 284 | if (this.wxpusher && this.wxpusher.token && this.wxpusher.topicId) { 285 | result.push(this.sendWxPusher(title, content)) 286 | } 287 | 288 | if (this.qmsg && this.qmsg.token) { 289 | result.push(this.sendQmsg(title, content, options)) 290 | } 291 | 292 | return result 293 | } 294 | } 295 | 296 | export default Notifier 297 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | '@wasmer/wasi': 9 | specifier: ^1.2.2 10 | version: 1.2.2 11 | https-proxy-agent: 12 | specifier: ^7.0.5 13 | version: 7.0.5 14 | p-limit: 15 | specifier: ^6.1.0 16 | version: 6.1.0 17 | semver: 18 | specifier: ^7.6.3 19 | version: 7.6.3 20 | timeout-signal: 21 | specifier: ^2.0.0 22 | version: 2.0.0 23 | tough-cookie: 24 | specifier: ^5.0.0 25 | version: 5.0.0 26 | 27 | devDependencies: 28 | dotenv: 29 | specifier: ^16.4.5 30 | version: 16.4.5 31 | eslint: 32 | specifier: ^9.13.0 33 | version: 9.13.0 34 | globals: 35 | specifier: ^15.10.0 36 | version: 15.11.0 37 | vitest: 38 | specifier: ^2.1.3 39 | version: 2.1.3 40 | 41 | packages: 42 | 43 | /@esbuild/aix-ppc64@0.21.5: 44 | resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} 45 | engines: {node: '>=12'} 46 | cpu: [ppc64] 47 | os: [aix] 48 | requiresBuild: true 49 | dev: true 50 | optional: true 51 | 52 | /@esbuild/android-arm64@0.21.5: 53 | resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} 54 | engines: {node: '>=12'} 55 | cpu: [arm64] 56 | os: [android] 57 | requiresBuild: true 58 | dev: true 59 | optional: true 60 | 61 | /@esbuild/android-arm@0.21.5: 62 | resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} 63 | engines: {node: '>=12'} 64 | cpu: [arm] 65 | os: [android] 66 | requiresBuild: true 67 | dev: true 68 | optional: true 69 | 70 | /@esbuild/android-x64@0.21.5: 71 | resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} 72 | engines: {node: '>=12'} 73 | cpu: [x64] 74 | os: [android] 75 | requiresBuild: true 76 | dev: true 77 | optional: true 78 | 79 | /@esbuild/darwin-arm64@0.21.5: 80 | resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} 81 | engines: {node: '>=12'} 82 | cpu: [arm64] 83 | os: [darwin] 84 | requiresBuild: true 85 | dev: true 86 | optional: true 87 | 88 | /@esbuild/darwin-x64@0.21.5: 89 | resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} 90 | engines: {node: '>=12'} 91 | cpu: [x64] 92 | os: [darwin] 93 | requiresBuild: true 94 | dev: true 95 | optional: true 96 | 97 | /@esbuild/freebsd-arm64@0.21.5: 98 | resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} 99 | engines: {node: '>=12'} 100 | cpu: [arm64] 101 | os: [freebsd] 102 | requiresBuild: true 103 | dev: true 104 | optional: true 105 | 106 | /@esbuild/freebsd-x64@0.21.5: 107 | resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} 108 | engines: {node: '>=12'} 109 | cpu: [x64] 110 | os: [freebsd] 111 | requiresBuild: true 112 | dev: true 113 | optional: true 114 | 115 | /@esbuild/linux-arm64@0.21.5: 116 | resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} 117 | engines: {node: '>=12'} 118 | cpu: [arm64] 119 | os: [linux] 120 | requiresBuild: true 121 | dev: true 122 | optional: true 123 | 124 | /@esbuild/linux-arm@0.21.5: 125 | resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} 126 | engines: {node: '>=12'} 127 | cpu: [arm] 128 | os: [linux] 129 | requiresBuild: true 130 | dev: true 131 | optional: true 132 | 133 | /@esbuild/linux-ia32@0.21.5: 134 | resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} 135 | engines: {node: '>=12'} 136 | cpu: [ia32] 137 | os: [linux] 138 | requiresBuild: true 139 | dev: true 140 | optional: true 141 | 142 | /@esbuild/linux-loong64@0.21.5: 143 | resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} 144 | engines: {node: '>=12'} 145 | cpu: [loong64] 146 | os: [linux] 147 | requiresBuild: true 148 | dev: true 149 | optional: true 150 | 151 | /@esbuild/linux-mips64el@0.21.5: 152 | resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} 153 | engines: {node: '>=12'} 154 | cpu: [mips64el] 155 | os: [linux] 156 | requiresBuild: true 157 | dev: true 158 | optional: true 159 | 160 | /@esbuild/linux-ppc64@0.21.5: 161 | resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} 162 | engines: {node: '>=12'} 163 | cpu: [ppc64] 164 | os: [linux] 165 | requiresBuild: true 166 | dev: true 167 | optional: true 168 | 169 | /@esbuild/linux-riscv64@0.21.5: 170 | resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} 171 | engines: {node: '>=12'} 172 | cpu: [riscv64] 173 | os: [linux] 174 | requiresBuild: true 175 | dev: true 176 | optional: true 177 | 178 | /@esbuild/linux-s390x@0.21.5: 179 | resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} 180 | engines: {node: '>=12'} 181 | cpu: [s390x] 182 | os: [linux] 183 | requiresBuild: true 184 | dev: true 185 | optional: true 186 | 187 | /@esbuild/linux-x64@0.21.5: 188 | resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} 189 | engines: {node: '>=12'} 190 | cpu: [x64] 191 | os: [linux] 192 | requiresBuild: true 193 | dev: true 194 | optional: true 195 | 196 | /@esbuild/netbsd-x64@0.21.5: 197 | resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} 198 | engines: {node: '>=12'} 199 | cpu: [x64] 200 | os: [netbsd] 201 | requiresBuild: true 202 | dev: true 203 | optional: true 204 | 205 | /@esbuild/openbsd-x64@0.21.5: 206 | resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} 207 | engines: {node: '>=12'} 208 | cpu: [x64] 209 | os: [openbsd] 210 | requiresBuild: true 211 | dev: true 212 | optional: true 213 | 214 | /@esbuild/sunos-x64@0.21.5: 215 | resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} 216 | engines: {node: '>=12'} 217 | cpu: [x64] 218 | os: [sunos] 219 | requiresBuild: true 220 | dev: true 221 | optional: true 222 | 223 | /@esbuild/win32-arm64@0.21.5: 224 | resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} 225 | engines: {node: '>=12'} 226 | cpu: [arm64] 227 | os: [win32] 228 | requiresBuild: true 229 | dev: true 230 | optional: true 231 | 232 | /@esbuild/win32-ia32@0.21.5: 233 | resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} 234 | engines: {node: '>=12'} 235 | cpu: [ia32] 236 | os: [win32] 237 | requiresBuild: true 238 | dev: true 239 | optional: true 240 | 241 | /@esbuild/win32-x64@0.21.5: 242 | resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} 243 | engines: {node: '>=12'} 244 | cpu: [x64] 245 | os: [win32] 246 | requiresBuild: true 247 | dev: true 248 | optional: true 249 | 250 | /@eslint-community/eslint-utils@4.4.0(eslint@9.13.0): 251 | resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} 252 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 253 | peerDependencies: 254 | eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 255 | dependencies: 256 | eslint: 9.13.0 257 | eslint-visitor-keys: 3.4.3 258 | dev: true 259 | 260 | /@eslint-community/regexpp@4.11.1: 261 | resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} 262 | engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 263 | dev: true 264 | 265 | /@eslint/config-array@0.18.0: 266 | resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} 267 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 268 | dependencies: 269 | '@eslint/object-schema': 2.1.4 270 | debug: 4.3.7 271 | minimatch: 3.1.2 272 | transitivePeerDependencies: 273 | - supports-color 274 | dev: true 275 | 276 | /@eslint/core@0.7.0: 277 | resolution: {integrity: sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==} 278 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 279 | dev: true 280 | 281 | /@eslint/eslintrc@3.1.0: 282 | resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} 283 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 284 | dependencies: 285 | ajv: 6.12.6 286 | debug: 4.3.7 287 | espree: 10.2.0 288 | globals: 14.0.0 289 | ignore: 5.3.2 290 | import-fresh: 3.3.0 291 | js-yaml: 4.1.0 292 | minimatch: 3.1.2 293 | strip-json-comments: 3.1.1 294 | transitivePeerDependencies: 295 | - supports-color 296 | dev: true 297 | 298 | /@eslint/js@9.13.0: 299 | resolution: {integrity: sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==} 300 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 301 | dev: true 302 | 303 | /@eslint/object-schema@2.1.4: 304 | resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} 305 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 306 | dev: true 307 | 308 | /@eslint/plugin-kit@0.2.1: 309 | resolution: {integrity: sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==} 310 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 311 | dependencies: 312 | levn: 0.4.1 313 | dev: true 314 | 315 | /@humanfs/core@0.19.0: 316 | resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} 317 | engines: {node: '>=18.18.0'} 318 | dev: true 319 | 320 | /@humanfs/node@0.16.5: 321 | resolution: {integrity: sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==} 322 | engines: {node: '>=18.18.0'} 323 | dependencies: 324 | '@humanfs/core': 0.19.0 325 | '@humanwhocodes/retry': 0.3.1 326 | dev: true 327 | 328 | /@humanwhocodes/module-importer@1.0.1: 329 | resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 330 | engines: {node: '>=12.22'} 331 | dev: true 332 | 333 | /@humanwhocodes/retry@0.3.1: 334 | resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} 335 | engines: {node: '>=18.18'} 336 | dev: true 337 | 338 | /@jridgewell/sourcemap-codec@1.5.0: 339 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 340 | dev: true 341 | 342 | /@rollup/rollup-android-arm-eabi@4.24.0: 343 | resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} 344 | cpu: [arm] 345 | os: [android] 346 | requiresBuild: true 347 | dev: true 348 | optional: true 349 | 350 | /@rollup/rollup-android-arm64@4.24.0: 351 | resolution: {integrity: sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==} 352 | cpu: [arm64] 353 | os: [android] 354 | requiresBuild: true 355 | dev: true 356 | optional: true 357 | 358 | /@rollup/rollup-darwin-arm64@4.24.0: 359 | resolution: {integrity: sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==} 360 | cpu: [arm64] 361 | os: [darwin] 362 | requiresBuild: true 363 | dev: true 364 | optional: true 365 | 366 | /@rollup/rollup-darwin-x64@4.24.0: 367 | resolution: {integrity: sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==} 368 | cpu: [x64] 369 | os: [darwin] 370 | requiresBuild: true 371 | dev: true 372 | optional: true 373 | 374 | /@rollup/rollup-linux-arm-gnueabihf@4.24.0: 375 | resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==} 376 | cpu: [arm] 377 | os: [linux] 378 | libc: [glibc] 379 | requiresBuild: true 380 | dev: true 381 | optional: true 382 | 383 | /@rollup/rollup-linux-arm-musleabihf@4.24.0: 384 | resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==} 385 | cpu: [arm] 386 | os: [linux] 387 | libc: [musl] 388 | requiresBuild: true 389 | dev: true 390 | optional: true 391 | 392 | /@rollup/rollup-linux-arm64-gnu@4.24.0: 393 | resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==} 394 | cpu: [arm64] 395 | os: [linux] 396 | libc: [glibc] 397 | requiresBuild: true 398 | dev: true 399 | optional: true 400 | 401 | /@rollup/rollup-linux-arm64-musl@4.24.0: 402 | resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==} 403 | cpu: [arm64] 404 | os: [linux] 405 | libc: [musl] 406 | requiresBuild: true 407 | dev: true 408 | optional: true 409 | 410 | /@rollup/rollup-linux-powerpc64le-gnu@4.24.0: 411 | resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==} 412 | cpu: [ppc64] 413 | os: [linux] 414 | libc: [glibc] 415 | requiresBuild: true 416 | dev: true 417 | optional: true 418 | 419 | /@rollup/rollup-linux-riscv64-gnu@4.24.0: 420 | resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==} 421 | cpu: [riscv64] 422 | os: [linux] 423 | libc: [glibc] 424 | requiresBuild: true 425 | dev: true 426 | optional: true 427 | 428 | /@rollup/rollup-linux-s390x-gnu@4.24.0: 429 | resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==} 430 | cpu: [s390x] 431 | os: [linux] 432 | libc: [glibc] 433 | requiresBuild: true 434 | dev: true 435 | optional: true 436 | 437 | /@rollup/rollup-linux-x64-gnu@4.24.0: 438 | resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} 439 | cpu: [x64] 440 | os: [linux] 441 | libc: [glibc] 442 | requiresBuild: true 443 | dev: true 444 | optional: true 445 | 446 | /@rollup/rollup-linux-x64-musl@4.24.0: 447 | resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==} 448 | cpu: [x64] 449 | os: [linux] 450 | libc: [musl] 451 | requiresBuild: true 452 | dev: true 453 | optional: true 454 | 455 | /@rollup/rollup-win32-arm64-msvc@4.24.0: 456 | resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==} 457 | cpu: [arm64] 458 | os: [win32] 459 | requiresBuild: true 460 | dev: true 461 | optional: true 462 | 463 | /@rollup/rollup-win32-ia32-msvc@4.24.0: 464 | resolution: {integrity: sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==} 465 | cpu: [ia32] 466 | os: [win32] 467 | requiresBuild: true 468 | dev: true 469 | optional: true 470 | 471 | /@rollup/rollup-win32-x64-msvc@4.24.0: 472 | resolution: {integrity: sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==} 473 | cpu: [x64] 474 | os: [win32] 475 | requiresBuild: true 476 | dev: true 477 | optional: true 478 | 479 | /@types/estree@1.0.6: 480 | resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 481 | dev: true 482 | 483 | /@types/json-schema@7.0.15: 484 | resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 485 | dev: true 486 | 487 | /@vitest/expect@2.1.3: 488 | resolution: {integrity: sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==} 489 | dependencies: 490 | '@vitest/spy': 2.1.3 491 | '@vitest/utils': 2.1.3 492 | chai: 5.1.1 493 | tinyrainbow: 1.2.0 494 | dev: true 495 | 496 | /@vitest/mocker@2.1.3(@vitest/spy@2.1.3)(vite@5.4.9): 497 | resolution: {integrity: sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==} 498 | peerDependencies: 499 | '@vitest/spy': 2.1.3 500 | msw: ^2.3.5 501 | vite: ^5.0.0 502 | peerDependenciesMeta: 503 | msw: 504 | optional: true 505 | vite: 506 | optional: true 507 | dependencies: 508 | '@vitest/spy': 2.1.3 509 | estree-walker: 3.0.3 510 | magic-string: 0.30.12 511 | vite: 5.4.9 512 | dev: true 513 | 514 | /@vitest/pretty-format@2.1.3: 515 | resolution: {integrity: sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==} 516 | dependencies: 517 | tinyrainbow: 1.2.0 518 | dev: true 519 | 520 | /@vitest/runner@2.1.3: 521 | resolution: {integrity: sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==} 522 | dependencies: 523 | '@vitest/utils': 2.1.3 524 | pathe: 1.1.2 525 | dev: true 526 | 527 | /@vitest/snapshot@2.1.3: 528 | resolution: {integrity: sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==} 529 | dependencies: 530 | '@vitest/pretty-format': 2.1.3 531 | magic-string: 0.30.12 532 | pathe: 1.1.2 533 | dev: true 534 | 535 | /@vitest/spy@2.1.3: 536 | resolution: {integrity: sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==} 537 | dependencies: 538 | tinyspy: 3.0.2 539 | dev: true 540 | 541 | /@vitest/utils@2.1.3: 542 | resolution: {integrity: sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==} 543 | dependencies: 544 | '@vitest/pretty-format': 2.1.3 545 | loupe: 3.1.2 546 | tinyrainbow: 1.2.0 547 | dev: true 548 | 549 | /@wasmer/wasi@1.2.2: 550 | resolution: {integrity: sha512-39ZB3gefOVhBmkhf7Ta79RRSV/emIV8LhdvcWhP/MOZEjMmtzoZWMzt7phdKj8CUXOze+AwbvGK60lKaKldn1w==} 551 | dev: false 552 | 553 | /acorn-jsx@5.3.2(acorn@8.13.0): 554 | resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 555 | peerDependencies: 556 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 557 | dependencies: 558 | acorn: 8.13.0 559 | dev: true 560 | 561 | /acorn@8.13.0: 562 | resolution: {integrity: sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==} 563 | engines: {node: '>=0.4.0'} 564 | hasBin: true 565 | dev: true 566 | 567 | /agent-base@7.1.1: 568 | resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} 569 | engines: {node: '>= 14'} 570 | dependencies: 571 | debug: 4.3.7 572 | transitivePeerDependencies: 573 | - supports-color 574 | dev: false 575 | 576 | /ajv@6.12.6: 577 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 578 | dependencies: 579 | fast-deep-equal: 3.1.3 580 | fast-json-stable-stringify: 2.1.0 581 | json-schema-traverse: 0.4.1 582 | uri-js: 4.4.1 583 | dev: true 584 | 585 | /ansi-styles@4.3.0: 586 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 587 | engines: {node: '>=8'} 588 | dependencies: 589 | color-convert: 2.0.1 590 | dev: true 591 | 592 | /argparse@2.0.1: 593 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 594 | dev: true 595 | 596 | /assertion-error@2.0.1: 597 | resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 598 | engines: {node: '>=12'} 599 | dev: true 600 | 601 | /balanced-match@1.0.2: 602 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 603 | dev: true 604 | 605 | /brace-expansion@1.1.11: 606 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 607 | dependencies: 608 | balanced-match: 1.0.2 609 | concat-map: 0.0.1 610 | dev: true 611 | 612 | /cac@6.7.14: 613 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 614 | engines: {node: '>=8'} 615 | dev: true 616 | 617 | /callsites@3.1.0: 618 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 619 | engines: {node: '>=6'} 620 | dev: true 621 | 622 | /chai@5.1.1: 623 | resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} 624 | engines: {node: '>=12'} 625 | dependencies: 626 | assertion-error: 2.0.1 627 | check-error: 2.1.1 628 | deep-eql: 5.0.2 629 | loupe: 3.1.2 630 | pathval: 2.0.0 631 | dev: true 632 | 633 | /chalk@4.1.2: 634 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 635 | engines: {node: '>=10'} 636 | dependencies: 637 | ansi-styles: 4.3.0 638 | supports-color: 7.2.0 639 | dev: true 640 | 641 | /check-error@2.1.1: 642 | resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} 643 | engines: {node: '>= 16'} 644 | dev: true 645 | 646 | /color-convert@2.0.1: 647 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 648 | engines: {node: '>=7.0.0'} 649 | dependencies: 650 | color-name: 1.1.4 651 | dev: true 652 | 653 | /color-name@1.1.4: 654 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 655 | dev: true 656 | 657 | /concat-map@0.0.1: 658 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 659 | dev: true 660 | 661 | /cross-spawn@7.0.3: 662 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 663 | engines: {node: '>= 8'} 664 | dependencies: 665 | path-key: 3.1.1 666 | shebang-command: 2.0.0 667 | which: 2.0.2 668 | dev: true 669 | 670 | /debug@4.3.7: 671 | resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} 672 | engines: {node: '>=6.0'} 673 | peerDependencies: 674 | supports-color: '*' 675 | peerDependenciesMeta: 676 | supports-color: 677 | optional: true 678 | dependencies: 679 | ms: 2.1.3 680 | 681 | /deep-eql@5.0.2: 682 | resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 683 | engines: {node: '>=6'} 684 | dev: true 685 | 686 | /deep-is@0.1.4: 687 | resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 688 | dev: true 689 | 690 | /dotenv@16.4.5: 691 | resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} 692 | engines: {node: '>=12'} 693 | dev: true 694 | 695 | /esbuild@0.21.5: 696 | resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} 697 | engines: {node: '>=12'} 698 | hasBin: true 699 | requiresBuild: true 700 | optionalDependencies: 701 | '@esbuild/aix-ppc64': 0.21.5 702 | '@esbuild/android-arm': 0.21.5 703 | '@esbuild/android-arm64': 0.21.5 704 | '@esbuild/android-x64': 0.21.5 705 | '@esbuild/darwin-arm64': 0.21.5 706 | '@esbuild/darwin-x64': 0.21.5 707 | '@esbuild/freebsd-arm64': 0.21.5 708 | '@esbuild/freebsd-x64': 0.21.5 709 | '@esbuild/linux-arm': 0.21.5 710 | '@esbuild/linux-arm64': 0.21.5 711 | '@esbuild/linux-ia32': 0.21.5 712 | '@esbuild/linux-loong64': 0.21.5 713 | '@esbuild/linux-mips64el': 0.21.5 714 | '@esbuild/linux-ppc64': 0.21.5 715 | '@esbuild/linux-riscv64': 0.21.5 716 | '@esbuild/linux-s390x': 0.21.5 717 | '@esbuild/linux-x64': 0.21.5 718 | '@esbuild/netbsd-x64': 0.21.5 719 | '@esbuild/openbsd-x64': 0.21.5 720 | '@esbuild/sunos-x64': 0.21.5 721 | '@esbuild/win32-arm64': 0.21.5 722 | '@esbuild/win32-ia32': 0.21.5 723 | '@esbuild/win32-x64': 0.21.5 724 | dev: true 725 | 726 | /escape-string-regexp@4.0.0: 727 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 728 | engines: {node: '>=10'} 729 | dev: true 730 | 731 | /eslint-scope@8.1.0: 732 | resolution: {integrity: sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==} 733 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 734 | dependencies: 735 | esrecurse: 4.3.0 736 | estraverse: 5.3.0 737 | dev: true 738 | 739 | /eslint-visitor-keys@3.4.3: 740 | resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 741 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 742 | dev: true 743 | 744 | /eslint-visitor-keys@4.1.0: 745 | resolution: {integrity: sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==} 746 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 747 | dev: true 748 | 749 | /eslint@9.13.0: 750 | resolution: {integrity: sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==} 751 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 752 | hasBin: true 753 | peerDependencies: 754 | jiti: '*' 755 | peerDependenciesMeta: 756 | jiti: 757 | optional: true 758 | dependencies: 759 | '@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0) 760 | '@eslint-community/regexpp': 4.11.1 761 | '@eslint/config-array': 0.18.0 762 | '@eslint/core': 0.7.0 763 | '@eslint/eslintrc': 3.1.0 764 | '@eslint/js': 9.13.0 765 | '@eslint/plugin-kit': 0.2.1 766 | '@humanfs/node': 0.16.5 767 | '@humanwhocodes/module-importer': 1.0.1 768 | '@humanwhocodes/retry': 0.3.1 769 | '@types/estree': 1.0.6 770 | '@types/json-schema': 7.0.15 771 | ajv: 6.12.6 772 | chalk: 4.1.2 773 | cross-spawn: 7.0.3 774 | debug: 4.3.7 775 | escape-string-regexp: 4.0.0 776 | eslint-scope: 8.1.0 777 | eslint-visitor-keys: 4.1.0 778 | espree: 10.2.0 779 | esquery: 1.6.0 780 | esutils: 2.0.3 781 | fast-deep-equal: 3.1.3 782 | file-entry-cache: 8.0.0 783 | find-up: 5.0.0 784 | glob-parent: 6.0.2 785 | ignore: 5.3.2 786 | imurmurhash: 0.1.4 787 | is-glob: 4.0.3 788 | json-stable-stringify-without-jsonify: 1.0.1 789 | lodash.merge: 4.6.2 790 | minimatch: 3.1.2 791 | natural-compare: 1.4.0 792 | optionator: 0.9.4 793 | text-table: 0.2.0 794 | transitivePeerDependencies: 795 | - supports-color 796 | dev: true 797 | 798 | /espree@10.2.0: 799 | resolution: {integrity: sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==} 800 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 801 | dependencies: 802 | acorn: 8.13.0 803 | acorn-jsx: 5.3.2(acorn@8.13.0) 804 | eslint-visitor-keys: 4.1.0 805 | dev: true 806 | 807 | /esquery@1.6.0: 808 | resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} 809 | engines: {node: '>=0.10'} 810 | dependencies: 811 | estraverse: 5.3.0 812 | dev: true 813 | 814 | /esrecurse@4.3.0: 815 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 816 | engines: {node: '>=4.0'} 817 | dependencies: 818 | estraverse: 5.3.0 819 | dev: true 820 | 821 | /estraverse@5.3.0: 822 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 823 | engines: {node: '>=4.0'} 824 | dev: true 825 | 826 | /estree-walker@3.0.3: 827 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 828 | dependencies: 829 | '@types/estree': 1.0.6 830 | dev: true 831 | 832 | /esutils@2.0.3: 833 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 834 | engines: {node: '>=0.10.0'} 835 | dev: true 836 | 837 | /fast-deep-equal@3.1.3: 838 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 839 | dev: true 840 | 841 | /fast-json-stable-stringify@2.1.0: 842 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 843 | dev: true 844 | 845 | /fast-levenshtein@2.0.6: 846 | resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 847 | dev: true 848 | 849 | /file-entry-cache@8.0.0: 850 | resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} 851 | engines: {node: '>=16.0.0'} 852 | dependencies: 853 | flat-cache: 4.0.1 854 | dev: true 855 | 856 | /find-up@5.0.0: 857 | resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 858 | engines: {node: '>=10'} 859 | dependencies: 860 | locate-path: 6.0.0 861 | path-exists: 4.0.0 862 | dev: true 863 | 864 | /flat-cache@4.0.1: 865 | resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} 866 | engines: {node: '>=16'} 867 | dependencies: 868 | flatted: 3.3.1 869 | keyv: 4.5.4 870 | dev: true 871 | 872 | /flatted@3.3.1: 873 | resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} 874 | dev: true 875 | 876 | /fsevents@2.3.3: 877 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 878 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 879 | os: [darwin] 880 | requiresBuild: true 881 | dev: true 882 | optional: true 883 | 884 | /glob-parent@6.0.2: 885 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 886 | engines: {node: '>=10.13.0'} 887 | dependencies: 888 | is-glob: 4.0.3 889 | dev: true 890 | 891 | /globals@14.0.0: 892 | resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} 893 | engines: {node: '>=18'} 894 | dev: true 895 | 896 | /globals@15.11.0: 897 | resolution: {integrity: sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==} 898 | engines: {node: '>=18'} 899 | dev: true 900 | 901 | /has-flag@4.0.0: 902 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 903 | engines: {node: '>=8'} 904 | dev: true 905 | 906 | /https-proxy-agent@7.0.5: 907 | resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} 908 | engines: {node: '>= 14'} 909 | dependencies: 910 | agent-base: 7.1.1 911 | debug: 4.3.7 912 | transitivePeerDependencies: 913 | - supports-color 914 | dev: false 915 | 916 | /ignore@5.3.2: 917 | resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 918 | engines: {node: '>= 4'} 919 | dev: true 920 | 921 | /import-fresh@3.3.0: 922 | resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} 923 | engines: {node: '>=6'} 924 | dependencies: 925 | parent-module: 1.0.1 926 | resolve-from: 4.0.0 927 | dev: true 928 | 929 | /imurmurhash@0.1.4: 930 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 931 | engines: {node: '>=0.8.19'} 932 | dev: true 933 | 934 | /is-extglob@2.1.1: 935 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 936 | engines: {node: '>=0.10.0'} 937 | dev: true 938 | 939 | /is-glob@4.0.3: 940 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 941 | engines: {node: '>=0.10.0'} 942 | dependencies: 943 | is-extglob: 2.1.1 944 | dev: true 945 | 946 | /isexe@2.0.0: 947 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 948 | dev: true 949 | 950 | /js-yaml@4.1.0: 951 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 952 | hasBin: true 953 | dependencies: 954 | argparse: 2.0.1 955 | dev: true 956 | 957 | /json-buffer@3.0.1: 958 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 959 | dev: true 960 | 961 | /json-schema-traverse@0.4.1: 962 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 963 | dev: true 964 | 965 | /json-stable-stringify-without-jsonify@1.0.1: 966 | resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 967 | dev: true 968 | 969 | /keyv@4.5.4: 970 | resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 971 | dependencies: 972 | json-buffer: 3.0.1 973 | dev: true 974 | 975 | /levn@0.4.1: 976 | resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 977 | engines: {node: '>= 0.8.0'} 978 | dependencies: 979 | prelude-ls: 1.2.1 980 | type-check: 0.4.0 981 | dev: true 982 | 983 | /locate-path@6.0.0: 984 | resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 985 | engines: {node: '>=10'} 986 | dependencies: 987 | p-locate: 5.0.0 988 | dev: true 989 | 990 | /lodash.merge@4.6.2: 991 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 992 | dev: true 993 | 994 | /loupe@3.1.2: 995 | resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} 996 | dev: true 997 | 998 | /magic-string@0.30.12: 999 | resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} 1000 | dependencies: 1001 | '@jridgewell/sourcemap-codec': 1.5.0 1002 | dev: true 1003 | 1004 | /minimatch@3.1.2: 1005 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 1006 | dependencies: 1007 | brace-expansion: 1.1.11 1008 | dev: true 1009 | 1010 | /ms@2.1.3: 1011 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1012 | 1013 | /nanoid@3.3.7: 1014 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 1015 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1016 | hasBin: true 1017 | dev: true 1018 | 1019 | /natural-compare@1.4.0: 1020 | resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 1021 | dev: true 1022 | 1023 | /optionator@0.9.4: 1024 | resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 1025 | engines: {node: '>= 0.8.0'} 1026 | dependencies: 1027 | deep-is: 0.1.4 1028 | fast-levenshtein: 2.0.6 1029 | levn: 0.4.1 1030 | prelude-ls: 1.2.1 1031 | type-check: 0.4.0 1032 | word-wrap: 1.2.5 1033 | dev: true 1034 | 1035 | /p-limit@3.1.0: 1036 | resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 1037 | engines: {node: '>=10'} 1038 | dependencies: 1039 | yocto-queue: 0.1.0 1040 | dev: true 1041 | 1042 | /p-limit@6.1.0: 1043 | resolution: {integrity: sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==} 1044 | engines: {node: '>=18'} 1045 | dependencies: 1046 | yocto-queue: 1.1.1 1047 | dev: false 1048 | 1049 | /p-locate@5.0.0: 1050 | resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 1051 | engines: {node: '>=10'} 1052 | dependencies: 1053 | p-limit: 3.1.0 1054 | dev: true 1055 | 1056 | /parent-module@1.0.1: 1057 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 1058 | engines: {node: '>=6'} 1059 | dependencies: 1060 | callsites: 3.1.0 1061 | dev: true 1062 | 1063 | /path-exists@4.0.0: 1064 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 1065 | engines: {node: '>=8'} 1066 | dev: true 1067 | 1068 | /path-key@3.1.1: 1069 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 1070 | engines: {node: '>=8'} 1071 | dev: true 1072 | 1073 | /pathe@1.1.2: 1074 | resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} 1075 | dev: true 1076 | 1077 | /pathval@2.0.0: 1078 | resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} 1079 | engines: {node: '>= 14.16'} 1080 | dev: true 1081 | 1082 | /picocolors@1.1.1: 1083 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 1084 | dev: true 1085 | 1086 | /postcss@8.4.47: 1087 | resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} 1088 | engines: {node: ^10 || ^12 || >=14} 1089 | dependencies: 1090 | nanoid: 3.3.7 1091 | picocolors: 1.1.1 1092 | source-map-js: 1.2.1 1093 | dev: true 1094 | 1095 | /prelude-ls@1.2.1: 1096 | resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 1097 | engines: {node: '>= 0.8.0'} 1098 | dev: true 1099 | 1100 | /punycode@2.3.1: 1101 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1102 | engines: {node: '>=6'} 1103 | dev: true 1104 | 1105 | /resolve-from@4.0.0: 1106 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 1107 | engines: {node: '>=4'} 1108 | dev: true 1109 | 1110 | /rollup@4.24.0: 1111 | resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==} 1112 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1113 | hasBin: true 1114 | dependencies: 1115 | '@types/estree': 1.0.6 1116 | optionalDependencies: 1117 | '@rollup/rollup-android-arm-eabi': 4.24.0 1118 | '@rollup/rollup-android-arm64': 4.24.0 1119 | '@rollup/rollup-darwin-arm64': 4.24.0 1120 | '@rollup/rollup-darwin-x64': 4.24.0 1121 | '@rollup/rollup-linux-arm-gnueabihf': 4.24.0 1122 | '@rollup/rollup-linux-arm-musleabihf': 4.24.0 1123 | '@rollup/rollup-linux-arm64-gnu': 4.24.0 1124 | '@rollup/rollup-linux-arm64-musl': 4.24.0 1125 | '@rollup/rollup-linux-powerpc64le-gnu': 4.24.0 1126 | '@rollup/rollup-linux-riscv64-gnu': 4.24.0 1127 | '@rollup/rollup-linux-s390x-gnu': 4.24.0 1128 | '@rollup/rollup-linux-x64-gnu': 4.24.0 1129 | '@rollup/rollup-linux-x64-musl': 4.24.0 1130 | '@rollup/rollup-win32-arm64-msvc': 4.24.0 1131 | '@rollup/rollup-win32-ia32-msvc': 4.24.0 1132 | '@rollup/rollup-win32-x64-msvc': 4.24.0 1133 | fsevents: 2.3.3 1134 | dev: true 1135 | 1136 | /semver@7.6.3: 1137 | resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} 1138 | engines: {node: '>=10'} 1139 | hasBin: true 1140 | dev: false 1141 | 1142 | /shebang-command@2.0.0: 1143 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1144 | engines: {node: '>=8'} 1145 | dependencies: 1146 | shebang-regex: 3.0.0 1147 | dev: true 1148 | 1149 | /shebang-regex@3.0.0: 1150 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1151 | engines: {node: '>=8'} 1152 | dev: true 1153 | 1154 | /siginfo@2.0.0: 1155 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 1156 | dev: true 1157 | 1158 | /source-map-js@1.2.1: 1159 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1160 | engines: {node: '>=0.10.0'} 1161 | dev: true 1162 | 1163 | /stackback@0.0.2: 1164 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 1165 | dev: true 1166 | 1167 | /std-env@3.7.0: 1168 | resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} 1169 | dev: true 1170 | 1171 | /strip-json-comments@3.1.1: 1172 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 1173 | engines: {node: '>=8'} 1174 | dev: true 1175 | 1176 | /supports-color@7.2.0: 1177 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1178 | engines: {node: '>=8'} 1179 | dependencies: 1180 | has-flag: 4.0.0 1181 | dev: true 1182 | 1183 | /text-table@0.2.0: 1184 | resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} 1185 | dev: true 1186 | 1187 | /timeout-signal@2.0.0: 1188 | resolution: {integrity: sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==} 1189 | engines: {node: '>=16'} 1190 | dev: false 1191 | 1192 | /tinybench@2.9.0: 1193 | resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 1194 | dev: true 1195 | 1196 | /tinyexec@0.3.1: 1197 | resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} 1198 | dev: true 1199 | 1200 | /tinypool@1.0.1: 1201 | resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} 1202 | engines: {node: ^18.0.0 || >=20.0.0} 1203 | dev: true 1204 | 1205 | /tinyrainbow@1.2.0: 1206 | resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} 1207 | engines: {node: '>=14.0.0'} 1208 | dev: true 1209 | 1210 | /tinyspy@3.0.2: 1211 | resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} 1212 | engines: {node: '>=14.0.0'} 1213 | dev: true 1214 | 1215 | /tldts-core@6.1.52: 1216 | resolution: {integrity: sha512-j4OxQI5rc1Ve/4m/9o2WhWSC4jGc4uVbCINdOEJRAraCi0YqTqgMcxUx7DbmuP0G3PCixoof/RZB0Q5Kh9tagw==} 1217 | dev: false 1218 | 1219 | /tldts@6.1.52: 1220 | resolution: {integrity: sha512-fgrDJXDjbAverY6XnIt0lNfv8A0cf7maTEaZxNykLGsLG7XP+5xhjBTrt/ieAsFjAlZ+G5nmXomLcZDkxXnDzw==} 1221 | hasBin: true 1222 | dependencies: 1223 | tldts-core: 6.1.52 1224 | dev: false 1225 | 1226 | /tough-cookie@5.0.0: 1227 | resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} 1228 | engines: {node: '>=16'} 1229 | dependencies: 1230 | tldts: 6.1.52 1231 | dev: false 1232 | 1233 | /type-check@0.4.0: 1234 | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 1235 | engines: {node: '>= 0.8.0'} 1236 | dependencies: 1237 | prelude-ls: 1.2.1 1238 | dev: true 1239 | 1240 | /uri-js@4.4.1: 1241 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1242 | dependencies: 1243 | punycode: 2.3.1 1244 | dev: true 1245 | 1246 | /vite-node@2.1.3: 1247 | resolution: {integrity: sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==} 1248 | engines: {node: ^18.0.0 || >=20.0.0} 1249 | hasBin: true 1250 | dependencies: 1251 | cac: 6.7.14 1252 | debug: 4.3.7 1253 | pathe: 1.1.2 1254 | vite: 5.4.9 1255 | transitivePeerDependencies: 1256 | - '@types/node' 1257 | - less 1258 | - lightningcss 1259 | - sass 1260 | - sass-embedded 1261 | - stylus 1262 | - sugarss 1263 | - supports-color 1264 | - terser 1265 | dev: true 1266 | 1267 | /vite@5.4.9: 1268 | resolution: {integrity: sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==} 1269 | engines: {node: ^18.0.0 || >=20.0.0} 1270 | hasBin: true 1271 | peerDependencies: 1272 | '@types/node': ^18.0.0 || >=20.0.0 1273 | less: '*' 1274 | lightningcss: ^1.21.0 1275 | sass: '*' 1276 | sass-embedded: '*' 1277 | stylus: '*' 1278 | sugarss: '*' 1279 | terser: ^5.4.0 1280 | peerDependenciesMeta: 1281 | '@types/node': 1282 | optional: true 1283 | less: 1284 | optional: true 1285 | lightningcss: 1286 | optional: true 1287 | sass: 1288 | optional: true 1289 | sass-embedded: 1290 | optional: true 1291 | stylus: 1292 | optional: true 1293 | sugarss: 1294 | optional: true 1295 | terser: 1296 | optional: true 1297 | dependencies: 1298 | esbuild: 0.21.5 1299 | postcss: 8.4.47 1300 | rollup: 4.24.0 1301 | optionalDependencies: 1302 | fsevents: 2.3.3 1303 | dev: true 1304 | 1305 | /vitest@2.1.3: 1306 | resolution: {integrity: sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==} 1307 | engines: {node: ^18.0.0 || >=20.0.0} 1308 | hasBin: true 1309 | peerDependencies: 1310 | '@edge-runtime/vm': '*' 1311 | '@types/node': ^18.0.0 || >=20.0.0 1312 | '@vitest/browser': 2.1.3 1313 | '@vitest/ui': 2.1.3 1314 | happy-dom: '*' 1315 | jsdom: '*' 1316 | peerDependenciesMeta: 1317 | '@edge-runtime/vm': 1318 | optional: true 1319 | '@types/node': 1320 | optional: true 1321 | '@vitest/browser': 1322 | optional: true 1323 | '@vitest/ui': 1324 | optional: true 1325 | happy-dom: 1326 | optional: true 1327 | jsdom: 1328 | optional: true 1329 | dependencies: 1330 | '@vitest/expect': 2.1.3 1331 | '@vitest/mocker': 2.1.3(@vitest/spy@2.1.3)(vite@5.4.9) 1332 | '@vitest/pretty-format': 2.1.3 1333 | '@vitest/runner': 2.1.3 1334 | '@vitest/snapshot': 2.1.3 1335 | '@vitest/spy': 2.1.3 1336 | '@vitest/utils': 2.1.3 1337 | chai: 5.1.1 1338 | debug: 4.3.7 1339 | magic-string: 0.30.12 1340 | pathe: 1.1.2 1341 | std-env: 3.7.0 1342 | tinybench: 2.9.0 1343 | tinyexec: 0.3.1 1344 | tinypool: 1.0.1 1345 | tinyrainbow: 1.2.0 1346 | vite: 5.4.9 1347 | vite-node: 2.1.3 1348 | why-is-node-running: 2.3.0 1349 | transitivePeerDependencies: 1350 | - less 1351 | - lightningcss 1352 | - msw 1353 | - sass 1354 | - sass-embedded 1355 | - stylus 1356 | - sugarss 1357 | - supports-color 1358 | - terser 1359 | dev: true 1360 | 1361 | /which@2.0.2: 1362 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1363 | engines: {node: '>= 8'} 1364 | hasBin: true 1365 | dependencies: 1366 | isexe: 2.0.0 1367 | dev: true 1368 | 1369 | /why-is-node-running@2.3.0: 1370 | resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 1371 | engines: {node: '>=8'} 1372 | hasBin: true 1373 | dependencies: 1374 | siginfo: 2.0.0 1375 | stackback: 0.0.2 1376 | dev: true 1377 | 1378 | /word-wrap@1.2.5: 1379 | resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 1380 | engines: {node: '>=0.10.0'} 1381 | dev: true 1382 | 1383 | /yocto-queue@0.1.0: 1384 | resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 1385 | engines: {node: '>=10'} 1386 | dev: true 1387 | 1388 | /yocto-queue@1.1.1: 1389 | resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} 1390 | engines: {node: '>=12.20'} 1391 | dev: false 1392 | --------------------------------------------------------------------------------