atm-droid 1.0.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,85 @@
1
+ # ATM Client CLI
2
+
3
+ 跨平台 Factory Token 管理工具,支持 Windows / macOS / Linux。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ # 全局安装
9
+ npm install -g atm-client
10
+
11
+ # 或本地安装后链接
12
+ cd atm-cli
13
+ npm install
14
+ npm link
15
+ ```
16
+
17
+ ## 使用
18
+
19
+ ```bash
20
+ # 查看帮助
21
+ atm --help
22
+
23
+ # 登录(使用激活码)
24
+ atm login ABC-DEF-123
25
+
26
+ # 查看账号列表
27
+ atm list
28
+
29
+ # 切换账号(交互式选择)
30
+ atm switch
31
+
32
+ # 切换到指定账号
33
+ atm switch 1
34
+
35
+ # 查看当前状态
36
+ atm status
37
+
38
+ # 检查余额并自动切换
39
+ atm check
40
+
41
+ # 启动后台服务(实时同步)
42
+ atm start
43
+
44
+ # 停止后台服务
45
+ atm stop
46
+
47
+ # 退出登录
48
+ atm logout
49
+ ```
50
+
51
+ ## 功能
52
+
53
+ - ✅ 激活码登录
54
+ - ✅ 多账号管理
55
+ - ✅ 手动切换账号
56
+ - ✅ 自动切换(余额用完时)
57
+ - ✅ 后台服务(WebSocket 实时同步)
58
+ - ✅ 自动写入 ~/.factory/auth.json
59
+
60
+ ## 后台服务
61
+
62
+ 启动后台服务后,会保持与服务器的 WebSocket 连接:
63
+ - 实时接收 Token 更新
64
+ - 自动刷新 auth.json
65
+ - 余额用完自动切换
66
+
67
+ ```bash
68
+ atm start # 启动
69
+ atm stop # 停止
70
+ atm status # 查看状态
71
+ ```
72
+
73
+ ## 配合 crontab 使用(Linux/macOS)
74
+
75
+ 如果不想运行后台服务,可以用 crontab 定时检查:
76
+
77
+ ```bash
78
+ # 每分钟检查一次
79
+ * * * * * /usr/local/bin/atm check
80
+ ```
81
+
82
+ ## 配置文件位置
83
+
84
+ - 配置: `~/.config/atm-client/config.json`
85
+ - Factory 认证: `~/.factory/auth.json`
package/bin/atm.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const chalk = require('chalk');
5
+ const pkg = require('../package.json');
6
+ const { login, logout, list, switchToken, status, start, stop, check } = require('../src/commands');
7
+
8
+ program
9
+ .name('atm')
10
+ .description('ATM Token Manager - Factory Token 管理工具')
11
+ .version(pkg.version);
12
+
13
+ program
14
+ .command('login <code>')
15
+ .description('使用激活码登录')
16
+ .action(login);
17
+
18
+ program
19
+ .command('logout')
20
+ .description('退出登录并清除数据')
21
+ .action(logout);
22
+
23
+ program
24
+ .command('list')
25
+ .alias('ls')
26
+ .description('列出所有可用账号')
27
+ .action(list);
28
+
29
+ program
30
+ .command('switch [index]')
31
+ .alias('sw')
32
+ .description('切换到指定账号')
33
+ .action(switchToken);
34
+
35
+ program
36
+ .command('status')
37
+ .alias('st')
38
+ .description('查看当前状态')
39
+ .action(status);
40
+
41
+ program
42
+ .command('start')
43
+ .description('启动后台服务(实时同步)')
44
+ .action(start);
45
+
46
+ program
47
+ .command('stop')
48
+ .description('停止后台服务')
49
+ .action(stop);
50
+
51
+ program
52
+ .command('check')
53
+ .description('检查并自动切换(余额不足时)')
54
+ .action(check);
55
+
56
+ program.parse();
57
+
58
+ if (!process.argv.slice(2).length) {
59
+ console.log(chalk.cyan('\n ATM Token Manager v' + pkg.version));
60
+ console.log(chalk.gray(' 跨平台 Factory Token 管理工具\n'));
61
+ program.outputHelp();
62
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "atm-droid",
3
+ "version": "1.0.0",
4
+ "description": "ATM Token Manager CLI - 跨平台 Factory Token 管理工具",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "atm": "./bin/atm.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/atm.js"
11
+ },
12
+ "keywords": ["factory", "token", "manager", "cli"],
13
+ "author": "ATM Team",
14
+ "license": "MIT",
15
+ "dependencies": {
16
+ "commander": "^11.1.0",
17
+ "chalk": "^4.1.2",
18
+ "inquirer": "^8.2.6",
19
+ "ws": "^8.14.2",
20
+ "node-fetch": "^2.7.0",
21
+ "ora": "^5.4.1",
22
+ "conf": "^10.2.0"
23
+ }
24
+ }
package/src/api.js ADDED
@@ -0,0 +1,104 @@
1
+ const fetch = require('node-fetch');
2
+ const { config, getDeviceId } = require('./config');
3
+
4
+ const getServerUrl = () => config.get('serverUrl');
5
+
6
+ // 激活码登录
7
+ async function activate(code) {
8
+ const deviceId = getDeviceId();
9
+ const response = await fetch(`${getServerUrl()}/api/activate`, {
10
+ method: 'POST',
11
+ headers: { 'Content-Type': 'application/json' },
12
+ body: JSON.stringify({ code, device_id: deviceId })
13
+ });
14
+ return response.json();
15
+ }
16
+
17
+ // 获取 Token 列表
18
+ async function getTokens() {
19
+ const sessionToken = config.get('sessionToken');
20
+ const deviceId = getDeviceId();
21
+
22
+ if (!sessionToken) {
23
+ throw new Error('未登录');
24
+ }
25
+
26
+ const response = await fetch(`${getServerUrl()}/api/tokens`, {
27
+ headers: {
28
+ 'Authorization': `Bearer ${sessionToken}`,
29
+ 'X-Device-ID': deviceId
30
+ }
31
+ });
32
+
33
+ const result = await response.json();
34
+
35
+ // 如果返回加密数据,需要解密
36
+ if (result.success && result.data && result.iv) {
37
+ // 简化版:假设服务器返回明文(或添加解密逻辑)
38
+ return result;
39
+ }
40
+
41
+ return result;
42
+ }
43
+
44
+ // 激活指定 Token
45
+ async function activateToken(tokenId) {
46
+ const sessionToken = config.get('sessionToken');
47
+ const deviceId = getDeviceId();
48
+
49
+ const response = await fetch(`${getServerUrl()}/api/tokens/activate`, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ 'Authorization': `Bearer ${sessionToken}`,
54
+ 'X-Device-ID': deviceId
55
+ },
56
+ body: JSON.stringify({ token_id: tokenId })
57
+ });
58
+
59
+ return response.json();
60
+ }
61
+
62
+ // 心跳
63
+ async function heartbeat() {
64
+ const sessionToken = config.get('sessionToken');
65
+ const deviceId = getDeviceId();
66
+
67
+ if (!sessionToken) return { valid: false };
68
+
69
+ const response = await fetch(`${getServerUrl()}/api/heartbeat`, {
70
+ method: 'POST',
71
+ headers: {
72
+ 'Content-Type': 'application/json',
73
+ 'Authorization': `Bearer ${sessionToken}`,
74
+ 'X-Device-ID': deviceId
75
+ }
76
+ });
77
+
78
+ return response.json();
79
+ }
80
+
81
+ // 解绑设备
82
+ async function unbind() {
83
+ const sessionToken = config.get('sessionToken');
84
+ const deviceId = getDeviceId();
85
+
86
+ const response = await fetch(`${getServerUrl()}/api/unbind`, {
87
+ method: 'POST',
88
+ headers: {
89
+ 'Content-Type': 'application/json',
90
+ 'Authorization': `Bearer ${sessionToken}`,
91
+ 'X-Device-ID': deviceId
92
+ }
93
+ });
94
+
95
+ return response.json();
96
+ }
97
+
98
+ module.exports = {
99
+ activate,
100
+ getTokens,
101
+ activateToken,
102
+ heartbeat,
103
+ unbind
104
+ };
@@ -0,0 +1,373 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const inquirer = require('inquirer');
4
+ const { spawn } = require('child_process');
5
+ const path = require('path');
6
+ const { config, writeFactoryAuth, readFactoryAuth, getFactoryAuthPath } = require('./config');
7
+ const api = require('./api');
8
+ const daemon = require('./daemon');
9
+
10
+ // 格式化数字
11
+ function formatNumber(num) {
12
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
13
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
14
+ return num.toString();
15
+ }
16
+
17
+ // 登录
18
+ async function login(code) {
19
+ const spinner = ora('正在激活...').start();
20
+
21
+ try {
22
+ const result = await api.activate(code);
23
+
24
+ if (result.success) {
25
+ config.set('sessionToken', result.session_token);
26
+ config.set('autoSwitch', result.auto_switch || false);
27
+
28
+ spinner.succeed(chalk.green('激活成功!'));
29
+ console.log(chalk.gray(` 会话有效期: ${new Date(result.expires_at * 1000).toLocaleString()}`));
30
+ console.log(chalk.gray(` 可用账号数: ${result.quota || 0}`));
31
+
32
+ if (result.auto_switch) {
33
+ console.log(chalk.cyan(' 自动切换: 已启用'));
34
+ }
35
+
36
+ console.log(chalk.yellow('\n 提示: 使用 atm list 查看账号列表'));
37
+ } else {
38
+ spinner.fail(chalk.red('激活失败: ' + (result.error || '未知错误')));
39
+ }
40
+ } catch (e) {
41
+ spinner.fail(chalk.red('激活失败: ' + e.message));
42
+ }
43
+ }
44
+
45
+ // 退出
46
+ async function logout() {
47
+ const { confirm } = await inquirer.prompt([{
48
+ type: 'confirm',
49
+ name: 'confirm',
50
+ message: '确定要退出登录吗?',
51
+ default: false
52
+ }]);
53
+
54
+ if (!confirm) return;
55
+
56
+ const spinner = ora('正在退出...').start();
57
+
58
+ try {
59
+ await api.unbind();
60
+ } catch (e) {
61
+ // 忽略错误
62
+ }
63
+
64
+ config.clear();
65
+ spinner.succeed(chalk.green('已退出登录'));
66
+ }
67
+
68
+ // 列出账号
69
+ async function list() {
70
+ const spinner = ora('获取账号列表...').start();
71
+
72
+ try {
73
+ const result = await api.getTokens();
74
+
75
+ if (!result.success) {
76
+ spinner.fail(chalk.red('获取失败: ' + (result.error || '未知错误')));
77
+ return;
78
+ }
79
+
80
+ const tokens = result.data || [];
81
+ const currentId = config.get('currentTokenId');
82
+
83
+ spinner.stop();
84
+
85
+ if (tokens.length === 0) {
86
+ console.log(chalk.yellow('\n 暂无可用账号'));
87
+ return;
88
+ }
89
+
90
+ console.log(chalk.cyan('\n 可用账号列表:\n'));
91
+
92
+ tokens.forEach((token, index) => {
93
+ const total = token.quota_total || 0;
94
+ const used = token.quota_used || 0;
95
+ const remaining = Math.max(0, total - used);
96
+ const isCurrent = token.id === currentId;
97
+
98
+ const prefix = isCurrent ? chalk.green('► ') : ' ';
99
+ const indexStr = chalk.gray(`${index + 1}.`);
100
+ const email = isCurrent ? chalk.green(token.email) : token.email;
101
+ const quota = remaining > 0
102
+ ? chalk.cyan(`(${formatNumber(remaining)})`)
103
+ : chalk.red('(已用完)');
104
+ const current = isCurrent ? chalk.green(' [当前]') : '';
105
+
106
+ console.log(` ${prefix}${indexStr} ${email} ${quota}${current}`);
107
+ });
108
+
109
+ console.log(chalk.gray('\n 使用 atm switch <序号> 切换账号'));
110
+
111
+ } catch (e) {
112
+ spinner.fail(chalk.red('获取失败: ' + e.message));
113
+ }
114
+ }
115
+
116
+ // 切换账号
117
+ async function switchToken(index) {
118
+ const spinner = ora('获取账号列表...').start();
119
+
120
+ try {
121
+ const result = await api.getTokens();
122
+
123
+ if (!result.success) {
124
+ spinner.fail(chalk.red('获取失败: ' + (result.error || '未知错误')));
125
+ return;
126
+ }
127
+
128
+ const tokens = result.data || [];
129
+
130
+ if (tokens.length === 0) {
131
+ spinner.fail(chalk.yellow('暂无可用账号'));
132
+ return;
133
+ }
134
+
135
+ spinner.stop();
136
+
137
+ let targetToken;
138
+
139
+ if (index) {
140
+ // 指定序号
141
+ const idx = parseInt(index) - 1;
142
+ if (idx < 0 || idx >= tokens.length) {
143
+ console.log(chalk.red('无效的序号'));
144
+ return;
145
+ }
146
+ targetToken = tokens[idx];
147
+ } else {
148
+ // 交互式选择
149
+ const choices = tokens.map((t, i) => {
150
+ const remaining = (t.quota_total || 0) - (t.quota_used || 0);
151
+ return {
152
+ name: `${t.email} (${formatNumber(remaining)})`,
153
+ value: t,
154
+ short: t.email
155
+ };
156
+ });
157
+
158
+ const { selected } = await inquirer.prompt([{
159
+ type: 'list',
160
+ name: 'selected',
161
+ message: '选择要切换的账号:',
162
+ choices
163
+ }]);
164
+
165
+ targetToken = selected;
166
+ }
167
+
168
+ // 执行切换
169
+ const switchSpinner = ora(`正在切换到 ${targetToken.email}...`).start();
170
+
171
+ const activateResult = await api.activateToken(targetToken.id);
172
+
173
+ if (activateResult.success) {
174
+ // 写入 auth.json
175
+ if (activateResult.access_token && activateResult.refresh_token) {
176
+ writeFactoryAuth(activateResult.access_token, activateResult.refresh_token);
177
+ config.set('currentTokenId', targetToken.id);
178
+
179
+ switchSpinner.succeed(chalk.green(`已切换到 ${targetToken.email}`));
180
+ console.log(chalk.gray(` auth.json 已更新: ${getFactoryAuthPath()}`));
181
+ } else {
182
+ switchSpinner.fail(chalk.red('切换失败: 未获取到 Token'));
183
+ }
184
+ } else {
185
+ switchSpinner.fail(chalk.red('切换失败: ' + (activateResult.error || '未知错误')));
186
+ }
187
+
188
+ } catch (e) {
189
+ spinner.fail(chalk.red('切换失败: ' + e.message));
190
+ }
191
+ }
192
+
193
+ // 查看状态
194
+ async function status() {
195
+ const sessionToken = config.get('sessionToken');
196
+
197
+ console.log(chalk.cyan('\n ATM Token Manager 状态\n'));
198
+
199
+ // 登录状态
200
+ if (sessionToken) {
201
+ console.log(chalk.green(' ✓ 已登录'));
202
+ } else {
203
+ console.log(chalk.red(' ✗ 未登录'));
204
+ console.log(chalk.gray(' 使用 atm login <激活码> 登录'));
205
+ return;
206
+ }
207
+
208
+ // 当前账号
209
+ const currentId = config.get('currentTokenId');
210
+ if (currentId) {
211
+ try {
212
+ const result = await api.getTokens();
213
+ if (result.success && result.data) {
214
+ const current = result.data.find(t => t.id === currentId);
215
+ if (current) {
216
+ const remaining = (current.quota_total || 0) - (current.quota_used || 0);
217
+ console.log(chalk.white(` 当前账号: ${current.email}`));
218
+ console.log(chalk.white(` 剩余配额: ${formatNumber(remaining)}`));
219
+ }
220
+ }
221
+ } catch (e) {
222
+ // 忽略
223
+ }
224
+ } else {
225
+ console.log(chalk.yellow(' 当前账号: 未选择'));
226
+ console.log(chalk.gray(' 使用 atm switch 选择账号'));
227
+ }
228
+
229
+ // 自动切换
230
+ const autoSwitch = config.get('autoSwitch');
231
+ console.log(chalk.white(` 自动切换: ${autoSwitch ? '已启用' : '未启用'}`));
232
+
233
+ // 后台服务
234
+ const daemonStatus = daemon.getDaemonStatus();
235
+ if (daemonStatus.running) {
236
+ console.log(chalk.green(` 后台服务: 运行中 (PID: ${daemonStatus.pid})`));
237
+ } else {
238
+ console.log(chalk.gray(' 后台服务: 未运行'));
239
+ console.log(chalk.gray(' 使用 atm start 启动后台服务'));
240
+ }
241
+
242
+ // auth.json 状态
243
+ const auth = readFactoryAuth();
244
+ if (auth && auth.accessToken) {
245
+ console.log(chalk.green(' auth.json: 已配置'));
246
+ } else {
247
+ console.log(chalk.yellow(' auth.json: 未配置'));
248
+ }
249
+
250
+ console.log();
251
+ }
252
+
253
+ // 启动后台服务
254
+ async function start() {
255
+ const daemonStatus = daemon.getDaemonStatus();
256
+
257
+ if (daemonStatus.running) {
258
+ console.log(chalk.yellow('后台服务已在运行 (PID: ' + daemonStatus.pid + ')'));
259
+ return;
260
+ }
261
+
262
+ const sessionToken = config.get('sessionToken');
263
+ if (!sessionToken) {
264
+ console.log(chalk.red('请先登录: atm login <激活码>'));
265
+ return;
266
+ }
267
+
268
+ console.log(chalk.cyan('正在启动后台服务...'));
269
+
270
+ // 使用 spawn 启动独立进程
271
+ const child = spawn(process.execPath, [
272
+ path.join(__dirname, 'daemon-runner.js')
273
+ ], {
274
+ detached: true,
275
+ stdio: 'ignore'
276
+ });
277
+
278
+ child.unref();
279
+
280
+ // 等待一下确认启动
281
+ await new Promise(resolve => setTimeout(resolve, 1000));
282
+
283
+ const newStatus = daemon.getDaemonStatus();
284
+ if (newStatus.running) {
285
+ console.log(chalk.green('✓ 后台服务已启动 (PID: ' + newStatus.pid + ')'));
286
+ console.log(chalk.gray(' 日志文件: ' + daemon.LOG_FILE));
287
+ } else {
288
+ console.log(chalk.red('启动失败,请检查日志: ' + daemon.LOG_FILE));
289
+ }
290
+ }
291
+
292
+ // 停止后台服务
293
+ async function stop() {
294
+ const result = daemon.stopDaemon();
295
+
296
+ if (result) {
297
+ console.log(chalk.green('✓ 后台服务已停止'));
298
+ }
299
+ }
300
+
301
+ // 检查并自动切换
302
+ async function check() {
303
+ const spinner = ora('检查账号状态...').start();
304
+
305
+ try {
306
+ const result = await api.getTokens();
307
+
308
+ if (!result.success) {
309
+ spinner.fail(chalk.red('检查失败: ' + (result.error || '未知错误')));
310
+ return;
311
+ }
312
+
313
+ const tokens = result.data || [];
314
+ const currentId = config.get('currentTokenId');
315
+
316
+ // 检查当前账号余额
317
+ const current = tokens.find(t => t.id === currentId);
318
+
319
+ if (current) {
320
+ const remaining = (current.quota_total || 0) - (current.quota_used || 0);
321
+
322
+ if (remaining > 0) {
323
+ spinner.succeed(chalk.green(`当前账号 ${current.email} 余额充足 (${formatNumber(remaining)})`));
324
+ return;
325
+ }
326
+
327
+ spinner.text = '当前账号余额不足,正在切换...';
328
+ }
329
+
330
+ // 找到有余额的账号
331
+ const available = tokens.filter(t => {
332
+ const r = (t.quota_total || 0) - (t.quota_used || 0);
333
+ return r > 0 && t.id !== currentId;
334
+ });
335
+
336
+ if (available.length === 0) {
337
+ spinner.fail(chalk.red('所有账号余额已用完'));
338
+ return;
339
+ }
340
+
341
+ // 切换到余额最少的
342
+ available.sort((a, b) => {
343
+ const ra = (a.quota_total || 0) - (a.quota_used || 0);
344
+ const rb = (b.quota_total || 0) - (b.quota_used || 0);
345
+ return ra - rb;
346
+ });
347
+
348
+ const target = available[0];
349
+ const activateResult = await api.activateToken(target.id);
350
+
351
+ if (activateResult.success && activateResult.access_token) {
352
+ writeFactoryAuth(activateResult.access_token, activateResult.refresh_token);
353
+ config.set('currentTokenId', target.id);
354
+ spinner.succeed(chalk.green(`已自动切换到 ${target.email}`));
355
+ } else {
356
+ spinner.fail(chalk.red('切换失败'));
357
+ }
358
+
359
+ } catch (e) {
360
+ spinner.fail(chalk.red('检查失败: ' + e.message));
361
+ }
362
+ }
363
+
364
+ module.exports = {
365
+ login,
366
+ logout,
367
+ list,
368
+ switchToken,
369
+ status,
370
+ start,
371
+ stop,
372
+ check
373
+ };
package/src/config.js ADDED
@@ -0,0 +1,77 @@
1
+ const Conf = require('conf');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ // 配置存储
7
+ const config = new Conf({
8
+ projectName: 'atm-client',
9
+ defaults: {
10
+ serverUrl: 'https://dd.776523718.xyz',
11
+ sessionToken: null,
12
+ deviceId: null,
13
+ currentTokenId: null,
14
+ autoSwitch: false
15
+ }
16
+ });
17
+
18
+ // Factory auth.json 路径
19
+ function getFactoryAuthPath() {
20
+ const home = os.homedir();
21
+ return path.join(home, '.factory', 'auth.json');
22
+ }
23
+
24
+ // 确保 .factory 目录存在
25
+ function ensureFactoryDir() {
26
+ const dir = path.join(os.homedir(), '.factory');
27
+ if (!fs.existsSync(dir)) {
28
+ fs.mkdirSync(dir, { recursive: true });
29
+ }
30
+ }
31
+
32
+ // 读取 Factory auth.json
33
+ function readFactoryAuth() {
34
+ const authPath = getFactoryAuthPath();
35
+ if (fs.existsSync(authPath)) {
36
+ try {
37
+ return JSON.parse(fs.readFileSync(authPath, 'utf8'));
38
+ } catch (e) {
39
+ return null;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ // 写入 Factory auth.json
46
+ function writeFactoryAuth(accessToken, refreshToken) {
47
+ ensureFactoryDir();
48
+ const authPath = getFactoryAuthPath();
49
+ const data = {
50
+ accessToken,
51
+ refreshToken,
52
+ expiresAt: Date.now() + 3600000 // 1小时后过期
53
+ };
54
+ fs.writeFileSync(authPath, JSON.stringify(data, null, 2));
55
+ return true;
56
+ }
57
+
58
+ // 生成设备ID
59
+ function getDeviceId() {
60
+ let deviceId = config.get('deviceId');
61
+ if (!deviceId) {
62
+ const crypto = require('crypto');
63
+ const machineInfo = `${os.hostname()}-${os.platform()}-${os.arch()}-${os.userInfo().username}`;
64
+ deviceId = crypto.createHash('sha256').update(machineInfo).digest('hex').substring(0, 32);
65
+ config.set('deviceId', deviceId);
66
+ }
67
+ return deviceId;
68
+ }
69
+
70
+ module.exports = {
71
+ config,
72
+ getFactoryAuthPath,
73
+ ensureFactoryDir,
74
+ readFactoryAuth,
75
+ writeFactoryAuth,
76
+ getDeviceId
77
+ };
@@ -0,0 +1,7 @@
1
+ // 独立运行的守护进程
2
+ const daemon = require('./daemon');
3
+
4
+ daemon.startDaemon();
5
+
6
+ // 保持进程运行
7
+ setInterval(() => {}, 1000 * 60 * 60); // 每小时空循环一次,保持进程存活
package/src/daemon.js ADDED
@@ -0,0 +1,257 @@
1
+ const WebSocket = require('ws');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { config, writeFactoryAuth, getDeviceId } = require('./config');
6
+ const api = require('./api');
7
+
8
+ const PID_FILE = path.join(os.tmpdir(), 'atm-daemon.pid');
9
+ const LOG_FILE = path.join(os.tmpdir(), 'atm-daemon.log');
10
+
11
+ let ws = null;
12
+ let reconnectTimer = null;
13
+ let heartbeatTimer = null;
14
+
15
+ function log(message) {
16
+ const timestamp = new Date().toISOString();
17
+ const line = `[${timestamp}] ${message}\n`;
18
+ fs.appendFileSync(LOG_FILE, line);
19
+ if (process.env.ATM_DEBUG) {
20
+ console.log(line.trim());
21
+ }
22
+ }
23
+
24
+ function connectWebSocket() {
25
+ const serverUrl = config.get('serverUrl').replace('https://', 'wss://').replace('http://', 'ws://');
26
+ const sessionToken = config.get('sessionToken');
27
+ const deviceId = getDeviceId();
28
+
29
+ if (!sessionToken) {
30
+ log('未登录,无法连接 WebSocket');
31
+ return;
32
+ }
33
+
34
+ const wsUrl = `${serverUrl}/ws?token=${sessionToken}&device=${deviceId}`;
35
+
36
+ log(`连接 WebSocket: ${serverUrl}/ws`);
37
+
38
+ ws = new WebSocket(wsUrl);
39
+
40
+ ws.on('open', () => {
41
+ log('WebSocket 已连接');
42
+ // 订阅 Token 更新
43
+ ws.send(JSON.stringify({ type: 'subscribe', channel: 'tokens' }));
44
+ });
45
+
46
+ ws.on('message', async (data) => {
47
+ try {
48
+ const message = JSON.parse(data.toString());
49
+ log(`收到消息: ${message.type}`);
50
+
51
+ if (message.type === 'token_updated') {
52
+ // Token 已更新,重新获取并写入 auth.json
53
+ await refreshCurrentToken();
54
+ } else if (message.type === 'quota_depleted') {
55
+ // 余额用完,自动切换
56
+ if (config.get('autoSwitch')) {
57
+ await autoSwitch();
58
+ }
59
+ }
60
+ } catch (e) {
61
+ log(`解析消息失败: ${e.message}`);
62
+ }
63
+ });
64
+
65
+ ws.on('close', () => {
66
+ log('WebSocket 断开,5秒后重连...');
67
+ scheduleReconnect();
68
+ });
69
+
70
+ ws.on('error', (err) => {
71
+ log(`WebSocket 错误: ${err.message}`);
72
+ });
73
+ }
74
+
75
+ function scheduleReconnect() {
76
+ if (reconnectTimer) clearTimeout(reconnectTimer);
77
+ reconnectTimer = setTimeout(() => {
78
+ connectWebSocket();
79
+ }, 5000);
80
+ }
81
+
82
+ async function refreshCurrentToken() {
83
+ try {
84
+ const currentTokenId = config.get('currentTokenId');
85
+ if (!currentTokenId) return;
86
+
87
+ const result = await api.activateToken(currentTokenId);
88
+ if (result.success && result.access_token && result.refresh_token) {
89
+ writeFactoryAuth(result.access_token, result.refresh_token);
90
+ log(`Token 已更新: ${currentTokenId}`);
91
+ }
92
+ } catch (e) {
93
+ log(`刷新 Token 失败: ${e.message}`);
94
+ }
95
+ }
96
+
97
+ async function autoSwitch() {
98
+ try {
99
+ const result = await api.getTokens();
100
+ if (!result.success || !result.data) return;
101
+
102
+ const tokens = result.data;
103
+ const currentId = config.get('currentTokenId');
104
+
105
+ // 找到有余额的 Token
106
+ const available = tokens.filter(t => {
107
+ const remaining = (t.quota_total || 0) - (t.quota_used || 0);
108
+ return remaining > 0 && t.id !== currentId;
109
+ });
110
+
111
+ if (available.length > 0) {
112
+ // 切换到余额最少的(用完一个再用下一个)
113
+ available.sort((a, b) => {
114
+ const ra = (a.quota_total || 0) - (a.quota_used || 0);
115
+ const rb = (b.quota_total || 0) - (b.quota_used || 0);
116
+ return ra - rb;
117
+ });
118
+
119
+ const target = available[0];
120
+ const activateResult = await api.activateToken(target.id);
121
+
122
+ if (activateResult.success) {
123
+ config.set('currentTokenId', target.id);
124
+ writeFactoryAuth(activateResult.access_token, activateResult.refresh_token);
125
+ log(`自动切换到: ${target.email}`);
126
+ }
127
+ } else {
128
+ log('所有账号余额已用完');
129
+ }
130
+ } catch (e) {
131
+ log(`自动切换失败: ${e.message}`);
132
+ }
133
+ }
134
+
135
+ function startHeartbeat() {
136
+ heartbeatTimer = setInterval(async () => {
137
+ try {
138
+ const result = await api.heartbeat();
139
+ if (!result.valid) {
140
+ log('会话已失效');
141
+ }
142
+ } catch (e) {
143
+ log(`心跳失败: ${e.message}`);
144
+ }
145
+ }, 30000); // 每30秒心跳一次
146
+ }
147
+
148
+ function writePidFile() {
149
+ fs.writeFileSync(PID_FILE, process.pid.toString());
150
+ }
151
+
152
+ function removePidFile() {
153
+ if (fs.existsSync(PID_FILE)) {
154
+ fs.unlinkSync(PID_FILE);
155
+ }
156
+ }
157
+
158
+ function getPid() {
159
+ if (fs.existsSync(PID_FILE)) {
160
+ return parseInt(fs.readFileSync(PID_FILE, 'utf8'));
161
+ }
162
+ return null;
163
+ }
164
+
165
+ function isRunning(pid) {
166
+ try {
167
+ process.kill(pid, 0);
168
+ return true;
169
+ } catch (e) {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ // 启动守护进程
175
+ function startDaemon() {
176
+ const existingPid = getPid();
177
+ if (existingPid && isRunning(existingPid)) {
178
+ console.log('后台服务已在运行 (PID: ' + existingPid + ')');
179
+ return false;
180
+ }
181
+
182
+ // 清空日志
183
+ fs.writeFileSync(LOG_FILE, '');
184
+
185
+ writePidFile();
186
+ log('守护进程启动');
187
+
188
+ connectWebSocket();
189
+ startHeartbeat();
190
+
191
+ // 优雅退出
192
+ process.on('SIGTERM', () => {
193
+ log('收到 SIGTERM,正在退出...');
194
+ cleanup();
195
+ process.exit(0);
196
+ });
197
+
198
+ process.on('SIGINT', () => {
199
+ log('收到 SIGINT,正在退出...');
200
+ cleanup();
201
+ process.exit(0);
202
+ });
203
+
204
+ return true;
205
+ }
206
+
207
+ function cleanup() {
208
+ if (ws) ws.close();
209
+ if (reconnectTimer) clearTimeout(reconnectTimer);
210
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
211
+ removePidFile();
212
+ }
213
+
214
+ // 停止守护进程
215
+ function stopDaemon() {
216
+ const pid = getPid();
217
+ if (!pid) {
218
+ console.log('后台服务未运行');
219
+ return false;
220
+ }
221
+
222
+ if (!isRunning(pid)) {
223
+ removePidFile();
224
+ console.log('后台服务未运行');
225
+ return false;
226
+ }
227
+
228
+ try {
229
+ process.kill(pid, 'SIGTERM');
230
+ removePidFile();
231
+ return true;
232
+ } catch (e) {
233
+ console.log('停止失败:', e.message);
234
+ return false;
235
+ }
236
+ }
237
+
238
+ // 获取守护进程状态
239
+ function getDaemonStatus() {
240
+ const pid = getPid();
241
+ if (!pid) return { running: false };
242
+
243
+ return {
244
+ running: isRunning(pid),
245
+ pid,
246
+ logFile: LOG_FILE
247
+ };
248
+ }
249
+
250
+ module.exports = {
251
+ startDaemon,
252
+ stopDaemon,
253
+ getDaemonStatus,
254
+ refreshCurrentToken,
255
+ autoSwitch,
256
+ LOG_FILE
257
+ };
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // ATM Client CLI - 主入口
2
+ module.exports = require('./commands');