creditdaddy 1.3.2 → 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,6 +11,7 @@
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
 
@@ -32,14 +33,15 @@
32
33
 
33
34
  | 平台 | 文件 | 说明 |
34
35
  | --- | --- | --- |
35
- | Windows | `CreditDaddy-Setup-<版本>.exe` | 安装版(推荐) |
36
- | Windows | `CreditDaddy-Portable-<版本>.exe` | 便携版,免安装,双击运行 |
37
- | macOS(Apple Silicon) | `mac-CreditDaddy-<版本>-arm64.dmg` | M1 及以后的芯片 |
38
- | macOS(Intel) | `mac-CreditDaddy-<版本>.dmg` | Intel 芯片 |
39
- | 飞牛 fnOS | `creditdaddy-window-<版本>-x86.fpk` | **推荐**:桌面窗口入口(x86) |
40
- | 飞牛 fnOS | `creditdaddy-window-<版本>-arm.fpk` | 桌面窗口入口(ARM) |
41
- | 飞牛 fnOS | `creditdaddy-<版本>-x86.fpk` | 独立全屏 / 兼容入口(x86) |
42
- | 飞牛 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) |
43
45
 
44
46
  > macOS 安装包未做代码签名:首次打开请在「应用程序」里**右键 → 打开**,或在终端执行 `xattr -cr /Applications/CreditDaddy.app`。
45
47
  > 飞牛 fnOS 部署自动启用密码保护,首次安装在向导中设置密码,「应用设置」中可重置。
@@ -102,6 +104,7 @@
102
104
  - **国际版领取与设备风控**:Qoder 国际版服务端严格校验设备风控身份(`Cosy-MachineToken / Cosy-MachineCode / Cosy-MachineType`)。
103
105
  - **设备组件依赖**:风控凭据由本机已安装的 Qoder 客户端组件生成(每 50 分钟刷新)。每台设备每日限领一个国际版账号,列表靠前账号优先领取。
104
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/`,首次运行自动创建)。
105
108
 
106
109
  ### mirasim(原生 AI 编程开发环境)
107
110
  - **账号来源**:读取 `~/.mirasim/setting.json`,本地通过 `secret.key` 派生密钥解密 `mrs1:` AES-256-GCM 凭据,敏感 Token 绝不出机。
@@ -113,6 +116,7 @@
113
116
  - **额度与版本**:走桌面网关 `https://catx.nocode.cn/api/gateway/*`(纯 token 鉴权,`X-Auth-Token` 头)——`auth/current-user` 用户信息、`credit/balance` 可用 Credits 与当前套餐版本(体验版 / 专业版,含到期与下次刷新时间)。网页端 `credit.catpaw.meituan.com` 接口只认浏览器 Cookie,daemon 不使用。
114
117
  - **无签到、无对外续期**:按套餐发放 Credits,无每日签到活动;凭据由妙手客户端登录轮转,失效(401)时在客户端重新登录后再本机导入即可。
115
118
  - **客户端切号**:妙手启动时读凭据恢复登录(无热加载),切换 = 重写加密凭据 + 重启客户端;客户端运行中默认拒绝切换,强制切换自动退出并重新拉起妙手。
119
+ - **仅 Windows**:妙手桌面客户端本身只有 Windows 版(凭据密钥派生自注册表 `MachineGuid`、进程名为 `妙手.exe`),因此本机导入 / 切号在 Linux / NAS 上不可用——该产品线在非 Windows 平台自动隐藏对应入口,其余 6 条产品线不受影响。
116
120
 
117
121
  ### Trae(字节 TRAE SOLO / Trae CN)
118
122
  - **账号来源**:解本机客户端 `<userData>/User/globalStorage/storage.json` 的 `iCubeAuthInfo://icube.cloudide`(Trae 自研「tc」信封:pepper 为随安装包分发的公开常量表,SHA512 两轮派生 + AES-128-CBC + SHA512 完整性前缀),纯 `node:crypto` 实现,不引入依赖、不需要 SQLite。敏感 Token 绝不出机。
@@ -137,17 +141,24 @@
137
141
 
138
142
  ### 方式二:飞牛 fnOS 应用包(fpk)
139
143
 
