Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@

- ✅ PushPlus 微信推送(国内推荐,无需翻墙)
- ✅ Server酱 微信推送(Turbo 版,扫码获取 SendKey 即用)
- ✅ 企业微信群机器人推送(群机器人 Webhook,无需第三方服务)
- ✅ 微信 ClawBot 推送(腾讯官方 iLink 直连,免费无第三方服务)
- ✅ Discord / Telegram / ntfy 推送
- ✅ 全面日志记录(日志过滤 + 落盘 `logs/` 按天分文件,自动清理 90 天前旧日志)
Expand Down Expand Up @@ -213,7 +214,7 @@ ACCOUNT_1_PASSWORD=your_password
- `RUN_ON_START`:容器启动时是否立即跑一次(默认 `true`)
- `CONFIG_SEARCH_QUERY_ENGINES`:查询源,国内推荐 `china,local`
- `CONFIG_CHINA_API_APPKEY`:gmya.net appkey,解除免费档限流(留空走免费档)
- `CONFIG_PUSHPLUS_*` / `CONFIG_SERVERCHAN_*` / `CONFIG_CLAWBOT_*`:微信推送
- `CONFIG_PUSHPLUS_*` / `CONFIG_SERVERCHAN_*` / `CONFIG_QYWXBOT_*` / `CONFIG_CLAWBOT_*`:微信推送

