creditdaddy 1.3.0 → 1.4.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 CHANGED
@@ -11,12 +11,16 @@
11
11
  [![Downloads](https://img.shields.io/github/downloads/techysy/CreditDaddy/total?label=%E4%B8%8B%E8%BD%BD&color=16a34a)](https://github.com/techysy/CreditDaddy/releases)
12
12
  [![npm](https://img.shields.io/npm/v/creditdaddy?label=npm&color=cb3837)](https://www.npmjs.com/package/creditdaddy)
13
13
  [![Platform](https://img.shields.io/badge/%E5%B9%B3%E5%8F%B0-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20fnOS-6b7280)](#下载)
14
+ [![Platform](https://img.shields.io/badge/%E5%85%A8%E5%B9%B3%E5%8F%B0%20%E5%AE%89%E8%A3%85-npm%20i%20--g%20creditdaddy-cb3837?logo=npm&logoColor=white)](#下载)
14
15
  [![Node](https://img.shields.io/badge/Node.js-%E2%89%A5%2020-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
15
16
  [![License](https://img.shields.io/github/license/techysy/CreditDaddy?label=%E8%AE%B8%E5%8F%AF&color=f59e0b)](LICENSE)
16
17
 
17
18
  [下载](#下载) · [架构](#%EF%B8%8F-架构) · [功能](#功能) · [10Router 集成](#10router-集成) · [产品线说明](#产品线说明) · [快速开始](#快速开始) · [HTTP API](#本地-http-api) · [相关项目](#-相关项目) · [贡献者致谢](#-贡献者致谢) · [许可证](#-许可证)
18
19
 
19
- <img src="docs/screenshot-dashboard.png" width="860" alt="CreditDaddy 仪表盘">
20
+ <picture>
21
+ <source media="(prefers-color-scheme: dark)" srcset="docs/screenshot-dashboard-dark.png">
22
+ <img src="docs/screenshot-dashboard-light.png" width="860" alt="CreditDaddy 仪表盘">
23
+ </picture>
20
24
 
21
25
  </div>
22
26
 
@@ -29,14 +33,15 @@
29
33
 
30
34
  | 平台 | 文件 | 说明 |
31
35
  | --- | --- | --- |
32
- | Windows | `CreditDaddy-Setup-<版本>.exe` | 安装版(推荐) |
33
- | Windows | `CreditDaddy-Portable-<版本>.exe` | 便携版,免安装,双击运行 |
34
- | macOS(Apple Silicon) | `mac-CreditDaddy-<版本>-arm64.dmg` | M1 及以后的芯片 |
35
- | macOS(Intel) | `mac-CreditDaddy-<版本>.dmg` | Intel 芯片 |
36
- | 飞牛 fnOS | `creditdaddy-window-<版本>-x86.fpk` | **推荐**:桌面窗口入口(x86) |
37
- | 飞牛 fnOS | `creditdaddy-window-<版本>-arm.fpk` | 桌面窗口入口(ARM) |
38
- | 飞牛 fnOS | `creditdaddy-<版本>-x86.fpk` | 独立全屏 / 兼容入口(x86) |
39
- | 飞牛 fnOS | `creditdaddy-<版本>-arm.fpk` | 独立全屏 / 兼容入口(ARM) |
36
+ | 全平台(安装包) | `npm i -g creditdaddy` | **任意平台**(Windows / macOS / Linux,x64 与 arm64 皆可),一行命令装成 `creditdaddy` 命令;也支持 `npx creditdaddy` 免安装试跑,详见[方式三](#方式三nodejs-cli--守护进程) |
37
+ | Windows | `CreditDaddy-Win-Setup-<版本>.exe` | 安装版(推荐) |
38
+ | Windows | `CreditDaddy-Win-Portable-<版本>.exe` | 便携版,免安装,双击运行 |
39
+ | macOS(Apple Silicon) | `CreditDaddy-Mac-Setup-<版本>-arm64.dmg` | M1 及以后的芯片 |
40
+ | macOS(Intel) | `CreditDaddy-Mac-Setup-<版本>-x64.dmg` | Intel 芯片 |
41
+ | 飞牛 fnOS | `CreditDaddy-FnOS-Window-<版本>-x86.fpk` | **推荐**:桌面窗口入口(x86) |
42
+ | 飞牛 fnOS | `CreditDaddy-FnOS-Window-<版本>-arm.fpk` | 桌面窗口入口(ARM) |
43
+ | 飞牛 fnOS | `CreditDaddy-FnOS-<版本>-x86.fpk` | 独立全屏 / 兼容入口(x86) |
44
+ | 飞牛 fnOS | `CreditDaddy-FnOS-<版本>-arm.fpk` | 独立全屏 / 兼容入口(ARM) |
40
45
 
41
46
  > macOS 安装包未做代码签名:首次打开请在「应用程序」里**右键 → 打开**,或在终端执行 `xattr -cr /Applications/CreditDaddy.app`。
42
47
  > 飞牛 fnOS 部署自动启用密码保护,首次安装在向导中设置密码,「应用设置」中可重置。
@@ -99,6 +104,7 @@
99
104
  - **国际版领取与设备风控**:Qoder 国际版服务端严格校验设备风控身份(`Cosy-MachineToken / Cosy-MachineCode / Cosy-MachineType`)。
100
105
  - **设备组件依赖**:风控凭据由本机已安装的 Qoder 客户端组件生成(每 50 分钟刷新)。每台设备每日限领一个国际版账号,列表靠前账号优先领取。
101
106
  - **Linux / fnOS 设备组件安装**:无客户端环境下,面板提供一键安装「设备身份组件」(或执行 `creditdaddy umid install`),自动从官方 `@qoder-ai/qodercli` 中提取 Linux x64 / arm64 UMID 核心组件。国内版不受风控限制,全部账号可正常签到。
107
+ > npm 全局安装的 Linux 用户同样适用:`sudo npm i -g creditdaddy && creditdaddy umid install`(组件落在数据目录 `qoder-umid/`,首次运行自动创建)。
102
108
 
103
109
  ### mirasim(原生 AI 编程开发环境)
104
110
  - **账号来源**:读取 `~/.mirasim/setting.json`,本地通过 `secret.key` 派生密钥解密 `mrs1:` AES-256-GCM 凭据,敏感 Token 绝不出机。
@@ -110,6 +116,7 @@
110
116
  - **额度与版本**:走桌面网关 `https://catx.nocode.cn/api/gateway/*`(纯 token 鉴权,`X-Auth-Token` 头)——`auth/current-user` 用户信息、`credit/balance` 可用 Credits 与当前套餐版本(体验版 / 专业版,含到期与下次刷新时间)。网页端 `credit.catpaw.meituan.com` 接口只认浏览器 Cookie,daemon 不使用。
111
117
  - **无签到、无对外续期**:按套餐发放 Credits,无每日签到活动;凭据由妙手客户端登录轮转,失效(401)时在客户端重新登录后再本机导入即可。
112
118
  - **客户端切号**:妙手启动时读凭据恢复登录(无热加载),切换 = 重写加密凭据 + 重启客户端;客户端运行中默认拒绝切换,强制切换自动退出并重新拉起妙手。
119
+ - **仅 Windows**:妙手桌面客户端本身只有 Windows 版(凭据密钥派生自注册表 `MachineGuid`、进程名为 `妙手.exe`),因此本机导入 / 切号在 Linux / NAS 上不可用——该产品线在非 Windows 平台自动隐藏对应入口,其余 6 条产品线不受影响。
113
120
 
114
121
  ### Trae(字节 TRAE SOLO / Trae CN)
115
122
  - **账号来源**:解本机客户端 `<userData>/User/globalStorage/storage.json` 的 `iCubeAuthInfo://icube.cloudide`(Trae 自研「tc」信封:pepper 为随安装包分发的公开常量表,SHA512 两轮派生 + AES-128-CBC + SHA512 完整性前缀),纯 `node:crypto` 实现,不引入依赖、不需要 SQLite。敏感 Token 绝不出机。
@@ -134,22 +141,89 @@
134
141
 
135
142
  ### 方式二:飞牛 fnOS 应用包(fpk)
136
143
 
137
- 1. 下载对应架构的 fpk 文件(推荐 `creditdaddy-window-<版本>-<架构>.fpk`)。
144
+ 1. 下载对应架构的 fpk 文件(推荐 `CreditDaddy-FnOS-Window-<版本>-<架构>.fpk`)。
138
145
  2. 在 fnOS 应用中心选择「手动安装」,安装过程中设置访问密码。
139
146
  3. 安装完成后通过桌面的窗口图标即可进入管理面板。
140
147
 
141
- ### 方式三:Node.js CLI / 守护进程
148
+ ### 方式三:Node.js CLI / 守护进程 —— 全平台通用(含 Linux / NAS / 服务器)
149
+
150
+ > **Linux 用户看这里**:这是本项目**全平台通吃**的安装方式,x64 / arm64 都能装,发行包只区分 CPU 架构、**不区分发行版**。
151
+ > Linux 上暂时没有像 Windows / macOS 那样的图形安装包,但用这条命令装出来的就是**同一套完整程序**:
152
+ > 本地守护进程 + 完整 Web 管理面板(浏览器里用,和桌面版界面一模一样)+ 全部产品线能力。
153
+ > 桌面端只是额外套了一层 Electron 托盘壳,核心功能没有少。
142
154
 
143
155
  要求 **Node.js ≥ 20**(纯标准库实现,零外部 npm 运行时依赖):
144
156
 
145
157
  ```bash
146
- # 全局安装(npm)
158
+ # 全局安装(任意平台:Windows / macOS / Linux,x64 / arm64)
147
159
  npm i -g creditdaddy
160
+ # 或者免安装直接跑一次
161
+ npx creditdaddy start
148
162
 
149
- # 启动后台守护进程(默认访问 http://127.0.0.1:47860)
163
+ # 后台启动(关掉终端也继续跑,默认 http://127.0.0.1:47860)
164
+ creditdaddy start
165
+
166
+ # 或者前台跑(Ctrl+C 停止,调试用)
150
167
  creditdaddy daemon
151
168
  ```
152
169
 
170
+ 装完在浏览器打开 `http://127.0.0.1:47860` 就是管理面板。
171
+
172
+ **Linux 怎么装 Node.js**:Node 官方为 `linux-x64` / `linux-arm64` 都提供预编译压缩包([nodejs.org/download](https://nodejs.org/en/download),**不需要编译、不挑发行版**),Debian / Ubuntu 也可用 [NodeSource](https://github.com/nodesource/distributions) 或发行版自带仓库;NAS / 容器里可用 `nodejs_v24` 这类社区应用包(fnOS 的 fpk 就是内置这一套)。已经能跑 `node -v` 就可以直接跳到 `npm i -g`。
173
+
174
+ > **关于 npm 的“平台”**:npm 包本身只是 JS 代码,是全平台通用的——`npm i -g creditdaddy` 在任何装了 Node ≥ 20 的系统上装的都是同一份包。所谓“平台差异”只在**可选的原生组件**上:本项目核心零依赖,只有两个平台相关能力(见下方表格),缺失时对应功能自动降级、不影响其余 6 条产品线。
175
+
176
+ **Linux 上各功能可用性**:
177
+
178
+ | 能力 | Linux / NAS | 说明 |
179
+ | --- | --- | --- |
180
+ | 登录授权 / 签到 / 额度 / 面板 / 导出导入 / 10Router | ✅ 全可用 | 不依赖任何本机客户端,纯标准库 |
181
+ | Qoder **国际版**签到 | ✅ 可用(装 UMID 组件) | 执行 `creditdaddy umid install`,自动从官方 `@qoder-ai/qodercli` 提取 **Linux x64 / arm64** 设备身份组件;不装则国际版无法领取(国内版不受限) |
182
+ | Qoder **国内版**签到 | ✅ 全可用 | 不走设备风控 |
183
+ | **本机导入**(读本机客户端登录) | ✅ 各产品线都有 Linux 路径 | Qoder `~/.config/Qoder`、WorkBuddy `~/.local/share/CodeBuddyExtension/…`、ZCode `~/.zcode`、mirasim `~/.mirasim`、MiniMax `~/.minimax`;Trae 读其用户数据目录 |
184
+ | **客户端一键切号** | ⚠️ 仅 Windows 完整 | 「结束客户端进程 → 写回凭据 → 重新拉起」这套是 Windows 实现(`taskkill`);Linux 适合用面板的**浏览器登录**加账号,或手动切号。不影响签到本身 |
185
+ | **妙手(CatPaw)本机导入 / 切号** | ❌ 仅 Windows | 妙手客户端只有 Windows 版:凭据解密密钥派生自注册表 `MachineGuid`,客户端进程名也是 `妙手.exe` |
186
+ | **用量同步**(ZCode / OpenCode / mirasim / MiMo 的 SQLite) | ✅ 可用,需 Node 22.5+ | 用内置 `node:sqlite` 读本地库;mirasim 走 ndjson 文件无需 SQLite |
187
+ | 开机自启 / 图形托盘 | ⚠️ 自建 | CLI 只做后台守护;Linux 可用 systemd user service 挂 `creditdaddy start` 实现开机自启 |
188
+
189
+ **Linux 后台常驻(systemd user service,可选)**:
190
+
191
+ ```bash
192
+ mkdir -p ~/.config/systemd/user
193
+ cat > ~/.config/systemd/user/creditdaddy.service <<'EOF'
194
+ [Unit]
195
+ Description=CreditDaddy daemon
196
+ After=network-online.target
197
+
198
+ [Service]
199
+ ExecStart=%h/.npm-global/bin/creditdaddy daemon
200
+ # 上面的路径换成 `command -v creditdaddy` 的输出;nvm 用户可写 node 直启:
201
+ # ExecStart=/usr/bin/node %h/.npm-global/lib/node_modules/creditdaddy/bin/creditdaddy.js daemon
202
+ Restart=always
203
+
204
+ [Install]
205
+ WantedBy=default.target
206
+ EOF
207
+ systemctl --user daemon-reload && systemctl --user enable --now creditdaddy
208
+ systemctl --user status creditdaddy # 用完 Ctrl+C 退出,不会停服务
209
+ ```
210
+
211
+ **后台运行**(CLI 用户用这个,不需要一直开着终端):
212
+
213
+ ```bash
214
+ creditdaddy start # 后台启动;已在运行则直接报出地址,不会起第二个
215
+ creditdaddy status # 查看运行状态(未运行时退出码 1,便于脚本判断)
216
+ creditdaddy stop # 停止(没在跑也算成功)
217
+ creditdaddy restart # 重启
218
+ ```
219
+
220
+ `start` 会把实例拉到独立进程组,终端窗口关掉、SSH 断开都不影响每日自动签到。运行信息记在
221
+ `<数据目录>/daemon.json`,日志在 `<数据目录>/logs/background.log`(按日归档的运行日志另在
222
+ `daemon-<日期>.log`)。`start` / `stop` 都是幂等的,脚本里可以放心串着写。
223
+
224
+ > 想要**托盘图标**和**开机自启**请用方式一的桌面版(Windows / macOS);CLI 这条路只做后台守护,不装任何原生依赖,
225
+ > Linux / NAS 上它就是主力形态(开机自启用上面的 systemd user service,或 Windows 的任务计划程序)。
226
+
153
227
  **CLI 常用命令**:
154
228
 
155
229
  ```bash
@@ -8,6 +8,8 @@
8
8
  * creditdaddy remove <id> 删除账号
9
9
  * creditdaddy export/import 导出/导入账号
10
10
  * creditdaddy logs 查看领取状态(state.json)
11
+ * creditdaddy start/stop 后台运行(关掉终端不影响每日签到)
12
+ * creditdaddy status 查看后台运行状态
11
13
  * creditdaddy help 显示帮助
12
14
  */
13
15
 
@@ -16,6 +18,7 @@ import { logger } from '../src/logger.js';
16
18
 
17
19
  const args = process.argv.slice(2);
18
20
  const cmd = args[0] || 'daemon';
21
+ const DEFAULT_BG_PORT = 47860; // 与 src/daemon.js 的 DEFAULT_PORT 一致
19
22
 
20
23
  const HELP = `CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae 多账号管理 + 每日积分自动领取
21
24
 
@@ -31,6 +34,10 @@ const HELP = `CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Tra
31
34
  creditdaddy import <file.json> [--password 口令] 导入 CreditDaddy / 10router 导出文件
32
35
  creditdaddy scan 导入本机 Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae 客户端已登录的账号
33
36
  creditdaddy umid [install|remove] Qoder 设备身份组件(Linux / fnOS 国际版领取用)
37
+ creditdaddy start [--port 47860] [--host 127.0.0.1] 后台启动(关掉终端也继续跑)
38
+ creditdaddy stop 停止后台实例
39
+ creditdaddy status 查看后台运行状态
40
+ creditdaddy restart 重启后台实例
34
41
  creditdaddy logs 查看领取状态
35
42
  creditdaddy help 显示本帮助`;
36
43
 
@@ -39,15 +46,138 @@ function flag(name) {
39
46
  return i >= 0 ? args[i + 1] : undefined;
40
47
  }
41
48
 
49
+ /** 面板访问地址(只监听回环时用 127.0.0.1,0.0.0.0 不能当访问地址用) */
50
+ function panelUrl(host, port) {
51
+ const h = host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host;
52
+ return `http://${h}:${port}`;
53
+ }
54
+
55
+ /** 把运行时长说成人话(status 用) */
56
+ function humanUptime(ms) {
57
+ if (!Number.isFinite(ms) || ms < 0) return '未知';
58
+ const min = Math.floor(ms / 60000);
59
+ if (min < 1) return '不到 1 分钟';
60
+ if (min < 60) return `${min} 分钟`;
61
+ const h = Math.floor(min / 60);
62
+ if (h < 24) return `${h} 小时 ${min % 60} 分`;
63
+ return `${Math.floor(h / 24)} 天 ${h % 24} 小时`;
64
+ }
65
+
66
+ /**
67
+ * 优雅退出。之前 src/ 里一个 process.on 都没有,Ctrl+C 是 Node 默认的硬杀:
68
+ * 状态文件留在原地(下次 start 会当成「已在运行」)、当天的日志既不落盘也不归档。
69
+ */
70
+ function installShutdown({ server, background }) {
71
+ let closing = false;
72
+ const shutdown = async (signal) => {
73
+ if (closing) return;
74
+ closing = true;
75
+ try { logger.info('CLI', '收到 {signal},正在退出...', { signal }); } catch {}
76
+ try { (await import('../src/checkin.js')).stopScheduler(); } catch {}
77
+ try { await server?.close(); } catch {}
78
+ if (background) {
79
+ // 只删自己写的状态文件:并发跑着第二个实例时别把它的状态抹掉
80
+ try { const { clearRuntime } = await import('../src/bgdaemon.js'); await clearRuntime(process.pid); } catch {}
81
+ }
82
+ // 等归档流真的 close(gzip 完成)再退,否则今天的日志丢了
83
+ try { await (await import('../src/logger.js')).closeArchiveStream(); } catch {}
84
+ process.exit(0);
85
+ };
86
+ process.on('SIGINT', () => shutdown('SIGINT'));
87
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
88
+ // 关掉终端会发 SIGHUP。后台实例(stdout 不是 TTY)本来就该在终端消失后活着,
89
+ // 照着这个忽略;前台则当作用户要收摊。
90
+ process.on('SIGHUP', () => { if (process.stdout.isTTY) shutdown('SIGHUP'); });
91
+ }
92
+
42
93
  async function main() {
43
94
  switch (cmd) {
44
95
  case 'daemon': {
45
96
  const port = Number(flag('--port')) || Number(process.env.PORT) || undefined;
46
97
  const host = flag('--host') || process.env.HOST || undefined;
47
- await startDaemon(port, host);
98
+ const { server, port: boundPort } = await startDaemon(port, host);
99
+ // 端口被占时 daemon 会自动 +1 重试,真正在听的是 boundPort(记录的必须用它)。
100
+ // runtime 记录不分前台/后台:单实例守护(daemon.js 启动守卫)靠 daemon.json 找活实例,
101
+ // 只有所有启动方式都写文件,另一个方向的探测才完整。清理按 pid 归属(见 stopDaemon/clearRuntime)。
102
+ const background = process.env.CREDITDADDY_BG === '1';
103
+ {
104
+ const { APP_VERSION } = await import('../src/constants.js');
105
+ const { writeRuntime } = await import('../src/bgdaemon.js');
106
+ await writeRuntime({
107
+ pid: process.pid,
108
+ port: boundPort,
109
+ host: host || '127.0.0.1',
110
+ startedAt: new Date().toISOString(),
111
+ version: APP_VERSION,
112
+ });
113
+ if (background) logger.info('CLI', '已在后台运行', {});
114
+ }
48
115
  const { startScheduler } = await import('../src/checkin.js');
49
116
  startScheduler();
50
- logger.info('CLI', '按 Ctrl+C 停止');
117
+ installShutdown({ server, background });
118
+ logger.info('CLI', background ? '按 creditdaddy stop 停止' : '按 Ctrl+C 停止', {});
119
+ break;
120
+ }
121
+ case 'start': {
122
+ const { startBackground, backgroundLogFile } = await import('../src/bgdaemon.js');
123
+ const { APP_VERSION } = await import('../src/constants.js');
124
+ const port = Number(flag('--port')) || Number(process.env.PORT) || DEFAULT_BG_PORT;
125
+ const host = flag('--host') || '127.0.0.1';
126
+ console.log('正在后台启动 CreditDaddy…');
127
+ const r = await startBackground({ port, host });
128
+ if (r.error) { console.error('✗', r.error); process.exit(1); }
129
+ const rec = r.rec;
130
+ console.log(r.already ? '✓ 已在运行' : '✓ 已启动');
131
+ console.log(' 面板:', panelUrl(rec.host, rec.port));
132
+ console.log(' 进程:pid', rec.pid, '· 端口', rec.port, '· 版本', rec.version || APP_VERSION);
133
+ console.log(' 日志:', backgroundLogFile());
134
+ console.log(' 停止:creditdaddy stop');
135
+ break;
136
+ }
137
+ case 'stop': {
138
+ const { stopDaemon } = await import('../src/bgdaemon.js');
139
+ const r = await stopDaemon();
140
+ // 没在跑也算成功:脚本里 stop 之后紧接 start 不该因为「本来就没跑」而中断
141
+ if (!r.stopped) {
142
+ console.log(r.stale ? '· 清理了残留状态(进程已不在)' : '· 未在运行');
143
+ break;
144
+ }
145
+ console.log('✓ 已停止(pid ' + r.rec.pid + ')');
146
+ break;
147
+ }
148
+ case 'status': {
149
+ const { statusRuntime } = await import('../src/bgdaemon.js');
150
+ const s = await statusRuntime();
151
+ if (!s.running) {
152
+ console.log('· 未在运行');
153
+ if (s.reason === 'no-state') {
154
+ console.log(' 启动:creditdaddy start');
155
+ } else if (s.pidAlive) {
156
+ // 进程在但端口不应答:多半卡在 listen 之前,或状态文件指向了旧实例
157
+ console.log(' 进程 pid ' + s.pid + ' 还在,但面板没有应答;建议 creditdaddy stop 后重启');
158
+ }
159
+ process.exit(1);
160
+ }
161
+ const startedMs = s.startedAt ? Date.now() - Date.parse(s.startedAt) : NaN;
162
+ console.log('✓ 运行中');
163
+ console.log(' 面板:', panelUrl(s.host, s.port));
164
+ console.log(' 进程:pid', s.pid, '· 端口', s.port, '· 版本', s.probe?.version || s.version || '未知');
165
+ console.log(' 已运行:', humanUptime(startedMs));
166
+ if (s.probe?.needsKey) console.log(' 账号数:需面板密码(creditdaddy 设过访问密码)');
167
+ else if (s.probe?.accountsCount != null) console.log(' 账号数:', s.probe.accountsCount);
168
+ if (s.probe?.nextTickAt) console.log(' 下次签到:', new Date(s.probe.nextTickAt).toLocaleString());
169
+ break;
170
+ }
171
+ case 'restart': {
172
+ const { stopDaemon, startBackground, backgroundLogFile } = await import('../src/bgdaemon.js');
173
+ await stopDaemon();
174
+ const port = Number(flag('--port')) || Number(process.env.PORT) || DEFAULT_BG_PORT;
175
+ const host = flag('--host') || '127.0.0.1';
176
+ const r = await startBackground({ port, host });
177
+ if (r.error) { console.error('✗', r.error); process.exit(1); }
178
+ console.log('✓ 已重启');
179
+ console.log(' 面板:', panelUrl(r.rec.host, r.rec.port));
180
+ console.log(' 日志:', backgroundLogFile());
51
181
  break;
52
182
  }
53
183
  case 'add': {
@@ -104,20 +234,27 @@ async function main() {
104
234
  }
105
235
  case 'export': {
106
236
  const { loadAccounts } = await import('../src/store.js');
107
- const { exportAccounts, TransferError } = await import('../src/transfer.js');
237
+ const { exportAccounts, buildExportPayload, TransferError } = await import('../src/transfer.js');
108
238
  const out = args[1] && !args[1].startsWith('--') ? args[1] : 'creditdaddy-secure-backup.json';
109
239
  const password = flag('--password');
110
240
  if (!password) { console.error('导出账号必须设置加密口令:creditdaddy export [file.json] --password 口令(至少 4 位)'); process.exit(1); }
111
241
  const provider = args.includes('--cn') ? 'qoder-cn' : args.includes('--intl') ? 'qoder' : undefined;
112
242
  const { writeFileSync } = await import('node:fs');
113
- let blob;
243
+ let blob, count;
114
244
  try {
115
- blob = exportAccounts(await loadAccounts(), { password, provider });
245
+ const accounts = await loadAccounts();
246
+ blob = exportAccounts(accounts, { password, provider });
247
+ count = buildExportPayload(accounts, { provider }).accounts.length;
116
248
  } catch (e) {
249
+ // 只 warn 后自然退出(exitCode),不 process.exit——硬退会吞掉归档流里还没落盘的那行
250
+ logger.warn('CLI', '导出失败:{err}', { err: e instanceof TransferError ? e.code : e.message });
117
251
  console.error(e instanceof TransferError ? `导出失败:${e.message}` : e.message);
118
- process.exit(1);
252
+ process.exitCode = 1;
253
+ break;
119
254
  }
120
255
  writeFileSync(out, JSON.stringify(blob, null, 2), { mode: 0o600 });
256
+ // 与 daemon /api/export 同口径落审计日志(写盘成功后才记);只记范围与条数,不记口令/token
257
+ logger.info('CLI', '导出账号({scope}):{count} 个 → {out}', { scope: provider ? provider : '全部账号', count, out });
121
258
  console.log('✓ 已导出加密文件(10router 可直接导入):', out);
122
259
  break;
123
260
  }
@@ -126,9 +263,22 @@ async function main() {
126
263
  if (!file) { console.error('用法: creditdaddy import <file.json>'); process.exit(1); }
127
264
  const { readFileSync } = await import('node:fs');
128
265
  const { importAccounts } = await import('../src/accounts.js');
129
- const { parseImport } = await import('../src/transfer.js');
130
- const parsed = parseImport(JSON.parse(readFileSync(file, 'utf8')), { password: flag('--password') });
266
+ const { parseImport, TransferError } = await import('../src/transfer.js');
267
+ let parsed;
268
+ try {
269
+ parsed = parseImport(JSON.parse(readFileSync(file, 'utf8')), { password: flag('--password') });
270
+ } catch (e) {
271
+ if (e instanceof TransferError) {
272
+ // 同上:warn 后自然退出,硬退 process.exit 会丢这行审计日志
273
+ logger.warn('CLI', '导入失败({file}):{code}', { file, code: e.code });
274
+ console.error(`导入失败:${e.message}`);
275
+ process.exitCode = 1;
276
+ break;
277
+ }
278
+ throw e;
279
+ }
131
280
  const r = await importAccounts(parsed.accounts);
281
+ logger.info('CLI', '导入完成({source}):新增 {added},续期 {updated},跳过 {skipped}', { source: parsed.source, added: r.added, updated: r.updated, skipped: r.skipped + parsed.skipped });
132
282
  console.log(`✓ 导入完成(来源 ${parsed.source}):新增 ${r.added},续期 ${r.updated},跳过 ${r.skipped + parsed.skipped}`);
133
283
  break;
134
284
  }
package/package.json CHANGED
@@ -1,29 +1,32 @@
1
- {
2
- "name": "creditdaddy",
3
- "version": "1.3.0",
4
- "description": "CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax 多账号本地管理与每日积分自动签到助手",
5
- "homepage": "https://github.com/techysy/CreditDaddy",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/techysy/CreditDaddy.git"
9
- },
10
- "type": "module",
11
- "bin": {
12
- "creditdaddy": "bin/creditdaddy.js"
13
- },
14
- "files": [
15
- "bin",
16
- "src",
17
- "README.md",
18
- "LICENSE"
19
- ],
20
- "engines": {
21
- "node": ">=20"
22
- },
23
- "scripts": {
24
- "start": "node bin/creditdaddy.js daemon",
25
- "test": "node --test",
26
- "release-check": "bash scripts/release-check.sh"
27
- },
28
- "license": "MIT"
29
- }
1
+ {
2
+ "name": "creditdaddy",
3
+ "version": "1.4.0",
4
+ "description": "CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax 多账号本地管理与每日积分自动签到助手",
5
+ "homepage": "https://github.com/techysy/CreditDaddy",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/techysy/CreditDaddy.git"
9
+ },
10
+ "type": "module",
11
+ "bin": {
12
+ "creditdaddy": "bin/creditdaddy.js"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "src",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "scripts": {
24
+ "start": "node bin/creditdaddy.js daemon",
25
+ "test": "node --test",
26
+ "release-check": "bash scripts/release-check.sh"
27
+ },
28
+ "license": "MIT",
29
+ "devDependencies": {
30
+ "opencc-js": "1.4.2"
31
+ }
32
+ }
package/src/accounts.js CHANGED
@@ -67,6 +67,16 @@ function refreshExisting(existing, incoming) {
67
67
  existing.meta = { ...(existing.meta || {}), ...incomingMeta };
68
68
  changed = true;
69
69
  }
70
+ // MiniMax:meta.authRecordKey 是「绑定本机客户端凭据链」的开关(刷新前对齐 + 刷新后回写
71
+ // auth.json)。它只该待在「这条链真正的主人」身上。若同一个 uid 先被本机导入(带 key)、
72
+ // 又被浏览器登录(不带 key)刷新,浅合并会把链留在账号上;反过来则会给设备登录账号凭空
73
+ // 挂上客户端的链——两种都会让两条链共用一次性 refreshToken 互相作废。
74
+ // 这里按来源回收:只有本机导入(source: 'local-app')才保留,其余来源清掉。
75
+ if (existing.provider === 'minimax' && existing.meta?.authRecordKey && incoming.source !== 'local-app') {
76
+ delete existing.meta.authRecordKey;
77
+ if (!Object.keys(existing.meta).length) delete existing.meta;
78
+ changed = true;
79
+ }
70
80
  if (changed) existing.updatedAt = new Date().toISOString();
71
81
  return changed;
72
82
  }
package/src/authDevice.js CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  import { publicAccount } from './store.js';
26
26
  import { addAccount } from './accounts.js';
27
27
  import { logger } from './logger.js';
28
+ import { t } from './i18n.js';
28
29
  import { startWorkbuddyLogin, pollWorkbuddyLogin } from './workbuddyAuth.js';
29
30
  import { startZcodeLogin, pollZcodeLogin } from './zcodeAuth.js';
30
31
  import { startMiniMaxLogin, pollMiniMaxLogin } from './minimaxAuth.js';
@@ -151,7 +152,7 @@ export async function startDeviceFlow(kind) {
151
152
  intervalMs: r.intervalMs, nextPollAt: 0,
152
153
  done: false, busy: false,
153
154
  });
154
- logger.info('DEVICE', '发起浏览器登录(' + labelOf(kind) + ')');
155
+ logger.info('DEVICE', '发起浏览器登录({kind})', { kind: labelOf(kind) });
155
156
  return {
156
157
  sessionId,
157
158
  url: r.url,
@@ -185,7 +186,7 @@ export async function pollDeviceFlow(sessionId) {
185
186
  result = await FLOWS[session.kind].poll(session.data);
186
187
  } catch (err) {
187
188
  sessions.delete(sessionId);
188
- logger.warn('DEVICE', labelOf(session.kind) + ' 登录失败:' + err.message);
189
+ logger.warn('DEVICE', '{kind} 登录失败:{err}', { kind: labelOf(session.kind), err: err.message });
189
190
  return { status: 'failed', error: err.message };
190
191
  } finally {
191
192
  session.busy = false;
@@ -202,7 +203,7 @@ export async function pollDeviceFlow(sessionId) {
202
203
  session.done = true;
203
204
  const { account, duplicate } = added;
204
205
  logger.info('DEVICE', duplicate
205
- ? labelOf(session.kind) + ' 登录成功:账号已存在,已更新凭据(' + (account.name || account.id) + ')'
206
- : labelOf(session.kind) + ' 登录成功:' + (account.name || account.id));
206
+ ? t('{kind} 登录成功:账号已存在,已更新凭据({name})', { kind: labelOf(session.kind), name: account.name || account.id })
207
+ : t('{kind} 登录成功:{name}', { kind: labelOf(session.kind), name: account.name || account.id }));
207
208
  return { status: 'ok', account: publicAccount(account), already: duplicate };
208
209
  }