140
- 1. 下载对应架构的 fpk 文件(推荐 `creditdaddy-window-<版本>-<架构>.fpk`)。
144
+ 1. 下载对应架构的 fpk 文件(推荐 `CreditDaddy-FnOS-Window-<版本>-<架构>.fpk`)。
141
145
  2. 在 fnOS 应用中心选择「手动安装」,安装过程中设置访问密码。
142
146
  3. 安装完成后通过桌面的窗口图标即可进入管理面板。
143
147
 
144
- ### 方式三:Node.js CLI / 守护进程
148
+ ### 方式三:Node.js CLI / 守护进程 —— 全平台通用(含 Linux / NAS / 服务器)
149
+
150
+ > **Linux 用户看这里**:这是本项目**全平台通吃**的安装方式,x64 / arm64 都能装,发行包只区分 CPU 架构、**不区分发行版**。
151
+ > Linux 上暂时没有像 Windows / macOS 那样的图形安装包,但用这条命令装出来的就是**同一套完整程序**:
152
+ > 本地守护进程 + 完整 Web 管理面板(浏览器里用,和桌面版界面一模一样)+ 全部产品线能力。
153
+ > 桌面端只是额外套了一层 Electron 托盘壳,核心功能没有少。
145
154
 
146
155
  要求 **Node.js ≥ 20**(纯标准库实现,零外部 npm 运行时依赖):
147
156
 
148
157
  ```bash
149
- # 全局安装(npm)
158
+ # 全局安装(任意平台:Windows / macOS / Linux,x64 / arm64)
150
159
  npm i -g creditdaddy
160
+ # 或者免安装直接跑一次
161
+ npx creditdaddy start
151
162
 
152
163
  # 后台启动(关掉终端也继续跑,默认 http://127.0.0.1:47860)
153
164
  creditdaddy start
@@ -156,6 +167,47 @@ creditdaddy start
156
167
  creditdaddy daemon
157
168
  ```
158
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
+
159
211
  **后台运行**(CLI 用户用这个,不需要一直开着终端):
160
212
 