> 完整的 `CONFIG_*` 环境变量列表见[配置参考](#️-配置参考)各表格。`CONFIG_*` 每次启动都会覆盖 `./config/config.json`。

Expand Down Expand Up @@ -471,10 +472,12 @@ docker compose restart # 重启(不重建)
| `webhook.ntfy.enabled` / `.url` / `.topic` / `.token` 等 | ntfy 推送 | `false` | `CONFIG_NTFY_*` |
| `webhook.pushplus.enabled` / `.token` / `.title` / `.template` / `.channel` | PushPlus 微信推送 | `false` | `CONFIG_PUSHPLUS_*` |
| `webhook.serverchan.enabled` / `.sendKey` / `.title` | Server酱 微信推送(Turbo) | `false` | `CONFIG_SERVERCHAN_ENABLED` / `_SENDKEY` / `_TITLE` |
| `webhook.qywxbot.enabled` / `.sendKey` | 企业微信群机器人推送 | `false` | `CONFIG_QYWXBOT_ENABLED` / `_SENDKEY` |
| `webhook.clawbot.enabled` / `.authFile` | 微信 ClawBot 推送 | `false` | `CONFIG_CLAWBOT_ENABLED` / `_AUTHFILE` |
| `webhook.webhookLogFilter.*` | Webhook 逐条日志过滤(结构同 consoleLogFilter) | `false` | `CONFIG_WEBHOOK_LOG_FILTER_*` |

> 💡 **国内推荐**:**PushPlus** 或 **微信 ClawBot**(均直达微信,无需翻墙)。Discord / Telegram / ntfy 需要能访问对应服务。
> 💡 **国内推荐**:**PushPlus**、**Server酱**、**企业微信群机器人** 或 **微信 ClawBot**(均直达微信,无需翻墙)。Discord / Telegram / ntfy 需要能访问对应服务。
> 企业微信群机器人的 `sendKey` 取群机器人 Webhook 地址中 `?key=` 后面那段(群设置 → 群机器人 → 添加机器人)。
> 运行结束的中文积分摘要会一次性推送(不逐条推日志)。

<details>
Expand Down
3 changes: 3 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ services:
#CONFIG_SERVERCHAN_ENABLED: 'true'
#CONFIG_SERVERCHAN_SENDKEY: 'SCTxxxxxxxxxxxxxxxx'
#CONFIG_SERVERCHAN_TITLE: 'Microsoft-Rewards-Script'
# 企业微信群机器人推送(群设置 → 群机器人 → 添加,取 Webhook 地址中 key= 后的串):
#CONFIG_QYWXBOT_ENABLED: 'true'
#CONFIG_QYWXBOT_SENDKEY: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
# 微信 ClawBot 推送(官方 iLink 直连,免费;需容器内完成一次扫码,
# 建议配合 API_MODE/tty 使用,或本地登录后把 clawbot-auth.json 挂载进容器):
#CONFIG_CLAWBOT_ENABLED: 'true'
Expand Down
4 changes: 4 additions & 0 deletions config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
"sendKey": "",
"title": "Microsoft-Rewards-Script"
},
"qywxbot": {
"enabled": false,
"sendKey": ""
},
"clawbot": {
"enabled": false,
"authFile": ""
Expand Down
28 changes: 28 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { sendNtfy, flushNtfyQueue } from './logging/Ntfy'
import { sendTelegram, flushTelegramQueue } from './logging/Telegram'
import { sendPushPlus, flushPushPlusQueue } from './logging/PushPlus'
import { sendServerChan, flushServerChanQueue } from './logging/ServerChan'
import { sendQywxBot, flushQywxBotQueue } from './logging/QywxBot'
import { sendClawBot, flushClawBotQueue, ensureClawBotReady } from './logging/ClawBot'
import type { DashboardData } from './interface/DashboardData'
import type { AppDashboardData } from './interface/AppDashBoardData'
Expand Down Expand Up @@ -79,6 +80,7 @@ async function flushAllWebhooks(timeoutMs = 5000): Promise<void> {
flushTelegramQueue(timeoutMs),
flushPushPlusQueue(timeoutMs),
flushServerChanQueue(timeoutMs),
flushQywxBotQueue(timeoutMs),
flushClawBotQueue(timeoutMs)
])
closeSessionStore()
Expand Down Expand Up @@ -391,6 +393,7 @@ export class MicrosoftRewardsBot {

await this.sendPushPlusSummary(allAccountStats, runStartTime, hadWorkerFailure)
await this.sendServerChanSummary(allAccountStats, runStartTime, hadWorkerFailure)
await this.sendQywxBotPushSummary(allAccountStats, runStartTime, hadWorkerFailure)
await this.sendClawBotSummary(allAccountStats, runStartTime, hadWorkerFailure)
await flushAllWebhooks()

Expand Down Expand Up @@ -501,6 +504,30 @@ export class MicrosoftRewardsBot {
await sendServerChan(serverchan, content)
}

private async sendQywxBotPushSummary(
accountStats: AccountStats[],
runStartTime: number,
hadWorkerFailure: boolean
): Promise<void> {
const qywxbot = this.config?.webhook?.qywxbot
if (!qywxbot?.enabled || !qywxbot.sendKey) {
return
}

const content = this.buildSummaryMessage(accountStats, runStartTime, hadWorkerFailure)
const result = await sendQywxBot(qywxbot, content)

if (result.skipped) {
return
}

if (result.ok) {
this.logger.info('main', 'QYWXBOT', '企业微信机器人推送成功', 'green')
} else {
this.logger.warn('main', 'QYWXBOT', `企业微信机器人推送失败 | ${result.message ?? '未知错误'}`)
}
}

private async sendClawBotSummary(
accountStats: AccountStats[],
runStartTime: number,
Expand Down Expand Up @@ -645,6 +672,7 @@ export class MicrosoftRewardsBot {
const hadFailure = accountStats.some(s => !s.success)
await this.sendPushPlusSummary(accountStats, runStartTime, hadFailure)
await this.sendServerChanSummary(accountStats, runStartTime, hadFailure)
await this.sendQywxBotPushSummary(accountStats, runStartTime, hadFailure)
await this.sendClawBotSummary(accountStats, runStartTime, hadFailure)
await flushAllWebhooks()

Expand Down
6 changes: 6 additions & 0 deletions src/interface/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export interface ConfigWebhook {
telegram?: WebhookTelegramConfig
pushplus?: WebhookPushPlusConfig
serverchan?: WebhookServerChanConfig
qywxbot?: WebhookQywxBotConfig
clawbot?: WebhookClawBotConfig
webhookLogFilter: LogFilter
}
Expand Down Expand Up @@ -158,6 +159,11 @@ export interface WebhookServerChanConfig {
title?: string
}

export interface WebhookQywxBotConfig {
enabled?: boolean
sendKey: string
}

export interface WebhookClawBotConfig {
enabled?: boolean
authFile?: string
Expand Down
64 changes: 64 additions & 0 deletions src/logging/QywxBot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { httpRequest } from '../util/Http'
import type { HttpRequestConfig } from '../util/Http'
import PQueue from 'p-queue'
import type { WebhookQywxBotConfig } from '../interface/Config'
import { flushQueue } from './Queue'

const qywxBotQueue = new PQueue({
interval: 1000,
intervalCap: 2,
carryoverConcurrencyCount: true
})

const QYWX_BOT_API = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send'
const TITLE = 'Microsoft-Rewards-Script'

export interface QywxBotSendResult {
ok: boolean
/** 未启用或缺少 sendKey,调用方无需记录日志 */
skipped?: boolean
message?: string
}

export async function sendQywxBot(config: WebhookQywxBotConfig, content: string): Promise<QywxBotSendResult> {
if (!config?.enabled || !config?.sendKey) {
return { ok: false, skipped: true, message: '未启用或 sendKey 为空' }
}

const request: HttpRequestConfig = {
method: 'POST',
url: `${QYWX_BOT_API}?key=${config.sendKey}`,
headers: { 'Content-Type': 'application/json;charset=utf-8' },
data: {
msgtype: 'text',
text: {
content: `${TITLE}\n\n${content}`
}
},
timeout: 15000
}

return qywxBotQueue.add(async () => {
try {
const response = await httpRequest<{ errcode?: number; errmsg?: string }>(request)
const { errcode, errmsg } = response?.data ?? {}

if (errcode === undefined || errcode === 0) {
return { ok: true }
}

return { ok: false, message: `errcode=${errcode}${errmsg ? ` errmsg=${errmsg}` : ''}` }
} catch (err) {
const status = (err as { response?: { status?: number } })?.response?.status
if (status === 429) {
return { ok: false, message: '触发接口频率限制(429),本次跳过' }
}

return { ok: false, message: err instanceof Error ? err.message : String(err) }
}
})
}

export function flushQywxBotQueue(timeoutMs = 5000): Promise<void> {
return flushQueue(qywxBotQueue, timeoutMs)
}
4 changes: 4 additions & 0 deletions src/util/ConfigEnvOverrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export const ENV_OVERRIDES: EnvOverrideEntry[] = [
{ env: 'CONFIG_SERVERCHAN_SENDKEY', path: 'webhook.serverchan.sendKey', type: 'string' },
{ env: 'CONFIG_SERVERCHAN_TITLE', path: 'webhook.serverchan.title', type: 'string' },

// 企业微信群机器人 webhook(国内微信推送,sendKey 为群机器人 Webhook 地址中的 key 参数)
{ env: 'CONFIG_QYWXBOT_ENABLED', path: 'webhook.qywxbot.enabled', type: 'bool' },
{ env: 'CONFIG_QYWXBOT_SENDKEY', path: 'webhook.qywxbot.sendKey', type: 'string' },

// Humanize 拟人化运行策略(quietHours 为数组结构,请直接编辑 config.json)
{ env: 'CONFIG_HUMANIZE_ENABLED', path: 'humanize.enabled', type: 'bool' },
{ env: 'CONFIG_HUMANIZE_SKIP_WHEN_COMPLETED', path: 'humanize.skipWhenCompletedToday', type: 'bool' },
Expand Down
13 changes: 13 additions & 0 deletions src/util/Validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,19 @@ const WebhookSchema = z.object({
channel: z.string().optional()
})
.optional(),
serverchan: z
.object({
enabled: z.boolean().optional(),
sendKey: z.string(),
title: z.string().optional()
})
.optional(),
qywxbot: z
.object({
enabled: z.boolean().optional(),
sendKey: z.string()
})
.optional(),
clawbot: z
.object({
enabled: z.boolean().optional(),
Expand Down