tokenforbes-cli 0.1.2 → 0.1.4
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/bin.js +65 -26
- package/package.json +1 -1
package/bin.js
CHANGED
|
@@ -30,42 +30,52 @@ if (cmd === 'login') {
|
|
|
30
30
|
} else if (cmd === 'report') {
|
|
31
31
|
await report();
|
|
32
32
|
} else if (!cmd) {
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
if (key) await report(); else await link();
|
|
33
|
+
// 一条命令走完:report 内部会兜底——没密钥或密钥失效(服务器重置过)都自动带去登录再上报。
|
|
34
|
+
await report();
|
|
36
35
|
} else {
|
|
37
36
|
console.log('命令: link | register <昵称> | login <密钥> | report | daemon <install|status|uninstall>');
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
//
|
|
41
|
-
|
|
39
|
+
// 设备授权登录:先弹浏览器让你批准 → 趁这工夫后台抓用量 → 批准一到立刻上传。
|
|
40
|
+
// preDays: 若上层已抓好用量(比如 report 探到密钥失效),直接传进来,不再重抓。
|
|
41
|
+
async function link(preDays) {
|
|
42
42
|
const c = loadConfig();
|
|
43
43
|
const server = c.server || DEFAULT_SERVER;
|
|
44
44
|
const device = detectDevice();
|
|
45
45
|
const s = await post(server + '/api/device/start', { device });
|
|
46
46
|
const url = `${server}/link.html?code=${s.user_code}`;
|
|
47
|
-
|
|
48
|
-
console.log('
|
|
49
|
-
console.log('
|
|
47
|
+
// 第一步就把浏览器弹出去,别让用户对着黑终端干等。
|
|
48
|
+
console.log('\n① 浏览器这就打开,填个昵称、点「注册并批准」就行。没弹出来就手动复制这条链接:');
|
|
49
|
+
console.log(' → ' + url);
|
|
50
|
+
console.log(' 验证码: ' + s.user_code + '(跟网页上那串核对一致再批准)');
|
|
50
51
|
openBrowser(url);
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
// 趁你在浏览器填昵称、点批准的工夫,后台把本地用量先抓好,批准一到就能立刻上传。
|
|
54
|
+
let days = preDays;
|
|
55
|
+
if (!days) {
|
|
56
|
+
process.stdout.write('\n② 正在后台读取本地用量(ccusage)…');
|
|
57
|
+
days = readUsage();
|
|
58
|
+
console.log(days.length ? ` 已抓好 ${days.length} 天,就等你在浏览器批准。` : ' 没读到用量。');
|
|
59
|
+
} else {
|
|
60
|
+
console.log('\n② 本地用量已经抓好,就等你在浏览器批准。');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
process.stdout.write(' 等你点确认…');
|
|
53
64
|
const until = Date.now() + s.expires_in * 1000;
|
|
54
65
|
while (Date.now() < until) {
|
|
55
66
|
await sleep(s.interval * 1000);
|
|
56
67
|
const p = await post(server + '/api/device/poll', { device_code: s.device_code });
|
|
57
68
|
if (p.status === 'approved') {
|
|
58
69
|
c.key = p.key; c.server = server; saveConfig(c);
|
|
59
|
-
console.log('\n✓
|
|
60
|
-
|
|
61
|
-
await report(); // 登录即上报,一条命令闭环,别让用户卡在「登录完不知道下一步」
|
|
70
|
+
console.log('\n✓ 已批准,密钥已存到 ' + CONFIG);
|
|
71
|
+
await uploadDays(days, { server, key: p.key, device });
|
|
62
72
|
console.log('\n想让它每半小时自动后台上报? npm i -g tokenforbes-cli 后跑 tokenforbes daemon install');
|
|
63
73
|
return;
|
|
64
74
|
}
|
|
65
75
|
if (p.status === 'expired') break;
|
|
66
76
|
process.stdout.write('.');
|
|
67
77
|
}
|
|
68
|
-
die('\n登录超时或验证码已过期,重新跑一次 npx tokenforbes-cli
|
|
78
|
+
die('\n登录超时或验证码已过期,重新跑一次 npx -y tokenforbes-cli');
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
@@ -85,34 +95,63 @@ async function register(nickname) {
|
|
|
85
95
|
const j = await post(server + '/api/register', { nickname, device });
|
|
86
96
|
c.key = j.key; c.server = server; saveConfig(c);
|
|
87
97
|
console.log(`注册成功,昵称「${j.nickname}」。密钥已存到 ${CONFIG}`);
|
|
88
|
-
console.log('
|
|
98
|
+
console.log('正在读取本地用量(ccusage)…');
|
|
99
|
+
await uploadDays(readUsage(), { server, key: j.key, device }); // 注册完顺手上报,别让用户再跑一条
|
|
89
100
|
}
|
|
90
101
|
|
|
91
|
-
async function report() {
|
|
102
|
+
async function report({ allowRelink = true } = {}) {
|
|
92
103
|
const c = loadConfig();
|
|
93
|
-
|
|
104
|
+
// 没密钥:直接带去登录(link 会先弹浏览器,再后台抓用量),不让用户对着报错发愣。
|
|
105
|
+
if (!c.key) {
|
|
106
|
+
if (allowRelink) return link();
|
|
107
|
+
die('还没登录。先跑 npx -y tokenforbes-cli (浏览器一键登录),或 register / login');
|
|
108
|
+
}
|
|
94
109
|
const server = c.server || DEFAULT_SERVER;
|
|
95
|
-
console.log('读取本地用量(ccusage)…');
|
|
96
|
-
const days = readUsage();
|
|
97
|
-
if (!days.length) return console.log('没读到用量数据。');
|
|
98
110
|
const device = detectDevice();
|
|
111
|
+
// 先用一个很快的空请求验密钥,别等抓完几十天用量才发现要重登——那样浏览器弹得太迟。
|
|
112
|
+
const probe = await postRaw(server + '/api/report', { key: c.key, device, days: [] });
|
|
113
|
+
if (!probe.ok) {
|
|
114
|
+
const authFailed = probe.status === 401 || /密钥/.test(probe.json?.error || '');
|
|
115
|
+
if (authFailed && allowRelink) {
|
|
116
|
+
console.log('本地密钥已失效,这就打开浏览器带你重新登录…');
|
|
117
|
+
delete c.key; saveConfig(c);
|
|
118
|
+
return link(); // 密钥无效,还没抓用量;交给 link 先弹浏览器再后台抓
|
|
119
|
+
}
|
|
120
|
+
die('上报失败:' + (probe.json?.error || probe.netError || probe.status));
|
|
121
|
+
}
|
|
122
|
+
// 密钥有效:抓用量并上传。
|
|
123
|
+
console.log('正在读取本地用量(ccusage)…');
|
|
124
|
+
await uploadDays(readUsage(), { server, key: c.key, device });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 把抓好的用量传上去。抽出来给 report / link / register 共用,避免各写一遍、避免重复抓。
|
|
128
|
+
async function uploadDays(days, { server, key, device }) {
|
|
129
|
+
if (!days || !days.length) return console.log('没读到用量数据(这台电脑还没用过 Claude Code / Codex?)。');
|
|
99
130
|
const total = days.reduce((s, d) => s + d.input + d.output + (d.cacheCreation || 0) + (d.cacheRead || 0), 0);
|
|
100
131
|
console.log(`设备:${device}`);
|
|
101
|
-
console.log(`共 ${days.length} 天,合计约 ${Math.round(total / 1e4).toLocaleString()} 万 token
|
|
102
|
-
const
|
|
103
|
-
console.log(`上报成功 ✓ 已记录 ${
|
|
132
|
+
console.log(`共 ${days.length} 天,合计约 ${Math.round(total / 1e4).toLocaleString()} 万 token,正在上传…`);
|
|
133
|
+
const res = await postRaw(server + '/api/report', { key, device, days });
|
|
134
|
+
if (res.ok) console.log(`上报成功 ✓ 已记录 ${res.json.days} 天。去 ${server} 看你的排名。`);
|
|
135
|
+
else die('上报失败:' + (res.json?.error || res.netError || res.status));
|
|
104
136
|
}
|
|
105
137
|
|
|
138
|
+
// 会抛错就 die 的版本:给登录/注册这类「失败就该停」的调用用。
|
|
106
139
|
async function post(url, body) {
|
|
140
|
+
const res = await postRaw(url, body);
|
|
141
|
+
if (!res.ok) die('失败:' + (res.json?.error || res.netError || res.status));
|
|
142
|
+
return res.json;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 不 die、把结果原样返回:给 report 这类「失败要自己判断怎么办」的调用用。
|
|
146
|
+
async function postRaw(url, body) {
|
|
107
147
|
let res;
|
|
108
148
|
try {
|
|
109
149
|
res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
110
150
|
} catch (e) {
|
|
111
|
-
|
|
151
|
+
return { ok: false, netError: `连不上服务器(${url}):${e.message}` };
|
|
112
152
|
}
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
return j;
|
|
153
|
+
const json = await res.json().catch(() => ({}));
|
|
154
|
+
return { ok: res.ok, status: res.status, json };
|
|
116
155
|
}
|
|
117
156
|
|
|
118
157
|
function die(msg) { console.error(msg); process.exit(1); }
|