161
213
  ```bash
@@ -169,8 +221,8 @@ creditdaddy restart # 重启
169
221
  `<数据目录>/daemon.json`,日志在 `<数据目录>/logs/background.log`(按日归档的运行日志另在
170
222
  `daemon-<日期>.log`)。`start` / `stop` 都是幂等的,脚本里可以放心串着写。
171
223
 
172
- > 想要**托盘图标**和**开机自启**请用方式一的桌面版;CLI 这条路只做后台守护,不装任何原生依赖。
173
- > 也可以把 `creditdaddy start` 挂到系统服务 / 任务计划程序上实现开机自启。
224
+ > 想要**托盘图标**和**开机自启**请用方式一的桌面版(Windows / macOS);CLI 这条路只做后台守护,不装任何原生依赖,
225
+ > Linux / NAS 上它就是主力形态(开机自启用上面的 systemd user service,或 Windows 的任务计划程序)。
174
226
 
175
227
  **CLI 常用命令**:
176
228
 
@@ -72,7 +72,7 @@ function installShutdown({ server, background }) {
72
72
  const shutdown = async (signal) => {
73
73
  if (closing) return;
74
74
  closing = true;
75
- try { logger.info('CLI', `收到 ${signal},正在退出…`); } catch {}
75
+ try { logger.info('CLI', '收到 {signal},正在退出...', { signal }); } catch {}
76
76
  try { (await import('../src/checkin.js')).stopScheduler(); } catch {}
77
77
  try { await server?.close(); } catch {}
78
78
  if (background) {
@@ -96,10 +96,11 @@ async function main() {
96
96
  const port = Number(flag('--port')) || Number(process.env.PORT) || undefined;
97
97
  const host = flag('--host') || process.env.HOST || undefined;
98
98
  const { server, port: boundPort } = await startDaemon(port, host);
99
- // 端口被占时 daemon 会自动 +1 重试,真正在听的是 boundPort;
100
- // 后台模式要把它记进状态文件,start/status 打印的必须是这个值
99
+ // 端口被占时 daemon 会自动 +1 重试,真正在听的是 boundPort(记录的必须用它)。
100
+ // runtime 记录不分前台/后台:单实例守护(daemon.js 启动守卫)靠 daemon.json 找活实例,
101
+ // 只有所有启动方式都写文件,另一个方向的探测才完整。清理按 pid 归属(见 stopDaemon/clearRuntime)。
101
102
  const background = process.env.CREDITDADDY_BG === '1';
102
- if (background) {
103
+ {
103
104
  const { APP_VERSION } = await import('../src/constants.js');
104
105
  const { writeRuntime } = await import('../src/bgdaemon.js');
105
106
  await writeRuntime({
@@ -109,12 +110,12 @@ async function main() {
109
110
  startedAt: new Date().toISOString(),
110
111
  version: APP_VERSION,
111
112
  });
112
- logger.info('CLI', '已在后台运行');
113
+ if (background) logger.info('CLI', '已在后台运行', {});
113
114
  }
114
115
  const { startScheduler } = await import('../src/checkin.js');
115
116
  startScheduler();
116
117
  installShutdown({ server, background });
117
- logger.info('CLI', background ? '按 creditdaddy stop 停止' : '按 Ctrl+C 停止');
118
+ logger.info('CLI', background ? '按 creditdaddy stop 停止' : '按 Ctrl+C 停止', {});
118
119
  break;
119
120
  }
120
121
  case 'start': {
@@ -233,20 +234,27 @@ async function main() {
233
234
  }
234
235
  case 'export': {
235
236
  const { loadAccounts } = await import('../src/store.js');
236
- const { exportAccounts, TransferError } = await import('../src/transfer.js');
237
+ const { exportAccounts, buildExportPayload, TransferError } = await import('../src/transfer.js');
237
238
  const out = args[1] && !args[1].startsWith('--') ? args[1] : 'creditdaddy-secure-backup.json';
238
239
  const password = flag('--password');
239
240
  if (!password) { console.error('导出账号必须设置加密口令:creditdaddy export [file.json] --password 口令(至少 4 位)'); process.exit(1); }
240
241
  const provider = args.includes('--cn') ? 'qoder-cn' : args.includes('--intl') ? 'qoder' : undefined;
241
242
  const { writeFileSync } = await import('node:fs');
242
- let blob;
243
+ let blob, count;
243
244
  try {
244
- blob = exportAccounts(await loadAccounts(), { password, provider });
245
+ const accounts = await loadAccounts();
246
+ blob = exportAccounts(accounts, { password, provider });
247
+ count = buildExportPayload(accounts, { provider }).accounts.length;
245
248
  } catch (e) {
249
+ // 只 warn 后自然退出(exitCode),不 process.exit——硬退会吞掉归档流里还没落盘的那行
250
+ logger.warn('CLI', '导出失败:{err}', { err: e instanceof TransferError ? e.code : e.message });
246
251
  console.error(e instanceof TransferError ? `导出失败:${e.message}` : e.message);
247
- process.exit(1);
252
+ process.exitCode = 1;
253
+ break;
248
254
  }
249
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 });
250
258
  console.log('✓ 已导出加密文件(10router 可直接导入):', out);
251
259
  break;
252
260
  }
@@ -255,9 +263,22 @@ async function main() {
255
263
  if (!file) { console.error('用法: creditdaddy import <file.json>'); process.exit(1); }
256
264
  const { readFileSync } = await import('node:fs');
257
265
  const { importAccounts } = await import('../src/accounts.js');
258
- const { parseImport } = await import('../src/transfer.js');
259
- 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
+ }
260
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 });
261
282
  console.log(`✓ 导入完成(来源 ${parsed.source}):新增 ${r.added},续期 ${r.updated},跳过 ${r.skipped + parsed.skipped}`);
262
283
  break;
263
284
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "creditdaddy",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax 多账号本地管理与每日积分自动签到助手",
5
5
  "homepage": "https://github.com/techysy/CreditDaddy",
6
6
  "repository": {
@@ -25,5 +25,8 @@
25
25
  "test": "node --test",
26
26
  "release-check": "bash scripts/release-check.sh"
27
27
  },
28
- "license": "MIT"
28
+ "license": "MIT",
29
+ "devDependencies": {
30
+ "opencc-js": "1.4.2"
31
+ }
29
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
  }
package/src/bgdaemon.js CHANGED
@@ -24,6 +24,7 @@ import path from 'node:path';
24
24
  import { fileURLToPath } from 'node:url';
25
25
  import { dataDir } from './store.js';
26
26
  import { logger } from './logger.js';
27
+ import { t } from './i18n.js';
27
28
 
28
29
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
30
  const CLI_ENTRY = path.join(__dirname, '..', 'bin', 'creditdaddy.js');
@@ -33,7 +34,9 @@ const DEFAULT_HOST = '127.0.0.1';
33
34
  // 后台启动最多等这么久:daemon 要读账号库、建 HTTP server、起签到定时器,冷启动慢一点正常
34
35
  const READY_TIMEOUT_MS = 30_000;
35
36
  const READY_INTERVAL_MS = 250;
36
- const PROBE_TIMEOUT_MS = 2000;
37
+ // /api/status 需要 DPAPI 解密与本机进程探测,冷机/高负载下单次可达 2-4s(实测 Windows 稳态 ~1.9s)。
38
+ // 2.5s 的探测超时和稳态耗时贴边,机器一忙就出现「探针永远超时 → 误判启动失败」的抖动,宽限到 8s。
39
+ const PROBE_TIMEOUT_MS = 8000;
37
40
  const STOP_TIMEOUT_MS = 8000;
38
41
 
39
42
  /** 状态文件路径:跟随 store.js 的 dataDir(),尊重 CREDITDADDY_HOME */
@@ -65,7 +68,7 @@ export async function writeRuntime(rec) {
65
68
  await fs.writeFile(file, JSON.stringify(rec, null, 2), { mode: 0o600 });
66
69
  return true;
67
70
  } catch (e) {
68
- logger.warn('BG', `写状态文件失败:${e?.message || e}`);
71
+ logger.warn('BG', '写状态文件失败:{err}', { err: e?.message || e });
69
72
  return false;
70
73
  }
71
74
  }
@@ -159,7 +162,13 @@ export async function statusRuntime() {
159
162
  const rec = readRuntime();
160
163
  if (!rec) return { running: false, reason: 'no-state' };
161
164
  const pidAlive = isAlive(rec.pid);
162
- const probe = pidAlive ? await probeStatus(rec) : { reachable: false };
165
+ let probe = pidAlive ? await probeStatus(rec) : { reachable: false };
166
+ // pid 还在但立刻探不通 = 大概率正在启动途中的瞬断(listening 刚触发、首个请求还没排到)。
167
+ // 只多等一拍再探一次:错了方向是放行双实例(状态互写)比错一刀拦死更危险
168
+ if (pidAlive && !probe.reachable) {
169
+ await new Promise((r) => setTimeout(r, 350));
170
+ probe = await probeStatus(rec);
171
+ }
163
172
  return { running: pidAlive && probe.reachable, pidAlive, probe, ...rec };
164
173
  }
165
174
 
@@ -326,7 +335,7 @@ export async function stopDaemon() {
326
335
  // 必须等到 pid 真的没了再报 stopped,否则调用方紧接着 isAlive(pid) 仍读到 true。
327
336
  const dead = await waitPidDead(rec.pid);
328
337
  await clearRuntime();
329
- if (!free) logger.warn('BG', `端口 ${rec.port} 迟迟没有释放,进程可能仍在收尾`);
330
- if (!dead) logger.warn('BG', `进程 ${rec.pid} 在 ${Math.round(STOP_TIMEOUT_MS / 1000)}s 内没有退出干净`);
338
+ if (!free) logger.warn('BG', '端口 {port} 迟迟没有释放,进程可能仍在收尾', { port: rec.port });
339
+ if (!dead) logger.warn('BG', '进程 {pid} 在 {sec}s 内没有退出干净', { pid: rec.pid, sec: Math.round(STOP_TIMEOUT_MS / 1000) });
331
340
  return { stopped: true, rec, portFree: free, pidDead: dead };
332
341
  }
package/src/checkin.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * 签到调度:普通账号每 ~2h 扫描一次;ZCode 自动活动领取使用独立的短周期调度。
2
+ * 签到调度:普通账号按配置的间隔(默认每 2h,面板设置可调 1-8h、可全局关闭)扫描一次;
3
+ * ZCode 自动活动领取使用独立的短周期调度。
3
4
  */
4
5
 
5
- import { loadAccounts, loadState, withAccounts, withState } from './store.js';
6
+ import { loadAccounts, loadSettings, loadState, withAccounts, withState } from './store.js';
6
7
  import { productImpl } from './providers.js';
7
8
  import { productOf, PROVIDER_LABEL } from './constants.js';
8
9
  import { startUsageSyncScheduler } from './tenrouter.js';
@@ -15,8 +16,15 @@ import {
15
16
  warmZcodeAppVersion,
16
17
  } from './zcodeClient.js';
17
18
  import { logger } from './logger.js';
19
+ import { t as tr } from './i18n.js';
18
20
 
19
- const TICK_MS = 2 * 60 * 60 * 1000;
21
+ /** 全局自动签到配置:默认开启、每 120 分钟一轮;面板「设置 → 通用」可关 / 调频。 */
22
+ export async function autoCheckinEnabled() {
23
+ return (await loadSettings()).autoCheckin !== false;
24
+ }
25
+ export async function checkinIntervalMin() {
26
+ return Math.max(15, Number((await loadSettings()).checkinIntervalMin) || 120);
27
+ }
20
28
  const TICK_JITTER_MS = 10 * 60 * 1000;
21
29
  const ZCODE_JITTER_MS = 15 * 1000;
22
30
  const ZCODE_FIRST_DELAY_MS = 2 * 60 * 1000;
@@ -39,10 +47,13 @@ let lastTick = null;
39
47
  let ticking = false;
40
48
 
41
49
  /** 调度器状态(面板展示用) */
42
- export function getSchedulerInfo() {
50
+ export async function getSchedulerInfo() {
43
51
  const enabled = autoClaimEnabled();
52
+ const autoCk = await autoCheckinEnabled();
44
53
  return {
45
- running: Boolean(timerHandle), nextTickAt, lastTick, ticking,
54
+ enabled: autoCk,
55
+ intervalMin: await checkinIntervalMin(),
56
+ running: Boolean(timerHandle), nextTickAt: autoCk ? nextTickAt : null, lastTick, ticking,
46
57
  zcode: {
47
58
  enabled,
48
59
  intervalMin: claimIntervalMin(),
@@ -61,8 +72,8 @@ export function dayKey(nowMs = Date.now(), provider = 'qoder') {
61
72
  return new Date(nowMs + (8 - h) * 3600 * 1000).toISOString().slice(0, 10);
62
73
  }
63
74
 
64
- export function msUntilNextTick(nowMs = Date.now(), rand = Math.random) {
65
- return Math.max(TICK_MS + Math.floor(rand() * TICK_JITTER_MS), 1000);
75
+ export async function msUntilNextTick(rand = Math.random) {
76
+ return Math.max((await checkinIntervalMin()) * 60 * 1000 + Math.floor(rand() * TICK_JITTER_MS), 1000);
66
77
  }
67
78
 
68
79
  export function zcodeMsUntilNextTick(rand = Math.random) {
@@ -104,7 +115,7 @@ function scheduleZcodeNextTick(delay = zcodeMsUntilNextTick()) {
104
115
  zcodeTimerHandle = setTimeout(() => {
105
116
  zcodeTimerHandle = null;
106
117
  zcodeNextTickAt = null;
107
- runZcodeTick().catch((err) => logger.error('ZCODE-CLAIM', `资格轮询失败:${err?.message || err}`))
118
+ runZcodeTick().catch((err) => logger.error('ZCODE-CLAIM', '资格轮询失败:{err}', { err: err?.message || err }))
108
119
  .finally(() => scheduleZcodeNextTick());
109
120
  }, delay);
110
121
  zcodeTimerHandle.unref?.();
@@ -131,7 +142,7 @@ function syncZcodeScheduler(startFresh = false) {
131
142
  zcodeExpiryTimerHandle = null;
132
143
  setAutoClaimEnabled(false);
133
144
  clearZcodeTimers();
134
- logger.info('ZCODE-CLAIM', '自动领取时段已结束,自动领取已关闭');
145
+ logger.info('ZCODE-CLAIM', '自动领取时段已结束,自动领取已关闭', {});
135
146
  }, Math.max(0, expiresAt - Date.now()));
136
147
  zcodeExpiryTimerHandle.unref?.();
137
148
  } else if (!expiresAt && zcodeExpiryTimerHandle) {
@@ -168,7 +179,7 @@ function runZcodeTick(accountIds, { force = false } = {}) {
168
179
  if (current) current.lastResult = lastResult;
169
180
  });
170
181
  } catch (err) {
171
- logger.warn('ZCODE-CLAIM', `${label} 自动领取异常:${err?.message || err}`);
182
+ logger.warn('ZCODE-CLAIM', '{label} 自动领取异常:{err}', { label, err: err?.message || err });
172
183
  }
173
184
  }
174
185
  zcodeLastTick = {
@@ -213,10 +224,13 @@ async function runTickNow(opts) {
213
224
 
214
225
  async function runTickInner(opts) {
215
226
  const allAccounts = await loadAccounts();
227
+ // WB 国际版活跃请求全局开关(面板「设置 → 通用」):关闭时该轮与手动领取都不再打
228
+ const wbActivity = (await loadSettings()).workbuddyActivity !== false;
216
229
  const accounts = allAccounts.filter(
217
230
  (a) => (!opts.provider || a.provider === opts.provider)
218
231
  && (!opts.product || productOf(a.provider) === opts.product)
219
232
  && (!opts.onlyAccountId || a.id === opts.onlyAccountId)
233
+ && (wbActivity || a.provider !== 'workbuddy-intl')
220
234
  && typeof productImpl(a.provider)?.checkin === 'function'
221
235
  );
222
236
  const zAccounts = (opts.includeZcode !== false && autoClaimEnabled()
@@ -247,7 +261,7 @@ async function runTickInner(opts) {
247
261
  continue;
248
262
  }
249
263
 
250
- const ctx = refreshContext(account, (message) => logger.info('CHECKIN', `${label}:${message}`));
264
+ const ctx = refreshContext(account, (message) => logger.info('CHECKIN', '{label}:{msg}', { label, msg: message }));
251
265
  let outcome = await productImpl(account.provider).checkin(account, ctx);
252
266
  outcome = { accountId: account.id, account: label, provider: account.provider, ...outcome };
253
267
  if (account.provider === 'qoder' && outcome.risk) {
@@ -270,23 +284,23 @@ async function runTickInner(opts) {
270
284
  if (outcome.status === 'checked-in') {
271
285
  memo[account.id] = todayFor;
272
286
  account.lastCheckin = new Date().toISOString();
273
- logger.info('CHECKIN', `${label} 领取成功 +${outcome.claimedAmount} Credits`);
287
+ logger.info('CHECKIN', '{label} 领取成功 +{amount} Credits', { label, amount: outcome.claimedAmount });
274
288
  } else if (outcome.status === 'already') {
275
289
  memo[account.id] = todayFor;
276
290
  account.lastCheckin = account.lastCheckin || new Date().toISOString();
277
- logger.info('CHECKIN', `${label}:${outcome.message || '今日已领'}`);
291
+ logger.info('CHECKIN', '{label}:{msg}', { label, msg: outcome.message || '今日已领' });
278
292
  } else if (outcome.status === 'limited') {
279
293
  memo[account.id] = todayFor;
280
- logger.info('CHECKIN', `${label}:${outcome.message}`);
294
+ logger.info('CHECKIN', '{label}:{msg}', { label, msg: outcome.message });
281
295
  } else if (outcome.status === 'no-activity') {
282
- logger.debug('CHECKIN', `${label}:${outcome.message || '当前无可领取的活动'}`);
296
+ logger.debug('CHECKIN', '{label}:{msg}', { label, msg: outcome.message || '当前无可领取的活动' });
283
297
  } else {
284
- logger.warn('CHECKIN', `${label} 领取失败:${outcome.error || outcome.status}`);
298
+ logger.warn('CHECKIN', '{label} 领取失败:{err}', { label, err: outcome.error || outcome.status });
285
299
  }
286
300
  } catch (err) {
287
301
  results.push({ accountId: account.id, account: label, provider: account.provider, status: 'failed', error: err?.message || String(err) });
288
302
  account.lastResult = { status: 'failed', message: err?.message || String(err), amount: 0, at: new Date().toISOString() };
289
- logger.error('CHECKIN', `${label} 异常:${err?.message || err}`);
303
+ logger.error('CHECKIN', '{label} 异常:{err}', { label, err: err?.message || err });
290
304
  }
291
305
  }
292
306
 
@@ -322,37 +336,53 @@ async function runTickInner(opts) {
322
336
  const summary = `签到汇总:成功 ${claimed.length}(+${totalCredits} Credits)、已领 ${already}`
323
337
  + (limited.length ? `、本机限领 ${limited.length}` : '')
324
338
  + `、无活动 ${none.length}、失败 ${failed.length}`;
325
- logger.info('CHECKIN', summary);
339
+ // 日志走 key+args(按界面语言实时重渲染);summary 原文继续供面板/状态展示
340
+ logger.info('CHECKIN', limited.length
341
+ ? '签到汇总:成功 {ok}(+{credits} Credits)、已领 {al}、本机限领 {limited}、无活动 {none}、失败 {fail}'
342
+ : '签到汇总:成功 {ok}(+{credits} Credits)、已领 {al}、无活动 {none}、失败 {fail}',
343
+ { ok: claimed.length, credits: totalCredits, al: already, limited: limited.length, none: none.length, fail: failed.length });
326
344
  lastTick = { at: new Date().toISOString(), summary };
327
345
  return { results, summary };
328
346
  }
329
347
 
348
+ async function scheduleTick() {
349
+ const delay = await msUntilNextTick();
350
+ nextTickAt = new Date(Date.now() + delay).toISOString();
351
+ timerHandle = setTimeout(async () => {
352
+ try {
353
+ await runCheckinTick({ skipIfCheckedToday: true, includeZcode: false });
354
+ } catch (err) {
355
+ logger.error('CHECKIN', '定时轮失败:{err}', { err: err?.message || err });
356
+ } finally {
357
+ scheduleTick();
358
+ }
359
+ }, delay);
360
+ timerHandle.unref?.();
361
+ logger.info('CHECKIN', '定时签到已启动,下次执行约 {min} 分钟后', { min: Math.round(delay / 60000) });
362
+ }
363
+
330
364
  /** 启动普通签到调度与 ZCode 独立轮询调度。 */
331
- export function startScheduler() {
365
+ export async function startScheduler() {
332
366
  if (timerHandle) return;
333
367
  startUsageSyncScheduler();
334
368
  warmZcodeAppVersion();
335
- const scheduleNext = () => {
336
- const delay = msUntilNextTick();
337
- nextTickAt = new Date(Date.now() + delay).toISOString();
338
- timerHandle = setTimeout(async () => {
339
- try {
340
- await runCheckinTick({ skipIfCheckedToday: true, includeZcode: false });
341
- } catch (err) {
342
- logger.error('CHECKIN', `定时轮失败:${err?.message || err}`);
343
- } finally {
344
- scheduleNext();
345
- }
346
- }, delay);
347
- timerHandle.unref?.();
348
- logger.info('CHECKIN', `定时签到已启动,下次执行约 ${Math.round(delay / 60000)} 分钟后`);
349
- };
350
- scheduleNext();
369
+ if (await autoCheckinEnabled()) {
370
+ scheduleTick();
371
+ setTimeout(() => {
372
+ runCheckinTick({ skipIfCheckedToday: true, includeZcode: false }).catch((err) =>
373
+ logger.error('CHECKIN', '引导轮失败:{err}', { err: err?.message || err }));
374
+ }, 15000).unref?.();
375
+ } else {
376
+ logger.info('CHECKIN', '自动签到已关闭(面板设置可开启)');
377
+ }
351
378
  syncZcodeScheduler();
352
- setTimeout(() => {
353
- runCheckinTick({ skipIfCheckedToday: true, includeZcode: false }).catch((err) =>
354
- logger.error('CHECKIN', `引导轮失败:${err?.message || err}`));
355
- }, 15000).unref?.();
379
+ }
380
+
381
+ /** 面板修改全局自动签到开关 / 频率后重同步(daemon PUT /api/settings 调用)。 */
382
+ export async function resyncAutoCheckinScheduler() {
383
+ if (timerHandle) { clearTimeout(timerHandle); timerHandle = null; nextTickAt = null; }
384
+ if (await autoCheckinEnabled()) scheduleTick();
385
+ else logger.info('CHECKIN', '自动签到已关闭');
356
386
  }
357
387
 
358
388
  export function stopScheduler() {