Skip to content
Merged
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
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SubsTracker — 订阅管理与提醒系统

基于 **Cloudflare Workers + KV** 的轻量级订阅到期提醒。在网页里管理订阅,到点通过 Telegram / Bark / 企业微信 / ntfy 等 **10 种渠道** 推送,并自带发送与调度日志方便排查。
基于 **Cloudflare Workers + KV** 的轻量级订阅到期提醒。在网页里管理订阅,到点通过 Telegram / Bark / 企业微信 / ntfy 等 **11 种渠道** 推送,并自带发送与调度日志方便排查。

**适合**:个人自托管、域名/会员/账单到期提醒。
**不适合**:多用户协作、复杂企业审批流。
Expand Down Expand Up @@ -129,6 +129,7 @@ Cloudflare Dashboard → **Workers & Pages → KV** → 打开 `SUBSCRIPTIONS_KV
| Bark | Device Key;自建可填 Server |
| 企业微信 | 群机器人 Webhook |
| ntfy | Server(默认 ntfy.sh)+ Topic;可选 Token |
| WPUSH | API Key;可选 Channel / Topic 编码 |
| 邮件 | Resend API Key + 收发邮箱 |
| Webhook | 任意 HTTP 地址 + 可选模板 |

Expand Down Expand Up @@ -227,7 +228,7 @@ Cloudflare Dashboard → **Workers & Pages → KV** → 打开 `SUBSCRIPTIONS_KV

### 通知渠道(10)

Telegram · NotifyX · Webhook · 企业微信 · Resend 邮件 · Bark · Gotify · Server酱 · PushPlus · **ntfy**
Telegram · NotifyX · Webhook · 企业微信 · Resend 邮件 · Bark · Gotify · Server酱 · PushPlus · ntfy · **WPUSH**

### 可观测

Expand Down Expand Up @@ -296,15 +297,19 @@ Telegram · NotifyX · Webhook · 企业微信 · Resend 邮件 · Bark · Gotif

启用 ntfy → Server 默认 `https://ntfy.sh` → 填自己的 Topic → 手机 ntfy App 订阅同一 Topic → 点测试。

### 10. 控制台里 `beacon.min.js` / cloudflareinsights 报错?
### 10. WPUSH 怎么配?

