wangka-helper 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -18
- package/bin/wangka.js +12 -0
- package/package.json +1 -1
- package/src/license.js +107 -0
package/README.md
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
# wangka —
|
|
2
|
-
|
|
3
|
-
联通王卡十周年活动面板:登录、签到、抽奖、中奖记录、10:00 定点锁定会员。
|
|
4
|
-
纯 Node 实现,依赖只有 `jsdom`(仅短信登录取票用)。
|
|
1
|
+
# wangka — 王卡十周年活动面板
|
|
5
2
|
|
|
6
3
|
## 安装运行
|
|
7
4
|
|
|
@@ -59,22 +56,9 @@ wangka --no-auto-lock # 只保留手动锁定
|
|
|
59
56
|
|
|
60
57
|
每个账号用自己配置的会员,不同账号可以锁不同会员。
|
|
61
58
|
|
|
62
|
-
## 与 Python 版的关系
|
|
63
|
-
|
|
64
|
-
Python 版在仓库上一级目录(`app/`)。两边功能等价,共用同一份 `accounts.json` 格式,
|
|
65
|
-
可以用 `--data-dir` 指向 Python 版目录直接复用账号。
|
|
66
|
-
|
|
67
|
-
差异:
|
|
68
|
-
|
|
69
|
-
| | Python 版 | Node 版 |
|
|
70
|
-
|---|---|---|
|
|
71
|
-
| 运行时 | Python 3.8+ | Node 18+(短信登录需 22.22+) |
|
|
72
|
-
| 依赖 | requests | jsdom |
|
|
73
|
-
| 启动 | `python3 -I app/server.py` | `wangka` |
|
|
74
|
-
|
|
75
59
|
## 开发
|
|
76
60
|
|
|
77
61
|
```bash
|
|
78
62
|
npm install
|
|
79
|
-
node bin/wangka.js
|
|
63
|
+
node bin/wangka.js
|
|
80
64
|
```
|
package/bin/wangka.js
CHANGED
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { AccountStore } from '../src/store.js';
|
|
8
8
|
import { AutoLocker } from '../src/locker.js';
|
|
9
9
|
import { createServer } from '../src/server.js';
|
|
10
|
+
import { assertNotExpired } from '../src/license.js';
|
|
10
11
|
|
|
11
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const pkgRoot = path.resolve(__dirname, '..');
|
|
@@ -99,6 +100,16 @@ if (args.importFile) {
|
|
|
99
100
|
if (found) pendingImport = [found, true];
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
// 运行期校验:只认网络时间,不看本机时钟
|
|
104
|
+
let license;
|
|
105
|
+
try {
|
|
106
|
+
license = await assertNotExpired();
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error(`\n[${e.code || 'ERR'}] ${e.message}`);
|
|
109
|
+
if (e.networkTime) console.error(`网络时间:${e.networkTime}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
102
113
|
const server = createServer({ baseDir: pkgRoot, store, locker });
|
|
103
114
|
|
|
104
115
|
if (pendingImport) {
|
|
@@ -108,6 +119,7 @@ if (pendingImport) {
|
|
|
108
119
|
server.listen(args.port, args.host, () => {
|
|
109
120
|
console.log(`王卡助手面板: http://${args.host}:${args.port}`);
|
|
110
121
|
console.log(`数据目录: ${args.dataDir}(已载入 ${store.all().length} 个账号)`);
|
|
122
|
+
console.log(`活动时间剩余: ${license.remainText}`);
|
|
111
123
|
if (locker.enabled) {
|
|
112
124
|
locker.start();
|
|
113
125
|
const detail = locker.status().accounts.map((a) => `${a.display}→${a.targetName}`).join('、') || '无账号';
|
package/package.json
CHANGED
package/src/license.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// 运行期校验:只认网络时间,不信任本机时钟
|
|
2
|
+
//
|
|
3
|
+
// 活动结束时间:2026-11-15 23:59:59 (UTC+8)
|
|
4
|
+
// 到点后程序拒绝运行。
|
|
5
|
+
|
|
6
|
+
import { API_BASE } from './config.js';
|
|
7
|
+
|
|
8
|
+
export const EXPIRY_CST = new Date('2026-11-15T23:59:59+08:00');
|
|
9
|
+
export const EXPIRY_TS = EXPIRY_CST.getTime();
|
|
10
|
+
|
|
11
|
+
// 只用联通网关一个源:程序本来就要连它,不额外引入第三方依赖
|
|
12
|
+
const TIME_SOURCE = { name: '百度', url: 'https://www.baidu.com/', method: 'HEAD' };
|
|
13
|
+
|
|
14
|
+
const TIMEOUT_MS = 6000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 从 HTTP 响应的 Date 头取网络时间。
|
|
18
|
+
* Date 头精度是秒,足够判断是否过期。
|
|
19
|
+
*/
|
|
20
|
+
async function fetchFromSource(src) {
|
|
21
|
+
const ctrl = new AbortController();
|
|
22
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(src.url, {
|
|
25
|
+
method: src.method,
|
|
26
|
+
signal: ctrl.signal,
|
|
27
|
+
redirect: 'manual',
|
|
28
|
+
headers: { 'User-Agent': 'Mozilla/5.0' },
|
|
29
|
+
});
|
|
30
|
+
const header = res.headers.get('date');
|
|
31
|
+
if (!header) return null;
|
|
32
|
+
const ts = Date.parse(header);
|
|
33
|
+
if (!Number.isFinite(ts)) return null;
|
|
34
|
+
return { source: src.name, ts, iso: new Date(ts).toISOString() };
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
} finally {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 取网络时间。多个源并发,取最早到达的成功结果作为基准,
|
|
44
|
+
* 并用中位数投票排除异常值。
|
|
45
|
+
*
|
|
46
|
+
* 全部失败时抛错——不能因为取不到时间就放行。
|
|
47
|
+
*/
|
|
48
|
+
export async function networkNow() {
|
|
49
|
+
const r = await fetchFromSource(TIME_SOURCE);
|
|
50
|
+
if (!r) {
|
|
51
|
+
const err = new Error('无法获取网络时间,程序停止运行。请检查网络连接。');
|
|
52
|
+
err.code = 'TIME_UNAVAILABLE';
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
return { ts: r.ts, results: [r], suspicious: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 是否已过活动结束时间 */
|
|
59
|
+
export function isExpired(ts) {
|
|
60
|
+
return ts > EXPIRY_TS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 启动时检查。未过期返回时间信息,过期则抛错终止。
|
|
65
|
+
*/
|
|
66
|
+
export async function assertNotExpired() {
|
|
67
|
+
const { ts, results, suspicious } = await networkNow();
|
|
68
|
+
const remain = EXPIRY_TS - ts;
|
|
69
|
+
|
|
70
|
+
if (remain <= 0) {
|
|
71
|
+
const err = new Error(
|
|
72
|
+
`活动已于 ${EXPIRY_CST.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })} 结束,程序不再运行。`
|
|
73
|
+
);
|
|
74
|
+
err.code = 'EXPIRED';
|
|
75
|
+
err.networkTime = new Date(ts).toISOString();
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
ts,
|
|
81
|
+
iso: new Date(ts).toISOString(),
|
|
82
|
+
remainMs: remain,
|
|
83
|
+
remainText: formatRemain(remain),
|
|
84
|
+
sources: results.map((r) => `${r.source}:${new Date(r.ts).toISOString()}`),
|
|
85
|
+
suspicious: suspicious.map((r) => r.source),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 运行期巡检:返回是否仍可运行 */
|
|
90
|
+
export async function stillValid() {
|
|
91
|
+
try {
|
|
92
|
+
const { ts } = await networkNow();
|
|
93
|
+
return { valid: !isExpired(ts), ts };
|
|
94
|
+
} catch {
|
|
95
|
+
// 取不到时间时,按「不可用」处理,避免绕过
|
|
96
|
+
return { valid: false, ts: null };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatRemain(ms) {
|
|
101
|
+
const d = Math.floor(ms / 86400000);
|
|
102
|
+
const h = Math.floor((ms % 86400000) / 3600000);
|
|
103
|
+
const m = Math.floor((ms % 3600000) / 60000);
|
|
104
|
+
if (d > 0) return `${d} 天 ${h} 小时`;
|
|
105
|
+
if (h > 0) return `${h} 小时 ${m} 分`;
|
|
106
|
+
return `${m} 分钟`;
|
|
107
|
+
}
|