lol-ban-cli 0.1.0

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 ADDED
@@ -0,0 +1,102 @@
1
+ # lol-ban-cli
2
+
3
+ LOL(英雄联盟)封号查询 CLI 工具。在终端输入 `lol` 即可查询账号封禁状态。
4
+
5
+ ## 功能
6
+
7
+ - 交互模式:输入 QQ 号回车查询,连续查多个账号
8
+ - 单次模式:`lol 2234310998` 直接出结果后退出
9
+ - 详细模式:`-v` 打印 token、原始 JSON、耗时
10
+ - 自动嗅探 token:每次启动从源站抓取最新 token,失效自动重试,全部失败用预设兜底
11
+ - token 本地缓存于 `~/.lol-ban-cli/token.json`,24 小时过期
12
+
13
+ ## 前提条件
14
+
15
+ - **Node.js >= 18**(自带原生 `fetch`)
16
+ - **关闭 Clash / V2Ray 等代理软件**——否则会被 fake-ip 拦截连不上 API
17
+
18
+ ## 安装方式
19
+
20
+ ### 方式 A:本机开发链接(在项目目录内)
21
+
22
+ ```bash
23
+ npm link
24
+ ```
25
+
26
+ 完成后任意终端输入 `lol` 即可。卸载:`npm unlink -g lol-ban-cli`
27
+
28
+ ### 方式 B:打 tarball 拷到别的电脑
29
+
30
+ 在本机:
31
+
32
+ ```bash
33
+ npm pack
34
+ # 产出 lol-ban-cli-0.1.0.tgz
35
+ ```
36
+
37
+ 把 `.tgz` 拷到其他电脑,在那台电脑上:
38
+
39
+ ```bash
40
+ npm install -g ./lol-ban-cli-0.1.0.tgz
41
+ ```
42
+
43
+ ### 方式 C:发布到 npm 公共仓库(任何人可装)
44
+
45
+ ```bash
46
+ npm login # 没账号去 npmjs.com 注册
47
+ npm publish --access public
48
+ ```
49
+
50
+ 之后任何人都能:
51
+
52
+ ```bash
53
+ npm install -g lol-ban-cli
54
+ ```
55
+
56
+ > ⚠️ `lol-ban-cli` 这个包名 npm 上可能被占。`npm publish` 前先 `npm view lol-ban-cli`,如显示 `404` 表示可用;如被占,改 `package.json` 里 `name` 字段,例如 `@your-name/lol-ban-cli`(scope 包基本不会冲突)。
57
+
58
+ ## 用法
59
+
60
+ ```bash
61
+ lol # 交互模式
62
+ lol 2234310998 # 单次查询
63
+ lol -v 2234310998 # 详细模式(带 token 与原始 JSON)
64
+ lol -h # 帮助
65
+ ```
66
+
67
+ 交互模式内命令:
68
+
69
+ | 命令 | 作用 |
70
+ |------|------|
71
+ | `<QQ号>` | 查询 |
72
+ | `q` / `quit` / `exit` | 退出 |
73
+ | `-v` | 切换详细模式 |
74
+ | `-h` | 显示帮助 |
75
+
76
+ ## 数据来源
77
+
78
+ 接口:`yun.4png.com/api/query.html`,由 `vba.hzmxm.top` 间接付费维持。token 自动从 `vba.hzmxm.top/user/ban/check` 抓取,失败兜底使用预设值。
79
+
80
+ **接口不稳定,会失效。** 失效时可手动改 `src/config.js` 里的 `FALLBACK_TOKEN`。
81
+
82
+ ## 目录结构
83
+
84
+ ```
85
+ .
86
+ ├── bin/
87
+ │ └── lol.js # 入口,处理 argv,分发到 REPL 或单次查询
88
+ ├── src/
89
+ │ ├── config.js # 常量(URL、token、UA、缓存)
90
+ │ ├── colors.js # ANSI 颜色(无紫色,NO_COLOR/非 TTY 自动剥离)
91
+ │ ├── token.js # 自动嗅探 + 缓存 + 兜底
92
+ │ ├── api.js # 4png 接口调用 + 失败 token 重试
93
+ │ ├── format.js # 解析 banmsg + 提取原因 + 渲染输出
94
+ │ └── repl.js # 交互模式
95
+ ├── package.json
96
+ ├── README.md
97
+ └── .gitignore
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT
package/bin/lol.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const { startRepl, handleQQ, showHelp } = require('../src/repl');
6
+
7
+ function getVersion() {
8
+ try {
9
+ return require(path.join(__dirname, '..', 'package.json')).version || '0.0.0';
10
+ } catch {
11
+ return '0.0.0';
12
+ }
13
+ }
14
+
15
+ (async function main() {
16
+ const args = process.argv.slice(2);
17
+ const state = { verbose: false };
18
+
19
+ if (args.includes('-h') || args.includes('--help')) {
20
+ showHelp();
21
+ process.exit(0);
22
+ }
23
+ if (args.includes('-v') || args.includes('--verbose')) {
24
+ state.verbose = true;
25
+ }
26
+
27
+ const qq = args.find(a => /^\d{5,12}$/.test(a));
28
+ if (qq) {
29
+ await handleQQ(qq, state);
30
+ process.exit(0);
31
+ }
32
+
33
+ startRepl(getVersion(), state);
34
+ })();
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "lol-ban-cli",
3
+ "version": "0.1.0",
4
+ "description": "LOL 封号查询 CLI - 终端输入 lol 即可查询英雄联盟账号封禁状态",
5
+ "bin": {
6
+ "lol": "bin/lol.js"
7
+ },
8
+ "main": "src/api.js",
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "keywords": [
18
+ "lol",
19
+ "league-of-legends",
20
+ "ban",
21
+ "封号",
22
+ "cli",
23
+ "qq"
24
+ ],
25
+ "license": "MIT"
26
+ }
package/src/api.js ADDED
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const { API_BASE, REFERER, UA, FALLBACK_TOKEN } = require('./config');
4
+ const { resolveToken, sniffToken, saveCache } = require('./token');
5
+ const c = require('./colors');
6
+
7
+ async function callApi(qq, token) {
8
+ const url = `${API_BASE}?token=${token}&qq=${encodeURIComponent(qq)}&_=${Date.now()}`;
9
+ const resp = await fetch(url, {
10
+ headers: { Referer: REFERER, 'User-Agent': UA, Accept: '*/*' },
11
+ });
12
+ const text = await resp.text();
13
+ try {
14
+ return JSON.parse(text);
15
+ } catch {
16
+ throw new Error(`非 JSON 响应 (HTTP ${resp.status}): ${text.slice(0, 200)}`);
17
+ }
18
+ }
19
+
20
+ async function queryQQ(qq, verbose) {
21
+ let token = await resolveToken(verbose);
22
+ let j = await callApi(qq, token);
23
+
24
+ if (!j || j.code !== 200) {
25
+ if (verbose) console.error(`${c.dim}[首次失败 code=${j && j.code},刷新 token 重试]${c.reset}`);
26
+ const fresh = await sniffToken(verbose);
27
+ if (fresh && fresh !== token) {
28
+ saveCache(fresh);
29
+ j = await callApi(qq, fresh);
30
+ token = fresh;
31
+ } else if (token !== FALLBACK_TOKEN) {
32
+ j = await callApi(qq, FALLBACK_TOKEN);
33
+ token = FALLBACK_TOKEN;
34
+ }
35
+ }
36
+ return { json: j, tokenUsed: token };
37
+ }
38
+
39
+ module.exports = { callApi, queryQQ };
package/src/colors.js ADDED
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
4
+
5
+ const palette = {
6
+ reset: '\x1b[0m',
7
+ bold: '\x1b[1m',
8
+ dim: '\x1b[2m',
9
+ red: '\x1b[31m',
10
+ green: '\x1b[32m',
11
+ yellow: '\x1b[33m',
12
+ blue: '\x1b[34m',
13
+ cyan: '\x1b[36m',
14
+ gray: '\x1b[90m',
15
+ };
16
+
17
+ module.exports = useColor ? palette : new Proxy({}, { get: () => '' });
package/src/config.js ADDED
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ module.exports = {
7
+ PRIMARY_PAGE: 'https://vba.hzmxm.top/user/ban/check',
8
+ REFERER: 'https://vba.hzmxm.top/',
9
+ API_BASE: 'https://yun.4png.com/api/query.html',
10
+ FALLBACK_TOKEN: '22fd19520d851dea',
11
+ UA: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36',
12
+
13
+ CACHE_DIR: path.join(os.homedir(), '.lol-ban-cli'),
14
+ CACHE_FILE: path.join(os.homedir(), '.lol-ban-cli', 'token.json'),
15
+ CACHE_TTL: 24 * 60 * 60 * 1000,
16
+ };
package/src/format.js ADDED
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const c = require('./colors');
4
+
5
+ function parseBanMsg(banmsg) {
6
+ const r = { deadline: null, remaining: null, reason: null };
7
+ if (!banmsg) return r;
8
+
9
+ const m1 = banmsg.match(/封禁至\s*([\d\- :]+?)(?:[,,。]|$)/);
10
+ if (m1) r.deadline = m1[1].trim();
11
+
12
+ const m2 = banmsg.match(/距离解封还有\s*([^,,。]+)/);
13
+ if (m2) r.remaining = m2[1].trim();
14
+
15
+ const reasonPatterns = [
16
+ /(?:封号|封禁|处罚)原因[::]\s*([^,,。\n]+)/,
17
+ /原因[::]\s*([^,,。\n]+)/,
18
+ ];
19
+ for (const p of reasonPatterns) {
20
+ const m = banmsg.match(p);
21
+ if (m) { r.reason = m[1].trim(); break; }
22
+ }
23
+ return r;
24
+ }
25
+
26
+ function extractReason(data) {
27
+ const fields = ['reason', 'ban_reason', 'punish_reason', 'cause', 'type', 'category', 'punish_type'];
28
+ for (const k of fields) {
29
+ const v = data[k];
30
+ if (typeof v === 'string' && v.trim()) return v.trim();
31
+ }
32
+ return parseBanMsg(data.banmsg || '').reason;
33
+ }
34
+
35
+ function divider(char = '═', len = 36) {
36
+ return char.repeat(len);
37
+ }
38
+
39
+ function printResult(qq, j, tokenUsed, verbose) {
40
+ console.log();
41
+ if (!j || j.code !== 200 || !j.data) {
42
+ console.log(`${c.red}查询失败:接口返回异常${c.reset}`);
43
+ console.log(`${c.gray}原始响应:${JSON.stringify(j)}${c.reset}\n`);
44
+ return;
45
+ }
46
+
47
+ const d = j.data;
48
+ const status = (d.return || '').trim();
49
+
50
+ console.log(`${c.bold}${c.cyan}${divider()}${c.reset}`);
51
+ console.log(`${c.bold}${c.cyan} 查询结果${c.reset}`);
52
+ console.log(`${c.bold}${c.cyan}${divider()}${c.reset}`);
53
+ console.log(` ${c.gray}QQ 号 :${c.reset} ${c.cyan}${qq}${c.reset}`);
54
+
55
+ if (status === '封禁') {
56
+ const parsed = parseBanMsg(d.banmsg || '');
57
+ const reason = extractReason(d);
58
+
59
+ console.log(` ${c.gray}账号状态 :${c.reset} ${c.red}${c.bold}✖ 已封禁${c.reset}`);
60
+ console.log(` ${c.gray}解封时间 :${c.reset} ${c.yellow}${parsed.deadline || '—'}${c.reset}`);
61
+ console.log(` ${c.gray}剩余时间 :${c.reset} ${c.yellow}${parsed.remaining || d.rammsg || '—'}${c.reset}`);
62
+ if (reason) {
63
+ console.log(` ${c.gray}封号原因 :${c.reset} ${c.red}${reason}${c.reset}`);
64
+ } else {
65
+ console.log(` ${c.gray}封号原因 :${c.reset} ${c.dim}(接口未单独返回原因字段,详见下方完整信息)${c.reset}`);
66
+ }
67
+ console.log(` ${c.gray}详细信息 :${c.reset} ${d.banmsg || '—'}`);
68
+ } else {
69
+ console.log(` ${c.gray}账号状态 :${c.reset} ${c.green}${c.bold}✔ 状态正常${c.reset}`);
70
+ console.log(` ${c.gray} ${c.reset} ${c.dim}该账号暂无封禁记录${c.reset}`);
71
+ }
72
+
73
+ if (verbose) {
74
+ console.log(` ${c.gray}─── 详细数据 ───${c.reset}`);
75
+ console.log(` ${c.gray}token :${c.reset} ${tokenUsed.slice(0, 8)}...`);
76
+ console.log(` ${c.gray}原始 JSON:${c.reset}`);
77
+ const raw = JSON.stringify(j, null, 2).split('\n').map(s => ` ${c.gray}${s}${c.reset}`).join('\n');
78
+ console.log(raw);
79
+ }
80
+
81
+ console.log(`${c.bold}${c.cyan}${divider()}${c.reset}\n`);
82
+ }
83
+
84
+ module.exports = { parseBanMsg, extractReason, printResult, divider };
package/src/repl.js ADDED
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+ const { queryQQ } = require('./api');
5
+ const { printResult, divider } = require('./format');
6
+ const c = require('./colors');
7
+
8
+ function isValidQQ(s) { return /^\d{5,12}$/.test(s); }
9
+
10
+ async function handleQQ(qq, state) {
11
+ process.stdout.write(`${c.dim}查询中...${c.reset}`);
12
+ const t0 = Date.now();
13
+ try {
14
+ const { json, tokenUsed } = await queryQQ(qq, state.verbose);
15
+ process.stdout.write(`\r${' '.repeat(60)}\r`);
16
+ if (state.verbose) console.log(`${c.gray}(耗时 ${Date.now() - t0}ms)${c.reset}`);
17
+ printResult(qq, json, tokenUsed, state.verbose);
18
+ } catch (e) {
19
+ process.stdout.write(`\r${' '.repeat(60)}\r`);
20
+ console.log(`${c.red}请求失败:${e.message}${c.reset}`);
21
+ console.log(`${c.dim}请检查:1) 已关闭 Clash/代理 2) 网络通畅 3) 接口未失效${c.reset}\n`);
22
+ }
23
+ }
24
+
25
+ function showHelp() {
26
+ console.log(`
27
+ LOL 封号查询 CLI
28
+
29
+ 用法:
30
+ ${c.cyan}lol${c.reset} 进入交互模式
31
+ ${c.cyan}lol <QQ号>${c.reset} 直接查询单个 QQ 后退出
32
+ ${c.cyan}lol -v <QQ号>${c.reset} 详细模式:打印 token 与原始 JSON
33
+ ${c.cyan}lol -h${c.reset} 显示此帮助
34
+
35
+ 交互模式内命令:
36
+ ${c.gray}q / quit / exit${c.reset} 退出
37
+ ${c.gray}-v${c.reset} 切换详细模式
38
+ ${c.gray}-h${c.reset} 显示帮助
39
+ `);
40
+ }
41
+
42
+ function banner(version) {
43
+ console.log();
44
+ console.log(`${c.bold}${c.cyan}${divider()}${c.reset}`);
45
+ console.log(`${c.bold}${c.cyan} LOL 封号查询 CLI${c.reset} ${c.gray}v${version}${c.reset}`);
46
+ console.log(`${c.bold}${c.cyan}${divider()}${c.reset}`);
47
+ console.log(` ${c.gray}输入 QQ 号回车查询 │ q 退出 │ -v 切换详细模式${c.reset}`);
48
+ console.log();
49
+ }
50
+
51
+ function startRepl(version, state) {
52
+ banner(version);
53
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
54
+
55
+ rl.on('SIGINT', () => {
56
+ console.log(`\n${c.dim}已退出${c.reset}`);
57
+ rl.close();
58
+ process.exit(0);
59
+ });
60
+
61
+ const ask = () => {
62
+ rl.question(`${c.cyan}QQ > ${c.reset}`, async (input) => {
63
+ const cmd = input.trim();
64
+ if (!cmd) return ask();
65
+ if (['q', 'quit', 'exit', ':q'].includes(cmd.toLowerCase())) {
66
+ console.log(`${c.dim}再见~${c.reset}`);
67
+ rl.close();
68
+ return;
69
+ }
70
+ if (cmd === '-v' || cmd === '--verbose') {
71
+ state.verbose = !state.verbose;
72
+ console.log(`${c.dim}详细模式:${state.verbose ? '已开启' : '已关闭'}${c.reset}\n`);
73
+ return ask();
74
+ }
75
+ if (cmd === '-h' || cmd === '--help' || cmd === '?') {
76
+ showHelp();
77
+ return ask();
78
+ }
79
+ if (!isValidQQ(cmd)) {
80
+ console.log(`${c.red}❗ 输入不合法(QQ 号应为 5-12 位数字)${c.reset}\n`);
81
+ return ask();
82
+ }
83
+ await handleQQ(cmd, state);
84
+ ask();
85
+ });
86
+ };
87
+
88
+ ask();
89
+ }
90
+
91
+ module.exports = { startRepl, handleQQ, showHelp, isValidQQ };
package/src/token.js ADDED
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { PRIMARY_PAGE, UA, FALLBACK_TOKEN, CACHE_DIR, CACHE_FILE, CACHE_TTL } = require('./config');
5
+ const c = require('./colors');
6
+
7
+ function loadCache() {
8
+ try {
9
+ return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ function saveCache(token) {
16
+ try {
17
+ if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
18
+ fs.writeFileSync(CACHE_FILE, JSON.stringify({ token, ts: Date.now() }, null, 2));
19
+ } catch {
20
+ /* ignore */
21
+ }
22
+ }
23
+
24
+ async function sniffToken(verbose) {
25
+ try {
26
+ const resp = await fetch(PRIMARY_PAGE, {
27
+ headers: { 'User-Agent': UA, Accept: 'text/html,*/*' },
28
+ });
29
+ if (!resp.ok) return null;
30
+ const html = await resp.text();
31
+ const m = html.match(/token=([a-f0-9]{8,32})/i);
32
+ return m ? m[1] : null;
33
+ } catch (e) {
34
+ if (verbose) console.error(`${c.dim}[嗅探异常: ${e.message}]${c.reset}`);
35
+ return null;
36
+ }
37
+ }
38
+
39
+ async function resolveToken(verbose) {
40
+ const cache = loadCache();
41
+ if (cache && cache.token && Date.now() - cache.ts < CACHE_TTL) {
42
+ if (verbose) console.error(`${c.dim}[使用缓存 token: ${cache.token.slice(0, 8)}...]${c.reset}`);
43
+ return cache.token;
44
+ }
45
+ const fresh = await sniffToken(verbose);
46
+ if (fresh) {
47
+ saveCache(fresh);
48
+ if (verbose) console.error(`${c.dim}[嗅探到最新 token: ${fresh.slice(0, 8)}...]${c.reset}`);
49
+ return fresh;
50
+ }
51
+ if (cache && cache.token) {
52
+ if (verbose) console.error(`${c.dim}[嗅探失败,使用过期缓存 token]${c.reset}`);
53
+ return cache.token;
54
+ }
55
+ if (verbose) console.error(`${c.dim}[使用预设兜底 token]${c.reset}`);
56
+ return FALLBACK_TOKEN;
57
+ }
58
+
59
+ module.exports = { sniffToken, resolveToken, saveCache, loadCache };