creditdaddy 1.5.0 → 1.5.2
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 +38 -3
- package/build.json +3 -3
- package/package.json +1 -1
- package/src/daemon.js +106 -7
- package/src/externalQuota.js +37 -7
- package/src/i18n/en.json +1755 -1743
- package/src/i18n/ja.json +1755 -1743
- package/src/i18n/ko.json +1755 -1743
- package/src/i18n/zh-TW.json +1755 -1743
- package/src/logger.js +56 -52
- package/src/panel.html +33 -4
- package/src/traeClient.js +294 -266
- package/src/workbuddyClient.js +87 -0
- package/src/zcodeClient.js +13 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# CreditDaddy
|
|
6
6
|
|
|
7
|
-
**AI 编程工具多账号本地管理 + 每日积分自动领取(领鸡蛋)助手:Qoder · WorkBuddy · ZCode · mirasim · 妙手 · Trae · MiniMax,集成 10Router
|
|
7
|
+
**AI 编程工具多账号本地管理 + 每日积分自动领取(领鸡蛋)助手:Qoder · WorkBuddy · ZCode · mirasim · 妙手 · Trae · MiniMax · ModelScope,集成 10Router 额度总览、用量同步与网页会话额度卡(Token Harbor / 小米 MiMo / River / ModelScope 魔搭 / LongCat)**
|
|
8
8
|
|
|
9
9
|
[](https://github.com/techysy/CreditDaddy/releases/latest)
|
|
10
10
|
[](https://github.com/techysy/CreditDaddy/actions/workflows/ci.yml)
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
- **客户端一键换号**:一键将桌面客户端切换至指定账号(WorkBuddy 自动热切换;ZCode / mirasim / 妙手 / Trae 退出客户端后写回并自动重新拉起),切号前自动保存当前在线 Token,各账号拥有独立设备指纹,杜绝风控串号。
|
|
70
70
|
- **内置隐私(无痕)授权**:桌面版提供隔离的一次性会话窗口进行网页登录授权,不污染系统浏览器 Cookie,支持同平台无缝扩增多账号。
|
|
71
71
|
- **全流程浏览器登录**:支持四家产品线在面板内直接完成网页 / 设备码授权并入库 —— Qoder 设备码授权(PKCE + nonce)、WorkBuddy 状态轮询、ZCode CLI 轮询(支持智谱 BigModel 与 Z.ai)、MiniMax 官方 OAuth 设备码授权(S256 PKCE)。
|
|
72
|
+
- **网页会话免费额度捕获**:配置驱动的通用浏览器 cookie 捕获框架,接入 Token Harbor / 小米 MiMo / River AI / ModelScope 魔搭 / LongCat(美团)五家「凭据只在网页会话」的供应商——桌面版隐私窗登录自动收割(含 localStorage 凭据变体与鉴权 cookie 轮询等待),支持同站点多号并存、按身份归户、粘贴捕获兜底、就地重探(免重登刷新身份与余额)与三色探活呼吸灯 + 死会话低频重试;凭据只存本机,面板「免费额度」页统一管理。
|
|
72
73
|
|
|
73
74
|
**自动领取与资产保障**
|
|
74
75
|
- **每日智能轮询**:本地守护进程每 ~2 小时自动扫描执行;当日已领成功的账号自动记忆跳过,未开启活动的账号不落记忆,以每日 10:00 (UTC+8) 刷新周期为准自动重试。
|
|
@@ -85,6 +86,7 @@
|
|
|
85
86
|
- **账号同步(一键推送为 10Router 连接)**:把本机 Qoder / WorkBuddy 账号直接推为 10Router 上游连接。配置 10Router 面板密码后走 **OAuth 通道**(accessToken + refreshToken,10R 端按身份去重并原位更新、自动续期,反复同步幂等);仅配虚拟 key 时走 **apikey 通道**(同名连接不覆盖,token 过期后需重新同步)。zcode → GLM 映射待反代就绪后补充(映射表已留接口)。
|
|
86
87
|
- **用量自动同步(10Router 1.0.7+)**:内置与 10router-sync 插件一致的同步逻辑,支持一键或每小时自动同步本机 ZCode(`db.sqlite` 官方渠道)、OpenCode(`opencode.db`)、mirasim(`usage-*.ndjson`)、小米 MiMo(`mimocode.db`)的真实用量,以及**妙手(CatPaw)云端用量**(`v1/usage/token/daily` 按天 token 总量;当天桶隔天入账,云端无输入/输出拆分、整包计入输入,模型列默认 `unknown`,可在 `tenrouter.json` 的 `sync.catpawModel` 标注)至 10Router 统计。支持断点续传与 2 天重叠补偿。10Router 按**行内容签名**(时间戳 / provider / model / token 数等)去重,行内容还在变(会话进行中、按天桶增长)会被当成新行——因此客户端发出去的行保证已定型:1 小时结算窗内暂不发送、妙手按天桶只发差量、缺时间戳的行直接跳过;日志显示「新增 N 行,跳过 M 行重复」让去重结果可见。
|
|
87
88
|
- *注:读取 SQLite 依赖 Node 22.5+ 内置的 `node:sqlite`(桌面版与 fnOS nodejs_v24 环境原生支持)。*
|
|
89
|
+
- **外部配额卡推送(10Router 联动)**:把网页会话站点的免费额度以「外部配额卡」推入 10Router 配额追踪器——Token Harbor / 小米 MiMo / River / ModelScope / LongCat / MiniMax 免费线各自成 source,余额数字与逐包明细腿直接出卡(储值口径 / 今日-长期拆分 / 赠送包徽注),会话失效自动降级状态卡;载荷经出口强校验**只带标量数字,凭据永不出本机**。卡面带签名回调 `refreshUrl`,10Router 侧点「刷新配额」即回源 CreditDaddy 现拉最新余额重推(8 天过期 + HMAC 签名 + 限流);同名认领可把额度卡挂到连接卡上,改账号别名即时生效。
|
|
88
90
|
|
|
89
91
|
## 产品线说明
|
|
90
92
|
|
|
@@ -130,6 +132,19 @@
|
|
|
130
132
|
- **刷新轮换互斥的处理**:MiniMax 的 refreshToken 是一次性轮换的,本机客户端会独立刷新。为避免 CreditDaddy 用陈旧快照刷新触发 `invalid_grant`:本机导入账号在刷新前会从 `auth.json` 重读最新 refreshToken 对齐,刷新成功后再安全回写文件(仅客户端未运行时,运行中回写会作废客户端内存里的 token 故跳过)。浏览器登录拿到的是独立 `loginEpoch` 凭据链,不与本机客户端共用,从根上消除互斥。
|
|
131
133
|
- **本地网关**:内置 Anthropic 兼容端点 `POST /gateway/minimax/v1/messages`,10Router 建 anthropic-compatible 节点指向即可把 MiniMax 当普通供应商调度;多账号轮换、401 自动刷新重试、429 冷却、SSE / JSON 流式透传、局域网 IP 白名单(与 ZCode 体验包接口同机制)。
|
|
132
134
|
|
|
135
|
+
### 网页会话免费额度站点(配置驱动捕获框架)
|
|
136
|
+
五家「凭据只存在于浏览器会话」的供应商共用一套配置驱动的网页会话捕获框架(`src/cookieSites.js` 站点表 + `src/externalQuota.js` 推卡 + 各站点 client):面板「免费额度」页登录窗捕获,凭据只存本机账号库,余额数字推 10Router 外部配额出卡。共同纪律:**余额接口即探测**(会话有效返 200 JSON、失效返 401)、就地重探免重登、死会话低频重试、载荷零凭据。
|
|
137
|
+
|
|
138
|
+
| 站点 | 凭据形态 | 额度口径 |
|
|
139
|
+
| --- | --- | --- |
|
|
140
|
+
| Token Harbor(issue #23) | 登录页 HTML 探针读数 | 免费 allowance 百分比 + 重置倒计时 |
|
|
141
|
+
| 小米 MiMo(issue #24/26) | 控制台会话 cookie(父域收割) | 充值可用余额(CNY 储值卡)+ UID 归户 |
|
|
142
|
+
| River AI | WorkOS localStorage JWT 变体(三源收割) | `$25` 免费额度 `balance_usd`,会话过期自动续期 |
|
|
143
|
+
| ModelScope 魔搭(issue #25) | 会话 cookie(双源) | 魔粒余额,卡面拆「今日发放 / 长期」两腿;每日额度登录自动发放、无签到端点 |
|
|
144
|
+
| LongCat 美团(issue #29) | 美团 SSO 会话 cookie | Token 资源包:Balance 汇总 + 逐包明细腿(官方消耗顺序、赠送包徽注、到期日) |
|
|
145
|
+
|
|
146
|
+
> 注:办公小浣熊曾按 issue #27 调研接入,因 `raccoon_token` 异步写回时序不可靠已于 v1.5.0 回滚移除。
|
|
147
|
+
|
|
133
148
|
## 快速开始
|
|
134
149
|
|
|
135
150
|
### 方式一:桌面安装版 / 便携版(推荐)
|
|
@@ -274,6 +289,12 @@ creditdaddy help # 查看完整命令行帮助
|
|
|
274
289
|
| `POST` | `/api/tenrouter/sync` | 立即触发本机模型用量向 10Router 同步 |
|
|
275
290
|
| `POST` | `/api/tenrouter/sync-accounts` | 把本机账号推送到 10Router 连接 `{adminPassword?}`(OAuth / apikey 双通道) |
|
|
276
291
|
| `GET` / `POST` | `/api/qoder/umid` | 获取 / 一键安装 Linux 平台 Qoder 设备身份组件 |
|
|
292
|
+
| `GET` | `/api/cookie-sites` | 网页会话站点列表与已捕获账号(含探活痕迹 / 捕获时间) |
|
|
293
|
+
| `POST` | `/api/cookie-sites/:id/capture` | 站点会话收割入库(桌面登录窗关闭时上报,余额/探测 API 即验证) |
|
|
294
|
+
| `POST` | `/api/cookie-sites/:id/reprobe` | 用已存会话就地重探(身份 + 余额回填,免重登) |
|
|
295
|
+
| `POST` | `/api/tenrouter/push-quota` | 立即向 10Router 推送一轮外部配额卡 |
|
|
296
|
+
| `POST` | `/api/quota-refresh` | 10Router 卡面「刷新配额」回调(HMAC 签名 + 限流,凭据不出载荷) |
|
|
297
|
+
| `GET` / `PUT` | `/api/{zcode,minimax,trae}-gateway` | 三个本地 Anthropic 兼容网关的配置与状态 |
|
|
277
298
|
| `GET` | `/api/logs` | 查看环形内存日志(`?tag=MINIMAX-GW` / `?tag=ZCODE-GW` 可只看网关调用日志) |
|
|
278
299
|
| `POST` | `/api/export` / `/api/import` | 账号数据安全加密导出与导入 |
|
|
279
300
|
|
|
@@ -298,6 +319,8 @@ CreditDaddy/
|
|
|
298
319
|
├── desktop/ # Electron 桌面壳工程(托盘、无痕授权窗口)
|
|
299
320
|
│ ├── main.js
|
|
300
321
|
│ ├── preload.js
|
|
322
|
+
│ ├── preload-container.js
|
|
323
|
+
│ ├── passwordStore.js
|
|
301
324
|
│ └── package.json
|
|
302
325
|
├── fnos-packaging/ # 飞牛 fnOS fpk 打包配置与声明
|
|
303
326
|
│ ├── cmd/
|
|
@@ -306,34 +329,46 @@ CreditDaddy/
|
|
|
306
329
|
├── src/ # 服务核心实现(纯标准库 ESM)
|
|
307
330
|
│ ├── accounts.js # 账号存储与生命周期管理
|
|
308
331
|
│ ├── authDevice.js # 网页 / 设备码授权分发中心
|
|
332
|
+
│ ├── bgdaemon.js # 后台实例启停与跨进程单实例守护
|
|
309
333
|
│ ├── catpawClient.js # 妙手桌面网关(用户信息 / 额度 / 套餐)请求
|
|
310
334
|
│ ├── catpawLocal.js # 本机妙手凭据 AES-GCM 解密与切号
|
|
311
335
|
│ ├── checkin.js # 自动轮询调度器
|
|
312
336
|
│ ├── constants.js # 服务接口与固定头常量
|
|
337
|
+
│ ├── cookieSites.js # 网页会话站点表(配置驱动捕获框架)
|
|
313
338
|
│ ├── daemon.js # 本地 HTTP API 与前端面板静态服务
|
|
339
|
+
│ ├── externalQuota.js # 10Router 外部配额卡推送(零凭据载荷)
|
|
340
|
+
│ ├── i18n.js · i18n/ # 多语言运行时与词典(en/ja/ko/zh-TW)
|
|
314
341
|
│ ├── localDetect.js # 客户端已安装环境侦测
|
|
315
|
-
│ ├── logger.js #
|
|
342
|
+
│ ├── logger.js # 内存环形日志与按日归档
|
|
343
|
+
│ ├── longcatClient.js # LongCat Token 资源包与身份请求
|
|
316
344
|
│ ├── mirasimClient.js # mirasim 套餐 / 额度请求
|
|
317
345
|
│ ├── mirasimLocal.js # 本机 mirasim 凭据解密与切号
|
|
318
346
|
│ ├── minimaxAuth.js # MiniMax 浏览器登录(OAuth 设备码 + S256 PKCE)
|
|
319
347
|
│ ├── minimaxClient.js # MiniMax 用户信息 / 额度 / 签到 / Token 续期请求
|
|
320
348
|
│ ├── minimaxGateway.js # MiniMax 本地 Anthropic 兼容网关(多账号轮换)
|
|
321
349
|
│ ├── minimaxLocal.js # 本机 MiniMax 凭据读取、刷新对齐与安全回写
|
|
350
|
+
│ ├── modelscopeClient.js # ModelScope 魔搭余额与流水拆分
|
|
322
351
|
│ ├── panel.html # Web 管理面板前端
|
|
323
352
|
│ ├── providers.js # 多产品线适配器注册表
|
|
324
353
|
│ ├── qoderApp.js # 本机 Qoder 客户端探测与 safeStorage 解密
|
|
325
354
|
│ ├── qoderClient.js # Qoder 业务接口请求封装
|
|
326
355
|
│ ├── qoderUmid.js # UMID 设备组件提取与运行
|
|
356
|
+
│ ├── quotaRefresh.js # 10Router 卡面「刷新配额」签名回调服务
|
|
357
|
+
│ ├── refreshGuard.js # Token 刷新互斥守卫(轮换型 refresh 防踢)
|
|
358
|
+
│ ├── riverClient.js # River AI WorkOS 会话与 Billing 余额请求
|
|
327
359
|
│ ├── store.js # 0600 本机原子化配置存储引擎
|
|
328
360
|
│ ├── tenrouter.js # 10Router 配额查询与调度
|
|
329
361
|
│ ├── tenrouterAccounts.js # 账号一键推送为 10Router 连接(OAuth / apikey 双通道)
|
|
330
362
|
│ ├── transfer.js # 加密备份导入导出
|
|
331
|
-
│ ├── traeClient.js # Trae 签到 /
|
|
363
|
+
│ ├── traeClient.js # Trae 签到 / 额度请求(设备码按 uid 派生)
|
|
364
|
+
│ ├── traeGateway.js # Trae 本地 Anthropic 兼容网关
|
|
332
365
|
│ ├── traeLocal.js # 本机 Trae「tc」信封凭据解密与切号
|
|
333
366
|
│ ├── usageSync.js # 本地多模型 SQLite 用量抽取
|
|
367
|
+
│ ├── winProc.js # Windows 进程探测(客户端在跑判定)
|
|
334
368
|
│ ├── workbuddyAuth.js # WorkBuddy 授权流程
|
|
335
369
|
│ ├── workbuddyClient.js # WorkBuddy 业务请求
|
|
336
370
|
│ ├── workbuddyLocal.js # 本机 WorkBuddy 会话拦截与切换
|
|
371
|
+
│ ├── xiaomiMimoClient.js # 小米 MiMo 控制台 UID 与余额请求
|
|
337
372
|
│ ├── zcodeAuth.js # ZCode 登录授权流程
|
|
338
373
|
│ ├── zcodeAutoClaim.js # ZCode 自动领活动与验证码处理
|
|
339
374
|
│ ├── zcodeClient.js # ZCode 权益与配额请求
|
package/build.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "creditdaddy",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax / ModelScope 多账号本地管理与每日积分自动签到助手,及 10Router 供应商 Cookie 会话捕获与外部配额(Token Harbor / 小米 MiMo / River / ModelScope 魔搭 / LongCat)",
|
|
5
5
|
"homepage": "https://github.com/techysy/CreditDaddy",
|
|
6
6
|
"repository": {
|
package/src/daemon.js
CHANGED
|
@@ -45,6 +45,7 @@ import crypto from 'node:crypto';
|
|
|
45
45
|
import os from 'node:os';
|
|
46
46
|
import fs from 'node:fs/promises';
|
|
47
47
|
import path from 'node:path';
|
|
48
|
+
import { execFileSync } from 'node:child_process';
|
|
48
49
|
import { fileURLToPath } from 'node:url';
|
|
49
50
|
import { logger, getLogs, readArchiveJsonl } from './logger.js';
|
|
50
51
|
import {
|
|
@@ -140,6 +141,37 @@ const DEFAULT_PORT = 47860;
|
|
|
140
141
|
const MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
141
142
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
|
|
142
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Windows 系统保留端口段。WSL2 镜像网络模式会把一段端口圈进 Hyper-V 的
|
|
146
|
+
* excludedportrange(实测 47860~47870 落进去),bind 该段直接 EACCES(或 EADDRINUSE),
|
|
147
|
+
* 应用根本拿不到绑定权限。查询 netsh 拿到全量保留段供选端口避开。
|
|
148
|
+
* 非 Windows / 查询失败返回 [](退化为 +1 重试逃逸,见 bind)。
|
|
149
|
+
*/
|
|
150
|
+
function windowsExcludedTcpRanges() {
|
|
151
|
+
if (process.platform !== 'win32') return [];
|
|
152
|
+
try {
|
|
153
|
+
const out = execFileSync('netsh', ['interface', 'ipv4', 'show', 'excludedportrange', 'protocol=tcp'],
|
|
154
|
+
{ encoding: 'utf8', windowsHide: true, timeout: 3000 });
|
|
155
|
+
// 行形如「 47860 47870」;表头/分隔线无两列纯数字,自然跳过
|
|
156
|
+
return [...String(out).matchAll(/^\s*(\d+)\s+(\d+)\s*$/gm)].map((m) => [Number(m[1]), Number(m[2])]);
|
|
157
|
+
} catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 从 start 起挑第一个不在系统保留段内的端口——确定性选择,避免每次启动漂到不同端口
|
|
164
|
+
* (10Router 网关节点 / 书签硬编码了端口,漂了就断)。netsh 不可用时原样返回 start,
|
|
165
|
+
* 交给 bind 的 +1 重试逃逸。ranges 可注入(测试用),默认查 netsh。
|
|
166
|
+
*/
|
|
167
|
+
export function pickBindablePort(start, ranges = windowsExcludedTcpRanges()) {
|
|
168
|
+
let p = Number(start) || DEFAULT_PORT;
|
|
169
|
+
for (let i = 0; i < 20000 && p <= 65535; i++, p++) {
|
|
170
|
+
if (!ranges.some(([a, b]) => p >= a && p <= b)) return p;
|
|
171
|
+
}
|
|
172
|
+
return Number(start) || DEFAULT_PORT;
|
|
173
|
+
}
|
|
174
|
+
|
|
143
175
|
// Qoder 当前登录缓存:面板 30s 一轮 status,DPAPI 解密结果没必要每轮重算
|
|
144
176
|
let qoderCurCache = { at: 0, data: null };
|
|
145
177
|
async function qoderCurrentInfo() {
|
|
@@ -566,6 +598,55 @@ async function handleApi(req, res, url) {
|
|
|
566
598
|
return json(res, 200, { results, summary });
|
|
567
599
|
}
|
|
568
600
|
|
|
601
|
+
// ── WorkBuddy 临时活动(额外补偿 / 首月赠礼)──
|
|
602
|
+
// 用已存 Bearer 凭据批量领取,免逐个网页点击。GET 预览可领项,POST 批量领。
|
|
603
|
+
// body 可选 { accountIds: [], kinds: [] } 过滤;默认扫全部 workbuddy 账号 × 全部活动。
|
|
604
|
+
if (p === '/api/workbuddy/activities' && method === 'GET') {
|
|
605
|
+
const accounts = await loadAccounts();
|
|
606
|
+
const targets = accounts.filter((a) => String(a.provider).startsWith('workbuddy'));
|
|
607
|
+
const { fetchWbActivities } = await import('./workbuddyClient.js');
|
|
608
|
+
const results = [];
|
|
609
|
+
for (const a of targets) {
|
|
610
|
+
const acts = await fetchWbActivities(a).catch((e) => [{ error: e.message }]);
|
|
611
|
+
results.push({ accountId: a.id, name: a.name || a.id, provider: a.provider, activities: acts });
|
|
612
|
+
}
|
|
613
|
+
return json(res, 200, { results });
|
|
614
|
+
}
|
|
615
|
+
if (p === '/api/workbuddy/claim-activities' && method === 'POST') {
|
|
616
|
+
const body = await readBody(req).catch(() => ({}));
|
|
617
|
+
const onlyIds = Array.isArray(body?.accountIds) && body.accountIds.length ? new Set(body.accountIds) : null;
|
|
618
|
+
const onlyKinds = Array.isArray(body?.kinds) && body.kinds.length ? body.kinds : null;
|
|
619
|
+
const accounts = await loadAccounts();
|
|
620
|
+
const targets = accounts.filter((a) => String(a.provider).startsWith('workbuddy') && (!onlyIds || onlyIds.has(a.id)));
|
|
621
|
+
const { claimWbActivity, fetchWbActivities } = await import('./workbuddyClient.js');
|
|
622
|
+
const results = [];
|
|
623
|
+
for (const a of targets) {
|
|
624
|
+
// 先查可领项,只对 active && !claimed 的发领取请求(幂等,多号批量安全)
|
|
625
|
+
let acts = [];
|
|
626
|
+
try { acts = await fetchWbActivities(a); } catch (e) { acts = [{ error: e.message }]; }
|
|
627
|
+
const claimable = acts.filter((x) => x.claimable && (!onlyKinds || onlyKinds.includes(x.kind)));
|
|
628
|
+
if (!claimable.length) {
|
|
629
|
+
results.push({ accountId: a.id, name: a.name || a.id, activities: acts, claimed: [] });
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const claimed = [];
|
|
633
|
+
for (const act of claimable) {
|
|
634
|
+
const r = await claimWbActivity(a, act.kind).catch((e) => ({ accountId: a.id, account: a.name || a.id, kind: act.kind, status: 'failed', error: e.message }));
|
|
635
|
+
claimed.push(r);
|
|
636
|
+
const ok = r.status === 'checked-in';
|
|
637
|
+
(ok ? logger.info : logger.warn)('WB-ACT', '{name} {label}:{msg}', { name: a.name || a.id, label: r.activityLabel || act.kind, msg: ok ? `领取成功 +${r.claimedAmount} 积分` : (r.error || r.message || r.status) });
|
|
638
|
+
}
|
|
639
|
+
results.push({ accountId: a.id, name: a.name || a.id, activities: acts, claimed });
|
|
640
|
+
}
|
|
641
|
+
const summary = {
|
|
642
|
+
claimed: results.reduce((s, r) => s + r.claimed.filter((x) => x.status === 'checked-in').length, 0),
|
|
643
|
+
already: results.reduce((s, r) => s + r.claimed.filter((x) => x.status === 'already').length, 0),
|
|
644
|
+
failed: results.reduce((s, r) => s + r.claimed.filter((x) => x.status === 'failed').length, 0),
|
|
645
|
+
total: results.reduce((s, r) => s + r.claimed.length, 0),
|
|
646
|
+
};
|
|
647
|
+
return json(res, 200, { results, summary });
|
|
648
|
+
}
|
|
649
|
+
|
|
569
650
|
// 积分查询(统一结构:total / used / remaining / parts[])
|
|
570
651
|
const quotaMatch = p.match(/^\/api\/accounts\/([\w-]+)\/quota$/);
|
|
571
652
|
if (quotaMatch && method === 'GET') {
|
|
@@ -578,9 +659,16 @@ async function handleApi(req, res, url) {
|
|
|
578
659
|
if (account.meta?.cookieSiteId) {
|
|
579
660
|
const site = getSite(account.meta.cookieSiteId);
|
|
580
661
|
if (!site) return json(res, 404, { error: `站点 ${account.meta.cookieSiteId} 未配置` });
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
662
|
+
// 频率治理(面板 2 分钟自动轮询曾把五家上游全轰一遍):默认读缓存快照零网络,
|
|
663
|
+
// 仅 ?live=1(手动「刷新积分」按钮 / 单卡回调)才真探上游并回填快照。缓存保鲜交给
|
|
664
|
+
// 后台推送循环(default 60 分钟)——它 live 拉到的余额本就该落库,顺手把这条链补上。
|
|
665
|
+
const live = url.searchParams.get('live') === '1';
|
|
666
|
+
let snap = account.meta?.quotaSnapshot || null;
|
|
667
|
+
if (live || !snap) {
|
|
668
|
+
const rep = await reprobeCookieAccount(site, account).catch((e) => ({ ok: false, error: e.message }));
|
|
669
|
+
if (!rep.ok) return json(res, 502, { error: rep.error || '探测失败' });
|
|
670
|
+
snap = rep.quotaSnapshot || {};
|
|
671
|
+
}
|
|
584
672
|
// 快照形状两样:API 站 total=余额;HTML 探针站(tokenharbor)total 缺失、
|
|
585
673
|
// used=已用百分比——统一结构按各自语义归一(HTML 站 remaining=100-used)。
|
|
586
674
|
const total = Number(snap.total);
|
|
@@ -1826,16 +1914,27 @@ export async function startDaemon(port = DEFAULT_PORT, host = '127.0.0.1') {
|
|
|
1826
1914
|
});
|
|
1827
1915
|
|
|
1828
1916
|
return new Promise((resolve, reject) => {
|
|
1917
|
+
const startPort = pickBindablePort(Number(port) || DEFAULT_PORT);
|
|
1829
1918
|
let attempts = 0;
|
|
1919
|
+
let triedFallback = false;
|
|
1830
1920
|
const bind = (p) => {
|
|
1831
1921
|
// 失败重试时必须摘掉上一次的 listening 监听,否则成功后会重复触发
|
|
1832
1922
|
const onError = (err) => {
|
|
1833
1923
|
server.off('listening', onListening);
|
|
1834
|
-
|
|
1924
|
+
const code = err && err.code;
|
|
1925
|
+
// EACCES = 落进系统保留段(WSL2 镜像网络 / Hyper-V excludedportrange),EADDRINUSE = 被别的进程占
|
|
1926
|
+
const blocked = code === 'EADDRINUSE' || code === 'EACCES';
|
|
1927
|
+
if (blocked && attempts < 100 && !triedFallback) {
|
|
1835
1928
|
attempts += 1;
|
|
1836
|
-
logger.warn('DAEMON', '端口 {port}
|
|
1929
|
+
logger.warn('DAEMON', '端口 {port} 不可用({code}),改试 {next}', { port: p, code: code || '未知', next: p + 1 });
|
|
1930
|
+
server.close();
|
|
1931
|
+
setTimeout(() => bind(p + 1), 30);
|
|
1932
|
+
} else if (blocked && !triedFallback) {
|
|
1933
|
+
// 起始端口起整段都不可用(保留段异常大):回退 OS 随机空闲端口,端口记进 daemon.json
|
|
1934
|
+
triedFallback = true;
|
|
1935
|
+
logger.warn('DAEMON', '端口 {start}~{end} 均不可用,回退系统随机空闲端口', { start: startPort, end: p });
|
|
1837
1936
|
server.close();
|
|
1838
|
-
setTimeout(() => bind(
|
|
1937
|
+
setTimeout(() => bind(0), 30);
|
|
1839
1938
|
} else {
|
|
1840
1939
|
// 兜底失败必须 reject:否则这个 promise 永远挂着,调用方的 startScheduler() 不会跑,
|
|
1841
1940
|
// 进程既不监听也不退出,只在日志里留一行,看起来像「启动了但没反应」
|
|
@@ -1857,6 +1956,6 @@ export async function startDaemon(port = DEFAULT_PORT, host = '127.0.0.1') {
|
|
|
1857
1956
|
server.once('listening', onListening);
|
|
1858
1957
|
server.listen(p, host);
|
|
1859
1958
|
};
|
|
1860
|
-
bind(
|
|
1959
|
+
bind(startPort);
|
|
1861
1960
|
});
|
|
1862
1961
|
}
|
package/src/externalQuota.js
CHANGED
|
@@ -215,6 +215,16 @@ export async function runQuotaPush({ endpoint, key, accounts, fetchImpl, onlySou
|
|
|
215
215
|
if (hit) hit.meta = { ...(hit.meta || {}), lastProbe: { ok: Boolean(ok), status, error: ok ? null : String(error || '').slice(0, 200), at: new Date().toISOString() } };
|
|
216
216
|
})).catch(() => {});
|
|
217
217
|
};
|
|
218
|
+
// live 余额落库(面板 /quota 缓存路径的数据源):推送循环每轮 live 拉到的数字顺手写回
|
|
219
|
+
// meta.quotaSnapshot + 各家明细字段,缓存随推送节奏(默认 60 分钟)保鲜。无条件落盘——
|
|
220
|
+
// 回调路径(quota-refresh → onlySource)拿的也是真数据,没理由只落 lastProbe 不落数字。
|
|
221
|
+
const persistLive = (a, patch) => {
|
|
222
|
+
if (!a?.id || !patch) return;
|
|
223
|
+
import('./store.js').then(({ withAccounts }) => withAccounts((list) => {
|
|
224
|
+
const hit = list.find((x) => x.id === a.id);
|
|
225
|
+
if (hit) hit.meta = { ...(hit.meta || {}), ...patch };
|
|
226
|
+
})).catch(() => {});
|
|
227
|
+
};
|
|
218
228
|
const deadSkip = (a) => {
|
|
219
229
|
const lp = a.meta?.lastProbe;
|
|
220
230
|
if (!lp || lp.ok || lp.status !== 'dead') return false;
|
|
@@ -256,8 +266,13 @@ export async function runQuotaPush({ endpoint, key, accounts, fetchImpl, onlySou
|
|
|
256
266
|
const uid = a.meta?.userId || a.uid || '';
|
|
257
267
|
try {
|
|
258
268
|
const b = await fetchXiaomiMimoBalance(a.token, uid, { fetchImpl });
|
|
259
|
-
if (b.ok)
|
|
260
|
-
|
|
269
|
+
if (b.ok) {
|
|
270
|
+
liveData = { balance: b.balance, cash: b.cash, voucher: b.voucher };
|
|
271
|
+
persistLive(a, {
|
|
272
|
+
mimoBalance: { balance: b.balance, cash: b.cash, voucher: b.voucher, at: new Date().toISOString() },
|
|
273
|
+
quotaSnapshot: { used: 0, total: b.balance, isCreditBalance: true, at: new Date().toISOString() },
|
|
274
|
+
});
|
|
275
|
+
} else liveData = liveError(b);
|
|
261
276
|
probeMark(a, b.ok, b.error);
|
|
262
277
|
} catch (err) {
|
|
263
278
|
liveData = { error: err.message };
|
|
@@ -293,8 +308,13 @@ export async function runQuotaPush({ endpoint, key, accounts, fetchImpl, onlySou
|
|
|
293
308
|
} else {
|
|
294
309
|
try {
|
|
295
310
|
const b = await fetchRiverBalance(a, { fetchImpl });
|
|
296
|
-
if (b.ok)
|
|
297
|
-
|
|
311
|
+
if (b.ok) {
|
|
312
|
+
liveData = { balance: b.balance };
|
|
313
|
+
persistLive(a, {
|
|
314
|
+
riverBalance: { balanceUsd: b.balance, at: new Date().toISOString() },
|
|
315
|
+
quotaSnapshot: { used: 0, total: b.balance, isCreditBalance: true, at: new Date().toISOString() },
|
|
316
|
+
});
|
|
317
|
+
} else liveData = liveError(b);
|
|
298
318
|
probeMark(a, b.ok, b.error);
|
|
299
319
|
} catch (err) {
|
|
300
320
|
liveData = { error: err.message };
|
|
@@ -334,6 +354,10 @@ export async function runQuotaPush({ endpoint, key, accounts, fetchImpl, onlySou
|
|
|
334
354
|
const ledger = await fetchMagicubeLedger(a.token, b.balance, { fetchImpl }).catch(() => null);
|
|
335
355
|
liveData = { balance: b.balance, ledger };
|
|
336
356
|
probeMark(a, true);
|
|
357
|
+
persistLive(a, {
|
|
358
|
+
msBalance: { amount: b.balance, today: ledger?.today ?? null, long: ledger?.long ?? null, at: new Date().toISOString() },
|
|
359
|
+
quotaSnapshot: { used: 0, total: b.balance, isCreditBalance: true, at: new Date().toISOString() },
|
|
360
|
+
});
|
|
337
361
|
} else {
|
|
338
362
|
liveData = liveError(b);
|
|
339
363
|
probeMark(a, false, b.error);
|
|
@@ -360,7 +384,7 @@ export async function runQuotaPush({ endpoint, key, accounts, fetchImpl, onlySou
|
|
|
360
384
|
// 储值卡 + 逐包明细(消耗顺序官方规则:购买包未动时先烧赠送包),会话失效降级 message 状态卡。
|
|
361
385
|
const lcAccounts = (accounts || []).filter((a) => (a.provider === 'longcat' || a.meta?.cookieSiteId === 'longcat') && a.token);
|
|
362
386
|
if (lcAccounts.length) {
|
|
363
|
-
const { fetchLongcatPacks, buildLongcatQuotaPayload } = await import('./longcatClient.js');
|
|
387
|
+
const { fetchLongcatPacks, longcatSnapshot, buildLongcatQuotaPayload } = await import('./longcatClient.js');
|
|
364
388
|
for (const a of lcAccounts) {
|
|
365
389
|
// onlySource 先判:别路 source 不打 LongCat 上游
|
|
366
390
|
const { source } = buildLongcatQuotaPayload(a);
|
|
@@ -372,8 +396,14 @@ export async function runQuotaPush({ endpoint, key, accounts, fetchImpl, onlySou
|
|
|
372
396
|
} else {
|
|
373
397
|
try {
|
|
374
398
|
const b = await fetchLongcatPacks(a.token, { fetchImpl });
|
|
375
|
-
if (b.ok)
|
|
376
|
-
|
|
399
|
+
if (b.ok) {
|
|
400
|
+
liveData = { packs: b.packs };
|
|
401
|
+
const lcTotal = b.packs.reduce((s, p) => s + Math.max(0, Number(p.remaining) || 0), 0);
|
|
402
|
+
persistLive(a, {
|
|
403
|
+
longcatPacks: longcatSnapshot(b.packs),
|
|
404
|
+
quotaSnapshot: { used: 0, total: lcTotal, isCreditBalance: true, at: new Date().toISOString() },
|
|
405
|
+
});
|
|
406
|
+
} else liveData = liveError(b);
|
|
377
407
|
probeMark(a, b.ok, b.error);
|
|
378
408
|
} catch (err) {
|
|
379
409
|
liveData = { error: err.message };
|