启用 WPUSH → 在 [设置页](https://wpush.cn/settings) 获取 API Key → 可选填写渠道 / Topic 编码 → 点测试。成功条件为接口返回 `code === 0`。

### 11. 控制台里 `beacon.min.js` / cloudflareinsights 报错?

那是 **Cloudflare 统计脚本**,不是本项目业务代码。一般可忽略,与订阅列表无关。

### 11. Authentication error [code: 10000](部署时)
### 12. Authentication error [code: 10000](部署时)

Token 权限不足或 Wrangler 缓存问题:检查 API Token 权限,必要时删 `.wrangler/` 后重试。

### 12. 第三方系统想调通知接口?
### 13. 第三方系统想调通知接口?

在系统配置生成 **第三方 API 令牌** 后:

Expand Down
7 changes: 6 additions & 1 deletion src/api/handlers/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const SECRET_FIELDS = [
'GOTIFY_APP_TOKEN',
'SERVERCHAN_SENDKEY',
'PUSHPLUS_TOKEN',
'NTFY_TOKEN'
'NTFY_TOKEN',
'WPUSH_APIKEY'
];

function isConfiguredSecret(value) {
Expand Down Expand Up @@ -122,6 +123,10 @@ async function handleUpdateConfig(request, env) {
NTFY_TOPIC: (newConfig.NTFY_TOPIC || '').trim(),
NTFY_TOKEN: mergeSecretField(config, newConfig, 'NTFY_TOKEN', clearSecretFields),

WPUSH_APIKEY: mergeSecretField(config, newConfig, 'WPUSH_APIKEY', clearSecretFields),
WPUSH_CHANNEL: (newConfig.WPUSH_CHANNEL || '').trim(),
WPUSH_TOPIC_CODE: (newConfig.WPUSH_TOPIC_CODE || '').trim(),

ENABLED_NOTIFIERS: newConfig.ENABLED_NOTIFIERS || ['notifyx'],
TIMEZONE: newConfig.TIMEZONE || config.TIMEZONE || 'UTC',

Expand Down
16 changes: 15 additions & 1 deletion src/api/handlers/test-notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { sendGotifyNotification } from '../../services/notify/gotify.js';
import { sendServerChanNotification } from '../../services/notify/serverchan.js';
import { sendPushPlusNotification } from '../../services/notify/pushplus.js';
import { sendNtfyNotification } from '../../services/notify/ntfy.js';
import { sendWPushNotification } from '../../services/notify/wpush.js';

async function handleTestNotification(request, env) {
try {
Expand All @@ -19,7 +20,7 @@ async function handleTestNotification(request, env) {
let message = '';

const type = typeof body.type === 'string' ? body.type.trim() : '';
const supportedTypes = ['telegram', 'notifyx', 'webhook', 'wechatbot', 'email', 'bark', 'gotify', 'serverchan', 'pushplus', 'ntfy'];
const supportedTypes = ['telegram', 'notifyx', 'webhook', 'wechatbot', 'email', 'bark', 'gotify', 'serverchan', 'pushplus', 'ntfy', 'wpush'];

if (!type) {
return new Response(
Expand Down Expand Up @@ -180,6 +181,19 @@ async function handleTestNotification(request, env) {
const content = '这是一条测试通知,用于验证 ntfy 通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime();
success = await sendNtfyNotification(title, content, testConfig);
message = success ? 'ntfy 通知发送成功' : 'ntfy 通知发送失败,请检查配置';
} else if (type === 'wpush') {
const testConfig = {
...config,
WPUSH_APIKEY: (typeof body.WPUSH_APIKEY === 'string' && body.WPUSH_APIKEY.trim().length > 0)
? body.WPUSH_APIKEY.trim()
: config.WPUSH_APIKEY,
WPUSH_CHANNEL: body.WPUSH_CHANNEL || config.WPUSH_CHANNEL,
WPUSH_TOPIC_CODE: body.WPUSH_TOPIC_CODE || config.WPUSH_TOPIC_CODE
};
const title = '测试通知';
const content = '这是一条测试通知,用于验证 WPUSH 通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime();
success = await sendWPushNotification(title, content, testConfig);
message = success ? 'WPUSH 通知发送成功' : 'WPUSH 通知发送失败,请检查配置';
}

return new Response(
Expand Down
5 changes: 4 additions & 1 deletion src/data/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ const DEFAULT_CONFIG = {
PUSHPLUS_CHANNEL: '',
NTFY_SERVER: 'https://ntfy.sh',
NTFY_TOPIC: '',
NTFY_TOKEN: ''
NTFY_TOKEN: '',
WPUSH_APIKEY: '',
WPUSH_CHANNEL: '',
WPUSH_TOPIC_CODE: ''
};

async function getConfig(env) {
Expand Down
4 changes: 3 additions & 1 deletion src/services/notify/dispatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { gotifyChannel } from './gotify.js';
import { serverChanChannel } from './serverchan.js';
import { pushplusChannel } from './pushplus.js';
import { ntfyChannel } from './ntfy.js';
import { wpushChannel } from './wpush.js';
import { writeLog } from '../../data/notification-logs.repo.js';

/** 名字到渠道实例的映射;新增渠道在此注册即可 */
Expand All @@ -32,7 +33,8 @@ export const ALL_CHANNELS = {
gotify: gotifyChannel,
serverchan: serverChanChannel,
pushplus: pushplusChannel,
ntfy: ntfyChannel
ntfy: ntfyChannel,
wpush: wpushChannel
};

/**
Expand Down
77 changes: 77 additions & 0 deletions src/services/notify/wpush.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// @ts-check
/**
* WPUSH 通知渠道
*
* 文档:https://wpush.cn/docs
* 发送:POST https://api.wpush.cn/api/v1/send
* 成功条件:响应 JSON 的 code === 0(注意:与 PushPlus 的 200 不同)
*
* 配置:
* - WPUSH_APIKEY API Key(必填,勿写入日志)
* - WPUSH_CHANNEL 可选推送渠道(wechat/app/sms/mail/webhook/dingtalk/feishu/wechat_work/clawbot/qqbot)
* - WPUSH_TOPIC_CODE 可选 Topic 广播编码
*/
import { ok, fail, errorMessage } from './channel.js';

/** @type {import('./channel.js').Channel} */
export const wpushChannel = {
name: 'wpush',

validateConfig(config) {
if (!config.WPUSH_APIKEY || !String(config.WPUSH_APIKEY).trim()) {
return { ok: false, error: '缺少 WPUSH_APIKEY' };
}
return { ok: true };
},

async send(payload, config) {
const v = wpushChannel.validateConfig(config);
if (!v.ok) return fail('wpush', v.error || '配置无效');

/** @type {Record<string, any>} */
const body = {
apikey: String(config.WPUSH_APIKEY).trim(),
title: payload.title || '订阅提醒',
content: payload.content || payload.title || '订阅提醒'
};
if (config.WPUSH_CHANNEL && String(config.WPUSH_CHANNEL).trim()) {
body.channel = String(config.WPUSH_CHANNEL).trim();
}
if (config.WPUSH_TOPIC_CODE && String(config.WPUSH_TOPIC_CODE).trim()) {
body.topic_code = String(config.WPUSH_TOPIC_CODE).trim();
}

try {
const r = await fetch('https://api.wpush.cn/api/v1/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const result = await r.json().catch(() => ({}));
// 切勿把 apikey 打进日志;失败信息只用 code / message
return result && result.code === 0
? ok('wpush', result)
: fail(
'wpush',
`WPUSH 返回 code=${result?.code} ${result?.message || result?.msg || ''}`,
result
);
} catch (err) {
return fail('wpush', errorMessage(err));
}
},

async test(config) {
return wpushChannel.send(
{ title: '订阅管理 - 测试通知', content: '这是一条 WPUSH 测试通知。' },
config
);
}
};

/** @deprecated 旧版兼容函数 */
export async function sendWPushNotification(title, content, config) {
const r = await wpushChannel.send({ title, content }, config);
if (!r.success) console.error('[WPUSH]', r.error);
return r.success;
}
Loading